@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
@@ -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.id` | string | A fixed document identity when the derived identity is unsuitable. |
39
- | `data.pack` | string | The shared compendium route, with a system block overriding it. |
40
- | `data.packFolder` | an Address, or a map of Addresses keyed by pack | The shared compendium folder Address, with a system block overriding it. |
41
- | `data.harnworld` | `{ realm?, ritual? }` | HârnWorld source details shared by every system — `realm` and `ritual`. |
42
- | `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. |
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. |
@@ -507,6 +507,10 @@ These fields are accepted by every note type. A field's value can still be irrel
507
507
  | `data.words[].tones` | a string | no | A tone pattern, such as `LHL`. |
508
508
  | `data.words[].root` | a string | no | A consonantal skeleton, such as `d-l-k`. |
509
509
  | `data.words[].frame` | a frame of the language's `sounds.frames` | no | The frame a root is set in, named in the language's `sounds.frames`. |
510
+ | `data.words[].native` | a list of `{ map, font?, text }` | no | The word's native form under a map, stated only where the map's rows spell it differently. Its derived forms are spelled by the rows. |
511
+ | `data.words[].native[].map` | a mapping note's `data.map.name` | yes | The map the native form is written for. |
512
+ | `data.words[].native[].font` | a face the map lists | no | The one face the native form is for; unstated, it is for every face the map lists, and a form stated for a face wins over it there. |
513
+ | `data.words[].native[].text` | the word in the script, as the map's characters | yes | The word as the script writes it, where the map's rows would write it otherwise. |
510
514
  | `data.affixes[].form` | the affix as written, such as `-rith` | no | The affix as a word's `built` names it; absent on a zero affix. |
511
515
  | `data.affixes[].zero` | `true` | no | The affix is written as nothing, such as a case with no ending; it states no `form`, prints as `∅`, and no `built` piece names it. |
512
516
  | `data.affixes[].kind` | one of case, derivation, tense, aspect, mood, voice, name, lineage | no | What the affix makes. |
@@ -523,15 +527,16 @@ These fields are accepted by every note type. A field's value can still be irrel
523
527
  | `data.grammar.plural` | list of `{ ending, becomes }` | no | Default noun plurals, replacing the first matching literal ending in authored order. A word's explicit plural wins; no word is checked against this rule. |
524
528
  | `data.grammar.plural[].ending` | a literal ending string, which may be empty | yes | The bare suffix replaced by this rule; empty matches every noun. |
525
529
  | `data.grammar.plural[].becomes` | a literal ending string, which may be empty | yes | The bare replacement; empty removes the matching suffix. |
526
- | `data.grammar.verbs` | a map — `{ invariant: true }` or `{ persons: { I, you, heShe, we, youAll, they } }` | no | One optional regular present pattern, printed once at the dictionary head. |
530
+ | `data.grammar.verbs` | a map — `{ invariant: true }` or `{ persons: { I, you, heShe, we, youAll, they }, replaces? }` | no | One optional regular present pattern, printed once at the dictionary head. |
527
531
  | `data.grammar.verbs.invariant` | true, as the alternative to persons | no | True where verbs keep the citation form for every person; excludes `persons`. |
