@heroiclands/package-build 20.3.1 → 20.4.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 (178) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/CONTENT.md +111 -92
  3. package/README.md +2 -9
  4. package/bin/content-build.mjs +70 -61
  5. package/bin/package-build.mjs +2 -2
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +187 -64
  8. package/docs/content-format.md +57 -68
  9. package/e2e.mjs +4 -4
  10. package/engine/actor-compiler.mjs +36 -36
  11. package/engine/address-charset.mjs +6 -6
  12. package/engine/address-diff.mjs +12 -11
  13. package/engine/anchored-sections.mjs +1 -1
  14. package/engine/anchors.mjs +1 -1
  15. package/engine/base-compiler.mjs +36 -38
  16. package/engine/bundle-notes.mjs +4 -4
  17. package/engine/bundles.mjs +8 -8
  18. package/engine/code-fences.mjs +3 -3
  19. package/engine/compendiums.mjs +8 -8
  20. package/engine/compile-corpus.mjs +4 -5
  21. package/engine/content-address.mjs +15 -15
  22. package/engine/content-charset.mjs +1 -1
  23. package/engine/content-format-check.mjs +24 -25
  24. package/engine/content-format.mjs +8 -8
  25. package/engine/content-html.mjs +202 -0
  26. package/engine/content-icons.mjs +196 -165
  27. package/engine/content-index.mjs +17 -17
  28. package/engine/content-links.mjs +30 -30
  29. package/engine/content-lint.mjs +20 -24
  30. package/engine/content-package.mjs +7 -9
  31. package/engine/content-slug.mjs +2 -2
  32. package/engine/content-tables.mjs +7 -8
  33. package/engine/content-tree.mjs +1 -1
  34. package/engine/derived-fields.mjs +174 -0
  35. package/engine/diagnostics.mjs +3 -4
  36. package/engine/document-subtypes.mjs +6 -7
  37. package/engine/field-reference.mjs +8 -8
  38. package/engine/field-spec.mjs +29 -30
  39. package/engine/folder-notes.mjs +14 -14
  40. package/engine/foreign-catalog.mjs +8 -9
  41. package/engine/foundry-entries.mjs +19 -20
  42. package/engine/frontmatter-lint.mjs +64 -69
  43. package/engine/frontmatter.mjs +11 -11
  44. package/engine/generate.mjs +39 -39
  45. package/engine/helpers.mjs +63 -54
  46. package/engine/homepage.mjs +20 -22
  47. package/engine/ids.mjs +15 -15
  48. package/engine/index-records.mjs +4 -4
  49. package/engine/index.mjs +14 -8
  50. package/engine/item-compiler.mjs +54 -29
  51. package/engine/item-docs.mjs +11 -13
  52. package/engine/item-registry.mjs +13 -13
  53. package/engine/journals.mjs +15 -15
  54. package/engine/macros.mjs +3 -3
  55. package/engine/map-notes.mjs +6 -6
  56. package/engine/metadata-index.mjs +5 -5
  57. package/engine/note-claims.mjs +36 -34
  58. package/engine/note-ids.mjs +5 -5
  59. package/engine/note-package.mjs +4 -4
  60. package/engine/note-renames.mjs +3 -3
  61. package/engine/note-schemas.mjs +3 -3
  62. package/engine/note-vocabulary.mjs +27 -26
  63. package/engine/pack-config.mjs +13 -13
  64. package/engine/pack-router.mjs +0 -0
  65. package/engine/prose-config.mjs +9 -9
  66. package/engine/prose-lint.mjs +12 -13
  67. package/engine/region-events.mjs +2 -2
  68. package/engine/retired-fields.mjs +24 -22
  69. package/engine/runtime-only-fields.mjs +1 -1
  70. package/engine/scene-levels.mjs +1 -1
  71. package/engine/scenes.mjs +19 -20
  72. package/engine/schema-check.mjs +20 -20
  73. package/engine/schema-extract.mjs +1 -1
  74. package/engine/site-build.mjs +45 -47
  75. package/engine/site-index.mjs +17 -18
  76. package/engine/sql-tables.mjs +21 -13
  77. package/engine/subtype-registry.mjs +5 -4
  78. package/engine/system-block.mjs +18 -19
  79. package/engine/systems.mjs +2 -2
  80. package/engine/web-wikilinks.mjs +35 -35
  81. package/engine/wikilink-syntax.mjs +16 -16
  82. package/engine/wikilinks.mjs +39 -39
  83. package/hm3/actors.mjs +22 -22
  84. package/hm3/document-subtypes.mjs +5 -5
  85. package/hm3/index.mjs +2 -2
  86. package/hm3/item-builders.mjs +1 -1
  87. package/hm3/item-fields.mjs +3 -3
  88. package/hm3/items.mjs +20 -8
  89. package/hm3/template-priority.mjs +2 -2
  90. package/lang.mjs +3 -3
  91. package/manifest.mjs +17 -20
  92. package/package.json +1 -2
  93. package/release.mjs +3 -3
  94. package/sohl/actors.mjs +28 -28
  95. package/sohl/affiliation-standings.mjs +2 -2
  96. package/sohl/being-info.mjs +5 -5
  97. package/sohl/default-item-art.mjs +5 -5
  98. package/sohl/document-subtypes.mjs +5 -5
  99. package/sohl/index.mjs +3 -3
  100. package/sohl/item-builders.mjs +7 -7
  101. package/sohl/item-fields.mjs +8 -9
  102. package/sohl/items.mjs +20 -6
  103. package/sohl/kb-passes.mjs +5 -5
  104. package/sohl/note-schemas.mjs +7 -7
  105. package/sohl/skill-base.mjs +1 -1
  106. package/types/content-config.d.mts +25 -24
  107. package/types/e2e.d.mts +3 -3
  108. package/types/engine/actor-compiler.d.mts +14 -13
  109. package/types/engine/address-charset.d.mts +6 -6
  110. package/types/engine/address-diff.d.mts +3 -3
  111. package/types/engine/base-compiler.d.mts +17 -19
  112. package/types/engine/bundle-notes.d.mts +3 -3
  113. package/types/engine/bundles.d.mts +1 -1
  114. package/types/engine/code-fences.d.mts +3 -3
  115. package/types/engine/compendiums.d.mts +3 -3
  116. package/types/engine/compile-corpus.d.mts +1 -1
  117. package/types/engine/content-address.d.mts +11 -11
  118. package/types/engine/content-format-check.d.mts +6 -6
  119. package/types/engine/content-format.d.mts +2 -2
  120. package/types/engine/content-html.d.mts +78 -0
  121. package/types/engine/content-icons.d.mts +139 -92
  122. package/types/engine/content-index.d.mts +2 -2
  123. package/types/engine/content-links.d.mts +8 -8
  124. package/types/engine/content-lint.d.mts +2 -2
  125. package/types/engine/content-package.d.mts +6 -8
  126. package/types/engine/derived-fields.d.mts +101 -0
  127. package/types/engine/diagnostics.d.mts +2 -2
  128. package/types/engine/document-subtypes.d.mts +3 -3
  129. package/types/engine/field-spec.d.mts +37 -39
  130. package/types/engine/folder-notes.d.mts +4 -5
  131. package/types/engine/foreign-catalog.d.mts +3 -3
  132. package/types/engine/foundry-entries.d.mts +6 -7
  133. package/types/engine/frontmatter-lint.d.mts +7 -7
  134. package/types/engine/frontmatter.d.mts +7 -7
  135. package/types/engine/generate.d.mts +7 -7
  136. package/types/engine/helpers.d.mts +25 -27
  137. package/types/engine/homepage.d.mts +16 -18
  138. package/types/engine/ids.d.mts +13 -13
  139. package/types/engine/index-records.d.mts +3 -3
  140. package/types/engine/index.d.mts +2 -0
  141. package/types/engine/item-compiler.d.mts +21 -5
  142. package/types/engine/item-docs.d.mts +2 -2
  143. package/types/engine/item-registry.d.mts +6 -6
  144. package/types/engine/journals.d.mts +3 -3
  145. package/types/engine/map-notes.d.mts +2 -2
  146. package/types/engine/metadata-index.d.mts +4 -4
  147. package/types/engine/note-claims.d.mts +16 -15
  148. package/types/engine/note-ids.d.mts +4 -4
  149. package/types/engine/note-package.d.mts +1 -1
  150. package/types/engine/note-renames.d.mts +3 -3
  151. package/types/engine/note-vocabulary.d.mts +9 -8
  152. package/types/engine/pack-config.d.mts +3 -3
  153. package/types/engine/pack-router.d.mts +1 -1
  154. package/types/engine/prose-config.d.mts +9 -9
  155. package/types/engine/prose-lint.d.mts +3 -4
  156. package/types/engine/region-events.d.mts +2 -2
  157. package/types/engine/retired-fields.d.mts +10 -9
  158. package/types/engine/schema-check.d.mts +11 -11
  159. package/types/engine/site-build.d.mts +18 -19
  160. package/types/engine/site-index.d.mts +3 -4
  161. package/types/engine/sql-tables.d.mts +11 -5
  162. package/types/engine/subtype-registry.d.mts +3 -3
  163. package/types/engine/system-block.d.mts +3 -3
  164. package/types/engine/web-wikilinks.d.mts +7 -7
  165. package/types/engine/wikilink-syntax.d.mts +17 -17
  166. package/types/engine/wikilinks.d.mts +13 -12
  167. package/types/hm3/actors.d.mts +1 -1
  168. package/types/hm3/document-subtypes.d.mts +1 -1
  169. package/types/hm3/items.d.mts +1 -2
  170. package/types/hm3/template-priority.d.mts +1 -1
  171. package/types/manifest.d.mts +8 -8
  172. package/types/sohl/actors.d.mts +2 -2
  173. package/types/sohl/affiliation-standings.d.mts +2 -2
  174. package/types/sohl/being-info.d.mts +2 -2
  175. package/types/sohl/document-subtypes.d.mts +1 -1
  176. package/types/sohl/items.d.mts +1 -2
  177. package/types/sohl/note-schemas.d.mts +1 -1
  178. package/MIGRATING.md +0 -608
