@heroiclands/package-build 23.0.0 → 23.1.1
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 +66 -0
- package/README.md +1 -1
- package/bin/content-commands.mjs +1 -1
- package/ci/content-format-reference.mjs +46 -0
- package/docs/api.md +124 -141
- package/docs/authoring/assets.md +1 -1
- package/docs/authoring/dates-and-calendars.md +12 -6
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +145 -29
- package/docs/commands.md +8 -10
- package/docs/configuration.md +13 -11
- package/docs/content-format.md +2 -2
- package/docs/guides/book.md +59 -4
- package/docs/guides/icons.md +4 -5
- package/docs/guides/packs.md +6 -0
- package/docs/guides/reference-pages.md +21 -1
- package/docs/guides/release-and-deploy.md +39 -7
- package/docs/guides/site.md +17 -18
- package/docs/reference/format-details.md +877 -276
- package/docs/reference/note-types.md +278 -91
- package/docs/reference/theme-pages.md +58 -97
- package/engine/address-values.mjs +124 -4
- package/engine/address.mjs +44 -3
- package/engine/anchors.mjs +103 -3
- package/engine/art-slots.mjs +2 -9
- package/engine/base-compiler.mjs +25 -6
- package/engine/calendar-artifacts.mjs +4 -1
- package/engine/calendar-notes.mjs +166 -102
- package/engine/content-format-check.mjs +2 -3
- package/engine/content-format.mjs +44 -1
- package/engine/content-format.yaml +79 -16
- package/engine/content-images.mjs +10 -1
- package/engine/content-index.mjs +37 -9
- package/engine/content-links.mjs +24 -5
- package/engine/content-tables.mjs +24 -3
- package/engine/data-keys.mjs +347 -0
- package/engine/derived-sections.mjs +322 -0
- package/engine/diagnostics.mjs +28 -0
- package/engine/event-fields.mjs +187 -0
- package/engine/event-rows.mjs +126 -0
- package/engine/event-views.mjs +305 -0
- package/engine/exported-scene.mjs +29 -53
- package/engine/frontmatter-lint.mjs +103 -77
- package/engine/frontmatter.mjs +7 -2
- package/engine/generate.mjs +53 -0
- package/engine/generated-sections.mjs +223 -0
- package/engine/heading-attributes.mjs +4 -2
- package/engine/helpers.mjs +12 -1
- package/engine/holdings.mjs +12 -33
- package/engine/ids.mjs +17 -11
- package/engine/index.mjs +4 -4
- package/engine/infobox-render.mjs +21 -6
- package/engine/infobox.mjs +217 -99
- package/engine/journals.mjs +104 -47
- package/engine/literature-notes.mjs +4 -1
- package/engine/literature-works.mjs +21 -13
- package/engine/map-graphviz.mjs +4 -18
- package/engine/markdown-expressions.mjs +116 -11
- package/engine/metadata-index.mjs +26 -0
- package/engine/near-miss.mjs +90 -0
- package/engine/note-addresses.mjs +75 -19
- package/engine/note-dates.mjs +5 -3
- package/engine/note-event-terms.mjs +364 -0
- package/engine/note-events.mjs +823 -0
- package/engine/note-vocabulary.mjs +184 -88
- package/engine/office-holders.mjs +20 -54
- package/engine/packagebuild-index.mjs +16 -1
- package/engine/pdf-build.mjs +97 -60
- package/engine/pdf-index.mjs +263 -0
- package/engine/pdf-render.mjs +47 -5
- package/engine/place-relation-terms.mjs +174 -0
- package/engine/place-relations.mjs +72 -160
- package/engine/rank-ladder.mjs +14 -41
- package/engine/reckoning-markers.mjs +2 -0
- package/engine/region-events.mjs +6 -12
- package/engine/scenes.mjs +73 -468
- package/engine/site-build.mjs +75 -101
- package/engine/site-index.mjs +3 -1
- package/engine/site-maps.mjs +43 -29
- package/engine/site-preview.mjs +40 -31
- package/engine/social-ties.mjs +2 -1
- package/engine/sql-tables.mjs +212 -9
- package/engine/standing-terms.mjs +145 -6
- package/engine/standings.mjs +37 -49
- package/engine/web-wikilinks.mjs +21 -1
- package/engine/wikilink-syntax.mjs +34 -4
- package/engine/wikilinks.mjs +27 -2
- package/engine/yaml-lint.mjs +7 -4
- package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
- package/hugo-theme/layouts/partials/page-body.html +0 -3
- package/hugo-theme/static/css/style.css +25 -55
- package/package.json +1 -2
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +62 -4
- package/sohl/note-schemas.mjs +8 -73
- package/types/engine/address-values.d.mts +50 -3
- package/types/engine/address.d.mts +21 -0
- package/types/engine/anchors.d.mts +49 -2
- package/types/engine/art-slots.d.mts +1 -1
- package/types/engine/base-compiler.d.mts +13 -4
- package/types/engine/calendar-notes.d.mts +7 -0
- package/types/engine/content-format.d.mts +52 -0
- package/types/engine/content-images.d.mts +5 -1
- package/types/engine/content-tables.d.mts +9 -1
- package/types/engine/data-keys.d.mts +145 -0
- package/types/engine/derived-sections.d.mts +73 -0
- package/types/engine/diagnostics.d.mts +15 -0
- package/types/engine/event-fields.d.mts +99 -0
- package/types/engine/event-rows.d.mts +12 -0
- package/types/engine/event-views.d.mts +67 -0
- package/types/engine/exported-scene.d.mts +5 -2
- package/types/engine/frontmatter-lint.d.mts +3 -3
- package/types/engine/generated-sections.d.mts +143 -0
- package/types/engine/holdings.d.mts +6 -20
- package/types/engine/ids.d.mts +15 -12
- package/types/engine/index.d.mts +1 -1
- package/types/engine/infobox.d.mts +13 -3
- package/types/engine/journals.d.mts +24 -11
- package/types/engine/literature-works.d.mts +1 -2
- package/types/engine/map-graphviz.d.mts +2 -10
- package/types/engine/markdown-expressions.d.mts +12 -3
- package/types/engine/near-miss.d.mts +42 -0
- package/types/engine/note-addresses.d.mts +7 -1
- package/types/engine/note-event-terms.d.mts +68 -0
- package/types/engine/note-events.d.mts +71 -0
- package/types/engine/note-vocabulary.d.mts +37 -8
- package/types/engine/office-holders.d.mts +3 -0
- package/types/engine/pdf-index.d.mts +35 -0
- package/types/engine/pdf-render.d.mts +8 -2
- package/types/engine/place-relation-terms.d.mts +83 -0
- package/types/engine/place-relations.d.mts +11 -66
- package/types/engine/rank-ladder.d.mts +2 -0
- package/types/engine/region-events.d.mts +6 -12
- package/types/engine/scenes.d.mts +6 -32
- package/types/engine/site-build.d.mts +8 -22
- package/types/engine/site-maps.d.mts +15 -7
- package/types/engine/sql-tables.d.mts +77 -2
- package/types/engine/standing-terms.d.mts +37 -4
- package/types/engine/standings.d.mts +14 -9
- package/types/engine/wikilink-syntax.d.mts +20 -0
- package/types/engine/wikilinks.d.mts +3 -2
- package/types/engine/yaml-lint.d.mts +5 -4
- package/types/release.d.mts +4 -0
- package/docs/guides/government-migration.md +0 -52
- package/engine/itinerary-scenes.mjs +0 -166
- package/engine/lore-events.mjs +0 -197
- package/engine/map-notes.mjs +0 -1309
- package/engine/map-raster.mjs +0 -51
- package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
- package/hugo-theme/layouts/partials/holdings.html +0 -61
- package/hugo-theme/layouts/partials/literature-works.html +0 -29
- package/hugo-theme/layouts/partials/place-map.html +0 -9
- package/types/engine/itinerary-scenes.d.mts +0 -29
- package/types/engine/lore-events.d.mts +0 -14
- package/types/engine/map-notes.d.mts +0 -297
- package/types/engine/map-raster.d.mts +0 -8
|
@@ -9,12 +9,12 @@ subType: reference
|
|
|
9
9
|
|
|
10
10
|
`package-build site` writes a page's front matter; the shared Hugo theme reads
|
|
11
11
|
it and draws the page around it. This reference describes that second half —
|
|
12
|
-
the infobox rail, the hero band, the
|
|
13
|
-
|
|
12
|
+
the infobox rail, the hero band, the generated sections, the related card,
|
|
13
|
+
search, and the markup an image or a figure renders as — and names the
|
|
14
14
|
classes a consumer styles against.
|
|
15
15
|
|
|
16
16
|
**None of the sections below is an authoring instruction.** `infoboxes`,
|
|
17
|
-
`related`, `
|
|
17
|
+
`related`, `package`, `slug`, `url`
|
|
18
18
|
and `title` cannot be written by a note at all: a note's top-level region is
|
|
19
19
|
closed and does not admit them, so each arrives already resolved by the time
|
|
20
20
|
the theme sees it. Read this page when a panel is missing or a class needs
|
|
@@ -47,11 +47,18 @@ infoboxes:
|
|
|
47
47
|
- label: Name
|
|
48
48
|
kind: text
|
|
49
49
|
value: Brànwâal Dôrgaar
|
|
50
|
-
- label:
|
|
50
|
+
- label: Homes
|
|
51
51
|
kind: links
|
|
52
52
|
value:
|
|
53
|
-
- text:
|
|
54
|
-
url: /thalorna/
|
|
53
|
+
- text: Vylar
|
|
54
|
+
url: /thalorna/place-vylar/
|
|
55
|
+
- id: affiliations
|
|
56
|
+
label: Affiliations
|
|
57
|
+
layout: list
|
|
58
|
+
entries:
|
|
59
|
+
- lead: "Captain, Blade (4), of "
|
|
60
|
+
text: The Silent Talon Company
|
|
61
|
+
url: /thalorna/affiliation-slntlncmpny/
|
|
55
62
|
- id: sohl
|
|
56
63
|
kind: system
|
|
57
64
|
system: sohl
|
|
@@ -84,6 +91,11 @@ Each layout draws under its own class:
|
|
|
84
91
|
|
|
85
92
|
A `link` value renders as an anchor where the build reached the page, and as
|
|
86
93
|
its own words where it did not; a whole `number` is set with digit grouping.
|
|
94
|
+
An entry of a `runin` group or a `list` section is `{lead, text, url}`: `lead`,
|
|
95
|
+
where present, is plain text set before the link, so a membership line —
|
|
96
|
+
`Captain, Blade (4), of The Silent Talon Company` — links the body's name and
|
|
97
|
+
nothing else. A being's memberships arrive as a `list` section of their own,
|
|
98
|
+
one entry per membership, because a line already holds commas.
|
|
87
99
|
|
|
88
100
|
**How the boxes are drawn.**
|
|
89
101
|
|
|
@@ -166,97 +178,46 @@ artwork where the considered answer was "none". See
|
|
|
166
178
|
declines one; the spelling differs from the hand-authored page's, and the two
|
|
167
179
|
are not interchangeable.
|
|
168
180
|
|
|
169
|
-
##
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
`
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
`
|
|
192
|
-
`
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
| `.holdings-table-grid` | the grid of entries (Governed by; the innermost grid of Within/Governed places) |
|
|
210
|
-
| `.holdings-kind-group` | one kind's block within Within or Governed places |
|
|
211
|
-
| `.holdings-kind-label` | the kind label heading a group |
|
|
212
|
-
| `.holdings-entry-name` | an entry's title, linked or plain |
|
|
213
|
-
| `.holdings-entry-kind` | the kind printed beside a Governed by entry |
|
|
214
|
-
|
|
215
|
-
A page whose front matter declares none of `contains`, `governed_by` or
|
|
216
|
-
`governed_places` renders no card at all.
|
|
217
|
-
|
|
218
|
-
## The "In song and story" card
|
|
219
|
-
|
|
220
|
-
`partials/literature-works.html` renders a second card directly below the
|
|
221
|
-
government card, listing the works of literature that name the page's subject.
|
|
222
|
-
The build derives `works` from every `literature` lore note's `data.subjects`,
|
|
223
|
-
including works a fetched index carries, so no note writes it:
|
|
224
|
-
|
|
225
|
-
```yaml
|
|
226
|
-
---
|
|
227
|
-
works: [{ title: …, url: …, form: … }] # works naming this page, sorted by title
|
|
228
|
-
---
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
`form` is the work's `data.form`, printed in parentheses beside its title, and is
|
|
232
|
-
absent when the work states none. A work naming one subject twice lists once, and
|
|
233
|
-
a work naming itself is not listed on its own page. An entry's `url` is absent
|
|
234
|
-
for a work that publishes no page, which renders as plain text through
|
|
235
|
-
`partials/holdings/entry.html`. A page no work names carries no `works` and
|
|
236
|
-
renders no card.
|
|
237
|
-
|
|
238
|
-
The card reuses the government card's classes, so a consumer styling one styles
|
|
239
|
-
both; `.literature-works` on the `<section>` tells them apart.
|
|
240
|
-
|
|
241
|
-
| class | drawn as |
|
|
242
|
-
| ----------------------- | --------------------------------- |
|
|
243
|
-
| `.literature-works` | the card, beside `.holdings` |
|
|
244
|
-
| `.holdings-table-label` | "In song and story" |
|
|
245
|
-
| `.holdings-table-grid` | the grid of works |
|
|
246
|
-
| `.holdings-entry-name` | a work's title, linked or plain |
|
|
247
|
-
| `.holdings-entry-kind` | the work's form, beside its title |
|
|
248
|
-
|
|
249
|
-
## The place map
|
|
250
|
-
|
|
251
|
-
A place page with `map: from-<shortcode>.svg` displays a **From here** panel
|
|
252
|
-
between Holdings and Related. The map is a page-bundle SVG the build draws
|
|
253
|
-
from the place's authored borders and routes, and `map:` is not a key a note
|
|
254
|
-
may write. Its place names link to their pages. A page with no `map` value or
|
|
255
|
-
no matching bundle resource displays no panel.
|
|
256
|
-
|
|
257
|
-
The drawing fits the content column and scrolls horizontally when the
|
|
258
|
-
available width is narrow. The relevant classes are `.place-map`,
|
|
259
|
-
`.place-map-heading`, and `.place-map-drawing`.
|
|
181
|
+
## Generated sections
|
|
182
|
+
|
|
183
|
+
The build appends sections to the end of a note's body before the page is
|
|
184
|
+
written: what lies within a place, who governs it and what an affiliation
|
|
185
|
+
governs, the events concerning the note, the works naming it, and the map
|
|
186
|
+
from a place. Each is an H1 with a fixed anchor followed by ordinary
|
|
187
|
+
Markdown, so the theme draws it as it draws any authored section — a heading
|
|
188
|
+
in the page, an entry in the table of contents — and no partial, class or
|
|
189
|
+
front-matter key is involved. The same Markdown is a page of the Foundry
|
|
190
|
+
journal and a section of the book.
|
|
191
|
+
|
|
192
|
+
They follow the author's text in this order, each only where it has
|
|
193
|
+
something to show:
|
|
194
|
+
|
|
195
|
+
| Section | Anchor | On | Shows |
|
|
196
|
+
| ----------------- | ---------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
197
|
+
| Within | `within` | a place | the places whose `data.parents` names it, a paragraph to each kind of place |
|
|
198
|
+
| Governed by | `governedby` | a place | the affiliation its `data.government` names, with that affiliation's kind |
|
|
199
|
+
| Governed places | `governedplaces` | an affiliation | the places whose `data.government` names it, a paragraph to each kind of place |
|
|
200
|
+
| Chronology | `chronology` | a place | the [event views](format-details.md#event-views) |
|
|
201
|
+
| Events | `events` | a being, an affiliation or a people | the event views |
|
|
202
|
+
| Accounts | `accounts` | an affiliation, a place or a people | the event views |
|
|
203
|
+
| What followed | `followed` | a note holding events | the event views |
|
|
204
|
+
| In song and story | `insongandstory` | any note a work names | the `literature` works whose `data.subjects` name the note or one of its events, with a form |
|
|
205
|
+
| From here | `fromhere` | a place that states, or is named in, a border or route | the map drawn from the place |
|
|
206
|
+
|
|
207
|
+
An entry naming a note with a page links to it and shows the note's name; an
|
|
208
|
+
entry naming a stub, which publishes no page, is its name as plain text.
|
|
209
|
+
|
|
210
|
+
**The map is set inline.** The website places the drawing inside the **From
|
|
211
|
+
here** section as an SVG within a `figure.note-image.note-image-full-width`,
|
|
212
|
+
so its place names stay links to their pages. The drawing fits the content
|
|
213
|
+
column and scrolls sideways on a narrow screen. `site.maps: false` in the
|
|
214
|
+
configuration gives no place the section.
|
|
215
|
+
|
|
216
|
+
**An author's own section replaces a generated one.** A note whose body
|
|
217
|
+
declares a prose anchor with a section's slug — a heading such as
|
|
218
|
+
`# Within {#within}`, or a block or span with that `id` — gets no generated
|
|
219
|
+
section for it, and its own section is published exactly as written. This
|
|
220
|
+
is the way to choose different contents for one, or to turn one off.
|
|
260
221
|
|
|
261
222
|
## The related card
|
|
262
223
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { isAddressTuple, renderAddress } from "./address.mjs";
|
|
8
|
+
import { isAddressTuple, readCanonicalKey, renderAddress } from "./address.mjs";
|
|
9
9
|
|
|
10
10
|
/** Address-keyed properties hold typed entries rather than string object keys. */
|
|
11
11
|
export class AddressEntries {
|
|
@@ -17,17 +17,137 @@ export class AddressEntries {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** An Address and
|
|
20
|
+
/** An Address and an anchor within its note, with the anchor's kind where it is known. */
|
|
21
21
|
export class AddressLink {
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} target - Complete tuple.
|
|
24
|
+
* @param {string} anchor - The anchor's slug.
|
|
25
|
+
* @param {"prose"|"event"|null} [kind] - The anchor's kind, or `null` where the
|
|
26
|
+
* position that holds the link does not fix one.
|
|
27
|
+
*/
|
|
28
|
+
constructor(target, anchor, kind = null) {
|
|
24
29
|
if (!isAddressTuple(target)) throw new TypeError("An Address link needs a complete tuple");
|
|
25
30
|
this.target = target;
|
|
26
31
|
this.anchor = anchor;
|
|
32
|
+
this.kind = kind;
|
|
27
33
|
Object.freeze(this);
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
/** The keys of an Address as the published index writes it, and no others. */
|
|
38
|
+
export const PUBLISHED_ADDRESS_KEYS = Object.freeze(["address", "anchor", "anchorKind"]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether a parsed JSON value is an Address in its published form.
|
|
42
|
+
*
|
|
43
|
+
* @param {unknown} value - A value read from a published index.
|
|
44
|
+
* @returns {boolean} Whether it is a map of exactly `address`, `anchor` and
|
|
45
|
+
* `anchorKind`, with `address` a string.
|
|
46
|
+
*/
|
|
47
|
+
export function isPublishedAddress(value) {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
49
|
+
const keys = Object.keys(value);
|
|
50
|
+
return (
|
|
51
|
+
keys.length === PUBLISHED_ADDRESS_KEYS.length &&
|
|
52
|
+
PUBLISHED_ADDRESS_KEYS.every((key) => keys.includes(key)) &&
|
|
53
|
+
typeof value.address === "string"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Write typed properties in the published index's form.
|
|
59
|
+
*
|
|
60
|
+
* Every Address becomes `{ address, anchor, anchorKind }`: `address` the full
|
|
61
|
+
* canonical Address, `anchor` the anchor's slug or `null`, `anchorKind` its
|
|
62
|
+
* kind or `null`. An Address-keyed map keeps string keys — JSON has no other —
|
|
63
|
+
* and a key takes no anchor, so nothing is lost.
|
|
64
|
+
*
|
|
65
|
+
* @param {any} value - A tuple-bearing tree.
|
|
66
|
+
* @param {WeakMap<object, object>} [seen] - Shared containers in this output.
|
|
67
|
+
* @returns {any} A serializable tree.
|
|
68
|
+
*/
|
|
69
|
+
export function publishAddresses(value, seen = new WeakMap()) {
|
|
70
|
+
if (value && typeof value === "object" && seen.has(value)) return seen.get(value);
|
|
71
|
+
if (isAddressTuple(value))
|
|
72
|
+
return { address: renderAddress(value), anchor: null, anchorKind: null };
|
|
73
|
+
if (value instanceof AddressLink)
|
|
74
|
+
return {
|
|
75
|
+
address: renderAddress(value.target),
|
|
76
|
+
anchor: value.anchor,
|
|
77
|
+
anchorKind: value.kind ?? null,
|
|
78
|
+
};
|
|
79
|
+
if (value instanceof AddressEntries) {
|
|
80
|
+
const out = {};
|
|
81
|
+
seen.set(value, out);
|
|
82
|
+
for (const { target, value: entry } of value.entries) {
|
|
83
|
+
const key = renderAddress(target);
|
|
84
|
+
if (Object.hasOwn(out, key)) throw new Error(`Repeated Address map key: ${key}`);
|
|
85
|
+
out[key] = publishAddresses(entry, seen);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
const out = [];
|
|
91
|
+
seen.set(value, out);
|
|
92
|
+
for (const entry of value) out.push(publishAddresses(entry, seen));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype)
|
|
96
|
+
return value;
|
|
97
|
+
const out = {};
|
|
98
|
+
seen.set(value, out);
|
|
99
|
+
for (const [key, entry] of Object.entries(value)) out[key] = publishAddresses(entry, seen);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Read a published index record back into typed properties: every published
|
|
105
|
+
* Address becomes a tuple, or an {@link AddressLink} where it carries an anchor.
|
|
106
|
+
*
|
|
107
|
+
* @param {any} value - A parsed record.
|
|
108
|
+
* @returns {any} The same tree, typed.
|
|
109
|
+
* @throws {Error} When a published Address is not a complete canonical one.
|
|
110
|
+
*/
|
|
111
|
+
export function readPublishedAddresses(value) {
|
|
112
|
+
// A record already typed in memory keeps its tuples: each is identified by
|
|
113
|
+
// the object itself, which a copy is not.
|
|
114
|
+
if (isAddressTuple(value) || value instanceof AddressLink || value instanceof AddressEntries)
|
|
115
|
+
return value;
|
|
116
|
+
if (isPublishedAddress(value)) {
|
|
117
|
+
const target = readCanonicalKey(value.address);
|
|
118
|
+
if (!isAddressTuple(target))
|
|
119
|
+
throw new Error(`Published Address is not complete: ${JSON.stringify(value.address)}`);
|
|
120
|
+
return value.anchor == null ?
|
|
121
|
+
target
|
|
122
|
+
: new AddressLink(target, value.anchor, value.anchorKind ?? null);
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(value)) return value.map(readPublishedAddresses);
|
|
125
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype)
|
|
126
|
+
return value;
|
|
127
|
+
return Object.fromEntries(
|
|
128
|
+
Object.entries(value).map(([key, entry]) => [key, readPublishedAddresses(entry)]),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Write every published Address in a parsed record as a plain string — the
|
|
134
|
+
* canonical Address, with `#<anchor>` where it carries one. What the SQL tables
|
|
135
|
+
* present, so a fence compares an Address as the text an author writes.
|
|
136
|
+
*
|
|
137
|
+
* @param {any} value - A parsed record.
|
|
138
|
+
* @returns {any} The same tree with each Address a string.
|
|
139
|
+
*/
|
|
140
|
+
export function flattenPublishedAddresses(value) {
|
|
141
|
+
if (isPublishedAddress(value))
|
|
142
|
+
return value.anchor == null ? value.address : `${value.address}#${value.anchor}`;
|
|
143
|
+
if (Array.isArray(value)) return value.map(flattenPublishedAddresses);
|
|
144
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype)
|
|
145
|
+
return value;
|
|
146
|
+
return Object.fromEntries(
|
|
147
|
+
Object.entries(value).map(([key, entry]) => [key, flattenPublishedAddresses(entry)]),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
31
151
|
/**
|
|
32
152
|
* Render typed properties at a generated-output boundary.
|
|
33
153
|
* @param {any} value - A tuple-bearing tree.
|
package/engine/address.mjs
CHANGED
|
@@ -34,9 +34,23 @@
|
|
|
34
34
|
* this module and nothing here qualifies one. A field the corpus writes at
|
|
35
35
|
* both lengths is an Address; a field never written qualified is a shortcode.
|
|
36
36
|
* - A **Wikilink** is `[[<Address>[#<anchor>]|<text>]]` — a different thing that
|
|
37
|
-
* *contains* an Address. The
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* *contains* an Address. The label belongs to the Wikilink.
|
|
38
|
+
*
|
|
39
|
+
* ## An anchor names a place inside what an Address names
|
|
40
|
+
*
|
|
41
|
+
* Any written form may end in `#<anchor>`: `place-ironfells#sack`,
|
|
42
|
+
* `thalorna-note-place-ironfells#sack`, or `ironfells#sack` where the
|
|
43
|
+
* position supplies the type. {@link splitAnchor} is the one reader of that
|
|
44
|
+
* suffix — a Wikilink's anchor and a frontmatter field's are read by it alike —
|
|
45
|
+
* and it hands back the Address part, which {@link parseAddress} reads as it
|
|
46
|
+
* reads any other. The anchor is never part of the tuple: it names a place
|
|
47
|
+
* *within* the note the tuple names, and every anchor a note declares has one
|
|
48
|
+
* of two kinds — `prose` for one its body declares, `event` for an event's `id`
|
|
49
|
+
* ({@link module:engine/anchors}). A wikilink names a `prose` anchor only.
|
|
50
|
+
*
|
|
51
|
+
* **A frontmatter field accepts an anchor only where it declares the kinds it
|
|
52
|
+
* takes** (`anchors: ["event"]`). Every other Address field refuses one with
|
|
53
|
+
* a finding at that value.
|
|
40
54
|
*
|
|
41
55
|
* **How an author writes an Address is any suffix of the tuple. At the code
|
|
42
56
|
* level there is one shape.**
|
|
@@ -315,6 +329,33 @@ export function parseAddress(
|
|
|
315
329
|
return tuple;
|
|
316
330
|
}
|
|
317
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Split a written value at its anchor.
|
|
334
|
+
*
|
|
335
|
+
* The anchor is everything after the first `#`, and the Address everything
|
|
336
|
+
* before it; both are trimmed. A value with no `#` has no `anchor` key at
|
|
337
|
+
* all, which is how a caller tells "no anchor" from `place-x#`, an empty one.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} written - The value as authored, any Wikilink label removed.
|
|
340
|
+
* @returns {{address: string, anchor?: string}} The two parts.
|
|
341
|
+
*/
|
|
342
|
+
export function splitAnchor(written) {
|
|
343
|
+
const text = String(written ?? "");
|
|
344
|
+
const hash = text.indexOf("#");
|
|
345
|
+
if (hash === -1) return { address: text.trim() };
|
|
346
|
+
return { address: text.slice(0, hash).trim(), anchor: text.slice(hash + 1).trim() };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Whether a written value carries an anchor.
|
|
351
|
+
*
|
|
352
|
+
* @param {unknown} written - The value as authored.
|
|
353
|
+
* @returns {boolean} Whether it is a string holding a `#`.
|
|
354
|
+
*/
|
|
355
|
+
export function hasAnchor(written) {
|
|
356
|
+
return typeof written === "string" && written.includes("#");
|
|
357
|
+
}
|
|
358
|
+
|
|
318
359
|
/** The values created by the Address reader, distinct from authored mappings. */
|
|
319
360
|
const TUPLES = new WeakSet();
|
|
320
361
|
|
package/engine/anchors.mjs
CHANGED
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The anchors a note declares on headings, captions, divs, poems and spans
|
|
15
|
+
* The anchors a note declares on headings, captions, divs, poems and spans,
|
|
16
|
+
* and on its events.
|
|
17
|
+
*
|
|
18
|
+
* **Every anchor has a kind** — one of {@link ANCHOR_KINDS} — so a position
|
|
19
|
+
* that accepts an anchor can say which kinds it takes: a `follows` edge names
|
|
20
|
+
* an `event`, and a wikilink a `prose` anchor. **A note has one namespace**: its event `id`s
|
|
21
|
+
* sit beside its body anchors, and a slug two of them share is a finding.
|
|
16
22
|
*
|
|
17
23
|
* **A leaf, deliberately.** This is asked by the link checker, by the content
|
|
18
24
|
* index, and by the builds that emit a link, and they cannot all import one
|
|
@@ -37,6 +43,14 @@ import { codeRegions } from "./code-fences.mjs";
|
|
|
37
43
|
import { slugify } from "./content-slug.mjs";
|
|
38
44
|
import { scanFigures } from "./content-figures.mjs";
|
|
39
45
|
import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
46
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What an anchor marks. `prose` is every anchor the body declares — a heading,
|
|
50
|
+
* a caption, a block, an alert, a poem or a span — and `event` is the `id` of
|
|
51
|
+
* an entry in `data.events`.
|
|
52
|
+
*/
|
|
53
|
+
export const ANCHOR_KINDS = Object.freeze(["prose", "event"]);
|
|
40
54
|
|
|
41
55
|
/**
|
|
42
56
|
* The identifiers a note declares on headings, captions, divs, poems and spans.
|
|
@@ -52,8 +66,8 @@ import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
|
52
66
|
* picture's address to the role its asset declares, so a figure anchor's
|
|
53
67
|
* `name` reads `Map 1` rather than `Figure 1` where it is due — see
|
|
54
68
|
* {@link module:engine/content-figures.scanFigures}.
|
|
55
|
-
* @returns {Array<{slug: string, name: string, level: number, line: number
|
|
56
|
-
* In document order.
|
|
69
|
+
* @returns {Array<{slug: string, name: string, level: number, line: number,
|
|
70
|
+
* kind: string}>} In document order.
|
|
57
71
|
*/
|
|
58
72
|
export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
59
73
|
const anchors = [];
|
|
@@ -76,6 +90,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
76
90
|
name: attributes.text,
|
|
77
91
|
level: heading[1].length,
|
|
78
92
|
line: bodyLine + i,
|
|
93
|
+
kind: "prose",
|
|
79
94
|
});
|
|
80
95
|
}
|
|
81
96
|
for (const figure of scanFigures(body, { resolveRole }).figures) {
|
|
@@ -85,6 +100,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
85
100
|
name: figure.label || figure.caption,
|
|
86
101
|
level: 1,
|
|
87
102
|
line: bodyLine + figure.line - 1,
|
|
103
|
+
kind: "prose",
|
|
88
104
|
});
|
|
89
105
|
}
|
|
90
106
|
const blockAnchors = (source, firstLine) => {
|
|
@@ -96,6 +112,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
96
112
|
name: block.title || block.id,
|
|
97
113
|
level: 0,
|
|
98
114
|
line: firstLine + block.start,
|
|
115
|
+
kind: "prose",
|
|
99
116
|
});
|
|
100
117
|
blockAnchors(
|
|
101
118
|
sourceLines.slice(block.start + 1, block.end).join("\n"),
|
|
@@ -111,6 +128,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
111
128
|
name: alert.title,
|
|
112
129
|
level: 0,
|
|
113
130
|
line: bodyLine + alert.start,
|
|
131
|
+
kind: "prose",
|
|
114
132
|
});
|
|
115
133
|
}
|
|
116
134
|
for (const poem of scanPoetry(body).blocks) {
|
|
@@ -120,6 +138,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
120
138
|
name: poem.id,
|
|
121
139
|
level: 0,
|
|
122
140
|
line: bodyLine + poem.start,
|
|
141
|
+
kind: "prose",
|
|
123
142
|
});
|
|
124
143
|
}
|
|
125
144
|
for (const span of scanSpans(body).spans) {
|
|
@@ -129,6 +148,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
129
148
|
name: span.text,
|
|
130
149
|
level: 0,
|
|
131
150
|
line: bodyLine + span.line - 1,
|
|
151
|
+
kind: "prose",
|
|
132
152
|
});
|
|
133
153
|
}
|
|
134
154
|
return anchors.sort((a, b) => a.line - b.line);
|
|
@@ -158,3 +178,83 @@ export function markupAnchorFindings(body) {
|
|
|
158
178
|
}
|
|
159
179
|
return errors;
|
|
160
180
|
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The anchors a note's events declare: one per `data.events` entry carrying
|
|
184
|
+
* an `id`, at the line its `id` is written on.
|
|
185
|
+
*
|
|
186
|
+
* The `id` is taken as written — {@link module:engine/note-events} holds it to
|
|
187
|
+
* the address-segment charset — and named by the note's own name for the event
|
|
188
|
+
* when one is given, else by its summary.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} fm - The note's frontmatter.
|
|
191
|
+
* @param {string} [raw] - The note's full text, for the line; without it the
|
|
192
|
+
* line is omitted rather than guessed.
|
|
193
|
+
* @returns {Array<{slug: string, name: string, level: number, line?: number,
|
|
194
|
+
* kind: "event"}>} In entry order.
|
|
195
|
+
*/
|
|
196
|
+
export function eventAnchors(fm, raw) {
|
|
197
|
+
const events = fm?.data?.events;
|
|
198
|
+
if (!Array.isArray(events)) return [];
|
|
199
|
+
const out = [];
|
|
200
|
+
events.forEach((entry, position) => {
|
|
201
|
+
if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || !entry.id)
|
|
202
|
+
return;
|
|
203
|
+
const named =
|
|
204
|
+
Array.isArray(entry.names) ? entry.names.find((n) => n?.name)?.name : undefined;
|
|
205
|
+
const { line } =
|
|
206
|
+
raw ? positionOfFrontmatterPath(raw, ["data", "events", position, "id"]) : {};
|
|
207
|
+
out.push({
|
|
208
|
+
slug: entry.id,
|
|
209
|
+
name: String(named ?? entry.summary ?? entry.id),
|
|
210
|
+
level: 0,
|
|
211
|
+
...(line ? { line } : {}),
|
|
212
|
+
kind: "event",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Event `id`s that repeat one of the note's body anchors.
|
|
220
|
+
*
|
|
221
|
+
* A note's anchors are one namespace, so `place-ironfells#sack` names one
|
|
222
|
+
* thing whether `sack` is a heading or an event. Two body anchors that collide
|
|
223
|
+
* are reported by the scanners that read them, and two events sharing an `id`
|
|
224
|
+
* by the event check; this reports the remaining case, an event against a body
|
|
225
|
+
* anchor, at the event's `id`.
|
|
226
|
+
*
|
|
227
|
+
* @param {{file: string, fm: object, raw?: string, body?: string}} note - The
|
|
228
|
+
* note, as the link index hands it over.
|
|
229
|
+
* @returns {object[]} Findings, each at the colliding `id`.
|
|
230
|
+
*/
|
|
231
|
+
export function noteAnchorFindings(note) {
|
|
232
|
+
const events = eventAnchors(note.fm, note.raw);
|
|
233
|
+
if (!events.length) return [];
|
|
234
|
+
const body =
|
|
235
|
+
typeof note.body === "string" ?
|
|
236
|
+
note.body
|
|
237
|
+
: String(note.raw ?? "").replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
238
|
+
const taken = new Map();
|
|
239
|
+
for (const anchor of collectAnchors(body)) {
|
|
240
|
+
const slug = slugify(anchor.slug);
|
|
241
|
+
if (!taken.has(slug)) taken.set(slug, anchor);
|
|
242
|
+
}
|
|
243
|
+
const findings = [];
|
|
244
|
+
const entries = note.fm.data.events;
|
|
245
|
+
for (const event of events) {
|
|
246
|
+
const other = taken.get(slugify(event.slug));
|
|
247
|
+
if (!other) continue;
|
|
248
|
+
const position = entries.findIndex((entry) => entry?.id === event.slug);
|
|
249
|
+
findings.push({
|
|
250
|
+
file: note.file,
|
|
251
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "events", position, "id"]),
|
|
252
|
+
severity: "error",
|
|
253
|
+
message:
|
|
254
|
+
`\`data.events.${position}.id\` ${event.slug} is already a prose anchor ` +
|
|
255
|
+
"in this note — a note's anchors are one namespace, so an event's `id` " +
|
|
256
|
+
"differs from every heading, caption and block slug",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return findings;
|
|
260
|
+
}
|
package/engine/art-slots.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
/**
|
|
30
30
|
* What every art slot accepts, whichever type it defaults to.
|
|
31
31
|
*
|
|
32
|
-
* A sound is not art: `audio` is refused at
|
|
32
|
+
* A sound is not art: `audio` is refused at every slot, and every slot
|
|
33
33
|
* that defaults to `icon` or to `image` accepts either — the corpus is why:
|
|
34
34
|
* `sohl-kethira-basic` writes a deity's profile art as `icon: image-…` twenty
|
|
35
35
|
* times, one full illustration per faith tradition, and a slot refusing the
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
const ART_TYPES = Object.freeze(["icon", "image"]);
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* The
|
|
43
|
+
* The art slots, in the order the specification tabulates them.
|
|
44
44
|
*
|
|
45
45
|
* `banner` is the one that reaches no compiled document: it is the page's hero
|
|
46
46
|
* image, read by the site and by nothing else. The book's section plates read a
|
|
@@ -65,13 +65,6 @@ export const ART_SLOTS = Object.freeze([
|
|
|
65
65
|
document: true,
|
|
66
66
|
describe: "What a token on the canvas wears; unset, it follows `icon`.",
|
|
67
67
|
}),
|
|
68
|
-
Object.freeze({
|
|
69
|
-
key: "bgImage",
|
|
70
|
-
type: "image",
|
|
71
|
-
accepts: ART_TYPES,
|
|
72
|
-
document: true,
|
|
73
|
-
describe: "A map's background art, resolved into `background.src`.",
|
|
74
|
-
}),
|
|
75
68
|
Object.freeze({
|
|
76
69
|
key: "banner",
|
|
77
70
|
type: "image",
|