@heroiclands/package-build 22.22.0 → 22.24.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 +46 -0
- package/README.md +1 -0
- package/bin/content-commands.mjs +51 -14
- package/ci/content-format-reference.mjs +1 -1
- package/content-config.mjs +80 -2
- package/docs/api.md +85 -72
- package/docs/authoring/assets.md +29 -0
- package/docs/authoring/dates-and-calendars.md +37 -1
- package/docs/authoring/links-and-markup.md +112 -31
- package/docs/commands.md +30 -29
- package/docs/configuration.md +92 -19
- package/docs/content-format.md +1 -1
- package/docs/guides/book.md +5 -4
- package/docs/guides/module-project.md +2 -2
- package/docs/guides/site.md +41 -0
- package/docs/project-setup.md +4 -4
- package/docs/reference/format-details.md +323 -60
- package/docs/reference/note-types.md +9 -9
- package/docs/reference/theme-pages.md +496 -0
- package/engine/anchors.mjs +16 -10
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +9 -2
- 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 +54 -2
- package/engine/bundles.mjs +13 -1
- package/engine/content-blocks.mjs +4 -4
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +10 -2
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +36 -10
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +145 -19
- package/engine/frontmatter-lint.mjs +73 -1
- package/engine/heading-attributes.mjs +31 -18
- package/engine/helpers.mjs +102 -33
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox.mjs +47 -8
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +264 -39
- package/engine/lore-events.mjs +197 -0
- package/engine/markdown-expressions.mjs +257 -7
- package/engine/note-dates.mjs +305 -12
- package/engine/note-vocabulary.mjs +11 -5
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +357 -34
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +160 -36
- package/engine/scenes.mjs +16 -4
- package/engine/site-build.mjs +127 -13
- package/engine/site-config.mjs +19 -24
- package/engine/site-index.mjs +72 -1
- package/engine/site-preview.mjs +2 -0
- package/engine/system-block.mjs +44 -16
- package/hm3/actors.mjs +2 -2
- package/hugo-theme/data/banners.yaml +69 -0
- package/hugo-theme/layouts/404.html +85 -0
- package/hugo-theme/layouts/_default/baseof.html +64 -0
- package/hugo-theme/layouts/_default/list.html +139 -0
- package/hugo-theme/layouts/_default/single.html +4 -0
- package/hugo-theme/layouts/blog/list.html +54 -0
- package/hugo-theme/layouts/blog-post/single.html +36 -0
- package/hugo-theme/layouts/index.html +75 -0
- package/hugo-theme/layouts/partials/banner-url.html +117 -0
- package/hugo-theme/layouts/partials/cdn-url.html +36 -0
- package/hugo-theme/layouts/partials/draft-notice.html +41 -0
- package/hugo-theme/layouts/partials/draft-state.html +26 -0
- package/hugo-theme/layouts/partials/footer.html +29 -0
- package/hugo-theme/layouts/partials/header.html +35 -0
- package/hugo-theme/layouts/partials/hero-banner.html +86 -0
- package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
- package/hugo-theme/layouts/partials/holdings.html +61 -0
- package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
- package/hugo-theme/layouts/partials/infobox/items.html +26 -0
- package/hugo-theme/layouts/partials/infobox/section.html +56 -0
- package/hugo-theme/layouts/partials/infobox/value.html +43 -0
- package/hugo-theme/layouts/partials/infobox.html +57 -0
- package/hugo-theme/layouts/partials/page-body.html +154 -0
- package/hugo-theme/layouts/partials/place-map.html +9 -0
- package/hugo-theme/layouts/partials/related.html +121 -0
- package/hugo-theme/layouts/partials/search.html +84 -0
- package/hugo-theme/layouts/partials/site-url.html +32 -0
- package/hugo-theme/layouts/partials/tags.html +30 -0
- package/hugo-theme/layouts/partials/toc.html +28 -0
- package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
- package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
- package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
- package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
- package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
- package/hugo-theme/static/css/sohl-icons.css +57 -0
- package/hugo-theme/static/css/style.css +2953 -0
- package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
- package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
- package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
- package/hugo-theme/theme.toml +11 -0
- package/manifest.mjs +30 -24
- package/package.json +2 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/item-fields.mjs +4 -3
- package/types/content-config.d.mts +48 -1
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/art-slots.d.mts +2 -1
- 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 +0 -17
- 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 +7 -0
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- 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/heading-attributes.d.mts +13 -8
- package/types/engine/helpers.d.mts +28 -10
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +31 -9
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +23 -2
- package/types/engine/note-dates.d.mts +64 -1
- 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 +12 -0
- package/types/engine/site-build.d.mts +14 -3
- package/types/engine/site-config.d.mts +9 -9
- package/types/engine/site-index.d.mts +54 -0
- package/types/engine/system-block.d.mts +26 -2
- package/types/hm3/actors.d.mts +18 -0
- package/types/manifest.d.mts +19 -23
- package/engine/content-captions.mjs +0 -230
- package/types/engine/content-captions.d.mts +0 -58
|
@@ -58,10 +58,12 @@
|
|
|
58
58
|
* agree: a figure is a block on all three, and nothing has to decide what a
|
|
59
59
|
* floated run of text inside a paragraph would mean.
|
|
60
60
|
*
|
|
61
|
-
* **
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* **Alt text renders as a caption on no surface.** A caption is drawn only for
|
|
62
|
+
* an image inside a `:::figure` fence that carries an authored caption, and the
|
|
63
|
+
* text drawn is always that caption. An image with no fence, or a fence with no
|
|
64
|
+
* caption, draws no line under the picture anywhere. The HTML surfaces still
|
|
65
|
+
* carry the alt text as the `img` element's `alt` attribute, for the reader a
|
|
66
|
+
* screen reader serves.
|
|
65
67
|
*
|
|
66
68
|
* ## Where the address resolves
|
|
67
69
|
*
|
|
@@ -79,7 +81,9 @@ import path from "node:path";
|
|
|
79
81
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
80
82
|
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
81
83
|
import { positionInBody } from "./diagnostics.mjs";
|
|
84
|
+
import { FOOTNOTE_REFERENCE } from "./content-footnotes.mjs";
|
|
82
85
|
import { foundryAddressProblem, pathnameProblem, servesFoundry } from "./pathnames.mjs";
|
|
86
|
+
import { ASSET_ROLES } from "./asset-index.mjs";
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
89
|
* The width classes an image may carry, and what each means to a renderer.
|
|
@@ -273,13 +277,27 @@ export function parseImageDirective(raw) {
|
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
/**
|
|
276
|
-
* The
|
|
280
|
+
* The class a picture's role names, or `""` for no role or one outside the
|
|
281
|
+
* closed set — the same silent-degrade rule every other unrecognised value
|
|
282
|
+
* here gets, since a role this far downstream has already been validated or
|
|
283
|
+
* dropped by the asset record that carries it.
|
|
277
284
|
*
|
|
278
|
-
* @param {
|
|
285
|
+
* @param {string} [role] - The role, from the resolved asset record.
|
|
286
|
+
* @returns {string} The class, or `""`.
|
|
287
|
+
*/
|
|
288
|
+
export function roleClass(role) {
|
|
289
|
+
return role && ASSET_ROLES.includes(role) ? `note-image-role-${role}` : "";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The classes a figure carries, from a parsed directive and its asset's role.
|
|
294
|
+
*
|
|
295
|
+
* @param {{classes?: string[], size?: string, float?: string, role?: string}} [directive] -
|
|
296
|
+
* As parsed, plus the role the resolved asset record carries, if any.
|
|
279
297
|
* @returns {string} A space-separated class list, always naming
|
|
280
298
|
* {@link IMAGE_FIGURE_CLASS} first.
|
|
281
299
|
*/
|
|
282
|
-
export function figureClasses({ classes = [], size = "auto", float = "" } = {}) {
|
|
300
|
+
export function figureClasses({ classes = [], size = "auto", float = "", role = "" } = {}) {
|
|
283
301
|
const names = [IMAGE_FIGURE_CLASS];
|
|
284
302
|
for (const name of classes) {
|
|
285
303
|
const spec = IMAGE_CLASSES[/** @type {keyof typeof IMAGE_CLASSES} */ (name)];
|
|
@@ -288,6 +306,8 @@ export function figureClasses({ classes = [], size = "auto", float = "" } = {})
|
|
|
288
306
|
if (size !== "auto" && IMAGE_SIZES.includes(size)) names.push(`note-image-size-${size}`);
|
|
289
307
|
const position = IMAGE_FLOATS[/** @type {keyof typeof IMAGE_FLOATS} */ (float)];
|
|
290
308
|
if (position) names.push(position.class);
|
|
309
|
+
const roleName = roleClass(role);
|
|
310
|
+
if (roleName) names.push(roleName);
|
|
291
311
|
return names.join(" ");
|
|
292
312
|
}
|
|
293
313
|
|
|
@@ -313,30 +333,85 @@ export function escapeHtml(text) {
|
|
|
313
333
|
* function, so the two cannot drift into styling the same directive through
|
|
314
334
|
* different class names.
|
|
315
335
|
*
|
|
336
|
+
* **No `<figcaption>` is emitted here.** A `:::figure` fence draws its own
|
|
337
|
+
* label and authored caption around whatever this returns; the alt text stays
|
|
338
|
+
* on the `img` element alone.
|
|
339
|
+
*
|
|
340
|
+
* **The role class and the `width`/`height` attributes are what a stylesheet
|
|
341
|
+
* elsewhere keys a role's size on.** This module states none — the measure a
|
|
342
|
+
* `portrait` fills on a page is a Hugo theme's or a Foundry system's own
|
|
343
|
+
* decision — and emits only what the resolved asset record states: a role
|
|
344
|
+
* class when the picture declares one, and the attributes whenever the
|
|
345
|
+
* picture resolved to an asset record at all, blank for a vector. The
|
|
346
|
+
* attributes reserve the picture's own layout space before it loads, which is
|
|
347
|
+
* what lets a page avoid reflowing as pictures load in.
|
|
348
|
+
*
|
|
316
349
|
* @param {object} image - The image.
|
|
317
350
|
* @param {string} image.src - The address, resolved for the surface.
|
|
318
|
-
* @param {string} [image.alt] - The alt text,
|
|
351
|
+
* @param {string} [image.alt] - The alt text, for a reader who cannot see the
|
|
352
|
+
* picture.
|
|
319
353
|
* @param {string[]} [image.classes] - Width classes, from the directive.
|
|
320
354
|
* @param {string} [image.size] - Named display size, from the directive.
|
|
321
355
|
* @param {string} [image.float] - The float position, from the directive.
|
|
356
|
+
* @param {string} [image.role] - The picture's declared role, from the
|
|
357
|
+
* resolved asset record.
|
|
358
|
+
* @param {number|""} [image.width] - The file's own pixel width, from the
|
|
359
|
+
* resolved asset record; `""` for a vector. Omitted entirely when the
|
|
360
|
+
* address resolved to no asset record at all.
|
|
361
|
+
* @param {number|""} [image.height] - As `width`.
|
|
322
362
|
* @returns {string} The figure, as one HTML block.
|
|
323
363
|
*/
|
|
324
|
-
export function imageFigureHtml({
|
|
325
|
-
|
|
364
|
+
export function imageFigureHtml({
|
|
365
|
+
src,
|
|
366
|
+
alt = "",
|
|
367
|
+
classes = [],
|
|
368
|
+
size = "auto",
|
|
369
|
+
float = "",
|
|
370
|
+
role = "",
|
|
371
|
+
width,
|
|
372
|
+
height,
|
|
373
|
+
}) {
|
|
374
|
+
const dims =
|
|
375
|
+
width !== undefined && height !== undefined ?
|
|
376
|
+
` width="${escapeHtml(String(width))}" height="${escapeHtml(String(height))}"`
|
|
377
|
+
: "";
|
|
326
378
|
return (
|
|
327
|
-
`<figure class="${figureClasses({ classes, size, float })}">\n` +
|
|
328
|
-
`<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}"
|
|
379
|
+
`<figure class="${figureClasses({ classes, size, float, role })}">\n` +
|
|
380
|
+
`<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}"${dims}>\n` +
|
|
329
381
|
`</figure>`
|
|
330
382
|
);
|
|
331
383
|
}
|
|
332
384
|
|
|
385
|
+
/**
|
|
386
|
+
* A body extension's own delimiter — a `:::` opener or closer, or a figure's
|
|
387
|
+
* `///`.
|
|
388
|
+
*
|
|
389
|
+
* It bounds a paragraph exactly as a blank line does: the line belongs to the
|
|
390
|
+
* construct rather than to the prose, and the pass that owns the construct
|
|
391
|
+
* hands its body on by itself. So a picture written tight against one is a
|
|
392
|
+
* block of its own, which is how `:::figure` is written.
|
|
393
|
+
*/
|
|
394
|
+
const DELIMITER = /^(?::{3,}[^\n]*|\/{3,}[ \t]*)$/;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* A footnote reference immediately trailing a match, anchored so it only
|
|
398
|
+
* consumes one sitting right against the end of it.
|
|
399
|
+
*/
|
|
400
|
+
const TRAILING_FOOTNOTE_REFERENCE = new RegExp(`^${FOOTNOTE_REFERENCE.source}`);
|
|
401
|
+
|
|
333
402
|
/**
|
|
334
403
|
* Whether a match sits alone in its own paragraph.
|
|
335
404
|
*
|
|
336
|
-
* "Alone" is the whole of a block: nothing else on its line, and a blank line
|
|
337
|
-
* or the end of the body either side of it. A
|
|
338
|
-
* disqualifies it for the same reason a word does —
|
|
339
|
-
* holds something the figure would have to be
|
|
405
|
+
* "Alone" is the whole of a block: nothing else on its line, and a blank line,
|
|
406
|
+
* a body-extension delimiter or the end of the body either side of it. A
|
|
407
|
+
* leading `>` or list marker disqualifies it for the same reason a word does —
|
|
408
|
+
* the paragraph it belongs to holds something the figure would have to be
|
|
409
|
+
* lifted out of.
|
|
410
|
+
*
|
|
411
|
+
* **A footnote reference trailing the match is not "other text."** `[^note]`
|
|
412
|
+
* annotates the picture rather than sitting beside it in prose, the same
|
|
413
|
+
* standing its own directive already has, so `[^note]` is
|
|
414
|
+
* still alone.
|
|
340
415
|
*
|
|
341
416
|
* @param {string} text - The body the match indexes into.
|
|
342
417
|
* @param {number} start - Where the match begins.
|
|
@@ -350,12 +425,18 @@ export function standsAlone(text, start, end) {
|
|
|
350
425
|
// makes it an indented code block, which `matchAllOutsideCode` skips.
|
|
351
426
|
if (!/^[ \t]*$/.test(src.slice(lineStart, start))) return false;
|
|
352
427
|
const lineEnd = src.indexOf("\n", end);
|
|
353
|
-
|
|
428
|
+
const afterMatch = src.slice(end, lineEnd === -1 ? src.length : lineEnd);
|
|
429
|
+
const reference = TRAILING_FOOTNOTE_REFERENCE.exec(afterMatch);
|
|
430
|
+
if (!/^[ \t]*$/.test(reference ? afterMatch.slice(reference[0].length) : afterMatch))
|
|
431
|
+
return false;
|
|
354
432
|
|
|
355
433
|
const before = src.slice(0, lineStart);
|
|
356
|
-
|
|
434
|
+
const previous = before.replace(/\n$/, "").split("\n").pop() ?? "";
|
|
435
|
+
if (before.trim() && !/\n[ \t]*\n[ \t]*$/.test(before) && !DELIMITER.test(previous))
|
|
436
|
+
return false;
|
|
357
437
|
const after = lineEnd === -1 ? "" : src.slice(lineEnd);
|
|
358
|
-
|
|
438
|
+
const next = after.replace(/^\n/, "").split("\n")[0] ?? "";
|
|
439
|
+
if (after.trim() && !/^\n[ \t]*\n/.test(after) && !DELIMITER.test(next)) return false;
|
|
359
440
|
return true;
|
|
360
441
|
}
|
|
361
442
|
|
|
@@ -414,6 +495,9 @@ export function imageSourcesIn(body) {
|
|
|
414
495
|
* @param {object} [opts]
|
|
415
496
|
* @param {number} [opts.bodyLine=1] - The 1-based file line the body starts on.
|
|
416
497
|
* @param {number} [opts.bodyColumn=1] - The 1-based file column it starts at.
|
|
498
|
+
* @param {Array<{line: number, generated: boolean}>} [opts.lineMap] - From
|
|
499
|
+
* table expansion, mapping each line of `body` back to the line an author
|
|
500
|
+
* wrote, for a caller handing in a body whose tables already expanded.
|
|
417
501
|
* @param {object} [opts.config] - The resolved build configuration. Supplied,
|
|
418
502
|
* an address is also held to the one surface a pathname can be dead on
|
|
419
503
|
* without any other pass noticing — see the Foundry address below. Omitted,
|
|
@@ -423,7 +507,7 @@ export function imageSourcesIn(body) {
|
|
|
423
507
|
* severity: "error", message: string}>} One finding per defect, in source
|
|
424
508
|
* order.
|
|
425
509
|
*/
|
|
426
|
-
export function checkImages(body, file, { bodyLine = 1, bodyColumn = 1, config } = {}) {
|
|
510
|
+
export function checkImages(body, file, { bodyLine = 1, bodyColumn = 1, lineMap, config } = {}) {
|
|
427
511
|
const text = String(body ?? "");
|
|
428
512
|
if (!text) return [];
|
|
429
513
|
|
|
@@ -434,7 +518,7 @@ export function checkImages(body, file, { bodyLine = 1, bodyColumn = 1, config }
|
|
|
434
518
|
* @param {string} message - What is wrong.
|
|
435
519
|
*/
|
|
436
520
|
const report = (offset, message) => {
|
|
437
|
-
const { line, column } = positionInBody(text, offset, { bodyLine, bodyColumn });
|
|
521
|
+
const { line, column } = positionInBody(text, offset, { bodyLine, bodyColumn, lineMap });
|
|
438
522
|
findings.push({ file, line, column, severity: /** @type {"error"} */ ("error"), message });
|
|
439
523
|
};
|
|
440
524
|
|
|
@@ -454,7 +538,7 @@ export function checkImages(body, file, { bodyLine = 1, bodyColumn = 1, config }
|
|
|
454
538
|
report(
|
|
455
539
|
image.index,
|
|
456
540
|
`\`"${image.title}"\` is a title on an image, and no surface here draws ` +
|
|
457
|
-
"one —
|
|
541
|
+
"one — a caption is written in a `:::figure` fence's /// section",
|
|
458
542
|
);
|
|
459
543
|
}
|
|
460
544
|
if (!image.block) {
|
|
@@ -587,9 +671,13 @@ export function lintContentImages(contentBase, { skipDirectories = [], config }
|
|
|
587
671
|
* @param {(src: string) => string} [resolveSrc] - Translates an authored
|
|
588
672
|
* pathname into the address this surface serves. The default is the identity,
|
|
589
673
|
* for a caller rendering the format rather than publishing it.
|
|
674
|
+
* @param {(src: string) => {type?: string, role?: string, width?: number|"", height?: number|""}|undefined}
|
|
675
|
+
* [lookupAsset] - What the resolved asset record says about the address, as
|
|
676
|
+
* authored — before `resolveSrc` translates it. The default answers nothing,
|
|
677
|
+
* which is what an address resolving to no asset record gets anyway.
|
|
590
678
|
* @returns {string} The same body, with each block image as a `<figure>`.
|
|
591
679
|
*/
|
|
592
|
-
export function renderImageFigures(body, resolveSrc = (src) => src) {
|
|
680
|
+
export function renderImageFigures(body, resolveSrc = (src) => src, lookupAsset = () => undefined) {
|
|
593
681
|
const text = String(body ?? "");
|
|
594
682
|
let out = "";
|
|
595
683
|
let last = 0;
|
|
@@ -598,12 +686,16 @@ export function renderImageFigures(body, resolveSrc = (src) => src) {
|
|
|
598
686
|
const { classes, size, float, problems } = parseImageDirective(image.directive);
|
|
599
687
|
if (problems.length) continue;
|
|
600
688
|
out += text.slice(last, image.index);
|
|
689
|
+
const asset = lookupAsset(image.src);
|
|
601
690
|
out += imageFigureHtml({
|
|
602
691
|
src: resolveSrc(image.src),
|
|
603
692
|
alt: image.alt,
|
|
604
693
|
classes,
|
|
605
694
|
size,
|
|
606
695
|
float,
|
|
696
|
+
role: asset?.role,
|
|
697
|
+
width: asset?.width,
|
|
698
|
+
height: asset?.height,
|
|
607
699
|
});
|
|
608
700
|
last = image.index + image.length;
|
|
609
701
|
}
|
|
@@ -632,9 +724,13 @@ export function renderImageFigures(body, resolveSrc = (src) => src) {
|
|
|
632
724
|
* address into the one this surface serves. Foundry is handed the path inside
|
|
633
725
|
* the install; a renderer that resolves the address itself — the book stages
|
|
634
726
|
* its own copy — passes nothing and gets the address as authored.
|
|
727
|
+
* @param {(src: string) => {type?: string, role?: string, width?: number|"", height?: number|""}|undefined}
|
|
728
|
+
* [lookupAsset] - What the resolved asset record says about the address, as
|
|
729
|
+
* authored — before `resolveSrc` runs. The default answers nothing, which is
|
|
730
|
+
* what an address resolving to no asset record gets anyway.
|
|
635
731
|
* @returns {(md: object) => void} A markdown-it plugin.
|
|
636
732
|
*/
|
|
637
|
-
export function imagePlugin(resolveSrc = (src) => src) {
|
|
733
|
+
export function imagePlugin(resolveSrc = (src) => src, lookupAsset = () => undefined) {
|
|
638
734
|
return (md) => {
|
|
639
735
|
/** @type {any} */ (md).core.ruler.push("heroiclands_image", (state) => {
|
|
640
736
|
attachImageDirectives(state.tokens);
|
|
@@ -649,12 +745,17 @@ export function imagePlugin(resolveSrc = (src) => src) {
|
|
|
649
745
|
) => {
|
|
650
746
|
const token = tokens[idx];
|
|
651
747
|
if (!token.meta?.block) return base(tokens, idx, options, env, self);
|
|
748
|
+
const src = token.attrGet("src") ?? "";
|
|
749
|
+
const asset = lookupAsset(src);
|
|
652
750
|
return `${imageFigureHtml({
|
|
653
|
-
src: resolveSrc(
|
|
751
|
+
src: resolveSrc(src),
|
|
654
752
|
alt: token.content ?? "",
|
|
655
753
|
classes: token.meta.classes,
|
|
656
754
|
size: token.meta.size,
|
|
657
755
|
float: token.meta.float,
|
|
756
|
+
role: asset?.role,
|
|
757
|
+
width: asset?.width,
|
|
758
|
+
height: asset?.height,
|
|
658
759
|
})}\n`;
|
|
659
760
|
};
|
|
660
761
|
};
|
package/engine/content-index.mjs
CHANGED
|
@@ -104,6 +104,8 @@ import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
|
104
104
|
import { addressSlug, canonicalKey } from "./content-address.mjs";
|
|
105
105
|
import { ownDocumentSystem } from "./address.mjs";
|
|
106
106
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
107
|
+
import { assetAddressIndex } from "./art-fields.mjs";
|
|
108
|
+
import { embedRole } from "./content-embeds.mjs";
|
|
107
109
|
// One reader for a note's anchors, shared with the link checker and with the
|
|
108
110
|
// builds that emit a link. Re-exported because this is where callers
|
|
109
111
|
// have always addressed it.
|
|
@@ -443,6 +445,10 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage) {
|
|
|
443
445
|
* entries are derived against.
|
|
444
446
|
* @param {object} [options.addressContext] - Address resolution context.
|
|
445
447
|
* @param {object} [options.dateContext] - Calendar conversion context.
|
|
448
|
+
* @param {(address: string) => string|undefined} [options.resolveRole] - From
|
|
449
|
+
* a picture's address to the role its asset declares, so a figure anchor's
|
|
450
|
+
* `name` reads `Map 1` rather than `Figure 1` where it is due — see
|
|
451
|
+
* {@link module:engine/content-figures.scanFigures}.
|
|
446
452
|
* @returns {Record<string, any>} The record, keys sorted at every depth. A
|
|
447
453
|
* **stub** — a note with an empty body, on a type an empty body suppresses —
|
|
448
454
|
* carries `address` and `anchors` as `null`: it publishes no page, so it
|
|
@@ -464,6 +470,7 @@ export function buildIndexRecord({
|
|
|
464
470
|
manifest,
|
|
465
471
|
addressContext,
|
|
466
472
|
dateContext,
|
|
473
|
+
resolveRole,
|
|
467
474
|
}) {
|
|
468
475
|
assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage);
|
|
469
476
|
|
|
@@ -514,7 +521,7 @@ export function buildIndexRecord({
|
|
|
514
521
|
// there rather than search for the heading.
|
|
515
522
|
anchors:
|
|
516
523
|
stub ? null : (
|
|
517
|
-
collectAnchors(body, bodyLine).map((a) => ({
|
|
524
|
+
collectAnchors(body, bodyLine, resolveRole).map((a) => ({
|
|
518
525
|
...a,
|
|
519
526
|
link: address ? `${address.slug}#${a.slug}` : null,
|
|
520
527
|
}))
|
|
@@ -615,6 +622,7 @@ export function indexRecordsForNote({
|
|
|
615
622
|
manifest,
|
|
616
623
|
addressContext,
|
|
617
624
|
dateContext,
|
|
625
|
+
resolveRole,
|
|
618
626
|
}) {
|
|
619
627
|
resolveNoteId(frontmatter, { pkg: contentPackage });
|
|
620
628
|
const record = buildIndexRecord({
|
|
@@ -627,6 +635,7 @@ export function indexRecordsForNote({
|
|
|
627
635
|
manifest,
|
|
628
636
|
addressContext,
|
|
629
637
|
dateContext,
|
|
638
|
+
resolveRole,
|
|
630
639
|
});
|
|
631
640
|
const records = [decodeIndexAddresses(record, addressContext ?? { package: contentPackage })];
|
|
632
641
|
const address = noteAddress(frontmatter, contentPackage);
|
|
@@ -726,7 +735,24 @@ export function collectContentIndex(
|
|
|
726
735
|
// this same walk states one — the whole tree is already in memory, so no
|
|
727
736
|
// second read is needed to answer a question about all of it.
|
|
728
737
|
const present = presentAmongFrontmatters(parsedNotes.map((n) => n.frontmatter));
|
|
729
|
-
|
|
738
|
+
// Carried on the same context `resolvedDateFields` reads: a recurring
|
|
739
|
+
// event's `next` is computed against this present exactly as a being's
|
|
740
|
+
// `age` already is, above.
|
|
741
|
+
const dates = {
|
|
742
|
+
...reckoningContext({ notes: parsedNotes.map((n) => n.frontmatter) }),
|
|
743
|
+
present,
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// Walked ahead of the notes, rather than after them as the records
|
|
747
|
+
// themselves are emitted: a note's anchors are collected below, and a
|
|
748
|
+
// figure fence's `map` counter needs this package's own asset roles
|
|
749
|
+
// before the first note is read, the same precedent the book's own
|
|
750
|
+
// cross-file numbering sets. A package with no `assetsBase` resolves no
|
|
751
|
+
// role, which is the one it would have resolved anyway.
|
|
752
|
+
const assetRecords =
|
|
753
|
+
assetsBase ? collectAssetRecords(assetsBase, { contentPackage, problems }) : [];
|
|
754
|
+
const assetIndex = assetAddressIndex(assetRecords, { config: { contentPackage } });
|
|
755
|
+
const resolveRole = (address) => embedRole(assetIndex, address);
|
|
730
756
|
|
|
731
757
|
for (const { frontmatter, body, bodyLine, absPath } of parsedNotes) {
|
|
732
758
|
const fm = frontmatter ?? {};
|
|
@@ -744,6 +770,7 @@ export function collectContentIndex(
|
|
|
744
770
|
manifest,
|
|
745
771
|
addressContext,
|
|
746
772
|
dateContext: dates,
|
|
773
|
+
resolveRole,
|
|
747
774
|
}),
|
|
748
775
|
);
|
|
749
776
|
} catch (err) {
|
|
@@ -776,14 +803,13 @@ export function collectContentIndex(
|
|
|
776
803
|
}
|
|
777
804
|
}
|
|
778
805
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
806
|
+
// Sorted at every depth like a note's record, and for the same reason: the
|
|
807
|
+
// declaration order of the `asset` fields is a fact about the emitter, not
|
|
808
|
+
// about the content, and the artifact is meant to be byte-identical across
|
|
809
|
+
// two runs over an unchanged tree. Collected above, ahead of the notes —
|
|
810
|
+
// see `resolveRole` — rather than walked a second time here.
|
|
811
|
+
for (const record of assetRecords) {
|
|
812
|
+
records.push(/** @type {Record<string, any>} */ (sortKeysDeep(record)));
|
|
787
813
|
}
|
|
788
814
|
|
|
789
815
|
// Source path, then the canonical address, then the note id. The walk
|
|
@@ -123,16 +123,41 @@ function fieldTable(fields) {
|
|
|
123
123
|
return padTable(rows);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The one line explaining why a single field's shared top-level fallback is
|
|
128
|
+
* off.
|
|
129
|
+
*
|
|
130
|
+
* **The field's reason stands on its own** — a complete sentence, or more
|
|
131
|
+
* than one — rather than completing a clause the frame starts. A field that
|
|
132
|
+
* collides with a top-level spelling meaning something else can say so
|
|
133
|
+
* ("`weight` is the item's own, independent of the note's."); a field whose
|
|
134
|
+
* top-level spelling is refused outright by the closed vocabulary can say
|
|
135
|
+
* that instead, honestly, which "there it means …" cannot: a refused key
|
|
136
|
+
* carries no meaning to report.
|
|
137
|
+
*
|
|
138
|
+
* Exported so a guard can walk every field declaring `topLevelMeans` across
|
|
139
|
+
* every registry and check that each one composes into a grammatical
|
|
140
|
+
* sentence, without reimplementing the frame it is checking.
|
|
141
|
+
*
|
|
142
|
+
* @param {import("./field-spec.mjs").FieldSpec} field - A field declaring
|
|
143
|
+
* `topLevelMeans`.
|
|
144
|
+
* @returns {string} The composed markdown line.
|
|
145
|
+
*/
|
|
146
|
+
export function topLevelExemptionLine(field) {
|
|
147
|
+
return `**\`${field.name}\` is not read from the note's top level.** ${field.topLevelMeans}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
/**
|
|
127
151
|
* The fields of one type that are **not** filled from the note's top level.
|
|
128
152
|
*
|
|
129
153
|
* A field ordinarily falls back to the top-level property spelled like its
|
|
130
154
|
* name, so an author who writes `weight: 3` at the top of a note reasonably
|
|
131
|
-
* expects it to reach the document. Where that spelling
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
155
|
+
* expects it to reach the document. Where that spelling is wrong at the note
|
|
156
|
+
* level — it means something else there, or the closed top-level vocabulary
|
|
157
|
+
* refuses it outright — the fallback is off, and an author has no way to tell
|
|
158
|
+
* from the table — the field is there, the value is written, and the document
|
|
159
|
+
* ships the default. So the reason each such field declares is rendered
|
|
160
|
+
* beside its table rather than left in the source.
|
|
136
161
|
*
|
|
137
162
|
* Below the table, not inside it: the reason is a sentence or two, and
|
|
138
163
|
* {@link padTable} pads every column to its widest cell, so a cell holding it
|
|
@@ -144,11 +169,7 @@ function fieldTable(fields) {
|
|
|
144
169
|
function sharedExemptions(fields) {
|
|
145
170
|
const exempt = authoredFields(fields).filter((field) => field.topLevelMeans);
|
|
146
171
|
if (!exempt.length) return [];
|
|
147
|
-
return exempt.flatMap((field) => [
|
|
148
|
-
`**\`${field.name}\` is not read from the note's top level.** There it means ` +
|
|
149
|
-
`${field.topLevelMeans}`,
|
|
150
|
-
"",
|
|
151
|
-
]);
|
|
172
|
+
return exempt.flatMap((field) => [topLevelExemptionLine(field), ""]);
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
/**
|
package/engine/field-spec.mjs
CHANGED
|
@@ -84,19 +84,21 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
|
|
|
84
84
|
* retiring one — so a field can move into `data:` while the corpus catches
|
|
85
85
|
* up, instead of on a flag day across four repositories. A declaration that
|
|
86
86
|
* names one is mid-sweep by construction; see {@link readsLegacyKey}.
|
|
87
|
-
* @property {string} [topLevelMeans] - **
|
|
88
|
-
* name
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* @property {string} [topLevelMeans] - **Why the note's top-level key of this
|
|
88
|
+
* name is not this field's source** — declared wherever that key's spelling
|
|
89
|
+
* means something else at the note level, or the closed top-level
|
|
90
|
+
* vocabulary refuses it outright, and stating either removes the shared
|
|
91
|
+
* top-level position from this field's resolution order.
|
|
91
92
|
*
|
|
92
93
|
* A field's `name` doubles as its identity and as the shared property it
|
|
93
94
|
* draws from, which is right wherever the two levels state the same quantity
|
|
94
95
|
* — `data.weight` is the weight, whoever reads it. It is wrong wherever a
|
|
95
|
-
* spelling collides across the two vocabularies
|
|
96
|
+
* spelling collides across the two vocabularies, or names a key the closed
|
|
97
|
+
* top-level vocabulary does not admit at all. An `affiliation` item's
|
|
96
98
|
* `system.title` is the style of address an office carries; a note's
|
|
97
|
-
* top-level `title` is
|
|
98
|
-
*
|
|
99
|
-
*
|
|
99
|
+
* top-level `title` is refused outright. Nothing relates them, and before
|
|
100
|
+
* this key one silently fed the other, stringifying an authored `title:
|
|
101
|
+
* null` into fifteen documents.
|
|
100
102
|
*
|
|
101
103
|
* **The value is the reason**, not a flag with a comment beside it. A boolean
|
|
102
104
|
* would record the decision and lose the case for it, and the next person
|