@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
|
@@ -11,18 +11,35 @@ Choose a note's `type` for the subject it describes. Use `subType` when its type
|
|
|
11
11
|
|
|
12
12
|
The [first-note guide](../authoring/first-note.md) teaches the shape of a note. [Frontmatter](../authoring/frontmatter.md) explains shared and system-specific values. The [detailed reference](format-details.md) gives the full behavior and examples.
|
|
13
13
|
|
|
14
|
+
## Top-level keys
|
|
15
|
+
|
|
16
|
+
These are the only keys a note writes at its top level, in the order the formatter puts them. A key absent from this table is a lint error at its own line: the region is closed, so the key reaches no document and no page. Facts about the subject belong under `data`, and values for one game system belong inside that system's block.
|
|
17
|
+
|
|
18
|
+
| Key | Meaning |
|
|
19
|
+
| ------------- | ----------------------------------------------------------------------------------------- |
|
|
20
|
+
| `shortcode` | The note's own address segment, unique within its type. |
|
|
21
|
+
| `name` | The display names — a required `full`, and any `aliases`. |
|
|
22
|
+
| `type` | What the note is about, which decides its vocabulary and its document. |
|
|
23
|
+
| `subType` | The type's own genre, where it declares one. |
|
|
24
|
+
| `description` | The short page summary. |
|
|
25
|
+
| `tags` | Draft state, GM routing, and the descriptive labels a page list reads. |
|
|
26
|
+
| `data` | The facts about the subject itself, shared by every system. |
|
|
27
|
+
| `dnd5e` | What the `dnd5e` system makes of the subject — its document's mechanics, routing and art. |
|
|
28
|
+
| `hm3` | What the `hm3` system makes of the subject — its document's mechanics, routing and art. |
|
|
29
|
+
| `sohl` | What the `sohl` system makes of the subject — its document's mechanics, routing and art. |
|
|
30
|
+
|
|
14
31
|
## Shared `data` fields
|
|
15
32
|
|
|
16
33
|
These fields are accepted by every note type. A field's value can still be irrelevant to a particular output; the build reports that where it can.
|
|
17
34
|
|
|
18
|
-
| Field | Shape | Meaning
|
|
19
|
-
| ----------------- | ----------------------------------------------- |
|
|
20
|
-
| `data.id` | string | A fixed document identity when the derived identity is unsuitable.
|
|
21
|
-
| `data.pack` | string | The shared compendium route, with a system block overriding it.
|
|
22
|
-
| `data.packFolder` | an Address, or a map of Addresses keyed by pack | The shared compendium folder Address, with a system block overriding it.
|
|
23
|
-
| `data.harnworld` | map | HârnWorld source details shared by every system.
|
|
24
|
-
| `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`.
|
|
25
|
-
| `data.banner` | an Address | The page's hero image — an `image` address. Reaches no compiled document. |
|
|
35
|
+
| Field | Shape | Meaning |
|
|
36
|
+
| ----------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
37
|
+
| `data.id` | string | A fixed document identity when the derived identity is unsuitable. |
|
|
38
|
+
| `data.pack` | string | The shared compendium route, with a system block overriding it. |
|
|
39
|
+
| `data.packFolder` | an Address, or a map of Addresses keyed by pack | The shared compendium folder Address, with a system block overriding it. |
|
|
40
|
+
| `data.harnworld` | map | HârnWorld source details shared by every system. |
|
|
41
|
+
| `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
|
|
42
|
+
| `data.banner` | an Address | The page's hero image — an `image` address, cut to 1792×768. Reaches no compiled document. |
|
|
26
43
|
|
|
27
44
|
## Type-specific fields
|
|
28
45
|
|
|
@@ -41,9 +58,9 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
41
58
|
| `data.lore` | list of Addresses | Lore concerning this being — the law it lives under, the customs it is subject to, the traditions it was raised in. |
|
|
42
59
|
| `data.culture` | an Address | The being's primary culture, as a culture lore note. |
|
|
43
60
|
| `data.homes` | list of Addresses | Places the being calls home. |
|
|
44
|
-
| `data.affiliations` | a standing map keyed by Address, or a list of Addresses | Bodies the being belongs to, keyed by Address, each entry holding the standing it holds there — `rank`, a level on that body's own ladder, and `office`, a post that body names.
|
|
61
|
+
| `data.affiliations` | a standing map keyed by Address, or a list of Addresses | Bodies the being belongs to, keyed by Address, each entry holding the standing it holds there — `rank`, required, a level on that body's own ladder, and `office`, optional, a post that body names. |
|
|
45
62
|
| `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.) |
|
|
46
|
-
| `data.gender` | string | `male`, `female` or `other`.
|
|
63
|
+
| `data.gender` | string | One of `male`, `female`, `nonbinary`, `none` or `other`. `none` says the being has no gender; an absent field says its gender is unrecorded. |
|
|
47
64
|
| `data.species` | an Address | The being's species, as a lore note. |
|
|
48
65
|
| `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
|
|
49
66
|
| `data.calendar` | an Address | Calendar used to display this being's dates. |
|
|
@@ -52,11 +69,11 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
52
69
|
| `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
|
|
53
70
|
| `data.height` | metres or feet and inches | Height in metres or feet and inches. |
|
|
54
71
|
| `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
|
|
55
|
-
| `data.frame` | string | Relative frame — `scant`, `light`, `medium`, `
|
|
72
|
+
| `data.frame` | string | Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`. |
|
|
56
73
|
| `data.appearance.eye_color` | string | Eye colour. |
|
|
57
74
|
| `data.appearance.hair_color` | string | Hair colour. |
|
|
58
75
|
| `data.appearance.skin_color` | string | Skin colour. |
|
|
59
|
-
| `data.appearance.complexion` | string
|
|
76
|
+
| `data.appearance.complexion` | string or list | The skin's condition — one value, or several, because a face carries more than one at once. A single value means a list of one. |
|
|
60
77
|
| `data.appearance.extra_features` | list | Anything else a stranger would notice. |
|
|
61
78
|
|
|
62
79
|
**System mappings**
|
|
@@ -415,7 +432,7 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
415
432
|
| `data.namedDays` | list of `{ name, abbreviation?, day }` | Names assigned to particular days of the year. |
|
|
416
433
|
| `data.eras` | list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }` | The year-counts kept in this calendar. A marker names one era and uses these months. |
|
|
417
434
|
| `data.formats` | map of named Calendaria format strings | Named patterns for reading and writing this calendar's dates. |
|
|
418
|
-
| `data.
|
|
435
|
+
| `data.events` | list of `{ when, until?, recurs? }` | This note's dated occurrences — a founding once, an anniversary that recurs, or a list of recorded happenings. |
|
|
419
436
|
|
|
420
437
|
### map
|
|
421
438
|
|
|
@@ -56,6 +56,7 @@ import { emitDiagnostic } from "./diagnostics.mjs";
|
|
|
56
56
|
// The `{#appearance}` / `{#dossier}` convention is the note format's, so the
|
|
57
57
|
// extraction is shared; which field a section lands in stays the system's.
|
|
58
58
|
export { extractAnchorSection, renderSection } from "./anchored-sections.mjs";
|
|
59
|
+
import { misplacedAnchorSection } from "./anchored-sections.mjs";
|
|
59
60
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
60
61
|
import { beingDefaultArt } from "./art-fields.mjs";
|
|
61
62
|
import { contentPackage } from "./content-package.mjs";
|
|
@@ -472,6 +473,28 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
472
473
|
return { img, token: this.artPath(fm, "tokenIcon") ?? img };
|
|
473
474
|
}
|
|
474
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Report an `{#appearance}` or `{#dossier}` anchor on a heading
|
|
478
|
+
* {@link extractAnchorSection} cannot read, rather than letting the field
|
|
479
|
+
* it feeds compile blank with nothing to say why.
|
|
480
|
+
*
|
|
481
|
+
* @param {string} body - The note body.
|
|
482
|
+
* @param {string} anchorId - `"appearance"` or `"dossier"`.
|
|
483
|
+
* @param {string} ctx - The actor's label, for the message.
|
|
484
|
+
* @returns {void}
|
|
485
|
+
*/
|
|
486
|
+
checkAnchoredSection(body, anchorId, ctx) {
|
|
487
|
+
const misplaced = misplacedAnchorSection(body, anchorId, this.currentNote?.bodyLine);
|
|
488
|
+
if (!misplaced) return;
|
|
489
|
+
this.noteError(
|
|
490
|
+
`${ctx}: {#${anchorId}} sits on a heading below the top level, and only ` +
|
|
491
|
+
`an H1 feeds the ${anchorId} field — move the heading to the top level ` +
|
|
492
|
+
"or drop the anchor",
|
|
493
|
+
{ line: misplaced.line },
|
|
494
|
+
);
|
|
495
|
+
this.errorCount++;
|
|
496
|
+
}
|
|
497
|
+
|
|
475
498
|
/**
|
|
476
499
|
* Which `(actor, subType:identity)` each resolved entry claimed, and the
|
|
477
500
|
* entry that claimed it first.
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* **Anchored body sections** — the `# Heading {#anchor}` convention, and how a
|
|
16
16
|
* compiler pulls one section out of a note's prose.
|
|
17
17
|
*
|
|
18
|
-
* The content format gives
|
|
19
|
-
* `{#dossier}
|
|
18
|
+
* The content format gives two anchors a document meaning: `{#appearance}` and
|
|
19
|
+
* `{#dossier}`. *Which field* each lands in is a system's
|
|
20
20
|
* business — SoHL writes the first to an actor's `appearance`, HM3 to an
|
|
21
21
|
* actor's and an item's `description` — but *finding* it is not, so the
|
|
22
22
|
* extraction lives here where every compiler reaches it.
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { renderFoundryMarkdown } from "./helpers.mjs";
|
|
33
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
34
|
+
import { splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Extract the body of an H1 section whose heading carries the explicit
|
|
@@ -39,6 +41,11 @@ import { renderFoundryMarkdown } from "./helpers.mjs";
|
|
|
39
41
|
* heading exists. Fenced code blocks are respected so `# foo` inside
|
|
40
42
|
* ``` blocks does not trigger a match.
|
|
41
43
|
*
|
|
44
|
+
* An anchor declared on a lower heading is found by {@link collectAnchors}
|
|
45
|
+
* but not by the `#\s+` match below, so it extracts as absent rather than as
|
|
46
|
+
* wrong — {@link misplacedAnchorSection} is the check a caller runs first to
|
|
47
|
+
* tell the two apart.
|
|
48
|
+
*
|
|
42
49
|
* @param {string} body - The note body.
|
|
43
50
|
* @param {string} anchorId - The anchor to find.
|
|
44
51
|
* @returns {string} The section's markdown, or "".
|
|
@@ -57,8 +64,7 @@ export function extractAnchorSection(body, anchorId) {
|
|
|
57
64
|
}
|
|
58
65
|
const h1Match = !inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
|
|
59
66
|
if (h1Match) {
|
|
60
|
-
const
|
|
61
|
-
const id = anchor?.[1]?.trim().toLowerCase() || null;
|
|
67
|
+
const id = splitHeadingAttributes(h1Match[1]).id.toLowerCase() || null;
|
|
62
68
|
if (capturing) break;
|
|
63
69
|
if (id === wanted) {
|
|
64
70
|
capturing = true;
|
|
@@ -81,3 +87,28 @@ export function renderSection(body, anchorId) {
|
|
|
81
87
|
const slice = extractAnchorSection(body, anchorId);
|
|
82
88
|
return slice ? renderFoundryMarkdown(slice) : "";
|
|
83
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Where an `{#appearance}` or `{#dossier}` anchor sits on a heading below the
|
|
93
|
+
* top level, which {@link extractAnchorSection} cannot read.
|
|
94
|
+
*
|
|
95
|
+
* {@link module:engine/anchors.collectAnchors} finds the anchor at any
|
|
96
|
+
* level, because a lower heading legitimately starts a journal page. It does
|
|
97
|
+
* not feed an actor field: an H1-only extractor reads nothing from one, and a
|
|
98
|
+
* caller that trusts the empty result as "no such section" rather than
|
|
99
|
+
* "found, but unreadable" compiles a blank field with nothing to say why.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} body - The note body.
|
|
102
|
+
* @param {string} anchorId - The anchor to check, case-insensitively.
|
|
103
|
+
* @param {number} [bodyLine=1] - The 1-based file line the body starts on.
|
|
104
|
+
* @returns {{line: number}|null} The misplaced anchor's line, or `null` when
|
|
105
|
+
* it is absent or already on an H1.
|
|
106
|
+
*/
|
|
107
|
+
export function misplacedAnchorSection(body, anchorId, bodyLine = 1) {
|
|
108
|
+
const wanted = String(anchorId).toLowerCase();
|
|
109
|
+
const found = collectAnchors(body, bodyLine).find(
|
|
110
|
+
(anchor) => anchor.slug.toLowerCase() === wanted,
|
|
111
|
+
);
|
|
112
|
+
if (!found || found.level === 1) return null;
|
|
113
|
+
return { line: found.line };
|
|
114
|
+
}
|
package/engine/anchors.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The anchors a note declares on headings and
|
|
15
|
+
* The anchors a note declares on headings and figure fences.
|
|
16
16
|
*
|
|
17
17
|
* **A leaf, deliberately.** This is asked by the link checker, by the content
|
|
18
18
|
* index, and by the builds that emit a link, and they cannot all import one
|
|
@@ -22,37 +22,35 @@
|
|
|
22
22
|
* anchors does this note declare?" had two answers and they disagreed on
|
|
23
23
|
* `{#CalendarFormat}`.
|
|
24
24
|
*
|
|
25
|
+
* The suffix itself is read by {@link module:engine/heading-attributes}, which
|
|
26
|
+
* {@link module:engine/journals.splitPages} also reads it through — so the index
|
|
27
|
+
* cannot name an anchor the pages do not carry.
|
|
28
|
+
*
|
|
25
29
|
* @module
|
|
26
30
|
*/
|
|
27
31
|
|
|
28
|
-
import {
|
|
32
|
+
import { scanFigures } from "./content-figures.mjs";
|
|
33
|
+
import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
|
-
*
|
|
36
|
+
* The `{#slug}` and `:::figure {#slug}` anchors a note declares.
|
|
32
37
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* would advertise a link that resolves nowhere. `tests/content-index.test.ts`
|
|
37
|
-
* asserts the two find the same anchors, so drift fails the suite rather than
|
|
38
|
-
* shipping.
|
|
39
|
-
*/
|
|
40
|
-
const HEADING = /^\s*(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
41
|
-
const ANCHOR = /^(.*?)\s*\{#([^}]+)\}\s*$/;
|
|
42
|
-
/**
|
|
43
|
-
* The `{#slug}` and `:::caption {#slug}` anchors a note declares.
|
|
44
|
-
*
|
|
45
|
-
* A bare `#` heading starts a journal page without declaring a slug. Captioned
|
|
46
|
-
* blocks declare a slug and start an addressable journal page.
|
|
38
|
+
* A bare `#` heading starts a journal page without declaring a slug, and so
|
|
39
|
+
* does a figure fence that declares no id. A fence that declares one declares a
|
|
40
|
+
* slug and starts an addressable journal page.
|
|
47
41
|
*
|
|
48
42
|
* @param {string} body - The note's markdown body, frontmatter already removed.
|
|
49
43
|
* @param {number} [bodyLine] - The 1-based file line the body starts on, from
|
|
50
44
|
* `parseMarkdownFile`. Anchors are reported at their position in the **file**,
|
|
51
45
|
* so an editor can jump straight to one; passing nothing numbers from the body.
|
|
46
|
+
* @param {(address: string) => string|undefined} [resolveRole] - From a
|
|
47
|
+
* picture's address to the role its asset declares, so a figure anchor's
|
|
48
|
+
* `name` reads `Map 1` rather than `Figure 1` where it is due — see
|
|
49
|
+
* {@link module:engine/content-figures.scanFigures}.
|
|
52
50
|
* @returns {Array<{slug: string, name: string, level: number, line: number}>}
|
|
53
51
|
* In document order.
|
|
54
52
|
*/
|
|
55
|
-
export function collectAnchors(body, bodyLine = 1) {
|
|
53
|
+
export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
56
54
|
const anchors = [];
|
|
57
55
|
let inCodeBlock = false;
|
|
58
56
|
const lines = String(body ?? "").split("\n");
|
|
@@ -66,26 +64,24 @@ export function collectAnchors(body, bodyLine = 1) {
|
|
|
66
64
|
}
|
|
67
65
|
if (inCodeBlock) continue;
|
|
68
66
|
|
|
69
|
-
const heading =
|
|
67
|
+
const heading = HEADING_LINE.exec(lines[i]);
|
|
70
68
|
if (!heading) continue;
|
|
71
|
-
const
|
|
72
|
-
if (!
|
|
73
|
-
|
|
74
|
-
const slug = anchor[2].trim();
|
|
75
|
-
if (!slug) continue;
|
|
69
|
+
const attributes = splitHeadingAttributes(heading[2]);
|
|
70
|
+
if (!attributes.id) continue;
|
|
76
71
|
anchors.push({
|
|
77
|
-
slug,
|
|
78
|
-
name:
|
|
72
|
+
slug: attributes.id,
|
|
73
|
+
name: attributes.text,
|
|
79
74
|
level: heading[1].length,
|
|
80
75
|
line: bodyLine + i,
|
|
81
76
|
});
|
|
82
77
|
}
|
|
83
|
-
for (const
|
|
78
|
+
for (const figure of scanFigures(body, { resolveRole }).figures) {
|
|
79
|
+
if (!figure.id) continue;
|
|
84
80
|
anchors.push({
|
|
85
|
-
slug:
|
|
86
|
-
name:
|
|
81
|
+
slug: figure.id,
|
|
82
|
+
name: figure.label,
|
|
87
83
|
level: 1,
|
|
88
|
-
line: bodyLine +
|
|
84
|
+
line: bodyLine + figure.line - 1,
|
|
89
85
|
});
|
|
90
86
|
}
|
|
91
87
|
return anchors.sort((a, b) => a.line - b.line);
|
package/engine/art-fields.mjs
CHANGED
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
* @module
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
+
import path from "node:path";
|
|
48
|
+
|
|
47
49
|
import { encodeAddresses } from "./address-values.mjs";
|
|
48
50
|
import { ASSET_SYSTEM, isAssetType } from "./asset-types.mjs";
|
|
49
51
|
import { ASSETS_SEGMENT } from "./pathnames.mjs";
|
|
@@ -129,9 +131,49 @@ export function assetAddressIndex(records = [], { config, foreign, types = [] }
|
|
|
129
131
|
.filter(([key]) => key),
|
|
130
132
|
),
|
|
131
133
|
foreign: foreign?.index ?? new Map(),
|
|
134
|
+
// Foreign first, local last: a renderer sizing a picture looks it up
|
|
135
|
+
// by the pathname the record resolved to, never by address, and a
|
|
136
|
+
// local record is entered after a foreign one so it wins the lookup on
|
|
137
|
+
// the collision neither should ever cause.
|
|
138
|
+
byPath: assetImageInfoByPathname([
|
|
139
|
+
...(foreign?.index?.values() ?? []),
|
|
140
|
+
...records.filter(isAssetRecord),
|
|
141
|
+
]),
|
|
132
142
|
};
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
/**
|
|
146
|
+
* What a renderer needs to size one picture, keyed by the pathname its
|
|
147
|
+
* address resolves to.
|
|
148
|
+
*
|
|
149
|
+
* An image reaches a renderer as the resolved pathname an embed or an authored
|
|
150
|
+
* body image already carries — see {@link readAssetAddress} — never as the
|
|
151
|
+
* address that produced it, so the lookup a renderer wants is by pathname, not
|
|
152
|
+
* by {@link assetAddressIndex}'s own `assets` map.
|
|
153
|
+
*
|
|
154
|
+
* @param {readonly object[]} records - Asset records, local or foreign.
|
|
155
|
+
* @returns {Map<string, {type: string, role?: string, width: number|"", height: number|""}>}
|
|
156
|
+
* One entry per addressable file, carrying only what a renderer sizes a
|
|
157
|
+
* picture from. `role` is omitted when the record states none; `width` and
|
|
158
|
+
* `height` are always present, blank (`""`) for a vector — the record's own
|
|
159
|
+
* convention, carried through rather than collapsed into an absence a
|
|
160
|
+
* renderer could not tell apart from "no asset resolved at all".
|
|
161
|
+
*/
|
|
162
|
+
export function assetImageInfoByPathname(records = []) {
|
|
163
|
+
const map = new Map();
|
|
164
|
+
for (const record of records) {
|
|
165
|
+
if (!record?.asset?.path || !record.package) continue;
|
|
166
|
+
const key = `${record.package}/${ASSETS_SEGMENT}/${record.asset.path}`;
|
|
167
|
+
map.set(key, {
|
|
168
|
+
type: record.type,
|
|
169
|
+
role: record.asset.role || undefined,
|
|
170
|
+
width: typeof record.asset.width === "number" ? record.asset.width : "",
|
|
171
|
+
height: typeof record.asset.height === "number" ? record.asset.height : "",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
|
|
135
177
|
/**
|
|
136
178
|
* The asset one authored value names, or why it names none.
|
|
137
179
|
*
|
|
@@ -200,6 +242,82 @@ export function resolveArtRecord(index, value, defaultType, accepts) {
|
|
|
200
242
|
return readAssetAddress(index, value, defaultType, accepts).record;
|
|
201
243
|
}
|
|
202
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Every asset's role, keyed by the pathname it resolves to — the form a body
|
|
247
|
+
* carries once an embed's address has been rewritten into the ordinary image
|
|
248
|
+
* every surface renders ({@link module:engine/content-embeds.resolveEmbeds}).
|
|
249
|
+
*
|
|
250
|
+
* This is the lookup a `:::figure` fence's `map` counter reaches through —
|
|
251
|
+
* see {@link module:engine/content-figures.scanFigures}'s `resolveRole` —
|
|
252
|
+
* for a caller that reads a body after that rewrite: a Foundry journal, an
|
|
253
|
+
* item or actor's documentation, and the book.
|
|
254
|
+
*
|
|
255
|
+
* @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
|
|
256
|
+
* or the equivalent the site and the book build.
|
|
257
|
+
* @returns {Map<string, string>} Pathname → role, one entry per asset that
|
|
258
|
+
* declares one.
|
|
259
|
+
*/
|
|
260
|
+
export function pathnameRoles(index) {
|
|
261
|
+
const roles = new Map();
|
|
262
|
+
for (const record of [
|
|
263
|
+
...(index?.assets?.values() ?? []),
|
|
264
|
+
...(index?.foreign?.values() ?? []),
|
|
265
|
+
]) {
|
|
266
|
+
const role = record?.asset?.role;
|
|
267
|
+
if (!role || !record.asset?.path || !record.package) continue;
|
|
268
|
+
roles.set(`${record.package}/${ASSETS_SEGMENT}/${record.asset.path}`, role);
|
|
269
|
+
}
|
|
270
|
+
return roles;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Flag a note whose art slot names a picture that is not the size that slot
|
|
275
|
+
* is cut to.
|
|
276
|
+
*
|
|
277
|
+
* The size is stated by the slot itself, in
|
|
278
|
+
* {@link module:engine/art-slots.ART_SLOTS}, so it is written once and a slot
|
|
279
|
+
* that states none asks nothing of the picture it names. Today the hero image
|
|
280
|
+
* is the one slot with a fixed strip to fill; a picture written in prose is
|
|
281
|
+
* fitted to the room it has and is never measured here.
|
|
282
|
+
*
|
|
283
|
+
* A vector carries no pixel size to compare, and neither does a raster whose
|
|
284
|
+
* header cannot be read; neither is flagged.
|
|
285
|
+
*
|
|
286
|
+
* @param {readonly object[]} records - The corpus, notes and assets together.
|
|
287
|
+
* @param {object} [opts]
|
|
288
|
+
* @param {object} [opts.config] - The resolved build configuration.
|
|
289
|
+
* @param {string} [opts.assetsBase] - Where the asset roots sit, so a finding
|
|
290
|
+
* names the file from the working directory as every other finding does.
|
|
291
|
+
* @returns {object[]} A finding per picture off its slot's size,
|
|
292
|
+
* `severity: "error"`.
|
|
293
|
+
*/
|
|
294
|
+
export function checkArtSlotSizes(records = [], { config, assetsBase = "" } = {}) {
|
|
295
|
+
const sized = ART_SLOTS.filter((slot) => slot.size);
|
|
296
|
+
if (!sized.length) return [];
|
|
297
|
+
const index = assetAddressIndex(records, { config });
|
|
298
|
+
const findings = [];
|
|
299
|
+
for (const record of records) {
|
|
300
|
+
const data = record?.data;
|
|
301
|
+
if (!data) continue;
|
|
302
|
+
for (const slot of sized) {
|
|
303
|
+
const value = data[slot.key];
|
|
304
|
+
if (!value) continue;
|
|
305
|
+
const asset = resolveArtRecord(index, value, slot.type, slot.accepts);
|
|
306
|
+
const { path: file, width, height } = asset?.asset ?? {};
|
|
307
|
+
if (!file || !width || !height) continue;
|
|
308
|
+
if (Number(width) === slot.size.width && Number(height) === slot.size.height) continue;
|
|
309
|
+
findings.push({
|
|
310
|
+
file: path.join(assetsBase, file),
|
|
311
|
+
severity: "error",
|
|
312
|
+
message:
|
|
313
|
+
`${width}×${height} is not the ${slot.size.width}×${slot.size.height} ` +
|
|
314
|
+
`a \`${slot.key}\` is cut to`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return findings;
|
|
319
|
+
}
|
|
320
|
+
|
|
203
321
|
/**
|
|
204
322
|
* The pathname an art value names, in the form an authored one takes.
|
|
205
323
|
*
|
package/engine/art-slots.mjs
CHANGED
|
@@ -76,6 +76,12 @@ export const ART_SLOTS = Object.freeze([
|
|
|
76
76
|
type: "image",
|
|
77
77
|
accepts: ART_TYPES,
|
|
78
78
|
document: false,
|
|
79
|
-
|
|
79
|
+
// A hero image fills one fixed strip wherever it is drawn, so every
|
|
80
|
+
// banner is cut to one size and a picture of another size is resized
|
|
81
|
+
// to it. A picture written in prose fills no fixed strip and is
|
|
82
|
+
// fitted to whatever room it has, so no slot but this one states a
|
|
83
|
+
// size.
|
|
84
|
+
size: Object.freeze({ width: 1792, height: 768 }),
|
|
85
|
+
describe: "The page's hero image, cut to 1792×768. Reaches no compiled document.",
|
|
80
86
|
}),
|
|
81
87
|
]);
|