@heroiclands/package-build 22.9.1 → 22.10.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 +54 -0
- package/CONTENT.md +33 -0
- package/bin/content-build.mjs +3 -1
- package/docs/api.md +10 -6
- package/docs/content-format.md +384 -200
- package/docs/project-setup.md +23 -0
- package/engine/actor-compiler.mjs +20 -7
- package/engine/address-fields.mjs +404 -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 +59 -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 +20 -4
- package/engine/folder-notes.mjs +44 -15
- package/engine/foundry-entries.mjs +3 -0
- package/engine/frontmatter-lint.mjs +144 -43
- 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 +310 -0
- package/engine/note-vocabulary.mjs +145 -26
- 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/githooks/attribution.mjs +82 -0
- package/githooks/commit-message.mjs +26 -0
- package/githooks/commit-msg +3 -35
- package/hm3/actors.mjs +8 -24
- package/hm3/item-fields.mjs +15 -5
- package/package.json +1 -1
- package/sohl/infobox.mjs +80 -17
- package/sohl/item-fields.mjs +114 -41
- package/types/engine/address-fields.d.mts +90 -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 +44 -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 +4 -5
- package/types/sohl/item-fields.d.mts +8 -0
package/engine/site-build.mjs
CHANGED
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
* @module
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
+
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
55
|
+
import { decodeNoteAddresses, noteAddressContext, encodeAddresses } from "./note-addresses.mjs";
|
|
54
56
|
import fs from "node:fs";
|
|
55
57
|
import path from "node:path";
|
|
56
58
|
import { createRequire } from "node:module";
|
|
@@ -136,13 +138,28 @@ function siteCorpusFiles(contentBase, ctx) {
|
|
|
136
138
|
* @param {string} file - Absolute path.
|
|
137
139
|
* @returns {{fm: object, body: string}|null}
|
|
138
140
|
*/
|
|
139
|
-
function readNote(file) {
|
|
141
|
+
function readNote(file, ctx) {
|
|
142
|
+
let parsed;
|
|
140
143
|
try {
|
|
141
|
-
|
|
142
|
-
return { fm: data, body: content };
|
|
144
|
+
parsed = matter(fs.readFileSync(file, "utf8"));
|
|
143
145
|
} catch {
|
|
144
146
|
return null;
|
|
145
147
|
}
|
|
148
|
+
const context = {
|
|
149
|
+
...noteAddressContext(ctx.config ?? {}),
|
|
150
|
+
package: ctx.contentPackage ?? ctx.config?.contentPackage,
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
return { fm: decodeNoteAddresses(parsed.data, context), body: parsed.content };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const position = positionOfYamlPath(parsed.matter, error.keyPath ?? [], {
|
|
156
|
+
key: error.addressKey,
|
|
157
|
+
});
|
|
158
|
+
if (position.line) position.line++;
|
|
159
|
+
error.file = file;
|
|
160
|
+
error.position = position;
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
146
163
|
}
|
|
147
164
|
|
|
148
165
|
/**
|
|
@@ -182,7 +199,7 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
182
199
|
);
|
|
183
200
|
|
|
184
201
|
for (const file of siteCorpusFiles(contentBase, ctx)) {
|
|
185
|
-
const note = readNote(file);
|
|
202
|
+
const note = readNote(file, ctx);
|
|
186
203
|
if (!note) continue;
|
|
187
204
|
applyComputedBeingAge(note.fm, present);
|
|
188
205
|
const { fm, body } = note;
|
|
@@ -288,7 +305,7 @@ export function collectHomepages(contentBase, ctx) {
|
|
|
288
305
|
const pages = [];
|
|
289
306
|
const addressFindings = [];
|
|
290
307
|
for (const file of siteCorpusFiles(contentBase, ctx)) {
|
|
291
|
-
const note = readNote(file);
|
|
308
|
+
const note = readNote(file, ctx);
|
|
292
309
|
if (!note || !isHomepage(note.fm)) continue;
|
|
293
310
|
try {
|
|
294
311
|
addressSlug(note.fm);
|
|
@@ -347,7 +364,7 @@ export function writeHomepages(outRoot, pages, config, { related } = {}) {
|
|
|
347
364
|
});
|
|
348
365
|
const dest = path.join(outRoot, HOMEPAGE_DESTINATION);
|
|
349
366
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
350
|
-
fs.writeFileSync(dest, matter.stringify(page.body, data));
|
|
367
|
+
fs.writeFileSync(dest, matter.stringify(page.body, encodeAddresses(data)));
|
|
351
368
|
}
|
|
352
369
|
return pages.length;
|
|
353
370
|
}
|
|
@@ -377,7 +394,7 @@ export function writeHomepages(outRoot, pages, config, { related } = {}) {
|
|
|
377
394
|
* @param {object} options.config - The resolved build configuration.
|
|
378
395
|
* @returns {object} The gate results and, when they pass, the built index.
|
|
379
396
|
*/
|
|
380
|
-
export function siteGates(pages, findings, { config }) {
|
|
397
|
+
export function siteGates(pages, findings, { config, records }) {
|
|
381
398
|
const out = {
|
|
382
399
|
// Always empty here: the homepage count is decided in `buildSite`
|
|
383
400
|
// before the content walk, and a failing count returns without ever
|
|
@@ -410,7 +427,10 @@ export function siteGates(pages, findings, { config }) {
|
|
|
410
427
|
if (out.unaddressable.length) return out;
|
|
411
428
|
|
|
412
429
|
out.index = buildSiteIndex(pages, {
|
|
430
|
+
package: config.contentPackage,
|
|
413
431
|
foreignIndex: foreign.index,
|
|
432
|
+
foreignReferences: foreign.references,
|
|
433
|
+
records,
|
|
414
434
|
noIndexPackages: noContentIndexPackages(config),
|
|
415
435
|
});
|
|
416
436
|
return out;
|
|
@@ -526,7 +546,7 @@ export function tableUniverse(pages) {
|
|
|
526
546
|
* @param {object} options
|
|
527
547
|
* @param {(data: object, page: object) => void} [options.decorate] - Called
|
|
528
548
|
* with each page's frontmatter, for whatever a consumer's own pass adds.
|
|
529
|
-
* @param {(value: unknown, type: string) => string|null} [options.artSrc] -
|
|
549
|
+
* @param {(value: unknown, type: string, accepts?: Iterable<string>) => string|null} [options.artSrc] -
|
|
530
550
|
* Translates an art address into the pathname the website resolver takes, or
|
|
531
551
|
* `null` where nothing answers it.
|
|
532
552
|
* @param {(src: string) => string} [options.webSrc] - Translates an authored
|
|
@@ -593,17 +613,17 @@ function pageTitle(page) {
|
|
|
593
613
|
*
|
|
594
614
|
* @param {object} data - The frontmatter being emitted, rewritten in place.
|
|
595
615
|
* @param {(src: string) => string} webSrc - The website's pathname resolver.
|
|
596
|
-
* @param {(value: unknown, type: string) => string|null} artSrc -
|
|
597
|
-
* resolver, yielding the pathname `webSrc` takes.
|
|
616
|
+
* @param {(value: unknown, type: string, accepts?: Iterable<string>) => string|null} artSrc -
|
|
617
|
+
* The address resolver, yielding the pathname `webSrc` takes.
|
|
598
618
|
* @returns {void}
|
|
599
619
|
*/
|
|
600
620
|
function resolveArtFields(data, webSrc, artSrc) {
|
|
601
|
-
for (const { key, type } of ART_SLOTS) {
|
|
621
|
+
for (const { key, type, accepts } of ART_SLOTS) {
|
|
602
622
|
const holder = isPlainObject(data.data) ? data.data : null;
|
|
603
623
|
if (!holder) continue;
|
|
604
624
|
const value = holder[key];
|
|
605
625
|
if (typeof value !== "string" || value === "") continue;
|
|
606
|
-
const pathname = artSrc(value, type);
|
|
626
|
+
const pathname = artSrc(value, type, accepts);
|
|
607
627
|
if (pathname === null) delete holder[key];
|
|
608
628
|
else holder[key] = webSrc(pathname);
|
|
609
629
|
}
|
|
@@ -798,7 +818,8 @@ export function renderPages(pages, options) {
|
|
|
798
818
|
});
|
|
799
819
|
|
|
800
820
|
const webSrc = webAddresses(page.file);
|
|
801
|
-
const artSrc = (value, type) =>
|
|
821
|
+
const artSrc = (value, type, accepts) =>
|
|
822
|
+
artPathname(artIndex, value, type, accepts).pathname;
|
|
802
823
|
const resolve = (text) => {
|
|
803
824
|
let t = text;
|
|
804
825
|
if (pass.beforeLinks) t = pass.beforeLinks(t, page);
|
|
@@ -856,7 +877,7 @@ export function renderPages(pages, options) {
|
|
|
856
877
|
data.map = map.name;
|
|
857
878
|
withMap += 1;
|
|
858
879
|
}
|
|
859
|
-
fs.writeFileSync(dest, matter.stringify(body, data));
|
|
880
|
+
fs.writeFileSync(dest, matter.stringify(body, encodeAddresses(data)));
|
|
860
881
|
byKind[page.kind] = (byKind[page.kind] ?? 0) + 1;
|
|
861
882
|
}
|
|
862
883
|
|
|
@@ -1078,7 +1099,10 @@ export function buildSite({ config, sqlTables, locate } = {}) {
|
|
|
1078
1099
|
url: base,
|
|
1079
1100
|
}));
|
|
1080
1101
|
|
|
1081
|
-
const gates = siteGates([...pages, ...homepageEntries], content, {
|
|
1102
|
+
const gates = siteGates([...pages, ...homepageEntries], content, {
|
|
1103
|
+
config: resolved,
|
|
1104
|
+
records: ctx.records,
|
|
1105
|
+
});
|
|
1082
1106
|
if (gatesFailed(gates)) {
|
|
1083
1107
|
return {
|
|
1084
1108
|
gates,
|
package/engine/site-index.mjs
CHANGED
|
@@ -49,9 +49,20 @@
|
|
|
49
49
|
* @module
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
+
import { buildReferenceTargets } from "./reference-targets.mjs";
|
|
53
|
+
|
|
54
|
+
import { resolveShortcodeReference } from "./shortcode-references.mjs";
|
|
55
|
+
|
|
52
56
|
import path from "node:path";
|
|
53
57
|
|
|
54
58
|
import { canonicalKey, readCanonicalKey } from "./content-address.mjs";
|
|
59
|
+
import {
|
|
60
|
+
readQualifier,
|
|
61
|
+
parseAddress,
|
|
62
|
+
renderAddress,
|
|
63
|
+
isAddressTuple,
|
|
64
|
+
completeAddress,
|
|
65
|
+
} from "./address.mjs";
|
|
55
66
|
import { NO_SYSTEM } from "./systems.mjs";
|
|
56
67
|
import { systemOf } from "./document-subtypes.mjs";
|
|
57
68
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
|
|
@@ -156,7 +167,13 @@ function mergeForeign(index, foreignIndex) {
|
|
|
156
167
|
*/
|
|
157
168
|
export function buildSiteIndex(
|
|
158
169
|
entries,
|
|
159
|
-
{
|
|
170
|
+
{
|
|
171
|
+
foreignIndex = new Map(),
|
|
172
|
+
noIndexPackages = new Set(),
|
|
173
|
+
package: builderPackage,
|
|
174
|
+
records = [],
|
|
175
|
+
foreignReferences = new Map(),
|
|
176
|
+
} = {},
|
|
160
177
|
) {
|
|
161
178
|
const index = new Map();
|
|
162
179
|
const contentTypes = new Set();
|
|
@@ -165,7 +182,7 @@ export function buildSiteIndex(
|
|
|
165
182
|
// vendored manifest speaks for. Without it `readQualifier` cannot see the
|
|
166
183
|
// leading package segment of a canonical address, and `kethira-place-x`
|
|
167
184
|
// reads as the unknown type `kethira`.
|
|
168
|
-
const ownPackage = contentPackage();
|
|
185
|
+
const ownPackage = builderPackage ?? contentPackage();
|
|
169
186
|
const packages = new Set(ownPackage ? [ownPackage] : []);
|
|
170
187
|
|
|
171
188
|
// A page is addressed by `(type, shortcode)` and nothing else. Its name,
|
|
@@ -240,7 +257,13 @@ export function buildSiteIndex(
|
|
|
240
257
|
// ordinary in-page anchor. One authored link, correct in both
|
|
241
258
|
// builds — restricted to the types that actually have an item doc,
|
|
242
259
|
// so a qualifier the packs would reject is reported broken here too.
|
|
243
|
-
|
|
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)) {
|
|
244
267
|
contentTypes.add(`doc${type}`);
|
|
245
268
|
index.set(`doc${type}/${shortcode}`.toLowerCase(), value);
|
|
246
269
|
// The canonical documentation address too, so the page answers
|
|
@@ -259,6 +282,8 @@ export function buildSiteIndex(
|
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
return {
|
|
285
|
+
contentPackage: ownPackage,
|
|
286
|
+
referenceTargets: buildReferenceTargets(records, new Map([...foreignReferences, ...index])),
|
|
262
287
|
index,
|
|
263
288
|
ambiguous,
|
|
264
289
|
contentTypes,
|
|
@@ -319,7 +344,7 @@ export function wikiContext(
|
|
|
319
344
|
// none. Taken from the resolved configuration, the same source
|
|
320
345
|
// the index's own addresses are built from, so a bare link cannot
|
|
321
346
|
// resolve against a package the index never keyed.
|
|
322
|
-
contentPackage: contentPackage
|
|
347
|
+
contentPackage: built.contentPackage,
|
|
323
348
|
type,
|
|
324
349
|
errors,
|
|
325
350
|
src,
|
|
@@ -329,79 +354,36 @@ export function wikiContext(
|
|
|
329
354
|
}
|
|
330
355
|
|
|
331
356
|
/**
|
|
332
|
-
* Resolve one
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* and the lookup is narrowed to it.
|
|
340
|
-
*
|
|
341
|
-
* **Without a hint the types are tried in sorted order**, so two trees holding
|
|
342
|
-
* the same note resolve it the same way. A shortcode two types both claim
|
|
343
|
-
* answers with the first alphabetically, which is a stable wrong answer rather
|
|
344
|
-
* than an unstable one; a caller that cares supplies the hint.
|
|
345
|
-
*
|
|
346
|
-
* The `address` on the answer is the slug form — `type-shortcode` — because
|
|
347
|
-
* that is what the book's own link map is keyed by and what a wikilink is
|
|
348
|
-
* written as.
|
|
349
|
-
*
|
|
350
|
-
* @param {SiteIndex} siteIndex - The index.
|
|
351
|
-
* @param {unknown} ref - The reference, as authored.
|
|
352
|
-
* @param {object} [hint] - `{type}`, where the caller knows it.
|
|
353
|
-
* @returns {{name?: string, url?: string, address?: string, subType?: string}|undefined}
|
|
354
|
-
* The page, or `undefined` where nothing answers.
|
|
357
|
+
* Resolve one Address against the site's canonical identity index.
|
|
358
|
+
* A bare Shortcode requires a declared default type. The returned Address is
|
|
359
|
+
* the complete tuple; URL and display name are presentation values.
|
|
360
|
+
* @param {SiteIndex} siteIndex - The site index.
|
|
361
|
+
* @param {unknown} ref - Tuple or authored reference.
|
|
362
|
+
* @param {object} [hint] - A declared default type.
|
|
363
|
+
* @returns {object|undefined} The matching page and tuple.
|
|
355
364
|
*/
|
|
356
365
|
export function resolveInfoboxRef(siteIndex, ref, hint) {
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const segments = wanted.split("-");
|
|
367
|
-
if (segments.length >= 2) {
|
|
368
|
-
keys.push(`${segments[segments.length - 2]}/${segments[segments.length - 1]}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (!hint?.type) {
|
|
372
|
-
for (const type of [...(siteIndex?.contentTypes ?? [])].sort()) {
|
|
373
|
-
keys.push(`${type}/${wanted}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
for (const key of keys) {
|
|
377
|
-
const found = siteIndex?.index?.get(key);
|
|
378
|
-
if (!found) continue;
|
|
379
|
-
const slug = key.includes("/") ? key.replace("/", "-") : addressSlugOfKey(key);
|
|
380
|
-
// No `uuid`, although a foreign entry carries one: this resolver
|
|
381
|
-
// answers for the **published** surfaces, which address a page by URL
|
|
382
|
-
// and an entry in the book by its slug. A compendium reference is what
|
|
383
|
-
// `resolveReference` answers with, from the compile's own index.
|
|
384
|
-
return {
|
|
385
|
-
...(found.name ? { name: found.name } : {}),
|
|
386
|
-
...(found.url ? { url: found.url } : {}),
|
|
387
|
-
...(found.subType ? { subType: found.subType } : {}),
|
|
388
|
-
address: slug,
|
|
389
|
-
};
|
|
366
|
+
if (hint?.kind === "shortcode") {
|
|
367
|
+
const found = resolveShortcodeReference(
|
|
368
|
+
[siteIndex.referenceTargets ?? siteIndex.index],
|
|
369
|
+
ref,
|
|
370
|
+
hint,
|
|
371
|
+
);
|
|
372
|
+
return found ?
|
|
373
|
+
{ name: found.name, url: found.url, subType: found.subType, address: found.address }
|
|
374
|
+
: undefined;
|
|
390
375
|
}
|
|
376
|
+
const context = {
|
|
377
|
+
package: siteIndex.contentPackage ?? contentPackage(),
|
|
378
|
+
system: "none",
|
|
379
|
+
types: siteIndex.contentTypes,
|
|
380
|
+
packages: siteIndex.packages,
|
|
381
|
+
};
|
|
382
|
+
const tuple = parseAddress(ref, { ...context, type: hint?.type });
|
|
383
|
+
if (tuple.reason) return undefined;
|
|
384
|
+
const found =
|
|
385
|
+
siteIndex.referenceTargets?.get(renderAddress(tuple)) ??
|
|
386
|
+
siteIndex.index?.get(renderAddress(tuple));
|
|
387
|
+
if (found) return { name: found.name, url: found.url, subType: found.subType, address: tuple };
|
|
391
388
|
return undefined;
|
|
392
389
|
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* The `type-shortcode` slug of a canonical key.
|
|
396
|
-
*
|
|
397
|
-
* A canonical key is `package-system-type-shortcode`, and its slug is the last
|
|
398
|
-
* two segments — the same rule `contentAddress` states, applied to a key
|
|
399
|
-
* rather than to frontmatter.
|
|
400
|
-
*
|
|
401
|
-
* @param {string} key - The canonical key.
|
|
402
|
-
* @returns {string} The slug.
|
|
403
|
-
*/
|
|
404
|
-
function addressSlugOfKey(key) {
|
|
405
|
-
const segments = String(key).split("-");
|
|
406
|
-
return segments.length >= 2 ? segments.slice(-2).join("-") : key;
|
|
407
|
-
}
|
package/engine/sql-tables.mjs
CHANGED
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
* @module
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
import { parseAddress, renderAddress, isAddressTuple } from "./address.mjs";
|
|
37
|
+
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
38
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
36
39
|
import fs from "node:fs";
|
|
37
40
|
import os from "node:os";
|
|
38
41
|
import path from "node:path";
|
|
@@ -41,7 +44,7 @@ import { FENCE_LINE, parseHeaderArgs } from "./code-fences.mjs";
|
|
|
41
44
|
import { MARKET_CLASSES } from "./market-class.mjs";
|
|
42
45
|
import { parseMarkdownFile } from "./helpers.mjs";
|
|
43
46
|
// The record accessors only — see `engine/index-records.mjs`.
|
|
44
|
-
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
47
|
+
import { isNoteRecord, noteFile, sortKeysDeep } from "./index-records.mjs";
|
|
45
48
|
|
|
46
49
|
/** Rendered in a cell whose value is absent. */
|
|
47
50
|
const EMPTY_CELL = "—";
|
|
@@ -157,12 +160,15 @@ export function findSqlBlocks(markdown) {
|
|
|
157
160
|
* @returns {Promise<{query: (sql: string) => Promise<object[]>,
|
|
158
161
|
* close: () => Promise<void>}>} The open database.
|
|
159
162
|
*/
|
|
160
|
-
export async function openNotesDatabase(records, { dir, dependencies = [] } = {}) {
|
|
163
|
+
export async function openNotesDatabase(records, { dir, dependencies = [], addressContext } = {}) {
|
|
161
164
|
const { DuckDBInstance } = await import("@duckdb/node-api");
|
|
162
165
|
const base = dir ?? fs.mkdtempSync(path.join(os.tmpdir(), "content-sql-"));
|
|
163
166
|
fs.mkdirSync(base, { recursive: true });
|
|
164
167
|
const jsonl = path.join(base, "notes.jsonl");
|
|
165
|
-
fs.writeFileSync(
|
|
168
|
+
fs.writeFileSync(
|
|
169
|
+
jsonl,
|
|
170
|
+
records.map((record) => JSON.stringify(sortKeysDeep(encodeAddresses(record)))).join("\n"),
|
|
171
|
+
);
|
|
166
172
|
|
|
167
173
|
const instance = await DuckDBInstance.create(":memory:");
|
|
168
174
|
const connection = await instance.connect();
|
|
@@ -244,6 +250,11 @@ export async function openNotesDatabase(records, { dir, dependencies = [] } = {}
|
|
|
244
250
|
}
|
|
245
251
|
|
|
246
252
|
return {
|
|
253
|
+
addressContext: addressContext ?? {
|
|
254
|
+
package: records.find((record) => record.package)?.package,
|
|
255
|
+
types: new Set(Object.keys(NOTE_VOCABULARY)),
|
|
256
|
+
system: "none",
|
|
257
|
+
},
|
|
247
258
|
stubsExcluded,
|
|
248
259
|
async query(sql) {
|
|
249
260
|
const reader = await connection.runAndReadAll(sql);
|
|
@@ -368,6 +379,15 @@ function readJsonAuto(file) {
|
|
|
368
379
|
*/
|
|
369
380
|
export async function runSqlQuery(db, sql) {
|
|
370
381
|
const { rows, columnNames } = await db.query(sql);
|
|
382
|
+
if (db.addressContext?.package) {
|
|
383
|
+
for (const row of rows) {
|
|
384
|
+
const value = row[RENDER_ALIASES.ref];
|
|
385
|
+
if (!value) continue;
|
|
386
|
+
const tuple = parseAddress(value, db.addressContext, { declared: true });
|
|
387
|
+
if (tuple.reason) throw new Error(`SQL _ref is not an Address: ${value}`);
|
|
388
|
+
row[RENDER_ALIASES.ref] = tuple;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
371
391
|
return { columns: columnNames.filter((key) => !key.startsWith("_")), rows };
|
|
372
392
|
}
|
|
373
393
|
|
|
@@ -454,7 +474,10 @@ function cellText(value, column) {
|
|
|
454
474
|
* @param {number} [opts.sectionLevel=2] - Heading level for `_section`.
|
|
455
475
|
* @returns {string} The markdown.
|
|
456
476
|
*/
|
|
457
|
-
export function renderSqlTable(
|
|
477
|
+
export function renderSqlTable(
|
|
478
|
+
result,
|
|
479
|
+
{ linkable = () => true, sectionLevel = 2, addressContext } = {},
|
|
480
|
+
) {
|
|
458
481
|
const { columns, rows } = result;
|
|
459
482
|
if (!columns.length) throw new Error("query selects no rendered column");
|
|
460
483
|
|
|
@@ -479,11 +502,18 @@ export function renderSqlTable(result, { linkable = () => true, sectionLevel = 2
|
|
|
479
502
|
const cells = group.rows.map((row) =>
|
|
480
503
|
columns.map((column, index) => {
|
|
481
504
|
const text = cellText(row[column], column);
|
|
482
|
-
const
|
|
483
|
-
if (index !== 0 || !
|
|
505
|
+
const written = row[RENDER_ALIASES.ref];
|
|
506
|
+
if (index !== 0 || !written) return text;
|
|
507
|
+
const ref =
|
|
508
|
+
isAddressTuple(written) ? written : (
|
|
509
|
+
parseAddress(written, addressContext ?? {}, { declared: true })
|
|
510
|
+
);
|
|
511
|
+
if (ref.reason)
|
|
512
|
+
throw new Error(`SQL _ref needs a complete Address context: ${written}`);
|
|
513
|
+
if (!linkable(ref)) return text;
|
|
484
514
|
// A wikilink's own separator is a literal `|`, written `\|`
|
|
485
515
|
// inside a table cell, so the label must not carry one.
|
|
486
|
-
return `[[${ref}\\|${text.replace(/\\?\|/g, "/")}]]`;
|
|
516
|
+
return `[[${renderAddress(ref)}\\|${text.replace(/\\?\|/g, "/")}]]`;
|
|
487
517
|
}),
|
|
488
518
|
);
|
|
489
519
|
const align = columns.map((_column, index) => {
|
|
@@ -542,6 +572,7 @@ export async function prepareSqlTables(db, sources, { linkable } = {}) {
|
|
|
542
572
|
forNote.push({
|
|
543
573
|
markdown: renderSqlTable(result, {
|
|
544
574
|
linkable,
|
|
575
|
+
addressContext: db.addressContext,
|
|
545
576
|
sectionLevel: block.sectionLevel,
|
|
546
577
|
}),
|
|
547
578
|
rows: result.rows.length,
|
|
@@ -621,7 +652,8 @@ export async function prepareTreeSqlTables(contentBase, { config, skipDirectorie
|
|
|
621
652
|
const addresses = new Set(
|
|
622
653
|
indexRecords
|
|
623
654
|
.filter((record) => !record.documents)
|
|
624
|
-
.map((record) => record.address?.
|
|
655
|
+
.map((record) => record.documentation ?? record.address?.canonical)
|
|
656
|
+
.map((target) => target && encodeAddresses(target))
|
|
625
657
|
.filter(Boolean),
|
|
626
658
|
);
|
|
627
659
|
// Each declared dependency's published index, attached as its own schema so
|
|
@@ -639,7 +671,9 @@ export async function prepareTreeSqlTables(contentBase, { config, skipDirectorie
|
|
|
639
671
|
}
|
|
640
672
|
const db = await openNotesDatabase(indexRecords, { dependencies });
|
|
641
673
|
try {
|
|
642
|
-
return await prepareSqlTables(db, sources, {
|
|
674
|
+
return await prepareSqlTables(db, sources, {
|
|
675
|
+
linkable: (ref) => addresses.has(renderAddress(ref)),
|
|
676
|
+
});
|
|
643
677
|
} finally {
|
|
644
678
|
await db.close();
|
|
645
679
|
}
|
package/engine/system-block.mjs
CHANGED
|
@@ -419,6 +419,27 @@ export function retiredTopLevelKey(field) {
|
|
|
419
419
|
return rest === "" ? undefined : rest;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
/**
|
|
423
|
+
* A shared source as the key an author writes, without the container holding it.
|
|
424
|
+
*
|
|
425
|
+
* `data.weight` is authored as `weight:` one level inside `data:`, so every
|
|
426
|
+
* question about the *note* rather than about the resolution order asks it under
|
|
427
|
+
* that spelling: which presentation overlay a row takes, which key a finding
|
|
428
|
+
* names, and whether two statements of one fact are the same fact. The
|
|
429
|
+
* specification's mapping tables write the whole path and a `data:` vocabulary
|
|
430
|
+
* writes the key, and this is what makes the two comparable.
|
|
431
|
+
*
|
|
432
|
+
* It takes a **name** rather than a declaration because both sides of that
|
|
433
|
+
* comparison reach it: one holds a field, the other a row of a table.
|
|
434
|
+
*
|
|
435
|
+
* @param {string|undefined} name - The shared source, as either side spells it.
|
|
436
|
+
* @returns {string} The key, without the container.
|
|
437
|
+
*/
|
|
438
|
+
export function authoredKey(name) {
|
|
439
|
+
const path = String(name ?? "");
|
|
440
|
+
return path.startsWith(DATA_PREFIX) ? path.slice(DATA_PREFIX.length) : path;
|
|
441
|
+
}
|
|
442
|
+
|
|
422
443
|
/**
|
|
423
444
|
* Where a declared field's value came from.
|
|
424
445
|
*
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Systems named by package declarations and pack targets.
|
|
10
|
+
* @param {object} [config] - Resolved build configuration.
|
|
11
|
+
* @returns {string[]} Declared system identifiers.
|
|
12
|
+
*/
|
|
13
|
+
export function declaredSystems(config) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const system of Object.keys(config?.systems ?? {})) {
|
|
16
|
+
if (!out.includes(system)) out.push(system);
|
|
17
|
+
}
|
|
18
|
+
for (const pack of config?.packs ?? []) {
|
|
19
|
+
const system = pack?.system;
|
|
20
|
+
if (typeof system === "string" && system && !out.includes(system)) out.push(system);
|
|
21
|
+
}
|
|
22
|
+
if (out.length) return out;
|
|
23
|
+
const packageWide = config?.stats?.systemId;
|
|
24
|
+
return typeof packageWide === "string" && packageWide ? [packageWide] : [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* System field vocabularies supplied by the configuration and caller.
|
|
29
|
+
* @param {object} [config] - Resolved build configuration.
|
|
30
|
+
* @param {object} [options] - Caller schema context.
|
|
31
|
+
* @param {string} [options.schemaSystem] - Owner of the caller's schemas.
|
|
32
|
+
* @returns {object} System block declarations.
|
|
33
|
+
*/
|
|
34
|
+
export function systemBlocksFor(config, { schemaSystem } = {}) {
|
|
35
|
+
const byName = config?.itemFieldsBySystem ?? {};
|
|
36
|
+
/** @type {Record<string, object>} */
|
|
37
|
+
const blocks = {};
|
|
38
|
+
for (const system of declaredSystems(config)) {
|
|
39
|
+
/** @type {object} */
|
|
40
|
+
const spec = {};
|
|
41
|
+
if (system === schemaSystem) spec.fieldVocabulary = true;
|
|
42
|
+
if (byName[system]) spec.fields = byName[system];
|
|
43
|
+
if (Object.keys(spec).length) blocks[system] = Object.freeze(spec);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze(blocks);
|
|
46
|
+
}
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
// cannot drift apart on it: they disagreed once over the unlabelled hyphen
|
|
42
42
|
// form, which the packs showed as a raw shortcode and the knowledgebase as a
|
|
43
43
|
// name.
|
|
44
|
+
import { readWikilink } from "./wikilink-syntax.mjs";
|
|
44
45
|
import { readQualifier } from "./wikilinks.mjs";
|
|
45
46
|
import { replaceOutsideCode } from "./code-fences.mjs";
|
|
46
47
|
// The canonical `package-system-type-shortcode` key, so a package-qualified
|
|
@@ -390,8 +391,14 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
390
391
|
// Code is verbatim: a `[[…]]` inside a code fence, an indented block or an
|
|
391
392
|
// inline span is source text, not a link.
|
|
392
393
|
return replaceOutsideCode(embedded.markdown, WIKILINK, (all, rawInner) => {
|
|
393
|
-
const parsed =
|
|
394
|
-
|
|
394
|
+
const parsed = readWikilink(rawInner, {
|
|
395
|
+
package: ctx.contentPackage,
|
|
396
|
+
system: "none",
|
|
397
|
+
types: ctx.contentTypes ?? new Set(),
|
|
398
|
+
packages: ctx.packages,
|
|
399
|
+
noIndexPackages: ctx.noIndexPackages,
|
|
400
|
+
});
|
|
401
|
+
const { rawTarget: target, anchor, display } = parsed;
|
|
395
402
|
// An empty label is not a label: `[[x|]]` addresses the target and
|
|
396
403
|
// shows its name, so `""` falls through to the same place `null` does.
|
|
397
404
|
// One reading, from {@link authoredLabel}.
|
|
@@ -415,12 +422,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
415
422
|
// The canonical separator has to be resolved, not merely
|
|
416
423
|
// recognised. `null` here means the target is not an address at all,
|
|
417
424
|
// which is a defect: there is no other namespace to try.
|
|
418
|
-
const read =
|
|
419
|
-
target,
|
|
420
|
-
ctx.contentTypes ?? new Set(),
|
|
421
|
-
ctx.packages,
|
|
422
|
-
ctx.noIndexPackages,
|
|
423
|
-
);
|
|
425
|
+
const read = parsed.problem ?? parsed.target;
|
|
424
426
|
const rawKey = target.toLowerCase();
|
|
425
427
|
const hit =
|
|
426
428
|
lookupRead(ctx.index, read, ctx.contentPackage) ??
|
|
@@ -475,7 +477,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
475
477
|
// names its package. A qualified target that matched more than one
|
|
476
478
|
// document is reported by the matcher instead, not by this set.
|
|
477
479
|
const collideKey =
|
|
478
|
-
read && !read.reason &&
|
|
480
|
+
read && !read.reason && read.package === ctx.contentPackage ?
|
|
479
481
|
`${read.type}/${read.shortcode}`.toLowerCase()
|
|
480
482
|
: rawKey;
|
|
481
483
|
const reason =
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { parseAddress, isAddressTuple } from "./address.mjs";
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* What a `[[…]]` **is**, before anything decides where it points.
|
|
16
18
|
*
|
|
@@ -368,3 +370,22 @@ export function linkFindingMessage({ reason, target, packages, anchor, type, stu
|
|
|
368
370
|
export function isSamePage({ target, anchor }) {
|
|
369
371
|
return !target && Boolean(anchor);
|
|
370
372
|
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Read an operational link target in its enclosing Address context.
|
|
376
|
+
* @param {string} inner - Lexical link contents.
|
|
377
|
+
* @param {object} context - Builder defaults and type vocabulary.
|
|
378
|
+
* @returns {object} Typed target and diagnostic source spelling.
|
|
379
|
+
*/
|
|
380
|
+
export function readWikilink(inner, context) {
|
|
381
|
+
const parsed = parseWikilink(inner);
|
|
382
|
+
const rawTarget = parsed.target;
|
|
383
|
+
if (!rawTarget) return { ...parsed, rawTarget, target: { kind: "same-page" } };
|
|
384
|
+
const target = parseAddress(rawTarget, context);
|
|
385
|
+
if (target.reason === "no-type") target.reason = "not-an-address";
|
|
386
|
+
if (!target.reason && !isAddressTuple(target))
|
|
387
|
+
throw new Error("Wikilink read requires complete builder Address context");
|
|
388
|
+
return target.reason ?
|
|
389
|
+
{ ...parsed, rawTarget, target: { kind: "literal", text: rawTarget }, problem: target }
|
|
390
|
+
: { ...parsed, rawTarget, target };
|
|
391
|
+
}
|