@heroiclands/package-build 22.12.2 → 22.13.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 +20 -0
- package/README.md +3 -0
- package/docs/api.md +19 -18
- package/docs/commands.md +2 -2
- package/docs/content-format.md +59 -66
- package/docs/getting-started.md +2 -3
- package/engine/address.mjs +39 -153
- package/engine/art-fields.mjs +1 -1
- package/engine/bundles.mjs +3 -2
- package/engine/content-address.mjs +6 -9
- package/engine/content-index.mjs +8 -10
- package/engine/content-links.mjs +9 -17
- package/engine/content-tables.mjs +1 -1
- package/engine/foundry-entries.mjs +4 -8
- package/engine/frontmatter-lint.mjs +2 -2
- package/engine/metadata-index.mjs +23 -6
- package/engine/note-addresses.mjs +27 -9
- package/engine/note-ids.mjs +6 -1
- package/engine/place-relations.mjs +4 -4
- package/engine/reference-targets.mjs +7 -7
- package/engine/shortcode-references.mjs +1 -1
- package/engine/site-index.mjs +8 -39
- package/engine/sql-tables.mjs +1 -1
- package/engine/systems.mjs +4 -1
- package/engine/web-wikilinks.mjs +3 -3
- package/engine/wikilinks.mjs +31 -106
- package/package.json +1 -1
- package/sohl/infobox.mjs +2 -2
- package/types/engine/address.d.mts +21 -88
- package/types/engine/content-address.d.mts +3 -6
- package/types/engine/content-links.d.mts +2 -6
- package/types/engine/note-addresses.d.mts +2 -0
- package/types/engine/systems.d.mts +2 -0
|
@@ -24,7 +24,13 @@ export { AddressEntries, encodeAddresses, cloneAddressState } from "./address-va
|
|
|
24
24
|
|
|
25
25
|
/** Structured positions whose Address is nested inside a declared container. */
|
|
26
26
|
export const STRUCTURED_ADDRESSES = Object.freeze([
|
|
27
|
-
{
|
|
27
|
+
{
|
|
28
|
+
path: ["packFolder"],
|
|
29
|
+
shape: "scalar-or-map",
|
|
30
|
+
type: "folder",
|
|
31
|
+
accepts: ["folder"],
|
|
32
|
+
system: "none",
|
|
33
|
+
},
|
|
28
34
|
{
|
|
29
35
|
noteType: "affiliation",
|
|
30
36
|
path: ["data", "governance", "ranks", "*", "lore"],
|
|
@@ -67,6 +73,9 @@ export function addressPositions(fm, context = {}) {
|
|
|
67
73
|
shape,
|
|
68
74
|
type: field.ref,
|
|
69
75
|
accepts: field.accepts,
|
|
76
|
+
...(ASSET_TYPE_NAMES.has(field.ref) || field.ref === "folder" ?
|
|
77
|
+
{ system: "none" }
|
|
78
|
+
: {}),
|
|
70
79
|
});
|
|
71
80
|
}
|
|
72
81
|
out.push(...STRUCTURED_ADDRESSES.filter((p) => !p.noteType || p.noteType === fm.type));
|
|
@@ -79,7 +88,7 @@ export function addressPositions(fm, context = {}) {
|
|
|
79
88
|
shape: "scalar-or-map",
|
|
80
89
|
type: "folder",
|
|
81
90
|
accepts: ["folder"],
|
|
82
|
-
system,
|
|
91
|
+
system: "none",
|
|
83
92
|
});
|
|
84
93
|
out.push({
|
|
85
94
|
path: [system, "items", "*", "model"],
|
|
@@ -93,7 +102,7 @@ export function addressPositions(fm, context = {}) {
|
|
|
93
102
|
shape: "value",
|
|
94
103
|
type: slot.type,
|
|
95
104
|
accepts: slot.accepts,
|
|
96
|
-
system,
|
|
105
|
+
system: "none",
|
|
97
106
|
});
|
|
98
107
|
const spec = context.systemBlocks?.[system];
|
|
99
108
|
for (const [index, entry] of (Array.isArray(fm[system]?.items) ?
|
|
@@ -121,7 +130,13 @@ export function addressPositions(fm, context = {}) {
|
|
|
121
130
|
field.address.holds === "items" ? "list" : (field.address.holds ?? "value"),
|
|
122
131
|
type: field.address.type,
|
|
123
132
|
accepts: field.address.accepts,
|
|
124
|
-
system
|
|
133
|
+
system:
|
|
134
|
+
(
|
|
135
|
+
ASSET_TYPE_NAMES.has(field.address.type) ||
|
|
136
|
+
field.address.type === "folder"
|
|
137
|
+
) ?
|
|
138
|
+
"none"
|
|
139
|
+
: system,
|
|
125
140
|
});
|
|
126
141
|
}
|
|
127
142
|
}
|
|
@@ -135,7 +150,10 @@ export function addressPositions(fm, context = {}) {
|
|
|
135
150
|
shape: field.address.holds === "items" ? "list" : (field.address.holds ?? "value"),
|
|
136
151
|
type: field.address.type,
|
|
137
152
|
accepts: field.address.accepts,
|
|
138
|
-
system
|
|
153
|
+
system:
|
|
154
|
+
ASSET_TYPE_NAMES.has(field.address.type) || field.address.type === "folder" ?
|
|
155
|
+
"none"
|
|
156
|
+
: system,
|
|
139
157
|
};
|
|
140
158
|
out.push({ ...position, path: [system, "system", ...field.to.split(".")] });
|
|
141
159
|
out.push({
|
|
@@ -143,7 +161,7 @@ export function addressPositions(fm, context = {}) {
|
|
|
143
161
|
path: [system, ...(field.legacyKey ?? field.name).split(".")],
|
|
144
162
|
});
|
|
145
163
|
if (field.name?.startsWith("data.") && field.topLevelMeans === undefined)
|
|
146
|
-
out.push({ ...position,
|
|
164
|
+
out.push({ ...position, path: field.name.slice(5).split(".") });
|
|
147
165
|
}
|
|
148
166
|
}
|
|
149
167
|
for (const position of [...out]) {
|
|
@@ -171,7 +189,7 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
171
189
|
const defaults = {
|
|
172
190
|
...context,
|
|
173
191
|
types,
|
|
174
|
-
system: position.system ?? context.system ?? "
|
|
192
|
+
system: position.system ?? context.system ?? "note",
|
|
175
193
|
type: position.type,
|
|
176
194
|
};
|
|
177
195
|
const fail = (value, path, description) => {
|
|
@@ -277,7 +295,7 @@ export function noteAddressContext(config) {
|
|
|
277
295
|
}
|
|
278
296
|
return {
|
|
279
297
|
package: config.contentPackage,
|
|
280
|
-
system: "
|
|
298
|
+
system: "note",
|
|
281
299
|
systemBlocks,
|
|
282
300
|
};
|
|
283
301
|
}
|
|
@@ -313,7 +331,7 @@ export function decodeIndexAddresses(record, context) {
|
|
|
313
331
|
written,
|
|
314
332
|
{
|
|
315
333
|
...context,
|
|
316
|
-
system: "
|
|
334
|
+
system: "note",
|
|
317
335
|
types: new Set([...Object.keys(NOTE_VOCABULARY), ...ASSET_TYPE_NAMES]),
|
|
318
336
|
},
|
|
319
337
|
{ declared: true },
|
package/engine/note-ids.mjs
CHANGED
|
@@ -44,6 +44,9 @@ import { documentId } from "./content-address.mjs";
|
|
|
44
44
|
import { systemOf } from "./document-subtypes.mjs";
|
|
45
45
|
import { contentPackage } from "./content-package.mjs";
|
|
46
46
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
47
|
+
import { JOURNAL_TYPES } from "./ids.mjs";
|
|
48
|
+
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
49
|
+
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
47
50
|
// The folder id's derivation, taken from the pass that owns it rather than
|
|
48
51
|
// restated here — see the `folder` branch below. `folder-notes.mjs` reaches
|
|
49
52
|
// only `content-address`, `address-charset`, `ids` and `retired-fields`, none
|
|
@@ -112,7 +115,9 @@ export function noteDocId(fm, { pkg, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS } = {})
|
|
|
112
115
|
// the two must answer alike about the same note or the divergence this
|
|
113
116
|
// branch closes reopens under a capitalised `type: Folder`.
|
|
114
117
|
if (type.toLowerCase() === FOLDER_TYPE) return folderDocId(owner, shortcode);
|
|
115
|
-
|
|
118
|
+
const system =
|
|
119
|
+
JOURNAL_TYPES.has(type) || type === HOMEPAGE_TYPE ? NOTE_SYSTEM : systemOf(type, maps);
|
|
120
|
+
return documentId(owner, system, type, shortcode);
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
/**
|
|
@@ -67,7 +67,7 @@ import { encodeAddresses } from "./address-values.mjs";
|
|
|
67
67
|
|
|
68
68
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
69
69
|
import { acceptsType, parseAddress, renderAddress } from "./address.mjs";
|
|
70
|
-
import {
|
|
70
|
+
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
71
71
|
|
|
72
72
|
/* --------------------------------------------------------------------- */
|
|
73
73
|
/* The closed sets */
|
|
@@ -232,7 +232,7 @@ function addressDefaults(index) {
|
|
|
232
232
|
packages: index?.packages,
|
|
233
233
|
noIndexPackages: index?.noIndexPackages,
|
|
234
234
|
package: index?.contentPackage,
|
|
235
|
-
system:
|
|
235
|
+
system: NOTE_SYSTEM,
|
|
236
236
|
type: RELATION_TYPE,
|
|
237
237
|
};
|
|
238
238
|
}
|
|
@@ -356,7 +356,7 @@ function checkRelation(note, { field, index }) {
|
|
|
356
356
|
here.package && self ?
|
|
357
357
|
renderAddress({
|
|
358
358
|
package: here.package,
|
|
359
|
-
system:
|
|
359
|
+
system: NOTE_SYSTEM,
|
|
360
360
|
type: RELATION_TYPE,
|
|
361
361
|
shortcode: self,
|
|
362
362
|
})
|
|
@@ -425,7 +425,7 @@ function checkRelation(note, { field, index }) {
|
|
|
425
425
|
"to",
|
|
426
426
|
`${label(i, "to")} must name a place — the other place's \`shortcode\`, or ` +
|
|
427
427
|
`its address as \`${RELATION_TYPE}-<shortcode>\` or ` +
|
|
428
|
-
`\`<package>-${
|
|
428
|
+
`\`<package>-${NOTE_SYSTEM}-${RELATION_TYPE}-<shortcode>\` for a place in ` +
|
|
429
429
|
`another package — but reads ${JSON.stringify(encodeAddresses(to))}`,
|
|
430
430
|
);
|
|
431
431
|
} else if (!acceptsType(read, [RELATION_TYPE])) {
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { completeAddress, renderAddress } from "./address.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { completeAddress, ownDocumentSystem, renderAddress } from "./address.mjs";
|
|
8
|
+
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
9
|
+
import { JOURNAL_TYPES } from "./ids.mjs";
|
|
10
|
+
import { hasDocEntry } from "./item-docs.mjs";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Names and published destinations for reference presentation.
|
|
@@ -21,10 +22,9 @@ export function buildReferenceTargets(records, published = new Map()) {
|
|
|
21
22
|
for (const record of records) {
|
|
22
23
|
if (!record.package || !record.type || !record.shortcode) continue;
|
|
23
24
|
const value = { name: record.name?.full ?? record.name, subType: record.subType };
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
])) {
|
|
25
|
+
const systems = new Set([ownDocumentSystem(record.type)]);
|
|
26
|
+
if (JOURNAL_TYPES.has(record.type) || hasDocEntry(record.type)) systems.add(NOTE_SYSTEM);
|
|
27
|
+
for (const system of systems) {
|
|
28
28
|
const address = completeAddress({
|
|
29
29
|
package: record.package,
|
|
30
30
|
system,
|
|
@@ -52,7 +52,7 @@ export function resolveShortcodeReference(indexes, code, hint) {
|
|
|
52
52
|
const { address: native, value } = matches.values().next().value;
|
|
53
53
|
const address = completeAddress({
|
|
54
54
|
package: native.package,
|
|
55
|
-
system: "
|
|
55
|
+
system: "note",
|
|
56
56
|
type: native.type,
|
|
57
57
|
shortcode: native.shortcode,
|
|
58
58
|
});
|
package/engine/site-index.mjs
CHANGED
|
@@ -61,12 +61,9 @@ import {
|
|
|
61
61
|
parseAddress,
|
|
62
62
|
renderAddress,
|
|
63
63
|
isAddressTuple,
|
|
64
|
-
|
|
64
|
+
ownDocumentSystem,
|
|
65
65
|
} from "./address.mjs";
|
|
66
|
-
import {
|
|
67
|
-
import { systemOf } from "./document-subtypes.mjs";
|
|
68
|
-
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
|
|
69
|
-
import { hasDocEntry } from "./item-docs.mjs";
|
|
66
|
+
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
70
67
|
import { contentPackage } from "./content-package.mjs";
|
|
71
68
|
// The declared tag vocabulary, which is where `draft` is stated.
|
|
72
69
|
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
@@ -241,42 +238,14 @@ export function buildSiteIndex(
|
|
|
241
238
|
// resolves it and records it as `pkg`. Never read out of
|
|
242
239
|
// frontmatter: `package:` is retired.
|
|
243
240
|
index.set(
|
|
244
|
-
canonicalKey(
|
|
245
|
-
e.pkg ?? ownPackage,
|
|
246
|
-
systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
|
|
247
|
-
type,
|
|
248
|
-
shortcode,
|
|
249
|
-
),
|
|
241
|
+
canonicalKey(e.pkg ?? ownPackage, ownDocumentSystem(type), type, shortcode),
|
|
250
242
|
value,
|
|
251
243
|
);
|
|
252
244
|
if (e.pkg) packages.add(e.pkg);
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// ordinary in-page anchor. One authored link, correct in both
|
|
258
|
-
// builds — restricted to the types that actually have an item doc,
|
|
259
|
-
// so a qualifier the packs would reject is reported broken here too.
|
|
260
|
-
const proseAddress = completeAddress({
|
|
261
|
-
package: e.pkg ?? ownPackage,
|
|
262
|
-
system: NO_SYSTEM,
|
|
263
|
-
type,
|
|
264
|
-
shortcode,
|
|
265
|
-
});
|
|
266
|
-
if (proseAddress.type !== type || hasDocEntry(type)) {
|
|
267
|
-
contentTypes.add(`doc${type}`);
|
|
268
|
-
index.set(`doc${type}/${shortcode}`.toLowerCase(), value);
|
|
269
|
-
// The canonical documentation address too, so the page answers
|
|
270
|
-
// to the address a bare prose link expands to: body
|
|
271
|
-
// prose is under no system block, so it defaults to `none`, and
|
|
272
|
-
// a system-bearing type's `none` address is its `doc<type>`
|
|
273
|
-
// one. In Foundry that names a second document; here it names
|
|
274
|
-
// this same page, which is what makes one authored link correct
|
|
275
|
-
// in both builds.
|
|
276
|
-
index.set(
|
|
277
|
-
canonicalKey(e.pkg ?? ownPackage, NO_SYSTEM, `doc${type}`, shortcode),
|
|
278
|
-
value,
|
|
279
|
-
);
|
|
245
|
+
// A web page is the readable `note` target for every type.
|
|
246
|
+
if (ownDocumentSystem(type) !== NOTE_SYSTEM) {
|
|
247
|
+
// The same page answers to its readable Address.
|
|
248
|
+
index.set(canonicalKey(e.pkg ?? ownPackage, NOTE_SYSTEM, type, shortcode), value);
|
|
280
249
|
}
|
|
281
250
|
}
|
|
282
251
|
}
|
|
@@ -375,7 +344,7 @@ export function resolveInfoboxRef(siteIndex, ref, hint) {
|
|
|
375
344
|
}
|
|
376
345
|
const context = {
|
|
377
346
|
package: siteIndex.contentPackage ?? contentPackage(),
|
|
378
|
-
system: "
|
|
347
|
+
system: "note",
|
|
379
348
|
types: siteIndex.contentTypes,
|
|
380
349
|
packages: siteIndex.packages,
|
|
381
350
|
};
|
package/engine/sql-tables.mjs
CHANGED
|
@@ -253,7 +253,7 @@ export async function openNotesDatabase(records, { dir, dependencies = [], addre
|
|
|
253
253
|
addressContext: addressContext ?? {
|
|
254
254
|
package: records.find((record) => record.package)?.package,
|
|
255
255
|
types: new Set(Object.keys(NOTE_VOCABULARY)),
|
|
256
|
-
system: "
|
|
256
|
+
system: "note",
|
|
257
257
|
},
|
|
258
258
|
stubsExcluded,
|
|
259
259
|
async query(sql) {
|
package/engine/systems.mjs
CHANGED
|
@@ -62,6 +62,9 @@ import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs
|
|
|
62
62
|
*/
|
|
63
63
|
export const NO_SYSTEM = "none";
|
|
64
64
|
|
|
65
|
+
/** The Address segment for a note's readable content. */
|
|
66
|
+
export const NOTE_SYSTEM = "note";
|
|
67
|
+
|
|
65
68
|
/**
|
|
66
69
|
* Every game system this toolchain compiles for.
|
|
67
70
|
*
|
|
@@ -86,7 +89,7 @@ export const SYSTEM_IDS = Object.freeze(new Set(["sohl", "hm3"]));
|
|
|
86
89
|
*
|
|
87
90
|
* @type {ReadonlySet<string>}
|
|
88
91
|
*/
|
|
89
|
-
export const SYSTEM_SEGMENTS = Object.freeze(new Set([...SYSTEM_IDS, NO_SYSTEM]));
|
|
92
|
+
export const SYSTEM_SEGMENTS = Object.freeze(new Set([...SYSTEM_IDS, NO_SYSTEM, NOTE_SYSTEM]));
|
|
90
93
|
|
|
91
94
|
/**
|
|
92
95
|
* Whether a value names a game system this toolchain knows.
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import { replaceOutsideCode } from "./code-fences.mjs";
|
|
|
48
48
|
// address is matched the way a vendored manifest publishes it — by the
|
|
49
49
|
// segments the target supplies, with the system wildcarded unless stated.
|
|
50
50
|
import { canonicalKey, expandAddress, readCanonicalKey } from "./content-address.mjs";
|
|
51
|
-
import {
|
|
51
|
+
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
52
52
|
// The one rule about a link's shape both builds share: it carries a label, and
|
|
53
53
|
// {@link unlabelledLinkMessage} is the one place that says so.
|
|
54
54
|
import { unlabelledLinkMessage } from "./wikilink-syntax.mjs";
|
|
@@ -102,7 +102,7 @@ function lookupRead(index, read, contentPackage) {
|
|
|
102
102
|
// It replaced a system-blind short key for the unqualified form, which
|
|
103
103
|
// could be silently overwritten by a second note of the same
|
|
104
104
|
// `(type, shortcode)` under another system.
|
|
105
|
-
const key = expandAddress(read, { package: contentPackage, system:
|
|
105
|
+
const key = expandAddress(read, { package: contentPackage, system: NOTE_SYSTEM });
|
|
106
106
|
const found = index.get(key);
|
|
107
107
|
if (found || read.package) return found;
|
|
108
108
|
|
|
@@ -393,7 +393,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
393
393
|
return replaceOutsideCode(embedded.markdown, WIKILINK, (all, rawInner) => {
|
|
394
394
|
const parsed = readWikilink(rawInner, {
|
|
395
395
|
package: ctx.contentPackage,
|
|
396
|
-
system: "
|
|
396
|
+
system: "note",
|
|
397
397
|
types: ctx.contentTypes ?? new Set(),
|
|
398
398
|
packages: ctx.packages,
|
|
399
399
|
noIndexPackages: ctx.noIndexPackages,
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -12,74 +12,17 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Resolve body wikilinks for Foundry packs.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* `[[type-shortcode|Text]]` defaults to the readable `note` Address. A stated
|
|
18
|
+
* game system names an Actor or Item; `none` names a systemless document.
|
|
19
|
+
* Journal links can carry a heading anchor. Actor, Item and Macro links open
|
|
20
|
+
* their sheets, so their anchors do not select a page.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `[[type-shortcode#slug|T]]` a section (see below)
|
|
22
|
-
* `[[#slug|Text]]` a section of the source note itself
|
|
23
|
-
* `[[doctype-shortcode|T]]` an item's *documentation* (see below)
|
|
22
|
+
* `doc<type>` is accepted as an input alias for the readable note Address.
|
|
23
|
+
* A link with an empty label displays the target's current name.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
* link written without one addresses nothing and is reported — see
|
|
27
|
-
* {@link unlabelledLinkMessage}, which states the rule for both builds. The
|
|
28
|
-
* bare `[[Alias]]` form and the index it was looked up in are retired.
|
|
29
|
-
*
|
|
30
|
-
* The qualifier is the note's **type**, which with its shortcode is the system's
|
|
31
|
-
* logical identity: `(type, shortcode)` is unique by rule (see the Shortcode
|
|
32
|
-
* Integrity doc). It is deliberately not the note's directory — shortcodes are
|
|
33
|
-
* unique per type, not per directory, so a directory qualifier would add nothing
|
|
34
|
-
* to the address while breaking every inbound link the moment a note is refiled.
|
|
35
|
-
*
|
|
36
|
-
* Nothing narrower than `(type, shortcode)` is consulted — a note's directory
|
|
37
|
-
* and its `category` play no part in resolution — and nothing wider: a note's
|
|
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
|
-
*
|
|
41
|
-
* At compile time each becomes a Foundry UUID enricher, routed to the pack that
|
|
42
|
-
* the target's type compiles into (see {@link packForType}):
|
|
43
|
-
*
|
|
44
|
-
* `@UUID[Compendium.sohl.items.Item.<id>]{Text}`
|
|
45
|
-
* `@UUID[Compendium.sohl.journals.JournalEntry.<id>.JournalEntryPage.<anchorId>]{Text}`
|
|
46
|
-
*
|
|
47
|
-
* **Every address is computed once, when the target is indexed** — see
|
|
48
|
-
* {@link buildWikilinkIndex} — and a link is resolved by looking that value up.
|
|
49
|
-
* Nothing here concatenates a prefix at the point of use.
|
|
50
|
-
*
|
|
51
|
-
* Section links address a **JournalEntryPage**, because Foundry UUIDs cannot
|
|
52
|
-
* target a position inside a page. A heading carrying `{#slug}` therefore starts
|
|
53
|
-
* its own page, whose id is {@link anchorPageId} — derived from the note id and
|
|
54
|
-
* the slug so that the link and the page agree without any shared state.
|
|
55
|
-
*
|
|
56
|
-
* **An anchor on an Item, an Actor or a Macro is a no-op** and is dropped. What
|
|
57
|
-
* such a link does is open that document's **sheet** — not its documentation —
|
|
58
|
-
* and a sheet has no sections to address. Only a JournalEntry link opens a
|
|
59
|
-
* journal, at its first page or at the page an anchor names. An item's pages are
|
|
60
|
-
* reached through its `doc<type>` counterpart, below.
|
|
61
|
-
*
|
|
62
|
-
* **A document and its documentation are two documents.** An item note
|
|
63
|
-
* compiles into an item — and, separately, its prose compiles into a
|
|
64
|
-
* JournalEntry in the journals pack (see
|
|
65
|
-
* {@link sohl.utils.packs.itemDocEntryId}); a macro note works the same way.
|
|
66
|
-
* `skill/wpnc` addresses the skill; the **virtual qualifier**
|
|
67
|
-
* `docskill/wpnc` addresses that skill's documentation, and
|
|
68
|
-
* `docskill/wpnc#crafting` a page within it. `docmacro/autoattack#script`
|
|
69
|
-
* reaches a macro's source. Every doc-carrying type has a `doc<type>`
|
|
70
|
-
* counterpart, formed by prefix and never enumerated; see
|
|
71
|
-
* {@link resolveItemDocType}. Without it a section link to an item note
|
|
72
|
-
* produced a UUID against the *items* pack, which cannot hold a
|
|
73
|
-
* JournalEntryPage, and dead-ended.
|
|
74
|
-
*
|
|
75
|
-
* **The two builds read the qualifier differently, by design.** In Foundry the
|
|
76
|
-
* item and its documentation are separate documents in separate packs, so the
|
|
77
|
-
* two qualifiers resolve to two different UUIDs. On the knowledgebase the item
|
|
78
|
-
* note renders as a single page which *is* its documentation, so `doc<type>` and
|
|
79
|
-
* `<type>` are aliases for the same URL and an anchor on either is an ordinary
|
|
80
|
-
* in-page anchor. One authored link, correct in both.
|
|
81
|
-
*
|
|
82
|
-
* Plain ESM with no Foundry and no filesystem access, so it is unit-testable.
|
|
25
|
+
* @module
|
|
83
26
|
*/
|
|
84
27
|
|
|
85
28
|
import { resolveShortcodeReference } from "./shortcode-references.mjs";
|
|
@@ -100,12 +43,13 @@ import {
|
|
|
100
43
|
renderAddress,
|
|
101
44
|
isAddressTuple,
|
|
102
45
|
completeAddress,
|
|
46
|
+
ownDocumentSystem,
|
|
103
47
|
} from "./address.mjs";
|
|
104
48
|
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
105
|
-
import {
|
|
49
|
+
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
106
50
|
import { systemOf } from "./document-subtypes.mjs";
|
|
107
51
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
108
|
-
import { itemDocEntryId } from "./item-docs.mjs";
|
|
52
|
+
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
109
53
|
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
110
54
|
// The syntax lives in `./wikilink-syntax.mjs`, so the web resolver and this
|
|
111
55
|
// one cannot disagree about what counts as a link.
|
|
@@ -228,10 +172,8 @@ export function buildWikilinkIndex(
|
|
|
228
172
|
// type and a UUID carries the pack name, so the address
|
|
229
173
|
// cannot be derived from the type alone.
|
|
230
174
|
uuid: compendiumUuid(packageId, d.type, d.id, d.pack),
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// is derived from the item's, so its address is knowable
|
|
234
|
-
// here too.
|
|
175
|
+
// The readable note has a separate JournalEntry whose id
|
|
176
|
+
// is derived from the item's id.
|
|
235
177
|
docUuid: compendiumUuid(packageId, "doc", itemDocEntryId(d.id), d.docPack),
|
|
236
178
|
},
|
|
237
179
|
);
|
|
@@ -249,19 +191,8 @@ export function buildWikilinkIndex(
|
|
|
249
191
|
for (const v of foreignByKey.values()) {
|
|
250
192
|
if (v.type) foreignTypes.push(norm(v.type));
|
|
251
193
|
}
|
|
252
|
-
// A
|
|
253
|
-
//
|
|
254
|
-
// here would make it one, and a real type owns its own name, so the
|
|
255
|
-
// virtual reading would stop firing and every `[[docskill-wpnc]]` would
|
|
256
|
-
// resolve nowhere. The virtual form still reaches a foreign documentation
|
|
257
|
-
// entry: it reads as `skill` + `itemDoc`, which the manifest lookup then
|
|
258
|
-
// asks for as `docskill`.
|
|
259
|
-
//
|
|
260
|
-
// Real types are admitted **first**, in a pass of their own, so the test
|
|
261
|
-
// below sees the complete set. Done in one pass it would depend on
|
|
262
|
-
// manifest iteration order wherever the base type is published only by a
|
|
263
|
-
// foreign package — `docmacro` admitted or not according to whether
|
|
264
|
-
// `macro` happened to come first.
|
|
194
|
+
// A legacy index may contain `doc<type>` records. Keep the authored type
|
|
195
|
+
// vocabulary separate so the input alias can resolve to its base type.
|
|
265
196
|
for (const t of foreignTypes) {
|
|
266
197
|
if (!t.startsWith(ITEM_DOC_PREFIX)) types.add(t);
|
|
267
198
|
}
|
|
@@ -334,7 +265,8 @@ function findForeign(index, read) {
|
|
|
334
265
|
*/
|
|
335
266
|
function foreignHits(index, read) {
|
|
336
267
|
if (!read || read.reason || !index.foreign?.size) return [];
|
|
337
|
-
const
|
|
268
|
+
const tuple = completeAddress(read, { package: index.contentPackage, system: NOTE_SYSTEM });
|
|
269
|
+
const wanted = norm(tuple.type);
|
|
338
270
|
const shortcode = norm(read.shortcode);
|
|
339
271
|
|
|
340
272
|
// **An omitted package means this package**, so a short form
|
|
@@ -348,7 +280,7 @@ function foreignHits(index, read) {
|
|
|
348
280
|
// than a filter. That is what makes a cross-package `ambiguous` impossible:
|
|
349
281
|
// one key, one entry.
|
|
350
282
|
if (!read.package) return [];
|
|
351
|
-
const wantedSystem = norm(
|
|
283
|
+
const wantedSystem = norm(tuple.system);
|
|
352
284
|
const hits = [];
|
|
353
285
|
for (const [key, entry] of index.foreign) {
|
|
354
286
|
const parts = readCanonicalKey(key);
|
|
@@ -460,7 +392,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
460
392
|
const out = replaceOutsideCode(markdown, WIKILINK, (all, rawInner, offset) => {
|
|
461
393
|
const parsed = readWikilink(rawInner, {
|
|
462
394
|
package: index.contentPackage,
|
|
463
|
-
system: "
|
|
395
|
+
system: "note",
|
|
464
396
|
types: index.types,
|
|
465
397
|
packages: index.packages,
|
|
466
398
|
noIndexPackages: index.noIndexPackages,
|
|
@@ -516,20 +448,13 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
516
448
|
});
|
|
517
449
|
return unresolvedLink(text || target, target);
|
|
518
450
|
}
|
|
519
|
-
//
|
|
520
|
-
// and a body is under no system block, so it is `none`. Under
|
|
521
|
-
// `none` a system-bearing type addresses its *documentation* — a
|
|
522
|
-
// note's `none` address IS its `doc<type>` entry — which is what a
|
|
523
|
-
// prose link almost always means. Stating the system is how prose
|
|
524
|
-
// reaches the Item instead.
|
|
525
|
-
//
|
|
526
|
-
// Only a type whose *own* document carries a system is redirected.
|
|
527
|
-
// A `macro` and the map types have documentation journals too, but
|
|
528
|
-
// their own documents are core ones already at `none`, so
|
|
529
|
-
// `macro-autoattack` names the Macro and `docmacro-autoattack` its
|
|
530
|
-
// journal — two live addresses the redirect would collapse.
|
|
451
|
+
// Ordinary wikilinks default to readable note content.
|
|
531
452
|
const baseType = resolveItemDocType(qualified.type, index.types);
|
|
532
|
-
|
|
453
|
+
const address = completeAddress(qualified, {
|
|
454
|
+
package: index.contentPackage,
|
|
455
|
+
system: NOTE_SYSTEM,
|
|
456
|
+
});
|
|
457
|
+
itemDoc = address.system === NOTE_SYSTEM && hasDocEntry(baseType ?? qualified.type);
|
|
533
458
|
if (qualified.package === index.contentPackage)
|
|
534
459
|
doc = index.byShortcode.get(`${baseType ?? qualified.type}/${qualified.shortcode}`);
|
|
535
460
|
}
|
|
@@ -615,7 +540,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
615
540
|
// *sheet*, which has no sections, so the anchor has nothing to address
|
|
616
541
|
// and is dropped. Forging a JournalEntryPage id onto a document that
|
|
617
542
|
// can never hold one is what made such links dead-end; an
|
|
618
|
-
// item's pages are addressed through its `
|
|
543
|
+
// an item's pages are addressed through its `note` Address.
|
|
619
544
|
// A `#section` the target declares no heading for. Checked here, and
|
|
620
545
|
// not only by `content-build links`, because this is the build that
|
|
621
546
|
// *emits* the link: `anchorPageId` will hash any slug into a page id,
|
|
@@ -672,7 +597,7 @@ export function resolveReference(index, ref, hint) {
|
|
|
672
597
|
);
|
|
673
598
|
const tuple = parseAddress(ref, {
|
|
674
599
|
package: index.contentPackage,
|
|
675
|
-
system: "
|
|
600
|
+
system: "note",
|
|
676
601
|
types: index.types,
|
|
677
602
|
packages: index.packages,
|
|
678
603
|
noIndexPackages: index.noIndexPackages,
|
|
@@ -699,15 +624,15 @@ export function resolveReference(index, ref, hint) {
|
|
|
699
624
|
if (foreign) return { ...foreign, address: tuple };
|
|
700
625
|
return undefined;
|
|
701
626
|
}
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
if (tuple.system !== (
|
|
627
|
+
const type = tuple.type;
|
|
628
|
+
const documentation = tuple.system === NOTE_SYSTEM && hasDocEntry(type);
|
|
629
|
+
if (tuple.system !== (documentation ? NOTE_SYSTEM : ownDocumentSystem(type)))
|
|
705
630
|
return undefined;
|
|
706
631
|
const local = index.byShortcode?.get(`${type}/${tuple.shortcode}`);
|
|
707
632
|
if (!local) return undefined;
|
|
708
633
|
const addresses = index.uuidByDoc?.get(local);
|
|
709
634
|
const uuid =
|
|
710
|
-
|
|
635
|
+
documentation ?
|
|
711
636
|
Object.hasOwn(local, "documentationUuid") ?
|
|
712
637
|
local.documentationUuid
|
|
713
638
|
: addresses?.docUuid
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.13.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",
|
package/sohl/infobox.mjs
CHANGED
|
@@ -270,7 +270,7 @@ function itemPresentation(item, resolve, block) {
|
|
|
270
270
|
if (!item.shortcode) return undefined;
|
|
271
271
|
if (!item.address)
|
|
272
272
|
return resolve?.(item.shortcode, { kind: "shortcode", type: item.type, system: block });
|
|
273
|
-
const documentation = completeAddress({ ...item.address, system: "
|
|
273
|
+
const documentation = completeAddress({ ...item.address, system: "note" });
|
|
274
274
|
const found = resolve?.(documentation, { type: documentation.type });
|
|
275
275
|
const native =
|
|
276
276
|
!found?.name || !found?.subType ? resolve?.(item.address, { type: item.type }) : undefined;
|
|
@@ -556,7 +556,7 @@ export function affiliationSections(fm, ctx) {
|
|
|
556
556
|
const entries = value.map((skill) => {
|
|
557
557
|
const native = ctx.resolve?.(skill, { type: "skill", system: "sohl" });
|
|
558
558
|
const docAddress =
|
|
559
|
-
isAddressTuple(skill) ? completeAddress({ ...skill, system: "
|
|
559
|
+
isAddressTuple(skill) ? completeAddress({ ...skill, system: "note" }) : undefined;
|
|
560
560
|
const doc = docAddress ? ctx.resolve?.(docAddress, { type: docAddress.type }) : undefined;
|
|
561
561
|
return {
|
|
562
562
|
text: doc?.name || native?.name || humanizeValue(skill),
|