@heroiclands/package-build 23.12.5 → 23.12.7

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/assets/styles/content-base.css +25 -0
  3. package/bin/content-commands.mjs +264 -3
  4. package/bin/package-build.mjs +25 -0
  5. package/content-config.mjs +98 -0
  6. package/docs/api.md +18 -14
  7. package/docs/authoring/dates-and-calendars.md +29 -1
  8. package/docs/authoring/links-and-markup.md +442 -50
  9. package/docs/commands.md +127 -0
  10. package/docs/configuration.md +90 -3
  11. package/docs/content-format.md +3 -1
  12. package/docs/guides/book.md +11 -0
  13. package/docs/guides/module-project.md +17 -1
  14. package/docs/guides/site.md +19 -0
  15. package/docs/reference/format-details.md +434 -23
  16. package/docs/reference/note-types.md +45 -21
  17. package/engine/base-compiler.mjs +2 -0
  18. package/engine/bundles.mjs +7 -2
  19. package/engine/compile-corpus.mjs +3 -1
  20. package/engine/content-charset.mjs +21 -2
  21. package/engine/content-format.yaml +1 -2
  22. package/engine/content-index.mjs +33 -2
  23. package/engine/content-links.mjs +3 -0
  24. package/engine/content-poetry.mjs +22 -4
  25. package/engine/content-scripts.mjs +1219 -0
  26. package/engine/content-session.mjs +2 -0
  27. package/engine/data-keys.mjs +5 -1
  28. package/engine/dictionary-layout.mjs +18 -12
  29. package/engine/dictionary-rows.mjs +11 -2
  30. package/engine/event-rows.mjs +63 -18
  31. package/engine/event-views.mjs +28 -15
  32. package/engine/helpers.mjs +8 -2
  33. package/engine/index-records.mjs +2 -0
  34. package/engine/infobox.mjs +4 -8
  35. package/engine/journals.mjs +14 -3
  36. package/engine/language-data.mjs +137 -1
  37. package/engine/language-tag.mjs +103 -0
  38. package/engine/language-terms.mjs +16 -4
  39. package/engine/map-lexicon.mjs +762 -0
  40. package/engine/markdown-expressions.mjs +72 -17
  41. package/engine/markup-validation.mjs +17 -3
  42. package/engine/names.mjs +1 -1
  43. package/engine/note-calendars.mjs +314 -0
  44. package/engine/note-vocabulary.mjs +124 -16
  45. package/engine/pdf-build.mjs +28 -10
  46. package/engine/pdf-render.mjs +116 -11
  47. package/engine/reckoning-markers.mjs +63 -3
  48. package/engine/run-rows.mjs +119 -0
  49. package/engine/run-words.mjs +266 -0
  50. package/engine/scenes.mjs +1 -0
  51. package/engine/script-font-delivery.mjs +321 -0
  52. package/engine/script-fonts.mjs +172 -0
  53. package/engine/site-build.mjs +10 -3
  54. package/engine/site-config.mjs +5 -0
  55. package/engine/site-root.mjs +19 -5
  56. package/engine/sql-tables.mjs +120 -4
  57. package/engine/text-maps.mjs +607 -0
  58. package/engine/verb-pattern.mjs +99 -0
  59. package/engine/word-pattern.mjs +27 -0
  60. package/engine/word-rows.mjs +8 -2
  61. package/engine/word-terms.mjs +44 -0
  62. package/hugo-theme/layouts/_default/baseof.html +3 -0
  63. package/hugo-theme/static/css/style.css +10 -0
  64. package/manifest.mjs +9 -1
  65. package/package.json +1 -1
  66. package/types/content-config.d.mts +38 -0
  67. package/types/engine/compile-corpus.d.mts +2 -1
  68. package/types/engine/content-index.d.mts +17 -3
  69. package/types/engine/content-poetry.d.mts +7 -0
  70. package/types/engine/content-scripts.d.mts +226 -0
  71. package/types/engine/data-keys.d.mts +2 -2
  72. package/types/engine/dictionary-rows.d.mts +15 -0
  73. package/types/engine/event-rows.d.mts +16 -1
  74. package/types/engine/event-views.d.mts +2 -1
  75. package/types/engine/helpers.d.mts +5 -1
  76. package/types/engine/journals.d.mts +7 -2
  77. package/types/engine/language-data.d.mts +2 -0
  78. package/types/engine/language-tag.d.mts +8 -0
  79. package/types/engine/language-terms.d.mts +2 -1
  80. package/types/engine/map-lexicon.d.mts +274 -0
  81. package/types/engine/markup-validation.d.mts +7 -2
  82. package/types/engine/note-calendars.d.mts +60 -0
  83. package/types/engine/pdf-build.d.mts +26 -3
  84. package/types/engine/pdf-render.d.mts +17 -2
  85. package/types/engine/reckoning-markers.d.mts +32 -1
  86. package/types/engine/run-rows.d.mts +89 -0
  87. package/types/engine/run-words.d.mts +86 -0
  88. package/types/engine/script-font-delivery.d.mts +134 -0
  89. package/types/engine/script-fonts.d.mts +168 -0
  90. package/types/engine/site-build.d.mts +2 -1
  91. package/types/engine/site-root.d.mts +11 -3
  92. package/types/engine/text-maps.d.mts +175 -0
  93. package/types/engine/verb-pattern.d.mts +65 -0
  94. package/types/engine/word-pattern.d.mts +13 -0
  95. package/types/engine/word-terms.d.mts +6 -0
