@heroiclands/package-build 21.0.0 → 21.2.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 +228 -0
- package/CONTENT.md +109 -7
- 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/ci/emit-asset-index.mjs +32 -0
- package/content-config.mjs +30 -1
- package/docs/api.md +100 -26
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +512 -109
- 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/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-render.mjs +4 -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 -10
- package/engine/pdf-render.mjs +41 -41
- 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/hm3/infobox.mjs +1 -1
- package/package.json +5 -2
- package/packagebuild-metadata.jsonl +13 -0
- package/sohl/actors.mjs +12 -86
- package/sohl/infobox.mjs +1 -1
- 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/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 -3
- 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/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);
|
|
@@ -498,14 +527,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
498
527
|
images,
|
|
499
528
|
headingOffset,
|
|
500
529
|
anchorPrefix,
|
|
501
|
-
// An entry opens a page, so its first paragraph opens the page too.
|
|
502
|
-
dropCap: true,
|
|
503
530
|
});
|
|
504
531
|
// The infobox is generated content in document order — prepended,
|
|
505
532
|
// before the prose. An image the note authored ahead of it still comes
|
|
506
533
|
// first, because the image lives in the body and the body follows.
|
|
507
534
|
const boxes = noteInfoboxes(page.fm, {
|
|
508
535
|
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
536
|
+
router: routerFor(resolved),
|
|
509
537
|
});
|
|
510
538
|
const panel = infoboxesToTypst(boxes, {
|
|
511
539
|
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
@@ -615,6 +643,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
615
643
|
}
|
|
616
644
|
|
|
617
645
|
const compiled = compileTypst(typPath, pdfPath, resolved.pdf);
|
|
646
|
+
findings.push(...compiled.findings);
|
|
618
647
|
if (!compiled.ok) {
|
|
619
648
|
findings.push({ file: typPath, severity: "error", message: compiled.message });
|
|
620
649
|
return { built: false, reason: null, findings, typ: typPath, pdf: null, stats };
|
|
@@ -622,24 +651,113 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
622
651
|
return { built: true, reason: null, findings, typ: typPath, pdf: pdfPath, stats };
|
|
623
652
|
}
|
|
624
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
|
+
|
|
625
744
|
/**
|
|
626
745
|
* Run Typst over the emitted source.
|
|
627
746
|
*
|
|
628
747
|
* @param {string} typPath - The `.typ` file.
|
|
629
748
|
* @param {string} pdfPath - Where the PDF goes.
|
|
630
749
|
* @param {object} pdf - The resolved `pdf:` block.
|
|
631
|
-
* @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.
|
|
632
752
|
*/
|
|
633
753
|
export function compileTypst(typPath, pdfPath, pdf = {}) {
|
|
634
754
|
const binary = pdf.binary || "typst";
|
|
635
|
-
const args =
|
|
636
|
-
if (pdf.fonts?.path) args.push("--font-path", pdf.fonts.path);
|
|
637
|
-
args.push(typPath, pdfPath);
|
|
755
|
+
const args = typstArgs(typPath, pdfPath, pdf);
|
|
638
756
|
let result;
|
|
639
757
|
try {
|
|
640
758
|
result = spawnSync(binary, args, { encoding: "utf8" });
|
|
641
759
|
} catch (err) {
|
|
642
|
-
return { ok: false, message: `could not run \`${binary}\`: ${err.message}
|
|
760
|
+
return { ok: false, message: `could not run \`${binary}\`: ${err.message}`, findings: [] };
|
|
643
761
|
}
|
|
644
762
|
if (result.error) {
|
|
645
763
|
const missing = /** @type {any} */ (result.error).code === "ENOENT";
|
|
@@ -651,13 +769,18 @@ export function compileTypst(typPath, pdfPath, pdf = {}) {
|
|
|
651
769
|
"`typst compile` over it produces the book. Name another binary with " +
|
|
652
770
|
"`pdf.binary`."
|
|
653
771
|
: `could not run \`${binary}\`: ${result.error.message}`,
|
|
772
|
+
findings: [],
|
|
654
773
|
};
|
|
655
774
|
}
|
|
656
775
|
if (result.status !== 0) {
|
|
657
776
|
const detail = String(result.stderr || result.stdout || "")
|
|
658
777
|
.trim()
|
|
659
778
|
.split("\n")[0];
|
|
660
|
-
return {
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
message: `\`${binary} compile\` failed: ${detail}`,
|
|
782
|
+
findings: [],
|
|
783
|
+
};
|
|
661
784
|
}
|
|
662
|
-
return { ok: true, message: "" };
|
|
785
|
+
return { ok: true, message: "", findings: typstWarnings(result.stderr) };
|
|
663
786
|
}
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -168,7 +168,6 @@ export function createParser(registry) {
|
|
|
168
168
|
* note's own `##` nests beneath the entry heading the book gave it.
|
|
169
169
|
* @param {string} [opts.anchorPrefix] - The entry's anchor, which namespaces
|
|
170
170
|
* every `{#slug}` the body declares.
|
|
171
|
-
* @param {boolean} [opts.dropCap] - Whether to open the body with a raised
|
|
172
171
|
* capital. Set for an entry, which begins a page; not for front matter or a
|
|
173
172
|
* prose file, which carry headings of their own.
|
|
174
173
|
* @returns {string} Typst markup.
|
|
@@ -181,7 +180,6 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
181
180
|
images = new Map(),
|
|
182
181
|
headingOffset = 0,
|
|
183
182
|
anchorPrefix = "",
|
|
184
|
-
dropCap = false,
|
|
185
183
|
} = opts;
|
|
186
184
|
const tokens = md.parse(String(markdown ?? ""), {});
|
|
187
185
|
// One map for the whole body, not one per block: a heading inside a
|
|
@@ -194,7 +192,6 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
194
192
|
images,
|
|
195
193
|
headingOffset,
|
|
196
194
|
anchorPrefix,
|
|
197
|
-
dropCap,
|
|
198
195
|
seen: new Map(),
|
|
199
196
|
});
|
|
200
197
|
}
|
|
@@ -258,7 +255,7 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
258
255
|
return 3;
|
|
259
256
|
}
|
|
260
257
|
case "paragraph_open": {
|
|
261
|
-
out.push(`\n${
|
|
258
|
+
out.push(`\n${renderInline(tokens[i + 1], ctx)}\n\n`);
|
|
262
259
|
return 3;
|
|
263
260
|
}
|
|
264
261
|
case "fence":
|
|
@@ -271,7 +268,7 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
271
268
|
return 1;
|
|
272
269
|
case "blockquote_open": {
|
|
273
270
|
const end = matching(tokens, i, "blockquote_open", "blockquote_close");
|
|
274
|
-
const inner = renderTokens(tokens.slice(i + 1, end), { ...ctx
|
|
271
|
+
const inner = renderTokens(tokens.slice(i + 1, end), { ...ctx });
|
|
275
272
|
out.push(`\n#quote(block: true)[${inner}]\n\n`);
|
|
276
273
|
return end - i + 1;
|
|
277
274
|
}
|
|
@@ -298,29 +295,6 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
298
295
|
}
|
|
299
296
|
}
|
|
300
297
|
|
|
301
|
-
/**
|
|
302
|
-
* The first paragraph of an entry, opened with a raised capital.
|
|
303
|
-
*
|
|
304
|
-
* Only the first, and only when it begins with a letter: a paragraph opening
|
|
305
|
-
* on a link, a bold run or a number has no character to raise, and raising
|
|
306
|
-
* whatever happened to be first would put a 26pt accent on a bracket. The
|
|
307
|
-
* chance is spent either way — a body opens once — so a paragraph that cannot
|
|
308
|
-
* take the capital simply sets as itself.
|
|
309
|
-
*
|
|
310
|
-
* @param {string} rendered - The paragraph's Typst markup.
|
|
311
|
-
* @param {object} ctx - Render context.
|
|
312
|
-
* @returns {string} The same markup, or it with a raised capital.
|
|
313
|
-
*/
|
|
314
|
-
function openingParagraph(rendered, ctx) {
|
|
315
|
-
if (!ctx.dropCap) return rendered;
|
|
316
|
-
ctx.dropCap = false;
|
|
317
|
-
// A letter is never escaped, so the first character of the markup is the
|
|
318
|
-
// first character of the prose whenever the prose starts with one.
|
|
319
|
-
const match = /^(\p{L})([\s\S]*)$/u.exec(rendered);
|
|
320
|
-
if (!match) return rendered;
|
|
321
|
-
return `#book-dropcap[${match[1]}]#h(1pt)${match[2]}`;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
298
|
/**
|
|
325
299
|
* A heading's text, and the `{#slug}` it may end with.
|
|
326
300
|
*
|
|
@@ -438,7 +412,7 @@ function listItems(tokens, start, end, ctx) {
|
|
|
438
412
|
continue;
|
|
439
413
|
}
|
|
440
414
|
const close = matching(tokens, i, "list_item_open", "list_item_close");
|
|
441
|
-
items.push(renderTokens(tokens.slice(i + 1, close), { ...ctx
|
|
415
|
+
items.push(renderTokens(tokens.slice(i + 1, close), { ...ctx }));
|
|
442
416
|
i = close + 1;
|
|
443
417
|
}
|
|
444
418
|
return items;
|
|
@@ -825,6 +799,7 @@ export function bookTypstPreamble() {
|
|
|
825
799
|
'#let book-ink = rgb("#241f1a")',
|
|
826
800
|
'#let book-paper = rgb("#f4efe4")',
|
|
827
801
|
'#let book-accent = rgb("#7c3b1e")',
|
|
802
|
+
'#let book-head = rgb("#5e2b14")',
|
|
828
803
|
'#let book-faint = rgb("#6b6357")',
|
|
829
804
|
"#let book-ornament = box(baseline: 1pt, " +
|
|
830
805
|
"rotate(45deg, rect(width: 3pt, height: 3pt, fill: book-accent)))",
|
|
@@ -835,16 +810,34 @@ export function bookTypstPreamble() {
|
|
|
835
810
|
"}",
|
|
836
811
|
// A heading justifies and hyphenates like body text unless told
|
|
837
812
|
// otherwise, and both make a display line look broken.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
813
|
+
//
|
|
814
|
+
// Size, weight and the space above follow the level, so a parent reads
|
|
815
|
+
// as one without the words being read. The rule belongs to the note's own
|
|
816
|
+
// top level alone; every level under it is set apart by space. Headings set
|
|
817
|
+
// in the sans face and in the case they were authored in, so neither
|
|
818
|
+
// capitals nor tracking is carrying the distinction. The scale starts
|
|
819
|
+
// at 3: `book-plate` shadows this rule while it draws the title, so
|
|
820
|
+
// the section landing and the entry title never arrive here, and the
|
|
821
|
+
// headings that do are the note's own.
|
|
822
|
+
"#let book-sechead(it) = {\n" +
|
|
823
|
+
" let lv = it.level\n" +
|
|
824
|
+
" let size = if lv <= 3 { 26pt } else if lv == 4 { 19pt }\n" +
|
|
825
|
+
" else if lv == 5 { 14pt } else { 11pt }\n" +
|
|
826
|
+
" let above = if lv <= 3 { 2.5em } else if lv == 4 { 2.0em }\n" +
|
|
827
|
+
" else { 1.0em }\n" +
|
|
828
|
+
" let below = if lv <= 3 { 0.5em } else if lv == 4 { 0.36em }\n" +
|
|
829
|
+
" else { 0.28em }\n" +
|
|
830
|
+
' let weight = if lv <= 5 { "bold" } else { "regular" }\n' +
|
|
831
|
+
" block(width: 100%, above: above, below: below, breakable: false)[\n" +
|
|
832
|
+
" #set par(justify: false, first-line-indent: 0em)\n" +
|
|
833
|
+
" #set text(hyphenate: false)\n" +
|
|
834
|
+
" #text(size: size, weight: weight, fill: book-head)[#it.body]\n" +
|
|
835
|
+
" #if lv <= 3 {\n" +
|
|
836
|
+
" v(-0.30em)\n" +
|
|
837
|
+
" line(length: 100%, stroke: 0.5pt + book-accent)\n" +
|
|
838
|
+
" }\n" +
|
|
839
|
+
" ]\n" +
|
|
840
|
+
"}",
|
|
848
841
|
// The plate bleeds off the paper: the placed panel is the full width of
|
|
849
842
|
// the sheet and starts a margin above and to the left of wherever the
|
|
850
843
|
// flow has reached, which on an entry's first page is the top corner.
|
|
@@ -1014,7 +1007,9 @@ export function bookTypstPreamble() {
|
|
|
1014
1007
|
* @param {string} opts.title - The document's title.
|
|
1015
1008
|
* @param {string} [opts.subtitle] - Shown under it on the title page.
|
|
1016
1009
|
* @param {string[]} [opts.front] - Rendered Typst for each front-matter file.
|
|
1017
|
-
* @param {object} [opts.fonts] - `{ serif, sans, mono }` family names.
|
|
1010
|
+
* @param {object} [opts.fonts] - `{ serif, sans, mono }` family names. Each
|
|
1011
|
+
* falls back to the face the toolchain ships or the compiler embeds, so a
|
|
1012
|
+
* caller that names none still sets the book in all three.
|
|
1018
1013
|
* @param {string} [opts.version] - Stamped on the title page when given.
|
|
1019
1014
|
* @param {string} [opts.preamble] - Definitions the bodies call, emitted once
|
|
1020
1015
|
* above the title page. A panel every entry draws is a set of rules stated
|
|
@@ -1036,8 +1031,13 @@ export function renderBook({
|
|
|
1036
1031
|
preamble = "",
|
|
1037
1032
|
banners = new Map(),
|
|
1038
1033
|
} = {}) {
|
|
1034
|
+
// The two halves of one superfamily, chosen together: matched metrics are
|
|
1035
|
+
// most of why the sans can carry every heading over a serif body without
|
|
1036
|
+
// the page reading as two books. The mono is a separate claim — the
|
|
1037
|
+
// superfamily's own is missing the Latin Extended Additional letters this
|
|
1038
|
+
// corpus spells names with, where the compiler's embedded face carries them.
|
|
1039
1039
|
const serif = fonts.serif || "Libertinus Serif";
|
|
1040
|
-
const sans = fonts.sans ||
|
|
1040
|
+
const sans = fonts.sans || "Libertinus Sans";
|
|
1041
1041
|
const mono = fonts.mono || "DejaVu Sans Mono";
|
|
1042
1042
|
const out = [];
|
|
1043
1043
|
|
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: [],
|