@heroiclands/package-build 22.9.2 → 22.11.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 (97) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/CONTENT.md +54 -0
  3. package/bin/content-build.mjs +3 -1
  4. package/docs/api.md +11 -6
  5. package/docs/commands.md +1 -1
  6. package/docs/content-format.md +411 -201
  7. package/engine/actor-compiler.mjs +20 -7
  8. package/engine/address-fields.mjs +426 -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 +61 -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 +21 -4
  24. package/engine/folder-notes.mjs +44 -15
  25. package/engine/foundry-entries.mjs +3 -0
  26. package/engine/frontmatter-lint.mjs +150 -47
  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 +325 -0
  41. package/engine/note-vocabulary.mjs +140 -28
  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/hm3/actors.mjs +8 -24
  58. package/hm3/item-fields.mjs +15 -5
  59. package/package.json +1 -1
  60. package/sohl/infobox.mjs +111 -17
  61. package/sohl/item-fields.mjs +131 -41
  62. package/types/engine/address-fields.d.mts +95 -0
  63. package/types/engine/address-values.d.mts +34 -0
  64. package/types/engine/address.d.mts +452 -0
  65. package/types/engine/art-fields.d.mts +59 -53
  66. package/types/engine/art-slots.d.mts +40 -0
  67. package/types/engine/base-compiler.d.mts +24 -2
  68. package/types/engine/bundle-notes.d.mts +2 -2
  69. package/types/engine/content-address.d.mts +3 -139
  70. package/types/engine/content-embeds.d.mts +1 -1
  71. package/types/engine/content-format-check.d.mts +25 -0
  72. package/types/engine/content-index.d.mts +2 -2
  73. package/types/engine/content-links.d.mts +6 -5
  74. package/types/engine/field-spec.d.mts +46 -9
  75. package/types/engine/folder-notes.d.mts +11 -10
  76. package/types/engine/frontmatter-lint.d.mts +7 -97
  77. package/types/engine/frontmatter.d.mts +16 -1
  78. package/types/engine/helpers.d.mts +2 -2
  79. package/types/engine/infobox.d.mts +6 -0
  80. package/types/engine/item-docs.d.mts +1 -35
  81. package/types/engine/map-notes.d.mts +2 -2
  82. package/types/engine/note-addresses.d.mts +41 -0
  83. package/types/engine/note-vocabulary.d.mts +20 -3
  84. package/types/engine/place-relations.d.mts +12 -1
  85. package/types/engine/populations.d.mts +17 -2
  86. package/types/engine/reference-targets.d.mts +9 -0
  87. package/types/engine/shortcode-references.d.mts +14 -0
  88. package/types/engine/site-build.d.mts +3 -3
  89. package/types/engine/site-index.d.mts +9 -30
  90. package/types/engine/sql-tables.d.mts +2 -2
  91. package/types/engine/system-block.d.mts +17 -0
  92. package/types/engine/system-vocabulary.d.mts +16 -0
  93. package/types/engine/wikilink-syntax.d.mts +7 -0
  94. package/types/engine/wikilinks.d.mts +6 -109
  95. package/types/hm3/item-fields.d.mts +5 -0
  96. package/types/sohl/infobox.d.mts +6 -5
  97. package/types/sohl/item-fields.d.mts +8 -0
@@ -82,15 +82,30 @@
82
82
  * Plain ESM with no Foundry and no filesystem access, so it is unit-testable.
83
83
  */
84
84
 
85
+ import { resolveShortcodeReference } from "./shortcode-references.mjs";
86
+
87
+ import { readWikilink } from "./wikilink-syntax.mjs";
85
88
  import crypto from "crypto";
86
89
 
87
90
  import { compendiumUuid, ITEM_PACK, packForType, pageUuid, PACK_BY_TYPE } from "./ids.mjs";
88
91
  import { readCanonicalKey } from "./content-address.mjs";
92
+ // The address grammar. A wikilink *contains* an Address, so what counts as one
93
+ // is stated there and read here — the anchor and the label are this module's,
94
+ // and the tuple inside them is not.
95
+ import {
96
+ ITEM_DOC_PREFIX,
97
+ readQualifier,
98
+ resolveItemDocType,
99
+ parseAddress,
100
+ renderAddress,
101
+ isAddressTuple,
102
+ completeAddress,
103
+ } from "./address.mjs";
89
104
  import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
