@heroiclands/package-build 10.0.1 → 11.0.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 (44) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/CONTENT.md +218 -71
  3. package/MIGRATING.md +64 -0
  4. package/bin/content-build.mjs +59 -75
  5. package/docs/content-format.md +90 -67
  6. package/engine/base-compiler.mjs +7 -1
  7. package/engine/content-address.mjs +71 -18
  8. package/engine/content-format-check.mjs +1 -1
  9. package/engine/content-links.mjs +93 -112
  10. package/engine/content-lint.mjs +14 -10
  11. package/engine/content-slug.mjs +39 -105
  12. package/engine/diagnostics.mjs +16 -2
  13. package/engine/frontmatter-lint.mjs +26 -13
  14. package/engine/helpers.mjs +31 -68
  15. package/engine/homepage.mjs +131 -86
  16. package/engine/index.mjs +2 -5
  17. package/engine/manifest-emit.mjs +23 -4
  18. package/engine/note-vocabulary.mjs +58 -1
  19. package/engine/retired-fields.mjs +117 -6
  20. package/engine/site-build.mjs +182 -59
  21. package/engine/site-index.mjs +57 -102
  22. package/engine/web-wikilinks.mjs +183 -127
  23. package/engine/wikilink-syntax.mjs +174 -34
  24. package/engine/wikilinks.mjs +159 -117
  25. package/package.json +1 -1
  26. package/types/engine/base-compiler.d.mts +1 -1
  27. package/types/engine/content-address.d.mts +46 -14
  28. package/types/engine/content-links.d.mts +13 -17
  29. package/types/engine/content-slug.d.mts +11 -48
  30. package/types/engine/diagnostics.d.mts +14 -1
  31. package/types/engine/helpers.d.mts +4 -3
  32. package/types/engine/homepage.d.mts +96 -60
  33. package/types/engine/index.d.mts +0 -1
  34. package/types/engine/note-vocabulary.d.mts +43 -0
  35. package/types/engine/retired-fields.d.mts +78 -1
  36. package/types/engine/site-build.d.mts +70 -17
  37. package/types/engine/site-index.d.mts +19 -21
  38. package/types/engine/web-wikilinks.d.mts +29 -28
  39. package/types/engine/wikilink-syntax.d.mts +126 -40
  40. package/types/engine/wikilinks.d.mts +29 -24
  41. package/engine/abbreviations.mjs +0 -0
  42. package/engine/alias-index.mjs +0 -153
  43. package/types/engine/abbreviations.d.mts +0 -44
  44. package/types/engine/alias-index.d.mts +0 -122
@@ -34,7 +34,18 @@
34
34
  * not.
35
35
  *
36
36
  * So the parse lives here, once. A resolver receives the parts and decides only
37
- * what it is actually for: which address space the target belongs to.
37
+ * what it is actually for: which document the address names.
38
+ *
39
+ * The one rule about a link's *shape* that both resolvers share also lives here
40
+ * — **a link carries a label** ({@link unlabelledLinkMessage}) — because the
41
+ * two used to state it in their own words and the author met whichever ran
42
+ * first.
43
+ *
44
+ * For the same reason, so does the **vocabulary of link findings**
45
+ * ({@link LINK_FINDING_REASONS}) and the message each one reports through
46
+ * ({@link linkFindingMessage}). Three builds read one authored link; an author
47
+ * meets whichever ran first, and a consumer switching on a `reason` should not
48
+ * be switching on which build produced it (#184).
38
49
  *
39
50
  * @module
40
51
  */
@@ -55,14 +66,14 @@ export const WIKILINK = /\[\[([^\]\n]+)\]\]/g;
55
66
  * @typedef {object} ParsedWikilink
56
67
  * @property {string} inner - The whole interior, unescaped and trimmed. What an
57
68
  * unlabelled link displays, anchor and all.
58
- * @property {string} target - What is linked to: an address, an alias, or `""`
59
- * for a link to a section of the same page.
69
+ * @property {string} target - What is linked to: an address, or `""` for a
70
+ * link to a section of the same page.
60
71
  * @property {string} anchor - The `#section` slug, `""` when there is none.
61
72
  * @property {string|null} display - The text after `|`, or `null` when the link
62
73
  * is unlabelled. `null` and `""` differ: an author may write `[[x|]]`.