@@ -21,6 +21,7 @@ import * as sql from "./sql-tables.mjs";
21
21
  import { captureContentSources } from "./content-sources.mjs";
22
22
  import { buildLinkIndex } from "./content-links.mjs";
23
23
  import { buildContentLinkIndex } from "./helpers.mjs";
24
+ import { runContext } from "./content-scripts.mjs";
24
25
  import { loadForeignIndexes } from "./metadata-index.mjs";
25
26
  import { loadPackConfig } from "./pack-config.mjs";
26
27
  import { routerFor } from "./pack-router.mjs";
@@ -199,6 +200,7 @@ export async function createContentSession({
199
200
  return memo("compile", async () => {
200
201
  const sqlTables = await session.sqlTables("all");
201
202
  return {
203
+ runs: runContext(config, records),
202
204
  records,
203
205
  parsed,
204
206
  problems,
@@ -62,7 +62,7 @@ import { nearest } from "./near-miss.mjs";
62
62
  * @property {string} name - The key as an author writes it.
63
63
  * @property {string} [kind] - Its value's shape: `string`, `date`, `number`,
64
64
  * `integer`, `boolean`, `address`, `list`, `map`, `scalar-or-map`,
65
- * `string-or-list`, or `string-list-or-map` — see {@link fitsKind}. Absent, the check
65
+ * `string-or-list`, `string-list-or-map` or `list-or-map` — see {@link fitsKind}. Absent, the check
66
66
  * makes no claim about the value.
67
67
  * @property {readonly string[]} [allowed] - The closed set a present value is
68
68
  * one of, where the key takes a fixed word.
@@ -156,6 +156,10 @@ export function fitsKind(value, kind) {
156
156
  // described by `values`.
157
157
  case "string-list-or-map":
158
158
  return fitsKind(value, "string-or-list") || fitsKind(value, "map");
159
+ // A list described by `entries`, or a map with the keys `fields` names:
160
+ // a mapping row is `[from, to]` or `{from, to, before?, at?}`.
161
+ case "list-or-map":
162
+ return Array.isArray(value) || fitsKind(value, "map");
159
163
  default:
160
164
  return true;
161
165
  }
@@ -48,7 +48,7 @@
48
48
 
49
49
  import { parseAddress, readCanonicalKey, renderAddress, splitAnchor } from "./address.mjs";
50
50
  import { dictionaryEntryAnchor, dictionaryLetter } from "./dictionary-rows.mjs";
51
- import { VERB_PERSON_FIELDS } from "./language-terms.mjs";
51
+ import { verbPattern } from "./verb-pattern.mjs";
52
52
  import {
53
53
  WORD_CLASS_BASES,
54
54
  WORD_CLASS_MODIFIERS,
@@ -191,16 +191,22 @@ function verbTables(rows, opts) {
191
191
  patterns.set(opts.pageLanguage ?? "", { verbs: opts.verbs });
192
192
  const tables = [];
193
193
  for (const [language, { verbs, label }] of [...patterns].sort(([a], [b]) => compare(a, b))) {
194
- let endings;
195
- if (verbs.invariant === true) endings = [["All persons", "unchanged"]];
196
- else if (
197
- VERB_PERSON_FIELDS.every((person) => typeof verbs.persons?.[person.name] === "string")
198
- )
199
- endings = VERB_PERSON_FIELDS.map((person) => [
200
- person.label,
201
- verbs.persons[person.name] ? `-${verbs.persons[person.name]}` : "unchanged",
202
- ]);
203
- else continue;
194
+ const pattern = verbPattern(verbs);
195
+ if (!pattern) continue;
196
+ const replaced = pattern.replaces === undefined ? undefined : `-${pattern.replaces}`;
197
+ const endings =
198
+ pattern.invariant ?
199
+ [["All persons", "unchanged"]]
200
+ : pattern.endings.map(({ label, ending }) => [
201
+ label,
202
+ ending ? `-${ending}`
203
+ : replaced ? `${replaced} dropped`
204
+ : "unchanged",
205
+ ]);
206
+ const heading =
207
+ replaced && !pattern.invariant ?
208
+ `Ending in place of a closing ${replaced}`
209
+ : "Ending added to citation form";
204
210
  const title =
205
211
  patterns.size > 1 ?
206
212
  `**Verbs (${String(opts.labelOf?.(language) ?? label ?? language).replace(/[\r\n]+/g, " ")})**`
@@ -209,7 +215,7 @@ function verbTables(rows, opts) {
209
215
  [
210
216
  title,
211
217
  "",
212
- "| Person | Ending added to citation form |",
218
+ `| Person | ${verbCell(heading)} |`,
213
219
  "| --- | --- |",
214
220
  ...endings.map((ending) => `| ${ending.map(verbCell).join(" | ")} |`),
215
221
  ].join("\n"),
@@ -130,8 +130,17 @@ const isSingleWord = (title) => !/[\s\-‐‑]/u.test(title);
130
130
  /** Whether a glossary entry states a definition. */
131
131
  const defines = (entry) => typeof entry.definition === "string" && entry.definition.trim() !== "";
132
132
 
133
- /** A noun's stated plural, or the first literal ending replacement that fits. */
134
- function pluralOf(word, grammar) {
133
+ /**
134
+ * A noun's stated plural, or the first literal ending replacement that fits.
135
+ *
136
+ * @param {{form: string, class?: unknown, plural?: unknown}} word - A word of
137
+ * `data.words`.
138
+ * @param {Record<string, any>|undefined} grammar - Its language's
139
+ * `data.grammar`.
140
+ * @returns {string|null} The plural, or `null` for a word that is no noun or
141
+ * that no rule fits.
142
+ */
143
+ export function pluralOf(word, grammar) {
135
144
  if (parseWordClass(word.class).base !== "n") return null;
136
145
  if (typeof word.plural === "string" && word.plural) return word.plural;
137
146
  for (const rule of Array.isArray(grammar?.plural) ? grammar.plural : []) {
@@ -25,10 +25,19 @@
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 sees them**: printed
29
- * as the `dateformat` helper prints a date, in the calendar the owning note
30
- * names in `data.calendar`, else the one the world note names, else in the era
31
- * the date itself names. A date that does not resolve keeps its authored text.
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.
32
41
  *
33
42
  * @module
34
43
  */
@@ -36,6 +45,7 @@
36
45
  import { ownDocumentSystem, renderAddress } from "./address.mjs";
37
46
  import { printEventDateIn } from "./event-fields.mjs";
38
47
  import { EVENT_ENTRY_KEYS } from "./note-event-terms.mjs";
48
+ import { calendarAddress } from "./reckoning-markers.mjs";
39
49
 
40
50
  /** The columns a row carries besides the event's own keys, before them. */
41
51
  const IDENTITY_COLUMNS = Object.freeze(["note", "address"]);
@@ -46,12 +56,16 @@ export const EVENT_SORT_COLUMNS = Object.freeze(["whenSort", "whenYear", "untilS
46
56
  /** The printed dates a row carries, after the sort keys. */
47
57
  export const EVENT_TEXT_COLUMNS = Object.freeze(["whenText", "untilText"]);
48
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
+
49
62
  /** Every column of the `events` relation, in order. */
50
63
  export const EVENT_COLUMNS = Object.freeze([
51
64
  ...IDENTITY_COLUMNS,
52
65
  ...EVENT_ENTRY_KEYS,
53
66
  ...EVENT_SORT_COLUMNS,
54
67
  ...EVENT_TEXT_COLUMNS,
68
+ ...EVENT_CALENDAR_COLUMNS,
55
69
  ]);
56
70
 
57
71
  /** The keys whose authored value is a date, printed as the text it was written as. */
@@ -95,18 +109,35 @@ function axisKeys(date) {
95
109
  }
96
110
 
97
111
  /**
98
- * The calendar the world note names, which displays every date whose own note
99
- * names none.
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.
100
114
  *
101
- * @param {Record<string, any>[]} records - Index records.
102
- * @returns {string|undefined} The calendar's Address or shortcode.
115
+ * @param {Iterable<Record<string, any>>} records - Index records, every
116
+ * Address a string.
117
+ * @returns {string[]} The calendars, sorted.
103
118
  */
104
- function worldCalendar(records) {
119
+ export function eventCalendars(records) {
120
+ const calendars = new Set();
105
121
  for (const record of records) {
106
- if (record?.type === "place" && record.data?.year?.days && record.data.calendar)
107
- return String(record.data.calendar);
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
+ );
108
139
  }
109
- return undefined;
140
+ return [...calendars].sort();
110
141
  }
111
142
 
112
143
  /**
@@ -115,12 +146,16 @@ function worldCalendar(records) {
115
146
  * @param {Iterable<Record<string, any>>} records - Index records, every
116
147
  * Address a string, already narrowed to the audience being published to.
117
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`.
118
153
  * @returns {object[]} One row per event entry, in record then entry order.
119
154
  */
120
- export function eventRows(records, dates) {
155
+ export function eventRows(records, dates, calendars) {
121
156
  const rows = [];
122
157
  const all = [...records];
123
- const world = worldCalendar(all);
158
+ const keys = calendars ?? eventCalendars(all);
124
159
  for (const record of all) {
125
160
  const events = record?.data?.events;
126
161
  if (!Array.isArray(events) || !events.length) continue;
@@ -128,7 +163,7 @@ export function eventRows(records, dates) {
128
163
  if (!note) continue;
129
164
  const resolved =
130
165
  Array.isArray(record.resolvedDates?.events) ? record.resolvedDates.events : [];
131
- const calendar = record.data?.calendar ? String(record.data.calendar) : world;
166
+ const calendar = calendarAddress(record.resolvedCalendar);
132
167
  events.forEach((entry, position) => {
133
168
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
134
169
  const row = {
@@ -149,10 +184,20 @@ export function eventRows(records, dates) {
149
184
  row.untilSort = until.sort;
150
185
  row.untilYear = until.year;
151
186
  for (const key of DATE_KEYS) {
187
+ const date = resolved[position]?.[key];
152
188
  row[`${key}Text`] =
153
- row[key] === null ?
154
- null
155
- : printEventDateIn(row[key], resolved[position]?.[key], dates, calendar);
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;
156
201
  }
157
202
  rows.push(row);
158
203
  });
@@ -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,7 +76,7 @@ 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, columns?: {felt?: boolean}) => {fence: string, own?: string}|null,
79
+ * query: (self: string[], shape: CorpusShape, columns?: {felt?: boolean, calendar?: string}) => {fence: string, own?: string}|null,
69
80
  * owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>,
70
81
  * felt?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>}>>}
71
82
  */
@@ -87,13 +98,13 @@ export const EVENT_VIEWS = Object.freeze([
87
98
  (record.type === "lore" && record.subType === "culture"),
88
99
  owners: (db, selves, shape) =>
89
100
  namedOwners(db, selves, "SELECT unnest(who).ref AS ref FROM events", shape.who),
90
- query: (self, shape) => {
101
+ query: (self, shape, columns = {}) => {
91
102
  if (!shape.who) return null;
92
103
  const named = `list_filter(e.who, w -> w.ref IN (${literals(self)}))`;
93
104
  return {
94
105
  fence: [
95
106
  "SELECT e.note AS _ref,",
96
- ' e.whenText AS "When",',
107
+ ` ${whenColumn(columns.calendar)} AS "When",`,
97
108
  ' e.summary AS "What happened",',
98
109
  ` array_to_string(list_transform(${named}, w -> w.role), ', ') AS "Role"`,
99
110
  "FROM events e",
@@ -117,12 +128,12 @@ export const EVENT_VIEWS = Object.freeze([
117
128
  'SELECT account."by" AS ref FROM (SELECT unnest(accounts) AS account FROM events)',
118
129
  shape.accounts,
119
130
  ),
120
- query: (self, shape) => {
131
+ query: (self, shape, columns = {}) => {
121
132
  if (!shape.accounts) return null;
122
133
  return {
123
134
  fence: [
124
135
  "SELECT e.note AS _ref,",
125
- ' e.whenText AS "When",',
136
+ ` ${whenColumn(columns.calendar)} AS "When",`,
126
137
  ' e.summary AS "Event",',
127
138
  ' e.account.says AS "What they say",',
128
139
  ' e.account.agrees AS "Agrees"',
@@ -145,7 +156,7 @@ export const EVENT_VIEWS = Object.freeze([
145
156
  "FROM (SELECT unnest(follows) AS edge FROM events)",
146
157
  shape.follows,
147
158
  ),
148
- query: (self, shape) => {
159
+ query: (self, shape, columns = {}) => {
149
160
  if (!shape.follows) return null;
150
161
  const edges =
151
162
  `list_filter(e.follows, f -> split_part(f.event, '#', 1) ` +
@@ -153,7 +164,7 @@ export const EVENT_VIEWS = Object.freeze([
153
164
  return {
154
165
  fence: [
155
166
  "SELECT e.note AS _ref,",
156
- ' e.whenText AS "When",',
167
+ ` ${whenColumn(columns.calendar)} AS "When",`,
157
168
  ' e.summary AS "What followed",',
158
169
  ` array_to_string(list_transform(${edges}, f -> f.how), ', ') AS "How"`,
159
170
  "FROM events e",
@@ -307,8 +318,9 @@ async function chronologyRoots(db, selves, shape, conditions) {
307
318
  *
308
319
  * @param {string[]} self - The place's Address, in each form.
309
320
  * @param {CorpusShape} shape - What the corpus states.
310
- * @param {{felt?: boolean}} [columns] - `felt: false` when no row of this
311
- * place is felt there, which leaves the `Felt here` column out.
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.
312
324
  * @returns {{fence: string, own: string}|null} The query, and the one that
313
325
  * counts the place's own rows.
314
326
  */
@@ -332,7 +344,7 @@ function chronologyQuery(self, shape, columns = {}) {
332
344
  ...below,
333
345
  "here AS (SELECT list(place) AS places FROM below)",
334
346
  "SELECT e.note AS _ref,",
335
- ' e.whenText AS "When",',
347
+ ` ${whenColumn(columns.calendar)} AS "When",`,
336
348
  shape.reach && columns.felt !== false ?
337
349
  ' e.summary AS "What happened",'
338
350
  : ' e.summary AS "What happened"',
@@ -453,11 +465,12 @@ export async function eventViewSections(db, records, fileOf, only) {
453
465
  const sections = new Map();
454
466
  for (const view of EVENT_VIEWS) {
455
467
  if (!view.applies(record) || authoredSection(record, view.slug)) continue;
456
- const query = view.query(
457
- self,
458
- shape,
459
- felt.has(view.slug) ? { felt: felt.get(view.slug).has(self[0]) } : undefined,
460
- );
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
+ });
461
474
  if (!query) continue;
462
475
  const has =
463
476
  owned.has(view.slug) ?
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { renderAlerts } from "./content-alerts.mjs";
29
+ import { renderRuns } from "./content-scripts.mjs";
29
30
  import { buildReferenceTargets } from "./reference-targets.mjs";
30
31
  import { standingsDigest } from "./standings.mjs";
31
32
 
@@ -224,10 +225,15 @@ md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
224
225
  * @param {(address: string) => string|undefined} [resolveRole] - From a
225
226
  * picture's address to the role its asset declares — see
226
227
  * {@link module:engine/content-figures.scanFigures}.
228
+ * @param {import("./content-scripts.mjs").RunContext} [runs] - The package's
229
+ * languages, faces and maps. Given, text marked with `lang=`, `font=` and
230
+ * `map=` is rewritten first, so it is tagged for its language, mapped and
231
+ * set in its font.
227
232
  * @returns {string} The rendered HTML.
228
233
  */
229
- export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, resolveRole) {
230
- const figured = renderFigureBlocks(body, (block) => md.render(block), figures, {
234
+ export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, resolveRole, runs) {
235
+ const marked = runs ? renderRuns(body, runs, undefined, { target: "foundry" }).markdown : body;
236
+ const figured = renderFigureBlocks(marked, (block) => md.render(block), figures, {
231
237
  resolveRole,
232
238
  });
233
239
  const blocks = renderBlocks(figured.markdown, "foundry");
@@ -62,8 +62,10 @@ export const DERIVED_KEYS = Object.freeze([
62
62
  "documents",
63
63
  "asset",
64
64
  "resolvedDates",
65
+ "resolvedCalendar",
65
66
  "citations",
66
67
  "names",
68
+ "runs",
67
69
  ]);
68
70
 
69
71
  /**
@@ -128,6 +128,7 @@ import {
128
128
  standingLead,
129
129
  } from "./standings.mjs";
130
130
  import { AddressEntries } from "./address-values.mjs";
131
+ import { noteCalendar } from "./note-calendars.mjs";
131
132
 
132
133
  /**
133
134
  * How a section arranges what it holds.
@@ -1066,15 +1067,10 @@ function noteBox(
1066
1067
  : parsed?.qualifier ? dates.eras?.get(parsed.qualifier)
1067
1068
  : null;
1068
1069
  let printable = null;
1069
- if (parsed?.known && data.calendar) {
1070
+ const calendar = noteCalendar(fm, dates);
1071
+ if (parsed?.known && calendar) {
1070
1072
  try {
1071
- printable = formatDateInCalendar(
1072
- parsed,
1073
- isAddressTuple(data.calendar) ?
1074
- renderAddress(data.calendar)
1075
- : data.calendar,
1076
- dates,
1077
- );
1073
+ printable = formatDateInCalendar(parsed, calendar, dates);
1078
1074
  } catch {
1079
1075
  // The calendar reference is checked at the authored field.
1080
1076
  }
@@ -485,10 +485,12 @@ function soleFigurePicture(figure, page) {
485
485
  * @param {Map<string, number>} footnoteNumbers - Shared across the note.
486
486
  * @param {(address: string) => string|undefined} [resolveRole] - See
487
487
  * {@link splitPages}.
488
+ * @param {import("./content-scripts.mjs").RunContext} [runs] - The package's
489
+ * languages, faces and maps — see {@link module:engine/helpers.renderFoundryMarkdown}.
488
490
  * @returns {{name: string, type: "image", src: string, image: {caption?: string}}
489
491
  * |{name: string, type: "text", text: {format: number, content: string}}}
490
492
  */
491
- function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRole) {
493
+ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRole, runs) {
492
494
  const picture = soleFigurePicture(figure, page);
493
495
  if (picture && !captionCarriesMarkup(figure.caption)) {
494
496
  let src = picture;
@@ -515,6 +517,7 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
515
517
  footnoteNumbers,
516
518
  pageId,
517
519
  resolveRole,
520
+ runs,
518
521
  );
519
522
  return {
520
523
  name: figure.label || captionPageName(figure.caption),
@@ -547,13 +550,15 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
547
550
  * carries its own figure already, from {@link splitPages}.
548
551
  * @param {(address: string) => string|undefined} [resolveRole] - See
549
552
  * {@link splitPages}.
553
+ * @param {import("./content-scripts.mjs").RunContext} [runs] - The package's
554
+ * languages, faces and maps — see {@link module:engine/helpers.renderFoundryMarkdown}.
550
555
  * @returns {Array<{_id: string, name: string, type: string,
551
556
  * title: {show: boolean, level: number},
552
557
  * text: {format: number, content: string}, _key: string,
553
558
  * ownership?: {default: number}}>} The page documents, in order.
554
559
  * @throws {Error} When the note has no content at all, or repeats an anchor.
555
560
  */
556
- export function buildPages(rawPages, entryId, noteName, figures, resolveRole) {
561
+ export function buildPages(rawPages, entryId, noteName, figures, resolveRole, runs) {
557
562
  if (rawPages.length === 0) {
558
563
  throw new Error(
559
564
  `note "${noteName}" has no Introduction content and no H1 headings — nothing to compile`,
@@ -573,7 +578,7 @@ export function buildPages(rawPages, entryId, noteName, figures, resolveRole) {
573
578
  const withheld = (page.classes ?? []).includes(WITHHELD_CLASS);
574
579
  const fields =
575
580
  page.figure ?
576
- buildFigurePageFields(page.figure, page, pageId, footnoteNumbers, resolveRole)
581
+ buildFigurePageFields(page.figure, page, pageId, footnoteNumbers, resolveRole, runs)
577
582
  : {
578
583
  name: page.name,
579
584
  type: "text",
@@ -587,6 +592,7 @@ export function buildPages(rawPages, entryId, noteName, figures, resolveRole) {
587
592
  footnoteNumbers,
588
593
  pageId,
589
594
  resolveRole,
595
+ runs,
590
596
  )
591
597
  : "",
592
598
  },
@@ -638,6 +644,8 @@ export function buildPages(rawPages, entryId, noteName, figures, resolveRole) {
638
644
  * name reads `Map 1` rather than `Figure 1` where it is due — see
639
645
  * {@link module:engine/content-figures.scanFigures}.
640
646
  * @param {import("./content-session.mjs").ContentSyntax} [params.syntax] - Session scanner projections.
647
+ * @param {import("./content-scripts.mjs").RunContext} [params.runs] - The package's
648
+ * languages, faces and maps — see {@link module:engine/helpers.renderFoundryMarkdown}.
641
649
  * @returns {object} The JournalEntry document, keyed for the pack.
642
650
  */
643
651
  export function buildJournalEntry({
@@ -651,6 +659,7 @@ export function buildJournalEntry({
651
659
  notice = "",
652
660
  resolveRole,
653
661
  syntax,
662
+ runs,
654
663
  }) {
655
664
  const rawPages = splitPages(markdown, leadName, resolveRole, syntax);
656
665
  const pages = buildPages(
@@ -659,6 +668,7 @@ export function buildJournalEntry({
659
668
  name,
660
669
  scanFigures(markdown, { resolveRole }).figures,
661
670
  resolveRole,
671
+ runs,
662
672
  );
663
673
  for (const box of infoboxes) {
664
674
  const pageId = makeId("journal-infobox-page", `${id}:${box.id}`);
@@ -844,6 +854,7 @@ export class Journals extends BasePackCompiler {
844
854
  .filter(Boolean)
845
855
  .join("\n\n"),
846
856
  resolveRole: (pathname) => roles.get(pathname),
857
+ runs: this.corpus?.runs,
847
858
  infoboxes: boxes.map((box) => ({
848
859
  id: box.id,
849
860
  name: `${