@heroiclands/package-build 9.0.0 → 10.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.
- package/CHANGELOG.md +694 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +437 -7
- package/content-config.mjs +259 -28
- package/docs/content-format.md +1418 -0
- package/engine/address-charset.mjs +62 -0
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +194 -4
- package/engine/content-address.mjs +4 -4
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +132 -56
- package/engine/content-lint.mjs +8 -1
- package/engine/diagnostics.mjs +33 -0
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-spec.mjs +49 -43
- package/engine/frontmatter-lint.mjs +343 -27
- package/engine/generate.mjs +32 -4
- package/engine/helpers.mjs +41 -29
- package/engine/ids.mjs +19 -1
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -5
- package/engine/kb-manifest.mjs +36 -7
- package/engine/map-notes.mjs +34 -18
- package/engine/note-claims.mjs +383 -0
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +39 -22
- package/engine/pack-router.mjs +17 -6
- package/engine/prose-lint.mjs +55 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scenes.mjs +19 -1
- package/engine/schema-check.mjs +347 -3
- package/engine/site-build.mjs +1 -1
- package/engine/site-index.mjs +38 -21
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +112 -80
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +67 -51
- package/package.json +6 -2
- package/sohl/actors.mjs +249 -36
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +3 -0
- package/sohl/items.mjs +110 -14
- package/sohl/note-schemas.mjs +11 -7
- package/types/content-config.d.mts +48 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +14 -1
- package/types/engine/helpers.d.mts +21 -13
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -17,10 +17,15 @@
|
|
|
17
17
|
* The same authored links the pack compilers turn into Foundry `@UUID` enrichers
|
|
18
18
|
* (see `./wikilinks.mjs`) become site-local hrefs here:
|
|
19
19
|
*
|
|
20
|
-
* `[[type
|
|
20
|
+
* `[[type-shortcode|Text]]` → `[Text](/section/slug/)`
|
|
21
|
+
* `[[type-shortcode|]]` → the same, showing the target's own name
|
|
21
22
|
* `[[Text]]` → the same, via a type-scoped alias
|
|
22
|
-
* `[[type
|
|
23
|
-
* `[[#slug|Text]]`
|
|
23
|
+
* `[[type-shortcode#slug|Text]]` → `[Text](/section/slug/#slug)`
|
|
24
|
+
* `[[#slug|Text]]` → `[Text](#slug)`
|
|
25
|
+
*
|
|
26
|
+
* **The pipe decides which namespace a target belongs to** (#131), with no
|
|
27
|
+
* fallback either way — see {@link resolvesAsAddress}, which states the rule
|
|
28
|
+
* for this build and the pack build together.
|
|
24
29
|
*
|
|
25
30
|
* The KB *section* is not always the type: prose pages (`type: doc`) route by
|
|
26
31
|
* their `category`, so `doc/quickstart` lands on `/user-guide/sohl-quickstart/`.
|
|
@@ -32,12 +37,21 @@
|
|
|
32
37
|
* exactly the drift one rule with two implementations produces (#20).
|
|
33
38
|
*/
|
|
34
39
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
40
|
+
// How an address *parses* is the pack build's own rule, so the two builds
|
|
41
|
+
// cannot drift apart on it: they disagreed once over the unlabelled hyphen
|
|
42
|
+
// form, which the packs showed as a raw shortcode and the knowledgebase as a
|
|
43
|
+
// name (#1409).
|
|
39
44
|
import { readQualifier } from "./wikilinks.mjs";
|
|
40
45
|
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
46
|
+
// The canonical `package-type-shortcode` key, so a package-qualified address
|
|
47
|
+
// is looked up the way a vendored manifest publishes it.
|
|
48
|
+
import { canonicalKey } from "./kb-manifest.mjs";
|
|
49
|
+
// The alias half of the two namespaces — the key rule, shared with the pack
|
|
50
|
+
// build and the link checker (#131).
|
|
51
|
+
import { aliasKey } from "./alias-index.mjs";
|
|
52
|
+
// Which namespace a target belongs to. The pipe decides, and this is the one
|
|
53
|
+
// place that says so.
|
|
54
|
+
import { resolvesAsAddress } from "./wikilink-syntax.mjs";
|
|
41
55
|
// One slug rule for the whole build — see `./content-slug.mjs`. This module
|
|
42
56
|
// carried a copy that dropped non-ASCII letters rather than transliterating
|
|
43
57
|
// them, so a link to a heading named `Kûrbúl Helm` pointed at `#k-rb-l-helm`.
|
|
@@ -51,43 +65,33 @@ import { authoredLabel, WIKILINK, isSamePage, parseWikilink } from "./wikilink-s
|
|
|
51
65
|
/** KB heading/anchor slug: lowercase, non-alphanumerics to single hyphens. */
|
|
52
66
|
|
|
53
67
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* Delegates to the pack build's {@link readQualifier} so one rule serves both
|
|
58
|
-
* builds. A `reason` is as much an address as a resolved qualifier is — the
|
|
59
|
-
* target is qualified either way, it just names no known type — and the caller
|
|
60
|
-
* only ever asks this of a target that already resolved.
|
|
61
|
-
*
|
|
62
|
-
* @param {string} target - The link target, anchor already removed.
|
|
63
|
-
* @param {Set<string>} [contentTypes] - Every content type the KB build saw.
|
|
64
|
-
* @returns {boolean} `true` when the target is an address.
|
|
65
|
-
*/
|
|
66
|
-
function isAddress(target, contentTypes) {
|
|
67
|
-
return readQualifier(target, contentTypes ?? new Set()) !== null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The `type/shortcode` index key a qualified target resolves to, or `null`.
|
|
68
|
+
* The index key a **piped** target resolves to, or `null` when it does not
|
|
69
|
+
* parse as an address at all.
|
|
72
70
|
*
|
|
73
71
|
* The KB index is keyed by the canonical `type/shortcode`, so a target written
|
|
74
|
-
* in the hyphen separator — which is what the
|
|
75
|
-
* rewritten to it before lookup. Uses the
|
|
76
|
-
* {@link
|
|
77
|
-
* disagree: the
|
|
78
|
-
*
|
|
72
|
+
* in the hyphen separator — which is what the content tree authors (#1398) —
|
|
73
|
+
* has to be rewritten to it before lookup. Uses the pack build's own
|
|
74
|
+
* {@link readQualifier}, so recognising an address and resolving one can never
|
|
75
|
+
* disagree: the two separators and the optional leading package segment are
|
|
76
|
+
* stated once, there.
|
|
79
77
|
*
|
|
80
78
|
* The build indexes an item note under both `skill/climb` and `docskill/climb`,
|
|
81
79
|
* and `contentTypes` carries both qualifiers, so either form finds the page.
|
|
82
80
|
*
|
|
83
81
|
* @param {string} target - The link target, anchor already removed.
|
|
84
82
|
* @param {Set<string>} [contentTypes] - Every content type the KB build saw.
|
|
85
|
-
* @
|
|
83
|
+
* @param {Set<string>} [packages] - Every package an address may name.
|
|
84
|
+
* @returns {string | null} The index key, or `null` when not an address.
|
|
86
85
|
*/
|
|
87
|
-
function qualifiedKey(target, contentTypes) {
|
|
88
|
-
const read = readQualifier(target, contentTypes ?? new Set());
|
|
86
|
+
function qualifiedKey(target, contentTypes, packages) {
|
|
87
|
+
const read = readQualifier(target, contentTypes ?? new Set(), packages);
|
|
89
88
|
if (!read || read.reason) return null;
|
|
90
|
-
|
|
89
|
+
// A package-qualified address keeps its package: the canonical key is what
|
|
90
|
+
// a vendored manifest publishes, and dropping the segment would resolve
|
|
91
|
+
// another package's address against this one's short key.
|
|
92
|
+
return read.package ?
|
|
93
|
+
canonicalKey(read.package, read.type, read.shortcode)
|
|
94
|
+
: `${read.type}/${read.shortcode}`.toLowerCase();
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
/**
|
|
@@ -203,18 +207,27 @@ function isPlainMap(value) {
|
|
|
203
207
|
/**
|
|
204
208
|
* Rewrites the wikilinks in a markdown body as KB-local markdown links.
|
|
205
209
|
*
|
|
206
|
-
* A target is looked up case-insensitively
|
|
207
|
-
*
|
|
208
|
-
* directory and `category` play no part — then in the KB-wide `ctx.index` (keyed
|
|
209
|
-
* by the unambiguous `section/slug` and `type/shortcode`, plus name/filename/slug
|
|
210
|
-
* fallbacks).
|
|
210
|
+
* A target is looked up case-insensitively in **one** of two namespaces, and
|
|
211
|
+
* the pipe chooses which (#131):
|
|
211
212
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* in
|
|
213
|
+
* - **Unpiped** — an alias scoped to the source's own **type**
|
|
214
|
+
* (`ctx.typeAlias`, keyed `type|alias`). A note's directory and `category`
|
|
215
|
+
* play no part.
|
|
216
|
+
* - **Piped** — an address, parsed by {@link readQualifier} and looked up in
|
|
217
|
+
* the KB-wide `ctx.index` (the canonical `package-type-shortcode`,
|
|
218
|
+
* `type/shortcode`, and the site's own `section/slug`), then in the vendored
|
|
219
|
+
* `ctx.foreign` manifests.
|
|
220
|
+
*
|
|
221
|
+
* Neither falls back to the other, so the name/basename/slug fallbacks that
|
|
222
|
+
* share `ctx.index` no longer answer for an address: only a slash-qualified
|
|
223
|
+
* target reaches the raw key, which is what keeps `section/slug` addressable.
|
|
224
|
+
*
|
|
225
|
+
* An unresolved target fails the build when it is a genuine intra-KB problem —
|
|
226
|
+
* an ambiguous alias, a qualified `prefix/key` whose prefix is a real KB
|
|
227
|
+
* section or content directory, or a **piped** target that is not an address
|
|
228
|
+
* at all. Anything else is treated as an external reference — until every
|
|
229
|
+
* package's manifest is present, after which any address resolving nowhere
|
|
230
|
+
* fails too. Failures are collected in `ctx.errors`.
|
|
218
231
|
*
|
|
219
232
|
* Whether or not it fails the build, a target that resolves nowhere renders
|
|
220
233
|
* through {@link unresolvedLink} rather than as bare prose (#1665): the author's
|
|
@@ -229,7 +242,9 @@ function isPlainMap(value) {
|
|
|
229
242
|
*
|
|
230
243
|
* @param {string} body - The markdown body.
|
|
231
244
|
* @param {object} ctx - `{ index, typeAlias, collide, typeCollide, sections,
|
|
232
|
-
* contentTypes, foreign, manifestsComplete, type, errors, src }`.
|
|
245
|
+
* contentTypes, packages, foreign, manifestsComplete, type, errors, src }`.
|
|
246
|
+
* `packages` is every package an address may name, without which the leading
|
|
247
|
+
* package segment of a canonical address reads as an unknown type; `foreign`
|
|
233
248
|
* is the cross-package manifest index (#1446); `manifestsComplete` says
|
|
234
249
|
* whether every linkable package is accounted for. Together they decide
|
|
235
250
|
* whether an unresolved address is a typo or a package merely absent.
|
|
@@ -239,7 +254,8 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
239
254
|
// Code is verbatim: a `[[…]]` inside a code fence, an indented block or an
|
|
240
255
|
// inline span is source text, not a link (#1505).
|
|
241
256
|
return replaceOutsideCode(body, WIKILINK, (_m, rawInner) => {
|
|
242
|
-
const
|
|
257
|
+
const parsed = parseWikilink(rawInner);
|
|
258
|
+
const { target, anchor, display } = parsed;
|
|
243
259
|
// An empty label is not a label: `[[x|]]` addresses the target and
|
|
244
260
|
// shows its name, so `""` falls through to the same place `null` does
|
|
245
261
|
// (#113). One reading, from {@link authoredLabel}.
|
|
@@ -250,32 +266,41 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
250
266
|
return `[${label ?? anchor}](#${slugify(anchor)})`;
|
|
251
267
|
}
|
|
252
268
|
|
|
253
|
-
|
|
254
|
-
|
|
269
|
+
// **The pipe chooses the namespace, with no fallback either way**
|
|
270
|
+
// (#131). The two used to be tried in turn, so a note *name* that
|
|
271
|
+
// looked like an address resolved as one and a genuine address that
|
|
272
|
+
// resolved nowhere silently became a name lookup.
|
|
273
|
+
const addressed = resolvesAsAddress(parsed);
|
|
274
|
+
const typeKey = ctx.type ? aliasKey(ctx.type, target) : null;
|
|
255
275
|
// The canonical separator (#1398) has to be resolved, not merely
|
|
256
|
-
// recognised.
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
const
|
|
276
|
+
// recognised. `null` here means the piped target is not an address at
|
|
277
|
+
// all, which is now a defect rather than a reason to try the aliases.
|
|
278
|
+
const hyphenKey = addressed ? qualifiedKey(target, ctx.contentTypes, ctx.packages) : null;
|
|
279
|
+
const rawKey = target.toLowerCase();
|
|
260
280
|
const hit =
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
281
|
+
addressed ?
|
|
282
|
+
((hyphenKey ? ctx.index.get(hyphenKey) : undefined) ??
|
|
283
|
+
// `section/slug` is the site's own address for a page, and it
|
|
284
|
+
// is in the same map. Admitted only when the target carries a
|
|
285
|
+
// slash, which is what keeps the *alias* fallbacks sharing
|
|
286
|
+
// that map — a page's name, basename and slug — out of the
|
|
287
|
+
// address namespace.
|
|
288
|
+
(rawKey.includes("/") ? ctx.index.get(rawKey) : undefined) ??
|
|
289
|
+
// A manifest entry carries the same `{ url, name }` shape as a
|
|
290
|
+
// local one (#1446), so a cross-package hit needs no special
|
|
291
|
+
// case below. Local wins: a live build is authoritative and a
|
|
292
|
+
// vendored manifest can only be staler.
|
|
293
|
+
(hyphenKey ? ctx.foreign?.get(hyphenKey) : undefined))
|
|
294
|
+
: typeKey ? ctx.typeAlias.get(typeKey)
|
|
295
|
+
: undefined;
|
|
269
296
|
if (hit) {
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// ("worsens the [[Shock
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
// one, which is why the rule is the packs' own (#1409).
|
|
278
|
-
const text = label ?? (isAddress(target, ctx.contentTypes) ? hit.name : target);
|
|
297
|
+
// An address with no label has no prose to show (a shortcode is
|
|
298
|
+
// not display text), so the document's **current** name stands in
|
|
299
|
+
// and a rename shows at every citation. A bare `[[Text]]` is
|
|
300
|
+
// already the prose the author wrote — substituting the canonical
|
|
301
|
+
// name there would rewrite the sentence ("worsens the [[Shock
|
|
302
|
+
// State]]" must not render as "Shock").
|
|
303
|
+
const text = label ?? (addressed ? hit.name : target);
|
|
279
304
|
// A pack-only package publishes Foundry addresses and no pages
|
|
280
305
|
// (#1516), so its entries carry no `path` and resolve to no URL.
|
|
281
306
|
// The address is real — this is not a typo and must not fail the
|
|
@@ -289,15 +314,9 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
289
314
|
|
|
290
315
|
const slash = target.indexOf("/");
|
|
291
316
|
const prefix = slash === -1 ? null : target.slice(0, slash).toLowerCase();
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// package). Nothing in the syntax separates that from a typo,
|
|
296
|
-
// so failing here would break the build on correct content.
|
|
297
|
-
//
|
|
298
|
-
// A dead address is caught instead by `lint:content-links` (#1414),
|
|
299
|
-
// which holds the reviewed list of cross-package exceptions — and which,
|
|
300
|
-
// unlike this build, runs as part of `npm run lint` on every change.
|
|
317
|
+
// A slash-qualified target whose prefix is a real section or content
|
|
318
|
+
// type is definitely local, so it is a typo whatever the manifest
|
|
319
|
+
// situation.
|
|
301
320
|
const badQualified =
|
|
302
321
|
prefix !== null && (ctx.sections.has(prefix) || ctx.contentTypes.has(prefix));
|
|
303
322
|
// The hyphen form is the canonical address (#1398) and is what the
|
|
@@ -316,7 +335,17 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
316
335
|
// someone has to remember.
|
|
317
336
|
const badAddress = ctx.manifestsComplete === true && hyphenKey !== null;
|
|
318
337
|
|
|
319
|
-
if (
|
|
338
|
+
if (addressed && hyphenKey === null && !badQualified) {
|
|
339
|
+
// The author wrote a pipe, so they meant an address — and this is
|
|
340
|
+
// not one. Distinct from a dead address, because the fix is
|
|
341
|
+
// different: a name has to become an address, not be corrected
|
|
342
|
+
// (#131).
|
|
343
|
+
ctx.errors.push({ file: ctx.src, target, reason: "not-an-address" });
|
|
344
|
+
} else if (
|
|
345
|
+
addressed ?
|
|
346
|
+
rawKey.includes("/") && ctx.collide.has(rawKey)
|
|
347
|
+
: Boolean(typeKey && ctx.typeCollide.has(typeKey))
|
|
348
|
+
) {
|
|
320
349
|
ctx.errors.push({ file: ctx.src, target, reason: "ambiguous" });
|
|
321
350
|
} else if (badQualified || badAddress) {
|
|
322
351
|
ctx.errors.push({
|
|
@@ -325,6 +354,9 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
325
354
|
reason: "broken type/shortcode",
|
|
326
355
|
});
|
|
327
356
|
}
|
|
357
|
+
// An unresolved *alias* stays soft: it may be ordinary prose, or a
|
|
358
|
+
// worldbuilding placeholder for a note not yet written. It still
|
|
359
|
+
// renders marked, so a reader can see a link was intended.
|
|
328
360
|
return unresolvedLink(label ?? target, target);
|
|
329
361
|
});
|
|
330
362
|
}
|
|
@@ -116,6 +116,36 @@ export function authoredLabel({ display }) {
|
|
|
116
116
|
return display ? display : null;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
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 alias — which 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.
|
|
144
|
+
*/
|
|
145
|
+
export function resolvesAsAddress({ labelled }) {
|
|
146
|
+
return Boolean(labelled);
|
|
147
|
+
}
|
|
148
|
+
|
|
119
149
|
/**
|
|
120
150
|
* Whether a parsed link addresses a section of the page it is written on.
|
|
121
151
|
*
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -16,11 +16,17 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Content notes link to one another with wikilinks rather than file paths:
|
|
18
18
|
*
|
|
19
|
-
* `[[type
|
|
19
|
+
* `[[type-shortcode|Text]]` a document of that type
|
|
20
|
+
* `[[type-shortcode|]]` the same, showing the target's current name
|
|
20
21
|
* `[[Text]]` an alias unique within the source's own type
|
|
21
|
-
* `[[type
|
|
22
|
+
* `[[type-shortcode#slug|T]]` a section (see below)
|
|
22
23
|
* `[[#slug|Text]]` a section of the source note itself
|
|
23
|
-
* `[[doctype
|
|
24
|
+
* `[[doctype-shortcode|T]]` an item's *documentation* (see below)
|
|
25
|
+
*
|
|
26
|
+
* **The pipe decides which of the two namespaces a target belongs to** (#131),
|
|
27
|
+
* and neither falls back to the other — see {@link resolvesAsAddress}, which
|
|
28
|
+
* states the rule for both builds. A piped target is parsed by the address
|
|
29
|
+
* grammar; an unpiped one is looked up in the alias index.
|
|
24
30
|
*
|
|
25
31
|
* The qualifier is the note's **type**, which with its shortcode is the system's
|
|
26
32
|
* logical identity: `(type, shortcode)` is unique by rule (see the Shortcode
|
|
@@ -28,13 +34,13 @@
|
|
|
28
34
|
* unique per type, not per directory, so a directory qualifier would add nothing
|
|
29
35
|
* to the address while breaking every inbound link the moment a note is refiled.
|
|
30
36
|
*
|
|
31
|
-
* The bare form is
|
|
32
|
-
*
|
|
33
|
-
*
|
|
37
|
+
* The bare form is a **name**, not an abbreviated address: it resolves against
|
|
38
|
+
* the aliases of the source's **own type**, so a `doc` reaches any other `doc`
|
|
39
|
+
* by name wherever it is filed. Nothing narrower is consulted — a note's
|
|
34
40
|
* directory and its `category` play no part in resolution. Where two notes of a
|
|
35
41
|
* type legitimately share a name (a rules page and a user-guide page both called
|
|
36
42
|
* "Gear"), the bare form is ambiguous and resolves to neither; the author writes
|
|
37
|
-
* the
|
|
43
|
+
* the `[[type-shortcode|Text]]` address instead.
|
|
38
44
|
*
|
|
39
45
|
* At compile time each becomes a Foundry UUID enricher, routed to the pack that
|
|
40
46
|
* the target's type compiles into (see {@link packForType}):
|
|
@@ -87,7 +93,10 @@ import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
|
87
93
|
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
88
94
|
// The syntax lives in `./wikilink-syntax.mjs`, so the web resolver and this
|
|
89
95
|
// one cannot disagree about what counts as a link.
|
|
90
|
-
import { authoredLabel, WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
|
|
96
|
+
import { authoredLabel, WIKILINK, parseWikilink, resolvesAsAddress } from "./wikilink-syntax.mjs";
|
|
97
|
+
// The alias half of the two namespaces: what may be claimed, and how a claim
|
|
98
|
+
// is keyed. Shared with the site build and the link checker (#131).
|
|
99
|
+
import { aliasKey } from "./alias-index.mjs";
|
|
91
100
|
|
|
92
101
|
export { ITEM_PACK, PACK_BY_TYPE, packForType };
|
|
93
102
|
|
|
@@ -304,7 +313,7 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
|
|
|
304
313
|
|
|
305
314
|
if (d.shortcode) byShortcode.set(`${norm(d.type)}/${norm(d.shortcode)}`, d);
|
|
306
315
|
for (const a of d.aliases ?? []) {
|
|
307
|
-
const key =
|
|
316
|
+
const key = aliasKey(d.type, a);
|
|
308
317
|
// Second claimant poisons the alias: it can no longer be resolved.
|
|
309
318
|
byAlias.set(key, byAlias.has(key) && byAlias.get(key) !== d ? null : d);
|
|
310
319
|
// Every claimant is kept alongside, because poisoning the alias
|
|
@@ -475,55 +484,60 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
475
484
|
let text = labelled ? (authoredLabel(parsed) ?? "") : parsed.inner;
|
|
476
485
|
const slug = parsed.anchor || null;
|
|
477
486
|
|
|
478
|
-
// Resolve the document: same-page (empty target),
|
|
487
|
+
// Resolve the document: same-page (empty target), an address, or an
|
|
488
|
+
// alias. **The pipe chooses which**, with no fallback either way
|
|
489
|
+
// (#131) — see {@link resolvesAsAddress}.
|
|
479
490
|
let doc;
|
|
480
491
|
// Set when the qualifier was the virtual `doc<type>` form, so the UUID
|
|
481
492
|
// is built against the item doc entry rather than the item itself.
|
|
482
493
|
let itemDoc = false;
|
|
483
|
-
// Set when the target was read as
|
|
484
|
-
//
|
|
494
|
+
// Set when the target was read as an address, which is what decides
|
|
495
|
+
// whether a foreign manifest is consulted for it below.
|
|
485
496
|
let addressed = false;
|
|
486
497
|
// Kept for the foreign fallback below, which needs the parsed address.
|
|
487
498
|
let qualifiedRead = null;
|
|
488
499
|
if (target === "" && slug) {
|
|
489
500
|
doc = { type, id, pack, docPack };
|
|
490
|
-
} else {
|
|
501
|
+
} else if (resolvesAsAddress(parsed)) {
|
|
491
502
|
const qualified = readQualifier(target, index.types, index.packages);
|
|
492
503
|
qualifiedRead = qualified;
|
|
493
|
-
|
|
504
|
+
// The author wrote a pipe, so they meant an address. A target that
|
|
505
|
+
// does not parse as one is therefore a defect and not, as it was
|
|
506
|
+
// under the old resolve-by-shape rule, an invitation to try the
|
|
507
|
+
// alias index — which is what let a note *name* resolve here.
|
|
508
|
+
if (!qualified || qualified.reason) {
|
|
494
509
|
unresolved.push({
|
|
495
510
|
link: all,
|
|
496
511
|
target,
|
|
497
512
|
offset,
|
|
498
|
-
reason: qualified
|
|
513
|
+
reason: qualified?.reason ?? "not-an-address",
|
|
514
|
+
addressed: true,
|
|
499
515
|
});
|
|
500
516
|
return unresolvedLink(text || target, target);
|
|
501
517
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return unresolvedLink(text || target, target);
|
|
524
|
-
}
|
|
525
|
-
doc = hit;
|
|
518
|
+
addressed = true;
|
|
519
|
+
itemDoc = qualified.itemDoc;
|
|
520
|
+
doc = index.byShortcode.get(`${qualified.type}/${qualified.shortcode}`);
|
|
521
|
+
} else {
|
|
522
|
+
const key = aliasKey(type, target);
|
|
523
|
+
const hit = index.byAlias.get(key);
|
|
524
|
+
if (hit === null) {
|
|
525
|
+
unresolved.push({
|
|
526
|
+
link: all,
|
|
527
|
+
target,
|
|
528
|
+
offset,
|
|
529
|
+
reason: "ambiguous",
|
|
530
|
+
// Who claimed it, so the report can name the collision
|
|
531
|
+
// rather than the note that merely cites it (#13).
|
|
532
|
+
candidates: (index.aliasClaims?.get(key) ?? []).map((d) => ({
|
|
533
|
+
type: d.type,
|
|
534
|
+
shortcode: d.shortcode,
|
|
535
|
+
name: d.name,
|
|
536
|
+
})),
|
|
537
|
+
});
|
|
538
|
+
return unresolvedLink(text || target, target);
|
|
526
539
|
}
|
|
540
|
+
doc = hit;
|
|
527
541
|
}
|
|
528
542
|
if (!doc) {
|
|
529
543
|
// Nothing local answers. A foreign package may publish this
|
|
@@ -550,22 +564,24 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
550
564
|
target,
|
|
551
565
|
offset,
|
|
552
566
|
reason: "unknown",
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
addressed
|
|
567
|
+
// An *address* that resolves nowhere is a typo: every package
|
|
568
|
+
// it could name is either built here or vendored, so there is
|
|
569
|
+
// no third possibility left. A bare alias is not — it may
|
|
570
|
+
// simply be prose, or a worldbuilding placeholder.
|
|
571
|
+
addressed,
|
|
558
572
|
});
|
|
559
573
|
return unresolvedLink(text || target, target);
|
|
560
574
|
}
|
|
561
575
|
|
|
562
|
-
//
|
|
563
|
-
// shortcode
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
|
|
576
|
+
// An address with no label — `[[skill-clmb|]]` — has no prose to show,
|
|
577
|
+
// a shortcode being an address rather than display text, so the
|
|
578
|
+
// document's **current** name stands in and a rename shows at every
|
|
579
|
+
// citation with no link edited (#1409, #131). A bare `[[Text]]` is
|
|
580
|
+
// already the prose the author wrote, and substituting the canonical
|
|
581
|
+
// name there would rewrite the sentence ("worsens the [[Shock State]]"
|
|
582
|
+
// must not render as "Shock"). The knowledgebase build reads the same
|
|
583
|
+
// authored link the same way.
|
|
584
|
+
if (!text) text = doc.name ?? target;
|
|
569
585
|
|
|
570
586
|
// Both addresses were computed when the target was indexed. An item
|
|
571
587
|
// doc lives in the journals pack under its own derived entry id, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"container.mjs",
|
|
102
102
|
"content-config.mjs",
|
|
103
103
|
"coverage.mjs",
|
|
104
|
+
"docs",
|
|
104
105
|
"deploy.mjs",
|
|
105
106
|
"e2e.mjs",
|
|
106
107
|
"engine",
|
|
@@ -148,9 +149,12 @@
|
|
|
148
149
|
"build:types": "tsc -p tsconfig.dts.json",
|
|
149
150
|
"format": "prettier --write .",
|
|
150
151
|
"format:check": "prettier --check .",
|
|
151
|
-
"lint": "npm run format:check && npm run lint:markdown",
|
|
152
|
+
"lint": "npm run format:check && npm run lint:markdown && npm run lint:content-format",
|
|
152
153
|
"lint:markdown": "node bin/content-build.mjs markdown",
|
|
153
154
|
"lint:markdown:fix": "node bin/content-build.mjs markdown --fix",
|
|
155
|
+
"lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields",
|
|
156
|
+
"lint:content-format:schema": "node bin/content-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json",
|
|
157
|
+
"lint:content-format:fields": "node bin/content-build.mjs content-format fields --fields sohl",
|
|
154
158
|
"changeset": "changeset",
|
|
155
159
|
"changeset:check": "changeset status --since=origin/main",
|
|
156
160
|
"changeset:version": "changeset version && npm install --package-lock-only",
|