@heroiclands/package-build 22.15.1 → 22.17.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 +110 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +315 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +296 -236
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +85 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +633 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4362 -0
- package/docs/reference/note-types.md +532 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-format.mjs +441 -0
- package/engine/calendar-human.mjs +133 -0
- package/engine/calendar-notes.mjs +307 -88
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +746 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/markdown-expressions.mjs +118 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +314 -64
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +176 -166
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-format.d.mts +33 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +5 -3
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +13 -26
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
package/engine/pdf-build.mjs
CHANGED
|
@@ -32,24 +32,15 @@
|
|
|
32
32
|
* the site publishes every page, and the book publishes what its document tree
|
|
33
33
|
* asked for.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* pages, whatever else the configuration declares. A
|
|
39
|
-
* PDF of the content tree is a content surface by any reading — arguably the
|
|
40
|
-
* most portable one there is — so it is fenced on exactly the same terms, by
|
|
41
|
-
* asking the same {@link module:content-config.publishesContentPages} the
|
|
42
|
-
* site build asks. Four of the six packages that would adopt this run
|
|
43
|
-
* `homepage`, and a PDF that appeared there would breach the fence silently:
|
|
44
|
-
* nothing in those packages' configuration would say so. So the fence is
|
|
45
|
-
* checked **before** the tree is read, and the command says why it built
|
|
46
|
-
* nothing rather than emitting an empty document.
|
|
35
|
+
* The book builds when the authored tree contains content pages. The same
|
|
36
|
+
* {@link module:content-config.publishesContentPages} decision governs the
|
|
37
|
+
* site and the content index.
|
|
47
38
|
*
|
|
48
39
|
* ## Every gate reports; none exits
|
|
49
40
|
*
|
|
50
41
|
* A filter that selects nothing, a `file:` that resolves nowhere, a table that
|
|
51
42
|
* will not run and a missing Typst binary are all findings handed back to the
|
|
52
|
-
* caller. `bin/
|
|
43
|
+
* caller. `bin/package-build.mjs` decides what to do about them — which is the
|
|
53
44
|
* rule the whole engine is built on, and what lets one pass serve a lint, a
|
|
54
45
|
* build step and a unit test.
|
|
55
46
|
*
|
|
@@ -96,15 +87,32 @@ import { indexRecordsFor } from "./content-index.mjs";
|
|
|
96
87
|
import { isNoteRecord, isStub, noteFile } from "./index-records.mjs";
|
|
97
88
|
import { openNotesDatabase, prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
98
89
|
import { parseDocumentTree, runTreeFilters, planDocument } from "./pdf-toc.mjs";
|
|
99
|
-
import {
|
|
90
|
+
import {
|
|
91
|
+
collectContentPages,
|
|
92
|
+
siteGates,
|
|
93
|
+
tableUniverse,
|
|
94
|
+
gatesFailed,
|
|
95
|
+
resolveSitePass,
|
|
96
|
+
} from "./site-build.mjs";
|
|
100
97
|
import { resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
101
98
|
import { resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
102
99
|
import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
103
100
|
import { artPathname, assetAddressIndex } from "./art-fields.mjs";
|
|
104
101
|
import { expandContentTables } from "./content-tables.mjs";
|
|
102
|
+
import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
105
103
|
import { renderSecretBlocks } from "./content-secrets.mjs";
|
|
104
|
+
import { scanAdmonitions } from "./content-admonitions.mjs";
|
|
105
|
+
import { numberCaptions, scanCaptions } from "./content-captions.mjs";
|
|
106
106
|
import { protectCode } from "./code-fences.mjs";
|
|
107
|
-
import {
|
|
107
|
+
import { imagesIn, parseImageDirective } from "./content-images.mjs";
|
|
108
|
+
import {
|
|
109
|
+
PDF_IMAGE_INCHES,
|
|
110
|
+
PDF_PAGE,
|
|
111
|
+
pdfColumnWidth,
|
|
112
|
+
pdfTextHeight,
|
|
113
|
+
pdfTextWidth,
|
|
114
|
+
resamplePdfImages,
|
|
115
|
+
} from "./pdf-images.mjs";
|
|
108
116
|
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
109
117
|
import {
|
|
110
118
|
createParser,
|
|
@@ -114,7 +122,7 @@ import {
|
|
|
114
122
|
resolveDanglingLabels,
|
|
115
123
|
} from "./pdf-render.mjs";
|
|
116
124
|
import { draftNoticeTypst } from "./draft-notice.mjs";
|
|
117
|
-
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
125
|
+
import { isDraftNote, isGmNote } from "./note-vocabulary.mjs";
|
|
118
126
|
import { infoboxTypstPreamble, infoboxesToTypst, linkToTypst } from "./infobox-render.mjs";
|
|
119
127
|
import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
120
128
|
import { resolveIconGlyphs } from "./pdf-fonts.mjs";
|
|
@@ -304,8 +312,9 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
304
312
|
return {
|
|
305
313
|
built: false,
|
|
306
314
|
reason:
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
fs.existsSync(resolved.paths.content) ?
|
|
316
|
+
"The content tree contains only a homepage, so there is no book to build."
|
|
317
|
+
: "this package has no content tree, so there is nothing to print",
|
|
309
318
|
findings,
|
|
310
319
|
typ: null,
|
|
311
320
|
pdf: null,
|
|
@@ -354,11 +363,11 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
354
363
|
});
|
|
355
364
|
|
|
356
365
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "content-pdf-"));
|
|
357
|
-
const db = await openNotesDatabase(records, { dir });
|
|
366
|
+
const db = await openNotesDatabase(records, { dir, audience: "public" });
|
|
358
367
|
const ran = await runTreeFilters(parsed.nodes, db, {
|
|
359
368
|
// A stub is a note and is deliberately in the corpus, but it has no
|
|
360
369
|
// body — selecting one would print a blank page under its name.
|
|
361
|
-
keep: (record) => isNoteRecord(record) && !isStub(record),
|
|
370
|
+
keep: (record) => isNoteRecord(record) && !isStub(record) && !isGmNote(record),
|
|
362
371
|
text: tree.text,
|
|
363
372
|
});
|
|
364
373
|
findings.push(...ran.findings.map((f) => ({ file: resolved.pdf.document, ...f })));
|
|
@@ -403,6 +412,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
403
412
|
config: resolved,
|
|
404
413
|
skipDirectories: resolved.skipDirectories,
|
|
405
414
|
records,
|
|
415
|
+
audience: "public",
|
|
406
416
|
});
|
|
407
417
|
// The address space an `![[…]]` embed resolves against — the same one the
|
|
408
418
|
// site builds, so one authored picture reaches both surfaces or neither.
|
|
@@ -422,6 +432,8 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
422
432
|
|
|
423
433
|
/** @type {Map<string, string>} Authored address → the staged file's path. */
|
|
424
434
|
const images = new Map();
|
|
435
|
+
/** @type {Map<string, {from: string, to: string, relative: string, file: string, uses: Array<{width: number, height: number}>}>} */
|
|
436
|
+
const imageCandidates = new Map();
|
|
425
437
|
/** @type {Set<string>} Addresses already looked for, staged or not. */
|
|
426
438
|
const seenImages = new Set();
|
|
427
439
|
|
|
@@ -434,11 +446,25 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
434
446
|
*
|
|
435
447
|
* @param {string} body - The rendered markdown.
|
|
436
448
|
* @param {string} file - The note, for the finding.
|
|
449
|
+
* @param {number} columns - The page's column count.
|
|
437
450
|
* @returns {void}
|
|
438
451
|
*/
|
|
439
|
-
const stageImages = (body, file) => {
|
|
440
|
-
for (const
|
|
441
|
-
|
|
452
|
+
const stageImages = (body, file, columns = 2) => {
|
|
453
|
+
for (const image of imagesIn(body)) {
|
|
454
|
+
const src = image.src;
|
|
455
|
+
const directive = parseImageDirective(image.directive);
|
|
456
|
+
const wide =
|
|
457
|
+
directive.size === "full-width" || directive.classes.includes("full-width");
|
|
458
|
+
const availableWidth = wide ? pdfTextWidth : pdfColumnWidth(columns);
|
|
459
|
+
const use = {
|
|
460
|
+
width: Math.min(PDF_IMAGE_INCHES[directive.size] ?? availableWidth, availableWidth),
|
|
461
|
+
height: pdfTextHeight * 0.8,
|
|
462
|
+
};
|
|
463
|
+
if (seenImages.has(src)) {
|
|
464
|
+
const stagedTo = images.get(src);
|
|
465
|
+
if (stagedTo) imageCandidates.get(stagedTo)?.uses.push(use);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
442
468
|
seenImages.add(src);
|
|
443
469
|
// An **error**, where a picture the book cannot carry is a warning:
|
|
444
470
|
// this pathname resolves on no surface at all, and the replacement
|
|
@@ -476,6 +502,15 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
476
502
|
continue;
|
|
477
503
|
}
|
|
478
504
|
images.set(src, staged.to);
|
|
505
|
+
const candidate = imageCandidates.get(staged.to);
|
|
506
|
+
if (candidate) candidate.uses.push(use);
|
|
507
|
+
else
|
|
508
|
+
imageCandidates.set(staged.to, {
|
|
509
|
+
...staged,
|
|
510
|
+
relative: path.relative(resolved.rootDir, staged.from),
|
|
511
|
+
file,
|
|
512
|
+
uses: [use],
|
|
513
|
+
});
|
|
479
514
|
}
|
|
480
515
|
};
|
|
481
516
|
|
|
@@ -485,12 +520,37 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
485
520
|
* @param {object} page - A collected page.
|
|
486
521
|
* @param {number} headingOffset - Where the book put this entry.
|
|
487
522
|
* @param {string} anchorPrefix - The entry's anchor, namespacing its sections.
|
|
523
|
+
* @param {number} columns - The entry's page columns.
|
|
488
524
|
* @returns {string} Typst markup.
|
|
489
525
|
*/
|
|
490
|
-
const
|
|
526
|
+
const captionCounts = { code: 0, table: 0, figure: 0, prose: 0 };
|
|
527
|
+
const frontCaptions = new Map();
|
|
528
|
+
for (const file of resolved.pdf.front) {
|
|
529
|
+
try {
|
|
530
|
+
const scan = scanCaptions(fs.readFileSync(file, "utf8"));
|
|
531
|
+
frontCaptions.set(file, numberCaptions(scan.captions, captionCounts));
|
|
532
|
+
for (const error of scan.errors)
|
|
533
|
+
findings.push({
|
|
534
|
+
file,
|
|
535
|
+
line: error.line,
|
|
536
|
+
column: error.column,
|
|
537
|
+
severity: "error",
|
|
538
|
+
message: error.message,
|
|
539
|
+
});
|
|
540
|
+
} catch {
|
|
541
|
+
// The rendering pass reports an unreadable front-matter file.
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const pass = resolveSitePass(resolved.site?.pass, {
|
|
545
|
+
...resolved.site?.passOptions,
|
|
546
|
+
repoRoot: resolved.rootDir,
|
|
547
|
+
config: resolved,
|
|
548
|
+
book: true,
|
|
549
|
+
});
|
|
550
|
+
const renderPage = (page, headingOffset, anchorPrefix, columns) => {
|
|
491
551
|
const src = page.relPath ?? page.base;
|
|
492
552
|
const wikiErrors = [];
|
|
493
|
-
const { markdown, errors } = expandContentTables(page.body, {
|
|
553
|
+
const { markdown, errors, lineMap } = expandContentTables(page.body, {
|
|
494
554
|
docs: universe.get(page.pkg) ?? [],
|
|
495
555
|
linkable: (d) => Boolean(d.fm.shortcode),
|
|
496
556
|
source: src,
|
|
@@ -512,10 +572,34 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
512
572
|
foreignIndex: gates.foreign.index,
|
|
513
573
|
assets,
|
|
514
574
|
});
|
|
575
|
+
const captionScan = scanCaptions(markdown);
|
|
576
|
+
const numberedCaptions = numberCaptions(captionScan.captions, captionCounts);
|
|
577
|
+
linkCtx.captionLabels = new Map(
|
|
578
|
+
numberedCaptions.map((caption) => [caption.id, caption.label]),
|
|
579
|
+
);
|
|
580
|
+
for (const error of captionScan.errors)
|
|
581
|
+
findings.push({
|
|
582
|
+
file: page.file,
|
|
583
|
+
line: (page.bodyLine ?? 1) + (lineMap[error.line - 1]?.line ?? error.line - 1),
|
|
584
|
+
column: error.column,
|
|
585
|
+
severity: "error",
|
|
586
|
+
message: error.message,
|
|
587
|
+
});
|
|
588
|
+
linkCtx.output = "book";
|
|
515
589
|
// Code fences are protected for the same reason every other pass
|
|
516
590
|
// protects them: a wikilink shown as an example is prose about a
|
|
517
591
|
// wikilink, and resolving it would make the example impossible to write.
|
|
518
|
-
const
|
|
592
|
+
const expressions = renderMarkdownExpressions(markdown, {
|
|
593
|
+
fm: page.fm,
|
|
594
|
+
dates: gates.index.dateContext,
|
|
595
|
+
sqlResults: sqlTables?.inline?.get(page.file),
|
|
596
|
+
file: page.file,
|
|
597
|
+
bodyLine: page.bodyLine,
|
|
598
|
+
});
|
|
599
|
+
findings.push(...expressions.findings);
|
|
600
|
+
const resolvedBody = protectCode(expressions.markdown, (text) =>
|
|
601
|
+
resolveWebWikilinks(pass.beforeLinks ? pass.beforeLinks(text, page) : text, linkCtx),
|
|
602
|
+
);
|
|
519
603
|
for (const err of wikiErrors) {
|
|
520
604
|
findings.push({
|
|
521
605
|
file: page.file,
|
|
@@ -536,7 +620,16 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
536
620
|
message: error.message,
|
|
537
621
|
});
|
|
538
622
|
}
|
|
539
|
-
|
|
623
|
+
for (const error of scanAdmonitions(page.body).errors) {
|
|
624
|
+
findings.push({
|
|
625
|
+
file: page.file,
|
|
626
|
+
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
627
|
+
column: error.column,
|
|
628
|
+
severity: "error",
|
|
629
|
+
message: error.message,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
stageImages(secrets.markdown, page.file, columns);
|
|
540
633
|
const prose = markdownToTypst(secrets.markdown, {
|
|
541
634
|
md,
|
|
542
635
|
links: plan.links,
|
|
@@ -544,6 +637,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
544
637
|
images,
|
|
545
638
|
headingOffset,
|
|
546
639
|
anchorPrefix,
|
|
640
|
+
captions: numberedCaptions,
|
|
547
641
|
});
|
|
548
642
|
// The infobox is generated content in document order — prepended,
|
|
549
643
|
// before the prose. An image the note authored ahead of it still comes
|
|
@@ -551,6 +645,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
551
645
|
const boxes = noteInfoboxes(page.fm, {
|
|
552
646
|
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
553
647
|
router: routerFor(resolved),
|
|
648
|
+
dates: gates.index.dateContext,
|
|
554
649
|
});
|
|
555
650
|
const panel = infoboxesToTypst(boxes, {
|
|
556
651
|
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
@@ -577,7 +672,10 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
577
672
|
// entry. A note's body starts at `##` — its name is frontmatter, not
|
|
578
673
|
// an H1 — so an offset of the entry's depth puts that `##` one level
|
|
579
674
|
// below the entry heading at `depth + 1`, which is where it belongs.
|
|
580
|
-
bodies.set(
|
|
675
|
+
bodies.set(
|
|
676
|
+
entry.anchor,
|
|
677
|
+
renderPage(page, entry.depth, entry.anchor, entry.presentation?.page?.columns ?? 2),
|
|
678
|
+
);
|
|
581
679
|
continue;
|
|
582
680
|
}
|
|
583
681
|
if (entry.kind === "prose") {
|
|
@@ -593,7 +691,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
593
691
|
});
|
|
594
692
|
continue;
|
|
595
693
|
}
|
|
596
|
-
stageImages(text, file);
|
|
694
|
+
stageImages(text, file, entry.presentation?.page?.columns ?? 2);
|
|
597
695
|
bodies.set(
|
|
598
696
|
entry.anchor,
|
|
599
697
|
markdownToTypst(text, {
|
|
@@ -603,6 +701,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
603
701
|
images,
|
|
604
702
|
headingOffset: entry.depth,
|
|
605
703
|
anchorPrefix: entry.anchor,
|
|
704
|
+
captions: numberCaptions(scanCaptions(text).captions, captionCounts),
|
|
606
705
|
}),
|
|
607
706
|
);
|
|
608
707
|
}
|
|
@@ -617,15 +716,17 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
617
716
|
});
|
|
618
717
|
}
|
|
619
718
|
|
|
620
|
-
const front = resolved.pdf.front.map((file) => {
|
|
719
|
+
const front = resolved.pdf.front.map((file, index) => {
|
|
621
720
|
try {
|
|
622
721
|
const text = fs.readFileSync(file, "utf8");
|
|
623
|
-
stageImages(text, file);
|
|
722
|
+
stageImages(text, file, 1);
|
|
624
723
|
return markdownToTypst(text, {
|
|
625
724
|
md,
|
|
626
725
|
links: plan.links,
|
|
627
726
|
glyphs,
|
|
628
727
|
images,
|
|
728
|
+
footnotePrefix: `footnote-front-${index + 1}`,
|
|
729
|
+
captions: frontCaptions.get(file),
|
|
629
730
|
});
|
|
630
731
|
} catch {
|
|
631
732
|
findings.push({
|
|
@@ -638,9 +739,53 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
638
739
|
});
|
|
639
740
|
|
|
640
741
|
const banners = stageBanners(plan.entries, resolved, outDir, findings);
|
|
742
|
+
for (const [declared, to] of banners) {
|
|
743
|
+
const from = path.resolve(resolved.rootDir, declared);
|
|
744
|
+
imageCandidates.set(`banner:${declared}`, {
|
|
745
|
+
from,
|
|
746
|
+
to,
|
|
747
|
+
relative: path.relative(resolved.rootDir, from),
|
|
748
|
+
file: resolved.pdf.document,
|
|
749
|
+
uses: [{ width: PDF_PAGE.width, height: Infinity }],
|
|
750
|
+
});
|
|
751
|
+
}
|
|
641
752
|
|
|
642
|
-
/** @type {Map<string, {path: string, title: string}
|
|
753
|
+
/** @type {Map<string, Array<{path: string, title: string}>>} Entry anchor → map pages. */
|
|
643
754
|
const maps = new Map();
|
|
755
|
+
const addMapPage = (anchor, page) => {
|
|
756
|
+
if (!maps.has(anchor)) maps.set(anchor, []);
|
|
757
|
+
maps.get(anchor).push(page);
|
|
758
|
+
};
|
|
759
|
+
const stageMapBackground = (entry, value, title) => {
|
|
760
|
+
const file = noteFile(contentBase, entry.record);
|
|
761
|
+
const found = artPathname(assets, value, "image", ["image", "icon"]);
|
|
762
|
+
const staged = stagedImagePath(found.pathname ?? value, resolved);
|
|
763
|
+
if (!staged || !fs.existsSync(staged.from)) {
|
|
764
|
+
findings.push({
|
|
765
|
+
file,
|
|
766
|
+
severity: "warning",
|
|
767
|
+
message: `the map background "${value}" cannot be resolved for the book`,
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const target = path.join(outDir, staged.to);
|
|
772
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
773
|
+
fs.copyFileSync(staged.from, target);
|
|
774
|
+
addMapPage(entry.anchor, { path: staged.to, title });
|
|
775
|
+
const candidate = imageCandidates.get(staged.to);
|
|
776
|
+
const use = {
|
|
777
|
+
width: PDF_PAGE.height - 2 * PDF_PAGE.margin,
|
|
778
|
+
height: pdfTextWidth - 2 / 2.54,
|
|
779
|
+
};
|
|
780
|
+
if (candidate) candidate.uses.push(use);
|
|
781
|
+
else
|
|
782
|
+
imageCandidates.set(staged.to, {
|
|
783
|
+
...staged,
|
|
784
|
+
relative: path.relative(resolved.rootDir, staged.from),
|
|
785
|
+
file,
|
|
786
|
+
uses: [use],
|
|
787
|
+
});
|
|
788
|
+
};
|
|
644
789
|
const world = mapWorld({
|
|
645
790
|
records,
|
|
646
791
|
foreignIndex: gates.foreign.index,
|
|
@@ -661,7 +806,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
661
806
|
const drawn = buildMaps({ world, outDir: mapDir, from: centres });
|
|
662
807
|
findings.push(...drawn.findings);
|
|
663
808
|
for (const entry of mappedEntries) {
|
|
664
|
-
|
|
809
|
+
addMapPage(entry.anchor, {
|
|
665
810
|
path: `maps/from-${entry.record.shortcode}.svg`,
|
|
666
811
|
title: `From here: ${entry.record.name?.full ?? entry.record.shortcode}`,
|
|
667
812
|
});
|
|
@@ -669,27 +814,26 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
669
814
|
}
|
|
670
815
|
for (const entry of plan.entries) {
|
|
671
816
|
if (entry.kind !== "note" || entry.record?.type !== "map") continue;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
findings.push({
|
|
678
|
-
file: noteFile(contentBase, entry.record),
|
|
679
|
-
severity: "warning",
|
|
680
|
-
message: `the regional map's background "${image}" cannot be resolved for the book`,
|
|
681
|
-
});
|
|
817
|
+
const title = entry.record.name?.full ?? entry.record.shortcode;
|
|
818
|
+
if (["regionalmap", "totm"].includes(entry.record.subType)) {
|
|
819
|
+
if (entry.record.data?.bgImage) {
|
|
820
|
+
stageMapBackground(entry, entry.record.data.bgImage, title);
|
|
821
|
+
}
|
|
682
822
|
continue;
|
|
683
823
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
824
|
+
if (!["battlemap", "localmap"].includes(entry.record.subType)) continue;
|
|
825
|
+
for (const [index, level] of (entry.record.data?.scene?.levels ?? []).entries()) {
|
|
826
|
+
const paths = [
|
|
827
|
+
`.levels[${index}].background.src`,
|
|
828
|
+
...(level._id ? [`.levels[${level._id}].background.src`] : []),
|
|
829
|
+
];
|
|
830
|
+
const fixup = (entry.record.data?.fixup ?? []).find((item) =>
|
|
831
|
+
paths.includes(item.path),
|
|
832
|
+
);
|
|
833
|
+
const value = fixup?.value ?? level.background?.src;
|
|
834
|
+
if (value)
|
|
835
|
+
stageMapBackground(entry, value, level.name ? `${title}: ${level.name}` : title);
|
|
836
|
+
}
|
|
693
837
|
}
|
|
694
838
|
|
|
695
839
|
const assembled = renderBook({
|
|
@@ -713,18 +857,36 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
713
857
|
const pdfPath = path.join(outDir, stem);
|
|
714
858
|
fs.writeFileSync(typPath, source);
|
|
715
859
|
|
|
860
|
+
const imageReports = await resamplePdfImages(imageCandidates, outDir);
|
|
861
|
+
|
|
716
862
|
const stats = { ...plan.stats, entries: plan.entries.length, bytes: source.length };
|
|
717
863
|
if (!compile) {
|
|
718
|
-
return {
|
|
864
|
+
return {
|
|
865
|
+
built: true,
|
|
866
|
+
reason: null,
|
|
867
|
+
findings,
|
|
868
|
+
imageReports,
|
|
869
|
+
typ: typPath,
|
|
870
|
+
pdf: null,
|
|
871
|
+
stats,
|
|
872
|
+
};
|
|
719
873
|
}
|
|
720
874
|
|
|
721
875
|
const compiled = compileTypst(typPath, pdfPath, resolved.pdf);
|
|
722
876
|
findings.push(...compiled.findings);
|
|
723
877
|
if (!compiled.ok) {
|
|
724
878
|
findings.push({ file: typPath, severity: "error", message: compiled.message });
|
|
725
|
-
return {
|
|
879
|
+
return {
|
|
880
|
+
built: false,
|
|
881
|
+
reason: null,
|
|
882
|
+
findings,
|
|
883
|
+
imageReports,
|
|
884
|
+
typ: typPath,
|
|
885
|
+
pdf: null,
|
|
886
|
+
stats,
|
|
887
|
+
};
|
|
726
888
|
}
|
|
727
|
-
return { built: true, reason: null, findings, typ: typPath, pdf: pdfPath, stats };
|
|
889
|
+
return { built: true, reason: null, findings, imageReports, typ: typPath, pdf: pdfPath, stats };
|
|
728
890
|
}
|
|
729
891
|
|
|
730
892
|
/**
|
package/engine/pdf-fonts.mjs
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* ## Nothing here fails a build
|
|
38
38
|
*
|
|
39
39
|
* A font that cannot be read, or a name it does not carry, is a **finding** and
|
|
40
|
-
* the icon falls back to its literal `:icon
|
|
40
|
+
* the icon falls back to its literal `:icon …:` text. That is the failure mode
|
|
41
41
|
* {@link module:engine/content-icons} was designed around — visible on the
|
|
42
42
|
* page, where an author is looking — and it is strictly better than a tofu box
|
|
43
43
|
* or a silently missing glyph.
|
|
@@ -399,7 +399,7 @@ export function resolveIconGlyphs(registry, iconFonts = {}, findings = []) {
|
|
|
399
399
|
file: font.file,
|
|
400
400
|
severity: "warning",
|
|
401
401
|
message:
|
|
402
|
-
`\`:icon
|
|
402
|
+
`\`:icon ${name}:\` asks for glyph \`${glyphName}\`, which ` +
|
|
403
403
|
`${path0(font.file)} does not carry — it prints as its name`,
|
|
404
404
|
});
|
|
405
405
|
continue;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import sharp from "sharp";
|
|
17
|
+
|
|
18
|
+
/** Print geometry in inches; Typst's default column gutter is 4% of the text width. */
|
|
19
|
+
export const PDF_PAGE = Object.freeze({
|
|
20
|
+
width: 8.5,
|
|
21
|
+
height: 11,
|
|
22
|
+
margin: 1.9 / 2.54,
|
|
23
|
+
dpi: 300,
|
|
24
|
+
quality: 82,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Named image widths in inches, matching the book renderer's Typst sizes. */
|
|
28
|
+
export const PDF_IMAGE_INCHES = Object.freeze({
|
|
29
|
+
small: 1.6 / 2.54,
|
|
30
|
+
medium: 3.2 / 2.54,
|
|
31
|
+
large: 5.6 / 2.54,
|
|
32
|
+
xlarge: 8 / 2.54,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Usable width of a portrait page in inches. */
|
|
36
|
+
export const pdfTextWidth = PDF_PAGE.width - 2 * PDF_PAGE.margin;
|
|
37
|
+
/** Usable height of a portrait page in inches. */
|
|
38
|
+
export const pdfTextHeight = PDF_PAGE.height - 2 * PDF_PAGE.margin;
|
|
39
|
+
/** Width of a portrait page's text column in inches. */
|
|
40
|
+
export const pdfColumnWidth = (columns = 2) =>
|
|
41
|
+
(pdfTextWidth * (1 - 0.04 * (columns - 1))) / columns;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Optimize staged raster images without changing their authored files.
|
|
45
|
+
*
|
|
46
|
+
* @param {Map<string, {from: string, to: string, relative: string, file: string,
|
|
47
|
+
* uses: Array<{width: number, height: number}>}>} candidates
|
|
48
|
+
* @param {string} outDir - PDF staging directory.
|
|
49
|
+
* @returns {Promise<object[]>} Image reports with the authored file location.
|
|
50
|
+
*/
|
|
51
|
+
export async function resamplePdfImages(candidates, outDir) {
|
|
52
|
+
const reports = [];
|
|
53
|
+
for (const candidate of candidates.values()) {
|
|
54
|
+
const extension = path.extname(candidate.to).toLowerCase();
|
|
55
|
+
if (![".jpg", ".jpeg", ".png", ".webp", ".avif"].includes(extension)) continue;
|
|
56
|
+
const relative = candidate.relative.replaceAll(path.sep, "/");
|
|
57
|
+
const destination = path.join(outDir, candidate.to);
|
|
58
|
+
const original = await fs.readFile(destination);
|
|
59
|
+
const metadata = await sharp(original).metadata();
|
|
60
|
+
if (!metadata.width || !metadata.height) continue;
|
|
61
|
+
const ratio = metadata.width / metadata.height;
|
|
62
|
+
const target = Math.round(
|
|
63
|
+
Math.max(
|
|
64
|
+
...candidate.uses.map(({ width, height }) => Math.min(width, height * ratio)),
|
|
65
|
+
) * PDF_PAGE.dpi,
|
|
66
|
+
);
|
|
67
|
+
if (metadata.width <= target) continue;
|
|
68
|
+
let pipeline = sharp(original).resize({ width: target, withoutEnlargement: true });
|
|
69
|
+
if (extension === ".jpg" || extension === ".jpeg")
|
|
70
|
+
pipeline = pipeline.jpeg({ quality: PDF_PAGE.quality });
|
|
71
|
+
if (extension === ".webp") pipeline = pipeline.webp({ quality: PDF_PAGE.quality });
|
|
72
|
+
if (extension === ".avif") pipeline = pipeline.avif({ quality: PDF_PAGE.quality });
|
|
73
|
+
if (extension === ".png") pipeline = pipeline.png();
|
|
74
|
+
const output = await pipeline.toBuffer();
|
|
75
|
+
await fs.writeFile(destination, output);
|
|
76
|
+
reports.push({
|
|
77
|
+
file: candidate.file,
|
|
78
|
+
message: `book image ${relative}: ${metadata.width}×${metadata.height} → ${target} px wide; ${original.length} → ${output.length} bytes`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return reports;
|
|
82
|
+
}
|