90
- import { isSystemSegment, NO_SYSTEM } from "./systems.mjs";
105
+ import { NO_SYSTEM } from "./systems.mjs";
91
106
  import { systemOf } from "./document-subtypes.mjs";
92
107
  import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
93
- import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
108
+ import { itemDocEntryId } from "./item-docs.mjs";
94
109
  import { replaceOutsideCode } from "./code-fences.mjs";
95
110
  // The syntax lives in `./wikilink-syntax.mjs`, so the web resolver and this
96
111
  // one cannot disagree about what counts as a link.
@@ -104,227 +119,14 @@ import {
104
119
  export { ITEM_PACK, PACK_BY_TYPE, packForType };
105
120
 
106
121
  /**
107
- * The qualifier prefix that addresses an item's **documentation** rather than
108
- * the item: `docskill/wpnc` is the JournalEntry that `skill/wpnc`'s prose
109
- * compiled into. See {@link resolveItemDocType}.
110
- */
111
- const ITEM_DOC_PREFIX = "doc";
112
-
113
- const norm = (s) => String(s).toLowerCase().trim();
114
-
115
- /**
116
- * Reads a qualifier as the **virtual `doc<type>`** form, or reports that it is
117
- * not one.
118
- *
119
- * A document and its documentation are two documents in two packs, so they
120
- * need two addresses. `skill/wpnc` is the item; `docskill/wpnc` is the
121
- * JournalEntry its prose compiled into, and `docmacro/autoattack` is the same
122
- * arrangement for a macro.
123
- *
124
- * The virtual form exists for a type that carries separate documentation
125
- * ({@link sohl.utils.packs.docEntryTypes} — the set the journals compiler and
126
- * the link manifest read too), **or** for one that routes to the items pack.
127
- * The second clause is the older rule and stays: types that compile into items
128
- * are the open, unenumerated set, and a foreign package may publish an
129
- * item type this build has never heard of. Dropping it would silently unlink
130
- * every `doc<type>` address into such a package.
131
- *
132
- * A **real** type of the same name always wins: the virtual reading is only
133
- * consulted for a qualifier no authored note claims.
134
- *
135
- * @param {string} qualifier - The already-normalised text before the `/`.
136
- * @param {Set<string>} types - Every type the content tree contains.
137
- * @returns {string|null} The underlying document type, or `null` when the
138
- * qualifier is not a virtual one.
139
- */
140
- export function resolveItemDocType(qualifier, types) {
141
- if (types.has(qualifier)) return null; // a real type owns its own name
142
- if (!qualifier.startsWith(ITEM_DOC_PREFIX)) return null;
143
- const base = qualifier.slice(ITEM_DOC_PREFIX.length);
144
- if (!base || !types.has(base)) return null;
145
- if (hasDocEntry(base)) return base;
146
- return packForType(base).docType === ITEM_PACK.docType ? base : null;
147
- }
148
-
149
- /**
150
- * Read a link target as a **qualified** `type-shortcode` reference, or report
151
- * that it does not parse as one.
152
- *
153
- * Two separators are accepted, and they are **not** interchangeable in how
154
- * confidently they mark a target as qualified:
155
- *
156
- * - **`type-shortcode`** and its qualified forms — the canonical spelling.
157
- * Obsidian reads `/` inside a wikilink as a *path* and resolves it
158
- * against the vault's folders, so a slash-qualified link is a broken link in
159
- * the editor where the content is now authored.
160
- * - **`type/shortcode`** — the legacy form, still resolved so that a link
161
- * written before the vault migrated does not silently die. A slash is
162
- * *unconditionally* a qualifier: nothing else uses one, so an unknown type
163
- * before it is reported rather than guessed at. The split is at the **last**
164
- * slash, as it always was.
165
- *
166
- * **The grammar is strict, and omission runs left to right**:
167
- *
168
- * ```text
169
- * [[[[<package>-]<system>-]<type>-]<shortcode>]
170
- * ```
171
- *
172
- * So the written forms are exactly the suffixes of the canonical address —
173
- * `type-shortcode`, `system-type-shortcode`, `package-system-type-shortcode` —
174
- * and **`package-type-shortcode` is not one of them**. A link into another
175
- * package must therefore be fully qualified, which is the price of the segment
176
- * being positional rather than tagged.
177
- *
178
- * **Parsing is plain positional counting**, the same rule
179
- * {@link readCanonicalKey} follows, and it is sound for the same reason: every
180
- * segment matches `ADDRESS_SEGMENT_PATTERN` (enforced on shortcodes by
181
- * `content-lint.mjs`), so the hyphen is purely a
182
- * separator and the count alone determines every field. Verified across the
183
- * four content trees: 138,204 authored shortcodes, none carrying a separator.
184
- *
185
- * That replaced a first-hyphen split which let a shortcode contain a hyphen
186
- * (`trauma-self-pro` → `trauma` + `self-pro`). The tolerance predates the
187
- * charset rule and
188
- * outlived it; no tree has used it, and keeping it would make a three-segment
189
- * target ambiguous between a system and a hyphenated shortcode.
190
- *
191
- * **`sohl` is both a package and a system**, and positional counting is what
192
- * makes that harmless: three segments is `<system>-<type>-<shortcode>` whatever
193
- * the first segment could also have named, and four is the full form. Nothing
194
- * has to guess which sense was meant.
195
- *
196
- * **A partial address states what it states, and the rest is not invented.** An
197
- * omitted **system** is a *wildcard* — most links target items, which belong to
198
- * a system — so the caller matches on the segments supplied and requires
199
- * exactly one hit. An omitted **package** is instead *defaulted* to the citing
200
- * note's own, so an unqualified link resolves locally and only locally.
201
- *
202
- * @param {string} target - The link target, anchor already removed.
203
- * @param {Set<string>} types - Every type the content tree contains.
204
- * @param {Set<string>} [packages] - Every package an address may name. Omitted
205
- * by callers that resolve within one package, where the form cannot occur.
206
- * @param {Set<string>} [noIndexPackages] - Packages declared `contentIndex:
207
- * false` — a Foundry dependency only, with no fetched index. A fully
208
- * qualified target naming one is refused with `no-content-index` before its
209
- * type is even considered, since there is no index to resolve it against.
210
- * @returns {{type: string, shortcode: string, itemDoc: boolean,
211
- * package?: string, system?: string, reason?: undefined}
212
- * | {reason: "unknown-type"|"no-content-index", package?: string} | null}
213
- * The resolved qualifier; a `reason` when the target is definitely qualified
214
- * but names no known type or no fetched index; or `null` when it is not an
215
- * address at all.
216
- */
217
- export function readQualifier(target, types, packages, noIndexPackages) {
218
- // **Package, system and type are lowercase; the shortcode is not.** A
219
- // shortcode is case-sensitive and routinely mixed — `Clb`, `LtShoe`,
220
- // `HsTunic` — so it is written as the note declares it. The three segments
221
- // in front of it are closed vocabularies with one spelling each, and
222
- // accepting `Skill` beside `skill` would bless two ways of writing one
223
- // address. Reported rather than folded, so the corpus has one form.
224
- //
225
- // Tested only once the target *parses*: a note name is full of capitals
226
- // (`[[Shock State]]`), and calling that a badly-cased address rather than
227
- // not an address would name the wrong mistake. Neither tree carries a
228
- // violation — 10,538 authored targets — so this pins a rule already kept.
229
- const read = readQualifierCased(target, types, packages, noIndexPackages);
230
- if (read && !read.reason && qualifyingSegments(target).some((s) => /[A-Z]/.test(s))) {
231
- return { reason: "not-lowercase" };
232
- }
233
- return read;
234
- }
235
-
236
- /**
237
- * The segments of a target that must be lowercase — everything but the
238
- * shortcode, which is case-sensitive and keeps whatever the note declares.
239
- *
240
- * @param {string} target - The link target, anchor already removed.
241
- * @returns {string[]} The package / system / type segments, as written.
242
- */
243
- function qualifyingSegments(target) {
244
- const slash = target.lastIndexOf("/");
245
- // The legacy `type/shortcode` form states only a type.
246
- if (slash > 0) return [target.slice(0, slash)];
247
- const parts = target.split("-");
248
- return parts.slice(0, -1);
249
- }
250
-
251
- /**
252
- * {@link readQualifier} without the lowercase rule — the grammar alone.
122
+ * The address grammar, under the names the link resolvers hold it by.
253
123
  *
254
- * @param {string} target
255
- * @param {Set<string>} types
256
- * @param {Set<string>} [packages]
257
- * @param {Set<string>} [noIndexPackages]
258
- * @returns {object|null}
124
+ * {@link readQualifier} reads a link target as the partial Address the matchers
125
+ * consume, and {@link resolveItemDocType} is the `doc<type>` reading it applies.
259
126
  */
260
- function readQualifierCased(target, types, packages, noIndexPackages) {
261
- // The slash form is legacy and states neither package nor system, so it is
262
- // read first and separately. A slash is unconditionally a qualifier —
263
- // nothing else uses one — which is why an unknown type before it is
264
- // *reported* rather than read as prose.
265
- const slash = target.lastIndexOf("/");
266
- if (slash > 0) {
267
- const read = readTypeAndCode(target.slice(0, slash), target.slice(slash + 1), types);
268
- return read ?? { reason: "unknown-type" };
269
- }
270
-
271
- const parts = target.split("-");
272
- switch (parts.length) {
273
- // `<type>-<shortcode>`
274
- case 2:
275
- return readTypeAndCode(parts[0], parts[1], types);
276
-
277
- // `<system>-<type>-<shortcode>` — the package defaults to local.
278
- case 3: {
279
- const system = norm(parts[0]);
280
- if (!isSystemSegment(system)) return null;
281
- const read = readTypeAndCode(parts[1], parts[2], types);
282
- return read && { ...read, system };
283
- }
284
-
285
- // `<package>-<system>-<type>-<shortcode>` — the only form that names
286
- // another package, and the reason a cross-package link must be fully
287
- // qualified.
288
- case 4: {
289
- const pkg = norm(parts[0]);
290
- // Checked before the type: a package with no fetched index has no
291
- // vocabulary to resolve the rest of the target against, and the
292
- // fix is the config declaration, not the shortcode.
293
- if (noIndexPackages?.has(pkg)) return { reason: "no-content-index", package: pkg };
294
- if (!packages?.has(pkg)) return null;
295
- const system = norm(parts[1]);
296
- if (!isSystemSegment(system)) return null;
297
- const read = readTypeAndCode(parts[2], parts[3], types);
298
- return read && { ...read, system, package: pkg };
299
- }
300
-
301
- // One segment is a bare name, which is not an address; five or
302
- // more is not a hyphenated shortcode but a name that happens to carry
303
- // separators, since no segment may contain one.
304
- default:
305
- return null;
306
- }
307
- }
127
+ export { readQualifier, resolveItemDocType };
308
128
 
309
- /**
310
- * Resolve a qualifier/shortcode pair, honouring the virtual `doc<type>` form.
311
- *
312
- * @param {string} rawType
313
- * @param {string} rawCode
314
- * @param {Set<string>} types
315
- * @returns {{type: string, shortcode: string, itemDoc: boolean} | null}
316
- * `null` when the qualifier names no known type, or the shortcode is empty.
317
- */
318
- function readTypeAndCode(rawType, rawCode, types) {
319
- const shortcode = norm(rawCode);
320
- if (!shortcode) return null;
321
-
322
- let type = norm(rawType);
323
- const base = resolveItemDocType(type, types);
324
- if (base) return { type: base, shortcode, itemDoc: true };
325
- if (!types.has(type)) return null;
326
- return { type, shortcode, itemDoc: false };
327
- }
129
+ const norm = (s) => String(s).toLowerCase().trim();
328
130
 
329
131
  /**
330
132
  * The deterministic JournalEntryPage id for one anchor: SHA-256 of
@@ -384,7 +186,7 @@ export function buildWikilinkIndex(
384
186
  packageId,
385
187
  foreign,
386
188
  contentPackage,
387
- { assets, noIndexPackages } = {},
189
+ { assets, noIndexPackages, referenceTargets, foreignReferences } = {},
388
190
  ) {
389
191
  if (!packageId) {
390
192
  throw new Error(
@@ -479,6 +281,8 @@ export function buildWikilinkIndex(
479
281
 
480
282
  return {
481
283
  byShortcode,
284
+ referenceTargets,
285
+ foreignReferences,
482
286
  types,
483
287
  uuidByDoc,
484
288
  packageId,
@@ -654,8 +458,14 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
654
458
  // one capture group. It is what makes two identical unresolved links on
655
459
  // one note tellable apart, and a position reportable at all.
656
460
  const out = replaceOutsideCode(markdown, WIKILINK, (all, rawInner, offset) => {
657
- const parsed = parseWikilink(rawInner);
658
- const target = parsed.target;
461
+ const parsed = readWikilink(rawInner, {
462
+ package: index.contentPackage,
463
+ system: "none",
464
+ types: index.types,
465
+ packages: index.packages,
466
+ noIndexPackages: index.noIndexPackages,
467
+ });
468
+ const target = parsed.rawTarget;
659
469
  const slug = parsed.anchor || null;
660
470
 
661
471
  // **Every link carries a label**. Without one there is nothing
@@ -692,12 +502,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
692
502
  if (target === "" && slug) {
693
503
  doc = { type, id, pack, docPack };
694
504
  } else {
695
- const qualified = readQualifier(
696
- target,
697
- index.types,
698
- index.packages,
699
- index.noIndexPackages,
700
- );
505
+ const qualified = parsed.problem ?? parsed.target;
701
506
  qualifiedRead = qualified;
702
507
  // A target that does not parse as an address is a defect: there is
703
508
  // no second namespace left to fall through to.
@@ -723,11 +528,10 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
723
528
  // their own documents are core ones already at `none`, so
724
529
  // `macro-autoattack` names the Macro and `docmacro-autoattack` its
725
530
  // journal — two live addresses the redirect would collapse.
726
- itemDoc =
727
- qualified.itemDoc ||
728
- ((qualified.system ?? NO_SYSTEM) === NO_SYSTEM &&
729
- systemOf(qualified.type, KNOWN_DOCUMENT_SUBTYPE_MAPS) !== NO_SYSTEM);
730
- doc = index.byShortcode.get(`${qualified.type}/${qualified.shortcode}`);
531
+ const baseType = resolveItemDocType(qualified.type, index.types);
532
+ itemDoc = Boolean(baseType);
533
+ if (qualified.package === index.contentPackage)
534
+ doc = index.byShortcode.get(`${baseType ?? qualified.type}/${qualified.shortcode}`);
731
535
  }
732
536
  if (!doc) {
733
537
  // Nothing local answers. A foreign package may publish this
@@ -843,42 +647,6 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
843
647
  return { markdown: out, unresolved };
844
648
  }
845
649
 
846
- /**
847
- * The short `type/shortcode` view of an index's foreign entries.
848
- *
849
- * Derived once per index and memoised: a compile resolves references on every
850
- * one of thousands of notes, and rebuilding the view per note would rescan a
851
- * dependency's whole published index each time. Keyed on the index object, so
852
- * it is discarded with it.
853
- *
854
- * An address two foreign packages both claim is left out rather than resolved
855
- * to whichever loaded first — the same rule the wikilink resolver follows.
856
- *
857
- * @type {WeakMap<object, Map<string, object>>}
858
- */
859
- const FOREIGN_BY_SHORTCODE = new WeakMap();
860
-
861
- /** The memoised short view of `index.foreign`. */
862
- function foreignByShortcode(index) {
863
- const cached = FOREIGN_BY_SHORTCODE.get(index);
864
- if (cached) return cached;
865
- const short = new Map();
866
- const ambiguous = new Set();
867
- for (const [key, value] of index?.foreign ?? []) {
868
- const parts = readCanonicalKey(key);
869
- if (!parts) continue;
870
- const shortKey = `${norm(parts.type)}/${norm(parts.shortcode)}`;
871
- if (short.has(shortKey) && short.get(shortKey).package !== value.package) {
872
- ambiguous.add(shortKey);
873
- } else {
874
- short.set(shortKey, value);
875
- }
876
- }
877
- for (const key of ambiguous) short.delete(key);
878
- FOREIGN_BY_SHORTCODE.set(index, short);
879
- return short;
880
- }
881
-
882
650
  /**
883
651
  * Resolve one reference against a compile's address index.
884
652
  *
@@ -892,46 +660,64 @@ function foreignByShortcode(index) {
892
660
  * @param {object} index - From {@link buildWikilinkIndex}.
893
661
  * @param {unknown} ref - The reference, as authored.
894
662
  * @param {object} [hint] - `{type}`, where the caller knows what it expects.
895
- * @returns {{name?: string, uuid?: string, address?: string, subType?: string}|undefined}
663
+ * @returns {{name?: string, uuid?: string, address?: import("./address.mjs").AddressTuple, subType?: string}|undefined}
896
664
  * The target, or `undefined` where nothing answers.
897
665
  */
898
666
  export function resolveReference(index, ref, hint) {
899
- if (typeof ref !== "string" || !ref) return undefined;
900
- const wanted = norm(ref);
901
- const keys = [];
902
- if (hint?.type) keys.push(`${norm(hint.type)}/${wanted}`);
903
- if (wanted.includes("-")) {
904
- const segments = wanted.split("-");
905
- if (segments.length >= 2) {
906
- keys.push(`${segments[segments.length - 2]}/${segments[segments.length - 1]}`);
907
- }
908
- }
909
- if (!hint?.type) {
910
- for (const type of [...(index?.types ?? [])].sort()) keys.push(`${type}/${wanted}`);
911
- }
912
-
913
- const foreign = foreignByShortcode(index);
914
- for (const key of keys) {
915
- const local = index?.byShortcode?.get(key);
916
- if (local) {
917
- return {
918
- ...(local.name ? { name: local.name } : {}),
919
- ...(local.subType ? { subType: local.subType } : {}),
920
- ...(index.uuidByDoc?.get(local)?.uuid ?
921
- { uuid: index.uuidByDoc.get(local).uuid }
922
- : {}),
923
- address: key.replace("/", "-"),
924
- };
925
- }
926
- const away = foreign.get(key);
927
- if (away) {
928
- return {
929
- ...(away.name ? { name: away.name } : {}),
930
- ...(away.uuid ? { uuid: away.uuid } : {}),
931
- ...(away.subType ? { subType: away.subType } : {}),
932
- address: key.replace("/", "-"),
933
- };
667
+ if (hint?.kind === "shortcode")
668
+ return resolveShortcodeReference(
669
+ [index.referenceTargets, index.foreignReferences ?? index.foreign],
670
+ ref,
671
+ hint,
672
+ );
673
+ const tuple = parseAddress(ref, {
674
+ package: index.contentPackage,
675
+ system: "none",
676
+ types: index.types,
677
+ packages: index.packages,
678
+ noIndexPackages: index.noIndexPackages,
679
+ type: hint?.type,
680
+ });
681
+ if (tuple.reason) return undefined;
682
+ const key = renderAddress(tuple);
683
+ const published =
684
+ index.referenceTargets?.get(key) ??
685
+ index.foreignReferences?.get(key) ??
686
+ index.foreign?.get(key);
687
+ if (published)
688
+ return {
689
+ name: published.name,
690
+ subType: published.subType,
691
+ ...(published.uuid ? { uuid: published.uuid } : {}),
692
+ address: tuple,
693
+ };
694
+ if (index.referenceTargets) return undefined;
695
+ {
696
+ const key = renderAddress(tuple);
697
+ if (tuple.package !== index.contentPackage) {
698
+ const foreign = index.foreign?.get(key);
699
+ if (foreign) return { ...foreign, address: tuple };
700
+ return undefined;
934
701
  }
702
+ const base = resolveItemDocType(tuple.type, index.types);
703
+ const type = base ?? tuple.type;
704
+ if (tuple.system !== (base ? NO_SYSTEM : systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS)))
705
+ return undefined;
706
+ const local = index.byShortcode?.get(`${type}/${tuple.shortcode}`);
707
+ if (!local) return undefined;
708
+ const addresses = index.uuidByDoc?.get(local);
709
+ const uuid =
710
+ base ?
711
+ Object.hasOwn(local, "documentationUuid") ?
712
+ local.documentationUuid
713
+ : addresses?.docUuid
714
+ : addresses?.uuid;
715
+ return {
716
+ name: local.name,
717
+ subType: local.subType,
718
+ ...(uuid ? { uuid } : {}),
719
+ address: tuple,
720
+ };
935
721
  }
936
722
  return undefined;
937
723
  }
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.2",
3
+ "version": "22.11.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",