63
- * @property {boolean} labelled - Whether a `|` was present at all. What an
64
- * unlabelled link shows depends on whether its target read as an address
65
- * (SoHL#1409), so the caller needs to know.
74
+ * @property {boolean} labelled - Whether a `|` was present at all. A link
75
+ * without one addresses nothing and is a finding (#180) see
76
+ * {@link unlabelledLinkMessage} — so every reader has to be able to ask.
66
77
  */
67
78
 
68
79
  /**
@@ -98,8 +109,9 @@ export function parseWikilink(rawInner) {
98
109
  * means "address this target, and show the target's own name" — so `display:
99
110
  * ""` has to read as *absent* everywhere a fallback is chosen, exactly as
100
111
  * `display: null` does. The two are still distinguishable through
101
- * {@link ParsedWikilink.labelled}, which is the thing that genuinely differs
102
- * and which #1409 depends on.
112
+ * {@link ParsedWikilink.labelled}, which is the thing that genuinely differs:
113
+ * `[[x|]]` is labelled and `[[x]]` is not, and only the first addresses
114
+ * anything (#180).
103
115
  *
104
116
  * Stated here because the two resolvers had already drawn the line in two
105
117
  * places and drawn it differently: the packs tested falsiness and were right,
@@ -117,33 +129,161 @@ export function authoredLabel({ display }) {
117
129
  }
118
130
 
119
131
  /**
120
- * Which namespace a link resolves in: the **address** space, or the **alias**
121
- * space.
122
- *
123
- * **The pipe decides, and nothing else does** (#131). `[[x|…]]` is an address,
124
- * parsed by the address grammar; `[[x]]` is an alias, looked up within the
125
- * source note's own type. Neither falls back to the other.
126
- *
127
- * Both resolvers used to decide by *shape* instead try the address, fall
128
- * back to the aliaswhich had three costs. An author could not say which
129
- * they meant, so a note whose **name** looked like an address (`Grukar-ahk`)
130
- * was read as one, and a genuine address that resolved nowhere silently became
131
- * a name lookup and reported nothing. And a positional address grammar could
132
- * not split a target confidently until it had first ruled out every note name
133
- * in the corpus.
134
- *
135
- * An **empty** label is still a pipe: `[[x|]]` is an address that renders the
136
- * target's *current* name, so a rename shows at every citation with no link
137
- * edited. That is why this reads {@link ParsedWikilink.labelled} and not
138
- * {@link authoredLabel} — the two answer different questions, and only one of
139
- * them is about namespaces.
140
- *
141
- * @param {{labelled: boolean}} parsed - A parsed wikilink, or anything
142
- * carrying its `labelled`.
143
- * @returns {boolean} True when the target is an address.
132
+ * What an author writing an unlabelled link is told, in one place.
133
+ *
134
+ * Shared by the link checker, the pack compilers and the web resolver, because
135
+ * an author meets whichever runs first and they should read the same. It names
136
+ * the form to write rather than a value to correct: there is no value that
137
+ * makes an unlabelled link resolve.
138
+ *
139
+ * **Why there is nothing left for a bare link to mean** (#180). The pipe used
140
+ * to select between two namespaces address and alias and the alias one was
141
+ * empty in practice: across 8,305 wikilinks in three content trees, not one
142
+ * bare `[[Alias]]` resolved to a note. What the index it looked up in *did* do
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
145
+ * link is an address, and an address needs the pipe that says so.
146
+ *
147
+ * The **link part may still be an anchor**: `[[#slug|Text]]` addresses a
148
+ * section of the page it is written on. It is the label that is required, not
149
+ * a target.
150
+ *
151
+ * @param {string} target - The target as authored, named in the message.
152
+ * @returns {string} The message, unpunctuated at the end as a finding is.
153
+ */
154
+ export function unlabelledLinkMessage(target) {
155
+ return (
156
+ `wikilink [[${target}]] carries no label, so it addresses nothing — ` +
157
+ `write [[type-shortcode|Text]]. Every link is an address; the bare ` +
158
+ `[[Name]] form named an alias, and the alias namespace is retired`
159
+ );
160
+ }
161
+
162
+ /**
163
+ * Every way a link can fail, named once for all three resolvers (#184).
164
+ *
165
+ * A link is read in three places — the checker (`content-links.mjs`), the pack
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
168
+ * `unresolved` in another and `broken type/shortcode` in the third, so a
169
+ * consumer switching on a `reason` was switching on which build had produced
170
+ * it. The set is closed and lives here, beside the syntax the three share.
171
+ *
172
+ * - `unlabelled` — no `|`, so the link addresses nothing (#180).
173
+ * - `not-an-address` — labelled, but the target does not parse as an address.
174
+ * - `unknown-type` — definitely qualified, but names no type this build knows.
175
+ * - `unresolved` — parses as an address, and nothing publishes it.
176
+ * - `ambiguous` — more than one package publishes the short address.
177
+ * - `unknown-anchor` — the address resolved, the `#section` it names did not.
178
+ *
179
+ * @type {ReadonlySet<string>}
180
+ */
181
+ export const LINK_FINDING_REASONS = Object.freeze(
182
+ new Set([
183
+ "unlabelled",
184
+ "not-an-address",
185
+ "unknown-type",
186
+ "unresolved",
187
+ "ambiguous",
188
+ "unknown-anchor",
189
+ ]),
190
+ );
191
+
192
+ /**
193
+ * What an author writing an address that resolves to nothing is told.
194
+ *
195
+ * **Both corrections, because the author cannot tell which applies.** An
196
+ * address lands nowhere either because the shortcode is wrong or because the
197
+ * package publishing it has no manifest vendored here, and the link itself
198
+ * looks identical in the two cases.
199
+ *
200
+ * This used to be a **warning** in the checker and, in the site build, nothing
201
+ * at all until every linkable package had vendored a manifest — on the
202
+ * reasoning that a bare `[[Name]]` might be a placeholder for a note nobody had
203
+ * written yet. That reasoning was a property of the bare form, which is retired
204
+ * (#180); the intent behind it now has a real spelling, a `draft`-tagged note
205
+ * that exists and resolves and renders marked (#183). So an address naming no
206
+ * note is a typo or an omission, both want fixing, and all three builds say so.
207
+ *
208
+ * @param {string} target - The address as authored, named in the message.
209
+ * @returns {string} The message, unpunctuated at the end as a finding is.
210
+ */
211
+ export function unresolvedAddressMessage(target) {
212
+ return (
213
+ `address [[${target}]] resolves to no note — no package publishes ` +
214
+ `it. Fix the shortcode, or vendor the link manifest of the package ` +
215
+ `that does`
216
+ );
217
+ }
218
+
219
+ /**
220
+ * What an author writing a short address more than one package publishes is
221
+ * told.
222
+ *
223
+ * There is no defensible way to pick one, and the correction is mechanical:
224
+ * write the package-qualified form. So it fails rather than warning, and the
225
+ * message names the claimants so the author can choose between them without
226
+ * going looking.
227
+ *
228
+ * @param {string} target - The address as authored.
229
+ * @param {Iterable<string>} [packages] - The packages that publish it.
230
+ * @returns {string} The message.
231
+ */
232
+ export function ambiguousAddressMessage(target, packages = []) {
233
+ const named = [...packages].filter(Boolean).sort();
234
+ return (
235
+ `address [[${target}]] is published by ` +
236
+ (named.length ? `${named.join(" and ")}` : `more than one package`) +
237
+ `, so it names neither — write the package-qualified ` +
238
+ `[[package-type-shortcode|Text]]`
239
+ );
240
+ }
241
+
242
+ /**
243
+ * The message for one link finding, whichever resolver found it.
244
+ *
245
+ * The single table the checker, the pack compilers and the web resolver all
246
+ * report through, so one authored link cannot get three different explanations
247
+ * of the same mistake depending on which build the author ran first. Callers
248
+ * add their own context around it — the note's name, the file it sits in — and
249
+ * never their own wording for the defect.
250
+ *
251
+ * @param {object} finding
252
+ * @param {string} finding.reason - One of {@link LINK_FINDING_REASONS}.
253
+ * @param {string} finding.target - The link target as authored.
254
+ * @param {Iterable<string>} [finding.packages] - For `ambiguous`, the
255
+ * claimants.
256
+ * @param {string} [finding.anchor] - For `unknown-anchor`, the section named.
257
+ * @returns {string} The message.
258
+ * @throws {Error} On a reason outside the closed set — a resolver inventing one
259
+ * would otherwise report a link with no explanation at all.
144
260
  */
145
- export function resolvesAsAddress({ labelled }) {
146
- return Boolean(labelled);
261
+ export function linkFindingMessage({ reason, target, packages, anchor }) {
262
+ switch (reason) {
263
+ case "unlabelled":
264
+ return unlabelledLinkMessage(target);
265
+ case "not-an-address":
266
+ return (
267
+ `"${target}" is not an address — the "|" says one was meant, ` +
268
+ `so write [[type-shortcode|Text]]`
269
+ );
270
+ case "unknown-type":
271
+ return `address [[${target}]] names no known content type`;
272
+ case "ambiguous":
273
+ return ambiguousAddressMessage(target, packages ?? []);
274
+ case "unknown-anchor":
275
+ return (
276
+ `address [[${target}]] resolves, but no section "#${anchor ?? ""}" ` +
277
+ `is published for it`
278
+ );
279
+ case "unresolved":
280
+ return unresolvedAddressMessage(target);
281
+ default:
282
+ throw new Error(
283
+ `linkFindingMessage: "${reason}" is not one of ` +
284
+ `${[...LINK_FINDING_REASONS].join(", ")}`,
285
+ );
286
+ }
147
287
  }
148
288
 
149
289
  /**