@heroiclands/package-build 20.3.1 → 20.5.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 (195) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/CONTENT.md +322 -110
  3. package/README.md +21 -10
  4. package/bin/content-build.mjs +204 -92
  5. package/bin/package-build.mjs +48 -15
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +518 -151
  8. package/docs/api.md +1352 -0
  9. package/docs/commands.md +1609 -0
  10. package/docs/configuration.md +1432 -0
  11. package/docs/content-format.md +73 -74
  12. package/docs/diagnostics.md +356 -0
  13. package/docs/getting-started.md +813 -0
  14. package/docs/project-setup.md +469 -0
  15. package/e2e.mjs +4 -4
  16. package/engine/actor-compiler.mjs +64 -61
  17. package/engine/address-charset.mjs +6 -6
  18. package/engine/address-diff.mjs +57 -52
  19. package/engine/anchored-sections.mjs +1 -1
  20. package/engine/anchors.mjs +1 -1
  21. package/engine/base-compiler.mjs +42 -38
  22. package/engine/bundle-notes.mjs +4 -4
  23. package/engine/bundles.mjs +17 -8
  24. package/engine/code-fences.mjs +3 -3
  25. package/engine/compendiums.mjs +8 -8
  26. package/engine/compile-corpus.mjs +4 -5
  27. package/engine/content-address.mjs +24 -24
  28. package/engine/content-charset.mjs +1 -1
  29. package/engine/content-format-check.mjs +24 -25
  30. package/engine/content-format.mjs +8 -8
  31. package/engine/content-html.mjs +202 -0
  32. package/engine/content-icons.mjs +196 -165
  33. package/engine/content-index.mjs +61 -40
  34. package/engine/content-links.mjs +74 -41
  35. package/engine/content-lint.mjs +64 -34
  36. package/engine/content-package.mjs +7 -9
  37. package/engine/content-slug.mjs +2 -2
  38. package/engine/content-tables.mjs +38 -34
  39. package/engine/content-tree.mjs +1 -1
  40. package/engine/derived-fields.mjs +174 -0
  41. package/engine/diagnostics.mjs +3 -4
  42. package/engine/document-subtypes.mjs +6 -7
  43. package/engine/field-reference.mjs +8 -8
  44. package/engine/field-spec.mjs +29 -30
  45. package/engine/folder-notes.mjs +18 -16
  46. package/engine/foreign-catalog.mjs +8 -9
  47. package/engine/foundry-entries.mjs +19 -20
  48. package/engine/frontmatter-lint.mjs +95 -103
  49. package/engine/frontmatter.mjs +11 -11
  50. package/engine/generate.mjs +44 -39
  51. package/engine/helpers.mjs +147 -84
  52. package/engine/homepage.mjs +20 -22
  53. package/engine/ids.mjs +15 -15
  54. package/engine/index-records.mjs +4 -4
  55. package/engine/index.mjs +26 -10
  56. package/engine/item-compiler.mjs +54 -29
  57. package/engine/item-docs.mjs +11 -13
  58. package/engine/item-registry.mjs +13 -13
  59. package/engine/journals.mjs +24 -15
  60. package/engine/macros.mjs +3 -3
  61. package/engine/map-notes.mjs +6 -6
  62. package/engine/metadata-index.mjs +5 -5
  63. package/engine/note-claims.mjs +51 -41
  64. package/engine/note-ids.mjs +5 -5
  65. package/engine/note-package.mjs +4 -4
  66. package/engine/note-renames.mjs +3 -3
  67. package/engine/note-schemas.mjs +3 -8
  68. package/engine/note-vocabulary.mjs +56 -54
  69. package/engine/pack-config.mjs +39 -25
  70. package/engine/pack-router.mjs +0 -0
  71. package/engine/pdf-build.mjs +464 -0
  72. package/engine/pdf-fonts.mjs +420 -0
  73. package/engine/pdf-render.mjs +798 -0
  74. package/engine/pdf-toc.mjs +525 -0
  75. package/engine/prose-config.mjs +9 -9
  76. package/engine/prose-lint.mjs +12 -13
  77. package/engine/region-events.mjs +2 -2
  78. package/engine/retired-fields.mjs +24 -22
  79. package/engine/runtime-only-fields.mjs +1 -1
  80. package/engine/scene-levels.mjs +1 -1
  81. package/engine/scenes.mjs +33 -25
  82. package/engine/schema-check.mjs +21 -21
  83. package/engine/schema-extract.mjs +1 -1
  84. package/engine/site-build.mjs +66 -50
  85. package/engine/site-index.mjs +17 -18
  86. package/engine/sql-tables.mjs +21 -13
  87. package/engine/subtype-registry.mjs +5 -4
  88. package/engine/system-block.mjs +18 -19
  89. package/engine/systems.mjs +2 -2
  90. package/engine/web-wikilinks.mjs +41 -38
  91. package/engine/wikilink-syntax.mjs +16 -16
  92. package/engine/wikilinks.mjs +40 -42
  93. package/hm3/actors.mjs +30 -22
  94. package/hm3/document-subtypes.mjs +5 -5
  95. package/hm3/index.mjs +2 -2
  96. package/hm3/item-builders.mjs +1 -1
  97. package/hm3/item-fields.mjs +3 -3
  98. package/hm3/items.mjs +28 -8
  99. package/hm3/template-priority.mjs +2 -2
  100. package/lang.mjs +3 -3
  101. package/manifest.mjs +17 -20
  102. package/package.json +1 -2
  103. package/release.mjs +66 -6
  104. package/sohl/actors.mjs +36 -28
  105. package/sohl/affiliation-standings.mjs +2 -2
  106. package/sohl/being-info.mjs +5 -5
  107. package/sohl/default-item-art.mjs +5 -5
  108. package/sohl/document-subtypes.mjs +5 -5
  109. package/sohl/index.mjs +3 -3
  110. package/sohl/item-builders.mjs +7 -7
  111. package/sohl/item-fields.mjs +8 -9
  112. package/sohl/items.mjs +28 -6
  113. package/sohl/kb-passes.mjs +5 -5
  114. package/sohl/note-schemas.mjs +12 -12
  115. package/sohl/skill-base.mjs +1 -1
  116. package/types/content-config.d.mts +91 -39
  117. package/types/e2e.d.mts +3 -3
  118. package/types/engine/actor-compiler.d.mts +45 -40
  119. package/types/engine/address-charset.d.mts +6 -6
  120. package/types/engine/address-diff.d.mts +60 -6
  121. package/types/engine/base-compiler.d.mts +27 -21
  122. package/types/engine/bundle-notes.d.mts +3 -3
  123. package/types/engine/bundles.d.mts +10 -1
  124. package/types/engine/code-fences.d.mts +3 -3
  125. package/types/engine/compendiums.d.mts +3 -3
  126. package/types/engine/compile-corpus.d.mts +1 -1
  127. package/types/engine/content-address.d.mts +20 -20
  128. package/types/engine/content-format-check.d.mts +6 -6
  129. package/types/engine/content-format.d.mts +2 -2
  130. package/types/engine/content-html.d.mts +78 -0
  131. package/types/engine/content-icons.d.mts +139 -92
  132. package/types/engine/content-index.d.mts +59 -15
  133. package/types/engine/content-links.d.mts +8 -8
  134. package/types/engine/content-lint.d.mts +8 -6
  135. package/types/engine/content-package.d.mts +6 -8
  136. package/types/engine/content-tables.d.mts +49 -18
  137. package/types/engine/derived-fields.d.mts +101 -0
  138. package/types/engine/diagnostics.d.mts +2 -2
  139. package/types/engine/document-subtypes.d.mts +3 -3
  140. package/types/engine/field-spec.d.mts +37 -39
  141. package/types/engine/folder-notes.d.mts +4 -5
  142. package/types/engine/foreign-catalog.d.mts +3 -3
  143. package/types/engine/foundry-entries.d.mts +6 -7
  144. package/types/engine/frontmatter-lint.d.mts +10 -9
  145. package/types/engine/frontmatter.d.mts +7 -7
  146. package/types/engine/generate.d.mts +7 -7
  147. package/types/engine/helpers.d.mts +128 -56
  148. package/types/engine/homepage.d.mts +16 -18
  149. package/types/engine/ids.d.mts +13 -13
  150. package/types/engine/index-records.d.mts +3 -3
  151. package/types/engine/index.d.mts +6 -0
  152. package/types/engine/item-compiler.d.mts +21 -5
  153. package/types/engine/item-docs.d.mts +2 -2
  154. package/types/engine/item-registry.d.mts +6 -6
  155. package/types/engine/journals.d.mts +12 -3
  156. package/types/engine/map-notes.d.mts +2 -2
  157. package/types/engine/metadata-index.d.mts +4 -4
  158. package/types/engine/note-claims.d.mts +30 -22
  159. package/types/engine/note-ids.d.mts +4 -4
  160. package/types/engine/note-package.d.mts +1 -1
  161. package/types/engine/note-renames.d.mts +3 -3
  162. package/types/engine/note-vocabulary.d.mts +31 -203
  163. package/types/engine/pack-config.d.mts +7 -7
  164. package/types/engine/pack-router.d.mts +1 -1
  165. package/types/engine/pdf-build.d.mts +42 -0
  166. package/types/engine/pdf-fonts.d.mts +30 -0
  167. package/types/engine/pdf-render.d.mts +144 -0
  168. package/types/engine/pdf-toc.d.mts +114 -0
  169. package/types/engine/prose-config.d.mts +9 -9
  170. package/types/engine/prose-lint.d.mts +3 -4
  171. package/types/engine/region-events.d.mts +2 -2
  172. package/types/engine/retired-fields.d.mts +10 -9
  173. package/types/engine/scenes.d.mts +10 -1
  174. package/types/engine/schema-check.d.mts +13 -13
  175. package/types/engine/site-build.d.mts +52 -25
  176. package/types/engine/site-index.d.mts +3 -4
  177. package/types/engine/sql-tables.d.mts +11 -5
  178. package/types/engine/subtype-registry.d.mts +3 -3
  179. package/types/engine/system-block.d.mts +3 -3
  180. package/types/engine/web-wikilinks.d.mts +7 -7
  181. package/types/engine/wikilink-syntax.d.mts +17 -17
  182. package/types/engine/wikilinks.d.mts +14 -14
  183. package/types/hm3/actors.d.mts +9 -1
  184. package/types/hm3/document-subtypes.d.mts +1 -1
  185. package/types/hm3/items.d.mts +9 -2
  186. package/types/hm3/template-priority.d.mts +1 -1
  187. package/types/manifest.d.mts +8 -8
  188. package/types/release.d.mts +15 -4
  189. package/types/sohl/actors.d.mts +12 -4
  190. package/types/sohl/affiliation-standings.d.mts +2 -2
  191. package/types/sohl/being-info.d.mts +2 -2
  192. package/types/sohl/document-subtypes.d.mts +1 -1
  193. package/types/sohl/items.d.mts +9 -2
  194. package/types/sohl/note-schemas.d.mts +1 -1
  195. package/MIGRATING.md +0 -608
