@heroiclands/package-build 23.12.4 → 23.12.6
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 +28 -0
- package/content-config.mjs +26 -0
- package/docs/api.md +9 -5
- package/docs/authoring/dates-and-calendars.md +29 -1
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +7 -7
- package/docs/configuration.md +30 -4
- package/docs/guides/book.md +12 -12
- package/docs/guides/module-project.md +1 -1
- package/docs/reference/format-details.md +81 -24
- package/docs/reference/note-types.md +10 -11
- package/engine/content-format.yaml +0 -2
- package/engine/content-index.mjs +23 -41
- package/engine/content-links.mjs +3 -0
- package/engine/event-fields.mjs +24 -1
- package/engine/event-rows.mjs +82 -2
- package/engine/event-views.mjs +74 -17
- package/engine/frontmatter-lint.mjs +11 -0
- package/engine/generated-sections.mjs +8 -1
- package/engine/holdings.mjs +8 -2
- package/engine/index-records.mjs +1 -0
- package/engine/infobox-render.mjs +16 -5
- package/engine/infobox.mjs +27 -10
- package/engine/literature-works.mjs +8 -2
- package/engine/markdown-expressions.mjs +72 -17
- package/engine/metadata-index.mjs +3 -0
- package/engine/name-sort.mjs +91 -0
- package/engine/note-calendars.mjs +314 -0
- package/engine/note-frontmatter.mjs +1 -1
- package/engine/note-vocabulary.mjs +12 -17
- package/engine/page-lists.mjs +3 -2
- package/engine/pdf-index.mjs +11 -4
- package/engine/pdf-pages.mjs +2 -1
- package/engine/pdf-render.mjs +43 -13
- package/engine/pdf-toc.mjs +4 -3
- package/engine/reckoning-markers.mjs +63 -3
- package/engine/related-pages.mjs +12 -2
- package/engine/site-build.mjs +3 -0
- package/engine/sql-tables.mjs +28 -7
- package/package.json +1 -1
- package/types/content-config.d.mts +12 -0
- package/types/engine/content-index.d.mts +9 -32
- package/types/engine/event-fields.d.mts +11 -0
- package/types/engine/event-rows.d.mts +19 -1
- package/types/engine/event-views.d.mts +8 -3
- package/types/engine/generated-sections.d.mts +6 -0
- package/types/engine/holdings.d.mts +7 -1
- package/types/engine/infobox.d.mts +11 -0
- package/types/engine/literature-works.d.mts +5 -0
- package/types/engine/name-sort.d.mts +49 -0
- package/types/engine/note-calendars.d.mts +60 -0
- package/types/engine/pdf-render.d.mts +8 -0
- package/types/engine/pdf-toc.d.mts +1 -1
- package/types/engine/reckoning-markers.d.mts +32 -1
- package/types/engine/related-pages.d.mts +5 -0
|
@@ -18,7 +18,7 @@ These are the only keys a note writes at its top level, in the order the formatt
|
|
|
18
18
|
| Key | Meaning |
|
|
19
19
|
| ------------- | ----------------------------------------------------------------------------------------- |
|
|
20
20
|
| `shortcode` | The note's own address segment, unique within its type. |
|
|
21
|
-
| `name` | The display names — a required `full`, and any `aliases`.
|
|
21
|
+
| `name` | The display names — a required `full`, and any `aliases` and `sort`. |
|
|
22
22
|
| `type` | What the note is about, which decides its vocabulary and its document. |
|
|
23
23
|
| `subType` | The type's own genre, where it declares one. |
|
|
24
24
|
| `description` | The short page summary. |
|
|
@@ -32,14 +32,15 @@ These are the only keys a note writes at its top level, in the order the formatt
|
|
|
32
32
|
|
|
33
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. `data.icon` is not shared: it is listed under each type whose document carries art.
|
|
34
34
|
|
|
35
|
-
| Field | Shape | Meaning
|
|
36
|
-
| ----------------- | ----------------------------------------------- |
|
|
37
|
-
| `data.seeAlso` | list of `{ address, gloss? }` | Deliberate cross-references in reading order, with optional glosses; shown on the site, in journals and in books.
|
|
38
|
-
| `data.
|
|
39
|
-
| `data.
|
|
40
|
-
| `data.
|
|
41
|
-
| `data.
|
|
42
|
-
| `data.
|
|
35
|
+
| Field | Shape | Meaning |
|
|
36
|
+
| ----------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| `data.seeAlso` | list of `{ address, gloss? }` | Deliberate cross-references in reading order, with optional glosses; shown on the site, in journals and in books. |
|
|
38
|
+
| `data.calendar` | an Address | The calendar this note's dates print in, as a calendar lore note. Absent, the note takes its culture's calendar, then — for a place — its parents', then the configured `defaultCalendar`. |
|
|
39
|
+
| `data.id` | string | A fixed document identity when the derived identity is unsuitable. |
|
|
40
|
+
| `data.pack` | string | The shared compendium route, with a system block overriding it. |
|
|
41
|
+
| `data.packFolder` | an Address, or a map of Addresses keyed by pack | The shared compendium folder Address, with a system block overriding it. |
|
|
42
|
+
| `data.harnworld` | `{ realm?, ritual? }` | HârnWorld source details shared by every system — `realm` and `ritual`. |
|
|
43
|
+
| `data.banner` | an Address | The page's hero image — an `image` address, cut to 1792×768, or `none` to decline a hero image. Reaches no compiled document. |
|
|
43
44
|
|
|
44
45
|
**Inner keys** — a key a field's value does not declare is an error at its own line and column.
|
|
45
46
|
|
|
@@ -71,7 +72,6 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
71
72
|
| `data.gender` | string | One of `female`, `male`, `nonbinary`, `none` or `other`. `none` says the being has no gender; an absent field says its gender is unrecorded. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
72
73
|
| `data.species` | an Address | The being's species, as a lore note. |
|
|
73
74
|
| `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
|
|
74
|
-
| `data.calendar` | an Address | Calendar that displays this being's dates. |
|
|
75
75
|
| `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive. |
|
|
76
76
|
| `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown. |
|
|
77
77
|
| `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
|
|
@@ -867,7 +867,6 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
867
867
|
| Field | Shape | Meaning |
|
|
868
868
|
| -------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
869
869
|
| `data.events` | list of event entries | What happened to or at this subject — each entry one dated, attributed event: a founding, a war, a fall, or an occasion that recurs. |
|
|
870
|
-
| `data.calendar` | an Address | Calendar that displays this place's dates. |
|
|
871
870
|
| `data.demonym` | string | What a person from this place is called — a Vylarian. |
|
|
872
871
|
| `data.purpose` | string | The reason this settlement, site, or structure exists, selected from its placeCharacter tags. |
|
|
873
872
|
| `data.lore` | list of Addresses | Lore concerning this place — its peoples, its law, its calendar, its history. |
|
package/engine/content-index.mjs
CHANGED
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
*/
|
|
87
87
|
|
|
88
88
|
import { isAddressSegment } from "./address-charset.mjs";
|
|
89
|
+
import { configFileIn, resolveNoteCalendars } from "./note-calendars.mjs";
|
|
89
90
|
import { cultureInheritance, reportCultureParents } from "./culture-parents.mjs";
|
|
90
91
|
import {
|
|
91
92
|
formatDiagnostic,
|
|
@@ -108,8 +109,9 @@ import { analyzeNameSpans } from "./names.mjs";
|
|
|
108
109
|
import fs from "node:fs";
|
|
109
110
|
import path from "node:path";
|
|
110
111
|
|
|
111
|
-
import
|
|
112
|
+
import { asciiName } from "./name-sort.mjs";
|
|
112
113
|
|
|
114
|
+
export { asciiName };
|
|
113
115
|
import { loadForeignIndexes, metadataFileName } from "./metadata-index.mjs";
|
|
114
116
|
import { collectAssetRecords } from "./asset-index.mjs";
|
|
115
117
|
import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
@@ -161,7 +163,7 @@ import { resolveNoteId } from "./note-ids.mjs";
|
|
|
161
163
|
// The `package:` refusal, so a note declaring one is reported where it is
|
|
162
164
|
// rather than as a bare abort.
|
|
163
165
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
164
|
-
import { loadPackConfig } from "./pack-config.mjs";
|
|
166
|
+
import { CONFIG_FILENAMES, loadPackConfig } from "./pack-config.mjs";
|
|
165
167
|
// The record accessors, which live apart so that a module the compilers load
|
|
166
168
|
// can read a record without importing this one and closing a cycle.
|
|
167
169
|
// Re-exported because this is where callers have always addressed them.
|
|
@@ -245,44 +247,6 @@ export function noteAddress(frontmatter, contentPackage) {
|
|
|
245
247
|
};
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
/**
|
|
249
|
-
* A note's display name reduced to printable 7-bit ASCII.
|
|
250
|
-
*
|
|
251
|
-
* Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
|
|
252
|
-
* `Kèthîra` — and nobody types them. A reader searching the index, or an editor
|
|
253
|
-
* completing a wikilink, needs a form that matches what a keyboard produces, so
|
|
254
|
-
* the record states one rather than leaving every consumer to invent it (and to
|
|
255
|
-
* invent a *different* one, which is how two searches over the same data come
|
|
256
|
-
* to disagree).
|
|
257
|
-
*
|
|
258
|
-
* **Transliterated, not stripped.** `unidecode` — the same table
|
|
259
|
-
* {@link slugify} already runs, so an ASCII name and a slug can never disagree
|
|
260
|
-
* about a character — carries a letter across rather than deleting it:
|
|
261
|
-
* diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
|
|
262
|
-
* `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
|
|
263
|
-
* even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
|
|
264
|
-
* would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
|
|
265
|
-
*
|
|
266
|
-
* Anything still outside printable ASCII after that becomes a space, and runs
|
|
267
|
-
* of whitespace collapse — a space rather than nothing, so a character that
|
|
268
|
-
* transliterates away cannot silently weld two words together.
|
|
269
|
-
*
|
|
270
|
-
* The value is emitted even when it equals the name, so a consumer matching on
|
|
271
|
-
* it never has to branch on whether the name happened to be ASCII already.
|
|
272
|
-
*
|
|
273
|
-
* @param {unknown} name - The note's `name.full`.
|
|
274
|
-
* @returns {string|null} The ASCII form, or `null` when there is no name, or
|
|
275
|
-
* nothing printable survives.
|
|
276
|
-
*/
|
|
277
|
-
export function asciiName(name) {
|
|
278
|
-
if (typeof name !== "string") return null;
|
|
279
|
-
const folded = unidecode(name)
|
|
280
|
-
.replace(/[^\x20-\x7E]/g, " ")
|
|
281
|
-
.replace(/\s+/g, " ")
|
|
282
|
-
.trim();
|
|
283
|
-
return folded === "" ? null : folded;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
250
|
/**
|
|
287
251
|
* A note's `name.aliases` reduced to printable 7-bit ASCII, in order.
|
|
288
252
|
*
|
|
@@ -1099,6 +1063,10 @@ export function resolveCultures(
|
|
|
1099
1063
|
* reader receives each as a finding, and the emitter throws once, listing
|
|
1100
1064
|
* every one.
|
|
1101
1065
|
* @param {string} [options.currentDate="720.1"] - Setting-wide neutral current date.
|
|
1066
|
+
* @param {string} [options.defaultCalendar] - The calendar of last resort, a
|
|
1067
|
+
* calendar note's shortcode; see {@link module:engine/note-calendars}.
|
|
1068
|
+
* @param {string} [options.configFile] - The configuration file, where an
|
|
1069
|
+
* unknown `defaultCalendar` is reported.
|
|
1102
1070
|
* @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
|
|
1103
1071
|
* Another package's entry for a canonical address, read for a parent that
|
|
1104
1072
|
* package publishes; see {@link resolveCultures}.
|
|
@@ -1116,6 +1084,8 @@ export function collectContentIndex(
|
|
|
1116
1084
|
{
|
|
1117
1085
|
contentPackage,
|
|
1118
1086
|
currentDate,
|
|
1087
|
+
defaultCalendar,
|
|
1088
|
+
configFile,
|
|
1119
1089
|
skipDirectories,
|
|
1120
1090
|
assetsBase,
|
|
1121
1091
|
manifest,
|
|
@@ -1271,7 +1241,17 @@ export function collectContentIndex(
|
|
|
1271
1241
|
|
|
1272
1242
|
// Every note record exists and every Address in it is a tuple, so a parent
|
|
1273
1243
|
// is found by its canonical address. Before the sort, which reads nothing
|
|
1274
|
-
// the pass writes.
|
|
1244
|
+
// the pass writes. Calendars first: they read the culture a note states,
|
|
1245
|
+
// which the culture pass then replaces with the one it resolves.
|
|
1246
|
+
resolveNoteCalendars(records, {
|
|
1247
|
+
contentPackage,
|
|
1248
|
+
defaultCalendar,
|
|
1249
|
+
configFile,
|
|
1250
|
+
contentBase,
|
|
1251
|
+
foreignRecord,
|
|
1252
|
+
problems,
|
|
1253
|
+
parsed,
|
|
1254
|
+
});
|
|
1275
1255
|
resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems, parsed });
|
|
1276
1256
|
const cultureNotes = records
|
|
1277
1257
|
.filter((record) => isNoteRecord(record) && record.type === "lore")
|
|
@@ -1496,6 +1476,8 @@ export function indexRecordsFor({
|
|
|
1496
1476
|
return collectContentIndex(tree, {
|
|
1497
1477
|
contentPackage: resolved.contentPackage,
|
|
1498
1478
|
currentDate: resolved.currentDate,
|
|
1479
|
+
defaultCalendar: resolved.defaultCalendar ?? undefined,
|
|
1480
|
+
configFile: configFileIn(resolved.rootDir, CONFIG_FILENAMES),
|
|
1499
1481
|
addressContext: {
|
|
1500
1482
|
...noteAddressContext(resolved),
|
|
1501
1483
|
declaresAddress: dependencyAddresses(resolved),
|
package/engine/content-links.mjs
CHANGED
|
@@ -239,6 +239,9 @@ export function buildLinkIndex(
|
|
|
239
239
|
const body = splitFrontmatter(raw)?.body ?? raw;
|
|
240
240
|
const type = record.type.toLowerCase();
|
|
241
241
|
const note = { file: absPath, rel, fm, body, raw, type };
|
|
242
|
+
// The calendar the index resolved, which the reckoning context built
|
|
243
|
+
// over these notes hands to every date printed for this note.
|
|
244
|
+
if (record.resolvedCalendar) note.resolvedCalendar = record.resolvedCalendar;
|
|
242
245
|
const hierarchyProblems = cultureRecordProblems(record);
|
|
243
246
|
note.inheritedCultureFields = cultureInheritedFields(record);
|
|
244
247
|
if (hierarchyProblems)
|
package/engine/event-fields.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { ownDocumentSystem, parseAddress, readCanonicalKey, renderAddress } from "./address.mjs";
|
|
26
|
-
import { formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
26
|
+
import { formatDateInCalendar, formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
27
27
|
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
28
28
|
|
|
29
29
|
/** The fields of an event an inline reference may print, in the order a message names them. */
|
|
@@ -161,6 +161,29 @@ export function printEventDate(written, date, dates) {
|
|
|
161
161
|
return printable?.prose ?? printable?.text ?? date.prose ?? date.text ?? String(written);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A date as `dateformat` prints it in a calendar, where one is named and
|
|
166
|
+
* prints it; otherwise as {@link printEventDate} prints it.
|
|
167
|
+
*
|
|
168
|
+
* @param {unknown} written - The date as the event writes it.
|
|
169
|
+
* @param {object|null} date - The resolved date.
|
|
170
|
+
* @param {object} [dates] - The reckoning context.
|
|
171
|
+
* @param {string} [calendar] - The calendar's Address or shortcode.
|
|
172
|
+
* @returns {string} The printed date; the authored text when nothing resolves.
|
|
173
|
+
*/
|
|
174
|
+
export function printEventDateIn(written, date, dates, calendar) {
|
|
175
|
+
if (date?.known && calendar) {
|
|
176
|
+
try {
|
|
177
|
+
const printable = formatDateInCalendar(date, calendar, dates);
|
|
178
|
+
const text = printable?.prose ?? printable?.text;
|
|
179
|
+
if (text) return text;
|
|
180
|
+
} catch {
|
|
181
|
+
// A calendar that does not resolve leaves the date in its own era.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return printEventDate(written, date, dates);
|
|
185
|
+
}
|
|
186
|
+
|
|
164
187
|
/**
|
|
165
188
|
* One field of one event, as the text a reference prints, or why it cannot.
|
|
166
189
|
*
|
package/engine/event-rows.mjs
CHANGED
|
@@ -25,11 +25,27 @@
|
|
|
25
25
|
* `resolvedDates.events`, position for position with `data.events`, and the
|
|
26
26
|
* sort keys are read from it rather than parsed again here.
|
|
27
27
|
*
|
|
28
|
+
* **`whenText` and `untilText` are the dates as a reader of the owning note
|
|
29
|
+
* sees them**: printed as the `dateformat` helper prints a date, in the
|
|
30
|
+
* calendar the owning note resolves to — see
|
|
31
|
+
* {@link module:engine/note-calendars} — else in the era the date itself
|
|
32
|
+
* names. A date that does not resolve keeps its authored text.
|
|
33
|
+
*
|
|
34
|
+
* **`whenIn` and `untilIn` are the same dates in every calendar**: a struct
|
|
35
|
+
* keyed by each calendar note's canonical Address, every calendar the corpus
|
|
36
|
+
* holds or any note resolves to, so a query reads a date in the calendar it
|
|
37
|
+
* chooses — `e.whenIn['thalorna-note-lore-commoncal']`. Every row carries every
|
|
38
|
+
* key; a date a calendar cannot print reads there as it would in `whenText`
|
|
39
|
+
* with no calendar, and an absent date is `NULL`. The event views select the
|
|
40
|
+
* key of the page's calendar.
|
|
41
|
+
*
|
|
28
42
|
* @module
|
|
29
43
|
*/
|
|
30
44
|
|
|
31
45
|
import { ownDocumentSystem, renderAddress } from "./address.mjs";
|
|
46
|
+
import { printEventDateIn } from "./event-fields.mjs";
|
|
32
47
|
import { EVENT_ENTRY_KEYS } from "./note-event-terms.mjs";
|
|
48
|
+
import { calendarAddress } from "./reckoning-markers.mjs";
|
|
33
49
|
|
|
34
50
|
/** The columns a row carries besides the event's own keys, before them. */
|
|
35
51
|
const IDENTITY_COLUMNS = Object.freeze(["note", "address"]);
|
|
@@ -37,11 +53,19 @@ const IDENTITY_COLUMNS = Object.freeze(["note", "address"]);
|
|
|
37
53
|
/** The sort keys a row carries, after the event's own keys. */
|
|
38
54
|
export const EVENT_SORT_COLUMNS = Object.freeze(["whenSort", "whenYear", "untilSort", "untilYear"]);
|
|
39
55
|
|
|
56
|
+
/** The printed dates a row carries, after the sort keys. */
|
|
57
|
+
export const EVENT_TEXT_COLUMNS = Object.freeze(["whenText", "untilText"]);
|
|
58
|
+
|
|
59
|
+
/** The dates a row carries in every calendar, after the printed ones. */
|
|
60
|
+
export const EVENT_CALENDAR_COLUMNS = Object.freeze(["whenIn", "untilIn"]);
|
|
61
|
+
|
|
40
62
|
/** Every column of the `events` relation, in order. */
|
|
41
63
|
export const EVENT_COLUMNS = Object.freeze([
|
|
42
64
|
...IDENTITY_COLUMNS,
|
|
43
65
|
...EVENT_ENTRY_KEYS,
|
|
44
66
|
...EVENT_SORT_COLUMNS,
|
|
67
|
+
...EVENT_TEXT_COLUMNS,
|
|
68
|
+
...EVENT_CALENDAR_COLUMNS,
|
|
45
69
|
]);
|
|
46
70
|
|
|
47
71
|
/** The keys whose authored value is a date, printed as the text it was written as. */
|
|
@@ -84,22 +108,62 @@ function axisKeys(date) {
|
|
|
84
108
|
};
|
|
85
109
|
}
|
|
86
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Every calendar a set of records can print an event's date in: each calendar
|
|
113
|
+
* note's canonical Address, and every calendar a note resolves to.
|
|
114
|
+
*
|
|
115
|
+
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
116
|
+
* Address a string.
|
|
117
|
+
* @returns {string[]} The calendars, sorted.
|
|
118
|
+
*/
|
|
119
|
+
export function eventCalendars(records) {
|
|
120
|
+
const calendars = new Set();
|
|
121
|
+
for (const record of records) {
|
|
122
|
+
const resolved = calendarAddress(record?.resolvedCalendar);
|
|
123
|
+
if (resolved) calendars.add(resolved);
|
|
124
|
+
if (
|
|
125
|
+
record?.type === "lore" &&
|
|
126
|
+
String(record.subType ?? "").toLowerCase() === "calendar" &&
|
|
127
|
+
record.package &&
|
|
128
|
+
record.shortcode &&
|
|
129
|
+
!record.documents
|
|
130
|
+
)
|
|
131
|
+
calendars.add(
|
|
132
|
+
renderAddress({
|
|
133
|
+
package: String(record.package),
|
|
134
|
+
system: "note",
|
|
135
|
+
type: "lore",
|
|
136
|
+
shortcode: String(record.shortcode),
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return [...calendars].sort();
|
|
141
|
+
}
|
|
142
|
+
|
|
87
143
|
/**
|
|
88
144
|
* The `events` rows of a set of index records.
|
|
89
145
|
*
|
|
90
146
|
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
91
147
|
* Address a string, already narrowed to the audience being published to.
|
|
148
|
+
* @param {object} [dates] - The reckoning context the dates are printed in.
|
|
149
|
+
* @param {readonly string[]} [calendars] - The calendars `whenIn` and
|
|
150
|
+
* `untilIn` are keyed by: {@link eventCalendars} of every record of the
|
|
151
|
+
* corpus, whatever the audience, so every surface's relation has the same
|
|
152
|
+
* keys. Absent, those of `records`.
|
|
92
153
|
* @returns {object[]} One row per event entry, in record then entry order.
|
|
93
154
|
*/
|
|
94
|
-
export function eventRows(records) {
|
|
155
|
+
export function eventRows(records, dates, calendars) {
|
|
95
156
|
const rows = [];
|
|
96
|
-
|
|
157
|
+
const all = [...records];
|
|
158
|
+
const keys = calendars ?? eventCalendars(all);
|
|
159
|
+
for (const record of all) {
|
|
97
160
|
const events = record?.data?.events;
|
|
98
161
|
if (!Array.isArray(events) || !events.length) continue;
|
|
99
162
|
const note = noteAddressOf(record);
|
|
100
163
|
if (!note) continue;
|
|
101
164
|
const resolved =
|
|
102
165
|
Array.isArray(record.resolvedDates?.events) ? record.resolvedDates.events : [];
|
|
166
|
+
const calendar = calendarAddress(record.resolvedCalendar);
|
|
103
167
|
events.forEach((entry, position) => {
|
|
104
168
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
|
|
105
169
|
const row = {
|
|
@@ -119,6 +183,22 @@ export function eventRows(records) {
|
|
|
119
183
|
row.whenYear = when.year;
|
|
120
184
|
row.untilSort = until.sort;
|
|
121
185
|
row.untilYear = until.year;
|
|
186
|
+
for (const key of DATE_KEYS) {
|
|
187
|
+
const date = resolved[position]?.[key];
|
|
188
|
+
row[`${key}Text`] =
|
|
189
|
+
row[key] === null ? null : printEventDateIn(row[key], date, dates, calendar);
|
|
190
|
+
row[`${key}In`] =
|
|
191
|
+
keys.length ?
|
|
192
|
+
Object.fromEntries(
|
|
193
|
+
keys.map((each) => [
|
|
194
|
+
each,
|
|
195
|
+
row[key] === null ?
|
|
196
|
+
null
|
|
197
|
+
: printEventDateIn(row[key], date, dates, each),
|
|
198
|
+
]),
|
|
199
|
+
)
|
|
200
|
+
: null;
|
|
201
|
+
}
|
|
122
202
|
rows.push(row);
|
|
123
203
|
});
|
|
124
204
|
}
|
package/engine/event-views.mjs
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
import { ownDocumentSystem, renderAddress } from "./address.mjs";
|
|
43
43
|
import { authoredSection, sectionHeading } from "./generated-sections.mjs";
|
|
44
|
+
import { calendarAddress } from "./reckoning-markers.mjs";
|
|
44
45
|
|
|
45
46
|
/** A literal SQL string. */
|
|
46
47
|
const literal = (text) => `'${String(text).replace(/'/g, "''")}'`;
|
|
@@ -48,6 +49,16 @@ const literal = (text) => `'${String(text).replace(/'/g, "''")}'`;
|
|
|
48
49
|
/** A list of SQL string literals, for an `IN (…)`. */
|
|
49
50
|
const literals = (texts) => texts.map(literal).join(", ");
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The `When` column of a view on a page whose dates print in `calendar`: the
|
|
54
|
+
* event's date in that calendar, or — on a page with no calendar — in its
|
|
55
|
+
* owning note's.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} [calendar] - The page's calendar, a canonical Address.
|
|
58
|
+
* @returns {string} The column's expression.
|
|
59
|
+
*/
|
|
60
|
+
const whenColumn = (calendar) => (calendar ? `e.whenIn[${literal(calendar)}]` : "e.whenText");
|
|
61
|
+
|
|
51
62
|
/** The order every view sorts its rows in: by date, undated last. */
|
|
52
63
|
const BY_DATE = "ORDER BY e.whenSort NULLS LAST, e.address";
|
|
53
64
|
|
|
@@ -65,8 +76,9 @@ const BY_DATE = "ORDER BY e.whenSort NULLS LAST, e.address";
|
|
|
65
76
|
*
|
|
66
77
|
* @type {ReadonlyArray<Readonly<{slug: string, heading: string,
|
|
67
78
|
* applies: (record: object) => boolean,
|
|
68
|
-
* query: (self: string[], shape: CorpusShape) => {fence: string, own?: string}|null,
|
|
69
|
-
* owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string
|
|
79
|
+
* query: (self: string[], shape: CorpusShape, columns?: {felt?: boolean, calendar?: string}) => {fence: string, own?: string}|null,
|
|
80
|
+
* owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>,
|
|
81
|
+
* felt?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>}>>}
|
|
70
82
|
*/
|
|
71
83
|
export const EVENT_VIEWS = Object.freeze([
|
|
72
84
|
Object.freeze({
|
|
@@ -75,6 +87,7 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
75
87
|
applies: (record) => record.type === "place",
|
|
76
88
|
query: chronologyQuery,
|
|
77
89
|
owners: chronologyOwners,
|
|
90
|
+
felt: chronologyFeltOwners,
|
|
78
91
|
}),
|
|
79
92
|
Object.freeze({
|
|
80
93
|
slug: "events",
|
|
@@ -85,14 +98,14 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
85
98
|
(record.type === "lore" && record.subType === "culture"),
|
|
86
99
|
owners: (db, selves, shape) =>
|
|
87
100
|
namedOwners(db, selves, "SELECT unnest(who).ref AS ref FROM events", shape.who),
|
|
88
|
-
query: (self, shape) => {
|
|
101
|
+
query: (self, shape, columns = {}) => {
|
|
89
102
|
if (!shape.who) return null;
|
|
90
103
|
const named = `list_filter(e.who, w -> w.ref IN (${literals(self)}))`;
|
|
91
104
|
return {
|
|
92
105
|
fence: [
|
|
93
106
|
"SELECT e.note AS _ref,",
|
|
107
|
+
` ${whenColumn(columns.calendar)} AS "When",`,
|
|
94
108
|
' e.summary AS "What happened",',
|
|
95
|
-
' e."when" AS "When",',
|
|
96
109
|
` array_to_string(list_transform(${named}, w -> w.role), ', ') AS "Role"`,
|
|
97
110
|
"FROM events e",
|
|
98
111
|
`WHERE len(${named}) > 0`,
|
|
@@ -115,13 +128,13 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
115
128
|
'SELECT account."by" AS ref FROM (SELECT unnest(accounts) AS account FROM events)',
|
|
116
129
|
shape.accounts,
|
|
117
130
|
),
|
|
118
|
-
query: (self, shape) => {
|
|
131
|
+
query: (self, shape, columns = {}) => {
|
|
119
132
|
if (!shape.accounts) return null;
|
|
120
133
|
return {
|
|
121
134
|
fence: [
|
|
122
135
|
"SELECT e.note AS _ref,",
|
|
136
|
+
` ${whenColumn(columns.calendar)} AS "When",`,
|
|
123
137
|
' e.summary AS "Event",',
|
|
124
|
-
' e."when" AS "When",',
|
|
125
138
|
' e.account.says AS "What they say",',
|
|
126
139
|
' e.account.agrees AS "Agrees"',
|
|
127
140
|
"FROM (SELECT *, unnest(accounts) AS account FROM events) e",
|
|
@@ -143,7 +156,7 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
143
156
|
"FROM (SELECT unnest(follows) AS edge FROM events)",
|
|
144
157
|
shape.follows,
|
|
145
158
|
),
|
|
146
|
-
query: (self, shape) => {
|
|
159
|
+
query: (self, shape, columns = {}) => {
|
|
147
160
|
if (!shape.follows) return null;
|
|
148
161
|
const edges =
|
|
149
162
|
`list_filter(e.follows, f -> split_part(f.event, '#', 1) ` +
|
|
@@ -151,8 +164,8 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
151
164
|
return {
|
|
152
165
|
fence: [
|
|
153
166
|
"SELECT e.note AS _ref,",
|
|
167
|
+
` ${whenColumn(columns.calendar)} AS "When",`,
|
|
154
168
|
' e.summary AS "What followed",',
|
|
155
|
-
' e."when" AS "When",',
|
|
156
169
|
` array_to_string(list_transform(${edges}, f -> f.how), ', ') AS "How"`,
|
|
157
170
|
"FROM events e",
|
|
158
171
|
`WHERE len(${edges}) > 0`,
|
|
@@ -237,6 +250,36 @@ function chronologyOwnRows(shape) {
|
|
|
237
250
|
* @returns {Promise<Set<string>>} The roots with at least one own row.
|
|
238
251
|
*/
|
|
239
252
|
async function chronologyOwners(db, selves, shape) {
|
|
253
|
+
return chronologyRoots(db, selves, shape, chronologyOwnRows(shape));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Which places have a row felt there, asked of every place in one query. A
|
|
258
|
+
* chronology prints its `Felt here` column only for these.
|
|
259
|
+
*
|
|
260
|
+
* @param {object} db - From {@link module:engine/sql-tables.openNotesDatabase}.
|
|
261
|
+
* @param {string[][]} selves - Each place's Address, in each form.
|
|
262
|
+
* @param {CorpusShape} shape - What the corpus states.
|
|
263
|
+
* @returns {Promise<Set<string>>} The roots with at least one felt row.
|
|
264
|
+
*/
|
|
265
|
+
async function chronologyFeltOwners(db, selves, shape) {
|
|
266
|
+
if (!shape.reach) return new Set();
|
|
267
|
+
return chronologyRoots(db, selves, shape, [
|
|
268
|
+
`len(list_filter(${chronologyFelt()}, r -> coalesce(r.how, '') <> '')) > 0`,
|
|
269
|
+
]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The roots among `selves` for which an event satisfies one of `conditions`,
|
|
274
|
+
* over the places at or below each root.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} db - From {@link module:engine/sql-tables.openNotesDatabase}.
|
|
277
|
+
* @param {string[][]} selves - Each place's Address, in each form.
|
|
278
|
+
* @param {CorpusShape} shape - What the corpus states.
|
|
279
|
+
* @param {string[]} conditions - Alternatives over `e` and `here.places`.
|
|
280
|
+
* @returns {Promise<Set<string>>} The roots with at least one such event.
|
|
281
|
+
*/
|
|
282
|
+
async function chronologyRoots(db, selves, shape, conditions) {
|
|
240
283
|
const values = [...new Set(selves.map((self) => self[0]))]
|
|
241
284
|
.map((root) => `(${literal(root)})`)
|
|
242
285
|
.join(", ");
|
|
@@ -258,7 +301,7 @@ async function chronologyOwners(db, selves, shape) {
|
|
|
258
301
|
"here AS (SELECT root, list(place) AS places FROM below GROUP BY root)",
|
|
259
302
|
"SELECT DISTINCT here.root AS root",
|
|
260
303
|
"FROM events e, here",
|
|
261
|
-
`WHERE ${
|
|
304
|
+
`WHERE ${conditions.join(" OR ")}`,
|
|
262
305
|
].join("\n"),
|
|
263
306
|
);
|
|
264
307
|
return new Set(rows.map((row) => String(row.root)));
|
|
@@ -275,10 +318,13 @@ async function chronologyOwners(db, selves, shape) {
|
|
|
275
318
|
*
|
|
276
319
|
* @param {string[]} self - The place's Address, in each form.
|
|
277
320
|
* @param {CorpusShape} shape - What the corpus states.
|
|
321
|
+
* @param {{felt?: boolean, calendar?: string}} [columns] - `felt: false` when
|
|
322
|
+
* no row of this place is felt there, which leaves the `Felt here` column
|
|
323
|
+
* out; `calendar`, the page's calendar, which the `When` column prints in.
|
|
278
324
|
* @returns {{fence: string, own: string}|null} The query, and the one that
|
|
279
325
|
* counts the place's own rows.
|
|
280
326
|
*/
|
|
281
|
-
function chronologyQuery(self, shape) {
|
|
327
|
+
function chronologyQuery(self, shape, columns = {}) {
|
|
282
328
|
if (!shape.locus && !shape.reach) return null;
|
|
283
329
|
const below =
|
|
284
330
|
shape.parents ?
|
|
@@ -298,11 +344,13 @@ function chronologyQuery(self, shape) {
|
|
|
298
344
|
...below,
|
|
299
345
|
"here AS (SELECT list(place) AS places FROM below)",
|
|
300
346
|
"SELECT e.note AS _ref,",
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
347
|
+
` ${whenColumn(columns.calendar)} AS "When",`,
|
|
348
|
+
shape.reach && columns.felt !== false ?
|
|
349
|
+
' e.summary AS "What happened",'
|
|
350
|
+
: ' e.summary AS "What happened"',
|
|
351
|
+
...(shape.reach && columns.felt !== false ?
|
|
352
|
+
[` array_to_string(list_transform(${felt}, r -> r.how), '; ') AS "Felt here"`]
|
|
353
|
+
: []),
|
|
306
354
|
"FROM events e, here",
|
|
307
355
|
];
|
|
308
356
|
const context = shape.depth ? ["e.depth = 'world'"] : [];
|
|
@@ -394,6 +442,7 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
394
442
|
// A view that can say which notes have rows in one query is asked once, over
|
|
395
443
|
// every note it will be given, and the per-note count is skipped.
|
|
396
444
|
const owned = new Map();
|
|
445
|
+
const felt = new Map();
|
|
397
446
|
for (const view of EVENT_VIEWS) {
|
|
398
447
|
if (!view.owners) continue;
|
|
399
448
|
const selves = [];
|
|
@@ -404,7 +453,10 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
404
453
|
const self = selfForms(record);
|
|
405
454
|
if (view.query(self, shape)) selves.push(self);
|
|
406
455
|
}
|
|
407
|
-
if (selves.length)
|
|
456
|
+
if (selves.length) {
|
|
457
|
+
owned.set(view.slug, await view.owners(db, selves, shape));
|
|
458
|
+
if (view.felt) felt.set(view.slug, await view.felt(db, selves, shape));
|
|
459
|
+
}
|
|
408
460
|
}
|
|
409
461
|
for (const record of all) {
|
|
410
462
|
if (!record?.address || record.documents || !record.shortcode) continue;
|
|
@@ -413,7 +465,12 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
413
465
|
const sections = new Map();
|
|
414
466
|
for (const view of EVENT_VIEWS) {
|
|
415
467
|
if (!view.applies(record) || authoredSection(record, view.slug)) continue;
|
|
416
|
-
const query = view.query(self, shape
|
|
468
|
+
const query = view.query(self, shape, {
|
|
469
|
+
...(felt.has(view.slug) ? { felt: felt.get(view.slug).has(self[0]) } : {}),
|
|
470
|
+
...(calendarAddress(record.resolvedCalendar) ?
|
|
471
|
+
{ calendar: calendarAddress(record.resolvedCalendar) }
|
|
472
|
+
: {}),
|
|
473
|
+
});
|
|
417
474
|
if (!query) continue;
|
|
418
475
|
const has =
|
|
419
476
|
owned.has(view.slug) ?
|
|
@@ -1285,6 +1285,17 @@ export function lintNote(
|
|
|
1285
1285
|
});
|
|
1286
1286
|
}
|
|
1287
1287
|
}
|
|
1288
|
+
if (
|
|
1289
|
+
Object.hasOwn(name, "sort") &&
|
|
1290
|
+
(typeof name.sort !== "string" || !name.sort.trim())
|
|
1291
|
+
) {
|
|
1292
|
+
findings.push({
|
|
1293
|
+
file: note.file,
|
|
1294
|
+
...nameAt(["sort"]),
|
|
1295
|
+
severity: "error",
|
|
1296
|
+
message: "`name.sort` must be a nonempty string",
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1288
1299
|
for (const key of CHARACTER_NAME_KEYS) {
|
|
1289
1300
|
if (!Object.hasOwn(name, key) || !allowed.has(key)) continue;
|
|
1290
1301
|
if (typeof name[key] === "string" && name[key].trim()) continue;
|
|
@@ -41,6 +41,13 @@
|
|
|
41
41
|
* @module
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The anchor of the Chronology section, which the book sets across the page.
|
|
46
|
+
*
|
|
47
|
+
* @type {string}
|
|
48
|
+
*/
|
|
49
|
+
export const CHRONOLOGY_SLUG = "chronology";
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
52
|
* Every generated section, in the order it follows the author's text: where a
|
|
46
53
|
* place sits and who governs it, then what happened, then what was written
|
|
@@ -53,7 +60,7 @@ export const GENERATED_SECTIONS = Object.freeze(
|
|
|
53
60
|
{ slug: "within", heading: "Within" },
|
|
54
61
|
{ slug: "governedby", heading: "Governed by" },
|
|
55
62
|
{ slug: "governedplaces", heading: "Governed places" },
|
|
56
|
-
{ slug:
|
|
63
|
+
{ slug: CHRONOLOGY_SLUG, heading: "Chronology" },
|
|
57
64
|
{ slug: "events", heading: "Events" },
|
|
58
65
|
{ slug: "accounts", heading: "Accounts" },
|
|
59
66
|
{ slug: "followed", heading: "What followed" },
|