528
- | `data.grammar.verbs.persons` | a map — `{ I, you, heShe, we, youAll, they }` of ending strings | no | All six present endings, each added to the citation form; excludes `invariant`. |
529
- | `data.grammar.verbs.persons.I` | an ending string, empty for unchanged | yes | The ending added to the citation form for I. |
530
- | `data.grammar.verbs.persons.you` | an ending string, empty for unchanged | yes | The ending added to the citation form for you. |
531
- | `data.grammar.verbs.persons.heShe` | an ending string, empty for unchanged | yes | The ending added to the citation form for he/she. |
532
- | `data.grammar.verbs.persons.we` | an ending string, empty for unchanged | yes | The ending added to the citation form for we. |
533
- | `data.grammar.verbs.persons.youAll` | an ending string, empty for unchanged | yes | The ending added to the citation form for you all. |
534
- | `data.grammar.verbs.persons.they` | an ending string, empty for unchanged | yes | The ending added to the citation form for they. |
532
+ | `data.grammar.verbs.persons` | a map — `{ I, you, heShe, we, youAll, they }` of ending strings | no | All six present endings, each added to the citation form or in place of its closing `replaces`; excludes `invariant`. |
533
+ | `data.grammar.verbs.persons.I` | an ending string, empty for unchanged | yes | The ending for I, added to the citation form or in place of its closing `replaces`. |
534
+ | `data.grammar.verbs.persons.you` | an ending string, empty for unchanged | yes | The ending for you, added to the citation form or in place of its closing `replaces`. |
535
+ | `data.grammar.verbs.persons.heShe` | an ending string, empty for unchanged | yes | The ending for he/she, added to the citation form or in place of its closing `replaces`. |
536
+ | `data.grammar.verbs.persons.we` | an ending string, empty for unchanged | yes | The ending for we, added to the citation form or in place of its closing `replaces`. |
537
+ | `data.grammar.verbs.persons.youAll` | an ending string, empty for unchanged | yes | The ending for you all, added to the citation form or in place of its closing `replaces`. |
538
+ | `data.grammar.verbs.persons.they` | an ending string, empty for unchanged | yes | The ending for they, added to the citation form or in place of its closing `replaces`. |
539
+ | `data.grammar.verbs.replaces` | the closing letters every ending takes the place of | no | Letters a citation form closes on, swapped for each `persons` ending: with `then`, `lokathen` and `zor` give `lokazor`. A citation form closing otherwise takes the ending whole. With `persons` only. |
535
540
  | `data.tongues[].tag` | a tag such as `dunhari`, or `exonym:<tongue>` | yes | The tag a register row's `tongue` names: lowercase letters and digits, or a tag closing on `:<tongue>`, which declares one tag per tongue named after the colon. |
536
541
  | `data.tongues[].meaning` | a string | yes | What a name of this tongue is. |
537
542
  | `data.tongues[].language` | a language skill Address | no | The language note the tag names, where it is one. |
@@ -867,7 +872,6 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
867
872
  | Field | Shape | Meaning |
868
873
  | -------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