@@ -16,7 +16,8 @@
16
16
  * questions asked of the *list* rather than of any one map.
17
17
  *
18
18
  * One frozen list, and the two lookups that need to choose among its members
19
- * before a map can be consulted at all. It lived in `note-claims.mjs` until #270,
19
+ * before a map can be consulted at all. It sits here rather than in
20
+ * `note-claims.mjs`,
20
21
  * which is where the *questions* asked of it live — but that module imports
21
22
  * half the engine, so anything needing the bare list had to take all of it, and
22
23
  * `helpers.mjs` could not take it at all: `note-claims.mjs` imports
@@ -42,7 +43,7 @@ import { HM3_DOCUMENT_SUBTYPES } from "../hm3/document-subtypes.mjs";
42
43
  /**
43
44
  * The note-type → document-subtype maps this toolchain ships.
44
45
  *
45
- * Two, since `hm3/` landed (#139) — and it joined this list rather than the
46
+ * Two, since `hm3/` landed — and it joined this list rather than the
46
47
  * claim table growing a second copy of the same fact, which is what the list
47
48
  * was for.
48
49
  *
@@ -72,7 +73,7 @@ export const KNOWN_DOCUMENT_SUBTYPE_MAPS = Object.freeze([
72
73
  * {@link KNOWN_DOCUMENT_SUBTYPE_MAPS} exists rather than a hand-kept table.
73
74
  *
74
75
  * It exists because an actor note publishes documentation like every other
75
- * system-bearing note (#337). `docEntryTypes` was `itemTypes` plus `macro` and
76
+ * system-bearing note. `docEntryTypes` was `itemTypes` plus `macro` and
76
77
  * the map types, which left a being as the one system-bearing note with no
77
78
  * `none` address — nothing a prose link could land on, since its only address
78
79
  * named the Actor. Composing that set needs to know which types are actors, and
@@ -111,7 +112,7 @@ export function subtypeMapFor(system) {
111
112
  * translation the *schema* check needs, and the reason it needs one.
112
113
  *
113
114
  * A schema artifact is keyed by document subtype; a field declaration is keyed
114
- * by note type. Those were the same string until #78 renamed three of them, and
115
+ * by note type. Those would be the same string but for three renames, and
115
116
  * a check that went on joining them by name simply stopped reporting on
116
117
  * `armorgear` — a warning that vanishes is worse than one that fires, because
117
118
  * nothing says it went.
@@ -13,7 +13,7 @@
13
13
 
14
14
  /**
15
15
  * **The per-system frontmatter block** — how one note feeds more than one game
16
- * system (#58).
16
+ * system.
17
17
  *
18
18
  * A note is system-agnostic. The only system-specific things it carries are the
19
19
  * properties *named after a system*, and within one:
@@ -21,7 +21,7 @@
21
21
  * | property | maps to |
22
22
  * | --- | --- |
23
23
  * | `<system>.system` | `document.system` — the DataModel schema, verbatim paths |
24
- * | `<system>.type` | `document.type` — the subtype (#79's discriminator) |
24
+ * | `<system>.type` | `document.type` — the subtype discriminator |
25
25
  * | `<system>.img` | `document.img` |
26
26
  * | `<system>.items` | `document.items` — actors only |
27
27
  * | `<system>.effects` | `document.effects` |
@@ -47,7 +47,7 @@
47
47
  *
48
48
  * 1. `S.system.<to>` — authored directly, wins outright;
49
49
  * 2. `S.<legacyKey>` — the legacy in-block position the corpus still writes,
50
- * kept until #126 moves it;
50
+ * kept until the corpus moves it;
51
51
  * 3. the shared top-level property the field **declares** as its source, which
52
52
  * may be a dotted path (`data.portrait`) rather than a sibling key;
53
53
  * 3b. for a `data.` source, the bare top-level key that container gathered it
@@ -56,15 +56,15 @@
56
56
  *
57
57
  * ## Steps 2 and 3 are two declarations, because they are two positions
58
58
  *
59
- * They used to be one: both were keyed on `name`, which was fine only while the
60
- * shared source and the in-block key were the same word. `data:` (#128) ended
59
+ * They are not one: keying both on `name` holds only while the
60
+ * shared source and the in-block key were the same word. `data:` ended
61
61
  * that — a shared source is now a path *into* a container, so `data.species`
62
62
  * and `species` are two spellings of two different places, and no single value
63
63
  * of `name` reached both. `name: "species"` read `hm3.species` and could not
64
64
  * see `data.species`; `name: "data.species"` read the shared source and could
65
65
  * not see `hm3.species`. Each yielded the field's **default** wherever only the
66
66
  * other position was authored — silently, since the field compiles and the
67
- * document is emitted with the value simply gone (#305).
67
+ * document is emitted with the value simply gone.
68
68
  *
69
69
  * That made every move into `data:` a flag day. Each of this package's other
70
70
  * retirements — `package:`, `image`, `archetype`, `relation` — works because
@@ -77,11 +77,11 @@
77
77
  * that declares one is mid-sweep by construction, which is what
78
78
  * {@link module:engine/field-spec.readsLegacyKey} reports on.
79
79
  *
80
- * ## Step 3 has a retiring position too, and it is derived (#332)
80
+ * ## Step 3 has a retiring position too, and it is derived
81
81
  *
82
82
  * `legacyKey` retires the *in-block* position, and for a while that looked like
83
83
  * the whole of what `data:` left behind. It is not. The facts `data:` holds
84
- * were not invented by it — #128 **gathered** them out of the note's open top
84
+ * were not invented by it — the sweep **gathered** them out of the note's open top
85
85
  * level, where `portrait:` sat beside `img:` and `shortcode:` — so a field that
86
86
  * declares `data.portrait` has two shared spellings to read, not one, and
87
87
  * reading only the current one is the same silent miss `legacyKey` exists to
@@ -112,7 +112,7 @@
112
112
  * It was not a harmless coincidence either, because step 3 answers **without**
113
113
  * applying `field.default` — only step 2 does — so an authored `title: null`
114
114
  * reached the field's coercion unguarded and shipped as the literal string
115
- * `"null"` in fifteen documents (#218).
115
+ * `"null"` in fifteen documents.
116
116
  *
117
117
  * So a field may declare `topLevelMeans`: what the top-level key of that name
118
118
  * means *instead*. Declaring it removes the whole shared level — step 3 and the
@@ -132,9 +132,8 @@
132
132
  *
133
133
  * A block is addressed by name, and the name arrives from the caller — a
134
134
  * system's own document-subtype map declares it (`DocumentSubtypeMap.block`).
135
- * That is the `engine/` ÷ `sohl/` line this package draws everywhere else
136
- * (#36): note-format knowledge here, game-system knowledge in the system's own
137
- * half.
135
+ * That is the `engine/` ÷ `sohl/` line this package draws everywhere else:
136
+ * note-format knowledge here, game-system knowledge in the system's own half.
138
137
  *
139
138
  * @module
140
139
  */
