@heroiclands/package-build 22.9.2 → 22.11.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 +56 -0
- package/CONTENT.md +54 -0
- package/bin/content-build.mjs +3 -1
- package/docs/api.md +11 -6
- package/docs/commands.md +1 -1
- package/docs/content-format.md +411 -201
- package/engine/actor-compiler.mjs +20 -7
- package/engine/address-fields.mjs +426 -0
- package/engine/address-values.mjs +80 -0
- package/engine/address.mjs +769 -0
- package/engine/art-fields.mjs +112 -97
- package/engine/art-slots.mjs +81 -0
- package/engine/base-compiler.mjs +61 -7
- package/engine/bundle-notes.mjs +5 -2
- package/engine/bundles.mjs +17 -10
- package/engine/content-address.mjs +23 -186
- package/engine/content-embeds.mjs +18 -4
- package/engine/content-format-check.mjs +50 -22
- package/engine/content-index.mjs +85 -43
- package/engine/content-links.mjs +105 -104
- package/engine/content-lint.mjs +11 -1
- package/engine/content-tables.mjs +9 -1
- package/engine/field-spec.mjs +21 -4
- package/engine/folder-notes.mjs +44 -15
- package/engine/foundry-entries.mjs +3 -0
- package/engine/frontmatter-lint.mjs +150 -47
- package/engine/frontmatter.mjs +55 -10
- package/engine/generate.mjs +7 -1
- package/engine/helpers.mjs +37 -9
- package/engine/holdings.mjs +41 -12
- package/engine/index-records.mjs +2 -0
- package/engine/infobox-render.mjs +2 -1
- package/engine/infobox.mjs +26 -6
- package/engine/item-compiler.mjs +5 -0
- package/engine/item-docs.mjs +7 -44
- package/engine/map-dot.mjs +2 -0
- package/engine/map-notes.mjs +7 -2
- package/engine/map-places.mjs +42 -10
- package/engine/metadata-index.mjs +60 -5
- package/engine/note-addresses.mjs +325 -0
- package/engine/note-vocabulary.mjs +140 -28
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-toc.mjs +17 -2
- package/engine/place-relations.mjs +160 -50
- package/engine/populations.mjs +17 -9
- package/engine/reference-targets.mjs +54 -0
- package/engine/scenes.mjs +5 -2
- package/engine/shortcode-references.mjs +68 -0
- package/engine/site-build.mjs +39 -15
- package/engine/site-index.mjs +57 -75
- package/engine/sql-tables.mjs +43 -9
- package/engine/system-block.mjs +21 -0
- package/engine/system-vocabulary.mjs +46 -0
- package/engine/web-wikilinks.mjs +11 -9
- package/engine/wikilink-syntax.mjs +21 -0
- package/engine/wikilinks.mjs +92 -306
- package/hm3/actors.mjs +8 -24
- package/hm3/item-fields.mjs +15 -5
- package/package.json +1 -1
- package/sohl/infobox.mjs +111 -17
- package/sohl/item-fields.mjs +131 -41
- package/types/engine/address-fields.d.mts +95 -0
- package/types/engine/address-values.d.mts +34 -0
- package/types/engine/address.d.mts +452 -0
- package/types/engine/art-fields.d.mts +59 -53
- package/types/engine/art-slots.d.mts +40 -0
- package/types/engine/base-compiler.d.mts +24 -2
- package/types/engine/bundle-notes.d.mts +2 -2
- package/types/engine/content-address.d.mts +3 -139
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-format-check.d.mts +25 -0
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +6 -5
- package/types/engine/field-spec.d.mts +46 -9
- package/types/engine/folder-notes.d.mts +11 -10
- package/types/engine/frontmatter-lint.d.mts +7 -97
- package/types/engine/frontmatter.d.mts +16 -1
- package/types/engine/helpers.d.mts +2 -2
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/item-docs.d.mts +1 -35
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-addresses.d.mts +41 -0
- package/types/engine/note-vocabulary.d.mts +20 -3
- package/types/engine/place-relations.d.mts +12 -1
- package/types/engine/populations.d.mts +17 -2
- package/types/engine/reference-targets.d.mts +9 -0
- package/types/engine/shortcode-references.d.mts +14 -0
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/site-index.d.mts +9 -30
- package/types/engine/sql-tables.d.mts +2 -2
- package/types/engine/system-block.d.mts +17 -0
- package/types/engine/system-vocabulary.d.mts +16 -0
- package/types/engine/wikilink-syntax.d.mts +7 -0
- package/types/engine/wikilinks.d.mts +6 -109
- package/types/hm3/item-fields.d.mts +5 -0
- package/types/sohl/infobox.d.mts +6 -5
- package/types/sohl/item-fields.d.mts +8 -0
package/engine/wikilinks.mjs
CHANGED
|
@@ -82,15 +82,30 @@
|
|
|
82
82
|
* Plain ESM with no Foundry and no filesystem access, so it is unit-testable.
|
|
83
83
|
*/
|
|
84
84
|
|
|
85
|
+
import { resolveShortcodeReference } from "./shortcode-references.mjs";
|
|
86
|
+
|
|
87
|
+
import { readWikilink } from "./wikilink-syntax.mjs";
|
|
85
88
|
import crypto from "crypto";
|
|
86
89
|
|
|
87
90
|
import { compendiumUuid, ITEM_PACK, packForType, pageUuid, PACK_BY_TYPE } from "./ids.mjs";
|
|
88
91
|
import { readCanonicalKey } from "./content-address.mjs";
|
|
92
|
+
// The address grammar. A wikilink *contains* an Address, so what counts as one
|
|
93
|
+
// is stated there and read here — the anchor and the label are this module's,
|
|
94
|
+
// and the tuple inside them is not.
|
|
95
|
+
import {
|
|
96
|
+
ITEM_DOC_PREFIX,
|
|
97
|
+
readQualifier,
|
|
98
|
+
resolveItemDocType,
|
|
99
|
+
parseAddress,
|
|
100
|
+
renderAddress,
|
|
101
|
+
isAddressTuple,
|
|
102
|
+
completeAddress,
|
|
103
|
+
} from "./address.mjs";
|
|
89
104
|
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
90
|
-
import {
|
|
105
|
+
import { NO_SYSTEM } from "./systems.mjs";
|
|
91
106
|
import { systemOf } from "./document-subtypes.mjs";
|
|
92
107
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
93
|
-
import {
|
|
108
|
+
import { itemDocEntryId } from "./item-docs.mjs";
|
|
94
109
|
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
95
110
|
// The syntax lives in `./wikilink-syntax.mjs`, so the web resolver and this
|
|
96
111
|
// one cannot disagree about what counts as a link.
|
|
@@ -104,227 +119,14 @@ import {
|
|
|
104
119
|
export { ITEM_PACK, PACK_BY_TYPE, packForType };
|
|
105
120
|
|
|
106
121
|
/**
|
|
107
|
-
* The
|
|
108
|
-
* the item: `docskill/wpnc` is the JournalEntry that `skill/wpnc`'s prose
|
|
109
|
-
* compiled into. See {@link resolveItemDocType}.
|
|
110
|
-
*/
|
|
111
|
-
const ITEM_DOC_PREFIX = "doc";
|
|
112
|
-
|
|
113
|
-
const norm = (s) => String(s).toLowerCase().trim();
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Reads a qualifier as the **virtual `doc<type>`** form, or reports that it is
|
|
117
|
-
* not one.
|
|
118
|
-
*
|
|
119
|
-
* A document and its documentation are two documents in two packs, so they
|
|
120
|
-
* need two addresses. `skill/wpnc` is the item; `docskill/wpnc` is the
|
|
121
|
-
* JournalEntry its prose compiled into, and `docmacro/autoattack` is the same
|
|
122
|
-
* arrangement for a macro.
|
|
123
|
-
*
|
|
124
|
-
* The virtual form exists for a type that carries separate documentation
|
|
125
|
-
* ({@link sohl.utils.packs.docEntryTypes} — the set the journals compiler and
|
|
126
|
-
* the link manifest read too), **or** for one that routes to the items pack.
|
|
127
|
-
* The second clause is the older rule and stays: types that compile into items
|
|
128
|
-
* are the open, unenumerated set, and a foreign package may publish an
|
|
129
|
-
* item type this build has never heard of. Dropping it would silently unlink
|
|
130
|
-
* every `doc<type>` address into such a package.
|
|
131
|
-
*
|
|
132
|
-
* A **real** type of the same name always wins: the virtual reading is only
|
|
133
|
-
* consulted for a qualifier no authored note claims.
|
|
134
|
-
*
|
|
135
|
-
* @param {string} qualifier - The already-normalised text before the `/`.
|
|
136
|
-
* @param {Set<string>} types - Every type the content tree contains.
|
|
137
|
-
* @returns {string|null} The underlying document type, or `null` when the
|
|
138
|
-
* qualifier is not a virtual one.
|
|
139
|
-
*/
|
|
140
|
-
export function resolveItemDocType(qualifier, types) {
|
|
141
|
-
if (types.has(qualifier)) return null; // a real type owns its own name
|
|
142
|
-
if (!qualifier.startsWith(ITEM_DOC_PREFIX)) return null;
|
|
143
|
-
const base = qualifier.slice(ITEM_DOC_PREFIX.length);
|
|
144
|
-
if (!base || !types.has(base)) return null;
|
|
145
|
-
if (hasDocEntry(base)) return base;
|
|
146
|
-
return packForType(base).docType === ITEM_PACK.docType ? base : null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Read a link target as a **qualified** `type-shortcode` reference, or report
|
|
151
|
-
* that it does not parse as one.
|
|
152
|
-
*
|
|
153
|
-
* Two separators are accepted, and they are **not** interchangeable in how
|
|
154
|
-
* confidently they mark a target as qualified:
|
|
155
|
-
*
|
|
156
|
-
* - **`type-shortcode`** and its qualified forms — the canonical spelling.
|
|
157
|
-
* Obsidian reads `/` inside a wikilink as a *path* and resolves it
|
|
158
|
-
* against the vault's folders, so a slash-qualified link is a broken link in
|
|
159
|
-
* the editor where the content is now authored.
|
|
160
|
-
* - **`type/shortcode`** — the legacy form, still resolved so that a link
|
|
161
|
-
* written before the vault migrated does not silently die. A slash is
|
|
162
|
-
* *unconditionally* a qualifier: nothing else uses one, so an unknown type
|
|
163
|
-
* before it is reported rather than guessed at. The split is at the **last**
|
|
164
|
-
* slash, as it always was.
|
|
165
|
-
*
|
|
166
|
-
* **The grammar is strict, and omission runs left to right**:
|
|
167
|
-
*
|
|
168
|
-
* ```text
|
|
169
|
-
* [[[[<package>-]<system>-]<type>-]<shortcode>]
|
|
170
|
-
* ```
|
|
171
|
-
*
|
|
172
|
-
* So the written forms are exactly the suffixes of the canonical address —
|
|
173
|
-
* `type-shortcode`, `system-type-shortcode`, `package-system-type-shortcode` —
|
|
174
|
-
* and **`package-type-shortcode` is not one of them**. A link into another
|
|
175
|
-
* package must therefore be fully qualified, which is the price of the segment
|
|
176
|
-
* being positional rather than tagged.
|
|
177
|
-
*
|
|
178
|
-
* **Parsing is plain positional counting**, the same rule
|
|
179
|
-
* {@link readCanonicalKey} follows, and it is sound for the same reason: every
|
|
180
|
-
* segment matches `ADDRESS_SEGMENT_PATTERN` (enforced on shortcodes by
|
|
181
|
-
* `content-lint.mjs`), so the hyphen is purely a
|
|
182
|
-
* separator and the count alone determines every field. Verified across the
|
|
183
|
-
* four content trees: 138,204 authored shortcodes, none carrying a separator.
|
|
184
|
-
*
|
|
185
|
-
* That replaced a first-hyphen split which let a shortcode contain a hyphen
|
|
186
|
-
* (`trauma-self-pro` → `trauma` + `self-pro`). The tolerance predates the
|
|
187
|
-
* charset rule and
|
|
188
|
-
* outlived it; no tree has used it, and keeping it would make a three-segment
|
|
189
|
-
* target ambiguous between a system and a hyphenated shortcode.
|
|
190
|
-
*
|
|
191
|
-
* **`sohl` is both a package and a system**, and positional counting is what
|
|
192
|
-
* makes that harmless: three segments is `<system>-<type>-<shortcode>` whatever
|
|
193
|
-
* the first segment could also have named, and four is the full form. Nothing
|
|
194
|
-
* has to guess which sense was meant.
|
|
195
|
-
*
|
|
196
|
-
* **A partial address states what it states, and the rest is not invented.** An
|
|
197
|
-
* omitted **system** is a *wildcard* — most links target items, which belong to
|
|
198
|
-
* a system — so the caller matches on the segments supplied and requires
|
|
199
|
-
* exactly one hit. An omitted **package** is instead *defaulted* to the citing
|
|
200
|
-
* note's own, so an unqualified link resolves locally and only locally.
|
|
201
|
-
*
|
|
202
|
-
* @param {string} target - The link target, anchor already removed.
|
|
203
|
-
* @param {Set<string>} types - Every type the content tree contains.
|
|
204
|
-
* @param {Set<string>} [packages] - Every package an address may name. Omitted
|
|
205
|
-
* by callers that resolve within one package, where the form cannot occur.
|
|
206
|
-
* @param {Set<string>} [noIndexPackages] - Packages declared `contentIndex:
|
|
207
|
-
* false` — a Foundry dependency only, with no fetched index. A fully
|
|
208
|
-
* qualified target naming one is refused with `no-content-index` before its
|
|
209
|
-
* type is even considered, since there is no index to resolve it against.
|
|
210
|
-
* @returns {{type: string, shortcode: string, itemDoc: boolean,
|
|
211
|
-
* package?: string, system?: string, reason?: undefined}
|
|
212
|
-
* | {reason: "unknown-type"|"no-content-index", package?: string} | null}
|
|
213
|
-
* The resolved qualifier; a `reason` when the target is definitely qualified
|
|
214
|
-
* but names no known type or no fetched index; or `null` when it is not an
|
|
215
|
-
* address at all.
|
|
216
|
-
*/
|
|
217
|
-
export function readQualifier(target, types, packages, noIndexPackages) {
|
|
218
|
-
// **Package, system and type are lowercase; the shortcode is not.** A
|
|
219
|
-
// shortcode is case-sensitive and routinely mixed — `Clb`, `LtShoe`,
|
|
220
|
-
// `HsTunic` — so it is written as the note declares it. The three segments
|
|
221
|
-
// in front of it are closed vocabularies with one spelling each, and
|
|
222
|
-
// accepting `Skill` beside `skill` would bless two ways of writing one
|
|
223
|
-
// address. Reported rather than folded, so the corpus has one form.
|
|
224
|
-
//
|
|
225
|
-
// Tested only once the target *parses*: a note name is full of capitals
|
|
226
|
-
// (`[[Shock State]]`), and calling that a badly-cased address rather than
|
|
227
|
-
// not an address would name the wrong mistake. Neither tree carries a
|
|
228
|
-
// violation — 10,538 authored targets — so this pins a rule already kept.
|
|
229
|
-
const read = readQualifierCased(target, types, packages, noIndexPackages);
|
|
230
|
-
if (read && !read.reason && qualifyingSegments(target).some((s) => /[A-Z]/.test(s))) {
|
|
231
|
-
return { reason: "not-lowercase" };
|
|
232
|
-
}
|
|
233
|
-
return read;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* The segments of a target that must be lowercase — everything but the
|
|
238
|
-
* shortcode, which is case-sensitive and keeps whatever the note declares.
|
|
239
|
-
*
|
|
240
|
-
* @param {string} target - The link target, anchor already removed.
|
|
241
|
-
* @returns {string[]} The package / system / type segments, as written.
|
|
242
|
-
*/
|
|
243
|
-
function qualifyingSegments(target) {
|
|
244
|
-
const slash = target.lastIndexOf("/");
|
|
245
|
-
// The legacy `type/shortcode` form states only a type.
|
|
246
|
-
if (slash > 0) return [target.slice(0, slash)];
|
|
247
|
-
const parts = target.split("-");
|
|
248
|
-
return parts.slice(0, -1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* {@link readQualifier} without the lowercase rule — the grammar alone.
|
|
122
|
+
* The address grammar, under the names the link resolvers hold it by.
|
|
253
123
|
*
|
|
254
|
-
* @
|
|
255
|
-
* @
|
|
256
|
-
* @param {Set<string>} [packages]
|
|
257
|
-
* @param {Set<string>} [noIndexPackages]
|
|
258
|
-
* @returns {object|null}
|
|
124
|
+
* {@link readQualifier} reads a link target as the partial Address the matchers
|
|
125
|
+
* consume, and {@link resolveItemDocType} is the `doc<type>` reading it applies.
|
|
259
126
|
*/
|
|
260
|
-
|
|
261
|
-
// The slash form is legacy and states neither package nor system, so it is
|
|
262
|
-
// read first and separately. A slash is unconditionally a qualifier —
|
|
263
|
-
// nothing else uses one — which is why an unknown type before it is
|
|
264
|
-
// *reported* rather than read as prose.
|
|
265
|
-
const slash = target.lastIndexOf("/");
|
|
266
|
-
if (slash > 0) {
|
|
267
|
-
const read = readTypeAndCode(target.slice(0, slash), target.slice(slash + 1), types);
|
|
268
|
-
return read ?? { reason: "unknown-type" };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const parts = target.split("-");
|
|
272
|
-
switch (parts.length) {
|
|
273
|
-
// `<type>-<shortcode>`
|
|
274
|
-
case 2:
|
|
275
|
-
return readTypeAndCode(parts[0], parts[1], types);
|
|
276
|
-
|
|
277
|
-
// `<system>-<type>-<shortcode>` — the package defaults to local.
|
|
278
|
-
case 3: {
|
|
279
|
-
const system = norm(parts[0]);
|
|
280
|
-
if (!isSystemSegment(system)) return null;
|
|
281
|
-
const read = readTypeAndCode(parts[1], parts[2], types);
|
|
282
|
-
return read && { ...read, system };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// `<package>-<system>-<type>-<shortcode>` — the only form that names
|
|
286
|
-
// another package, and the reason a cross-package link must be fully
|
|
287
|
-
// qualified.
|
|
288
|
-
case 4: {
|
|
289
|
-
const pkg = norm(parts[0]);
|
|
290
|
-
// Checked before the type: a package with no fetched index has no
|
|
291
|
-
// vocabulary to resolve the rest of the target against, and the
|
|
292
|
-
// fix is the config declaration, not the shortcode.
|
|
293
|
-
if (noIndexPackages?.has(pkg)) return { reason: "no-content-index", package: pkg };
|
|
294
|
-
if (!packages?.has(pkg)) return null;
|
|
295
|
-
const system = norm(parts[1]);
|
|
296
|
-
if (!isSystemSegment(system)) return null;
|
|
297
|
-
const read = readTypeAndCode(parts[2], parts[3], types);
|
|
298
|
-
return read && { ...read, system, package: pkg };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// One segment is a bare name, which is not an address; five or
|
|
302
|
-
// more is not a hyphenated shortcode but a name that happens to carry
|
|
303
|
-
// separators, since no segment may contain one.
|
|
304
|
-
default:
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
127
|
+
export { readQualifier, resolveItemDocType };
|
|
308
128
|
|
|
309
|
-
|
|
310
|
-
* Resolve a qualifier/shortcode pair, honouring the virtual `doc<type>` form.
|
|
311
|
-
*
|
|
312
|
-
* @param {string} rawType
|
|
313
|
-
* @param {string} rawCode
|
|
314
|
-
* @param {Set<string>} types
|
|
315
|
-
* @returns {{type: string, shortcode: string, itemDoc: boolean} | null}
|
|
316
|
-
* `null` when the qualifier names no known type, or the shortcode is empty.
|
|
317
|
-
*/
|
|
318
|
-
function readTypeAndCode(rawType, rawCode, types) {
|
|
319
|
-
const shortcode = norm(rawCode);
|
|
320
|
-
if (!shortcode) return null;
|
|
321
|
-
|
|
322
|
-
let type = norm(rawType);
|
|
323
|
-
const base = resolveItemDocType(type, types);
|
|
324
|
-
if (base) return { type: base, shortcode, itemDoc: true };
|
|
325
|
-
if (!types.has(type)) return null;
|
|
326
|
-
return { type, shortcode, itemDoc: false };
|
|
327
|
-
}
|
|
129
|
+
const norm = (s) => String(s).toLowerCase().trim();
|
|
328
130
|
|
|
329
131
|
/**
|
|
330
132
|
* The deterministic JournalEntryPage id for one anchor: SHA-256 of
|
|
@@ -384,7 +186,7 @@ export function buildWikilinkIndex(
|
|
|
384
186
|
packageId,
|
|
385
187
|
foreign,
|
|
386
188
|
contentPackage,
|
|
387
|
-
{ assets, noIndexPackages } = {},
|
|
189
|
+
{ assets, noIndexPackages, referenceTargets, foreignReferences } = {},
|
|
388
190
|
) {
|
|
389
191
|
if (!packageId) {
|
|
390
192
|
throw new Error(
|
|
@@ -479,6 +281,8 @@ export function buildWikilinkIndex(
|
|
|
479
281
|
|
|
480
282
|
return {
|
|
481
283
|
byShortcode,
|
|
284
|
+
referenceTargets,
|
|
285
|
+
foreignReferences,
|
|
482
286
|
types,
|
|
483
287
|
uuidByDoc,
|
|
484
288
|
packageId,
|
|
@@ -654,8 +458,14 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
654
458
|
// one capture group. It is what makes two identical unresolved links on
|
|
655
459
|
// one note tellable apart, and a position reportable at all.
|
|
656
460
|
const out = replaceOutsideCode(markdown, WIKILINK, (all, rawInner, offset) => {
|
|
657
|
-
const parsed =
|
|
658
|
-
|
|
461
|
+
const parsed = readWikilink(rawInner, {
|
|
462
|
+
package: index.contentPackage,
|
|
463
|
+
system: "none",
|
|
464
|
+
types: index.types,
|
|
465
|
+
packages: index.packages,
|
|
466
|
+
noIndexPackages: index.noIndexPackages,
|
|
467
|
+
});
|
|
468
|
+
const target = parsed.rawTarget;
|
|
659
469
|
const slug = parsed.anchor || null;
|
|
660
470
|
|
|
661
471
|
// **Every link carries a label**. Without one there is nothing
|
|
@@ -692,12 +502,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
692
502
|
if (target === "" && slug) {
|
|
693
503
|
doc = { type, id, pack, docPack };
|
|
694
504
|
} else {
|
|
695
|
-
const qualified =
|
|
696
|
-
target,
|
|
697
|
-
index.types,
|
|
698
|
-
index.packages,
|
|
699
|
-
index.noIndexPackages,
|
|
700
|
-
);
|
|
505
|
+
const qualified = parsed.problem ?? parsed.target;
|
|
701
506
|
qualifiedRead = qualified;
|
|
702
507
|
// A target that does not parse as an address is a defect: there is
|
|
703
508
|
// no second namespace left to fall through to.
|
|
@@ -723,11 +528,10 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
723
528
|
// their own documents are core ones already at `none`, so
|
|
724
529
|
// `macro-autoattack` names the Macro and `docmacro-autoattack` its
|
|
725
530
|
// journal — two live addresses the redirect would collapse.
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
doc = index.byShortcode.get(`${qualified.type}/${qualified.shortcode}`);
|
|
531
|
+
const baseType = resolveItemDocType(qualified.type, index.types);
|
|
532
|
+
itemDoc = Boolean(baseType);
|
|
533
|
+
if (qualified.package === index.contentPackage)
|
|
534
|
+
doc = index.byShortcode.get(`${baseType ?? qualified.type}/${qualified.shortcode}`);
|
|
731
535
|
}
|
|
732
536
|
if (!doc) {
|
|
733
537
|
// Nothing local answers. A foreign package may publish this
|
|
@@ -843,42 +647,6 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
843
647
|
return { markdown: out, unresolved };
|
|
844
648
|
}
|
|
845
649
|
|
|
846
|
-
/**
|
|
847
|
-
* The short `type/shortcode` view of an index's foreign entries.
|
|
848
|
-
*
|
|
849
|
-
* Derived once per index and memoised: a compile resolves references on every
|
|
850
|
-
* one of thousands of notes, and rebuilding the view per note would rescan a
|
|
851
|
-
* dependency's whole published index each time. Keyed on the index object, so
|
|
852
|
-
* it is discarded with it.
|
|
853
|
-
*
|
|
854
|
-
* An address two foreign packages both claim is left out rather than resolved
|
|
855
|
-
* to whichever loaded first — the same rule the wikilink resolver follows.
|
|
856
|
-
*
|
|
857
|
-
* @type {WeakMap<object, Map<string, object>>}
|
|
858
|
-
*/
|
|
859
|
-
const FOREIGN_BY_SHORTCODE = new WeakMap();
|
|
860
|
-
|
|
861
|
-
/** The memoised short view of `index.foreign`. */
|
|
862
|
-
function foreignByShortcode(index) {
|
|
863
|
-
const cached = FOREIGN_BY_SHORTCODE.get(index);
|
|
864
|
-
if (cached) return cached;
|
|
865
|
-
const short = new Map();
|
|
866
|
-
const ambiguous = new Set();
|
|
867
|
-
for (const [key, value] of index?.foreign ?? []) {
|
|
868
|
-
const parts = readCanonicalKey(key);
|
|
869
|
-
if (!parts) continue;
|
|
870
|
-
const shortKey = `${norm(parts.type)}/${norm(parts.shortcode)}`;
|
|
871
|
-
if (short.has(shortKey) && short.get(shortKey).package !== value.package) {
|
|
872
|
-
ambiguous.add(shortKey);
|
|
873
|
-
} else {
|
|
874
|
-
short.set(shortKey, value);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
for (const key of ambiguous) short.delete(key);
|
|
878
|
-
FOREIGN_BY_SHORTCODE.set(index, short);
|
|
879
|
-
return short;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
650
|
/**
|
|
883
651
|
* Resolve one reference against a compile's address index.
|
|
884
652
|
*
|
|
@@ -892,46 +660,64 @@ function foreignByShortcode(index) {
|
|
|
892
660
|
* @param {object} index - From {@link buildWikilinkIndex}.
|
|
893
661
|
* @param {unknown} ref - The reference, as authored.
|
|
894
662
|
* @param {object} [hint] - `{type}`, where the caller knows what it expects.
|
|
895
|
-
* @returns {{name?: string, uuid?: string, address?:
|
|
663
|
+
* @returns {{name?: string, uuid?: string, address?: import("./address.mjs").AddressTuple, subType?: string}|undefined}
|
|
896
664
|
* The target, or `undefined` where nothing answers.
|
|
897
665
|
*/
|
|
898
666
|
export function resolveReference(index, ref, hint) {
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
};
|
|
667
|
+
if (hint?.kind === "shortcode")
|
|
668
|
+
return resolveShortcodeReference(
|
|
669
|
+
[index.referenceTargets, index.foreignReferences ?? index.foreign],
|
|
670
|
+
ref,
|
|
671
|
+
hint,
|
|
672
|
+
);
|
|
673
|
+
const tuple = parseAddress(ref, {
|
|
674
|
+
package: index.contentPackage,
|
|
675
|
+
system: "none",
|
|
676
|
+
types: index.types,
|
|
677
|
+
packages: index.packages,
|
|
678
|
+
noIndexPackages: index.noIndexPackages,
|
|
679
|
+
type: hint?.type,
|
|
680
|
+
});
|
|
681
|
+
if (tuple.reason) return undefined;
|
|
682
|
+
const key = renderAddress(tuple);
|
|
683
|
+
const published =
|
|
684
|
+
index.referenceTargets?.get(key) ??
|
|
685
|
+
index.foreignReferences?.get(key) ??
|
|
686
|
+
index.foreign?.get(key);
|
|
687
|
+
if (published)
|
|
688
|
+
return {
|
|
689
|
+
name: published.name,
|
|
690
|
+
subType: published.subType,
|
|
691
|
+
...(published.uuid ? { uuid: published.uuid } : {}),
|
|
692
|
+
address: tuple,
|
|
693
|
+
};
|
|
694
|
+
if (index.referenceTargets) return undefined;
|
|
695
|
+
{
|
|
696
|
+
const key = renderAddress(tuple);
|
|
697
|
+
if (tuple.package !== index.contentPackage) {
|
|
698
|
+
const foreign = index.foreign?.get(key);
|
|
699
|
+
if (foreign) return { ...foreign, address: tuple };
|
|
700
|
+
return undefined;
|
|
934
701
|
}
|
|
702
|
+
const base = resolveItemDocType(tuple.type, index.types);
|
|
703
|
+
const type = base ?? tuple.type;
|
|
704
|
+
if (tuple.system !== (base ? NO_SYSTEM : systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS)))
|
|
705
|
+
return undefined;
|
|
706
|
+
const local = index.byShortcode?.get(`${type}/${tuple.shortcode}`);
|
|
707
|
+
if (!local) return undefined;
|
|
708
|
+
const addresses = index.uuidByDoc?.get(local);
|
|
709
|
+
const uuid =
|
|
710
|
+
base ?
|
|
711
|
+
Object.hasOwn(local, "documentationUuid") ?
|
|
712
|
+
local.documentationUuid
|
|
713
|
+
: addresses?.docUuid
|
|
714
|
+
: addresses?.uuid;
|
|
715
|
+
return {
|
|
716
|
+
name: local.name,
|
|
717
|
+
subType: local.subType,
|
|
718
|
+
...(uuid ? { uuid } : {}),
|
|
719
|
+
address: tuple,
|
|
720
|
+
};
|
|
935
721
|
}
|
|
936
722
|
return undefined;
|
|
937
723
|
}
|
package/hm3/actors.mjs
CHANGED
|
@@ -26,13 +26,9 @@
|
|
|
26
26
|
* the note says which, by writing `hm3.type`. An absent one is an error naming
|
|
27
27
|
* the note, never a default.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* `data.templatePriority` → `flags.hm3.templatePriority` — and every one of
|
|
33
|
-
* them is *declared as that source* rather than as the bare key the corpus
|
|
34
|
-
* writes, so the specification's mapping is executable rather than
|
|
35
|
-
* aspirational. Plus the two anchored prose sections: `{#appearance}` is HM3's
|
|
29
|
+
* Shared gender, occupation and template priority map to HM3's native fields.
|
|
30
|
+
* Native species text is independent of the lore Address in `data.species`.
|
|
31
|
+
* The anchored prose section `{#appearance}` supplies HM3's
|
|
36
32
|
* `description` and `{#dossier}` its
|
|
37
33
|
* `biography`. Everything else an HM3 actor carries — the thirteen abilities,
|
|
38
34
|
* the sunsign, `move`, `fatigue`, `shockIndex`, a creature's `loadRating` — has
|
|
@@ -70,31 +66,19 @@ import {
|
|
|
70
66
|
import { blockField, blockProperty, mergeSystemData } from "../engine/system-block.mjs";
|
|
71
67
|
|
|
72
68
|
/**
|
|
73
|
-
*
|
|
69
|
+
* Native HM3 facts shared by its actor subtypes.
|
|
74
70
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* position, then the shared source, then the default — and so the
|
|
78
|
-
* author-facing reference can be generated from the same statement the compiler
|
|
79
|
-
* obeys.
|
|
80
|
-
*
|
|
81
|
-
* **Both positions, named separately.** The specification maps
|
|
82
|
-
* `data.species` onto `system.species`, and every HM3 note in the corpus writes
|
|
83
|
-
* `hm3.species`. Until the sweep those are two live positions for one
|
|
84
|
-
* field, so the declaration names both: `name` is the shared source, `legacyKey`
|
|
85
|
-
* the key the block still carries, and the block wins while it is there. Naming
|
|
86
|
-
* only one of them is what made the row unreadable — a plain `species` could
|
|
87
|
-
* not see `data.species`, and a dotted `data.species` could not see the 2,512
|
|
88
|
-
* notes that carry `hm3.species`, each yielding `""` in silence.
|
|
71
|
+
* Species is native text. The note's lore Address in `data.species` is
|
|
72
|
+
* independent and supplies no native value.
|
|
89
73
|
*
|
|
90
74
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
91
75
|
*/
|
|
92
76
|
const ACTOR_FIELDS = Object.freeze([
|
|
93
77
|
{
|
|
94
|
-
name: "
|
|
95
|
-
legacyKey: "species",
|
|
78
|
+
name: "species",
|
|
96
79
|
to: "species",
|
|
97
80
|
...STRING,
|
|
81
|
+
topLevelMeans: "The shared species is a lore Address, independent of native species text.",
|
|
98
82
|
default: "",
|
|
99
83
|
describe: "The kind of creature this is.",
|
|
100
84
|
},
|
package/hm3/item-fields.mjs
CHANGED
|
@@ -76,7 +76,8 @@ import { AS_AUTHORED, NUMBER } from "../engine/field-spec.mjs";
|
|
|
76
76
|
*/
|
|
77
77
|
const GEAR_COMMON = Object.freeze([
|
|
78
78
|
{
|
|
79
|
-
name: "weight",
|
|
79
|
+
name: "data.weight",
|
|
80
|
+
legacyKey: "weight",
|
|
80
81
|
to: "weight",
|
|
81
82
|
...AS_AUTHORED,
|
|
82
83
|
kind: "number",
|
|
@@ -84,7 +85,8 @@ const GEAR_COMMON = Object.freeze([
|
|
|
84
85
|
describe: "Weight of one, in pounds.",
|
|
85
86
|
},
|
|
86
87
|
{
|
|
87
|
-
name: "value",
|
|
88
|
+
name: "data.value",
|
|
89
|
+
legacyKey: "value",
|
|
88
90
|
to: "value",
|
|
89
91
|
...AS_AUTHORED,
|
|
90
92
|
kind: "number",
|
|
@@ -104,7 +106,8 @@ const GEAR_COMMON = Object.freeze([
|
|
|
104
106
|
* @type {import("../engine/field-spec.mjs").FieldSpec}
|
|
105
107
|
*/
|
|
106
108
|
const QUANTITY = Object.freeze({
|
|
107
|
-
name: "quantity",
|
|
109
|
+
name: "data.quantity",
|
|
110
|
+
legacyKey: "quantity",
|
|
108
111
|
to: "quantity",
|
|
109
112
|
...NUMBER,
|
|
110
113
|
default: 1,
|
|
@@ -124,6 +127,11 @@ const QUANTITY = Object.freeze({
|
|
|
124
127
|
* a registry is addressed by what a note calls itself. What the document is
|
|
125
128
|
* called is the map's answer, and only the map's.
|
|
126
129
|
*
|
|
130
|
+
* Each field names both of its authored positions, as
|
|
131
|
+
* {@link module:hm3/actors} does and for the same reason: `name` is the shared
|
|
132
|
+
* source the content format's mapping tables state, and `legacyKey` is the key
|
|
133
|
+
* the `hm3:` block still carries.
|
|
134
|
+
*
|
|
127
135
|
* @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
|
|
128
136
|
*/
|
|
129
137
|
export const HM3_ITEM_FIELDS = Object.freeze({
|
|
@@ -139,7 +147,8 @@ export const HM3_ITEM_FIELDS = Object.freeze({
|
|
|
139
147
|
containergear: Object.freeze([
|
|
140
148
|
...GEAR_COMMON,
|
|
141
149
|
{
|
|
142
|
-
name: "capacity",
|
|
150
|
+
name: "data.capacity",
|
|
151
|
+
legacyKey: "capacity",
|
|
143
152
|
to: "capacity.max",
|
|
144
153
|
...NUMBER,
|
|
145
154
|
default: 0,
|
|
@@ -161,7 +170,8 @@ export const HM3_ITEM_FIELDS = Object.freeze({
|
|
|
161
170
|
// through, and only the mastery level is declared here.
|
|
162
171
|
skill: Object.freeze([
|
|
163
172
|
{
|
|
164
|
-
name: "masteryLevel",
|
|
173
|
+
name: "data.masteryLevel",
|
|
174
|
+
legacyKey: "masteryLevel",
|
|
165
175
|
to: "masteryLevel",
|
|
166
176
|
...NUMBER,
|
|
167
177
|
default: 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.11.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",
|