@heroiclands/package-build 20.7.0 → 21.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 +159 -0
- package/CONTENT.md +134 -44
- package/bin/content-build.mjs +37 -5
- package/bin/package-build.mjs +77 -0
- package/content-config.mjs +59 -1
- package/docs/api.md +149 -19
- package/docs/commands.md +75 -0
- package/docs/configuration.md +37 -10
- package/docs/content-format.md +450 -49
- package/engine/content-format.mjs +52 -3
- package/engine/content-images.mjs +699 -0
- package/engine/dependency-bump.mjs +218 -0
- package/engine/frontmatter-lint.mjs +89 -2
- package/engine/helpers.mjs +81 -142
- package/engine/index.mjs +15 -0
- package/engine/infobox-registry.mjs +81 -0
- package/engine/infobox-render.mjs +381 -0
- package/engine/infobox.mjs +963 -0
- package/engine/item-registry.mjs +5 -5
- package/engine/journals.mjs +22 -1
- package/engine/map-notes.mjs +11 -5
- package/engine/metadata-index.mjs +5 -0
- package/engine/note-vocabulary.mjs +57 -2
- package/engine/pathnames.mjs +374 -0
- package/engine/pdf-build.mjs +208 -9
- package/engine/pdf-render.mjs +461 -21
- package/engine/pdf-toc.mjs +77 -5
- package/engine/scenes.mjs +2 -1
- package/engine/site-build.mjs +106 -7
- package/engine/site-index.mjs +93 -4
- package/engine/wikilinks.mjs +93 -0
- package/hm3/default-item-art.mjs +14 -15
- package/hm3/index.mjs +3 -0
- package/hm3/infobox.mjs +64 -0
- package/package.json +1 -1
- package/sohl/default-item-art.mjs +18 -16
- package/sohl/index.mjs +3 -0
- package/sohl/infobox.mjs +499 -0
- package/types/content-config.d.mts +7 -0
- package/types/engine/content-format.d.mts +36 -0
- package/types/engine/content-images.d.mts +281 -0
- package/types/engine/dependency-bump.d.mts +89 -0
- package/types/engine/frontmatter-lint.d.mts +23 -0
- package/types/engine/helpers.d.mts +30 -72
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +36 -0
- package/types/engine/infobox-render.d.mts +87 -0
- package/types/engine/infobox.d.mts +443 -0
- package/types/engine/item-registry.d.mts +5 -5
- package/types/engine/journals.d.mts +9 -1
- package/types/engine/note-vocabulary.d.mts +51 -0
- package/types/engine/pathnames.d.mts +189 -0
- package/types/engine/pdf-build.d.mts +46 -0
- package/types/engine/pdf-render.d.mts +99 -1
- package/types/engine/pdf-toc.d.mts +10 -5
- package/types/engine/site-build.d.mts +11 -3
- package/types/engine/site-index.d.mts +35 -3
- package/types/engine/wikilinks.d.mts +22 -0
- package/types/hm3/default-item-art.d.mts +5 -6
- package/types/hm3/index.d.mts +1 -0
- package/types/hm3/infobox.d.mts +22 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/infobox.d.mts +145 -0
package/engine/pdf-build.mjs
CHANGED
|
@@ -81,13 +81,129 @@ import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
|
81
81
|
import { openNotesDatabase, prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
82
82
|
import { parseDocumentTree, runTreeFilters, planDocument } from "./pdf-toc.mjs";
|
|
83
83
|
import { collectContentPages, siteGates, tableUniverse, gatesFailed } from "./site-build.mjs";
|
|
84
|
-
import { wikiContext } from "./site-index.mjs";
|
|
84
|
+
import { resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
85
85
|
import { resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
86
86
|
import { expandContentTables } from "./content-tables.mjs";
|
|
87
87
|
import { protectCode } from "./code-fences.mjs";
|
|
88
|
-
import {
|
|
88
|
+
import { imageSourcesIn } from "./content-images.mjs";
|
|
89
|
+
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
90
|
+
import {
|
|
91
|
+
createParser,
|
|
92
|
+
labelFor,
|
|
93
|
+
markdownToTypst,
|
|
94
|
+
renderBook,
|
|
95
|
+
resolveDanglingLabels,
|
|
96
|
+
} from "./pdf-render.mjs";
|
|
97
|
+
import { infoboxTypstPreamble, infoboxesToTypst, linkToTypst } from "./infobox-render.mjs";
|
|
98
|
+
import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
89
99
|
import { resolveIconGlyphs } from "./pdf-fonts.mjs";
|
|
90
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The file on disk an authored image pathname names, or `null`.
|
|
103
|
+
*
|
|
104
|
+
* Two of the four forms {@link module:engine/pathnames.resolvePathname}
|
|
105
|
+
* derives, used together: `local` is the file in this repository's own tree,
|
|
106
|
+
* and `pdf` is where the book stages a copy of it.
|
|
107
|
+
*
|
|
108
|
+
* **Typst decides where that copy goes.** It resolves a path against its root —
|
|
109
|
+
* the directory holding the source it is given — and refuses to read anything
|
|
110
|
+
* above it. So a file reaches the compiler by being copied under the output
|
|
111
|
+
* directory rather than by widening the root to the whole repository: the
|
|
112
|
+
* emitted `.typ` and everything it opens sit in one directory, which is what
|
|
113
|
+
* makes the source a consumer can compile by hand with no flags, and what keeps
|
|
114
|
+
* a build from touching a path outside its own output.
|
|
115
|
+
*
|
|
116
|
+
* Only a file **this** package ships can be staged. A pathname naming another
|
|
117
|
+
* package's file, or a URL, names something no build here can open — a build
|
|
118
|
+
* reaches no network — and the caller reports it as a picture the book will not
|
|
119
|
+
* carry.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} src - The pathname, as authored.
|
|
122
|
+
* @param {object} config - The resolved configuration.
|
|
123
|
+
* @returns {{from: string, to: string}|null} The file, and where under the
|
|
124
|
+
* output directory it is staged.
|
|
125
|
+
*/
|
|
126
|
+
export function stagedImagePath(src, config) {
|
|
127
|
+
const forms = resolvePathname(src, config);
|
|
128
|
+
if (!forms || forms.state !== "package" || !forms.own) return null;
|
|
129
|
+
return {
|
|
130
|
+
from: path.resolve(config.rootDir, forms.local),
|
|
131
|
+
to: forms.pdf,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Where the book keeps the pictures its section plates are drawn over.
|
|
137
|
+
*
|
|
138
|
+
* Typst resolves a path against its root, which is the directory holding the
|
|
139
|
+
* source it is given, and refuses to read anything above it. So a banner
|
|
140
|
+
* reaches the compiler by being **copied under the output directory** rather
|
|
141
|
+
* than by widening the root to the whole repository: the emitted `.typ` and
|
|
142
|
+
* everything it opens then sit in one directory, which is what makes the
|
|
143
|
+
* source a consumer can compile by hand with no flags.
|
|
144
|
+
*
|
|
145
|
+
* The declared path is mirrored beneath it, so two banners with the same base
|
|
146
|
+
* name cannot land on each other.
|
|
147
|
+
*
|
|
148
|
+
* @type {string}
|
|
149
|
+
*/
|
|
150
|
+
const STAGED_PLATES = "plates";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Copy every banner the document tree names into the output directory.
|
|
154
|
+
*
|
|
155
|
+
* **A missing banner is not a failure.** A section plate implies a banner per
|
|
156
|
+
* section and art arrives later than rendering does, so a section that names
|
|
157
|
+
* none draws its plate over the book's ink and says nothing about it. One that
|
|
158
|
+
* names a file the build cannot read is a different matter — that is a
|
|
159
|
+
* statement the tree makes and the build cannot honour — and it is reported.
|
|
160
|
+
*
|
|
161
|
+
* @param {object[]} entries - The plan's entries.
|
|
162
|
+
* @param {object} config - The resolved configuration.
|
|
163
|
+
* @param {string} outDir - Where the book is written.
|
|
164
|
+
* @param {object[]} findings - Collected here rather than thrown.
|
|
165
|
+
* @returns {Map<string, string>} Declared path → the staged file's path,
|
|
166
|
+
* relative to the `.typ`.
|
|
167
|
+
*/
|
|
168
|
+
export function stageBanners(entries, config, outDir, findings = []) {
|
|
169
|
+
const staged = new Map();
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
for (const entry of entries ?? []) {
|
|
172
|
+
const declared = entry?.presentation?.page?.banner;
|
|
173
|
+
if (typeof declared !== "string" || !declared.trim() || seen.has(declared)) continue;
|
|
174
|
+
seen.add(declared);
|
|
175
|
+
const from = path.resolve(config.rootDir, declared);
|
|
176
|
+
const within = path.relative(config.rootDir, from);
|
|
177
|
+
if (within.startsWith("..") || path.isAbsolute(within)) {
|
|
178
|
+
findings.push({
|
|
179
|
+
file: config.pdf.document,
|
|
180
|
+
severity: "warning",
|
|
181
|
+
message:
|
|
182
|
+
`the banner \`${declared}\` is outside this package, so the section ` +
|
|
183
|
+
"plate is drawn without a picture — a banner is a file this repository ships",
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const to = `${STAGED_PLATES}/${within.split(path.sep).join("/")}`;
|
|
188
|
+
const dest = path.join(outDir, to);
|
|
189
|
+
try {
|
|
190
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
191
|
+
fs.copyFileSync(from, dest);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
findings.push({
|
|
194
|
+
file: config.pdf.document,
|
|
195
|
+
severity: "warning",
|
|
196
|
+
message:
|
|
197
|
+
`the banner \`${declared}\` cannot be read, so the section plate is ` +
|
|
198
|
+
`drawn without a picture: ${err.message}`,
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
staged.set(declared, to);
|
|
203
|
+
}
|
|
204
|
+
return staged;
|
|
205
|
+
}
|
|
206
|
+
|
|
91
207
|
/**
|
|
92
208
|
* The file name a downloaded book identifies itself by.
|
|
93
209
|
*
|
|
@@ -266,6 +382,71 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
266
382
|
const md = createParser(resolved.icons);
|
|
267
383
|
const glyphs = resolveIconGlyphs(resolved.icons, resolved.pdf.iconFonts, findings);
|
|
268
384
|
|
|
385
|
+
const outDir = path.resolve(
|
|
386
|
+
resolved.rootDir,
|
|
387
|
+
out || resolved.pdf.out || path.join("build", "dist"),
|
|
388
|
+
);
|
|
389
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
390
|
+
|
|
391
|
+
/** @type {Map<string, string>} Authored address → the staged file's path. */
|
|
392
|
+
const images = new Map();
|
|
393
|
+
/** @type {Set<string>} Addresses already looked for, staged or not. */
|
|
394
|
+
const seenImages = new Set();
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Copy every picture one body names into the output directory.
|
|
398
|
+
*
|
|
399
|
+
* Run per body rather than in a pass of its own, because the addresses are
|
|
400
|
+
* read from the markdown *after* its tables have expanded — a generated
|
|
401
|
+
* table is as free to carry an image as prose is.
|
|
402
|
+
*
|
|
403
|
+
* @param {string} body - The rendered markdown.
|
|
404
|
+
* @param {string} file - The note, for the finding.
|
|
405
|
+
* @returns {void}
|
|
406
|
+
*/
|
|
407
|
+
const stageImages = (body, file) => {
|
|
408
|
+
for (const src of imageSourcesIn(body)) {
|
|
409
|
+
if (seenImages.has(src)) continue;
|
|
410
|
+
seenImages.add(src);
|
|
411
|
+
// An **error**, where a picture the book cannot carry is a warning:
|
|
412
|
+
// this pathname resolves on no surface at all, and the replacement
|
|
413
|
+
// is mechanical and named in the message.
|
|
414
|
+
const problem = pathnameProblem(src);
|
|
415
|
+
if (problem) {
|
|
416
|
+
findings.push({ file, severity: "error", message: problem });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const staged = stagedImagePath(src, resolved);
|
|
420
|
+
if (!staged) {
|
|
421
|
+
findings.push({
|
|
422
|
+
file,
|
|
423
|
+
severity: "warning",
|
|
424
|
+
message:
|
|
425
|
+
`\`${src}\` names a file this package does not ship, so the book ` +
|
|
426
|
+
"prints the caption where the picture would be — an image the book " +
|
|
427
|
+
"carries is addressed inside this package",
|
|
428
|
+
});
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const dest = path.join(outDir, staged.to);
|
|
432
|
+
try {
|
|
433
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
434
|
+
fs.copyFileSync(staged.from, dest);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
findings.push({
|
|
437
|
+
file,
|
|
438
|
+
severity: "warning",
|
|
439
|
+
message:
|
|
440
|
+
`\`${src}\` cannot be read from ` +
|
|
441
|
+
`\`${path.relative(resolved.rootDir, staged.from)}\`, so the book ` +
|
|
442
|
+
`prints the caption where the picture would be: ${err.message}`,
|
|
443
|
+
});
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
images.set(src, staged.to);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
269
450
|
/**
|
|
270
451
|
* One note's markdown, through the same passes the site runs.
|
|
271
452
|
*
|
|
@@ -309,13 +490,27 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
309
490
|
message: String(err.message ?? err),
|
|
310
491
|
});
|
|
311
492
|
}
|
|
312
|
-
|
|
493
|
+
stageImages(resolvedBody, page.file);
|
|
494
|
+
const prose = markdownToTypst(resolvedBody, {
|
|
313
495
|
md,
|
|
314
496
|
links: plan.links,
|
|
315
497
|
glyphs,
|
|
498
|
+
images,
|
|
316
499
|
headingOffset,
|
|
317
500
|
anchorPrefix,
|
|
501
|
+
// An entry opens a page, so its first paragraph opens the page too.
|
|
502
|
+
dropCap: true,
|
|
318
503
|
});
|
|
504
|
+
// The infobox is generated content in document order — prepended,
|
|
505
|
+
// before the prose. An image the note authored ahead of it still comes
|
|
506
|
+
// first, because the image lives in the body and the body follows.
|
|
507
|
+
const boxes = noteInfoboxes(page.fm, {
|
|
508
|
+
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
509
|
+
});
|
|
510
|
+
const panel = infoboxesToTypst(boxes, {
|
|
511
|
+
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
512
|
+
});
|
|
513
|
+
return panel ? `${panel}\n\n${prose}` : prose;
|
|
319
514
|
};
|
|
320
515
|
|
|
321
516
|
const bodies = new Map();
|
|
@@ -348,12 +543,14 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
348
543
|
});
|
|
349
544
|
continue;
|
|
350
545
|
}
|
|
546
|
+
stageImages(text, file);
|
|
351
547
|
bodies.set(
|
|
352
548
|
entry.anchor,
|
|
353
549
|
markdownToTypst(text, {
|
|
354
550
|
md,
|
|
355
551
|
links: plan.links,
|
|
356
552
|
glyphs,
|
|
553
|
+
images,
|
|
357
554
|
headingOffset: entry.depth,
|
|
358
555
|
anchorPrefix: entry.anchor,
|
|
359
556
|
}),
|
|
@@ -372,10 +569,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
372
569
|
|
|
373
570
|
const front = resolved.pdf.front.map((file) => {
|
|
374
571
|
try {
|
|
375
|
-
|
|
572
|
+
const text = fs.readFileSync(file, "utf8");
|
|
573
|
+
stageImages(text, file);
|
|
574
|
+
return markdownToTypst(text, {
|
|
376
575
|
md,
|
|
377
576
|
links: plan.links,
|
|
378
577
|
glyphs,
|
|
578
|
+
images,
|
|
379
579
|
});
|
|
380
580
|
} catch {
|
|
381
581
|
findings.push({
|
|
@@ -387,6 +587,8 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
387
587
|
}
|
|
388
588
|
});
|
|
389
589
|
|
|
590
|
+
const banners = stageBanners(plan.entries, resolved, outDir, findings);
|
|
591
|
+
|
|
390
592
|
const assembled = renderBook({
|
|
391
593
|
plan,
|
|
392
594
|
bodies,
|
|
@@ -395,16 +597,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
395
597
|
front,
|
|
396
598
|
fonts: resolved.pdf.fonts,
|
|
397
599
|
version,
|
|
600
|
+
preamble: infoboxTypstPreamble(),
|
|
601
|
+
banners,
|
|
398
602
|
});
|
|
399
603
|
// Last, over the whole document: a reference can only be checked once every
|
|
400
604
|
// declaration is in one string, and Typst treats a dangling one as fatal.
|
|
401
605
|
const source = resolveDanglingLabels(assembled, findings);
|
|
402
606
|
|
|
403
|
-
const outDir = path.resolve(
|
|
404
|
-
resolved.rootDir,
|
|
405
|
-
out || resolved.pdf.out || path.join("build", "dist"),
|
|
406
|
-
);
|
|
407
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
408
607
|
const stem = pdfFileName(resolved.foundryPackage?.id ?? resolved.contentPackage, version);
|
|
409
608
|
const typPath = path.join(outDir, stem.replace(/\.pdf$/, ".typ"));
|
|
410
609
|
const pdfPath = path.join(outDir, stem);
|