869
874
  | `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
875
  | `data.demonym` | string | What a person from this place is called — a Vylarian. |
872
876
  | `data.purpose` | string | The reason this settlement, site, or structure exists, selected from its placeCharacter tags. |
873
877
  | `data.lore` | list of Addresses | Lore concerning this place — its peoples, its law, its calendar, its history. |
@@ -963,9 +967,29 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
963
967
 
964
968
  **Subtypes:** `rules`, `userguide`, `reference`, `howto`, `concept`, `settingguide`.
965
969
 
966
- | Field | Shape | Meaning |
967
- | -------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
968
- | `data.culture` | an Address | The culture a setting guide introduces, as a culture lore note. Required on `settingguide`; no other subType states it. |
970
+ | Field | Shape | Meaning |
971
+ | -------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
972
+ | `data.culture` | an Address | The culture a setting guide introduces, as a culture lore note. Required on `settingguide`; no other subType states it. |
973
+ | `data.map` | a map — `{ name, vowels?, fonts, rows }` | A mapping, which text marked `map=` with its `name` is rewritten by before it is set. See [mapping notes](format-details.md#mapping-notes). |
974
+
975
+ **Inner keys** — a key a field's value does not declare is an error at its own line and column.
976
+
977
+ | Key | Shape | Required | Meaning |
978
+ | -------------------------------- | ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
979
+ | `data.map.name` | string | yes | What `map=` names the map by: lowercase letters, digits and `-`, unique in the package. |
980
+ | `data.map.vowels` | string | no | The characters a row's `before: vowel` reads as vowels; any other letter is a consonant. |
981
+ | `data.map.rows` | list of `[from, to]` or `{ from, to, before?, at? }` | yes | The rewrites, each `[from, to]` or `{ from, to, before?, at? }`: `before` is `vowel` or `consonant`, `at` is `start` or `end`. The longest `from` wins, and a character no row matches passes through. |
982
+ | `data.map.rows[].from` | a string of one or more characters | yes | The one or more characters the row matches. |
983
+ | `data.map.rows[].to` | a string, which may be empty | yes | What the row writes in their place; empty to drop them. |
984
+ | `data.map.rows[].before` | string | no | `vowel` or `consonant`: what must follow the match for the row to apply. |
985
+ | `data.map.rows[].at` | string | no | `start` or `end`: where the match must sit for the row to apply. |
986
+ | `data.map.fonts` | list of `{ font, rows? }` | yes | The faces the map is drawn in, at least one, each with any rows of its own. A run in another face is refused, and each face must draw every character its rows and the lexicon's native forms write. |
987
+ | `data.map.fonts[].font` | an installed face's slug | yes | A face the map is drawn in, by the slug `font=` names it by. |
988
+ | `data.map.fonts[].rows` | list of `[from, to]` or `{ from, to, before?, at? }` | no | Rows for this face alone, tried before the shared `rows`; the same shapes and conditions. |
989
+ | `data.map.fonts[].rows[].from` | a string of one or more characters | yes | The one or more characters the row matches. |
990
+ | `data.map.fonts[].rows[].to` | a string, which may be empty | yes | What the row writes in their place; empty to drop them. |
991
+ | `data.map.fonts[].rows[].before` | string | no | `vowel` or `consonant`: what must follow the match for the row to apply. |
992
+ | `data.map.fonts[].rows[].at` | string | no | `start` or `end`: where the match must sit for the row to apply. |
969
993
 
970
994
  ### macro
971
995
 
@@ -66,6 +66,7 @@ import { authoredFrontmatter } from "./index-records.mjs";
66
66
  import { markupAnchorFindings } from "./anchors.mjs";
67
67
  import { scanSpans } from "./content-spans.mjs";
68
68
  import { scanBlocks } from "./content-blocks.mjs";
69
+ import { scanRuns } from "./content-scripts.mjs";
69
70
  import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
70
71
  import { scanFigures } from "./content-figures.mjs";
71
72
  import { footnoteFindings } from "./content-footnotes.mjs";
@@ -691,6 +692,7 @@ export class BasePackCompiler {
691
692
  ...(scanned?.blocks ?? scanBlocks(body)).errors,
692
693
  ...(scanned?.alerts ?? scanAlerts(body)).errors,
693
694
  ...(scanned?.spans ?? scanSpans(body)).errors,
695
+ ...scanRuns(body, this.corpus?.runs, syntax).errors,
694
696
  ...markupAnchorFindings(body, syntax),
695
697
  ]) {
696
698
  findings.push({
@@ -332,8 +332,13 @@ export class Bundles extends BasePackCompiler {
332
332
  // it compiles into already has somewhere to put the prose.
333
333
  description:
334
334
  markdown.trim() ?
335
- renderFoundryMarkdown(markdown, undefined, undefined, undefined, (pathname) =>
336
- roles.get(pathname),
335
+ renderFoundryMarkdown(
336
+ markdown,
337
+ undefined,
338
+ undefined,
339
+ undefined,
340
+ (pathname) => roles.get(pathname),
341
+ this.corpus?.runs,
337
342
  )
338
343
  : "",
339
344
  folder: this.folderResolver(authoredFolder, { isAddress: folderIsAddress }),
@@ -41,6 +41,7 @@
41
41
  import { indexRecordsFor } from "./content-index.mjs";
42
42
  import { buildContentLinkIndex } from "./helpers.mjs";
43
43
  import { loadPackConfig } from "./pack-config.mjs";
44
+ import { runContext } from "./content-scripts.mjs";
44
45
  import { prepareTreeSqlTables } from "./sql-tables.mjs";
45
46
 
46
47
  /**
@@ -61,7 +62,7 @@ import { prepareTreeSqlTables } from "./sql-tables.mjs";
61
62
  * the index refuses is a note the compile must still *report*, exactly as the
62
63
  * compile loop reported it when the loop was the first to see it.
63
64
  * @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
64
- * @returns {Promise<{currentDate: string, records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
65
+ * @returns {Promise<{currentDate: string, runs: import("./content-scripts.mjs").RunContext, records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
65
66
  * its indexes, and the notes it could not record.
66
67
  */
67
68
  export async function buildCompileCorpus({
@@ -100,6 +101,7 @@ export async function buildCompileCorpus({
100
101
  const sqlTables = await prepareTreeSqlTables(contentBase, sqlOptions);
101
102
  return {
102
103
  currentDate: resolved.currentDate,
104
+ runs: runContext(resolved, records),
103
105
  records,
104
106
  parsed,
105
107
  problems: collected,
@@ -162,6 +162,19 @@ export const NOTATION = Object.freeze(
162
162
  ]),
163
163
  );
164
164
 
165
+ /**
166
+ * Tier 4 — the blocks the HeroicLands script fonts are registered in, so a
167
+ * mapping note or a `font=` run never draws a warning for a script character.
168
+ *
169
+ * @type {ReadonlyArray<readonly [number, number]>}
170
+ */
171
+ const SCRIPT_BLOCKS = Object.freeze([
172
+ [0xf0000, 0xf2fff], // plane 15: Pirzath, Thuravark, Semaron, the reserved alphabets, Teng Chou
173
+ [0x1e960, 0x1ec6f], // Zethu overflow, Qalezu, Zethu
174
+ [0x1ecc0, 0x1ecff], // Qalezu catch-before forms
175
+ [0x13430, 0x1345f], // the Egyptian format controls Zethu uses
176
+ ]);
177
+
165
178
  /**
166
179
  * Whether the charset admits a code point anywhere in a note.
167
180
  *
@@ -169,7 +182,12 @@ export const NOTATION = Object.freeze(
169
182
  * @returns {boolean} Whether it is allowed outside a code fence.
170
183
  */
171
184
  export function isAllowedCodePoint(cp) {
172
- return isLetterTier(cp) || TYPOGRAPHY.has(cp) || NOTATION.has(cp);
185
+ return (
186
+ isLetterTier(cp) ||
187
+ TYPOGRAPHY.has(cp) ||
188
+ NOTATION.has(cp) ||
189
+ SCRIPT_BLOCKS.some(([from, to]) => cp >= from && cp <= to)
190
+ );
173
191
  }
174
192
 
175
193
  /**
@@ -362,7 +380,8 @@ export function checkText(text, file) {
362
380
  line: i + 1,
363
381
  column,
364
382
  severity: /** @type {const} */ ("warning"),
365
- message: `\`${ch}\` ${hex(cp)} is ${refusalFor(cp)}`,
383
+ // A Private Use character draws as nothing, so quoting it leaves an empty span.
384
+ message: `${/\p{Co}/u.test(ch) ? "" : `\`${ch}\` `}${hex(cp)} is ${refusalFor(cp)}`,
366
385
  });
367
386
  }
368
387
  }
@@ -15,7 +15,6 @@ types:
15
15
  - gender
16
16
  - species
17
17
  - born
18
- - calendar
19
18
  - died
20
19
  - age
21
20
  - ageYears
@@ -315,7 +314,6 @@ types:
315
314
  place:
316
315
  data:
317
316
  - events
318
- - calendar
319
317
  - demonym
320
318
  - purpose
321
319
  - lore
@@ -365,6 +363,7 @@ types:
365
363
  doc:
366
364
  data:
367
365
  - culture
366
+ - map
368
367
  subTypes:
369
368
  - rules
370
369
  - userguide
@@ -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,
@@ -105,6 +106,7 @@ import { AddressLink } from "./address-values.mjs";
105
106
  import { scanSpans } from "./content-spans.mjs";
106
107
  import { rebaseNameSpans } from "./name-markup.mjs";
107
108
  import { analyzeNameSpans } from "./names.mjs";
109
+ import { recordRuns } from "./run-rows.mjs";
108
110
  import fs from "node:fs";
109
111
  import path from "node:path";
110
112
 
@@ -162,7 +164,7 @@ import { resolveNoteId } from "./note-ids.mjs";
162
164
  // The `package:` refusal, so a note declaring one is reported where it is
163
165
  // rather than as a bare abort.
164
166
  import { assertNoDeclaredPackage } from "./note-package.mjs";
165
- import { loadPackConfig } from "./pack-config.mjs";
167
+ import { CONFIG_FILENAMES, loadPackConfig } from "./pack-config.mjs";
166
168
  // The record accessors, which live apart so that a module the compilers load
167
169
  // can read a record without importing this one and closing a cycle.
168
170
  // Re-exported because this is where callers have always addressed them.
@@ -447,6 +449,8 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw)
447
449
  * `name` reads `Map 1` rather than `Figure 1` where it is due — see
448
450
  * {@link module:engine/content-figures.scanFigures}.
449
451
  * @param {import("./content-session.mjs").ContentSyntax} [options.syntax] - Session scanner projections.
452
+ * @param {import("./content-scripts.mjs").RunContext} [options.runContext] -
453
+ * The configured languages, which give each run's `lang=` its tag.
450
454
  * @returns {Record<string, any>} The record, keys sorted at every depth. A
451
455
  * **stub** — a note with an empty body, on a type an empty body suppresses —
452
456
  * carries `address` and `anchors` as `null`: it publishes no page, so it
@@ -473,6 +477,7 @@ export function buildIndexRecord({
473
477
  dateContext,
474
478
  resolveRole,
475
479
  syntax,
480
+ runContext,
476
481
  }) {
477
482
  assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw);
478
483
 
@@ -540,6 +545,7 @@ export function buildIndexRecord({
540
545
  ...(Object.keys(resolvedDates).length ? { resolvedDates } : {}),
541
546
  package: contentPackage,
542
547
  names: marked.names,
548
+ runs: recordRuns(source, { context: runContext, bodyLine, bodyColumn, syntax }),
543
549
  address: stub ? null : address,
544
550
  nameAscii: asciiName(frontmatter?.name?.full),
545
551
  aliasesAscii: asciiAliases(frontmatter?.name?.aliases),
@@ -694,6 +700,7 @@ export function indexRecordsForNote({
694
700
  dateContext,
695
701
  resolveRole,
696
702
  syntax,
703
+ runContext,
697
704
  }) {
698
705
  resolveNoteId(frontmatter, { pkg: contentPackage });
699
706
  const record = buildIndexRecord({
@@ -711,6 +718,7 @@ export function indexRecordsForNote({
711
718
  dateContext,
712
719
  resolveRole,
713
720
  syntax,
721
+ runContext,
714
722
  });
715
723
  const records = [decodeIndexAddresses(record, addressContext ?? { package: contentPackage })];
716
724
  const address = noteAddress(frontmatter, contentPackage);
@@ -1062,6 +1070,12 @@ export function resolveCultures(
1062
1070
  * reader receives each as a finding, and the emitter throws once, listing
1063
1071
  * every one.
1064
1072
  * @param {string} [options.currentDate="720.1"] - Setting-wide neutral current date.
1073
+ * @param {string} [options.defaultCalendar] - The calendar of last resort, a
1074
+ * calendar note's shortcode; see {@link module:engine/note-calendars}.
1075
+ * @param {string} [options.configFile] - The configuration file, where an
1076
+ * unknown `defaultCalendar` is reported.
1077
+ * @param {Readonly<Record<string, {tag: string}>>} [options.languages] - The
1078
+ * configured languages, which give each run's `lang=` its tag.
1065
1079
  * @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
1066
1080
  * Another package's entry for a canonical address, read for a parent that
1067
1081
  * package publishes; see {@link resolveCultures}.
@@ -1079,6 +1093,9 @@ export function collectContentIndex(
1079
1093
  {
1080
1094
  contentPackage,
1081
1095
  currentDate,
1096
+ defaultCalendar,
1097
+ configFile,
1098
+ languages,
1082
1099
  skipDirectories,
1083
1100
  assetsBase,
1084
1101
  manifest,
@@ -1185,6 +1202,7 @@ export function collectContentIndex(
1185
1202
  dateContext: dates,
1186
1203
  resolveRole,
1187
1204
  syntax,
1205
+ runContext: { languages },
1188
1206
  });
1189
1207
  records.push(...made);
1190
1208
  bodies.push({ record: made[0], body });
@@ -1234,7 +1252,17 @@ export function collectContentIndex(
1234
1252
 
1235
1253
  // Every note record exists and every Address in it is a tuple, so a parent
1236
1254
  // is found by its canonical address. Before the sort, which reads nothing
1237
- // the pass writes.
1255
+ // the pass writes. Calendars first: they read the culture a note states,
1256
+ // which the culture pass then replaces with the one it resolves.
1257
+ resolveNoteCalendars(records, {
1258
+ contentPackage,
1259
+ defaultCalendar,
1260
+ configFile,
1261
+ contentBase,
1262
+ foreignRecord,
1263
+ problems,
1264
+ parsed,
1265
+ });
1238
1266
  resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems, parsed });
1239
1267
  const cultureNotes = records
1240
1268
  .filter((record) => isNoteRecord(record) && record.type === "lore")
@@ -1459,6 +1487,9 @@ export function indexRecordsFor({
1459
1487
  return collectContentIndex(tree, {
1460
1488
  contentPackage: resolved.contentPackage,
1461
1489
  currentDate: resolved.currentDate,
1490
+ defaultCalendar: resolved.defaultCalendar ?? undefined,
1491
+ configFile: configFileIn(resolved.rootDir, CONFIG_FILENAMES),
1492
+ languages: resolved.languages,
1462
1493
  addressContext: {
1463
1494
  ...noteAddressContext(resolved),
1464
1495
  declaresAddress: dependencyAddresses(resolved),
@@ -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)
@@ -77,6 +77,24 @@ import { parseExtensionAttributes, refusedAttributes } from "./extension-attribu
77
77
 
78
78
  const structureParser = new MarkdownIt({ html: true });
79
79
 
80
+ /**
81
+ * The keys a poetry fence's attribute block takes. `lang`, `font` and `map`
82
+ * are read by `scanRuns` in content-scripts.mjs; `dir`, and on the site
83
+ * `data-pagefind-ignore`, are written by its rewrite of a fence set in one of
84
+ * the faces, and reach the element as they are written.
85
+ */
86
+ export const POETRY_KEYS = Object.freeze([
87
+ "form",
88
+ "meter",
89
+ "rhyme",
90
+ "syllables",
91
+ "lang",
92
+ "font",
93
+ "map",
94
+ "dir",
95
+ "data-pagefind-ignore",
96
+ ]);
97
+
80
98
  /** Read poetry code fences; start/end are inclusive zero-based source lines.
81
99
  * @param {string} source - Authored Markdown.
82
100
  */
@@ -157,15 +175,13 @@ export function scanPoetry(source) {
157
175
  for (const message of [...parsed.problems, ...refusedAttributes(parsed.values)])
158
176
  errors.push({ ...at, message });
159
177
  for (const [key, value] of Object.entries(parsed.values)) {
160
- if (!["form", "meter", "rhyme", "syllables", "lang"].includes(key))
178
+ if (!POETRY_KEYS.includes(key))
161
179
  errors.push({ ...at, message: `${key}= is not a poetry attribute` });
162
180
  else if (key === "syllables" && !/^[1-9]\d*(?:,[1-9]\d*)*$/.test(value))
163
181
  errors.push({
164
182
  ...at,
165
183
  message: "poetry syllables= needs positive comma-separated counts",
166
184
  });
167
- else if (key === "lang" && !/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value))
168
- errors.push({ ...at, message: "poetry lang= needs a language tag" });
169
185
  }
170
186
  }
171
187
  if (opening?.name === "poetry")
@@ -197,7 +213,9 @@ export function renderPoetry(source) {
197
213
  if (block.id) attrs.push(`id="${escape(slugify(block.id))}"`);
198
214
  for (const [key, value] of Object.entries(block.attributes))
199
215
  if (!refusedAttributes({ [key]: value }).length)
200
- attrs.push(`${key === "lang" ? key : `data-${key}`}="${escape(value)}"`);
216
+ attrs.push(
217
+ `${["lang", "dir", "data-pagefind-ignore"].includes(key) ? key : `data-${key}`}="${escape(value)}"`,
218
+ );
201
219
  let indent = block.indent;
202
220
  if (
203
221
  indent.length >= 4 &&