@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
|
@@ -104,6 +104,9 @@ import { authoredFields } from "./field-spec.mjs";
|
|
|
104
104
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
105
105
|
import { RETIRED_FIELD_ALIASES } from "./retired-fields.mjs";
|
|
106
106
|
import { authoredKey } from "./system-block.mjs";
|
|
107
|
+
// The top-level vocabulary, read rather than restated — a second list of the
|
|
108
|
+
// keys a note's own identity uses is one more thing to drift.
|
|
109
|
+
import { NOTE_TOP_LEVEL_KEY_SET } from "./note-vocabulary.mjs";
|
|
107
110
|
|
|
108
111
|
/**
|
|
109
112
|
* Every field path any subtype of a published schema declares.
|
|
@@ -273,18 +276,18 @@ function nearest(key, candidates) {
|
|
|
273
276
|
/**
|
|
274
277
|
* Top-level keys the format names as the note's own, whatever its type.
|
|
275
278
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
279
|
+
* The top-level vocabulary itself, read rather than restated: this set exists
|
|
280
|
+
* to keep a note's own identity fields from being mistaken for a misplaced
|
|
281
|
+
* `data` property where the two share a name, and the keys that are a note's
|
|
282
|
+
* own are exactly the ones the region declares. `type` and `subType` are the
|
|
283
|
+
* pair that matters — several mapping tables name `subType` as a shared source,
|
|
284
|
+
* and it is authored at top level by design. A key the region does *not*
|
|
285
|
+
* declare, such as `id` beside `data.id`, is reported as the misplaced `data`
|
|
286
|
+
* property it is.
|
|
282
287
|
*
|
|
283
288
|
* @type {ReadonlySet<string>}
|
|
284
289
|
*/
|
|
285
|
-
export const NOTE_LEVEL_KEYS =
|
|
286
|
-
new Set(["id", "type", "subType", "shortcode", "description", "tags", "name"]),
|
|
287
|
-
);
|
|
290
|
+
export const NOTE_LEVEL_KEYS = NOTE_TOP_LEVEL_KEY_SET;
|
|
288
291
|
|
|
289
292
|
/** Whether a value is a plain object a block could be written as. */
|
|
290
293
|
function isBlock(value) {
|
|
@@ -379,9 +382,9 @@ export function measureNote(note, format, { severity = "warning" } = {}) {
|
|
|
379
382
|
}
|
|
380
383
|
|
|
381
384
|
// The two regions a shared source is written in today, and neither is where
|
|
382
|
-
// the format puts it.
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
+
// the format puts it. A key the type's own table declares is not merely an
|
|
386
|
+
// unrecognised one — it is one whose home the format states, so the finding
|
|
387
|
+
// names that home rather than the vocabulary.
|
|
385
388
|
for (const key of spec.dataKeys) {
|
|
386
389
|
if (NOTE_LEVEL_KEYS.has(key)) continue;
|
|
387
390
|
if (Object.hasOwn(fm, key)) {
|
|
@@ -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
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
* over 15 types, from 9 on a `macro` to 72 on a `being`, and adding a field to
|
|
33
33
|
* one type is ordinary authoring. Any format that fixes a column set would turn
|
|
34
34
|
* that authoring into a schema migration, so nothing here selects, flattens, or
|
|
35
|
-
* renames — a reader addresses `sohl.body.weight.base` because that is
|
|
36
|
-
* note says, which a SQL query can address directly.
|
|
35
|
+
* renames — a reader addresses `sohl.system.body.weight.base` because that is
|
|
36
|
+
* what the note says, which a SQL query can address directly.
|
|
37
37
|
*
|
|
38
38
|
* **JSON Lines rather than a database.** The artifact has to survive its build
|
|
39
39
|
* and be usable by anything — a person with `jq`, an editor, a CI check,
|
|
@@ -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
|
package/engine/content-links.mjs
CHANGED
|
@@ -302,24 +302,25 @@ export function buildLinkIndex(
|
|
|
302
302
|
};
|
|
303
303
|
|
|
304
304
|
/**
|
|
305
|
-
* One note's body with its SQL tables expanded.
|
|
305
|
+
* One note's body with its SQL tables and page lists expanded.
|
|
306
306
|
*
|
|
307
307
|
* The body every body-level check reads, so a link and an embed in one note
|
|
308
|
-
* are found in the same text — a generated table is as free to
|
|
309
|
-
* as prose is.
|
|
308
|
+
* are found in the same text — a generated table or list is as free to
|
|
309
|
+
* carry either as prose is.
|
|
310
310
|
*
|
|
311
311
|
* @param {object} note - A note from this index.
|
|
312
312
|
* @returns {string} The markdown.
|
|
313
313
|
*/
|
|
314
314
|
function expandedBody(note) {
|
|
315
315
|
const body = note.body;
|
|
316
|
-
if (!/^[ \t]*(?:`{3,}|~{3,})[ \t]*sql\b/im.test(body)) return body;
|
|
316
|
+
if (!/^[ \t]*(?:`{3,}|~{3,})[ \t]*(?:sql|pagelist)\b/im.test(body)) return body;
|
|
317
317
|
return expandContentTables(body, {
|
|
318
318
|
source: note.file,
|
|
319
|
-
// A
|
|
320
|
-
//
|
|
319
|
+
// A generated link is checked like an authored one, so both
|
|
320
|
+
// directives are prepared ahead of this walk — see
|
|
321
321
|
// {@link module:engine/sql-tables.prepareTreeSqlTables}.
|
|
322
322
|
sqlTables: sqlTables?.get(note.file),
|
|
323
|
+
pageLists: sqlTables?.pageLists?.get(note.file),
|
|
323
324
|
}).markdown;
|
|
324
325
|
}
|
|
325
326
|
|
|
@@ -954,6 +955,13 @@ export function auditLinks(index) {
|
|
|
954
955
|
deadAnchors.push({
|
|
955
956
|
note,
|
|
956
957
|
link: `${rawTarget}#${anchor}`,
|
|
958
|
+
// The two the shared `unknown-anchor` wording reads, in the
|
|
959
|
+
// same spelling the build's own pass gives them: the
|
|
960
|
+
// address alone, and the slug the heading would have had to
|
|
961
|
+
// declare. Carrying them is what lets one sentence serve
|
|
962
|
+
// the checker and the build.
|
|
963
|
+
target: rawTarget,
|
|
964
|
+
anchor: slugify(anchor),
|
|
957
965
|
dest,
|
|
958
966
|
text,
|
|
959
967
|
occurrence,
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
2
|
|
|
3
3
|
import { FENCE_LINE } from "./code-fences.mjs";
|
|
4
|
+
import { PAGE_LIST_LANGUAGE } from "./page-lists.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Replace prepared SQL fences with Markdown
|
|
7
|
-
*
|
|
7
|
+
* Replace prepared SQL fences and page lists with Markdown while retaining
|
|
8
|
+
* source lines.
|
|
9
|
+
*
|
|
10
|
+
* Both directives read the content index, so both are answered before this
|
|
11
|
+
* pass runs — query execution in the asynchronous SQL preparation pass, page
|
|
12
|
+
* lists in {@link module:engine/page-lists.preparePageLists} — and each is
|
|
13
|
+
* looked up by the ordinal of its own kind of fence within the body.
|
|
14
|
+
*
|
|
8
15
|
* @param {string} markdown - The authored body.
|
|
9
|
-
* @param {{source?: string, sqlTables?: object[]}} [context] -
|
|
16
|
+
* @param {{source?: string, sqlTables?: object[], pageLists?: object[]}} [context] -
|
|
17
|
+
* Prepared results.
|
|
10
18
|
* @returns {{markdown: string, errors: object[], warnings: object[], lineMap: object[]}}
|
|
11
19
|
*/
|
|
12
|
-
export function expandContentTables(markdown, { source = "", sqlTables } = {}) {
|
|
20
|
+
export function expandContentTables(markdown, { source = "", sqlTables, pageLists } = {}) {
|
|
13
21
|
const lines = String(markdown ?? "").split("\n");
|
|
14
22
|
const out = [];
|
|
15
23
|
const lineMap = [];
|
|
16
24
|
const errors = [];
|
|
17
25
|
const warnings = [];
|
|
18
26
|
let sqlOrdinal = 0;
|
|
27
|
+
let pageListOrdinal = 0;
|
|
19
28
|
const emit = (value, line, generated = false) => {
|
|
20
29
|
out.push(value);
|
|
21
30
|
lineMap.push({ line, generated });
|
|
@@ -41,6 +50,38 @@ export function expandContentTables(markdown, { source = "", sqlTables } = {}) {
|
|
|
41
50
|
column: indent.length + 1,
|
|
42
51
|
});
|
|
43
52
|
}
|
|
53
|
+
if (language === PAGE_LIST_LANGUAGE) {
|
|
54
|
+
// The ordinal is consumed whether or not the fence was closed, so
|
|
55
|
+
// the two readings agree: `findPageListBlocks` records an unclosed
|
|
56
|
+
// directive as a directive with a problem rather than skipping it.
|
|
57
|
+
const prepared = pageLists?.[pageListOrdinal++];
|
|
58
|
+
const failure =
|
|
59
|
+
!prepared ? "page list was not prepared"
|
|
60
|
+
: prepared.reason ? prepared.reason
|
|
61
|
+
: prepared.pages === 0 && !prepared.allowEmpty ? prepared.empty
|
|
62
|
+
: undefined;
|
|
63
|
+
if (failure) {
|
|
64
|
+
errors.push({
|
|
65
|
+
source,
|
|
66
|
+
directive: block.join("\n"),
|
|
67
|
+
reason: failure,
|
|
68
|
+
line: i,
|
|
69
|
+
column: indent.length + 1,
|
|
70
|
+
});
|
|
71
|
+
block.forEach((value, offset) => emit(value, i + offset));
|
|
72
|
+
i = close;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// A list the directive permitted to be empty leaves nothing
|
|
76
|
+
// behind, rather than a blank line where a list would have been.
|
|
77
|
+
if (prepared.markdown) {
|
|
78
|
+
if (out.length && out.at(-1).trim()) emit("", i, true);
|
|
79
|
+
for (const row of prepared.markdown.split("\n")) emit(`${indent}${row}`, i, true);
|
|
80
|
+
if (close + 1 < lines.length && lines[close + 1].trim()) emit("", i, true);
|
|
81
|
+
}
|
|
82
|
+
i = close;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
44
85
|
if (language !== "sql" || close >= lines.length) {
|
|
45
86
|
block.forEach((value, offset) => emit(value, i + offset));
|
|
46
87
|
i = close;
|