@heroiclands/package-build 22.21.0 → 22.23.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 +223 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +77 -17
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +153 -6
- package/docs/api.md +120 -89
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +41 -3
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +325 -35
- package/docs/commands.md +12 -10
- package/docs/configuration.md +117 -14
- package/docs/content-format.md +4 -2
- package/docs/guides/book.md +5 -4
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +545 -104
- package/docs/reference/note-types.md +30 -13
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +26 -30
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +7 -1
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +139 -22
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +13 -2
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +197 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +38 -12
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +143 -18
- package/engine/frontmatter-lint.mjs +120 -65
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +321 -0
- package/engine/helpers.mjs +109 -41
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +129 -15
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +294 -50
- package/engine/lore-events.mjs +197 -0
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +434 -59
- package/engine/note-dates.mjs +354 -12
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +183 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +431 -74
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +563 -66
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +26 -7
- package/engine/site-build.mjs +166 -23
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +90 -3
- package/engine/site-preview.mjs +20 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standings.mjs +89 -6
- package/engine/system-block.mjs +56 -30
- package/engine/web-wikilinks.mjs +48 -1
- package/hm3/actors.mjs +8 -5
- package/manifest.mjs +43 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/actors.mjs +22 -99
- package/sohl/item-fields.mjs +5 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/content-config.d.mts +48 -1
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +19 -17
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +80 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +179 -0
- package/types/engine/helpers.d.mts +29 -10
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +41 -12
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +46 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +82 -6
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +84 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +27 -4
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +67 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standings.d.mts +21 -3
- package/types/engine/system-block.d.mts +37 -14
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/hm3/actors.d.mts +21 -3
- package/types/manifest.d.mts +19 -23
- package/types/sohl/actors.d.mts +6 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-captions.mjs +0 -181
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-captions.d.mts +0 -45
- package/types/engine/content-secrets.d.mts +0 -17
package/engine/pdf-build.mjs
CHANGED
|
@@ -94,18 +94,25 @@ import {
|
|
|
94
94
|
gatesFailed,
|
|
95
95
|
resolveSitePass,
|
|
96
96
|
} from "./site-build.mjs";
|
|
97
|
-
import {
|
|
97
|
+
import {
|
|
98
|
+
figureIndexKeys,
|
|
99
|
+
resolveCrossNoteFigures,
|
|
100
|
+
resolveInfoboxRef,
|
|
101
|
+
wikiContext,
|
|
102
|
+
} from "./site-index.mjs";
|
|
98
103
|
import { resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
99
104
|
import { linkFindingMessage } from "./wikilink-syntax.mjs";
|
|
100
|
-
import {
|
|
105
|
+
import { positionOfLiteral } from "./diagnostics.mjs";
|
|
106
|
+
import { artPathname, assetAddressIndex, pathnameRoles } from "./art-fields.mjs";
|
|
107
|
+
import { embedRole } from "./content-embeds.mjs";
|
|
101
108
|
import { expandContentTables } from "./content-tables.mjs";
|
|
102
109
|
import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
103
|
-
import {
|
|
104
|
-
import {
|
|
105
|
-
import { numberCaptions, scanCaptions } from "./content-captions.mjs";
|
|
110
|
+
import { numberFigures, scanFigures } from "./content-figures.mjs";
|
|
111
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
106
112
|
import { protectCode } from "./code-fences.mjs";
|
|
107
113
|
import { imagesIn, parseImageDirective } from "./content-images.mjs";
|
|
108
114
|
import {
|
|
115
|
+
bookImageWidthIn,
|
|
109
116
|
PDF_IMAGE_INCHES,
|
|
110
117
|
PDF_PAGE,
|
|
111
118
|
pdfColumnWidth,
|
|
@@ -113,7 +120,15 @@ import {
|
|
|
113
120
|
pdfTextWidth,
|
|
114
121
|
resamplePdfImages,
|
|
115
122
|
} from "./pdf-images.mjs";
|
|
116
|
-
import {
|
|
123
|
+
import {
|
|
124
|
+
ASSETS_SEGMENT,
|
|
125
|
+
assetPathnameKey,
|
|
126
|
+
pathnameProblem,
|
|
127
|
+
resolvePathname,
|
|
128
|
+
} from "./pathnames.mjs";
|
|
129
|
+
import { ASSET_SYSTEM, assetTypeOfRoot, isAssetShortcode } from "./asset-types.mjs";
|
|
130
|
+
import { canonicalKey } from "./address.mjs";
|
|
131
|
+
import { isComplete as isForeignCacheComplete, newestVersionDir } from "./metadata-index.mjs";
|
|
117
132
|
import {
|
|
118
133
|
createParser,
|
|
119
134
|
labelFor,
|
|
@@ -128,6 +143,7 @@ import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
|
128
143
|
import { resolveIconGlyphs } from "./pdf-fonts.mjs";
|
|
129
144
|
import { buildMaps, relatedPlaces } from "./map-build.mjs";
|
|
130
145
|
import { mapWorld } from "./map-places.mjs";
|
|
146
|
+
import { resolveAssetReplacement } from "./asset-replacement.mjs";
|
|
131
147
|
|
|
132
148
|
/**
|
|
133
149
|
* The file on disk an authored image pathname names, or `null`.
|
|
@@ -144,21 +160,165 @@ import { mapWorld } from "./map-places.mjs";
|
|
|
144
160
|
* makes the source a consumer can compile by hand with no flags, and what keeps
|
|
145
161
|
* a build from touching a path outside its own output.
|
|
146
162
|
*
|
|
147
|
-
*
|
|
148
|
-
* package's file
|
|
149
|
-
*
|
|
150
|
-
*
|
|
163
|
+
* A file **this** package ships stages directly. A pathname naming another
|
|
164
|
+
* package's file stages instead from a declared replacement's cached archive,
|
|
165
|
+
* when `opts.resolveReplacement` — the pure function
|
|
166
|
+
* {@link module:engine/asset-replacement.resolveAssetReplacement} —
|
|
167
|
+
* answers it; `opts.foreignIndex` is the merged index that answer is read
|
|
168
|
+
* against. A pathname naming a package no replacement carries it for, or a
|
|
169
|
+
* URL, names something no build here can open — a build reaches no network —
|
|
170
|
+
* and the caller reports it as a picture the book will not carry.
|
|
151
171
|
*
|
|
152
172
|
* @param {string} src - The pathname, as authored.
|
|
153
173
|
* @param {object} config - The resolved configuration.
|
|
174
|
+
* @param {object} [opts]
|
|
175
|
+
* @param {Map<string, object>} [opts.foreignIndex] - Every fetched
|
|
176
|
+
* dependency's own records, keyed by canonical address — the index a
|
|
177
|
+
* replacement's record is read out of.
|
|
178
|
+
* @param {Function} [opts.resolveReplacement] - {@link
|
|
179
|
+
* module:engine/asset-replacement.resolveAssetReplacement}, or an
|
|
180
|
+
* equivalent a caller injects for a test. Omitted, a foreign pathname this
|
|
181
|
+
* package does not own stays declined exactly as it always has.
|
|
154
182
|
* @returns {{from: string, to: string}|null} The file, and where under the
|
|
155
183
|
* output directory it is staged.
|
|
184
|
+
* @throws {Error} When a declared replacement answers the address but its
|
|
185
|
+
* archive was never fetched.
|
|
156
186
|
*/
|
|
157
|
-
export function stagedImagePath(src, config) {
|
|
187
|
+
export function stagedImagePath(src, config, opts = {}) {
|
|
158
188
|
const forms = resolvePathname(src, config);
|
|
159
|
-
if (!forms || forms.state !== "package"
|
|
189
|
+
if (!forms || forms.state !== "package") return null;
|
|
190
|
+
// A declared replacement answers before the package's own file does, which
|
|
191
|
+
// is the whole of what declaring one buys: the address a note writes is
|
|
192
|
+
// this package's own, and the picture staged for it is the replacement's.
|
|
193
|
+
// A miss falls through, so a package declaring none stages exactly what it
|
|
194
|
+
// ships.
|
|
195
|
+
const replaced = opts.resolveReplacement ? stagedReplacementPath(forms, config, opts) : null;
|
|
196
|
+
if (replaced) return replaced;
|
|
197
|
+
if (forms.own) {
|
|
198
|
+
return {
|
|
199
|
+
from: path.resolve(config.rootDir, forms.local),
|
|
200
|
+
to: forms.pdf,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Every declared relationship that opted a dependency in as this package's
|
|
208
|
+
* asset replacement, in declaration order.
|
|
209
|
+
*
|
|
210
|
+
* The same shape {@link module:engine/foreign-catalog.itemCatalogRelationships}
|
|
211
|
+
* returns, read directly rather than through that module: this file stays a
|
|
212
|
+
* leaf over the configuration it is handed, and the flag is as small to read
|
|
213
|
+
* back as it was to validate.
|
|
214
|
+
*
|
|
215
|
+
* @param {object} config - The resolved build configuration.
|
|
216
|
+
* @returns {Array<{id: string, package: string}>} The relationships, each
|
|
217
|
+
* with the package segment its own content index publishes under.
|
|
218
|
+
*/
|
|
219
|
+
function assetReplacementRelationships(config) {
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const entries of Object.values(config.relationships ?? {})) {
|
|
222
|
+
for (const rel of entries ?? []) {
|
|
223
|
+
if (rel.assetReplacement) {
|
|
224
|
+
out.push({ id: rel.id, package: rel.contentPackage ?? rel.id });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The address a pathname this package does not own is read back as, for
|
|
233
|
+
* asking whether a declared replacement answers it instead of the package
|
|
234
|
+
* the pathname names.
|
|
235
|
+
*
|
|
236
|
+
* {@link module:engine/asset-replacement.resolveAssetReplacement} only ever
|
|
237
|
+
* matches an address whose package segment is the compiling package's own,
|
|
238
|
+
* so the pathname's own package is set aside and its suffix — the one piece
|
|
239
|
+
* every surface derives from, and the piece a replacement publishes under
|
|
240
|
+
* its own package instead — is read back as this package's type and
|
|
241
|
+
* shortcode.
|
|
242
|
+
*
|
|
243
|
+
* @param {import("./pathnames.mjs").PathnameForms} forms - The pathname,
|
|
244
|
+
* resolved.
|
|
245
|
+
* @param {object} config - The resolved build configuration.
|
|
246
|
+
* @returns {string|null} The canonical address, or `null` when the suffix is
|
|
247
|
+
* not shaped like an asset's.
|
|
248
|
+
*/
|
|
249
|
+
function localAssetAddress(forms, config) {
|
|
250
|
+
const segments = String(forms.suffix ?? "").split("/");
|
|
251
|
+
const assetType = assetTypeOfRoot(segments[0]);
|
|
252
|
+
if (!assetType) return null;
|
|
253
|
+
const base = segments[segments.length - 1];
|
|
254
|
+
const ext = path.extname(base);
|
|
255
|
+
const shortcode = (ext ? base.slice(0, -ext.length) : base).toLowerCase();
|
|
256
|
+
if (!isAssetShortcode(shortcode)) return null;
|
|
257
|
+
return canonicalKey(config.contentPackage, ASSET_SYSTEM, assetType.type, shortcode);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The cache directory one relationship's archive was fetched into, found the
|
|
262
|
+
* way {@link module:engine/foreign-catalog.foreignItemCatalogDirs} finds an
|
|
263
|
+
* item catalogue's: the newest `.complete`-stamped `<id>@<version>`
|
|
264
|
+
* directory under `config.paths.foreignCache`.
|
|
265
|
+
*
|
|
266
|
+
* @param {object} config - The resolved build configuration.
|
|
267
|
+
* @param {string} id - The relationship's dependency id.
|
|
268
|
+
* @returns {string|null} The directory, or `null` when nothing is cached.
|
|
269
|
+
*/
|
|
270
|
+
function cachedReplacementDir(config, id) {
|
|
271
|
+
const root = config.paths.foreignCache;
|
|
272
|
+
if (!fs.existsSync(root)) return null;
|
|
273
|
+
const cached = fs
|
|
274
|
+
.readdirSync(root)
|
|
275
|
+
.filter((name) => name.startsWith(`${id}@`))
|
|
276
|
+
.map((name) => path.join(root, name))
|
|
277
|
+
.filter(isForeignCacheComplete);
|
|
278
|
+
return cached.length ? newestVersionDir(cached) : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The file a declared replacement stages in place of a pathname this package
|
|
283
|
+
* does not own, or `null` when no replacement answers it.
|
|
284
|
+
*
|
|
285
|
+
* @param {import("./pathnames.mjs").PathnameForms} forms - The pathname,
|
|
286
|
+
* resolved, with `forms.own` already `false`.
|
|
287
|
+
* @param {object} config - The resolved build configuration.
|
|
288
|
+
* @param {object} opts
|
|
289
|
+
* @param {Map<string, object>} [opts.foreignIndex] - Every fetched
|
|
290
|
+
* dependency's own records, keyed by canonical address.
|
|
291
|
+
* @param {Function} opts.resolveReplacement - {@link
|
|
292
|
+
* module:engine/asset-replacement.resolveAssetReplacement}.
|
|
293
|
+
* @returns {{from: string, to: string}|null} The file, staged at the same
|
|
294
|
+
* `pdf`-relative path this address would have taken had this package
|
|
295
|
+
* shipped it.
|
|
296
|
+
* @throws {Error} When the replacement that answers the address has not
|
|
297
|
+
* been fetched.
|
|
298
|
+
*/
|
|
299
|
+
function stagedReplacementPath(forms, config, { foreignIndex = new Map(), resolveReplacement }) {
|
|
300
|
+
const address = localAssetAddress(forms, config);
|
|
301
|
+
if (!address) return null;
|
|
302
|
+
const relationships = assetReplacementRelationships(config);
|
|
303
|
+
if (!relationships.length) return null;
|
|
304
|
+
const hit = resolveReplacement(address, {
|
|
305
|
+
localPackage: config.contentPackage,
|
|
306
|
+
replacements: relationships.map((rel) => rel.package),
|
|
307
|
+
index: foreignIndex,
|
|
308
|
+
});
|
|
309
|
+
if (!hit) return null;
|
|
310
|
+
const assetPath = hit.record?.asset?.path;
|
|
311
|
+
if (!assetPath) return null;
|
|
312
|
+
const rel = relationships.find((entry) => entry.package === hit.package);
|
|
313
|
+
const dir = cachedReplacementDir(config, rel?.id ?? hit.package);
|
|
314
|
+
if (!dir) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`\`${hit.package}\` declares \`assetReplacement: true\` and carries \`${address}\`, ` +
|
|
317
|
+
"but its archive has not been fetched. Run `package-build deps fetch` first.",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
160
320
|
return {
|
|
161
|
-
from: path.
|
|
321
|
+
from: path.join(dir, ASSETS_SEGMENT, assetPath),
|
|
162
322
|
to: forms.pdf,
|
|
163
323
|
};
|
|
164
324
|
}
|
|
@@ -301,9 +461,19 @@ function readDocumentTree(file) {
|
|
|
301
461
|
* @param {string} [opts.version] - Stamped on the title page and the file name.
|
|
302
462
|
* @param {boolean} [opts.compile] - Whether to run Typst. False leaves the
|
|
303
463
|
* `.typ` source, which is what the unit tests read.
|
|
464
|
+
* @param {Function} [opts.resolveReplacement] - {@link
|
|
465
|
+
* module:engine/asset-replacement.resolveAssetReplacement}, for tests.
|
|
466
|
+
* Loaded from that module otherwise, and only when a relationship declares
|
|
467
|
+
* `assetReplacement: true` — a build with none never imports it.
|
|
304
468
|
* @returns {Promise<object>} `{ built, reason, findings, typ, pdf, stats }`.
|
|
305
469
|
*/
|
|
306
|
-
export async function buildPdf({
|
|
470
|
+
export async function buildPdf({
|
|
471
|
+
config,
|
|
472
|
+
out,
|
|
473
|
+
version = "",
|
|
474
|
+
compile = true,
|
|
475
|
+
resolveReplacement,
|
|
476
|
+
} = {}) {
|
|
307
477
|
const resolved = config ?? loadPackConfig();
|
|
308
478
|
const findings = [];
|
|
309
479
|
|
|
@@ -378,6 +548,23 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
378
548
|
// addresses collide would print the wrong entry under the right name.
|
|
379
549
|
const scheme = resolved.publish.address;
|
|
380
550
|
const base = resolved.site.base || `/${resolved.contentPackage}/`;
|
|
551
|
+
// Where the pages this book cites are served. A page's address is written
|
|
552
|
+
// from the root of a site, which is right everywhere on the web and nowhere
|
|
553
|
+
// in a PDF, so the book resolves each one against this — see
|
|
554
|
+
// {@link module:engine/pdf-render.absolutePageUrl}. `package.json`'s
|
|
555
|
+
// `homepage` is the one absolute address a build holds, and the site reads
|
|
556
|
+
// its own `baseURL` from it, so the book and the website cannot disagree
|
|
557
|
+
// about where a page is.
|
|
558
|
+
const site = typeof resolved.homepage === "string" ? resolved.homepage : "";
|
|
559
|
+
if (!site) {
|
|
560
|
+
findings.push({
|
|
561
|
+
file: "package.json",
|
|
562
|
+
severity: "error",
|
|
563
|
+
message:
|
|
564
|
+
"`homepage` is not declared, so a page the book cites is set as a path " +
|
|
565
|
+
`rather than an address a reader can follow — add \`https://www.heroiclands.org/${resolved.contentPackage}/\``,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
381
568
|
const ctx = {
|
|
382
569
|
packages: new Set(
|
|
383
570
|
resolved.site.packages.length ? resolved.site.packages : [resolved.contentPackage],
|
|
@@ -424,6 +611,17 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
424
611
|
const md = createParser(resolved.icons);
|
|
425
612
|
const glyphs = resolveIconGlyphs(resolved.icons, resolved.pdf.iconFonts, findings);
|
|
426
613
|
|
|
614
|
+
// Asked only where a relationship declares `assetReplacement: true`: with
|
|
615
|
+
// none declared the resolver is never consulted, and staging gives the
|
|
616
|
+
// answer it gives a package that declares no replacement. A caller may
|
|
617
|
+
// supply its own, which is how the staging tests drive it.
|
|
618
|
+
const stagingOpts = {
|
|
619
|
+
foreignIndex: gates.foreign.index,
|
|
620
|
+
resolveReplacement:
|
|
621
|
+
resolveReplacement ??
|
|
622
|
+
(assetReplacementRelationships(resolved).length ? resolveAssetReplacement : null),
|
|
623
|
+
};
|
|
624
|
+
|
|
427
625
|
const outDir = path.resolve(
|
|
428
626
|
resolved.rootDir,
|
|
429
627
|
out || resolved.pdf.out || path.join("build", "dist"),
|
|
@@ -432,6 +630,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
432
630
|
|
|
433
631
|
/** @type {Map<string, string>} Authored address → the staged file's path. */
|
|
434
632
|
const images = new Map();
|
|
633
|
+
/** @type {Map<string, {type: string, role?: string, width: number|"", height: number|""}>}
|
|
634
|
+
* Authored address, exactly as the note wrote it → its asset's role and
|
|
635
|
+
* pixel size. Keyed the same way {@link images} is rather than by the
|
|
636
|
+
* asset index's own canonical pathname, because a body image is free to
|
|
637
|
+
* write the bare, own-package form — see
|
|
638
|
+
* {@link module:engine/pathnames.assetPathnameKey}. */
|
|
639
|
+
const imageAssets = new Map();
|
|
435
640
|
/** @type {Map<string, {from: string, to: string, relative: string, file: string, uses: Array<{width: number, height: number}>}>} */
|
|
436
641
|
const imageCandidates = new Map();
|
|
437
642
|
/** @type {Set<string>} Addresses already looked for, staged or not. */
|
|
@@ -456,8 +661,18 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
456
661
|
const wide =
|
|
457
662
|
directive.size === "full-width" || directive.classes.includes("full-width");
|
|
458
663
|
const availableWidth = wide ? pdfTextWidth : pdfColumnWidth(columns);
|
|
664
|
+
const assetKey = assetPathnameKey(src, resolved);
|
|
665
|
+
const asset = assetKey ? assets.byPath.get(assetKey) : undefined;
|
|
666
|
+
if (asset) imageAssets.set(src, asset);
|
|
667
|
+
// `auto` is the one size a role or an icon's nominal width can
|
|
668
|
+
// narrow — a named `size=` is the author's own statement, honoured
|
|
669
|
+
// as it always was.
|
|
670
|
+
const roleWidth = directive.size === "auto" ? bookImageWidthIn(asset) : undefined;
|
|
459
671
|
const use = {
|
|
460
|
-
width: Math.min(
|
|
672
|
+
width: Math.min(
|
|
673
|
+
roleWidth ?? PDF_IMAGE_INCHES[directive.size] ?? availableWidth,
|
|
674
|
+
availableWidth,
|
|
675
|
+
),
|
|
461
676
|
height: pdfTextHeight * 0.8,
|
|
462
677
|
};
|
|
463
678
|
if (seenImages.has(src)) {
|
|
@@ -474,7 +689,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
474
689
|
findings.push({ file, severity: "error", message: problem });
|
|
475
690
|
continue;
|
|
476
691
|
}
|
|
477
|
-
|
|
692
|
+
let staged;
|
|
693
|
+
try {
|
|
694
|
+
staged = stagedImagePath(src, resolved, stagingOpts);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
findings.push({ file, severity: "error", message: err.message });
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
478
699
|
if (!staged) {
|
|
479
700
|
findings.push({
|
|
480
701
|
file,
|
|
@@ -523,20 +744,24 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
523
744
|
* @param {number} columns - The entry's page columns.
|
|
524
745
|
* @returns {string} Typst markup.
|
|
525
746
|
*/
|
|
526
|
-
const
|
|
527
|
-
|
|
747
|
+
const figureCounts = { code: 0, table: 0, figure: 0, map: 0, prose: 0 };
|
|
748
|
+
// The role lookup a `:::figure` fence's `map` counter reaches through —
|
|
749
|
+
// see `engine/content-figures.mjs`'s `resolveRole`. Addressed form, for a
|
|
750
|
+
// page's own scan below, which runs before its embeds are rewritten into
|
|
751
|
+
// ordinary images; `pathRoles` is the same lookup keyed by the pathname
|
|
752
|
+
// that rewrite leaves behind, for the renderer's own rescan of the
|
|
753
|
+
// resolved body.
|
|
754
|
+
const resolveRole = (address) => embedRole(assets, address);
|
|
755
|
+
const pathRoles = pathnameRoles(assets);
|
|
756
|
+
const frontFigures = new Map();
|
|
528
757
|
for (const file of resolved.pdf.front) {
|
|
529
758
|
try {
|
|
530
|
-
const scan =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
column: error.column,
|
|
537
|
-
severity: "error",
|
|
538
|
-
message: error.message,
|
|
539
|
-
});
|
|
759
|
+
const scan = scanFigures(fs.readFileSync(file, "utf8"), { resolveRole });
|
|
760
|
+
// Numbered here, because a number runs across the whole book and only
|
|
761
|
+
// this loop knows the order; what is *wrong* with a figure is
|
|
762
|
+
// reported by the renderer, which reads every one of the three kinds
|
|
763
|
+
// of markdown a book is made of.
|
|
764
|
+
frontFigures.set(file, numberFigures(scan.figures, figureCounts));
|
|
540
765
|
} catch {
|
|
541
766
|
// The rendering pass reports an unreadable front-matter file.
|
|
542
767
|
}
|
|
@@ -547,21 +772,95 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
547
772
|
config: resolved,
|
|
548
773
|
book: true,
|
|
549
774
|
});
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Every entry's own figures, numbered across the whole book in reading
|
|
778
|
+
* order, before any entry's expressions render — so a cross-note `ref`
|
|
779
|
+
* finds the number its target carries in the finished book rather than a
|
|
780
|
+
* page-local recount. Walked in the same order {@link bodies} fills
|
|
781
|
+
* below, so `figureCounts` lands on the same final tallies either way;
|
|
782
|
+
* the note's table expansion is cached here too, so the render loop does
|
|
783
|
+
* not redo it.
|
|
784
|
+
*
|
|
785
|
+
* @type {Map<string, {markdown?: string, errors?: object[],
|
|
786
|
+
* lineMap?: object[], numberedFigures: object[]}>}
|
|
787
|
+
*/
|
|
788
|
+
const prepared = new Map();
|
|
789
|
+
// Every note's figures, by the address a `ref` crossing into it would
|
|
790
|
+
// write — see `engine/site-index.mjs`'s `figureIndexKeys`.
|
|
791
|
+
const figuresByAddress = new Map();
|
|
792
|
+
for (const entry of plan.entries) {
|
|
793
|
+
if (entry.kind === "note") {
|
|
794
|
+
const file = noteFile(contentBase, entry.record);
|
|
795
|
+
const page = byFile.get(file);
|
|
796
|
+
if (!page) continue; // reported as `missing` by the render loop below
|
|
797
|
+
const src = page.relPath ?? page.base;
|
|
798
|
+
const { markdown, errors, lineMap } = expandContentTables(page.body, {
|
|
799
|
+
docs: universe.get(page.pkg) ?? [],
|
|
800
|
+
linkable: (d) => Boolean(d.fm.shortcode),
|
|
801
|
+
source: src,
|
|
802
|
+
sqlTables: sqlTables?.get(page.file),
|
|
803
|
+
pageLists: sqlTables?.pageLists?.get(page.file),
|
|
804
|
+
self: { fm: page.fm, path: page.relPath },
|
|
805
|
+
});
|
|
806
|
+
const numberedFigures = numberFigures(
|
|
807
|
+
scanFigures(markdown, { resolveRole }).figures,
|
|
808
|
+
figureCounts,
|
|
809
|
+
);
|
|
810
|
+
prepared.set(entry.anchor, { markdown, errors, lineMap, numberedFigures });
|
|
811
|
+
const shortcode = page.fm.shortcode;
|
|
812
|
+
if (typeof shortcode === "string" && shortcode) {
|
|
813
|
+
const byId = new Map(
|
|
814
|
+
numberedFigures
|
|
815
|
+
.filter((figure) => figure.id)
|
|
816
|
+
.map((figure) => [
|
|
817
|
+
figure.id,
|
|
818
|
+
{
|
|
819
|
+
label: figure.label,
|
|
820
|
+
caption: figure.caption,
|
|
821
|
+
hasCaption: figure.hasCaption,
|
|
822
|
+
},
|
|
823
|
+
]),
|
|
824
|
+
);
|
|
825
|
+
const type = String(page.fm.type ?? "").toLowerCase();
|
|
826
|
+
for (const key of figureIndexKeys(page.pkg, type, shortcode))
|
|
827
|
+
figuresByAddress.set(key, byId);
|
|
828
|
+
}
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (entry.kind === "prose") {
|
|
832
|
+
const file = path.resolve(resolved.rootDir, entry.file);
|
|
833
|
+
let text;
|
|
834
|
+
try {
|
|
835
|
+
text = fs.readFileSync(file, "utf8");
|
|
836
|
+
} catch {
|
|
837
|
+
continue; // reported by the render loop below
|
|
838
|
+
}
|
|
839
|
+
prepared.set(entry.anchor, {
|
|
840
|
+
numberedFigures: numberFigures(
|
|
841
|
+
scanFigures(text, { resolveRole }).figures,
|
|
842
|
+
figureCounts,
|
|
843
|
+
),
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
550
848
|
const renderPage = (page, headingOffset, anchorPrefix, columns) => {
|
|
551
849
|
const src = page.relPath ?? page.base;
|
|
552
850
|
const wikiErrors = [];
|
|
553
|
-
const { markdown, errors, lineMap } =
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
self: { fm: page.fm, path: page.relPath },
|
|
559
|
-
});
|
|
851
|
+
const { markdown, errors, lineMap, numberedFigures } = prepared.get(anchorPrefix);
|
|
852
|
+
// A content-table finding states its `reason`, its 0-based line within
|
|
853
|
+
// the body and its column, exactly as the site build reports them.
|
|
854
|
+
// Reading it as `message` yields `[object Object]` and discards the
|
|
855
|
+
// position, which is the one thing a reader needs to find the fence.
|
|
560
856
|
for (const err of errors) {
|
|
561
857
|
findings.push({
|
|
562
858
|
file: page.file,
|
|
859
|
+
...(typeof err.line === "number" ?
|
|
860
|
+
{ line: (page.bodyLine ?? 1) + err.line, column: err.column }
|
|
861
|
+
: {}),
|
|
563
862
|
severity: "error",
|
|
564
|
-
message: String(err.message ?? err),
|
|
863
|
+
message: err.reason ?? String(err.message ?? err),
|
|
565
864
|
});
|
|
566
865
|
}
|
|
567
866
|
const linkCtx = wikiContext(gates.index, {
|
|
@@ -572,20 +871,31 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
572
871
|
foreignIndex: gates.foreign.index,
|
|
573
872
|
assets,
|
|
574
873
|
});
|
|
575
|
-
const captionScan = scanCaptions(markdown);
|
|
576
|
-
const numberedCaptions = numberCaptions(captionScan.captions, captionCounts);
|
|
577
874
|
linkCtx.captionLabels = new Map(
|
|
578
|
-
|
|
875
|
+
numberedFigures
|
|
876
|
+
.filter((figure) => figure.id)
|
|
877
|
+
.map((figure) => [figure.id, figure.label]),
|
|
579
878
|
);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
column: error.column,
|
|
585
|
-
severity: "error",
|
|
586
|
-
message: error.message,
|
|
587
|
-
});
|
|
879
|
+
// This page's own anchors, so a `[[#slug]]` self-link is checked
|
|
880
|
+
// against what the page actually declares, exactly as the site build
|
|
881
|
+
// checks it.
|
|
882
|
+
linkCtx.anchors = new Set(collectAnchors(markdown).map((anchor) => anchor.slug));
|
|
588
883
|
linkCtx.output = "book";
|
|
884
|
+
// What the `ref` expression helper needs beyond the label — the
|
|
885
|
+
// caption text and whether one was authored — keyed as
|
|
886
|
+
// `engine/site-build.mjs` keys its own, but built from this note's
|
|
887
|
+
// *book-wide* numbering rather than its page-local count: the book
|
|
888
|
+
// numbers each kind across the whole book in reading order, so a
|
|
889
|
+
// reference has to read the number the book gives the figure, not the
|
|
890
|
+
// number the note would give it alone.
|
|
891
|
+
const figuresById = new Map(
|
|
892
|
+
numberedFigures
|
|
893
|
+
.filter((figure) => figure.id)
|
|
894
|
+
.map((figure) => [
|
|
895
|
+
figure.id,
|
|
896
|
+
{ label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
|
|
897
|
+
]),
|
|
898
|
+
);
|
|
589
899
|
// Code fences are protected for the same reason every other pass
|
|
590
900
|
// protects them: a wikilink shown as an example is prose about a
|
|
591
901
|
// wikilink, and resolving it would make the example impossible to write.
|
|
@@ -595,58 +905,91 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
595
905
|
sqlResults: sqlTables?.inline?.get(page.file),
|
|
596
906
|
file: page.file,
|
|
597
907
|
bodyLine: page.bodyLine,
|
|
908
|
+
figures: {
|
|
909
|
+
get: (id) => figuresById.get(id),
|
|
910
|
+
// A cross-note `ref` resolves the written address exactly as
|
|
911
|
+
// a wikilink does, against every note's figures numbered
|
|
912
|
+
// before any entry's expressions render — see `prepared`
|
|
913
|
+
// above. The default link renders the target's own web
|
|
914
|
+
// address, which `renderLink` in `engine/pdf-render.mjs`
|
|
915
|
+
// resolves to the entry the book gave it, exactly as an
|
|
916
|
+
// ordinary cross-note wikilink does.
|
|
917
|
+
note: (target) =>
|
|
918
|
+
resolveCrossNoteFigures(target, {
|
|
919
|
+
contentTypes: gates.index.contentTypes,
|
|
920
|
+
packages: gates.index.packages,
|
|
921
|
+
noIndexPackages: gates.index.noIndexPackages,
|
|
922
|
+
contentPackage: gates.index.contentPackage,
|
|
923
|
+
siteIndexMap: gates.index.index,
|
|
924
|
+
figuresByAddress,
|
|
925
|
+
}),
|
|
926
|
+
},
|
|
598
927
|
});
|
|
599
928
|
findings.push(...expressions.findings);
|
|
600
929
|
const resolvedBody = protectCode(expressions.markdown, (text) =>
|
|
601
930
|
resolveWebWikilinks(pass.beforeLinks ? pass.beforeLinks(text, page) : text, linkCtx),
|
|
602
931
|
);
|
|
932
|
+
// A link finding carries the authored link and which occurrence of it
|
|
933
|
+
// this was, which is exactly what locates it: the position is implicit
|
|
934
|
+
// in the literal the pass matched, so it is recovered by searching the
|
|
935
|
+
// note for it rather than dropped. Read once per page, and only when
|
|
936
|
+
// there is a finding to locate.
|
|
937
|
+
let raw;
|
|
938
|
+
const noteSource = () => {
|
|
939
|
+
if (raw === undefined) {
|
|
940
|
+
try {
|
|
941
|
+
raw = fs.readFileSync(page.file, "utf8");
|
|
942
|
+
} catch {
|
|
943
|
+
raw = "";
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return raw;
|
|
947
|
+
};
|
|
948
|
+
// An **error**, as it is from the pack build and from the site build. One
|
|
949
|
+
// authored address that resolves nowhere is one defect, and a reader of
|
|
950
|
+
// the book is the one reader who cannot act on it: a page printing
|
|
951
|
+
// `(unresolved link)` has shipped the mistake to somebody holding paper.
|
|
952
|
+
// The source is written and the compiler still runs, so the finding is a
|
|
953
|
+
// report on a book that was built, exactly as the site writes its pages
|
|
954
|
+
// and refuses the run.
|
|
603
955
|
for (const err of wikiErrors) {
|
|
604
956
|
findings.push({
|
|
605
957
|
file: page.file,
|
|
606
|
-
|
|
958
|
+
...positionOfLiteral(noteSource(), err.link, err.occurrence),
|
|
959
|
+
severity: "error",
|
|
607
960
|
// A link finding names a `reason` from the shared table and no
|
|
608
961
|
// sentence of its own; an embed's directive complaint carries
|
|
609
962
|
// the sentence instead.
|
|
610
963
|
message: err.message ?? (err.reason ? linkFindingMessage(err) : String(err)),
|
|
611
964
|
});
|
|
612
965
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
findings.push({
|
|
616
|
-
file: page.file,
|
|
617
|
-
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
618
|
-
column: error.column,
|
|
619
|
-
severity: "error",
|
|
620
|
-
message: error.message,
|
|
621
|
-
});
|
|
622
|
-
}
|
|
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);
|
|
633
|
-
const prose = markdownToTypst(secrets.markdown, {
|
|
966
|
+
stageImages(resolvedBody, page.file, columns);
|
|
967
|
+
const prose = markdownToTypst(resolvedBody, {
|
|
634
968
|
md,
|
|
635
969
|
links: plan.links,
|
|
636
970
|
glyphs,
|
|
637
971
|
images,
|
|
972
|
+
assets: imageAssets,
|
|
638
973
|
headingOffset,
|
|
639
974
|
anchorPrefix,
|
|
640
|
-
captions:
|
|
975
|
+
captions: numberedFigures,
|
|
976
|
+
url: site,
|
|
977
|
+
findings,
|
|
978
|
+
file: page.file,
|
|
979
|
+
bodyLine: page.bodyLine,
|
|
980
|
+
lineMap,
|
|
981
|
+
prepared: true,
|
|
982
|
+
resolveRole: (pathname) => pathRoles.get(pathname),
|
|
641
983
|
});
|
|
642
984
|
// Infoboxes follow the authored body within this note's entry.
|
|
643
985
|
const boxes = noteInfoboxes(page.fm, {
|
|
644
986
|
resolve: (ref, hint) => resolveInfoboxRef(gates.index, ref, hint),
|
|
645
987
|
router: routerFor(resolved),
|
|
646
988
|
dates: gates.index.dateContext,
|
|
989
|
+
contentPackage: gates.index.contentPackage,
|
|
647
990
|
});
|
|
648
991
|
const panel = infoboxesToTypst(boxes, {
|
|
649
|
-
link: (value) => linkToTypst(value, plan.links, labelFor),
|
|
992
|
+
link: (value) => linkToTypst(value, plan.links, labelFor, site),
|
|
650
993
|
});
|
|
651
994
|
// Whether the entry is settled comes before anything it says: the
|
|
652
995
|
// notice leads the leaf, before the authored body.
|
|
@@ -696,9 +1039,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
696
1039
|
links: plan.links,
|
|
697
1040
|
glyphs,
|
|
698
1041
|
images,
|
|
1042
|
+
assets: imageAssets,
|
|
699
1043
|
headingOffset: entry.depth,
|
|
700
1044
|
anchorPrefix: entry.anchor,
|
|
701
|
-
captions:
|
|
1045
|
+
captions: prepared.get(entry.anchor).numberedFigures,
|
|
1046
|
+
url: site,
|
|
1047
|
+
findings,
|
|
1048
|
+
file,
|
|
702
1049
|
}),
|
|
703
1050
|
);
|
|
704
1051
|
}
|
|
@@ -722,8 +1069,12 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
722
1069
|
links: plan.links,
|
|
723
1070
|
glyphs,
|
|
724
1071
|
images,
|
|
1072
|
+
assets: imageAssets,
|
|
725
1073
|
footnotePrefix: `footnote-front-${index + 1}`,
|
|
726
|
-
captions:
|
|
1074
|
+
captions: frontFigures.get(file),
|
|
1075
|
+
url: site,
|
|
1076
|
+
findings,
|
|
1077
|
+
file,
|
|
727
1078
|
});
|
|
728
1079
|
} catch {
|
|
729
1080
|
findings.push({
|
|
@@ -756,7 +1107,13 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
756
1107
|
const stageMapBackground = (entry, value, title) => {
|
|
757
1108
|
const file = noteFile(contentBase, entry.record);
|
|
758
1109
|
const found = artPathname(assets, value, "image", ["image", "icon"]);
|
|
759
|
-
|
|
1110
|
+
let staged;
|
|
1111
|
+
try {
|
|
1112
|
+
staged = stagedImagePath(found.pathname ?? value, resolved, stagingOpts);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
findings.push({ file, severity: "error", message: err.message });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
760
1117
|
if (!staged || !fs.existsSync(staged.from)) {
|
|
761
1118
|
findings.push({
|
|
762
1119
|
file,
|