@heroiclands/package-build 22.9.1 → 22.10.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 (100) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/CONTENT.md +33 -0
  3. package/bin/content-build.mjs +3 -1
  4. package/docs/api.md +10 -6
  5. package/docs/content-format.md +384 -200
  6. package/docs/project-setup.md +23 -0
  7. package/engine/actor-compiler.mjs +20 -7
  8. package/engine/address-fields.mjs +404 -0
  9. package/engine/address-values.mjs +80 -0
  10. package/engine/address.mjs +769 -0
  11. package/engine/art-fields.mjs +112 -97
  12. package/engine/art-slots.mjs +81 -0
  13. package/engine/base-compiler.mjs +59 -7
  14. package/engine/bundle-notes.mjs +5 -2
  15. package/engine/bundles.mjs +17 -10
  16. package/engine/content-address.mjs +23 -186
  17. package/engine/content-embeds.mjs +18 -4
  18. package/engine/content-format-check.mjs +50 -22
  19. package/engine/content-index.mjs +85 -43
  20. package/engine/content-links.mjs +105 -104
  21. package/engine/content-lint.mjs +11 -1
  22. package/engine/content-tables.mjs +9 -1
  23. package/engine/field-spec.mjs +20 -4
  24. package/engine/folder-notes.mjs +44 -15
  25. package/engine/foundry-entries.mjs +3 -0
  26. package/engine/frontmatter-lint.mjs +144 -43
  27. package/engine/frontmatter.mjs +55 -10
  28. package/engine/generate.mjs +7 -1
  29. package/engine/helpers.mjs +37 -9
  30. package/engine/holdings.mjs +41 -12
  31. package/engine/index-records.mjs +2 -0
  32. package/engine/infobox-render.mjs +2 -1
  33. package/engine/infobox.mjs +26 -6
  34. package/engine/item-compiler.mjs +5 -0
  35. package/engine/item-docs.mjs +7 -44
  36. package/engine/map-dot.mjs +2 -0
  37. package/engine/map-notes.mjs +7 -2
  38. package/engine/map-places.mjs +42 -10
  39. package/engine/metadata-index.mjs +60 -5
  40. package/engine/note-addresses.mjs +310 -0
  41. package/engine/note-vocabulary.mjs +145 -26
  42. package/engine/pdf-build.mjs +1 -1
  43. package/engine/pdf-toc.mjs +17 -2
  44. package/engine/place-relations.mjs +160 -50
  45. package/engine/populations.mjs +17 -9
  46. package/engine/reference-targets.mjs +54 -0
  47. package/engine/scenes.mjs +5 -2
  48. package/engine/shortcode-references.mjs +68 -0
  49. package/engine/site-build.mjs +39 -15
  50. package/engine/site-index.mjs +57 -75
  51. package/engine/sql-tables.mjs +43 -9
  52. package/engine/system-block.mjs +21 -0
  53. package/engine/system-vocabulary.mjs +46 -0
  54. package/engine/web-wikilinks.mjs +11 -9
  55. package/engine/wikilink-syntax.mjs +21 -0
  56. package/engine/wikilinks.mjs +92 -306
  57. package/githooks/attribution.mjs +82 -0
  58. package/githooks/commit-message.mjs +26 -0
  59. package/githooks/commit-msg +3 -35
  60. package/hm3/actors.mjs +8 -24
  61. package/hm3/item-fields.mjs +15 -5
  62. package/package.json +1 -1
  63. package/sohl/infobox.mjs +80 -17
  64. package/sohl/item-fields.mjs +114 -41
  65. package/types/engine/address-fields.d.mts +90 -0
  66. package/types/engine/address-values.d.mts +34 -0
  67. package/types/engine/address.d.mts +452 -0
  68. package/types/engine/art-fields.d.mts +59 -53
  69. package/types/engine/art-slots.d.mts +40 -0
  70. package/types/engine/base-compiler.d.mts +24 -2
  71. package/types/engine/bundle-notes.d.mts +2 -2
  72. package/types/engine/content-address.d.mts +3 -139
  73. package/types/engine/content-embeds.d.mts +1 -1
  74. package/types/engine/content-format-check.d.mts +25 -0
  75. package/types/engine/content-index.d.mts +2 -2
  76. package/types/engine/content-links.d.mts +6 -5
  77. package/types/engine/field-spec.d.mts +44 -9
  78. package/types/engine/folder-notes.d.mts +11 -10
  79. package/types/engine/frontmatter-lint.d.mts +7 -97
  80. package/types/engine/frontmatter.d.mts +16 -1
  81. package/types/engine/helpers.d.mts +2 -2
  82. package/types/engine/infobox.d.mts +6 -0
  83. package/types/engine/item-docs.d.mts +1 -35
  84. package/types/engine/map-notes.d.mts +2 -2
  85. package/types/engine/note-addresses.d.mts +41 -0
  86. package/types/engine/note-vocabulary.d.mts +20 -3
  87. package/types/engine/place-relations.d.mts +12 -1
  88. package/types/engine/populations.d.mts +17 -2
  89. package/types/engine/reference-targets.d.mts +9 -0
  90. package/types/engine/shortcode-references.d.mts +14 -0
  91. package/types/engine/site-build.d.mts +3 -3
  92. package/types/engine/site-index.d.mts +9 -30
  93. package/types/engine/sql-tables.d.mts +2 -2
  94. package/types/engine/system-block.d.mts +17 -0
  95. package/types/engine/system-vocabulary.d.mts +16 -0
  96. package/types/engine/wikilink-syntax.d.mts +7 -0
  97. package/types/engine/wikilinks.d.mts +6 -109
  98. package/types/hm3/item-fields.d.mts +5 -0
  99. package/types/sohl/infobox.d.mts +4 -5
  100. package/types/sohl/item-fields.d.mts +8 -0
