@heroiclands/package-build 21.1.0 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +245 -0
- package/CONTENT.md +107 -3
- package/assets/fonts/libertinus/LibertinusMono-Regular.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Bold.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Italic.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Regular.otf +0 -0
- package/assets/fonts/libertinus/OFL.txt +94 -0
- package/assets/fonts/libertinus/provenance.yaml +11 -0
- package/assets/images/banners/afflictionbnr.webp +0 -0
- package/assets/images/banners/armorclothingbnr.webp +0 -0
- package/assets/images/banners/attributebnr.webp +0 -0
- package/assets/images/banners/containerbnr.webp +0 -0
- package/assets/images/banners/devdocsbnr.webp +0 -0
- package/assets/images/banners/miscgearbnr.webp +0 -0
- package/assets/images/banners/mysticalabilitybnr.webp +0 -0
- package/assets/images/banners/projectilebnr.webp +0 -0
- package/assets/images/banners/rulesbnr.webp +0 -0
- package/assets/images/banners/skillbnr.webp +0 -0
- package/assets/images/banners/traumabnr.webp +0 -0
- package/assets/images/banners/userguidebnr.webp +0 -0
- package/assets/images/banners/weapongearbnr.webp +0 -0
- package/assets/images/provenance.yaml +4 -0
- package/bin/content-build.mjs +19 -5
- package/bin/package-build.mjs +6 -5
- package/ci/emit-asset-index.mjs +32 -0
- package/content-config.mjs +30 -1
- package/docs/api.md +103 -27
- package/docs/commands.md +13 -11
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +523 -111
- package/engine/actor-compiler.mjs +58 -9
- package/engine/art-fields.mjs +292 -0
- package/engine/asset-index.mjs +397 -0
- package/engine/asset-types.mjs +192 -0
- package/engine/base-compiler.mjs +69 -1
- package/engine/bundles.mjs +5 -5
- package/engine/compile-corpus.mjs +4 -0
- package/engine/content-address.mjs +14 -2
- package/engine/content-charset.mjs +5 -1
- package/engine/content-embeds.mjs +314 -0
- package/engine/content-html.mjs +5 -1
- package/engine/content-icons.mjs +5 -1
- package/engine/content-images.mjs +26 -5
- package/engine/content-index.mjs +68 -33
- package/engine/content-links.mjs +164 -24
- package/engine/dependency-bump.mjs +14 -16
- package/engine/frontmatter-lint.mjs +36 -23
- package/engine/helpers.mjs +83 -31
- package/engine/index-records.mjs +63 -4
- package/engine/index.mjs +15 -0
- package/engine/infobox-registry.mjs +50 -3
- package/engine/infobox.mjs +12 -8
- package/engine/item-compiler.mjs +10 -10
- package/engine/journals.mjs +3 -0
- package/engine/macros.mjs +16 -13
- package/engine/map-notes.mjs +29 -22
- package/engine/metadata-index.mjs +44 -36
- package/engine/note-vocabulary.mjs +71 -29
- package/engine/pack-router.mjs +14 -0
- package/engine/packagebuild-index.mjs +124 -0
- package/engine/packages.mjs +83 -0
- package/engine/pathnames.mjs +74 -0
- package/engine/pdf-build.mjs +133 -8
- package/engine/pdf-render.mjs +49 -12
- package/engine/scenes.mjs +12 -9
- package/engine/site-build.mjs +43 -16
- package/engine/site-index.mjs +10 -1
- package/engine/subtype-registry.mjs +21 -0
- package/engine/web-wikilinks.mjs +46 -12
- package/engine/wikilink-syntax.mjs +20 -2
- package/engine/wikilinks.mjs +15 -2
- package/hm3/actors.mjs +11 -38
- package/manifest.mjs +67 -13
- package/package.json +5 -2
- package/packagebuild-metadata.jsonl +13 -0
- package/sohl/actors.mjs +12 -86
- package/sohl/note-schemas.mjs +4 -16
- package/types/content-config.d.mts +8 -0
- package/types/engine/actor-compiler.d.mts +29 -0
- package/types/engine/art-fields.d.mts +192 -0
- package/types/engine/asset-index.d.mts +92 -0
- package/types/engine/asset-types.d.mts +110 -0
- package/types/engine/base-compiler.d.mts +42 -1
- package/types/engine/compile-corpus.d.mts +4 -1
- package/types/engine/content-charset.d.mts +4 -0
- package/types/engine/content-embeds.d.mts +218 -0
- package/types/engine/content-html.d.mts +4 -0
- package/types/engine/content-icons.d.mts +4 -0
- package/types/engine/content-images.d.mts +15 -2
- package/types/engine/content-index.d.mts +27 -15
- package/types/engine/dependency-bump.d.mts +2 -2
- package/types/engine/frontmatter-lint.d.mts +11 -14
- package/types/engine/helpers.d.mts +7 -2
- package/types/engine/index-records.d.mts +44 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +36 -1
- package/types/engine/infobox.d.mts +6 -4
- package/types/engine/macros.d.mts +9 -5
- package/types/engine/map-notes.d.mts +10 -6
- package/types/engine/metadata-index.d.mts +2 -17
- package/types/engine/note-vocabulary.d.mts +30 -0
- package/types/engine/packagebuild-index.d.mts +56 -0
- package/types/engine/packages.d.mts +62 -0
- package/types/engine/pathnames.d.mts +41 -81
- package/types/engine/pdf-build.d.mts +58 -1
- package/types/engine/pdf-render.d.mts +3 -1
- package/types/engine/scenes.d.mts +2 -1
- package/types/engine/site-build.d.mts +5 -1
- package/types/engine/site-index.d.mts +4 -1
- package/types/engine/subtype-registry.d.mts +20 -0
- package/types/engine/web-wikilinks.d.mts +14 -4
- package/types/engine/wikilink-syntax.d.mts +13 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/manifest.d.mts +42 -1
- package/types/sohl/actors.d.mts +0 -1
package/engine/pathnames.mjs
CHANGED
|
@@ -93,6 +93,8 @@
|
|
|
93
93
|
* @module
|
|
94
94
|
*/
|
|
95
95
|
|
|
96
|
+
import { PACKAGEBUILD_PACKAGE } from "./packages.mjs";
|
|
97
|
+
|
|
96
98
|
/**
|
|
97
99
|
* The directory a package ships its files in, and the segment that marks a
|
|
98
100
|
* pathname's first segment as a package name.
|
|
@@ -220,6 +222,78 @@ export function pathnameProblem(raw) {
|
|
|
220
222
|
);
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Whether this build installs anything into a Foundry data directory.
|
|
227
|
+
*
|
|
228
|
+
* A `documentation` package compiles no packs and installs nowhere, so no note
|
|
229
|
+
* of its reaches a Foundry document and no pathname of its can be dead on a
|
|
230
|
+
* surface it never touches. A check that reports a missing Foundry address asks
|
|
231
|
+
* this first, so the one kind with no Foundry surface is not told about it.
|
|
232
|
+
*
|
|
233
|
+
* Read from {@link packageAddresses} rather than from a key, so "does this
|
|
234
|
+
* package have a Foundry root?" is answered in one place by the map that
|
|
235
|
+
* derives every other package's.
|
|
236
|
+
*
|
|
237
|
+
* @param {object} config - The resolved build configuration.
|
|
238
|
+
* @returns {boolean} Whether the package being built has a Foundry root.
|
|
239
|
+
*/
|
|
240
|
+
export function servesFoundry(config) {
|
|
241
|
+
const own = config?.contentPackage ? packageAddresses(config).get(config.contentPackage) : null;
|
|
242
|
+
return Boolean(own?.root);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Why a pathname has no Foundry address, or `""` when it has one.
|
|
247
|
+
*
|
|
248
|
+
* The sibling of {@link pathnameProblem} one step further on: that one asks
|
|
249
|
+
* whether a pathname is written correctly at all, and this one asks whether
|
|
250
|
+
* *this* build can turn a correctly written pathname into a path a Foundry
|
|
251
|
+
* install serves. Both answer in a sentence rather than a code, so the caller
|
|
252
|
+
* that holds a line and a column and the caller that holds only a file say the
|
|
253
|
+
* same thing about the same value.
|
|
254
|
+
*
|
|
255
|
+
* `""` for every pathname that resolves — and for the three shapes that
|
|
256
|
+
* legitimately reach Foundry untranslated: a URL, a protocol-relative
|
|
257
|
+
* `//host/…`, and a `/`-rooted path Foundry serves from its data root. A blank
|
|
258
|
+
* pathname is a deliberate blank and resolves to a blank on every surface, so
|
|
259
|
+
* it is not a problem either.
|
|
260
|
+
*
|
|
261
|
+
* `""` as well for a pathname {@link pathnameProblem} already refuses. That
|
|
262
|
+
* defect has its own finding naming its own replacement, and reporting a second
|
|
263
|
+
* one about the same characters would send the author to one mistake twice.
|
|
264
|
+
*
|
|
265
|
+
* @param {string|null|undefined} raw - The pathname, as authored.
|
|
266
|
+
* @param {object} config - The resolved build configuration.
|
|
267
|
+
* @returns {string} The problem, as a finding's sentence, or `""`.
|
|
268
|
+
*/
|
|
269
|
+
export function foundryAddressProblem(raw, config) {
|
|
270
|
+
if (pathnameProblem(raw)) return "";
|
|
271
|
+
const forms = resolvePathname(raw, config);
|
|
272
|
+
if (forms === null || forms.foundry !== null) return "";
|
|
273
|
+
if (forms.package === PACKAGEBUILD_PACKAGE) {
|
|
274
|
+
return (
|
|
275
|
+
`\`${forms.authored}\` names a file the \`${PACKAGEBUILD_PACKAGE}\` package ` +
|
|
276
|
+
"ships, and Foundry installs no package of that name — package-build is an " +
|
|
277
|
+
"npm dependency of a package rather than a package of its own, so its files " +
|
|
278
|
+
"reach the website and the book and never a Foundry document. Address a " +
|
|
279
|
+
"`/`-rooted path or a URL"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (forms.own) {
|
|
283
|
+
return (
|
|
284
|
+
`\`${forms.authored}\` names a file this package serves, and a ` +
|
|
285
|
+
"`documentation` package has no asset root to serve it from — Foundry " +
|
|
286
|
+
"installs no such package. Address a `/`-rooted path or a URL"
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return (
|
|
290
|
+
`\`${forms.authored}\` names a file the \`${forms.package}\` package ships, and ` +
|
|
291
|
+
"this build declares no relationship with a package of that name, so there is " +
|
|
292
|
+
"no Foundry address to derive. Declare it under `relationships`, or address " +
|
|
293
|
+
"the file by a `/`-rooted path"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
223
297
|
/**
|
|
224
298
|
* Every game system this build compiles content for.
|
|
225
299
|
*
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -64,6 +64,20 @@
|
|
|
64
64
|
* `.typ` source on disk, which is both the diagnostic and the thing a consumer
|
|
65
65
|
* can compile by hand.
|
|
66
66
|
*
|
|
67
|
+
* ## The faces are the toolchain's and the compiler's, never the machine's
|
|
68
|
+
*
|
|
69
|
+
* Every face the book sets resolves from one of two places that travel with the
|
|
70
|
+
* build: the faces this package ships — see {@link BOOK_FONTS_PATH} — and the
|
|
71
|
+
* ones the compiler embeds. The compile passes the shipped directory as
|
|
72
|
+
* `--font-path` and `--ignore-system-fonts` alongside it, so a machine carrying
|
|
73
|
+
* its own copy of a named family cannot quietly set a different book from the
|
|
74
|
+
* same source.
|
|
75
|
+
*
|
|
76
|
+
* `pdf.fonts.path` is searched as well, so a consumer naming a face of its own
|
|
77
|
+
* in `pdf.fonts` still resolves it — and a name nothing resolves is reported,
|
|
78
|
+
* because the compiler says so and a compile that says it still exits 0 with a
|
|
79
|
+
* book set in the fallback.
|
|
80
|
+
*
|
|
67
81
|
* @module
|
|
68
82
|
*/
|
|
69
83
|
|
|
@@ -71,10 +85,12 @@ import fs from "node:fs";
|
|
|
71
85
|
import os from "node:os";
|
|
72
86
|
import path from "node:path";
|
|
73
87
|
import { spawnSync } from "node:child_process";
|
|
88
|
+
import { fileURLToPath } from "node:url";
|
|
74
89
|
|
|
75
90
|
import YAML from "yaml";
|
|
76
91
|
|
|
77
92
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
93
|
+
import { routerFor } from "./pack-router.mjs";
|
|
78
94
|
import { publishesContentPages } from "../content-config.mjs";
|
|
79
95
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
80
96
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
@@ -83,6 +99,8 @@ import { parseDocumentTree, runTreeFilters, planDocument } from "./pdf-toc.mjs";
|
|
|
83
99
|
import { collectContentPages, siteGates, tableUniverse, gatesFailed } from "./site-build.mjs";
|
|
84
100
|
import { resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
85
101
|
import { resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
102
|
+
import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
103
|
+
import { assetAddressIndex } from "./art-fields.mjs";
|
|
86
104
|
import { expandContentTables } from "./content-tables.mjs";
|
|
87
105
|
import { protectCode } from "./code-fences.mjs";
|
|
88
106
|
import { imageSourcesIn } from "./content-images.mjs";
|
|
@@ -379,6 +397,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
379
397
|
skipDirectories: resolved.skipDirectories,
|
|
380
398
|
records,
|
|
381
399
|
});
|
|
400
|
+
// The address space an `![[…]]` embed resolves against — the same one the
|
|
401
|
+
// site builds, so one authored picture reaches both surfaces or neither.
|
|
402
|
+
const assets = assetAddressIndex(records, {
|
|
403
|
+
config: resolved,
|
|
404
|
+
foreign: gates.foreign,
|
|
405
|
+
types: gates.index?.contentTypes ?? [],
|
|
406
|
+
});
|
|
382
407
|
const md = createParser(resolved.icons);
|
|
383
408
|
const glyphs = resolveIconGlyphs(resolved.icons, resolved.pdf.iconFonts, findings);
|
|
384
409
|
|
|
@@ -478,6 +503,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
478
503
|
type: page.fm.type ?? null,
|
|
479
504
|
errors: wikiErrors,
|
|
480
505
|
foreignIndex: gates.foreign.index,
|
|
506
|
+
assets,
|
|
481
507
|
});
|
|
482
508
|
// Code fences are protected for the same reason every other pass
|
|
483
509
|
// protects them: a wikilink shown as an example is prose about a
|
|
@@ -487,7 +513,10 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
487
513
|
findings.push({
|
|
488
514
|
file: page.file,
|
|
489
515
|
severity: "warning",
|
|
490
|
-
|
|
516
|
+
// A link finding names a `reason` from the shared table and no
|
|
517
|
+
// sentence of its own; an embed's directive complaint carries
|
|
518
|
+
// the sentence instead.
|
|
519
|
+
message: err.message ?? (err.reason ? linkFindingMessage(err) : String(err)),
|
|
491
520
|
});
|
|
492
521
|
}
|
|
493
522
|
stageImages(resolvedBody, page.file);
|
|
@@ -504,6 +533,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
504
533
|
// first, because the image lives in the body and the body follows.
|
|
505
534
|
const boxes = noteInfoboxes(page.fm, {
|
|
506
535
|
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
536
|
+
router: routerFor(resolved),
|
|
507
537
|
});
|
|
508
538
|
const panel = infoboxesToTypst(boxes, {
|
|
509
539
|
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
@@ -613,6 +643,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
613
643
|
}
|
|
614
644
|
|
|
615
645
|
const compiled = compileTypst(typPath, pdfPath, resolved.pdf);
|
|
646
|
+
findings.push(...compiled.findings);
|
|
616
647
|
if (!compiled.ok) {
|
|
617
648
|
findings.push({ file: typPath, severity: "error", message: compiled.message });
|
|
618
649
|
return { built: false, reason: null, findings, typ: typPath, pdf: null, stats };
|
|
@@ -620,24 +651,113 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
620
651
|
return { built: true, reason: null, findings, typ: typPath, pdf: pdfPath, stats };
|
|
621
652
|
}
|
|
622
653
|
|
|
654
|
+
/**
|
|
655
|
+
* The faces the book is set in that the compiler does not carry itself.
|
|
656
|
+
*
|
|
657
|
+
* Resolved from this module rather than from the working directory, on the same
|
|
658
|
+
* rule the specification and `--version` follow: a consumer runs the build
|
|
659
|
+
* inside its own repository, and the faces it sets the book in are the ones
|
|
660
|
+
* that came with the toolchain version it resolved.
|
|
661
|
+
*
|
|
662
|
+
* What is here is the **sans**, in the three styles a heading can ask for, the
|
|
663
|
+
* superfamily's **mono** for a package that names it, and the licence they
|
|
664
|
+
* travel under. The serif is not: the compiler embeds one, and a second copy of
|
|
665
|
+
* a face it already carries is a file nothing selects.
|
|
666
|
+
*
|
|
667
|
+
* It is **not** an addressable asset root: the compiler matches a face by
|
|
668
|
+
* family name, so nothing addresses these files and nothing needs to.
|
|
669
|
+
*
|
|
670
|
+
* @type {string}
|
|
671
|
+
*/
|
|
672
|
+
export const BOOK_FONTS_PATH = path.join(
|
|
673
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
674
|
+
"..",
|
|
675
|
+
"assets",
|
|
676
|
+
"fonts",
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* The command line the compile runs, as data.
|
|
681
|
+
*
|
|
682
|
+
* Separate from running it so the flags that decide which faces are in play
|
|
683
|
+
* can be asserted without a compiler installed — which is the half of the
|
|
684
|
+
* invocation that changes what the book looks like.
|
|
685
|
+
*
|
|
686
|
+
* @param {string} typPath - The `.typ` file.
|
|
687
|
+
* @param {string} pdfPath - Where the PDF goes.
|
|
688
|
+
* @param {object} [pdf] - The resolved `pdf:` block.
|
|
689
|
+
* @returns {string[]} The arguments, in order.
|
|
690
|
+
*/
|
|
691
|
+
export function typstArgs(typPath, pdfPath, pdf = {}) {
|
|
692
|
+
// One flag, the paths joined by the platform's separator: the compiler
|
|
693
|
+
// takes a list, and the shipped faces come first so a consumer's own
|
|
694
|
+
// directory extends the set rather than standing in for it.
|
|
695
|
+
const fontPaths = [BOOK_FONTS_PATH];
|
|
696
|
+
if (pdf.fonts?.path) fontPaths.push(pdf.fonts.path);
|
|
697
|
+
return [
|
|
698
|
+
"compile",
|
|
699
|
+
"--ignore-system-fonts",
|
|
700
|
+
"--font-path",
|
|
701
|
+
fontPaths.join(path.delimiter),
|
|
702
|
+
typPath,
|
|
703
|
+
pdfPath,
|
|
704
|
+
];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* The compiler's own warnings, as findings.
|
|
709
|
+
*
|
|
710
|
+
* A compile that says `unknown font family` still exits 0 and still writes a
|
|
711
|
+
* book — one set in whatever face the fallback reached. That is the failure
|
|
712
|
+
* this surface is least able to see, so the compiler's warnings are read back
|
|
713
|
+
* and reported on the same terms as everything else the build finds.
|
|
714
|
+
*
|
|
715
|
+
* Typst writes a warning as a `warning:` line followed by a `┌─ file:line:col`
|
|
716
|
+
* locator over a source excerpt. The message and the position are taken; the
|
|
717
|
+
* excerpt is not, since the reader has the file.
|
|
718
|
+
*
|
|
719
|
+
* @param {string} output - What the compiler wrote to stderr.
|
|
720
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
721
|
+
* severity: string, message: string}>} One finding per warning.
|
|
722
|
+
*/
|
|
723
|
+
export function typstWarnings(output) {
|
|
724
|
+
const lines = String(output || "").split("\n");
|
|
725
|
+
const findings = [];
|
|
726
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
727
|
+
const warned = /^warning: (.+)$/.exec(lines[i]);
|
|
728
|
+
if (!warned) continue;
|
|
729
|
+
const finding = { file: "", severity: "warning", message: warned[1].trim() };
|
|
730
|
+
const at = /^\s*┌─ (.+):(\d+):(\d+)\s*$/.exec(lines[i + 1] ?? "");
|
|
731
|
+
if (at) {
|
|
732
|
+
// The compiler writes the path relative to its own working
|
|
733
|
+
// directory, which is this process's, so resolving it there is what
|
|
734
|
+
// recovers the file a reader can open.
|
|
735
|
+
finding.file = path.resolve(at[1]);
|
|
736
|
+
finding.line = Number(at[2]);
|
|
737
|
+
finding.column = Number(at[3]);
|
|
738
|
+
}
|
|
739
|
+
findings.push(finding);
|
|
740
|
+
}
|
|
741
|
+
return findings;
|
|
742
|
+
}
|
|
743
|
+
|
|
623
744
|
/**
|
|
624
745
|
* Run Typst over the emitted source.
|
|
625
746
|
*
|
|
626
747
|
* @param {string} typPath - The `.typ` file.
|
|
627
748
|
* @param {string} pdfPath - Where the PDF goes.
|
|
628
749
|
* @param {object} pdf - The resolved `pdf:` block.
|
|
629
|
-
* @returns {{ok: boolean, message: string}} What happened
|
|
750
|
+
* @returns {{ok: boolean, message: string, findings: object[]}} What happened,
|
|
751
|
+
* and what the compiler warned about on the way.
|
|
630
752
|
*/
|
|
631
753
|
export function compileTypst(typPath, pdfPath, pdf = {}) {
|
|
632
754
|
const binary = pdf.binary || "typst";
|
|
633
|
-
const args =
|
|
634
|
-
if (pdf.fonts?.path) args.push("--font-path", pdf.fonts.path);
|
|
635
|
-
args.push(typPath, pdfPath);
|
|
755
|
+
const args = typstArgs(typPath, pdfPath, pdf);
|
|
636
756
|
let result;
|
|
637
757
|
try {
|
|
638
758
|
result = spawnSync(binary, args, { encoding: "utf8" });
|
|
639
759
|
} catch (err) {
|
|
640
|
-
return { ok: false, message: `could not run \`${binary}\`: ${err.message}
|
|
760
|
+
return { ok: false, message: `could not run \`${binary}\`: ${err.message}`, findings: [] };
|
|
641
761
|
}
|
|
642
762
|
if (result.error) {
|
|
643
763
|
const missing = /** @type {any} */ (result.error).code === "ENOENT";
|
|
@@ -649,13 +769,18 @@ export function compileTypst(typPath, pdfPath, pdf = {}) {
|
|
|
649
769
|
"`typst compile` over it produces the book. Name another binary with " +
|
|
650
770
|
"`pdf.binary`."
|
|
651
771
|
: `could not run \`${binary}\`: ${result.error.message}`,
|
|
772
|
+
findings: [],
|
|
652
773
|
};
|
|
653
774
|
}
|
|
654
775
|
if (result.status !== 0) {
|
|
655
776
|
const detail = String(result.stderr || result.stdout || "")
|
|
656
777
|
.trim()
|
|
657
778
|
.split("\n")[0];
|
|
658
|
-
return {
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
message: `\`${binary} compile\` failed: ${detail}`,
|
|
782
|
+
findings: [],
|
|
783
|
+
};
|
|
659
784
|
}
|
|
660
|
-
return { ok: true, message: "" };
|
|
785
|
+
return { ok: true, message: "", findings: typstWarnings(result.stderr) };
|
|
661
786
|
}
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -667,14 +667,22 @@ function renderLink(href, inner, ctx) {
|
|
|
667
667
|
* ## The width class is the measure
|
|
668
668
|
*
|
|
669
669
|
* An image with no class is one column wide. That is `width: 100%` of whatever
|
|
670
|
-
* container it is set in —
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* `.full-width` has to leave its column, and
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
670
|
+
* container it is set in — a column of the two the body is set in — so the
|
|
671
|
+
* ordinary case needs nothing but an ordinary block, and lands exactly where it
|
|
672
|
+
* was written.
|
|
673
|
+
*
|
|
674
|
+
* `.full-width` has to leave its column, and only a float placed with
|
|
675
|
+
* `scope: "parent"` spans every column of a page. A float, though, is placed
|
|
676
|
+
* where the page has room rather than where it was written: it is set at the
|
|
677
|
+
* top of the page, above the prose that introduces it, and where the page is
|
|
678
|
+
* too far along to take it, on the next page — after prose that follows it in
|
|
679
|
+
* the note. Document order governs what follows an image, so
|
|
680
|
+
* {@link bookTypstPreamble}'s `book-figure` breaks the page first and places
|
|
681
|
+
* the figure at the top of the fresh one, where nothing is above it to displace
|
|
682
|
+
* it and nothing that follows it can print first.
|
|
683
|
+
*
|
|
684
|
+
* A `.full-width` image that **also states a `float:`** is asking for a float,
|
|
685
|
+
* and keeps one — deferral is the honest consequence of the request.
|
|
678
686
|
*
|
|
679
687
|
* ## A float occupies the measure
|
|
680
688
|
*
|
|
@@ -712,8 +720,10 @@ function renderImage(token, ctx) {
|
|
|
712
720
|
// In the flow where it was written: no class asking for the page, and no
|
|
713
721
|
// position asking for the top or the bottom of the column.
|
|
714
722
|
if (!float && scope === "column") return `\n${figure}\n\n`;
|
|
715
|
-
|
|
716
|
-
|
|
723
|
+
// The page, in document order: a width class says how wide the picture is
|
|
724
|
+
// and not when it appears.
|
|
725
|
+
if (!float) return `\n#book-figure[\n${figure}\n]\n\n`;
|
|
726
|
+
return `\n#place(${float.align}, float: true, scope: "${scope}", clearance: 0.7em)[\n${figure}\n]\n\n`;
|
|
717
727
|
}
|
|
718
728
|
|
|
719
729
|
/**
|
|
@@ -904,6 +914,26 @@ export function bookTypstPreamble() {
|
|
|
904
914
|
" place(top + left)[#book-plate(kicker, title, banner, 9cm, body)]\n" +
|
|
905
915
|
" pagebreak()\n" +
|
|
906
916
|
"}",
|
|
917
|
+
// A full-width figure spans the page in document order. Only a float
|
|
918
|
+
// spans every column, and a float is placed where the page has room
|
|
919
|
+
// rather than where it was written — at the top, above the prose that
|
|
920
|
+
// introduces it, or on the next page when this one is too far along.
|
|
921
|
+
// Breaking first puts it at the top of a page whose float region is
|
|
922
|
+
// empty, which is the one place it cannot be displaced. A picture
|
|
923
|
+
// taller than the page takes a page of its own, for the reason
|
|
924
|
+
// `book-wide` states.
|
|
925
|
+
//
|
|
926
|
+
// `measure` alone is stable here. A rule reading `here().position()`
|
|
927
|
+
// to keep the break for the cases that need it does not converge: the
|
|
928
|
+
// position decides the layout and the layout decides the position.
|
|
929
|
+
"#let book-figure(body) = context {\n" +
|
|
930
|
+
" if measure(block(width: book-text-width)[#body]).height >= book-text-height * 0.88 {\n" +
|
|
931
|
+
" page(columns: 1)[#body]\n" +
|
|
932
|
+
" } else {\n" +
|
|
933
|
+
" pagebreak(weak: true)\n" +
|
|
934
|
+
' place(top, float: true, scope: "parent", clearance: 0.7em)[#body]\n' +
|
|
935
|
+
" }\n" +
|
|
936
|
+
"}",
|
|
907
937
|
// Wide content spans the page, and how it spans depends on how tall it
|
|
908
938
|
// is: a float is unbreakable and silently overflows, so anything taller
|
|
909
939
|
// than a page takes pages of its own instead.
|
|
@@ -1007,7 +1037,9 @@ export function bookTypstPreamble() {
|
|
|
1007
1037
|
* @param {string} opts.title - The document's title.
|
|
1008
1038
|
* @param {string} [opts.subtitle] - Shown under it on the title page.
|
|
1009
1039
|
* @param {string[]} [opts.front] - Rendered Typst for each front-matter file.
|
|
1010
|
-
* @param {object} [opts.fonts] - `{ serif, sans, mono }` family names.
|
|
1040
|
+
* @param {object} [opts.fonts] - `{ serif, sans, mono }` family names. Each
|
|
1041
|
+
* falls back to the face the toolchain ships or the compiler embeds, so a
|
|
1042
|
+
* caller that names none still sets the book in all three.
|
|
1011
1043
|
* @param {string} [opts.version] - Stamped on the title page when given.
|
|
1012
1044
|
* @param {string} [opts.preamble] - Definitions the bodies call, emitted once
|
|
1013
1045
|
* above the title page. A panel every entry draws is a set of rules stated
|
|
@@ -1029,8 +1061,13 @@ export function renderBook({
|
|
|
1029
1061
|
preamble = "",
|
|
1030
1062
|
banners = new Map(),
|
|
1031
1063
|
} = {}) {
|
|
1064
|
+
// The two halves of one superfamily, chosen together: matched metrics are
|
|
1065
|
+
// most of why the sans can carry every heading over a serif body without
|
|
1066
|
+
// the page reading as two books. The mono is a separate claim — the
|
|
1067
|
+
// superfamily's own is missing the Latin Extended Additional letters this
|
|
1068
|
+
// corpus spells names with, where the compiler's embedded face carries them.
|
|
1032
1069
|
const serif = fonts.serif || "Libertinus Serif";
|
|
1033
|
-
const sans = fonts.sans ||
|
|
1070
|
+
const sans = fonts.sans || "Libertinus Sans";
|
|
1034
1071
|
const mono = fonts.mono || "DejaVu Sans Mono";
|
|
1035
1072
|
const out = [];
|
|
1036
1073
|
|
package/engine/scenes.mjs
CHANGED
|
@@ -53,7 +53,6 @@ import log from "loglevel";
|
|
|
53
53
|
import {
|
|
54
54
|
parseMarkdownFile,
|
|
55
55
|
sohlField,
|
|
56
|
-
resolveImg,
|
|
57
56
|
resolveName,
|
|
58
57
|
slugify,
|
|
59
58
|
defaultStats,
|
|
@@ -130,15 +129,15 @@ export class Scenes extends BasePackCompiler {
|
|
|
130
129
|
static label = "map";
|
|
131
130
|
|
|
132
131
|
/**
|
|
133
|
-
* A map note's `
|
|
134
|
-
* compiler refuses a note without one. It lands on the scene's level
|
|
135
|
-
* than on a property spelled `img`, which makes no difference to the
|
|
136
|
-
* question this declaration answers — the authored
|
|
137
|
-
* The place Adventure this pass bundles carries it too.
|
|
132
|
+
* A map note's `bgImage` is its background art, and it is **required**: the
|
|
133
|
+
* map compiler refuses a note without one. It lands on the scene's level
|
|
134
|
+
* rather than on a property spelled `img`, which makes no difference to the
|
|
135
|
+
* question this declaration answers — the authored address reaches the
|
|
136
|
+
* output. The place Adventure this pass bundles carries it too.
|
|
138
137
|
*
|
|
139
138
|
* @type {readonly string[]}
|
|
140
139
|
*/
|
|
141
|
-
static emitsArt = Object.freeze(["
|
|
140
|
+
static emitsArt = Object.freeze(["bgImage"]);
|
|
142
141
|
|
|
143
142
|
/** @type {string} */
|
|
144
143
|
adventureDir;
|
|
@@ -152,13 +151,14 @@ export class Scenes extends BasePackCompiler {
|
|
|
152
151
|
|
|
153
152
|
constructor({
|
|
154
153
|
contentBase,
|
|
154
|
+
assetsBase,
|
|
155
155
|
dest,
|
|
156
156
|
skipDirectories,
|
|
157
157
|
companionDests = {},
|
|
158
158
|
folderResolver = () => null,
|
|
159
159
|
repoRoot = process.cwd(),
|
|
160
160
|
}) {
|
|
161
|
-
super({ contentBase, dest, folderResolver, skipDirectories });
|
|
161
|
+
super({ contentBase, assetsBase, dest, folderResolver, skipDirectories });
|
|
162
162
|
if (!companionDests.adventures) {
|
|
163
163
|
throw new Error("Scenes compiler requires an `adventures` companion destination");
|
|
164
164
|
}
|
|
@@ -426,6 +426,9 @@ export class Scenes extends BasePackCompiler {
|
|
|
426
426
|
const warnings = [];
|
|
427
427
|
const scene = buildScene(fm, {
|
|
428
428
|
packageId: foundryPackageId(),
|
|
429
|
+
// The art resolver, so the map pass turns an address into the path
|
|
430
|
+
// each surface serves without holding an index of its own.
|
|
431
|
+
art: (value, key, type) => this.artPathOf(value, key, type),
|
|
429
432
|
name,
|
|
430
433
|
folder,
|
|
431
434
|
stats: this.stats,
|
|
@@ -473,7 +476,7 @@ export class Scenes extends BasePackCompiler {
|
|
|
473
476
|
this.places.set(placeKey, {
|
|
474
477
|
key: placeKey,
|
|
475
478
|
name: sohlField(fm, "placeName", null) || name,
|
|
476
|
-
img:
|
|
479
|
+
img: this.artPath(fm, "bgImage"),
|
|
477
480
|
pinned: false,
|
|
478
481
|
scenes: [],
|
|
479
482
|
journal: [],
|
package/engine/site-build.mjs
CHANGED
|
@@ -55,7 +55,6 @@ import { protectCode } from "./code-fences.mjs";
|
|
|
55
55
|
import { expandContentTables } from "./content-tables.mjs";
|
|
56
56
|
import { renderImageFigures } from "./content-images.mjs";
|
|
57
57
|
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
58
|
-
import { ART_FIELDS } from "./frontmatter-lint.mjs";
|
|
59
58
|
import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
60
59
|
import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
61
60
|
import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
@@ -63,10 +62,12 @@ import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
|
63
62
|
import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
|
|
64
63
|
import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
65
64
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
65
|
+
import { routerFor } from "./pack-router.mjs";
|
|
66
66
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
67
67
|
// The corpus, from the one pass that derives it.
|
|
68
68
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
69
69
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
70
|
+
import { ART_SLOTS, artPathname, assetAddressIndex } from "./art-fields.mjs";
|
|
70
71
|
import {
|
|
71
72
|
checkHomepageCount,
|
|
72
73
|
homepageDestination,
|
|
@@ -627,13 +628,16 @@ export function sectionFrontmatter(meta) {
|
|
|
627
628
|
* for.
|
|
628
629
|
* @param {(data: object, page: object) => void} [options.decorate] - Called
|
|
629
630
|
* with each page's frontmatter, for whatever a consumer's own pass adds.
|
|
631
|
+
* @param {(value: unknown, type: string) => string|null} [options.artSrc] -
|
|
632
|
+
* Translates an art address into the pathname the website resolver takes, or
|
|
633
|
+
* `null` where nothing answers it.
|
|
630
634
|
* @param {(src: string) => string} [options.webSrc] - Translates an authored
|
|
631
635
|
* pathname into the address the website serves. Every artwork field goes
|
|
632
636
|
* through it, so a page's `img:` and its body images name the same file the
|
|
633
637
|
* same way.
|
|
634
638
|
* @returns {object} The frontmatter to write.
|
|
635
639
|
*/
|
|
636
|
-
export function pageFrontmatter(page, { readmeSections = {}, decorate, webSrc }) {
|
|
640
|
+
export function pageFrontmatter(page, { readmeSections = {}, decorate, webSrc, artSrc }) {
|
|
637
641
|
const { fm, name, slug, sec, isReadme } = page;
|
|
638
642
|
let data;
|
|
639
643
|
if (page.kind === "content") {
|
|
@@ -665,29 +669,39 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate, webSrc })
|
|
|
665
669
|
if (meta) Object.assign(data, sectionFrontmatter(meta));
|
|
666
670
|
}
|
|
667
671
|
delete data.aliases;
|
|
668
|
-
if (webSrc) resolveArtFields(data, webSrc);
|
|
672
|
+
if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
|
|
669
673
|
return data;
|
|
670
674
|
}
|
|
671
675
|
|
|
672
676
|
/**
|
|
673
|
-
* Rewrite a page's artwork
|
|
677
|
+
* Rewrite a page's artwork addresses into the URLs the website serves.
|
|
674
678
|
*
|
|
675
|
-
* The
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
679
|
+
* The slots are read from {@link module:engine/art-fields.ART_SLOTS}, so a slot
|
|
680
|
+
* added to the vocabulary reaches the page without anyone remembering this
|
|
681
|
+
* function exists. Only an authored **string** is touched: `null` is a note
|
|
682
|
+
* naming no art and `""` is one naming none on purpose, and neither is an
|
|
683
|
+
* address to resolve.
|
|
684
|
+
*
|
|
685
|
+
* **An address nothing answers is dropped**, not emitted as written. The theme
|
|
686
|
+
* renders nothing where a value is absent — its silent-disappear convention —
|
|
687
|
+
* and a raw address left in place would reach the reader as a broken image
|
|
688
|
+
* source with nothing saying why.
|
|
680
689
|
*
|
|
681
690
|
* @param {object} data - The frontmatter being emitted, rewritten in place.
|
|
682
|
-
* @param {(src: string) => string} webSrc - The website's resolver.
|
|
691
|
+
* @param {(src: string) => string} webSrc - The website's pathname resolver.
|
|
692
|
+
* @param {(value: unknown, type: string) => string|null} artSrc - The address
|
|
693
|
+
* resolver, yielding the pathname `webSrc` takes.
|
|
683
694
|
* @returns {void}
|
|
684
695
|
*/
|
|
685
|
-
function resolveArtFields(data, webSrc) {
|
|
686
|
-
for (const { key,
|
|
687
|
-
const holder =
|
|
688
|
-
|
|
696
|
+
function resolveArtFields(data, webSrc, artSrc) {
|
|
697
|
+
for (const { key, type } of ART_SLOTS) {
|
|
698
|
+
const holder = isPlainObject(data.data) ? data.data : null;
|
|
699
|
+
if (!holder) continue;
|
|
700
|
+
const value = holder[key];
|
|
689
701
|
if (typeof value !== "string" || value === "") continue;
|
|
690
|
-
|
|
702
|
+
const pathname = artSrc(value, type);
|
|
703
|
+
if (pathname === null) delete holder[key];
|
|
704
|
+
else holder[key] = webSrc(pathname);
|
|
691
705
|
}
|
|
692
706
|
}
|
|
693
707
|
|
|
@@ -758,8 +772,17 @@ export function renderPages(pages, options) {
|
|
|
758
772
|
linkable = (d) => Boolean(d.fm.shortcode),
|
|
759
773
|
sqlTables,
|
|
760
774
|
config,
|
|
775
|
+
records = [],
|
|
761
776
|
} = options;
|
|
762
777
|
|
|
778
|
+
// The address space the art slots and the body's embeds resolve against:
|
|
779
|
+
// the files this package ships, and the ones a dependency published.
|
|
780
|
+
const artIndex = assetAddressIndex(records, {
|
|
781
|
+
config,
|
|
782
|
+
foreign,
|
|
783
|
+
types: index?.contentTypes ?? [],
|
|
784
|
+
});
|
|
785
|
+
|
|
763
786
|
const tableErrors = [];
|
|
764
787
|
const wikiErrors = [];
|
|
765
788
|
const imageErrors = [];
|
|
@@ -817,9 +840,11 @@ export function renderPages(pages, options) {
|
|
|
817
840
|
type: page.fm.type ?? null,
|
|
818
841
|
errors: wikiErrors,
|
|
819
842
|
foreignIndex: foreign.index,
|
|
843
|
+
assets: artIndex,
|
|
820
844
|
});
|
|
821
845
|
|
|
822
846
|
const webSrc = webAddresses(page.file);
|
|
847
|
+
const artSrc = (value, type) => artPathname(artIndex, value, type).pathname;
|
|
823
848
|
const resolve = (text) => {
|
|
824
849
|
let t = text;
|
|
825
850
|
if (pass.beforeLinks) t = pass.beforeLinks(t, page);
|
|
@@ -851,7 +876,7 @@ export function renderPages(pages, options) {
|
|
|
851
876
|
body = markdown;
|
|
852
877
|
}
|
|
853
878
|
|
|
854
|
-
const data = pageFrontmatter(page, { readmeSections, decorate, webSrc });
|
|
879
|
+
const data = pageFrontmatter(page, { readmeSections, decorate, webSrc, artSrc });
|
|
855
880
|
const dest = path.join(outRoot, pageDestination(page));
|
|
856
881
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
857
882
|
fs.writeFileSync(dest, matter.stringify(protectCode(body, resolve), data));
|
|
@@ -1235,6 +1260,7 @@ export function buildSite({ config, outRoot, sqlTables } = {}) {
|
|
|
1235
1260
|
outRoot: out,
|
|
1236
1261
|
sqlTables,
|
|
1237
1262
|
config: resolved,
|
|
1263
|
+
records: ctx.records,
|
|
1238
1264
|
index: gates.index,
|
|
1239
1265
|
foreign: gates.foreign,
|
|
1240
1266
|
universe: tableUniverse(pages),
|
|
@@ -1254,6 +1280,7 @@ export function buildSite({ config, outRoot, sqlTables } = {}) {
|
|
|
1254
1280
|
// book from one definition.
|
|
1255
1281
|
data.infoboxes = noteInfoboxes(page.fm, {
|
|
1256
1282
|
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
1283
|
+
router: routerFor(resolved),
|
|
1257
1284
|
});
|
|
1258
1285
|
},
|
|
1259
1286
|
});
|
package/engine/site-index.mjs
CHANGED
|
@@ -322,6 +322,8 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
322
322
|
* diagnostic names. Absent, `src` stands in.
|
|
323
323
|
* @param {Map<string, object>} [options.foreignIndex] - The foreign index, for
|
|
324
324
|
* resolvers that distinguish a foreign hit from a local one.
|
|
325
|
+
* @param {object} [options.assets] - The address space an `![[…]]` embed
|
|
326
|
+
* resolves against, shaped as every asset resolver reads one.
|
|
325
327
|
* @returns {object} The resolver context.
|
|
326
328
|
*
|
|
327
329
|
* There is deliberately **no `manifestsComplete`**. It used to let a resolver
|
|
@@ -330,10 +332,17 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
330
332
|
* checker never had it and one authored link must not get two verdicts. A
|
|
331
333
|
* caller still passing it is ignored rather than obeyed.
|
|
332
334
|
*/
|
|
333
|
-
export function wikiContext(
|
|
335
|
+
export function wikiContext(
|
|
336
|
+
built,
|
|
337
|
+
{ src, file, type = null, errors, foreignIndex = new Map(), assets },
|
|
338
|
+
) {
|
|
334
339
|
return {
|
|
335
340
|
index: built.index,
|
|
336
341
|
foreign: foreignIndex,
|
|
342
|
+
// The address space an embed resolves against. Separate from `index`
|
|
343
|
+
// because the two hold different record shapes: a note's entry carries
|
|
344
|
+
// a URL and a name, an asset's carries the path to a file.
|
|
345
|
+
assets,
|
|
337
346
|
collide: built.ambiguous,
|
|
338
347
|
sections: built.sections,
|
|
339
348
|
contentTypes: built.contentTypes,
|