@@ -393,7 +392,7 @@ const DATA_PREFIX = "data.";
393
392
  /**
394
393
  * The bare top-level key a `data:`-sourced field is being swept off — step 3b.
395
394
  *
396
- * `data:` (#128) did not invent the facts it holds; it *gathered* them, out of
395
+ * `data:` did not invent the facts it holds; it *gathered* them, out of
397
396
  * the note's open top level where each was a sibling of `img` and `shortcode`.
398
397
  * So the retiring spelling of `data.portrait` is not a second declaration
399
398
  * anyone has to write — it is `portrait`, mechanically, and the same holds for
@@ -466,8 +465,8 @@ export function resolveFieldValue(field, fm, { block = "sohl" } = {}) {
466
465
 
467
466
  // 2. The legacy in-block position, keyed on `legacyKey` — the shared
468
467
  // source is a path into `data:` and the in-block key is a bare word, so
469
- // the two are declared separately (#305). Every note in every tree
470
- // writes here today, and will until #126 moves them; dropping it would
468
+ // the two are declared separately. Every note in every tree
469
+ // writes here today, and will until the corpus moves them; dropping it would
471
470
  // be a corpus migration disguised as a mechanism change.
472
471
  const declared = systemBlock(fm, block);
473
472
  const legacyKey = legacyKeyOf(field);
@@ -482,14 +481,14 @@ export function resolveFieldValue(field, fm, { block = "sohl" } = {}) {
482
481
 
483
482
  // 3. The shared property this field declares as its source — unless the
484
483
  // field declares that the top-level key of that name means something
485
- // else, in which case there is no shared position to read (#218).
484
+ // else, in which case there is no shared position to read.
486
485
  if (field.topLevelMeans === undefined) {
487
486
  const shared = getFrontmatter(fm, field.name, undefined);
488
487
  if (shared !== undefined) return { value: shared, from: "shared" };
489
488
 
490
489
  // 3b. The bare top-level key the `data:` source was gathered off — the
491
490
  // retiring *shared* position, exactly as `legacyKey` is the
492
- // retiring *in-block* one (#332). Without it a field declaring
491
+ // retiring *in-block* one. Without it a field declaring
493
492
  // `data.portrait` cannot see the `portrait:` every tree still
494
493
  // writes, and the miss arrives at the caller's `?? default` as an
495
494
  // ordinary absence: 646 `sohl-thalorna` beings compiled the generic
@@ -556,7 +555,7 @@ export function systemDataPaths(data, prefix = "") {
556
555
  *
557
556
  * It stayed invisible while those maps were authored *outside* `<system>.system`
558
557
  * and so were never walked. The moment the corpus moves them to the destination
559
- * (#126) every one of them lights up — 62 findings on `sohl-thalorna` alone,
558
+ * every one of them lights up — 62 findings on `sohl-thalorna` alone,
560
559
  * none of them a defect.
561
560
  *
562
561
  * So descent is conditional on the schema declaring something *beneath* the
@@ -621,7 +620,7 @@ function declaresChildren(declared, path) {
621
620
  * @param {object} options - Options.
622
621
  * @param {Iterable<string>} options.known - The keys this system declares on
623
622
  * top of the shared vocabulary: its generators, its toolchain keys, and —
624
- * until #126 moves them — the field names its notes still author in the
623
+ * until the corpus moves them — the field names its notes still author in the
625
624
  * block.
626
625
  * @returns {string[]} The unrecognized keys, in authored order.
627
626
  */