@@ -22,7 +22,7 @@
22
22
  * `[[#slug|Text]]` a section of the source note itself
23
23
  * `[[doctype-shortcode|T]]` an item's *documentation* (see below)
24
24
  *
25
- * **Every link is an address, and every address carries a label** (#180). A
25
+ * **Every link is an address, and every address carries a label**. A
26
26
  * link written without one addresses nothing and is reported — see
27
27
  * {@link unlabelledLinkMessage}, which states the rule for both builds. The
28
28
  * bare `[[Alias]]` form and the index it was looked up in are retired.
@@ -36,8 +36,7 @@
36
36
  * Nothing narrower than `(type, shortcode)` is consulted — a note's directory
37
37
  * and its `category` play no part in resolution — and nothing wider: a note's
38
38
  * *name* is not an address, so two notes of a type may share a display name
39
- * ("Gear" as a rules page and as a user guide page) with nothing to disambiguate
40
- * (#179, #180).
39
+ * ("Gear" as a rules page and as a user guide page) with nothing to disambiguate.
41
40
  *
42
41
  * At compile time each becomes a Foundry UUID enricher, routed to the pack that
43
42
  * the target's type compiles into (see {@link packForType}):
@@ -47,7 +46,7 @@
47
46
  *
48
47
  * **Every address is computed once, when the target is indexed** — see
49
48
  * {@link buildWikilinkIndex} — and a link is resolved by looking that value up.
50
- * Nothing here concatenates a prefix at the point of use (#1498).
49
+ * Nothing here concatenates a prefix at the point of use.
51
50
  *
52
51
  * Section links address a **JournalEntryPage**, because Foundry UUIDs cannot
53
52
  * target a position inside a page. A heading carrying `{#slug}` therefore starts
@@ -63,15 +62,15 @@
63
62
  * **A document and its documentation are two documents.** An item note
64
63
  * compiles into an item — and, separately, its prose compiles into a
65
64
  * JournalEntry in the journals pack (see
66
- * {@link sohl.utils.packs.itemDocEntryId}); a macro note works the same way
67
- * (#1514). `skill/wpnc` addresses the skill; the **virtual qualifier**
65
+ * {@link sohl.utils.packs.itemDocEntryId}); a macro note works the same way.
66
+ * `skill/wpnc` addresses the skill; the **virtual qualifier**
68
67
  * `docskill/wpnc` addresses that skill's documentation, and
69
68
  * `docskill/wpnc#crafting` a page within it. `docmacro/autoattack#script`
70
69
  * reaches a macro's source. Every doc-carrying type has a `doc<type>`
71
70
  * counterpart, formed by prefix and never enumerated; see
72
71
  * {@link resolveItemDocType}. Without it a section link to an item note
73
72
  * produced a UUID against the *items* pack, which cannot hold a
74
- * JournalEntryPage, and dead-ended (#1362).
73
+ * JournalEntryPage, and dead-ended.
75
74
  *
76
75
  * **The two builds read the qualifier differently, by design.** In Foundry the
77
76
  * item and its documentation are separate documents in separate packs, so the
@@ -117,15 +116,15 @@ const norm = (s) => String(s).toLowerCase().trim();
117
116
  * not one.
118
117
  *
119
118
  * A document and its documentation are two documents in two packs, so they
120
- * need two addresses (#1362). `skill/wpnc` is the item; `docskill/wpnc` is the
119
+ * need two addresses. `skill/wpnc` is the item; `docskill/wpnc` is the
121
120
  * JournalEntry its prose compiled into, and `docmacro/autoattack` is the same
122
- * arrangement for a macro (#1514).
121
+ * arrangement for a macro.
123
122
  *
124
123
  * The virtual form exists for a type that carries separate documentation
125
124
  * ({@link sohl.utils.packs.docEntryTypes} — the set the journals compiler and
126
125
  * the link manifest read too), **or** for one that routes to the items pack.
127
126
  * The second clause is the older rule and stays: types that compile into items
128
- * are the open, unenumerated set (#1276), and a foreign package may publish an
127
+ * are the open, unenumerated set, and a foreign package may publish an
129
128
  * item type this build has never heard of. Dropping it would silently unlink
130
129
  * every `doc<type>` address into such a package.
131
130
  *
@@ -153,8 +152,8 @@ export function resolveItemDocType(qualifier, types) {
153
152
  * Two separators are accepted, and they are **not** interchangeable in how
154
153
  * confidently they mark a target as qualified:
155
154
  *
156
- * - **`type-shortcode`** and its qualified forms — the canonical spelling
157
- * (#1398). Obsidian reads `/` inside a wikilink as a *path* and resolves it
155
+ * - **`type-shortcode`** and its qualified forms — the canonical spelling.
156
+ * Obsidian reads `/` inside a wikilink as a *path* and resolves it
158
157
  * against the vault's folders, so a slash-qualified link is a broken link in
159
158
  * the editor where the content is now authored.
160
159
  * - **`type/shortcode`** — the legacy form, still resolved so that a link
@@ -163,7 +162,7 @@ export function resolveItemDocType(qualifier, types) {
163
162
  * before it is reported rather than guessed at. The split is at the **last**
164
163
  * slash, as it always was.
165
164
  *
166
- * **The grammar is strict, and omission runs left to right** (#59):
165
+ * **The grammar is strict, and omission runs left to right**:
167
166
  *
168
167
  * ```text
169
168
  * [[[[<package>-]<system>-]<type>-]<shortcode>]
@@ -178,12 +177,13 @@ export function resolveItemDocType(qualifier, types) {
178
177
  * **Parsing is plain positional counting**, the same rule
179
178
  * {@link readCanonicalKey} follows, and it is sound for the same reason: every
180
179
  * segment is `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN`, enforced on
181
- * shortcodes by `content-lint.mjs` since #1397), so the hyphen is purely a
180
+ * shortcodes by `content-lint.mjs`), so the hyphen is purely a
182
181
  * separator and the count alone determines every field. Verified across the
183
182
  * four content trees: 138,204 authored shortcodes, none carrying a separator.
184
183
  *
185
184
  * That replaced a first-hyphen split which let a shortcode contain a hyphen
186
- * (`trauma-self-pro` → `trauma` + `self-pro`). The tolerance predates #1397 and
185
+ * (`trauma-self-pro` → `trauma` + `self-pro`). The tolerance predates the
186
+ * charset rule and
187
187
  * outlived it; no tree has used it, and keeping it would make a three-segment
188
188
  * target ambiguous between a system and a hyphenated shortcode.
189
189
  *
@@ -287,7 +287,7 @@ function readQualifierCased(target, types, packages) {
287
287
  return read && { ...read, system, package: pkg };
288
288
  }
289
289
 
290
- // One segment is a bare name, which is not an address (#180); five or
290
+ // One segment is a bare name, which is not an address; five or
291
291
  // more is not a hyphenated shortcode but a name that happens to carry
292
292
  // separators, since no segment may contain one.
293
293
  default:
@@ -347,7 +347,7 @@ export function anchorPageId(noteId, anchorSlug) {
347
347
  * One entry per content note. `pack` / `docPack` name the packs the note's
348
348
  * document and its documentation entry landed in; omitted, the conventional
349
349
  * one-pack-per-type names stand in. `draft` says the note carries the `draft`
350
- * tag, which marks links *into* it and changes nothing else (#183).
350
+ * tag, which marks links *into* it and changes nothing else.
351
351
  * @param {string} packageId - The Foundry package shipping the packs; the first
352
352
  * segment of every emitted UUID.
353
353
  * @param {Map<string, object>} [foreign] - Canonically keyed entries from
@@ -363,7 +363,7 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
363
363
  throw new Error(
364
364
  "buildWikilinkIndex: packageId is required — it is the first " +
365
365
  "segment of every emitted UUID, and defaulting it is how links " +
366
- "came to address the wrong package (#1498).",
366
+ "came to address the wrong package.",
367
367
  );
368
368
  }
369
369
 
@@ -382,7 +382,7 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
382
382
  uuidByDoc.set(d, {
383
383
  // `d.pack` is where this note's document actually landed, resolved
384
384
  // by the pack router when the index was collected. A repository may
385
- // ship several packs of one type (#1566) and a UUID carries the
385
+ // ship several packs of one type and a UUID carries the
386
386
  // pack name, so the address cannot be derived from the type alone.
387
387
  uuid: compendiumUuid(packageId, d.type, d.id, d.pack),
388
388
  // An item's prose compiles into a separate JournalEntry, addressed
@@ -398,7 +398,7 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
398
398
  // foreign address resolves exactly like a local one and there is no
399
399
  // precedence rule to get wrong. A foreign package's types are added to
400
400
  // `types` too — without that, its addresses read as prose and silently lose
401
- // their link (#1499).
401
+ // their link.
402
402
  const foreignByKey = new Map(foreign ?? []);
403
403
  const foreignTypes = [];
404
404
  for (const v of foreignByKey.values()) {
@@ -448,7 +448,7 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
448
448
  * The foreign manifest entry an address names, or `null`.
449
449
  *
450
450
  * A target is a **partial** address, matched on the segments it supplies with
451
- * the rest wildcarded (#59) — so a package-qualified one is a scan rather than
451
+ * the rest wildcarded — so a package-qualified one is a scan rather than
452
452
  * a single lookup, and may still match one entry per system. An unqualified one
453
453
  * names no package either, so it resolves against whichever foreign package
454
454
  * publishes it — and, in both readings, only when exactly one entry matches.
@@ -470,7 +470,7 @@ function findForeign(index, read) {
470
470
  * Every foreign manifest entry an address names.
471
471
  *
472
472
  * The count is what separates *nothing publishes this* from *two packages do*,
473
- * and those are different findings with different fixes (#184), so the caller
473
+ * and those are different findings with different fixes, so the caller
474
474
  * gets the list rather than a single answer that has already collapsed the
475
475
  * distinction.
476
476
  *
@@ -484,7 +484,7 @@ function foreignHits(index, read) {
484
484
  const wanted = norm(read.itemDoc ? `doc${read.type}` : read.type);
485
485
  const shortcode = norm(read.shortcode);
486
486
 
487
- // **An omitted package means this package** (#336), so a short form
487
+ // **An omitted package means this package**, so a short form
488
488
  // addresses nothing foreign and never reaches a dependency's index. A link
489
489
  // that resolved into another package only because no local note claimed the
490
490
  // address was resolving by accident, and would have retargeted silently the
@@ -534,7 +534,7 @@ function unresolvedLink(text, target) {
534
534
  }
535
535
 
536
536
  /**
537
- * How a link to a **draft** note renders (#183).
537
+ * How a link to a **draft** note renders.
538
538
  *
539
539
  * A note tagged `draft` exists so a link into it is not dead, and nothing more.
540
540
  * Unmarked, a reader follows a promising link into an empty page and an author
@@ -552,7 +552,7 @@ function unresolvedLink(text, target) {
552
552
  * **Byte-identical with the site build's copy** in `web-wikilinks.mjs`, down to
553
553
  * the class name and the `title` wording — one authored link renders on two
554
554
  * surfaces, and the two builds have drifted before over exactly this kind of
555
- * detail (#1409). The argument is already-built markup and is deliberately not
555
+ * detail. The argument is already-built markup and is deliberately not
556
556
  * escaped; the *authored* text inside it was escaped, or made into a link, by
557
557
  * whichever resolver called this.
558
558
  *
@@ -574,7 +574,7 @@ function draftLink(inner) {
574
574
  *
575
575
  * **Code is verbatim.** A `[[…]]` inside a fenced or indented code block, or
576
576
  * inside an inline code span, is source text an author wrote to be read as
577
- * written, so it is left alone and not reported (#1505). Without that, a
577
+ * written, so it is left alone and not reported. Without that, a
578
578
  * script sample containing `grid[[0]]` became a link — and only for some
579
579
  * array shapes, `[[1,2],[3,4]]` having an inner `]` the pattern cannot cross,
580
580
  * so the corruption looked arbitrary. It reaches the reader through the
@@ -595,23 +595,23 @@ function draftLink(inner) {
595
595
  * @returns {{markdown: string, unresolved: Array<{link: string, target: string,
596
596
  * offset: number, reason: string, packages?: string[], anchor?: string}>}}
597
597
  * Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all
598
- * three resolvers share (#184) — `ambiguous` carries the claiming `packages`
598
+ * three resolvers share — `ambiguous` carries the claiming `packages`
599
599
  * and `unknown-anchor` the section it named. `offset` is the link's 0-based
600
600
  * position in `markdown`, which is what lets a caller report the line and
601
- * column it sits on (#17).
601
+ * column it sits on.
602
602
  */
603
603
  export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
604
604
  const unresolved = [];
605
605
 
606
606
  // `offset` is the third replacer argument because the pattern has exactly
607
607
  // one capture group. It is what makes two identical unresolved links on
608
- // one note tellable apart, and a position reportable at all (#17).
608
+ // one note tellable apart, and a position reportable at all.
609
609
  const out = replaceOutsideCode(markdown, WIKILINK, (all, rawInner, offset) => {
610
610
  const parsed = parseWikilink(rawInner);
611
611
  const target = parsed.target;
612
612
  const slug = parsed.anchor || null;
613
613
 
614
- // **Every link carries a label** (#180). Without one there is nothing
614
+ // **Every link carries a label**. Without one there is nothing
615
615
  // to resolve against: the alias namespace a bare `[[Text]]` was looked
616
616
  // up in is retired, and a shortcode is an address rather than prose, so
617
617
  // the link has neither a target this build can find nor text to show.
@@ -631,7 +631,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
631
631
 
632
632
  // An *empty* label is not a label — `[[x|]]` means "show the target's
633
633
  // name" — and that reading comes from {@link authoredLabel} so the web
634
- // resolver cannot draw the line somewhere else (#113).
634
+ // resolver cannot draw the line somewhere else.
635
635
  let text = authoredLabel(parsed) ?? "";
636
636
 
637
637
  // Resolve the document: the source note itself for an empty target, or
@@ -648,7 +648,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
648
648
  const qualified = readQualifier(target, index.types, index.packages);
649
649
  qualifiedRead = qualified;
650
650
  // A target that does not parse as an address is a defect: there is
651
- // no second namespace left to fall through to (#180).
651
+ // no second namespace left to fall through to.
652
652
  if (!qualified || qualified.reason) {
653
653
  unresolved.push({
654
654
  link: all,
@@ -659,7 +659,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
659
659
  });
660
660
  return unresolvedLink(text || target, target);
661
661
  }
662
- // An omitted system defaults from where the link is written (#336),
662
+ // An omitted system defaults from where the link is written,
663
663
  // and a body is under no system block, so it is `none`. Under
664
664
  // `none` a system-bearing type addresses its *documentation* — a
665
665
  // note's `none` address IS its `doc<type>` entry — which is what a
@@ -686,7 +686,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
686
686
  if (hits.length > 1) {
687
687
  // Two packages publish the short address, so it names neither.
688
688
  // Its own class: the fix is the package-qualified form, not a
689
- // corrected shortcode (#184).
689
+ // corrected shortcode.
690
690
  unresolved.push({
691
691
  link: all,
692
692
  target,
@@ -729,7 +729,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
729
729
  // An address with an *empty* label — `[[skill-clmb|]]` — has no prose
730
730
  // to show, a shortcode being an address rather than display text, so
731
731
  // the document's **current** name stands in and a rename shows at every
732
- // citation with no link edited (#1409). The knowledgebase build reads
732
+ // citation with no link edited. The knowledgebase build reads
733
733
  // the same authored link the same way.
734
734
  if (!text) text = doc.name ?? target;
735
735
 
@@ -751,13 +751,13 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
751
751
  // page an anchor names. An Item or Actor link opens that document's
752
752
  // *sheet*, which has no sections, so the anchor has nothing to address
753
753
  // and is dropped. Forging a JournalEntryPage id onto a document that
754
- // can never hold one is what made such links dead-end (#1362); an
754
+ // can never hold one is what made such links dead-end; an
755
755
  // item's pages are addressed through its `doc<type>` counterpart.
756
756
  // A `#section` the target declares no heading for. Checked here, and
757
757
  // not only by `content-build links`, because this is the build that
758
758
  // *emits* the link: `anchorPageId` will hash any slug into a page id,
759
759
  // so an undeclared one compiles to a `@UUID` that dead-ends for the
760
- // reader (#193). A foreign anchor has always been checked this way —
760
+ // reader. A foreign anchor has always been checked this way —
761
761
  // the manifest carries the map — and a local one now is too, from the
762
762
  // anchor set the index carries.
763
763
  if (slug && isJournal && doc.anchors && !doc.anchors.has(slug)) {
@@ -774,8 +774,8 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
774
774
  const uuid =
775
775
  slug && isJournal ? pageUuid(entryUuid, anchorPageId(entryId, slug)) : entryUuid;
776
776
  const link = `@UUID[${uuid}]{${text}}`;
777
- // A link into a note that exists but is not written renders marked
778
- // (#183). Presentation only — the UUID above is unchanged, and a
777
+ // A link into a note that exists but is not written renders marked.
778
+ // Presentation only — the UUID above is unchanged, and a
779
779
  // `[[#slug]]` self-link is not marked because the reader is already in
780
780
  // the note it would be telling them about.
781
781
  return doc.draft ? draftLink(link) : link;
package/hm3/actors.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * The machinery is {@link module:engine/actor-compiler}'s and is shared with
18
18
  * the SoHL pass: the predefined-item catalogue, the note-vocabulary →
19
- * document-subtype translation each embedded reference goes through (#140), the
19
+ * document-subtype translation each embedded reference goes through, the
20
20
  * merge and the stable embedded ids. This module states HM3's data model and
21
21
  * nothing else.
22
22
  *
@@ -24,7 +24,7 @@
24
24
  * splits what a note calls a `being` into a `character` and a `creature`, and
25
25
  * nothing in the note's own vocabulary partitions cleanly onto that split — so
26
26
  * the note says which, by writing `hm3.type`. An absent one is an error naming
27
- * the note, never a default (#139).
27
+ * the note, never a default.
28
28
  *
29
29
  * **What is emitted, and what is deliberately not.** Four rows of the content
30
30
  * format's `being` mapping table give HM3 a destination — `data.portrait` →
@@ -32,8 +32,8 @@
32
32
  * `data.templatePriority` → `flags.hm3.templatePriority` — and every one of
33
33
  * them is *declared as that source* rather than as the bare key the corpus
34
34
  * writes, so the specification's mapping is executable rather than
35
- * aspirational. Three were declared by #305; `data.portrait` was still read by
36
- * hand until #332, and so was the one row that did not work. Plus the two anchored prose sections: `{#appearance}` is HM3's `description` and `{#dossier}` its
35
+ * aspirational. Plus the two anchored prose sections: `{#appearance}` is HM3's
36
+ * `description` and `{#dossier}` its
37
37
  * `biography`. Everything else an HM3 actor carries — the thirteen abilities,
38
38
  * the sunsign, `move`, `fatigue`, `shockIndex`, a creature's `loadRating` — has
39
39
  * no shared source stated anywhere, so it is authored at its own path under
@@ -57,7 +57,7 @@ import { documentSubtype } from "../engine/document-subtypes.mjs";
57
57
  import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
58
58
  import { templateFlags } from "./template-priority.mjs";
59
59
  // The retirement window's reports, shared with the frontmatter lint so the
60
- // two cannot say different things about the same key (#305, #332).
60
+ // two cannot say different things about the same key.
61
61
  import {
62
62
  legacyKeyMessage,
63
63
  locateFrontmatterKey,
@@ -65,7 +65,7 @@ import {
65
65
  } from "../engine/retired-fields.mjs";
66
66
  // The note-level `hm3:` block: `hm3.system` onto the document's `system`
67
67
  // verbatim, and `hm3.img` / `hm3.items` / `hm3.effects` / `hm3.flags`
68
- // overriding their shared top-level forms for this system alone (#58).
68
+ // overriding their shared top-level forms for this system alone.
69
69
  import { blockField, blockProperty, mergeSystemData } from "../engine/system-block.mjs";
70
70
 
71
71
  /**
@@ -73,13 +73,13 @@ import { blockField, blockProperty, mergeSystemData } from "../engine/system-blo
73
73
  *
74
74
  * Declared rather than read by hand, so a value resolves by the same order
75
75
  * every other declared field does — `hm3.system.<to>` first, then the in-block
76
- * position, then the shared source, then the default (#58) — and so the
76
+ * position, then the shared source, then the default — and so the
77
77
  * author-facing reference can be generated from the same statement the compiler
78
- * obeys (#22).
78
+ * obeys.
79
79
  *
80
- * **Both positions, named separately (#305).** The specification maps
80
+ * **Both positions, named separately.** The specification maps
81
81
  * `data.species` onto `system.species`, and every HM3 note in the corpus writes
82
- * `hm3.species`. Until #126 sweeps them those are two live positions for one
82
+ * `hm3.species`. Until the sweep those are two live positions for one
83
83
  * field, so the declaration names both: `name` is the shared source, `legacyKey`
84
84
  * the key the block still carries, and the block wins while it is there. Naming
85
85
  * only one of them is what made the row unreadable — a plain `species` could
@@ -107,12 +107,12 @@ const ACTOR_FIELDS = Object.freeze([
107
107
  * `data.species` was: read with `blockProperty(fm, block, "portrait")` it saw
108
108
  * the block and the note's top level and nothing else, so the `data.portrait`
109
109
  * the specification names never reached the document and the `?? defaultImg`
110
- * beside it dressed the miss up as "this note names no art" (#332).
110
+ * beside it dressed the miss up as "this note names no art".
111
111
  *
112
112
  * It is **not** in `ACTOR_FIELDS`, because `buildFromFields` has no seam for
113
113
  * the subtype default that has to follow it — the `?? defaultImg` is the whole
114
- * of what distinguishes an unnamed portrait from a deliberately blank one
115
- * (#218), and it needs a subtype the coercion is not handed.
114
+ * of what distinguishes an unnamed portrait from a deliberately blank one,
115
+ * and it needs a subtype the coercion is not handed.
116
116
  *
117
117
  * @type {import("../engine/field-spec.mjs").FieldSpec}
118
118
  */
@@ -270,7 +270,7 @@ export class Hm3Actors extends SystemActorCompiler {
270
270
  const id = fm.id;
271
271
  const ctx = `actor "${name}"`;
272
272
  // The document's own subtype, and the art that goes with it. Both are
273
- // looked up from the note's `type` rather than spelled here (#79); for
273
+ // looked up from the note's `type` rather than spelled here; for
274
274
  // HM3 the row is one-to-many, so the note's `hm3.type` decides.
275
275
  const subType = /** @type {string} */ (
276
276
  documentSubtype(this.documentSubtypes, fm.type, fm, {
@@ -283,7 +283,7 @@ export class Hm3Actors extends SystemActorCompiler {
283
283
 
284
284
  // The sweep's progress signal, reported where an author meets it
285
285
  // soonest — every consumer runs the compile, and not every one runs the
286
- // lint (#142). A warning: the note compiles to the correct document
286
+ // lint. A warning: the note compiles to the correct document
287
287
  // either way, and refusing the position comes once no tree writes it.
288
288
  const onLegacyKey = (field) =>
289
289
  this.noteWarn(
@@ -292,7 +292,7 @@ export class Hm3Actors extends SystemActorCompiler {
292
292
  );
293
293
 
294
294
  // The shared level's own retiring position — the top-level key `data:`
295
- // gathered the field off (#332). Same signal, same severity, separate
295
+ // gathered the field off. Same signal, same severity, separate
296
296
  // callback: a note may have moved one position and not the other.
297
297
  // Anchored at column 1, because the two spellings coincide — `portrait`
298
298
  // names a block key and a top-level one — and a locator that took the
@@ -312,16 +312,16 @@ export class Hm3Actors extends SystemActorCompiler {
312
312
  // One spelling, as everywhere else: `packFolder` names a folder note
313
313
  // by its address. This pass once read only the Foundry id, so an HM3
314
314
  // tree could not file an actor by address at all — which its own sweep
315
- // needs; the id spelling is retired outright (#251, #255, #260).
315
+ // needs; the id spelling is retired outright.
316
316
  const folder = this.folderResolver(blockField(fm, block, "packFolder", null), {
317
317
  isAddress: true,
318
318
  });
319
319
 
320
320
  const system = {
321
- // Nullish, not `||` (#218): a note that names no portrait gets the
321
+ // Nullish, not `||`: a note that names no portrait gets the
322
322
  // subtype's default, one that writes `""` ships blank on purpose.
323
323
  // Resolved through the declaration so `data.portrait` is reached at
324
- // all — see {@link BIO_IMAGE_FIELD} (#332).
324
+ // all — see {@link BIO_IMAGE_FIELD}.
325
325
  bioImage: readField(BIO_IMAGE_FIELD, fm, reports) ?? defaultImg,
326
326
  description: renderSection(body || "", "appearance"),
327
327
  biography: renderSection(body || "", "dossier"),
@@ -332,13 +332,13 @@ export class Hm3Actors extends SystemActorCompiler {
332
332
  };
333
333
 
334
334
  // Whatever the note authors under `hm3.system`, at the DataModel's own
335
- // paths (#58). This pass has no field declaration, so it claims
335
+ // paths. This pass has no field declaration, so it claims
336
336
  // nothing: every authored path is the author's, and the fields above
337
337
  // are what a note that authors none still gets.
338
338
  mergeSystemData(system, fm, { block });
339
339
  this.reportUndeclaredSystemData(fm, block, "Actor", subType);
340
340
  // And what this pass wrote itself — there is no field declaration for a
341
- // being at all, so every key above is a compiler emission (#155).
341
+ // being at all, so every key above is a compiler emission.
342
342
  this.reportEmittedSystemData(system, {
343
343
  fm,
344
344
  block,
@@ -385,7 +385,7 @@ export class Hm3Actors extends SystemActorCompiler {
385
385
  * template priority.
386
386
  *
387
387
  * The rule itself is {@link module:hm3/template-priority.templateFlags},
388
- * because the Item pass writes the same flag from the same statement (#283)
388
+ * because the Item pass writes the same flag from the same statement
389
389
  * and two copies of it were one copy too many — this pass had the only one,
390
390
  * and the Item pass had none.
391
391
  *
@@ -14,18 +14,18 @@
14
14
  /**
15
15
  * **HM3's note-type → document-subtype map** — which Foundry document, and
16
16
  * which subtype of it, a note of each content type compiles into for HârnMaster
17
- * 3 (#139).
17
+ * 3.
18
18
  *
19
19
  * The mechanism is `engine/document-subtypes.mjs`, the same one SoHL's
20
20
  * declaration uses; the two halves share it and share nothing else. That is the
21
- * `engine/` ÷ system line this package draws everywhere (#36), and this file is
21
+ * `engine/` ÷ system line this package draws everywhere, and this file is
22
22
  * the first evidence that the line holds: a second system declared its map
23
23
  * without a line of the mechanism changing.
24
24
  *
25
25
  * **Where this map differs from SoHL's, and why that matters.**
26
26
  *
27
- * SoHL's map was the identity in every row until #78 renamed three of them,
28
- * which was a fact about SoHL rather than a rule (#79) — and while it stayed
27
+ * SoHL's map would be the identity in every row but for three renames,
28
+ * which was a fact about SoHL rather than a rule — and while it stayed
29
29
  * true nothing proved the map was doing any work. HM3's was never close. Four
30
30
  * of its rows are one-to-many, one renames outright (`projectile` →
31
31
  * `missilegear`), and five of its type names are SoHL's own with a *different
@@ -82,7 +82,7 @@ export const HM3_TYPE_KEY = "type";
82
82
  * map does not name — `affiliation`, `affliction`, `attribute`,
83
83
  * `concoction`, `mystery`, and every core type — compile into no HM3
84
84
  * document at all, silently and correctly: HM3 has no form of them, and a
85
- * finding on every such note would be the noise #79's rule exists to prevent.
85
+ * finding on every such note would be the noise the rule exists to prevent.
86
86
  *
87
87
  * @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
88
88
  */
package/hm3/index.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  /**
15
15
  * The HM3-specific half of the toolchain: the knowledge of the HârnMaster 3
16
- * data model that a generic content module must never receive (#139).
16
+ * data model that a generic content module must never receive.
17
17
  *
18
18
  * The second half, and the first evidence that `engine/` ÷ system is a real
19
19
  * line rather than an aspiration: HM3's item-type registry and its builders,
@@ -39,7 +39,7 @@ export * as itemBuilders from "./item-builders.mjs";
39
39
  /** The `hm3:` frontmatter vocabulary of every HM3 item type. */
40
40
  export * as itemFields from "./item-fields.mjs";
41
41
 
42
- /** This system's note-type → document-subtype map, one-to-many rows and all (#139). */
42
+ /** This system's note-type → document-subtype map, one-to-many rows and all. */
43
43
  export * as documentSubtypes from "./document-subtypes.mjs";
44
44
 
45
45
  /** The Item compiler. */
@@ -19,7 +19,7 @@
19
19
  * both systems names both: `itemBuilders: [sohl, hm3]`. The engine keeps them
20
20
  * apart from there on — `itemBuilder(type, system)` takes the system that is
21
21
  * asking, and a type both registries declare **throws** rather than resolving
22
- * when nobody says which (#58). That guard existed before there was a second
22
+ * when nobody says which. That guard existed before there was a second
23
23
  * registry to trip it; this is the registry it was written for.
24
24
  *
25
25
  * **The builders are generated, not written**, from the field declarations in
@@ -33,16 +33,16 @@
33
33
  * **One shared source, two destinations.** `name` is the shared property a
34
34
  * field draws from and `to` is where it lands, so a single authored `weight`
35
35
  * feeds `sohl.system.weightBase` *and* `hm3.system.weight` — one authored fact,
36
- * two documents (#58). That is the whole reason the two are declared
36
+ * two documents. That is the whole reason the two are declared
37
37
  * separately.
38
38
  *
39
39
  * The specification writes those sources as `data.weight`, and the sources here
40
- * are written bare, exactly as SoHL's are: the `data:` region is #128's
40
+ * are written bare, exactly as SoHL's are: the `data:` region is the sweep's
41
41
  * migration and neither half has moved to it. Spelling HM3's differently would
42
42
  * make a note feeding both systems author the same fact twice for no gain, and
43
43
  * would take these rows out of reach of `content-build content-format fields`,
44
44
  * which pairs a `data.<key>` claim with a bare declared `<key>`. Both halves
45
- * move together when #128 lands.
45
+ * move together when the sweep lands.
46
46
  *
47
47
  * **What a note authors under `hm3.system` is not declared here.** HM3 fields
48
48
  * that no shared `data:` property feeds — a skill's `type`, an armour's
package/hm3/items.mjs CHANGED
@@ -13,13 +13,13 @@
13
13
 
14
14
  /**
15
15
  * **HM3's Item pass** — the two things about compiling a note into an HM3 Item
16
- * that are facts about HM3 rather than about the note format (#139).
16
+ * that are facts about HM3 rather than about the note format.
17
17
  *
18
18
  * Everything else is {@link module:engine/item-compiler}'s, and is the same
19
19
  * code the SoHL pass runs: which notes are claimed, which subtype each becomes,
20
20
  * which registry builds it, the authored `hm3.system` passthrough, the schema
21
21
  * check, and the compendium envelope. A second system is a map and a handful of
22
- * emitted keys, which is the arrangement #79 and #58 were building towards.
22
+ * emitted keys, which is the arrangement the mapping tables build towards.
23
23
  *
24
24
  * **What HM3's compiler writes on every item: one key, and only when there is
25
25
  * something to write.** The content format gives an item's `{#appearance}`
@@ -35,9 +35,9 @@
35
35
  * priority is a shared statement — a note declaring `data.templatePriority` says
36
36
  * the same thing to both systems — but HM3 declares no `system` field for it, so
37
37
  * it lands under this system's own flag scope as `flags.hm3.templatePriority`,
38
- * exactly as the Actor pass writes it. This pass wrote no such flag until #283:
39
- * an item note declaring the priority compiled into a SoHL item that knew it was
40
- * a template and an HM3 item that did not.
38
+ * exactly as the Actor pass writes it. Without it, an item note declaring the
39
+ * priority compiles into a SoHL item that knows it is a template and an HM3
40
+ * item that does not.
41
41
  *
42
42
  * **There is no HM3 equivalent of `docHtml`.** SoHL points an item at the
43
43
  * JournalEntry its prose compiled into, and HM3's data model has nowhere to put
@@ -57,13 +57,25 @@ import { templateFlags } from "./template-priority.mjs";
57
57
  export class Hm3Items extends SystemItemCompiler {
58
58
  /**
59
59
  * HM3's note-type → document-subtype map — the one declaration that says
60
- * which block this pass reads, which notes it claims, and what each becomes
61
- * (#58/#79).
60
+ * which block this pass reads, which notes it claims, and what each becomes.
62
61
  *
63
62
  * @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
64
63
  */
65
64
  static documentSubtypes = HM3_DOCUMENT_SUBTYPES;
66
65
 
66
+ /**
67
+ * The `system` keys this pass derives from the note.
68
+ *
69
+ * `description` is the note's `{#appearance}` section, rendered. A note
70
+ * authoring it writes into a key the compiler overwrites from prose that
71
+ * may say something else.
72
+ *
73
+ * @type {readonly {key: string, from: string}[]}
74
+ */
75
+ static derivedSystemKeys = Object.freeze([
76
+ { key: "description", from: "the note's `{#appearance}` section" },
77
+ ]);
78
+
67
79
  /**
68
80
  * The `system.*` field HM3 writes on an item from the note's prose.
69
81
  *
@@ -85,7 +97,7 @@ export class Hm3Items extends SystemItemCompiler {
85
97
  * The same statement the Actor pass records, through the same
86
98
  * {@link module:hm3/template-priority.templateFlags} — see that module for
87
99
  * why the priority lives in flags at all, and for what its absence here
88
- * cost (#283).
100
+ * cost.
89
101
  *
90
102
  * @param {object} fm - The note's frontmatter.
91
103
  * @returns {object} The flags to emit.
@@ -13,7 +13,7 @@
13
13
 
14
14
  /**
15
15
  * **Where HM3 records the template priority** — one statement, read by both of
16
- * this system's passes (#283).
16
+ * this system's passes.
17
17
  *
18
18
  * `data.templatePriority` is the shared fact that a note is a *starting
19
19
  * template*, and the specification states it as a row every type maps:
@@ -48,7 +48,7 @@ import { blockProperty } from "../engine/system-block.mjs";
48
48
  * A note that is not a template — or states nothing — writes nothing, rather
49
49
  * than a `null` nothing reads.
50
50
  *
51
- * **Read through the shared resolver, not a field declaration** (#266). A
51
+ * **Read through the shared resolver, not a field declaration**. A
52
52
  * `FieldSpec`'s shared source is a single position, and this value has five:
53
53
  * `data:`, this block, the top level, and the retiring `archetype` spelling in
54
54
  * the latter two. The resolver is the single implementation of what a note said,
package/lang.mjs CHANGED
@@ -29,13 +29,13 @@
29
29
  * `expandObject` **throws**: it cannot create a `Note` property on the string
30
30
  * `"The Pall"`. Foundry catches that throw and discards the **entire** file,
31
31
  * so one colliding pair drops every translation in it and each string renders
32
- * as its raw key (#636). A key must be a leaf **or** a branch, never both.
32
+ * as its raw key. A key must be a leaf **or** a branch, never both.
33
33
  * - **A Handlebars placeholder.** Foundry interpolates with `format()` and
34
34
  * SINGLE braces, so a `{{…}}` value renders literally unless some call site
35
- * happens to hand it to a Handlebars pass (#1353).
35
+ * happens to hand it to a Handlebars pass.
36
36
  * - **Data baked into a key segment.** A segment carrying anything but
37
37
  * `[A-Za-z0-9_-]` is a path or a UUID in a key, and a dotted payload is how
38
- * the collision above gets in (#636, #1351).
38
+ * the collision above gets in.
39
39
  *
40
40
  * Every function here is pure — it takes source text and returns findings, and
41
41
  * touches no filesystem and emits nothing. The caller owns discovery and