package/hm3/actors.mjs CHANGED
@@ -26,13 +26,9 @@
26
26
  * the note says which, by writing `hm3.type`. An absent one is an error naming
27
27
  * the note, never a default.
28
28
  *
29
- * **What is emitted, and what is deliberately not.** Four rows of the content
30
- * format's `being` mapping table give HM3 a destination — `data.portrait` →
31
- * `system.bioImage`, `data.species`, `data.gender`, `data.occupation`, and
32
- * `data.templatePriority` → `flags.hm3.templatePriority` — and every one of
33
- * them is *declared as that source* rather than as the bare key the corpus
34
- * writes, so the specification's mapping is executable rather than
35
- * aspirational. Plus the two anchored prose sections: `{#appearance}` is HM3's
29
+ * Shared gender, occupation and template priority map to HM3's native fields.
30
+ * Native species text is independent of the lore Address in `data.species`.
31
+ * The anchored prose section `{#appearance}` supplies HM3's
36
32
  * `description` and `{#dossier}` its
37
33
  * `biography`. Everything else an HM3 actor carries — the thirteen abilities,
38
34
  * the sunsign, `move`, `fatigue`, `shockIndex`, a creature's `loadRating` — has
@@ -70,31 +66,19 @@ import {
70
66
  import { blockField, blockProperty, mergeSystemData } from "../engine/system-block.mjs";
71
67
 
72
68
  /**
73
- * The shared `data:` facts every HM3 actor takes, whatever its subtype.
69
+ * Native HM3 facts shared by its actor subtypes.
74
70
  *
75
- * Declared rather than read by hand, so a value resolves by the same order
76
- * every other declared field does — `hm3.system.<to>` first, then the in-block
77
- * position, then the shared source, then the default — and so the
78
- * author-facing reference can be generated from the same statement the compiler
79
- * obeys.
80
- *
81
- * **Both positions, named separately.** The specification maps
82
- * `data.species` onto `system.species`, and every HM3 note in the corpus writes
83
- * `hm3.species`. Until the sweep those are two live positions for one
84
- * field, so the declaration names both: `name` is the shared source, `legacyKey`
85
- * the key the block still carries, and the block wins while it is there. Naming
86
- * only one of them is what made the row unreadable — a plain `species` could
87
- * not see `data.species`, and a dotted `data.species` could not see the 2,512
88
- * notes that carry `hm3.species`, each yielding `""` in silence.
71
+ * Species is native text. The note's lore Address in `data.species` is
72
+ * independent and supplies no native value.
89
73
  *
90
74
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
91
75
  */
92
76
  const ACTOR_FIELDS = Object.freeze([
93
77
  {
94
- name: "data.species",
95
- legacyKey: "species",
78
+ name: "species",
96
79
  to: "species",
97
80
  ...STRING,
81
+ topLevelMeans: "The shared species is a lore Address, independent of native species text.",
98
82
  default: "",
99
83
  describe: "The kind of creature this is.",
100
84
  },
@@ -76,7 +76,8 @@ import { AS_AUTHORED, NUMBER } from "../engine/field-spec.mjs";
76
76
  */
77
77
  const GEAR_COMMON = Object.freeze([
78
78
  {
79
- name: "weight",
79
+ name: "data.weight",
80
+ legacyKey: "weight",
80
81
  to: "weight",
81
82
  ...AS_AUTHORED,
82
83
  kind: "number",
@@ -84,7 +85,8 @@ const GEAR_COMMON = Object.freeze([
84
85
  describe: "Weight of one, in pounds.",
85
86
  },
86
87
  {
87
- name: "value",
88
+ name: "data.value",
89
+ legacyKey: "value",
88
90
  to: "value",
89
91
  ...AS_AUTHORED,
90
92
  kind: "number",
@@ -104,7 +106,8 @@ const GEAR_COMMON = Object.freeze([
104
106
  * @type {import("../engine/field-spec.mjs").FieldSpec}
105
107
  */
106
108
  const QUANTITY = Object.freeze({
107
- name: "quantity",
109
+ name: "data.quantity",
110
+ legacyKey: "quantity",
108
111
  to: "quantity",
109
112
  ...NUMBER,
110
113
  default: 1,
@@ -124,6 +127,11 @@ const QUANTITY = Object.freeze({
124
127
  * a registry is addressed by what a note calls itself. What the document is
125
128
  * called is the map's answer, and only the map's.
126
129
  *
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.
134
+ *
127
135
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
128
136
  */
129
137
  export const HM3_ITEM_FIELDS = Object.freeze({
@@ -139,7 +147,8 @@ export const HM3_ITEM_FIELDS = Object.freeze({
139
147
  containergear: Object.freeze([
140
148
  ...GEAR_COMMON,
141
149
  {
142
- name: "capacity",
150
+ name: "data.capacity",
151
+ legacyKey: "capacity",
143
152
  to: "capacity.max",
144
153
  ...NUMBER,
145
154
  default: 0,
@@ -161,7 +170,8 @@ export const HM3_ITEM_FIELDS = Object.freeze({
161
170
  // through, and only the mastery level is declared here.
162
171
  skill: Object.freeze([
163
172
  {
164
- name: "masteryLevel",
173
+ name: "data.masteryLevel",
174
+ legacyKey: "masteryLevel",
165
175
  to: "masteryLevel",
166
176
  ...NUMBER,
167
177
  default: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.9.1",
3
+ "version": "22.10.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",
package/sohl/infobox.mjs CHANGED
@@ -51,6 +51,8 @@
51
51
  * @module
52
52
  */
53
53
 
54
+ import { parseAddress, isAddressTuple, completeAddress } from "../engine/address.mjs";
55
+ import { contentPackage } from "../engine/content-package.mjs";
54
56
  import {
55
57
  DURATION_LABELS,
56
58
  GEAR_UNITS,
@@ -63,6 +65,8 @@ import {
63
65
  import { currentType } from "../engine/ids.mjs";
64
66
  import { subTypes } from "../engine/note-vocabulary.mjs";
65
67
  import { systemBlock, systemData } from "../engine/system-block.mjs";
68
+ import { readCanonicalKey } from "../engine/content-address.mjs";
69
+ import { readQualifier } from "../engine/address.mjs";
66
70
  import { NOTE_SCHEMAS } from "./note-schemas.mjs";
67
71
  import { GEAR_TYPE_TO_KEY } from "./being-info.mjs";
68
72
 
@@ -174,15 +178,45 @@ function isMapping(value) {
174
178
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
175
179
  }
176
180
 
181
+ /**
182
+ * Every content type `model:` may name — the note vocabulary this system
183
+ * declares, engine types included.
184
+ *
185
+ * @type {ReadonlySet<string>}
186
+ */
187
+ const MODEL_TYPES = new Set(Object.keys(NOTE_SCHEMAS));
188
+
189
+ /**
190
+ * The `(type, shortcode)` an entry's `model:` names, or `undefined`.
191
+ *
192
+ * `model` is an Address, read by the same grammar every wikilink is —
193
+ * `type-shortcode` within this package, `package-system-type-shortcode` to
194
+ * name a dependency's catalogue. This decode answers neither of the two
195
+ * questions {@link module:engine/address} asks about the package: a runtime
196
+ * lookup among one actor's embedded items resolves by `(type, shortcode)`
197
+ * alone, where packages do not exist, so a fully qualified `model` is read for
198
+ * its last two segments exactly as a bare one is.
199
+ *
200
+ * @param {unknown} model - The authored value.
201
+ * @returns {{type: string, shortcode: string}|undefined} What it names.
202
+ */
203
+ function modelAddress(model) {
204
+ const target = parseAddress(
205
+ model,
206
+ { package: contentPackage(), system: "sohl", types: MODEL_TYPES },
207
+ { declared: true, legacyShortcodeCase: true },
208
+ );
209
+ return target.reason ? undefined : target;
210
+ }
211
+
177
212
  /**
178
213
  * What one entry of `sohl.items` names.
179
214
  *
180
215
  * An entry addresses its item three ways and every tree uses all three: a
181
- * `model` address whose last two hyphen-separated segments are always
182
- * `<type>-<shortcode>`, explicit `type` / `shortcode` keys, or a locally
183
- * authored item stating its type with the shortcode inside its own `system`
184
- * block. One decode covers all of them, so nothing downstream has to know
185
- * which form a note happened to use.
216
+ * `model` address, explicit `type` / `shortcode` keys, or a locally authored
217
+ * item stating its type with the shortcode inside its own `system` block. One
218
+ * decode covers all of them, so nothing downstream has to know which form a
219
+ * note happened to use.
186
220
  *
187
221
  * @param {object} entry - One `sohl.items` entry.
188
222
  * @returns {{type: string, shortcode: string, name: string, system: object}|undefined}
@@ -190,19 +224,21 @@ function isMapping(value) {
190
224
  */
191
225
  export function decodeItem(entry) {
192
226
  if (!isMapping(entry)) return undefined;
227
+ const address = entry.model ? modelAddress(entry.model) : undefined;
193
228
  let type = typeof entry.type === "string" ? entry.type : "";
194
229
  let shortcode = typeof entry.shortcode === "string" ? entry.shortcode : "";
195
- if (!type && typeof entry.model === "string") {
196
- const segments = entry.model.split("-");
197
- if (segments.length >= 2) {
198
- type = segments[segments.length - 2];
199
- if (!shortcode) shortcode = segments[segments.length - 1];
230
+ if (!type) {
231
+ const named = address;
232
+ if (named) {
233
+ type = named.type;
234
+ if (!shortcode) shortcode = named.shortcode;
200
235
  }
201
236
  }
202
237
  const system = isMapping(entry.system) ? entry.system : {};
203
238
  if (!shortcode && typeof system.shortcode === "string") shortcode = system.shortcode;
204
239
  if (!type) return undefined;
205
240
  return {
241
+ ...(address ? { address } : {}),
206
242
  type: currentType(type),
207
243
  shortcode,
208
244
  name: typeof entry.name === "string" ? entry.name : "",
@@ -222,6 +258,32 @@ function itemsOf(fm, block) {
222
258
  return (Array.isArray(declared) ? declared : []).map(decodeItem).filter(Boolean);
223
259
  }
224
260
 
261
+ /**
262
+ * A model's documentation presentation, with its native name as plain text
263
+ * when no documentation is published. The model keeps its native identity.
264
+ * @param {object} item - Decoded native item.
265
+ * @param {Function} resolve - The medium's reference resolver.
266
+ * @param {string} block - The native system.
267
+ * @returns {object|undefined} Documentation links and display metadata.
268
+ */
269
+ function itemPresentation(item, resolve, block) {
270
+ if (!item.shortcode) return undefined;
271
+ if (!item.address)
272
+ return resolve?.(item.shortcode, { kind: "shortcode", type: item.type, system: block });
273
+ const documentation = completeAddress({ ...item.address, system: "none" });
274
+ const found = resolve?.(documentation, { type: documentation.type });
275
+ const native =
276
+ !found?.name || !found?.subType ? resolve?.(item.address, { type: item.type }) : undefined;
277
+ if (!found && !native) return undefined;
278
+ return {
279
+ name: found?.name ?? native?.name,
280
+ subType: found?.subType ?? native?.subType,
281
+ address: documentation,
282
+ ...(found?.url ? { url: found.url } : {}),
283
+ ...(found?.uuid ? { uuid: found.uuid } : {}),
284
+ };
285
+ }
286
+
225
287
  /**
226
288
  * One item as a value a renderer can draw: its name, and a link where the
227
289
  * index reached it.
@@ -229,10 +291,11 @@ function itemsOf(fm, block) {
229
291
  * @param {object} item - A decoded item.
230
292
  * @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
231
293
  * medium's resolver.
232
- * @returns {{text: string, url?: string, uuid?: string, address?: string}} The value.
294
+ * @param {string} block - The native system.
295
+ * @returns {{text: string, url?: string, uuid?: string, address?: import("../engine/address.mjs").AddressTuple}} The value.
233
296
  */
234
- function itemValue(item, resolve) {
235
- const found = item.shortcode ? resolve?.(item.shortcode, { type: item.type }) : undefined;
297
+ function itemValue(item, resolve, block) {
298
+ const found = itemPresentation(item, resolve, block);
236
299
  const value = { text: item.name || found?.name || humanizeValue(item.shortcode) };
237
300
  if (found?.url) value.url = found.url;
238
301
  if (found?.uuid) value.uuid = found.uuid;
@@ -277,9 +340,9 @@ export function beingSections(fm, { block, resolve }) {
277
340
  if (item.type !== "skill") continue;
278
341
  const mastery = item.system.masteryLevelBase;
279
342
  if (!hasValue(mastery)) continue;
280
- const found = item.shortcode ? resolve?.(item.shortcode, { type: "skill" }) : undefined;
343
+ const found = itemPresentation(item, resolve, block);
281
344
  const family = item.system.subType ?? found?.subType;
282
- const value = itemValue(item, resolve);
345
+ const value = itemValue(item, resolve, block);
283
346
  const entry = { ...value, text: `${value.text} ${mastery}` };
284
347
  if (family && families.has(family)) families.get(family).push(entry);
285
348
  else unplaced.push(entry);
@@ -298,7 +361,7 @@ export function beingSections(fm, { block, resolve }) {
298
361
  const mystical = [];
299
362
  for (const item of items) {
300
363
  if (item.type !== "mysticalability") continue;
301
- mystical.push(itemValue(item, resolve));
364
+ mystical.push(itemValue(item, resolve, block));
302
365
  }
303
366
  if (mystical.length) {
304
367
  sections.push({
@@ -313,7 +376,7 @@ export function beingSections(fm, { block, resolve }) {
313
376
  for (const item of items) {
314
377
  const key = GEAR_TYPE_TO_KEY[item.type];
315
378
  if (!key) continue;
316
- gear.get(key).push(itemValue(item, resolve));
379
+ gear.get(key).push(itemValue(item, resolve, block));
317
380
  }
318
381
  const gearGroups = [];
319
382
  for (const [key, entries] of gear) {