@@ -13,7 +13,7 @@
13
13
 
14
14
  /**
15
15
  * The closed registry of system ids, and the `none` that stands for no system
16
- * at all (#59).
16
+ * at all.
17
17
  *
18
18
  * A canonical address carries the system in a fixed position —
19
19
  * `harnadventures-none-being-grod` — so "which systems exist" is a question the
@@ -47,7 +47,7 @@
47
47
 
48
48
  // The one charset, read rather than restated — a system id is an address
49
49
  // segment like any other, and a second spelling of the pattern is how the
50
- // disagreements in #202/#203 happened.
50
+ // disagreements between them arise.
51
51
  import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs";
52
52
 
53
53
  /**
@@ -22,7 +22,7 @@
22
22
  * `[[type-shortcode#slug|Text]]` → `[Text](/section/slug/#slug)`
23
23
  * `[[#slug|Text]]` → `[Text](#slug)`
24
24
  *
25
- * **Every link is an address and carries a label** (#180). One written without
25
+ * **Every link is an address and carries a label**. One written without
26
26
  * a label addresses nothing and is reported — see
27
27
  * {@link unlabelledLinkMessage}, which states the rule for this build and the
28
28
  * pack build together.
@@ -34,22 +34,22 @@
34
34
  * Lives here rather than in a consumer so every package resolves a link the
35
35
  * same way. `sohl-thalorna` carried a forked copy of this as
36
36
  * `utils/site-wikilinks.mjs`, already 3 KB adrift from the original, which is
37
- * exactly the drift one rule with two implementations produces (#20).
37
+ * exactly the drift one rule with two implementations produces.
38
38
  */
