@heroiclands/package-build 20.7.0 → 21.1.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 +183 -0
- package/CONTENT.md +132 -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 +382 -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 +206 -9
- package/engine/pdf-render.mjs +453 -20
- 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 +97 -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,25 @@ 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,
|
|
318
501
|
});
|
|
502
|
+
// The infobox is generated content in document order — prepended,
|
|
503
|
+
// before the prose. An image the note authored ahead of it still comes
|
|
504
|
+
// first, because the image lives in the body and the body follows.
|
|
505
|
+
const boxes = noteInfoboxes(page.fm, {
|
|
506
|
+
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
507
|
+
});
|
|
508
|
+
const panel = infoboxesToTypst(boxes, {
|
|
509
|
+
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
510
|
+
});
|
|
511
|
+
return panel ? `${panel}\n\n${prose}` : prose;
|
|
319
512
|
};
|
|
320
513
|
|
|
321
514
|
const bodies = new Map();
|
|
@@ -348,12 +541,14 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
348
541
|
});
|
|
349
542
|
continue;
|
|
350
543
|
}
|
|
544
|
+
stageImages(text, file);
|
|
351
545
|
bodies.set(
|
|
352
546
|
entry.anchor,
|
|
353
547
|
markdownToTypst(text, {
|
|
354
548
|
md,
|
|
355
549
|
links: plan.links,
|
|
356
550
|
glyphs,
|
|
551
|
+
images,
|
|
357
552
|
headingOffset: entry.depth,
|
|
358
553
|
anchorPrefix: entry.anchor,
|
|
359
554
|
}),
|
|
@@ -372,10 +567,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
372
567
|
|
|
373
568
|
const front = resolved.pdf.front.map((file) => {
|
|
374
569
|
try {
|
|
375
|
-
|
|
570
|
+
const text = fs.readFileSync(file, "utf8");
|
|
571
|
+
stageImages(text, file);
|
|
572
|
+
return markdownToTypst(text, {
|
|
376
573
|
md,
|
|
377
574
|
links: plan.links,
|
|
378
575
|
glyphs,
|
|
576
|
+
images,
|
|
379
577
|
});
|
|
380
578
|
} catch {
|
|
381
579
|
findings.push({
|
|
@@ -387,6 +585,8 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
387
585
|
}
|
|
388
586
|
});
|
|
389
587
|
|
|
588
|
+
const banners = stageBanners(plan.entries, resolved, outDir, findings);
|
|
589
|
+
|
|
390
590
|
const assembled = renderBook({
|
|
391
591
|
plan,
|
|
392
592
|
bodies,
|
|
@@ -395,16 +595,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
395
595
|
front,
|
|
396
596
|
fonts: resolved.pdf.fonts,
|
|
397
597
|
version,
|
|
598
|
+
preamble: infoboxTypstPreamble(),
|
|
599
|
+
banners,
|
|
398
600
|
});
|
|
399
601
|
// Last, over the whole document: a reference can only be checked once every
|
|
400
602
|
// declaration is in one string, and Typst treats a dangling one as fatal.
|
|
401
603
|
const source = resolveDanglingLabels(assembled, findings);
|
|
402
604
|
|
|
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
605
|
const stem = pdfFileName(resolved.foundryPackage?.id ?? resolved.contentPackage, version);
|
|
409
606
|
const typPath = path.join(outDir, stem.replace(/\.pdf$/, ".typ"));
|
|
410
607
|
const pdfPath = path.join(outDir, stem);
|