@heroiclands/package-build 23.1.1 → 23.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 +63 -0
- package/bin/content-commands.mjs +68 -149
- package/bin/package-build.mjs +56 -10
- package/ci/check-banners.mjs +42 -6
- package/ci/confirm-npm-version.mjs +135 -0
- package/ci/content-format-reference.mjs +14 -5
- package/config.mjs +1 -0
- package/container.mjs +185 -4
- package/content-config.mjs +333 -210
- package/deploy.mjs +116 -10
- package/docs/api.md +238 -246
- package/docs/authoring/assets.md +11 -5
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +87 -0
- package/docs/commands.md +138 -99
- package/docs/configuration.md +277 -125
- package/docs/content-format.md +2 -0
- package/docs/diagnostics.md +48 -13
- package/docs/guides/book.md +341 -21
- package/docs/guides/e2e.md +13 -2
- package/docs/guides/icons.md +14 -4
- package/docs/guides/module-project.md +18 -8
- package/docs/guides/release-and-deploy.md +32 -0
- package/docs/guides/site.md +5 -2
- package/docs/project-setup.md +30 -10
- package/docs/reference/format-details.md +373 -170
- package/docs/reference/note-types.md +128 -109
- package/docs/reference/theme-pages.md +31 -6
- package/e2e.mjs +140 -28
- package/engine/actor-compiler.mjs +22 -31
- package/engine/address-charset.mjs +4 -17
- package/engine/address-fields.mjs +1 -1
- package/engine/address-render.mjs +12 -1
- package/engine/address-values.mjs +6 -0
- package/engine/address.mjs +46 -33
- package/engine/art-fields.mjs +30 -5
- package/engine/art-slots.mjs +10 -1
- package/engine/asset-bindings.mjs +12 -1
- package/engine/base-compiler.mjs +34 -65
- package/engine/being-age.mjs +2 -7
- package/engine/being-measurements.mjs +12 -1
- package/engine/calendar-artifacts.mjs +6 -0
- package/engine/calendar-choice.mjs +12 -1
- package/engine/calendar-format.mjs +12 -1
- package/engine/calendar-human.mjs +12 -1
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-address.mjs +9 -12
- package/engine/content-alerts.mjs +13 -1
- package/engine/content-blocks.mjs +14 -3
- package/engine/content-embeds.mjs +11 -3
- package/engine/content-figures.mjs +9 -0
- package/engine/content-footnotes.mjs +12 -1
- package/engine/content-format-check.mjs +11 -262
- package/engine/content-format.yaml +5 -2
- package/engine/content-images.mjs +3 -0
- package/engine/content-index.mjs +443 -20
- package/engine/content-links.mjs +29 -35
- package/engine/content-lint.mjs +2 -3
- package/engine/content-poetry.mjs +12 -1
- package/engine/content-slug.mjs +2 -14
- package/engine/content-spans.mjs +13 -1
- package/engine/content-tables.mjs +12 -1
- package/engine/culture-choice.mjs +210 -13
- package/engine/data-keys.mjs +19 -16
- package/engine/date-conversion.mjs +12 -1
- package/engine/declaration-check.mjs +9 -0
- package/engine/derived-fields.mjs +6 -7
- package/engine/diagnostics.mjs +34 -4
- package/engine/docs-checks.mjs +9 -0
- package/engine/document-subtypes.mjs +47 -34
- package/engine/event-views.mjs +139 -14
- package/engine/exported-scene.mjs +92 -28
- package/engine/extension-attributes.mjs +12 -1
- package/engine/field-spec.mjs +11 -102
- package/engine/folder-notes.mjs +0 -64
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/format-generated.mjs +9 -0
- package/engine/foundry-entries.mjs +10 -28
- package/engine/frontmatter-lint.mjs +83 -392
- package/engine/frontmatter.mjs +20 -70
- package/engine/generate.mjs +10 -15
- package/engine/heading-attributes.mjs +12 -1
- package/engine/helpers.mjs +41 -102
- package/engine/holdings.mjs +1 -1
- package/engine/homepage.mjs +3 -10
- package/engine/ids.mjs +1 -167
- package/engine/index-records.mjs +6 -4
- package/engine/index.mjs +0 -3
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +13 -14
- package/engine/item-compiler.mjs +4 -10
- package/engine/item-docs.mjs +3 -5
- package/engine/item-registry.mjs +2 -10
- package/engine/journals.mjs +1 -5
- package/engine/literature-notes.mjs +4 -12
- package/engine/macros.mjs +1 -2
- package/engine/map-graphviz.mjs +12 -1
- package/engine/markdown-expressions.mjs +12 -1
- package/engine/markdownlint-run.mjs +567 -0
- package/engine/metadata-index.mjs +42 -12
- package/engine/note-addresses.mjs +143 -35
- package/engine/note-claims.mjs +14 -28
- package/engine/note-dates.mjs +3 -3
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-ids.mjs +8 -14
- package/engine/note-package.mjs +8 -25
- package/engine/note-state.mjs +1 -3
- package/engine/note-vocabulary.mjs +148 -84
- package/engine/number-words.mjs +12 -1
- package/engine/pack-config.mjs +15 -25
- package/engine/pack-router.mjs +10 -16
- package/engine/pathnames.mjs +8 -22
- package/engine/pdf-build.mjs +773 -121
- package/engine/pdf-coverage.mjs +188 -0
- package/engine/pdf-index.mjs +54 -7
- package/engine/pdf-pages.mjs +247 -0
- package/engine/pdf-render.mjs +114 -4
- package/engine/pdf-toc.mjs +29 -32
- package/engine/project-init.mjs +46 -6
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +52 -88
- package/engine/readability-lint.mjs +9 -0
- package/engine/reckoning-markers.mjs +12 -1
- package/engine/reference-targets.mjs +7 -0
- package/engine/runtime-only-fields.mjs +5 -8
- package/engine/scene-levels.mjs +2 -2
- package/engine/scenes.mjs +1 -1
- package/engine/shortcode-references.mjs +7 -0
- package/engine/site-build.mjs +30 -16
- package/engine/site-index.mjs +10 -16
- package/engine/site-preview.mjs +6 -0
- package/engine/site-root.mjs +8 -1
- package/engine/social-tie-terms.mjs +12 -1
- package/engine/social-ties.mjs +3 -0
- package/engine/sql-tables.mjs +56 -20
- package/engine/standing-terms.mjs +11 -1
- package/engine/standings.mjs +11 -27
- package/engine/system-block.mjs +39 -141
- package/engine/system-vocabulary.mjs +6 -0
- package/engine/walk-files.mjs +69 -0
- package/engine/web-wikilinks.mjs +23 -3
- package/engine/wikilink-syntax.mjs +32 -21
- package/engine/wikilinks.mjs +27 -16
- package/githooks/attribution.mjs +8 -0
- package/githooks/commit-message.mjs +9 -0
- package/hm3/actors.mjs +9 -48
- package/hm3/default-item-art.mjs +9 -9
- package/hm3/item-fields.mjs +2 -9
- package/hm3/template-priority.mjs +6 -7
- package/hugo-theme/data/banner-kinds.yaml +78 -0
- package/hugo-theme/data/banners.yaml +2 -3
- package/hugo-theme/layouts/404.html +3 -3
- package/hugo-theme/layouts/partials/banner-url.html +31 -7
- package/hugo-theme/layouts/partials/hero-banner.html +13 -14
- package/hugo-theme/layouts/partials/related.html +1 -1
- package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
- package/hugo-theme/static/css/style.css +1 -1
- package/manifest.mjs +11 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +6 -3
- package/release.mjs +21 -14
- package/sohl/actors.mjs +16 -27
- package/sohl/default-item-art.mjs +3 -3
- package/sohl/document-subtypes.mjs +1 -16
- package/sohl/infobox.mjs +2 -3
- package/sohl/item-builders.mjs +5 -8
- package/sohl/item-fields.mjs +5 -35
- package/sohl/items.mjs +0 -4
- package/sohl/kb-passes.mjs +3 -3
- package/types/container.d.mts +116 -5
- package/types/content-config.d.mts +83 -35
- package/types/deploy.d.mts +42 -6
- package/types/e2e.d.mts +35 -3
- package/types/engine/address-charset.d.mts +4 -17
- package/types/engine/address.d.mts +17 -8
- package/types/engine/art-fields.d.mts +7 -4
- package/types/engine/art-slots.d.mts +6 -0
- package/types/engine/base-compiler.d.mts +15 -17
- package/types/engine/content-address.d.mts +9 -12
- package/types/engine/content-embeds.d.mts +4 -2
- package/types/engine/content-format-check.d.mts +0 -64
- package/types/engine/content-index.d.mts +82 -3
- package/types/engine/content-links.d.mts +2 -4
- package/types/engine/culture-choice.d.mts +51 -8
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/derived-fields.d.mts +1 -2
- package/types/engine/diagnostics.d.mts +27 -4
- package/types/engine/document-subtypes.d.mts +12 -31
- package/types/engine/event-views.d.mts +14 -2
- package/types/engine/exported-scene.d.mts +3 -2
- package/types/engine/field-spec.d.mts +13 -122
- package/types/engine/folder-notes.d.mts +0 -39
- package/types/engine/foreign-catalog.d.mts +2 -1
- package/types/engine/foundry-entries.d.mts +4 -5
- package/types/engine/frontmatter-lint.d.mts +2 -2
- package/types/engine/frontmatter.d.mts +19 -45
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +25 -42
- package/types/engine/homepage.d.mts +3 -10
- package/types/engine/ids.d.mts +0 -114
- package/types/engine/index-records.d.mts +6 -4
- package/types/engine/index.d.mts +0 -1
- package/types/engine/infobox-render.d.mts +7 -1
- package/types/engine/infobox.d.mts +3 -2
- package/types/engine/item-docs.d.mts +1 -1
- package/types/engine/item-registry.d.mts +2 -4
- package/types/engine/literature-notes.d.mts +1 -1
- package/types/engine/markdownlint-run.d.mts +40 -0
- package/types/engine/metadata-index.d.mts +9 -1
- package/types/engine/note-addresses.d.mts +27 -2
- package/types/engine/note-claims.d.mts +3 -5
- package/types/engine/note-ids.d.mts +2 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-state.d.mts +1 -3
- package/types/engine/note-vocabulary.d.mts +28 -21
- package/types/engine/pack-config.d.mts +2 -6
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/pathnames.d.mts +3 -3
- package/types/engine/pdf-build.d.mts +98 -12
- package/types/engine/pdf-coverage.d.mts +60 -0
- package/types/engine/pdf-index.d.mts +34 -5
- package/types/engine/pdf-pages.d.mts +143 -0
- package/types/engine/pdf-render.d.mts +45 -1
- package/types/engine/pdf-toc.d.mts +12 -14
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +11 -10
- package/types/engine/runtime-only-fields.d.mts +2 -2
- package/types/engine/site-build.d.mts +9 -5
- package/types/engine/site-index.d.mts +4 -5
- package/types/engine/site-root.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -2
- package/types/engine/standing-terms.d.mts +2 -1
- package/types/engine/standings.d.mts +7 -9
- package/types/engine/system-block.d.mts +10 -44
- package/types/engine/walk-files.d.mts +15 -0
- package/types/engine/wikilink-syntax.d.mts +18 -19
- package/types/hm3/actors.d.mts +1 -2
- package/types/hm3/item-fields.d.mts +2 -4
- package/types/hm3/template-priority.d.mts +6 -7
- package/types/manifest.d.mts +3 -2
- package/types/release.d.mts +8 -7
- package/types/sohl/actors.d.mts +2 -1
- package/types/sohl/document-subtypes.d.mts +1 -2
- package/types/sohl/item-fields.d.mts +3 -7
- package/engine/retired-fields.mjs +0 -631
- package/types/engine/retired-fields.d.mts +0 -354
package/engine/pdf-build.mjs
CHANGED
|
@@ -80,12 +80,14 @@ import { fileURLToPath } from "node:url";
|
|
|
80
80
|
|
|
81
81
|
import YAML from "yaml";
|
|
82
82
|
|
|
83
|
-
import { loadPackConfig } from "./pack-config.mjs";
|
|
83
|
+
import { CONFIG_FILENAMES, loadPackConfig, packConfigPath } from "./pack-config.mjs";
|
|
84
84
|
import { routerFor } from "./pack-router.mjs";
|
|
85
85
|
import { publishesContentPages } from "../content-config.mjs";
|
|
86
86
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
87
|
+
import { noteFile } from "./index-records.mjs";
|
|
88
|
+
import { coverage, isPrintable, recordKey } from "./pdf-coverage.mjs";
|
|
89
|
+
import { elsewhereFor, homeEntries, movedPages, pageTable, readEntryPages } from "./pdf-pages.mjs";
|
|
90
|
+
import { cachedDependencies, openNotesDatabase, prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
89
91
|
import { parseDocumentTree, runTreeFilters, planDocument } from "./pdf-toc.mjs";
|
|
90
92
|
import {
|
|
91
93
|
collectContentPages,
|
|
@@ -102,7 +104,12 @@ import {
|
|
|
102
104
|
} from "./site-index.mjs";
|
|
103
105
|
import { resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
104
106
|
import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
105
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
positionOfFrontmatterPath,
|
|
109
|
+
positionOfLiteral,
|
|
110
|
+
positionOfYamlPath,
|
|
111
|
+
yamlKeyPath,
|
|
112
|
+
} from "./diagnostics.mjs";
|
|
106
113
|
import { artPathname, assetAddressIndex, pathnameRoles } from "./art-fields.mjs";
|
|
107
114
|
import { embedRole } from "./content-embeds.mjs";
|
|
108
115
|
import { expandContentTables } from "./content-tables.mjs";
|
|
@@ -138,7 +145,7 @@ import {
|
|
|
138
145
|
resolveDanglingLabels,
|
|
139
146
|
} from "./pdf-render.mjs";
|
|
140
147
|
import { draftNoticeTypst } from "./draft-notice.mjs";
|
|
141
|
-
import { isDraftNote
|
|
148
|
+
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
142
149
|
import { infoboxTypstPreamble, infoboxesToTypst, linkToTypst } from "./infobox-render.mjs";
|
|
143
150
|
import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
144
151
|
import { resolveIconGlyphs } from "./pdf-fonts.mjs";
|
|
@@ -150,7 +157,7 @@ import { resolveAssetReplacement } from "./asset-replacement.mjs";
|
|
|
150
157
|
* The file on disk an authored image pathname names, or `null`.
|
|
151
158
|
*
|
|
152
159
|
* Two of the four forms {@link module:engine/pathnames.resolvePathname}
|
|
153
|
-
* derives,
|
|
160
|
+
* derives, taken together: `local` is the file in this repository's own tree,
|
|
154
161
|
* and `pdf` is where the book stages a copy of it.
|
|
155
162
|
*
|
|
156
163
|
* **Typst decides where that copy goes.** It resolves a path against its root —
|
|
@@ -357,21 +364,33 @@ const BOOK_MAP_DIR = "maps";
|
|
|
357
364
|
* @param {object} config - The resolved configuration.
|
|
358
365
|
* @param {string} outDir - Where the book is written.
|
|
359
366
|
* @param {object[]} findings - Collected here rather than thrown.
|
|
367
|
+
* @param {string} treeFile - The file the book's tree was read from — its
|
|
368
|
+
* `document`, or the configuration for a tree written inline — which is
|
|
369
|
+
* where a banner is declared.
|
|
370
|
+
* @param {string} [treeText] - That file's source, so a finding is located at
|
|
371
|
+
* the `banner:` key that declared the value.
|
|
360
372
|
* @returns {Map<string, string>} Declared path → the staged file's path,
|
|
361
373
|
* relative to the `.typ`.
|
|
362
374
|
*/
|
|
363
|
-
export function stageBanners(entries, config, outDir, findings =
|
|
375
|
+
export function stageBanners(entries, config, outDir, findings, treeFile, treeText = "") {
|
|
364
376
|
const staged = new Map();
|
|
365
377
|
const seen = new Set();
|
|
366
378
|
for (const entry of entries ?? []) {
|
|
367
379
|
const declared = entry?.presentation?.page?.banner;
|
|
368
380
|
if (typeof declared !== "string" || !declared.trim() || seen.has(declared)) continue;
|
|
369
381
|
seen.add(declared);
|
|
382
|
+
// The banner is declared by the `page:` mapping in force for the entry,
|
|
383
|
+
// which the plan carries as the path to the section that wrote it.
|
|
384
|
+
const at =
|
|
385
|
+
Array.isArray(entry.pageAt) ?
|
|
386
|
+
positionOfYamlPath(treeText, [...entry.pageAt, "banner"], { key: true })
|
|
387
|
+
: {};
|
|
370
388
|
const from = path.resolve(config.rootDir, declared);
|
|
371
389
|
const within = path.relative(config.rootDir, from);
|
|
372
390
|
if (within.startsWith("..") || path.isAbsolute(within)) {
|
|
373
391
|
findings.push({
|
|
374
|
-
file:
|
|
392
|
+
file: treeFile,
|
|
393
|
+
...at,
|
|
375
394
|
severity: "warning",
|
|
376
395
|
message:
|
|
377
396
|
`the banner \`${declared}\` is outside this package, so the section ` +
|
|
@@ -386,7 +405,8 @@ export function stageBanners(entries, config, outDir, findings = []) {
|
|
|
386
405
|
fs.copyFileSync(from, dest);
|
|
387
406
|
} catch (err) {
|
|
388
407
|
findings.push({
|
|
389
|
-
file:
|
|
408
|
+
file: treeFile,
|
|
409
|
+
...at,
|
|
390
410
|
severity: "warning",
|
|
391
411
|
message:
|
|
392
412
|
`the banner \`${declared}\` cannot be read, so the section plate is ` +
|
|
@@ -404,15 +424,15 @@ export function stageBanners(entries, config, outDir, findings = []) {
|
|
|
404
424
|
*
|
|
405
425
|
* The zip and the manifest take their names from the manifest, so an asset's
|
|
406
426
|
* name and its advertised URL cannot disagree. A PDF has no advertised URL, so
|
|
407
|
-
* its name is
|
|
408
|
-
*
|
|
427
|
+
* its name is the book's own file stem — its `out`, default
|
|
428
|
+
* `<contentPackage>-<id>` — with the version appended.
|
|
409
429
|
*
|
|
410
|
-
* @param {string}
|
|
430
|
+
* @param {string} stem - The book's file stem.
|
|
411
431
|
* @param {string} version - The version being released.
|
|
412
|
-
* @returns {string} `<
|
|
432
|
+
* @returns {string} `<stem>-<version>.pdf`, or `<stem>.pdf` unversioned.
|
|
413
433
|
*/
|
|
414
|
-
export function pdfFileName(
|
|
415
|
-
const safe = String(
|
|
434
|
+
export function pdfFileName(stem, version) {
|
|
435
|
+
const safe = String(stem || "content").replace(/[^\w.-]+/g, "-");
|
|
416
436
|
return version ? `${safe}-${version}.pdf` : `${safe}.pdf`;
|
|
417
437
|
}
|
|
418
438
|
|
|
@@ -420,9 +440,10 @@ export function pdfFileName(artifact, version) {
|
|
|
420
440
|
* Read and parse the declared document tree.
|
|
421
441
|
*
|
|
422
442
|
* @param {string} file - Absolute path to the document tree.
|
|
443
|
+
* @param {string} field - The key that names it, for the finding.
|
|
423
444
|
* @returns {{raw: unknown, text: string, findings: object[]}} What it held.
|
|
424
445
|
*/
|
|
425
|
-
function readDocumentTree(file) {
|
|
446
|
+
function readDocumentTree(file, field) {
|
|
426
447
|
let text;
|
|
427
448
|
try {
|
|
428
449
|
text = fs.readFileSync(file, "utf8");
|
|
@@ -434,7 +455,7 @@ function readDocumentTree(file) {
|
|
|
434
455
|
{
|
|
435
456
|
file,
|
|
436
457
|
severity: "error",
|
|
437
|
-
message:
|
|
458
|
+
message: `the document tree named by \`${field}\` cannot be read`,
|
|
438
459
|
},
|
|
439
460
|
],
|
|
440
461
|
};
|
|
@@ -457,96 +478,498 @@ function readDocumentTree(file) {
|
|
|
457
478
|
}
|
|
458
479
|
|
|
459
480
|
/**
|
|
460
|
-
*
|
|
481
|
+
* The release assets the declared books produce, in configuration order.
|
|
482
|
+
*
|
|
483
|
+
* Builds nothing and reads no note: each entry is the file {@link buildPdf}
|
|
484
|
+
* writes for that book at `version`, as a path relative to the working
|
|
485
|
+
* directory inside `out` (default the configured `pdf.out`, else
|
|
486
|
+
* `build/dist`). A package that publishes no book — no `pdf:` block, or a
|
|
487
|
+
* content tree holding only a homepage — lists none.
|
|
488
|
+
*
|
|
489
|
+
* @param {object} [opts] - Options.
|
|
490
|
+
* @param {object} [opts.config] - A resolved configuration; loaded when absent.
|
|
491
|
+
* @param {string} [opts.version] - The version stamped in each file name.
|
|
492
|
+
* @param {string} [opts.out] - Where the books are written, overriding `pdf.out`.
|
|
493
|
+
* @returns {Array<{id: string, title: string, file: string}>} One entry per book.
|
|
494
|
+
*/
|
|
495
|
+
export function listBookAssets({ config, version = "", out } = {}) {
|
|
496
|
+
const resolved = config ?? loadPackConfig();
|
|
497
|
+
if (!resolved.pdf || !publishesContentPages(resolved)) return [];
|
|
498
|
+
const outDir = path.resolve(
|
|
499
|
+
resolved.rootDir,
|
|
500
|
+
out || resolved.pdf.out || path.join("build", "dist"),
|
|
501
|
+
);
|
|
502
|
+
return resolved.pdf.books.map((book) => ({
|
|
503
|
+
id: book.id,
|
|
504
|
+
title: book.title,
|
|
505
|
+
file: path.relative(process.cwd(), path.join(outDir, pdfFileName(book.out, version))),
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* The configuration file a finding about a configured key names.
|
|
511
|
+
*
|
|
512
|
+
* The path the loader read when it loaded the configuration; for a
|
|
513
|
+
* configuration handed in by a caller, the one configuration file in its
|
|
514
|
+
* `rootDir`, which is where a YAML configuration always sits.
|
|
515
|
+
*
|
|
516
|
+
* @param {object|undefined} config - The configuration the caller supplied.
|
|
517
|
+
* @param {string} rootDir - The resolved configuration's root.
|
|
518
|
+
* @returns {string} Its absolute path, or `rootDir` when there is none to name.
|
|
519
|
+
*/
|
|
520
|
+
function configFileFor(config, rootDir) {
|
|
521
|
+
if (!config) return packConfigPath();
|
|
522
|
+
const found = CONFIG_FILENAMES.map((name) => path.join(rootDir, name)).find((file) =>
|
|
523
|
+
fs.existsSync(file),
|
|
524
|
+
);
|
|
525
|
+
return found ?? rootDir;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Build the books the `pdf:` block declares — every one, or the one `book`
|
|
530
|
+
* names.
|
|
531
|
+
*
|
|
532
|
+
* Every declared book's filters run against one corpus and one database first,
|
|
533
|
+
* so coverage and each note's home book are computed over the whole set
|
|
534
|
+
* whichever books are requested. A book whose filters select no note is an
|
|
535
|
+
* error at its `id` and is not built; a section that selects none is left out.
|
|
536
|
+
*
|
|
537
|
+
* A finding about a configured key — an unknown `--book`, a `coverImage`,
|
|
538
|
+
* `document` or `front` file that does not exist — names the configuration
|
|
539
|
+
* file and, for a YAML one, the line and column of that key. A book with a
|
|
540
|
+
* missing file is not built; the others still are.
|
|
461
541
|
*
|
|
462
542
|
* @param {object} [opts] - Options.
|
|
463
543
|
* @param {object} [opts.config] - A resolved configuration; loaded when absent.
|
|
544
|
+
* @param {string} [opts.configPath] - The file `config` was read from, for
|
|
545
|
+
* findings at a configured key. Defaults to the file the loader read, or to
|
|
546
|
+
* the configuration file in `config.rootDir`.
|
|
547
|
+
* @param {string} [opts.book] - The `id` of the one book to build; every book
|
|
548
|
+
* when absent.
|
|
464
549
|
* @param {string} [opts.out] - Where to write, overriding `pdf.out`.
|
|
465
550
|
* @param {string} [opts.version] - Stamped on the title page and the file name.
|
|
466
551
|
* @param {boolean} [opts.compile] - Whether to run Typst. False leaves the
|
|
467
|
-
* `.typ` source, which is what the unit tests read
|
|
552
|
+
* `.typ` source, which is what the unit tests read, and runs a single pass:
|
|
553
|
+
* a reference to a note another book is home to prints `p. ?`. True renders
|
|
554
|
+
* every book, measures the page each entry starts on, writes `pages.json`
|
|
555
|
+
* into the output directory, and renders again with those pages — see
|
|
556
|
+
* {@link module:engine/pdf-pages}.
|
|
557
|
+
* @param {Function} [opts.readPages] - {@link
|
|
558
|
+
* module:engine/pdf-pages.readEntryPages}, for tests.
|
|
468
559
|
* @param {Function} [opts.resolveReplacement] - {@link
|
|
469
560
|
* module:engine/asset-replacement.resolveAssetReplacement}, for tests.
|
|
470
561
|
* Loaded from that module otherwise, and only when a relationship declares
|
|
471
562
|
* `assetReplacement: true` — a build with none never imports it.
|
|
472
|
-
* @returns {Promise<
|
|
563
|
+
* @returns {Promise<{built: boolean, reason: string|null, findings: object[],
|
|
564
|
+
* imageReports: object[], books: Array<{id: string, title: string,
|
|
565
|
+
* built: boolean, typ: string, pdf: string|null, stats: object}>,
|
|
566
|
+
* homes: Map<string, string>, summary: object|null}>}
|
|
567
|
+
* `built` is true when every requested book was written (and compiled, when
|
|
568
|
+
* asked); `books` holds each book whose source was written. `homes` maps a
|
|
569
|
+
* selected note's canonical address to the id of the first book that selects
|
|
570
|
+
* it, and `summary` is {@link module:engine/pdf-coverage.coverage}'s, over
|
|
571
|
+
* every declared book whichever was requested; both are empty when a book's
|
|
572
|
+
* selection could not be read.
|
|
473
573
|
*/
|
|
474
574
|
export async function buildPdf({
|
|
475
575
|
config,
|
|
576
|
+
configPath,
|
|
577
|
+
book,
|
|
476
578
|
out,
|
|
477
579
|
version = "",
|
|
478
580
|
compile = true,
|
|
581
|
+
readPages = readEntryPages,
|
|
479
582
|
resolveReplacement,
|
|
480
583
|
} = {}) {
|
|
481
584
|
const resolved = config ?? loadPackConfig();
|
|
482
585
|
const findings = [];
|
|
586
|
+
const none = (reason) => ({
|
|
587
|
+
built: false,
|
|
588
|
+
reason,
|
|
589
|
+
findings,
|
|
590
|
+
imageReports: [],
|
|
591
|
+
books: [],
|
|
592
|
+
homes: new Map(),
|
|
593
|
+
summary: null,
|
|
594
|
+
});
|
|
483
595
|
|
|
484
|
-
// The fence, first — before
|
|
596
|
+
// The fence, first — before any tree is read, before anything is walked.
|
|
485
597
|
if (!publishesContentPages(resolved)) {
|
|
486
|
-
return
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
: "this package has no content tree, so there is nothing to print",
|
|
492
|
-
findings,
|
|
493
|
-
typ: null,
|
|
494
|
-
pdf: null,
|
|
495
|
-
stats: null,
|
|
496
|
-
};
|
|
598
|
+
return none(
|
|
599
|
+
fs.existsSync(resolved.paths.content) ?
|
|
600
|
+
"The content tree contains only a homepage, so there is no book to build."
|
|
601
|
+
: "this package has no content tree, so there is nothing to print",
|
|
602
|
+
);
|
|
497
603
|
}
|
|
498
604
|
if (!resolved.pdf) {
|
|
499
|
-
return
|
|
500
|
-
built: false,
|
|
501
|
-
reason: "no `pdf:` block is configured, so this package publishes no book",
|
|
502
|
-
findings,
|
|
503
|
-
typ: null,
|
|
504
|
-
pdf: null,
|
|
505
|
-
stats: null,
|
|
506
|
-
};
|
|
605
|
+
return none("no `pdf:` block is configured, so this package publishes no book");
|
|
507
606
|
}
|
|
508
|
-
|
|
509
607
|
const contentBase = resolved.paths.content;
|
|
510
608
|
if (!fs.existsSync(contentBase)) {
|
|
511
|
-
return
|
|
512
|
-
built: false,
|
|
513
|
-
reason: "this package has no content tree, so there is nothing to print",
|
|
514
|
-
findings,
|
|
515
|
-
typ: null,
|
|
516
|
-
pdf: null,
|
|
517
|
-
stats: null,
|
|
518
|
-
};
|
|
609
|
+
return none("this package has no content tree, so there is nothing to print");
|
|
519
610
|
}
|
|
520
611
|
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
612
|
+
const configFile = configPath ?? configFileFor(config, resolved.rootDir);
|
|
613
|
+
// Only a YAML configuration is searched for positions: JavaScript fed to a
|
|
614
|
+
// YAML parser parses as something, and a position that is wrong is worse
|
|
615
|
+
// than none.
|
|
616
|
+
let configText = "";
|
|
617
|
+
if (/\.ya?ml$/i.test(configFile)) {
|
|
618
|
+
try {
|
|
619
|
+
configText = fs.readFileSync(configFile, "utf8");
|
|
620
|
+
} catch {
|
|
621
|
+
configText = "";
|
|
622
|
+
}
|
|
525
623
|
}
|
|
624
|
+
const atKey = (field) => ({
|
|
625
|
+
file: configFile,
|
|
626
|
+
...positionOfYamlPath(configText, yamlKeyPath(field), { key: true }),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const declared = resolved.pdf.books.map((entry, index) => ({ book: entry, index }));
|
|
630
|
+
const requested =
|
|
631
|
+
book === undefined ? declared : declared.filter((entry) => entry.book.id === book);
|
|
632
|
+
if (!requested.length) {
|
|
633
|
+
findings.push({
|
|
634
|
+
...atKey("pdf.books"),
|
|
635
|
+
severity: "error",
|
|
636
|
+
message:
|
|
637
|
+
`--book \`${book}\` names no declared book; declared: ` +
|
|
638
|
+
declared.map((entry) => entry.book.id).join(", "),
|
|
639
|
+
});
|
|
640
|
+
return none(null);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const outDir = path.resolve(
|
|
644
|
+
resolved.rootDir,
|
|
645
|
+
out || resolved.pdf.out || path.join("build", "dist"),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const world = await prepareWorld(resolved, { contentBase, findings, resolveReplacement });
|
|
649
|
+
if (!world) return none(null);
|
|
650
|
+
|
|
651
|
+
const books = [];
|
|
652
|
+
const imageReports = [];
|
|
653
|
+
let built = true;
|
|
654
|
+
let homes = new Map();
|
|
655
|
+
let summary = null;
|
|
656
|
+
try {
|
|
657
|
+
// Every declared book's filters run before any book renders, because
|
|
658
|
+
// coverage and the home book are properties of the whole set.
|
|
659
|
+
const selections = [];
|
|
660
|
+
for (const { book: entry, index } of declared) {
|
|
661
|
+
const where = `pdf.books[${index}]`;
|
|
662
|
+
const wanted = requested.some((r) => r.index === index);
|
|
663
|
+
// Checked here rather than when the configuration loads, so a command
|
|
664
|
+
// that builds no book never fails for a cover held outside the tree.
|
|
665
|
+
// A book that is not requested is read for its selection alone.
|
|
666
|
+
const missing = [
|
|
667
|
+
...(wanted && entry.coverImage ?
|
|
668
|
+
[[`${where}.coverImage`, entry.coverImage, "cover image"]]
|
|
669
|
+
: []),
|
|
670
|
+
...(entry.document ? [[`${where}.document`, entry.document, "document tree"]] : []),
|
|
671
|
+
...(wanted ?
|
|
672
|
+
entry.front.map((file, j) => [
|
|
673
|
+
`${where}.front[${j}]`,
|
|
674
|
+
file,
|
|
675
|
+
"front-matter file",
|
|
676
|
+
])
|
|
677
|
+
: []),
|
|
678
|
+
].filter(([, file]) => !fs.existsSync(file));
|
|
679
|
+
for (const [field, file, what] of missing) {
|
|
680
|
+
findings.push({
|
|
681
|
+
...atKey(field),
|
|
682
|
+
severity: "error",
|
|
683
|
+
message:
|
|
684
|
+
`book \`${entry.id}\`: the ${what} \`` +
|
|
685
|
+
`${path.relative(resolved.rootDir, file).split(path.sep).join("/")}\` ` +
|
|
686
|
+
(wanted ?
|
|
687
|
+
"does not exist, so the book is not built"
|
|
688
|
+
: "does not exist, so coverage cannot be computed"),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (missing.length) {
|
|
692
|
+
selections.push({ usable: false, broken: true });
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
selections.push(
|
|
696
|
+
await selectBook(entry, index, {
|
|
697
|
+
world,
|
|
698
|
+
configFile,
|
|
699
|
+
configText,
|
|
700
|
+
findings,
|
|
701
|
+
}),
|
|
702
|
+
);
|
|
703
|
+
}
|
|
526
704
|
|
|
527
|
-
|
|
528
|
-
|
|
705
|
+
// Coverage, over every book whether or not it renders.
|
|
706
|
+
let unprinted = [];
|
|
707
|
+
let unprintedKnown = true;
|
|
708
|
+
if (resolved.pdf.unprinted) {
|
|
709
|
+
try {
|
|
710
|
+
({ rows: unprinted } = await world.db.query(
|
|
711
|
+
`SELECT * FROM notes WHERE ${resolved.pdf.unprinted}`,
|
|
712
|
+
));
|
|
713
|
+
} catch (err) {
|
|
714
|
+
unprintedKnown = false;
|
|
715
|
+
findings.push({
|
|
716
|
+
...atKey("pdf.unprinted"),
|
|
717
|
+
severity: "error",
|
|
718
|
+
message:
|
|
719
|
+
"the query did not run, so coverage cannot be computed — " +
|
|
720
|
+
String(err.message).split("\n")[0],
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (unprintedKnown && selections.every((selection) => selection.usable)) {
|
|
725
|
+
const covered = coverage({
|
|
726
|
+
books: declared.map(({ book: entry }, i) => ({
|
|
727
|
+
id: entry.id,
|
|
728
|
+
records: selections[i].records,
|
|
729
|
+
})),
|
|
730
|
+
corpus: world.records,
|
|
731
|
+
unprinted,
|
|
732
|
+
mode: resolved.pdf.coverage,
|
|
733
|
+
locate: (record) => locateType(contentBase, record),
|
|
734
|
+
});
|
|
735
|
+
findings.push(...covered.findings);
|
|
736
|
+
homes = covered.homes;
|
|
737
|
+
summary = covered.summary;
|
|
738
|
+
}
|
|
529
739
|
|
|
740
|
+
// Which books render, and which of those the caller asked for. A book
|
|
741
|
+
// whose selection could not be read, or selects nothing, is reported
|
|
742
|
+
// when it is requested and left out of the page table either way.
|
|
743
|
+
const buildable = [];
|
|
744
|
+
for (const { book: entry, index } of declared) {
|
|
745
|
+
const selection = selections[index];
|
|
746
|
+
const wanted = requested.some((r) => r.index === index);
|
|
747
|
+
if (!selection.usable) {
|
|
748
|
+
if (wanted) built = false;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (!selection.records.length) {
|
|
752
|
+
if (wanted) {
|
|
753
|
+
built = false;
|
|
754
|
+
// A tree that failed to read already has its findings.
|
|
755
|
+
if (!selection.broken) {
|
|
756
|
+
findings.push({
|
|
757
|
+
...atKey(`pdf.books[${index}].id`),
|
|
758
|
+
severity: "error",
|
|
759
|
+
message: `book \`${entry.id}\` selects no note`,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
buildable.push({ entry, index, selection, wanted });
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// The books that render are the books that are measured, so a page
|
|
769
|
+
// table is only ever read from the books it describes.
|
|
770
|
+
const toRender = compile ? buildable : buildable.filter((b) => b.wanted);
|
|
771
|
+
const homed = homeEntries({
|
|
772
|
+
books: buildable.map((b) => ({
|
|
773
|
+
id: b.entry.id,
|
|
774
|
+
title: b.entry.title,
|
|
775
|
+
plan: b.selection.plan,
|
|
776
|
+
})),
|
|
777
|
+
homes,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Render every book once against a page table.
|
|
782
|
+
*
|
|
783
|
+
* @param {Record<string, {book: string, page: number}>|undefined} table
|
|
784
|
+
* @returns {Promise<{written: Map<string, object>, local: object[], used: number}>}
|
|
785
|
+
*/
|
|
786
|
+
const renderAll = async (table) => {
|
|
787
|
+
const local = [];
|
|
788
|
+
const written = new Map();
|
|
789
|
+
let used = 0;
|
|
790
|
+
for (const { entry, selection } of toRender) {
|
|
791
|
+
const elsewhere = elsewhereFor({
|
|
792
|
+
entries: homed,
|
|
793
|
+
book: entry.id,
|
|
794
|
+
printed: selection.plan.links,
|
|
795
|
+
table,
|
|
796
|
+
seeAlso: resolved.pdf.indexSeeAlso,
|
|
797
|
+
});
|
|
798
|
+
const result = await buildBook(entry, {
|
|
799
|
+
resolved,
|
|
800
|
+
world,
|
|
801
|
+
selection,
|
|
802
|
+
authors: resolved.authors ?? [],
|
|
803
|
+
copyright: resolved.pdf.copyright,
|
|
804
|
+
license: resolved.pdf.license,
|
|
805
|
+
outDir,
|
|
806
|
+
contentBase,
|
|
807
|
+
version,
|
|
808
|
+
findings: local,
|
|
809
|
+
elsewhere,
|
|
810
|
+
homes,
|
|
811
|
+
});
|
|
812
|
+
used += elsewhere.used;
|
|
813
|
+
written.set(entry.id, result);
|
|
814
|
+
}
|
|
815
|
+
return { written, local, used };
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Lay every written book out and read the page each entry starts on.
|
|
820
|
+
*
|
|
821
|
+
* @param {Map<string, object>} written - From {@link renderAll}.
|
|
822
|
+
* @returns {{table: Record<string, {book: string, page: number}>|null,
|
|
823
|
+
* message: string}} The table, or null with why it could not be read.
|
|
824
|
+
*/
|
|
825
|
+
const measureAll = (written) => {
|
|
826
|
+
const fontArgs = typstFontArgs(resolved.pdf);
|
|
827
|
+
const measured = new Map();
|
|
828
|
+
for (const { entry } of toRender) {
|
|
829
|
+
const result = readPages(
|
|
830
|
+
written.get(entry.id).typ,
|
|
831
|
+
fontArgs,
|
|
832
|
+
resolved.pdf.binary || "typst",
|
|
833
|
+
);
|
|
834
|
+
if (!result.ok) return { table: null, message: result.message };
|
|
835
|
+
measured.set(entry.id, result.pages);
|
|
836
|
+
}
|
|
837
|
+
return { table: pageTable(homed, measured), message: "" };
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// The first pass prints `p. ?` for every note another book is home to.
|
|
841
|
+
// Without Typst there is nothing to measure, and that pass is the
|
|
842
|
+
// output.
|
|
843
|
+
let pass = await renderAll(undefined);
|
|
844
|
+
if (compile) {
|
|
845
|
+
const unread = (message) => {
|
|
846
|
+
findings.push({
|
|
847
|
+
file: configFile,
|
|
848
|
+
severity: "error",
|
|
849
|
+
message: `the page of each entry could not be read: ${message}`,
|
|
850
|
+
});
|
|
851
|
+
built = false;
|
|
852
|
+
};
|
|
853
|
+
let measure = measureAll(pass.written);
|
|
854
|
+
if (!measure.table) unread(measure.message);
|
|
855
|
+
else {
|
|
856
|
+
let table = measure.table;
|
|
857
|
+
// A second pass prints the measured pages. Those words change
|
|
858
|
+
// the layout, so the pages are measured again: when none moved
|
|
859
|
+
// the pass stands, and when any did the books are rendered a
|
|
860
|
+
// final time against the new table and whatever still differs
|
|
861
|
+
// is reported.
|
|
862
|
+
if (pass.used) {
|
|
863
|
+
pass = await renderAll(table);
|
|
864
|
+
measure = measureAll(pass.written);
|
|
865
|
+
if (!measure.table) unread(measure.message);
|
|
866
|
+
else if (movedPages(table, measure.table).length) {
|
|
867
|
+
const printed = measure.table;
|
|
868
|
+
pass = await renderAll(printed);
|
|
869
|
+
measure = measureAll(pass.written);
|
|
870
|
+
if (!measure.table) unread(measure.message);
|
|
871
|
+
else {
|
|
872
|
+
for (const moved of movedPages(printed, measure.table)) {
|
|
873
|
+
const home = homed.find((h) => h.key === moved.key);
|
|
874
|
+
findings.push({
|
|
875
|
+
file: noteFile(contentBase, home.record),
|
|
876
|
+
severity: "warning",
|
|
877
|
+
message:
|
|
878
|
+
`\`${home.record?.name?.full ?? home.slug}\` starts on page ` +
|
|
879
|
+
`${moved.after} of \`${home.title}\`, but the references ` +
|
|
880
|
+
`to it in other books print page ${moved.before}`,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
table = measure.table ?? table;
|
|
886
|
+
}
|
|
887
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
888
|
+
fs.writeFileSync(
|
|
889
|
+
path.join(outDir, "pages.json"),
|
|
890
|
+
`${JSON.stringify(table, null, 2)}\n`,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
findings.push(...pass.local);
|
|
895
|
+
|
|
896
|
+
for (const { entry, wanted } of toRender) {
|
|
897
|
+
if (!wanted) continue;
|
|
898
|
+
const result = pass.written.get(entry.id);
|
|
899
|
+
imageReports.push(...(result.imageReports ?? []));
|
|
900
|
+
const ok = compile ? compileBook(result, resolved.pdf, findings) : true;
|
|
901
|
+
if (!ok) built = false;
|
|
902
|
+
books.push({
|
|
903
|
+
id: entry.id,
|
|
904
|
+
title: entry.title,
|
|
905
|
+
built: ok,
|
|
906
|
+
typ: result.typ,
|
|
907
|
+
pdf: compile && ok ? result.pdf : null,
|
|
908
|
+
stats: result.stats,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
} finally {
|
|
912
|
+
await world.db.close?.();
|
|
913
|
+
fs.rmSync(world.dir, { recursive: true, force: true });
|
|
914
|
+
}
|
|
915
|
+
return { built, reason: null, findings, imageReports, books, homes, summary };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Where a note's `type:` key sits, for a finding about the note as a whole.
|
|
920
|
+
*
|
|
921
|
+
* @param {string} contentBase - The content tree.
|
|
922
|
+
* @param {object} record - The note's index record.
|
|
923
|
+
* @returns {{file: string, line?: number, column?: number}} The note's file and,
|
|
924
|
+
* when the key is found, its line and column.
|
|
925
|
+
*/
|
|
926
|
+
function locateType(contentBase, record) {
|
|
927
|
+
const file = noteFile(contentBase, record);
|
|
928
|
+
let raw;
|
|
929
|
+
try {
|
|
930
|
+
raw = fs.readFileSync(file, "utf8");
|
|
931
|
+
} catch {
|
|
932
|
+
return { file };
|
|
933
|
+
}
|
|
934
|
+
return { file, ...positionOfFrontmatterPath(raw, ["type"], { key: true }) };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Everything every book reads and none writes: the corpus, the notes database,
|
|
939
|
+
* the collected pages and their gates, the SQL tables, the address space
|
|
940
|
+
* pictures resolve against and the staging options.
|
|
941
|
+
*
|
|
942
|
+
* Built once per run. A book renders from it; coverage and the home book are
|
|
943
|
+
* computed over it.
|
|
944
|
+
*
|
|
945
|
+
* @param {object} resolved - The resolved configuration.
|
|
946
|
+
* @param {object} shared - Options.
|
|
947
|
+
* @param {string} shared.contentBase - The content tree.
|
|
948
|
+
* @param {object[]} shared.findings - Collected here rather than thrown.
|
|
949
|
+
* @param {Function} [shared.resolveReplacement] - As {@link buildPdf}.
|
|
950
|
+
* @returns {Promise<object|null>} The world, or null when a site gate fired
|
|
951
|
+
* and the findings say why.
|
|
952
|
+
*/
|
|
953
|
+
async function prepareWorld(resolved, { contentBase, findings, resolveReplacement }) {
|
|
530
954
|
// The corpus, once — the same records the index, the site and the SQL
|
|
531
955
|
// tables read, so the book cannot select a note the other surfaces do not
|
|
532
956
|
// have.
|
|
957
|
+
const parsed = new Map();
|
|
533
958
|
const records = indexRecordsFor({
|
|
534
959
|
contentBase,
|
|
535
960
|
config: resolved,
|
|
536
961
|
skipDirectories: resolved.skipDirectories,
|
|
962
|
+
parsed,
|
|
537
963
|
});
|
|
538
964
|
|
|
539
965
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "content-pdf-"));
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
966
|
+
// The one database over the corpus: the books' filters, the generated
|
|
967
|
+
// sections and every authored query read it.
|
|
968
|
+
const db = await openNotesDatabase(records, {
|
|
969
|
+
dir,
|
|
970
|
+
dependencies: await cachedDependencies(resolved),
|
|
971
|
+
audience: "public",
|
|
546
972
|
});
|
|
547
|
-
findings.push(...ran.findings.map((f) => ({ file: resolved.pdf.document, ...f })));
|
|
548
|
-
|
|
549
|
-
const plan = planDocument(parsed.nodes, { selections: ran.selections });
|
|
550
973
|
|
|
551
974
|
// Pages, indexed and gated exactly as the site does. A book of a tree whose
|
|
552
975
|
// addresses collide would print the wrong entry under the right name.
|
|
@@ -594,7 +1017,7 @@ export async function buildPdf({
|
|
|
594
1017
|
});
|
|
595
1018
|
}
|
|
596
1019
|
}
|
|
597
|
-
return
|
|
1020
|
+
return null;
|
|
598
1021
|
}
|
|
599
1022
|
|
|
600
1023
|
const byFile = new Map(collected.pages.map((page) => [page.file, page]));
|
|
@@ -603,6 +1026,8 @@ export async function buildPdf({
|
|
|
603
1026
|
config: resolved,
|
|
604
1027
|
skipDirectories: resolved.skipDirectories,
|
|
605
1028
|
records,
|
|
1029
|
+
parsed,
|
|
1030
|
+
database: db,
|
|
606
1031
|
audience: "public",
|
|
607
1032
|
});
|
|
608
1033
|
// The address space an `![[…]]` embed resolves against — the same one the
|
|
@@ -626,10 +1051,141 @@ export async function buildPdf({
|
|
|
626
1051
|
(assetReplacementRelationships(resolved).length ? resolveAssetReplacement : null),
|
|
627
1052
|
};
|
|
628
1053
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1054
|
+
return {
|
|
1055
|
+
records,
|
|
1056
|
+
db,
|
|
1057
|
+
dir,
|
|
1058
|
+
site,
|
|
1059
|
+
gates,
|
|
1060
|
+
byFile,
|
|
1061
|
+
universe,
|
|
1062
|
+
sqlTables,
|
|
1063
|
+
assets,
|
|
1064
|
+
md,
|
|
1065
|
+
glyphs,
|
|
1066
|
+
stagingOpts,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Read one book's tree and run its filters against the shared database.
|
|
1072
|
+
*
|
|
1073
|
+
* @param {import("../content-config.mjs").ResolvedPdfBook} book - The book.
|
|
1074
|
+
* @param {number} index - Its place in `pdf.books`.
|
|
1075
|
+
* @param {object} shared - What the selection reads.
|
|
1076
|
+
* @param {object} shared.world - From {@link prepareWorld}.
|
|
1077
|
+
* @param {string} shared.configFile - The configuration file.
|
|
1078
|
+
* @param {string} shared.configText - Its text, for positions.
|
|
1079
|
+
* @param {object[]} shared.findings - Collected here rather than thrown.
|
|
1080
|
+
* @returns {Promise<{usable: boolean, broken: boolean, treeFile?: string,
|
|
1081
|
+
* treeText?: string, plan?: object, records?: object[]}>} `usable` is false when the tree
|
|
1082
|
+
* cannot be read; `broken` is true when it was read with findings;
|
|
1083
|
+
* `records` holds every record the filters selected, a repeat counted again.
|
|
1084
|
+
*/
|
|
1085
|
+
async function selectBook(book, index, { world, configFile, configText, findings }) {
|
|
1086
|
+
// The tree is written inline in the configuration or kept in its own
|
|
1087
|
+
// file, and every finding about it names whichever file holds it.
|
|
1088
|
+
let treeFile;
|
|
1089
|
+
let treeText;
|
|
1090
|
+
let raw;
|
|
1091
|
+
let at;
|
|
1092
|
+
if (book.contents) {
|
|
1093
|
+
treeFile = configFile;
|
|
1094
|
+
treeText = configText;
|
|
1095
|
+
raw = { contents: book.contents };
|
|
1096
|
+
at = ["pdf", "books", index];
|
|
1097
|
+
} else {
|
|
1098
|
+
const tree = readDocumentTree(book.document, `pdf.books[${index}].document`);
|
|
1099
|
+
findings.push(...tree.findings);
|
|
1100
|
+
if (!tree.raw) return { usable: false, broken: true };
|
|
1101
|
+
treeFile = book.document;
|
|
1102
|
+
treeText = tree.text;
|
|
1103
|
+
raw = tree.raw;
|
|
1104
|
+
at = [];
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const parsed = parseDocumentTree(raw, { text: treeText, at });
|
|
1108
|
+
const parseFindings = parsed.findings.map((f) => ({ file: treeFile, ...f }));
|
|
1109
|
+
findings.push(...parseFindings);
|
|
1110
|
+
const ran = await runTreeFilters(parsed.nodes, world.db, {
|
|
1111
|
+
// A stub is a note and is deliberately in the corpus, but it has no
|
|
1112
|
+
// body — selecting one would print a blank page under its name.
|
|
1113
|
+
keep: isPrintable,
|
|
1114
|
+
text: treeText,
|
|
1115
|
+
});
|
|
1116
|
+
const filterFindings = ran.findings.map((f) => ({ file: treeFile, ...f }));
|
|
1117
|
+
findings.push(...filterFindings);
|
|
1118
|
+
return {
|
|
1119
|
+
usable: true,
|
|
1120
|
+
broken: [...parseFindings, ...filterFindings].some((f) => f.severity === "error"),
|
|
1121
|
+
treeFile,
|
|
1122
|
+
treeText,
|
|
1123
|
+
plan: planDocument(parsed.nodes, { selections: ran.selections }),
|
|
1124
|
+
records: [...ran.selections.values()].flat(),
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Build one book.
|
|
1130
|
+
*
|
|
1131
|
+
* Everything a book does not share with its siblings — its tree, its title,
|
|
1132
|
+
* subtitle and cover, its front matter and its file stem — is read from
|
|
1133
|
+
* `book`; everything shared from `shared`. The corpus, the database, the site
|
|
1134
|
+
* gates and the book's selection are computed before any book renders. Pictures
|
|
1135
|
+
* are staged under a directory named for the book's file stem, so one book's
|
|
1136
|
+
* resampling cannot change a file another book's source names.
|
|
1137
|
+
*
|
|
1138
|
+
* @param {import("../content-config.mjs").ResolvedPdfBook} book - The book.
|
|
1139
|
+
* @param {object} shared - What every book is built with.
|
|
1140
|
+
* @param {object} shared.resolved - The resolved configuration.
|
|
1141
|
+
* @param {number} shared.index - The book's place in `pdf.books`.
|
|
1142
|
+
* @param {object} shared.world - From {@link prepareWorld}.
|
|
1143
|
+
* @param {{treeFile: string, treeText: string, plan: object}} shared.selection - From
|
|
1144
|
+
* {@link selectBook}.
|
|
1145
|
+
* @param {readonly object[]} shared.authors - The top-level `authors`.
|
|
1146
|
+
* @param {string} shared.copyright - `pdf.copyright`; `""` when none.
|
|
1147
|
+
* @param {string} shared.license - `pdf.license`; `""` when none.
|
|
1148
|
+
* @param {string} shared.outDir - Where the book is written.
|
|
1149
|
+
* @param {string} shared.contentBase - The content tree.
|
|
1150
|
+
* @param {string} shared.version - Stamped on the title page and the file name.
|
|
1151
|
+
* @param {Map<string, string>} [shared.homes] - Each note's key → its home
|
|
1152
|
+
* book's id.
|
|
1153
|
+
* @param {{get: Function}} [shared.elsewhere] - From
|
|
1154
|
+
* {@link module:engine/pdf-pages.elsewhereFor}: the notes another book is
|
|
1155
|
+
* home to, which this book names by book and page rather than links.
|
|
1156
|
+
* @param {Function} [shared.resolveReplacement] - As {@link buildPdf}.
|
|
1157
|
+
* @param {object[]} shared.findings - Collected here rather than thrown.
|
|
1158
|
+
* @returns {Promise<{built: boolean, typ: string|null, pdf: string|null,
|
|
1159
|
+
* stats: object|null, imageReports?: object[]}>} What was written: the
|
|
1160
|
+
* source and its resampled pictures. The compile is {@link compileBook}'s.
|
|
1161
|
+
*/
|
|
1162
|
+
async function buildBook(book, shared) {
|
|
1163
|
+
const { resolved, world, selection, outDir, contentBase, version, findings, elsewhere } =
|
|
1164
|
+
shared;
|
|
1165
|
+
// The index lists the notes this book is home to; a note whose home is
|
|
1166
|
+
// another book is a reprint, and its term is in that book's index.
|
|
1167
|
+
const index = {
|
|
1168
|
+
owns: (entry) => (shared.homes?.get(recordKey(entry.record)) ?? book.id) === book.id,
|
|
1169
|
+
...(elsewhere?.seeAlso ? { foreign: elsewhere.listed } : {}),
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
const { treeFile, treeText, plan } = selection;
|
|
1173
|
+
// The directory this book stages its pictures in, beside its source.
|
|
1174
|
+
const stageDir = pdfFileName(book.out, "").replace(/\.pdf$/, "");
|
|
1175
|
+
const inBook = (relative) => `${stageDir}/${relative}`;
|
|
1176
|
+
/**
|
|
1177
|
+
* {@link stagedImagePath}, with the file placed in this book's directory.
|
|
1178
|
+
*
|
|
1179
|
+
* @param {string} src - The authored address.
|
|
1180
|
+
* @returns {{from: string, to: string}|null} As {@link stagedImagePath}.
|
|
1181
|
+
*/
|
|
1182
|
+
const stage = (src) => {
|
|
1183
|
+
const staged = stagedImagePath(src, resolved, world.stagingOpts);
|
|
1184
|
+
return staged && { ...staged, to: inBook(staged.to) };
|
|
1185
|
+
};
|
|
1186
|
+
const { records, site, gates, byFile, universe, sqlTables, assets, md, glyphs, stagingOpts } =
|
|
1187
|
+
world;
|
|
1188
|
+
|
|
633
1189
|
fs.mkdirSync(outDir, { recursive: true });
|
|
634
1190
|
|
|
635
1191
|
/** @type {Map<string, string>} Authored address → the staged file's path. */
|
|
@@ -659,14 +1215,32 @@ export async function buildPdf({
|
|
|
659
1215
|
* @returns {void}
|
|
660
1216
|
*/
|
|
661
1217
|
const stageImages = (body, file, columns = 2) => {
|
|
1218
|
+
// A finding about an image is located by searching the note for the
|
|
1219
|
+
// address the body matched, at the occurrence this image is among the
|
|
1220
|
+
// body's images of that address. Read once, and only for a finding.
|
|
1221
|
+
let raw;
|
|
1222
|
+
const seenHere = new Map();
|
|
1223
|
+
/** @param {string} src @returns {{line?: number, column?: number}} */
|
|
1224
|
+
const where = (src, occurrence) => {
|
|
1225
|
+
if (raw === undefined) {
|
|
1226
|
+
try {
|
|
1227
|
+
raw = fs.readFileSync(file, "utf8");
|
|
1228
|
+
} catch {
|
|
1229
|
+
raw = "";
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return positionOfLiteral(raw, src, occurrence);
|
|
1233
|
+
};
|
|
662
1234
|
for (const image of imagesIn(body)) {
|
|
663
1235
|
const src = image.src;
|
|
1236
|
+
const occurrence = (seenHere.get(src) ?? 0) + 1;
|
|
1237
|
+
seenHere.set(src, occurrence);
|
|
664
1238
|
// A generated section's drawing is the one this build drew beside
|
|
665
1239
|
// its source — see `drawn` below — and is vector, so nothing
|
|
666
1240
|
// resamples it.
|
|
667
1241
|
const drawing = generatedDrawing(src);
|
|
668
1242
|
if (drawing) {
|
|
669
|
-
if (drawn.maps.has(drawing)) images.set(src, `${BOOK_MAP_DIR}/${drawing}`);
|
|
1243
|
+
if (drawn.maps.has(drawing)) images.set(src, inBook(`${BOOK_MAP_DIR}/${drawing}`));
|
|
670
1244
|
continue;
|
|
671
1245
|
}
|
|
672
1246
|
const directive = parseImageDirective(image.directive);
|
|
@@ -698,19 +1272,30 @@ export async function buildPdf({
|
|
|
698
1272
|
// is mechanical and named in the message.
|
|
699
1273
|
const problem = pathnameProblem(src);
|
|
700
1274
|
if (problem) {
|
|
701
|
-
findings.push({
|
|
1275
|
+
findings.push({
|
|
1276
|
+
file,
|
|
1277
|
+
...where(src, occurrence),
|
|
1278
|
+
severity: "error",
|
|
1279
|
+
message: problem,
|
|
1280
|
+
});
|
|
702
1281
|
continue;
|
|
703
1282
|
}
|
|
704
1283
|
let staged;
|
|
705
1284
|
try {
|
|
706
|
-
staged =
|
|
1285
|
+
staged = stage(src);
|
|
707
1286
|
} catch (err) {
|
|
708
|
-
findings.push({
|
|
1287
|
+
findings.push({
|
|
1288
|
+
file,
|
|
1289
|
+
...where(src, occurrence),
|
|
1290
|
+
severity: "error",
|
|
1291
|
+
message: err.message,
|
|
1292
|
+
});
|
|
709
1293
|
continue;
|
|
710
1294
|
}
|
|
711
1295
|
if (!staged) {
|
|
712
1296
|
findings.push({
|
|
713
1297
|
file,
|
|
1298
|
+
...where(src, occurrence),
|
|
714
1299
|
severity: "warning",
|
|
715
1300
|
message:
|
|
716
1301
|
`\`${src}\` names a file this package does not ship, so the book ` +
|
|
@@ -726,6 +1311,7 @@ export async function buildPdf({
|
|
|
726
1311
|
} catch (err) {
|
|
727
1312
|
findings.push({
|
|
728
1313
|
file,
|
|
1314
|
+
...where(src, occurrence),
|
|
729
1315
|
severity: "warning",
|
|
730
1316
|
message:
|
|
731
1317
|
`\`${src}\` cannot be read from ` +
|
|
@@ -766,7 +1352,7 @@ export async function buildPdf({
|
|
|
766
1352
|
const resolveRole = (address) => embedRole(assets, address);
|
|
767
1353
|
const pathRoles = pathnameRoles(assets);
|
|
768
1354
|
const frontFigures = new Map();
|
|
769
|
-
for (const file of
|
|
1355
|
+
for (const file of book.front) {
|
|
770
1356
|
try {
|
|
771
1357
|
const scan = scanFigures(fs.readFileSync(file, "utf8"), { resolveRole });
|
|
772
1358
|
// Numbered here, because a number runs across the whole book and only
|
|
@@ -802,7 +1388,7 @@ export async function buildPdf({
|
|
|
802
1388
|
// any body is rendered so the section's image names a file the book holds.
|
|
803
1389
|
// A place's map is printed there, inside its entry, and nowhere else.
|
|
804
1390
|
const drawn = drawMapsFrom(records, {
|
|
805
|
-
outDir: path.join(outDir, BOOK_MAP_DIR),
|
|
1391
|
+
outDir: path.join(outDir, stageDir, BOOK_MAP_DIR),
|
|
806
1392
|
foreignIndex: gates.foreign.index,
|
|
807
1393
|
config: resolved,
|
|
808
1394
|
only: plan.entries
|
|
@@ -1018,6 +1604,7 @@ export async function buildPdf({
|
|
|
1018
1604
|
headingOffset,
|
|
1019
1605
|
anchorPrefix,
|
|
1020
1606
|
url: site,
|
|
1607
|
+
elsewhere,
|
|
1021
1608
|
findings,
|
|
1022
1609
|
file: page.file,
|
|
1023
1610
|
bodyLine: page.bodyLine,
|
|
@@ -1045,7 +1632,7 @@ export async function buildPdf({
|
|
|
1045
1632
|
contentPackage: gates.index.contentPackage,
|
|
1046
1633
|
});
|
|
1047
1634
|
const panel = infoboxesToTypst(boxes, {
|
|
1048
|
-
link: (value) => linkToTypst(value, plan.links, labelFor, site),
|
|
1635
|
+
link: (value) => linkToTypst(value, plan.links, labelFor, site, elsewhere),
|
|
1049
1636
|
});
|
|
1050
1637
|
// Whether the entry is settled comes before anything it says: the
|
|
1051
1638
|
// notice leads the leaf, before the authored body.
|
|
@@ -1081,7 +1668,7 @@ export async function buildPdf({
|
|
|
1081
1668
|
text = fs.readFileSync(file, "utf8");
|
|
1082
1669
|
} catch {
|
|
1083
1670
|
findings.push({
|
|
1084
|
-
file:
|
|
1671
|
+
file: treeFile,
|
|
1085
1672
|
severity: "error",
|
|
1086
1673
|
message: `the prose file \`${entry.file}\` cannot be read`,
|
|
1087
1674
|
});
|
|
@@ -1100,6 +1687,7 @@ export async function buildPdf({
|
|
|
1100
1687
|
anchorPrefix: entry.anchor,
|
|
1101
1688
|
captions: prepared.get(entry.anchor).numberedFigures,
|
|
1102
1689
|
url: site,
|
|
1690
|
+
elsewhere,
|
|
1103
1691
|
indexMentions: true,
|
|
1104
1692
|
findings,
|
|
1105
1693
|
file,
|
|
@@ -1117,7 +1705,7 @@ export async function buildPdf({
|
|
|
1117
1705
|
});
|
|
1118
1706
|
}
|
|
1119
1707
|
|
|
1120
|
-
const front =
|
|
1708
|
+
const front = book.front.map((file, position) => {
|
|
1121
1709
|
try {
|
|
1122
1710
|
const text = fs.readFileSync(file, "utf8");
|
|
1123
1711
|
stageImages(text, file, 1);
|
|
@@ -1127,9 +1715,10 @@ export async function buildPdf({
|
|
|
1127
1715
|
glyphs,
|
|
1128
1716
|
images,
|
|
1129
1717
|
assets: imageAssets,
|
|
1130
|
-
footnotePrefix: `footnote-front-${
|
|
1718
|
+
footnotePrefix: `footnote-front-${position + 1}`,
|
|
1131
1719
|
captions: frontFigures.get(file),
|
|
1132
1720
|
url: site,
|
|
1721
|
+
elsewhere,
|
|
1133
1722
|
indexMentions: true,
|
|
1134
1723
|
findings,
|
|
1135
1724
|
file,
|
|
@@ -1138,20 +1727,31 @@ export async function buildPdf({
|
|
|
1138
1727
|
findings.push({
|
|
1139
1728
|
file,
|
|
1140
1729
|
severity: "error",
|
|
1141
|
-
message:
|
|
1730
|
+
message: `the front-matter file named by \`pdf.books[${index}].front\` cannot be read`,
|
|
1142
1731
|
});
|
|
1143
1732
|
return "";
|
|
1144
1733
|
}
|
|
1145
1734
|
});
|
|
1146
1735
|
|
|
1147
|
-
const banners =
|
|
1736
|
+
const banners = new Map(
|
|
1737
|
+
[
|
|
1738
|
+
...stageBanners(
|
|
1739
|
+
plan.entries,
|
|
1740
|
+
resolved,
|
|
1741
|
+
path.join(outDir, stageDir),
|
|
1742
|
+
findings,
|
|
1743
|
+
treeFile,
|
|
1744
|
+
treeText,
|
|
1745
|
+
),
|
|
1746
|
+
].map(([declared, to]) => [declared, inBook(to)]),
|
|
1747
|
+
);
|
|
1148
1748
|
for (const [declared, to] of banners) {
|
|
1149
1749
|
const from = path.resolve(resolved.rootDir, declared);
|
|
1150
1750
|
imageCandidates.set(`banner:${declared}`, {
|
|
1151
1751
|
from,
|
|
1152
1752
|
to,
|
|
1153
1753
|
relative: path.relative(resolved.rootDir, from),
|
|
1154
|
-
file:
|
|
1754
|
+
file: treeFile,
|
|
1155
1755
|
uses: [{ width: PDF_PAGE.width, height: Infinity }],
|
|
1156
1756
|
});
|
|
1157
1757
|
}
|
|
@@ -1162,19 +1762,31 @@ export async function buildPdf({
|
|
|
1162
1762
|
if (!maps.has(anchor)) maps.set(anchor, []);
|
|
1163
1763
|
maps.get(anchor).push(page);
|
|
1164
1764
|
};
|
|
1165
|
-
const stageMapBackground = (entry, value, title) => {
|
|
1765
|
+
const stageMapBackground = (entry, value, title, keyPath) => {
|
|
1166
1766
|
const file = noteFile(contentBase, entry.record);
|
|
1767
|
+
// Located at the frontmatter key that declared the value. Read only
|
|
1768
|
+
// for a finding.
|
|
1769
|
+
const where = () => {
|
|
1770
|
+
let raw = "";
|
|
1771
|
+
try {
|
|
1772
|
+
raw = fs.readFileSync(file, "utf8");
|
|
1773
|
+
} catch {
|
|
1774
|
+
return {};
|
|
1775
|
+
}
|
|
1776
|
+
return positionOfFrontmatterPath(raw, keyPath);
|
|
1777
|
+
};
|
|
1167
1778
|
const found = artPathname(assets, value, "image", ["image", "icon"]);
|
|
1168
1779
|
let staged;
|
|
1169
1780
|
try {
|
|
1170
|
-
staged =
|
|
1781
|
+
staged = stage(found.pathname ?? value);
|
|
1171
1782
|
} catch (err) {
|
|
1172
|
-
findings.push({ file, severity: "error", message: err.message });
|
|
1783
|
+
findings.push({ file, ...where(), severity: "error", message: err.message });
|
|
1173
1784
|
return;
|
|
1174
1785
|
}
|
|
1175
1786
|
if (!staged || !fs.existsSync(staged.from)) {
|
|
1176
1787
|
findings.push({
|
|
1177
1788
|
file,
|
|
1789
|
+
...where(),
|
|
1178
1790
|
severity: "warning",
|
|
1179
1791
|
message: `the map background "${value}" cannot be resolved for the book`,
|
|
1180
1792
|
});
|
|
@@ -1218,32 +1830,76 @@ export async function buildPdf({
|
|
|
1218
1830
|
: []),
|
|
1219
1831
|
];
|
|
1220
1832
|
const fixups = entry.record.data?.fixup;
|
|
1221
|
-
const
|
|
1833
|
+
const fixupIndex = (Array.isArray(fixups) ? fixups : []).findIndex((item) =>
|
|
1222
1834
|
paths.includes(item?.path),
|
|
1223
1835
|
);
|
|
1836
|
+
const fixup = fixupIndex < 0 ? undefined : fixups[fixupIndex];
|
|
1224
1837
|
const value = fixup?.value ?? level.background?.src;
|
|
1838
|
+
// The key that supplied the value: the fixup's, else the Scene's.
|
|
1839
|
+
const keyPath =
|
|
1840
|
+
fixup?.value ?
|
|
1841
|
+
["data", "fixup", fixupIndex, "value"]
|
|
1842
|
+
: ["data", "scene", "levels", index, "background", "src"];
|
|
1225
1843
|
if (value)
|
|
1226
|
-
stageMapBackground(
|
|
1844
|
+
stageMapBackground(
|
|
1845
|
+
entry,
|
|
1846
|
+
value,
|
|
1847
|
+
level.name ? `${title}: ${level.name}` : title,
|
|
1848
|
+
keyPath,
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// The cover is staged like a banner and resampled to the larger of the
|
|
1854
|
+
// two sizes that fill the page when cropped.
|
|
1855
|
+
let cover = "";
|
|
1856
|
+
if (book.coverImage) {
|
|
1857
|
+
cover = inBook(`cover/${path.basename(book.coverImage)}`);
|
|
1858
|
+
try {
|
|
1859
|
+
fs.mkdirSync(path.dirname(path.join(outDir, cover)), { recursive: true });
|
|
1860
|
+
fs.copyFileSync(book.coverImage, path.join(outDir, cover));
|
|
1861
|
+
imageCandidates.set(`cover:${book.coverImage}`, {
|
|
1862
|
+
from: book.coverImage,
|
|
1863
|
+
to: cover,
|
|
1864
|
+
relative: path.relative(resolved.rootDir, book.coverImage),
|
|
1865
|
+
file: treeFile,
|
|
1866
|
+
uses: [
|
|
1867
|
+
{ width: PDF_PAGE.width, height: Infinity },
|
|
1868
|
+
{ width: Infinity, height: PDF_PAGE.height },
|
|
1869
|
+
],
|
|
1870
|
+
});
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
cover = "";
|
|
1873
|
+
findings.push({
|
|
1874
|
+
file: treeFile,
|
|
1875
|
+
severity: "warning",
|
|
1876
|
+
message: `the cover image \`${book.coverImage}\` cannot be read, so the book opens on its title page: ${err.message}`,
|
|
1877
|
+
});
|
|
1227
1878
|
}
|
|
1228
1879
|
}
|
|
1229
1880
|
|
|
1230
1881
|
const assembled = renderBook({
|
|
1231
1882
|
plan,
|
|
1232
1883
|
bodies,
|
|
1233
|
-
title:
|
|
1234
|
-
subtitle:
|
|
1884
|
+
title: book.title,
|
|
1885
|
+
subtitle: book.subtitle,
|
|
1235
1886
|
front,
|
|
1236
1887
|
fonts: resolved.pdf.fonts,
|
|
1237
1888
|
version,
|
|
1238
1889
|
preamble: infoboxTypstPreamble(),
|
|
1239
1890
|
banners,
|
|
1240
1891
|
maps,
|
|
1892
|
+
coverImage: cover,
|
|
1893
|
+
authors: shared.authors,
|
|
1894
|
+
copyright: shared.copyright,
|
|
1895
|
+
license: shared.license,
|
|
1896
|
+
index,
|
|
1241
1897
|
});
|
|
1242
1898
|
// Last, over the whole document: a reference can only be checked once every
|
|
1243
1899
|
// declaration is in one string, and Typst treats a dangling one as fatal.
|
|
1244
1900
|
const source = resolveDanglingLabels(assembled, findings);
|
|
1245
1901
|
|
|
1246
|
-
const stem = pdfFileName(
|
|
1902
|
+
const stem = pdfFileName(book.out, version);
|
|
1247
1903
|
const typPath = path.join(outDir, stem.replace(/\.pdf$/, ".typ"));
|
|
1248
1904
|
const pdfPath = path.join(outDir, stem);
|
|
1249
1905
|
fs.writeFileSync(typPath, source);
|
|
@@ -1251,33 +1907,25 @@ export async function buildPdf({
|
|
|
1251
1907
|
const imageReports = await resamplePdfImages(imageCandidates, outDir);
|
|
1252
1908
|
|
|
1253
1909
|
const stats = { ...plan.stats, entries: plan.entries.length, bytes: source.length };
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
built: true,
|
|
1257
|
-
reason: null,
|
|
1258
|
-
findings,
|
|
1259
|
-
imageReports,
|
|
1260
|
-
typ: typPath,
|
|
1261
|
-
pdf: null,
|
|
1262
|
-
stats,
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1910
|
+
return { built: true, imageReports, typ: typPath, pdf: pdfPath, stats };
|
|
1911
|
+
}
|
|
1265
1912
|
|
|
1266
|
-
|
|
1913
|
+
/**
|
|
1914
|
+
* Compile one written book.
|
|
1915
|
+
*
|
|
1916
|
+
* @param {{typ: string, pdf: string}} written - From {@link buildBook}.
|
|
1917
|
+
* @param {object} pdf - The resolved `pdf:` block.
|
|
1918
|
+
* @param {object[]} findings - Collected here rather than thrown.
|
|
1919
|
+
* @returns {boolean} Whether the PDF was written.
|
|
1920
|
+
*/
|
|
1921
|
+
function compileBook(written, pdf, findings) {
|
|
1922
|
+
const compiled = compileTypst(written.typ, written.pdf, pdf);
|
|
1267
1923
|
findings.push(...compiled.findings);
|
|
1268
1924
|
if (!compiled.ok) {
|
|
1269
|
-
findings.push({ file:
|
|
1270
|
-
return
|
|
1271
|
-
built: false,
|
|
1272
|
-
reason: null,
|
|
1273
|
-
findings,
|
|
1274
|
-
imageReports,
|
|
1275
|
-
typ: typPath,
|
|
1276
|
-
pdf: null,
|
|
1277
|
-
stats,
|
|
1278
|
-
};
|
|
1925
|
+
findings.push({ file: written.typ, severity: "error", message: compiled.message });
|
|
1926
|
+
return false;
|
|
1279
1927
|
}
|
|
1280
|
-
return
|
|
1928
|
+
return true;
|
|
1281
1929
|
}
|
|
1282
1930
|
|
|
1283
1931
|
/**
|
|
@@ -1318,19 +1966,23 @@ export const BOOK_FONTS_PATH = path.join(
|
|
|
1318
1966
|
* @returns {string[]} The arguments, in order.
|
|
1319
1967
|
*/
|
|
1320
1968
|
export function typstArgs(typPath, pdfPath, pdf = {}) {
|
|
1969
|
+
return ["compile", ...typstFontArgs(pdf), typPath, pdfPath];
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* The flags that decide which faces a layout uses, shared by the compile and by
|
|
1974
|
+
* the page measurement so both lay the book out in the same metrics.
|
|
1975
|
+
*
|
|
1976
|
+
* @param {object} [pdf] - The resolved `pdf:` block.
|
|
1977
|
+
* @returns {string[]} The arguments, in order.
|
|
1978
|
+
*/
|
|
1979
|
+
export function typstFontArgs(pdf = {}) {
|
|
1321
1980
|
// One flag, the paths joined by the platform's separator: the compiler
|
|
1322
1981
|
// takes a list, and the shipped faces come first so a consumer's own
|
|
1323
1982
|
// directory extends the set rather than standing in for it.
|
|
1324
1983
|
const fontPaths = [BOOK_FONTS_PATH];
|
|
1325
1984
|
if (pdf.fonts?.path) fontPaths.push(pdf.fonts.path);
|
|
1326
|
-
return [
|
|
1327
|
-
"compile",
|
|
1328
|
-
"--ignore-system-fonts",
|
|
1329
|
-
"--font-path",
|
|
1330
|
-
fontPaths.join(path.delimiter),
|
|
1331
|
-
typPath,
|
|
1332
|
-
pdfPath,
|
|
1333
|
-
];
|
|
1985
|
+
return ["--ignore-system-fonts", "--font-path", fontPaths.join(path.delimiter)];
|
|
1334
1986
|
}
|
|
1335
1987
|
|
|
1336
1988
|
/**
|