39
39
 
40
40
  // How an address *parses* is the pack build's own rule, so the two builds
41
41
  // cannot drift apart on it: they disagreed once over the unlabelled hyphen
42
42
  // form, which the packs showed as a raw shortcode and the knowledgebase as a
43
- // name (#1409).
43
+ // name.
44
44
  import { readQualifier } from "./wikilinks.mjs";
45
45
  import { replaceOutsideCode } from "./code-fences.mjs";
46
46
  // The canonical `package-system-type-shortcode` key, so a package-qualified
47
47
  // address is matched the way a vendored manifest publishes it — by the
48
- // segments the target supplies, with the system wildcarded unless stated (#59).
48
+ // segments the target supplies, with the system wildcarded unless stated.
49
49
  import { canonicalKey, expandAddress, readCanonicalKey } from "./content-address.mjs";
50
50
  import { NO_SYSTEM } from "./systems.mjs";
51
51
  // The one rule about a link's shape both builds share: it carries a label, and
52
- // {@link unlabelledLinkMessage} is the one place that says so (#180).
52
+ // {@link unlabelledLinkMessage} is the one place that says so.
53
53
  import { unlabelledLinkMessage } from "./wikilink-syntax.mjs";
54
54
  // One slug rule for the whole build — see `./content-slug.mjs`. This module
55
55
  // carried a copy that dropped non-ASCII letters rather than transliterating
@@ -58,17 +58,16 @@ import { slugify } from "./content-slug.mjs";
58
58
 
59
59
  // Re-exported so a site build keeps one import path for the whole of link
60
60
  // resolution: the same rule that names a page also names an anchor within it.
61
+ /** KB heading/anchor slug: lowercase, non-alphanumerics to single hyphens. */
61
62
  export { slugify };
62
63
  import { authoredLabel, WIKILINK, isSamePage, parseWikilink } from "./wikilink-syntax.mjs";
63
64
 
64
- /** KB heading/anchor slug: lowercase, non-alphanumerics to single hyphens. */
65
-
66
65
  /**
67
66
  * The index key a **piped** target resolves to, or `null` when it does not
68
67
  * parse as an address at all.
69
68
  *
70
69
  * The KB index is keyed by the canonical `type/shortcode`, so a target written
71
- * in the hyphen separator — which is what the content tree authors (#1398)
70
+ * in the hyphen separator — which is what the content tree authors —
72
71
  * has to be rewritten to it before lookup. The target is read by the pack
73
72
  * build's own {@link readQualifier}, so recognising an address and resolving
74
73
  * one can never disagree: the two separators and the optional leading package
@@ -77,18 +76,22 @@ import { authoredLabel, WIKILINK, isSamePage, parseWikilink } from "./wikilink-s
77
76
  * It takes the **parsed** qualifier rather than the raw target because the
78
77
  * caller needs the parse for a second purpose: `unknown-type` and
79
78
  * `not-an-address` are different findings with different fixes, and only the
80
- * `reason` tells them apart (#184). Reading the target twice would let the two
79
+ * `reason` tells them apart. Reading the target twice would let the two
81
80
  * readings drift.
82
81
  *
83
82
  * The build indexes an item note under both `skill/climb` and `docskill/climb`,
84
83
  * and `contentTypes` carries both qualifiers, so either form finds the page.
85
84
  *
85
+ * @param {Map<string, object>} index - The page index to look in.
86
86
  * @param {object|null} read - From {@link readQualifier}.
87
- * @returns {string | null} The index key, or `null` when not an address.
87
+ * @param {string} contentPackage - The citing package, which an omitted package
88
+ * segment defaults from.
89
+ * @returns {object|undefined} The indexed page, or `undefined` when the target
90
+ * is not an address or nothing publishes it.
88
91
  */
89
92
  function lookupRead(index, read, contentPackage) {
90
93
  if (!read || read.reason) return undefined;
91
- // Every omitted segment defaults from where the link is written (#336)
94
+ // Every omitted segment defaults from where the link is written —
92
95
  // package from the citing package, system from the block, which on a page
93
96
  // body is `none` — so the target expands to exactly one canonical address
94
97
  // and this is a plain lookup. No filter, no single-hit rule, and no
@@ -104,7 +107,7 @@ function lookupRead(index, read, contentPackage) {
104
107
  // A local target may also be keyed by its short form. On the KB that is not
105
108
  // a second answer: an item note renders as **one page** which is its own
106
109
  // documentation, so `skill/climb`, `docskill/climb` and both canonical
107
- // addresses are all the same value (#1362) — the fallback cannot pick
110
+ // addresses are all the same value — the fallback cannot pick
108
111
  // differently, only earlier. It stays because an index built before the
109
112
  // canonical documentation key was added still carries the short one, and
110
113
  // because a page collision here is caught by the site index's own
@@ -127,10 +130,10 @@ function lookupRead(index, read, contentPackage) {
127
130
  * This is deliberately identical to the pack compiler's own `unresolvedLink`,
128
131
  * down to the class name and the `title` wording. One authored link renders on
129
132
  * two surfaces, and the two builds have drifted before over exactly this kind
130
- * of detail (#1409) — matching markup is what keeps a reader's cue the same in
133
+ * of detail — matching markup is what keeps a reader's cue the same in
131
134
  * a journal and on the page. Duplicated rather than imported only because the
132
135
  * function is not exported from `@heroiclands/package-build`; hoisting it there
133
- * is HeroicLands/content-build#13.
136
+ * is.
134
137
  *
135
138
  * The knowledgebase renders with `unsafe = true` (`kb/hugo.toml`), so raw HTML
136
139
  * in generated markdown reaches the page. That makes escaping obligatory: this
@@ -154,7 +157,7 @@ function unresolvedLink(text, target) {
154
157
  }
155
158
 
156
159
  /**
157
- * How a link to a **draft** note renders (#183).
160
+ * How a link to a **draft** note renders.
158
161
  *
159
162
  * A note tagged `draft` exists so a link into it is not dead, and nothing more.
160
163
  * Unmarked, a reader follows a promising link into an empty page and an author
@@ -173,8 +176,8 @@ function unresolvedLink(text, target) {
173
176
  * **Byte-identical with the pack build's copy** in `wikilinks.mjs`, down to the
174
177
  * class name and the `title` wording — one authored link renders on two
175
178
  * surfaces, and the two builds have drifted before over exactly this kind of
176
- * detail (#1409). Duplicated rather than imported for the same reason
177
- * {@link unresolvedLink} is; hoisting both is HeroicLands/content-build#13.
179
+ * detail. Duplicated rather than imported for the same reason
180
+ * {@link unresolvedLink} is; hoisting both is.
178
181
  *
179
182
  * The argument is already-built markup and is deliberately not escaped; the
180
183
  * *authored* text inside it was escaped, or made into a link, by the caller.
@@ -198,7 +201,7 @@ function draftLink(inner) {
198
201
  const FRONTMATTER_WIKILINK = new RegExp(WIKILINK.source, "g");
199
202
 
200
203
  /**
201
- * Every wikilink authored inside a frontmatter value (#1428).
204
+ * Every wikilink authored inside a frontmatter value.
202
205
  *
203
206
  * Wikilinks are resolved in a note's **body** — by {@link resolveWebWikilinks}
204
207
  * here, and by the pack compilers' `convertWikilinks` for Foundry. Frontmatter
@@ -267,19 +270,19 @@ function isPlainMap(value) {
267
270
  * `section/slug`), then in the vendored `ctx.foreign` manifests. A target is a
268
271
  * *partial* address: an omitted package means this package, an omitted system
269
272
  * is a wildcard, and exactly one match resolves — two are `ambiguous`. A link written
270
- * without a label addresses nothing at all and is reported as such (#180)
273
+ * without a label addresses nothing at all and is reported as such —
271
274
  * there is no second namespace left for it to name.
272
275
  *
273
276
  * Only a slash-qualified target reaches the raw key, which is what keeps
274
277
  * `section/slug` addressable without a page's own slug answering for it.
275
278
  *
276
- * **Every target that resolves nowhere fails the build** (#184), and is
279
+ * **Every target that resolves nowhere fails the build**, and is
277
280
  * classified into the vocabulary all three resolvers share — `unlabelled`,
278
281
  * `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`. Failures are
279
282
  * collected in `ctx.errors`, each carrying the authored `link` and its
280
283
  * `occurrence` so a caller can report the line and column it sits on.
281
284
  *
282
- * There used to be one exception: a hyphen-form address was let through while
285
+ * There is deliberately no exception letting a hyphen-form address through while
283
286
  * any linkable package had no vendored manifest, since a real cross-package
284
287
  * reference and a typo look identical from here. The pack compilers and the
285
288
  * link checker never made that allowance, so its only surviving effect was to
@@ -287,13 +290,13 @@ function isPlainMap(value) {
287
290
  * instead.
288
291
  *
289
292
  * A target that resolves nowhere still renders through {@link unresolvedLink}
290
- * rather than as bare prose (#1665): the author's text is kept, marked so a
293
+ * rather than as bare prose: the author's text is kept, marked so a
291
294
  * reader can see a link was intended. The marking and the failure are separate
292
295
  * jobs and always were — the mark is for whoever reads the page a *previous*
293
296
  * build emitted, the failure is for the author of this one.
294
297
  *
295
298
  * A target that **resolved** to an entry with no page is not this case and is
296
- * not marked: a pack-only package (#1516) publishes Foundry addresses and no
299
+ * not marked: a pack-only package publishes Foundry addresses and no
297
300
  * web pages, so the author wrote a real address and there is simply nothing to
298
301
  * link to.
299
302
  *
@@ -302,7 +305,7 @@ function isPlainMap(value) {
302
305
  * foreign, type, errors, src, file }`.
303
306
  * `packages` is every package an address may name, without which the leading
304
307
  * package segment of a canonical address reads as an unknown type; `foreign`
305
- * is the cross-package manifest index (#1446). `src` is the page's display
308
+ * is the cross-package manifest index. `src` is the page's display
306
309
  * path and `file` the source file a diagnostic should name — absent, `src`
307
310
  * stands in.
308
311
  * @returns {string} The body with wikilinks rewritten.
@@ -311,7 +314,7 @@ export function resolveWebWikilinks(body, ctx) {
311
314
  // How many times each authored link has been seen, so two identical links
312
315
  // on one page are located at their own positions in the source file — the
313
316
  // same counting the checker does, and what turns a finding into a
314
- // `file:line:column:` diagnostic rather than a note-wide one (#17, #184).
317
+ // `file:line:column:` diagnostic rather than a note-wide one.
315
318
  const seen = new Map();
316
319
  /**
317
320
  * Records a finding, and returns the marked-up link it renders as.
@@ -338,16 +341,16 @@ export function resolveWebWikilinks(body, ctx) {
338
341
  };
339
342
 
340
343
  // Code is verbatim: a `[[…]]` inside a code fence, an indented block or an
341
- // inline span is source text, not a link (#1505).
344
+ // inline span is source text, not a link.
342
345
  return replaceOutsideCode(body, WIKILINK, (all, rawInner) => {
343
346
  const parsed = parseWikilink(rawInner);
344
347
  const { target, anchor, display } = parsed;
345
348
  // An empty label is not a label: `[[x|]]` addresses the target and
346
- // shows its name, so `""` falls through to the same place `null` does
347
- // (#113). One reading, from {@link authoredLabel}.
349
+ // shows its name, so `""` falls through to the same place `null` does.
350
+ // One reading, from {@link authoredLabel}.
348
351
  const label = authoredLabel({ display });
349
352
 
350
- // **Every link carries a label** (#180). Without one there is nothing
353
+ // **Every link carries a label**. Without one there is nothing
351
354
  // to resolve against — the alias namespace a bare `[[Text]]` named is
352
355
  // retired — and nothing to show either, a shortcode being an address
353
356
  // rather than prose. Reported before the same-page form, because the
@@ -362,7 +365,7 @@ export function resolveWebWikilinks(body, ctx) {
362
365
  return `[${label ?? anchor}](#${slugify(anchor)})`;
363
366
  }
364
367
 
365
- // The canonical separator (#1398) has to be resolved, not merely
368
+ // The canonical separator has to be resolved, not merely
366
369
  // recognised. `null` here means the target is not an address at all,
367
370
  // which is a defect: there is no other namespace to try.
368
371
  const read = readQualifier(target, ctx.contentTypes ?? new Set(), ctx.packages);
@@ -374,10 +377,10 @@ export function resolveWebWikilinks(body, ctx) {
374
377
  // a page's bare slug cannot answer for an address.
375
378
  (rawKey.includes("/") ? ctx.index.get(rawKey) : undefined) ??
376
379
  // A manifest entry carries the same `{ url, name }` shape as a
377
- // local one (#1446), so a cross-package hit needs no special case
380
+ // local one, so a cross-package hit needs no special case
378
381
  // below. Local wins: a live build is authoritative and a vendored
379
382
  // manifest can only be staler.
380
- // A short form names *this* package (#336), so it never reaches a
383
+ // A short form names *this* package, so it never reaches a
381
384
  // vendored manifest; only a fully qualified address does.
382
385
  (ctx.foreign && read?.package ?
383
386
  lookupRead(ctx.foreign, read, ctx.contentPackage)
@@ -387,8 +390,8 @@ export function resolveWebWikilinks(body, ctx) {
387
390
  // shortcode is not display text), so the document's **current**
388
391
  // name stands in and a rename shows at every citation.
389
392
  const text = label ?? hit.name;
390
- // A pack-only package publishes Foundry addresses and no pages
391
- // (#1516), so its entries carry no `path` and resolve to no URL.
393
+ // A pack-only package publishes Foundry addresses and no pages,
394
+ // so its entries carry no `path` and resolve to no URL.
392
395
  // The address is real — this is not a typo and must not fail the
393
396
  // build — but there is nothing on the web to point at, so the
394
397
  // reader gets the text and no href. Emitting the href anyway is
@@ -396,8 +399,8 @@ export function resolveWebWikilinks(body, ctx) {
396
399
  // as a link and goes nowhere.
397
400
  const link =
398
401
  hit.url ? `[${text}](${anchor ? `${hit.url}#${slugify(anchor)}` : hit.url})` : text;
399
- // A link into a note that exists but is not written renders marked
400
- // (#183). Presentation only — the href above is unchanged, and a
402
+ // A link into a note that exists but is not written renders marked.
403
+ // Presentation only — the href above is unchanged, and a
401
404
  // `[[#anchor]]` self-link is not marked because the reader is
402
405
  // already on the page it would be telling them about.
403
406
  return hit.draft ? draftLink(link) : link;
@@ -413,7 +416,7 @@ export function resolveWebWikilinks(body, ctx) {
413
416
  // it parses as an address.)
414
417
  const siteAddress = prefix !== null && ctx.sections.has(prefix);
415
418
 
416
- // **An address resolving nowhere is a failure, unconditionally** (#184).
419
+ // **An address resolving nowhere is a failure, unconditionally**.
417
420
  //
418
421
  // It was gated on a manifest-completeness check — while any linkable package was
419
422
  // invisible here, `Rules/Bestiary.md` addressing `being-grkrahk` in the
@@ -443,7 +446,7 @@ export function resolveWebWikilinks(body, ctx) {
443
446
  : read?.reason === "unknown-type" ? "unknown-type"
444
447
  // Every link is an address, and this is not one. Distinct from
445
448
  // a dead address, because the fix is different: a name has to
446
- // become an address, not be corrected (#180).
449
+ // become an address, not be corrected.
447
450
  : "not-an-address";
448
451
 
449
452
  // Whether or not it failed the build, the link renders marked: the
@@ -29,7 +29,7 @@
29
29
  * The web side's pattern omitted `\n` from the excluded set, so an unclosed
30
30
  * bracket consumed everything up to the next `]]` anywhere in the document —
31
31
  * the same shape of corruption a hand-rolled code-fence regex caused on the one
32
- * page whose subject is link syntax (SoHL#1665). It was also internally
32
+ * page whose subject is link syntax. It was also internally
33
33
  * inconsistent: its *frontmatter* scan excluded newlines while its body scan did
34
34
  * not.
35
35
  *
@@ -45,7 +45,7 @@
45
45
  * ({@link LINK_FINDING_REASONS}) and the message each one reports through
46
46
  * ({@link linkFindingMessage}). Three builds read one authored link; an author
47
47
  * meets whichever ran first, and a consumer switching on a `reason` should not
48
- * be switching on which build produced it (#184).
48
+ * be switching on which build produced it.
49
49
  *
50
50
  * @module
51
51
  */
@@ -72,7 +72,7 @@ export const WIKILINK = /\[\[([^\]\n]+)\]\]/g;
72
72
  * @property {string|null} display - The text after `|`, or `null` when the link
73
73
  * is unlabelled. `null` and `""` differ: an author may write `[[x|]]`.
74
74
  * @property {boolean} labelled - Whether a `|` was present at all. A link
75
- * without one addresses nothing and is a finding (#180) — see
75
+ * without one addresses nothing and is a finding — see
76
76
  * {@link unlabelledLinkMessage} — so every reader has to be able to ask.
77
77
  */
78
78
 
@@ -111,12 +111,12 @@ export function parseWikilink(rawInner) {
111
111
  * `display: null` does. The two are still distinguishable through
112
112
  * {@link ParsedWikilink.labelled}, which is the thing that genuinely differs:
113
113
  * `[[x|]]` is labelled and `[[x]]` is not, and only the first addresses
114
- * anything (#180).
114
+ * anything.
115
115
  *
116
116
  * Stated here because the two resolvers had already drawn the line in two
117
117
  * places and drawn it differently: the packs tested falsiness and were right,
118
118
  * the web tested `??` — which falls through on `null` only — and emitted
119
- * `[](/url/)`, a link with no clickable text, through every build (#113). That
119
+ * `[](/url/)`, a link with no clickable text, through every build. That
120
120
  * is the same drift this module exists to prevent, in the case its own
121
121
  * {@link ParsedWikilink} docstring calls out. One reading, one place.
122
122
  *
@@ -136,12 +136,12 @@ export function authoredLabel({ display }) {
136
136
  * the form to write rather than a value to correct: there is no value that
137
137
  * makes an unlabelled link resolve.
138
138
  *
139
- * **Why there is nothing left for a bare link to mean** (#180). The pipe used
139
+ * **Why there is nothing left for a bare link to mean**. The pipe used
140
140
  * to select between two namespaces — address and alias — and the alias one was
141
141
  * empty in practice: across 8,305 wikilinks in three content trees, not one
142
142
  * bare `[[Alias]]` resolved to a note. What the index it looked up in *did* do
143
143
  * was fold every note's `name.full` into itself, which forbade two notes of a
144
- * type from sharing a display name (#179). So the namespace is gone, every
144
+ * type from sharing a display name. So the namespace is gone, every
145
145
  * link is an address, and an address needs the pipe that says so.
146
146
  *
147
147
  * The **link part may still be an anchor**: `[[#slug|Text]]` addresses a
@@ -160,16 +160,16 @@ export function unlabelledLinkMessage(target) {
160
160
  }
161
161
 
162
162
  /**
163
- * Every way a link can fail, named once for all three resolvers (#184).
163
+ * Every way a link can fail, named once for all three resolvers.
164
164
  *
165
165
  * A link is read in three places — the checker (`content-links.mjs`), the pack
166
166
  * compilers (`wikilinks.mjs`) and the web resolver (`web-wikilinks.mjs`) — and
167
- * each used to name the failures in its own words. `unknown` in one was
167
+ * each would otherwise name the failures in its own words. `unknown` in one is
168
168
  * `unresolved` in another and `broken type/shortcode` in the third, so a
169
169
  * consumer switching on a `reason` was switching on which build had produced
170
170
  * it. The set is closed and lives here, beside the syntax the three share.
171
171
  *
172
- * - `unlabelled` — no `|`, so the link addresses nothing (#180).
172
+ * - `unlabelled` — no `|`, so the link addresses nothing.
173
173
  * - `not-an-address` — labelled, but the target does not parse as an address.
174
174
  * - `unknown-type` — definitely qualified, but names no type this build knows.
175
175
  * - `unresolved` — parses as an address, and nothing publishes it.
@@ -198,12 +198,12 @@ export const LINK_FINDING_REASONS = Object.freeze(
198
198
  * package publishing it is not a declared dependency — or is one whose index
199
199
  * has not been fetched — and the link itself looks identical in every case.
200
200
  *
201
- * This used to be a **warning** in the checker and, in the site build, nothing
201
+ * As a **warning** in the checker and, in the site build, nothing
202
202
  * at all until every linkable package's manifest was accounted for — on the
203
203
  * reasoning that a bare `[[Name]]` might be a placeholder for a note nobody had
204
- * written yet. That reasoning was a property of the bare form, which is retired
205
- * (#180); the intent behind it now has a real spelling, a `draft`-tagged note
206
- * that exists and resolves and renders marked (#183). So an address naming no
204
+ * written yet. That reasoning was a property of the bare form, which is retired;
205
+ * the intent behind it now has a real spelling, a `draft`-tagged note
206
+ * that exists and resolves and renders marked. So an address naming no
207
207
  * note is a typo or an omission, both want fixing, and all three builds say so.
208
208
  *
209
209
  * @param {string} target - The address as authored, named in the message.
@@ -226,9 +226,9 @@ export function unresolvedAddressMessage(target) {
226
226
  * message names the claimants so the author can choose between them without
227
227
  * going looking.
228
228
  *
229
- * **The correction is the canonical form**, all four segments (#59). Omission
229
+ * **The correction is the canonical form**, all four segments. Omission
230
230
  * runs strictly left to right, so there is no `package-type-shortcode` to offer
231
- * — naming a package means naming the system before the type. That used to be
231
+ * — naming a package means naming the system before the type. That would be
232
232
  * the correction here, back when a written target could state three segments at
233
233
  * most; the grammar is positional now, and a three-segment target names a
234
234
  * *system*, not a package.