@heroiclands/package-build 25.5.4 → 25.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +7 -0
  3. package/assets/styles/content-base.css +1 -1
  4. package/bin/content-commands.mjs +32 -8
  5. package/docs/api.md +81 -32
  6. package/docs/authoring/frontmatter.md +1 -1
  7. package/docs/authoring/links-and-markup.md +59 -10
  8. package/docs/commands.md +60 -17
  9. package/docs/configuration.md +3 -2
  10. package/docs/content-format.md +50 -1
  11. package/docs/diagnostics.md +17 -0
  12. package/docs/guides/module-project.md +11 -3
  13. package/docs/guides/reference-pages.md +4 -1
  14. package/docs/guides/site.md +13 -3
  15. package/docs/guides/writing-in-invented-scripts.md +5 -2
  16. package/docs/reference/format-details.md +233 -138
  17. package/docs/reference/note-types.md +165 -152
  18. package/docs/reference/theme-pages.md +1 -0
  19. package/e2e.mjs +1 -1
  20. package/engine/base-compiler.mjs +2 -0
  21. package/engine/compile-corpus.mjs +6 -2
  22. package/engine/content-charset.mjs +1 -1
  23. package/engine/content-format.yaml +9 -7
  24. package/engine/content-html.mjs +3 -2
  25. package/engine/content-index.mjs +23 -0
  26. package/engine/content-links.mjs +8 -0
  27. package/engine/content-render.mjs +14 -2
  28. package/engine/content-scripts.mjs +44 -6
  29. package/engine/content-session.mjs +11 -7
  30. package/engine/content-spans.mjs +3 -4
  31. package/engine/content-tables.mjs +12 -5
  32. package/engine/content-verse.mjs +52 -10
  33. package/engine/core-report.mjs +27 -18
  34. package/engine/culture-reach.mjs +3 -0
  35. package/engine/declared-sections.mjs +100 -0
  36. package/engine/derived-sections.mjs +36 -2
  37. package/engine/dictionary-rows.mjs +2 -1
  38. package/engine/generated-sections.mjs +2 -0
  39. package/engine/holdings.mjs +107 -3
  40. package/engine/index-records.mjs +3 -1
  41. package/engine/index.mjs +1 -1
  42. package/engine/language-data.mjs +0 -1
  43. package/engine/language-names.mjs +53 -23
  44. package/engine/language-rows.mjs +1 -3
  45. package/engine/language-terms.mjs +7 -10
  46. package/engine/map-dot.mjs +22 -0
  47. package/engine/markdown-blocks.mjs +23 -8
  48. package/engine/markdownlint-run.mjs +1 -1
  49. package/engine/markup-validation.mjs +2 -0
  50. package/engine/metadata-index.mjs +1 -0
  51. package/engine/note-addresses.mjs +16 -0
  52. package/engine/note-vocabulary.mjs +29 -13
  53. package/engine/npc-generate.mjs +52 -7
  54. package/engine/office-holders.mjs +76 -0
  55. package/engine/page-breaks.mjs +143 -0
  56. package/engine/pdf-build.mjs +12 -4
  57. package/engine/pdf-render.mjs +12 -2
  58. package/engine/place-kinds.mjs +106 -0
  59. package/engine/preview-index.mjs +4 -2
  60. package/engine/settlement-tables.mjs +410 -675
  61. package/engine/settlement-terms.mjs +118 -0
  62. package/engine/site-build.mjs +14 -7
  63. package/engine/site-preview.mjs +3 -0
  64. package/engine/site-render.mjs +18 -4
  65. package/engine/site-root.mjs +10 -1
  66. package/engine/site-serve-http.mjs +151 -0
  67. package/engine/site-serve.mjs +196 -126
  68. package/engine/sql-tables.mjs +101 -7
  69. package/engine/standing-terms.mjs +9 -5
  70. package/engine/standings.mjs +83 -31
  71. package/engine/unique-items.mjs +85 -0
  72. package/engine/verse-metre.mjs +50 -5
  73. package/engine/verse-regions.mjs +16 -0
  74. package/package.json +9 -4
  75. package/site-theme/data/banner-kinds.yaml +2 -0
  76. package/sohl/actors.mjs +4 -3
  77. package/types/e2e.d.mts +1 -1
  78. package/types/engine/content-html.d.mts +2 -1
  79. package/types/engine/content-scripts.d.mts +9 -1
  80. package/types/engine/content-verse.d.mts +11 -2
  81. package/types/engine/core-report.d.mts +6 -2
  82. package/types/engine/culture-reach.d.mts +3 -0
  83. package/types/engine/declared-sections.d.mts +30 -0
  84. package/types/engine/derived-sections.d.mts +5 -1
  85. package/types/engine/holdings.d.mts +34 -2
  86. package/types/engine/index-records.d.mts +2 -1
  87. package/types/engine/language-names.d.mts +18 -6
  88. package/types/engine/language-terms.d.mts +5 -1
  89. package/types/engine/markdown-blocks.d.mts +5 -1
  90. package/types/engine/npc-generate.d.mts +3 -0
  91. package/types/engine/office-holders.d.mts +33 -0
  92. package/types/engine/page-breaks.d.mts +38 -0
  93. package/types/engine/pdf-render.d.mts +2 -0
  94. package/types/engine/place-kinds.d.mts +18 -0
  95. package/types/engine/settlement-tables.d.mts +25 -158
  96. package/types/engine/settlement-terms.d.mts +53 -0
  97. package/types/engine/site-build.d.mts +3 -1
  98. package/types/engine/site-render.d.mts +6 -2
  99. package/types/engine/site-root.d.mts +3 -1
  100. package/types/engine/site-serve-http.d.mts +1 -0
  101. package/types/engine/site-serve.d.mts +12 -6
  102. package/types/engine/sql-tables.d.mts +35 -3
  103. package/types/engine/standing-terms.d.mts +3 -2
  104. package/types/engine/standings.d.mts +14 -2
  105. package/types/engine/unique-items.d.mts +12 -0
  106. package/types/engine/verse-metre.d.mts +2 -1
  107. package/types/engine/verse-regions.d.mts +8 -0
  108. package/vendor/markdownlint.mjs +855 -569
@@ -218,6 +218,7 @@ something to show:
218
218
  | Within | `within` | a place | the places whose `data.parents` names it, a paragraph to each kind of place |
219
219
  | Governed by | `governedby` | a place | the affiliation its `data.government` names, with that affiliation's kind |
220
220
  | Governed places | `governedplaces` | an affiliation | the places whose `data.government` names it, a paragraph to each kind of place |
221
+ | Office holders | `officeholders` | an affiliation | each office a living being holds, one line each, with its holders |
221
222
  | Chronology | `chronology` | a place | the [event views](format-details.md#event-views) |
222
223
  | Events | `events` | a being, an affiliation or a people | the event views |
223
224
  | Accounts | `accounts` | an affiliation, a place or a people | the event views |
package/e2e.mjs CHANGED
@@ -799,7 +799,7 @@ export function suiteExecutables(command) {
799
799
  }
800
800
 
801
801
  /** The Chrome for Testing build shared by local and CI Cypress runs. */
802
- export const CHROME_BUILD_ID = "158.0.8122.0";
802
+ export const CHROME_BUILD_ID = "159.0.8136.0";
803
803
 
804
804
  /**
805
805
  * The browser cache survives reseeding and container teardown.
@@ -61,6 +61,7 @@
61
61
  * @module
62
62
  */
63
63
 
64
+ import { scanPageBreaks } from "./page-breaks.mjs";
64
65
  import { scanAlerts } from "./content-alerts.mjs";
65
66
  import { authoredFrontmatter } from "./index-records.mjs";
66
67
  import { markupAnchorFindings } from "./anchors.mjs";
@@ -697,6 +698,7 @@ export class BasePackCompiler {
697
698
  for (const secretError of [
698
699
  ...(scanned?.blocks ?? scanBlocks(body)).errors,
699
700
  ...(scanned?.alerts ?? scanAlerts(body)).errors,
701
+ ...scanPageBreaks(body).errors,
700
702
  ...(scanned?.spans ?? scanSpans(body)).errors,
701
703
  ...scanRuns(body, this.corpus?.runs, syntax).errors,
702
704
  ...markupAnchorFindings(body, syntax),
@@ -42,6 +42,7 @@ import { indexRecordsFor } from "./content-index.mjs";
42
42
  import { buildContentLinkIndex } from "./helpers.mjs";
43
43
  import { loadPackConfig } from "./pack-config.mjs";
44
44
  import { runContext } from "./content-scripts.mjs";
45
+ import { resolveReference } from "./wikilinks.mjs";
45
46
  import { prepareTreeSqlTables } from "./sql-tables.mjs";
46
47
 
47
48
  /**
@@ -99,13 +100,16 @@ export async function buildCompileCorpus({
99
100
  const corpusOptions = { config: resolved, records };
100
101
  const sqlOptions = { ...corpusOptions, skipDirectories, parsed };
101
102
  const sqlTables = await prepareTreeSqlTables(contentBase, sqlOptions);
103
+ const linkIndex = buildContentLinkIndex(contentBase, router, { ...corpusOptions, sqlTables });
102
104
  return {
103
105
  currentDate: resolved.currentDate,
104
- runs: runContext(resolved, records),
106
+ runs: runContext(resolved, records, {
107
+ referenceName: (target) => resolveReference(linkIndex, target)?.name,
108
+ }),
105
109
  records,
106
110
  parsed,
107
111
  problems: collected,
108
- linkIndex: buildContentLinkIndex(contentBase, router, { ...corpusOptions, sqlTables }),
112
+ linkIndex,
109
113
  sqlTables,
110
114
  };
111
115
  }
@@ -177,7 +177,7 @@ export const NOTATION = Object.freeze(
177
177
  */
178
178
  const SCRIPT_BLOCKS = Object.freeze([
179
179
  [0xf0000, 0xf2fff], // plane 15: Pirzath, Thuravark, Semaron, the reserved alphabets, Teng Chou
180
- [0xf3000, 0xf3fff], // Ch'alish P'ilam Common
180
+ [0xf3000, 0xf4fff], // Ch'alish P'ilam Common and Ceremonial
181
181
  [0x1e960, 0x1ec6f], // Zethu overflow, Qalezu, Zethu
182
182
  [0x1ecc0, 0x1ecff], // Qalezu catch-before forms
183
183
  [0x1ed50, 0x1ed7f], // Veldikko
@@ -96,6 +96,7 @@ types:
96
96
  - durability
97
97
  - made
98
98
  - lost
99
+ - unique
99
100
  subTypes: []
100
101
  armorlocation:
101
102
  data:
@@ -114,6 +115,7 @@ types:
114
115
  - durability
115
116
  - made
116
117
  - lost
118
+ - unique
117
119
  - quantity
118
120
  - potency
119
121
  - strength
@@ -130,6 +132,7 @@ types:
130
132
  - durability
131
133
  - made
132
134
  - lost
135
+ - unique
133
136
  - capacity
134
137
  subTypes: []
135
138
  miscgear:
@@ -141,6 +144,7 @@ types:
141
144
  - durability
142
145
  - made
143
146
  - lost
147
+ - unique
144
148
  - quantity
145
149
  subTypes: []
146
150
  mystery:
@@ -189,6 +193,7 @@ types:
189
193
  - durability
190
194
  - made
191
195
  - lost
196
+ - unique
192
197
  - quantity
193
198
  subTypes:
194
199
  - none
@@ -268,6 +273,7 @@ types:
268
273
  - durability
269
274
  - made
270
275
  - lost
276
+ - unique
271
277
  subTypes: []
272
278
  lore:
273
279
  data:
@@ -323,6 +329,7 @@ types:
323
329
  - culture
324
330
  - population
325
331
  - government
332
+ - settlements
326
333
  - market
327
334
  - borders
328
335
  - routes
@@ -343,10 +350,12 @@ types:
343
350
  subTypes:
344
351
  - world
345
352
  - region
353
+ - district
346
354
  - settlement
347
355
  - site
348
356
  - structure
349
357
  - feature
358
+ - route
350
359
  - celestial
351
360
  scenario:
352
361
  data:
@@ -355,7 +364,6 @@ types:
355
364
  - cast
356
365
  - factions
357
366
  - follows
358
- - status
359
367
  - party.size
360
368
  - party.archetypes
361
369
  subTypes:
@@ -740,12 +748,6 @@ vocabularies:
740
748
  - "4"
741
749
  - "5"
742
750
  - "6"
743
- holdingModel:
744
- - held
745
- - endowed
746
- - registered
747
- - kin
748
- - polis
749
751
  bearing:
750
752
  - N
751
753
  - NE
@@ -64,7 +64,8 @@ import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
64
64
  * markdown's own angle brackets out of it: an autolink is `<https://…>`, and
65
65
  * `https` is followed by `:` rather than whitespace or `>`, so the pattern
66
66
  * stops. A comparison written in prose — `a < b` — has no name after the
67
- * bracket at all.
67
+ * bracket at all. A doubled `<<` opens a quotation, so its second bracket
68
+ * never starts an HTML tag.
68
69
  *
69
70
  * Attributes are consumed as "anything but a bracket", deliberately loosely: a
70
71
  * finding names the tag, and a pattern that tried to parse attribute syntax
@@ -73,7 +74,7 @@ import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
73
74
  *
74
75
  * @type {RegExp}
75
76
  */
76
- export const HTML_TAG = /<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^<>]*)?\/?>/g;
77
+ export const HTML_TAG = /(?<!<)<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^<>]*)?\/?>/g;
77
78
 
78
79
  /**
79
80
  * What a note carrying raw HTML is told.
@@ -141,6 +141,8 @@ import { EXPRESSION, renderMarkdownExpressions } from "./markdown-expressions.mj
141
141
  import { scanFigures } from "./content-figures.mjs";
142
142
  import { eventNoteIndex } from "./event-fields.mjs";
143
143
  import { derivedSections } from "./derived-sections.mjs";
144
+ import { officeBodyKey, officeHolders } from "./office-holders.mjs";
145
+ import { inheritDistrictGovernments } from "./holdings.mjs";
144
146
  // One reader for a note's anchors, shared with the link checker and with the
145
147
  // builds that emit a link. Re-exported because this is where callers
146
148
  // have always addressed it.
@@ -1311,6 +1313,8 @@ export function collectContentIndex(
1311
1313
  parsed,
1312
1314
  });
1313
1315
  resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems, parsed });
1316
+ // A district stating no government falls under its parent's.
1317
+ inheritDistrictGovernments(records, { foreignRecord });
1314
1318
  const cultureNotes = records
1315
1319
  .filter((record) => isNoteRecord(record) && record.type === "lore")
1316
1320
  .map((record) => ({
@@ -1333,6 +1337,24 @@ export function collectContentIndex(
1333
1337
  for (const [note, inherited] of inheritance.values)
1334
1338
  if (Object.keys(inherited).length) note.fm.data = { ...note.fm.data, ...inherited };
1335
1339
 
1340
+ // An office's holders are the living beings naming it, read once every
1341
+ // being is recorded. A public body lists only public beings, as its page
1342
+ // does.
1343
+ phase("index office holders");
1344
+ const publicHolders = officeHolders(records.filter((record) => !isGmNote(record)));
1345
+ const allHolders = officeHolders(records);
1346
+ for (const record of records) {
1347
+ if (!isNoteRecord(record) || record.type !== "affiliation") continue;
1348
+ const held = (isGmNote(record) ? allHolders : publicHolders).get(officeBodyKey(record));
1349
+ if (!held) continue;
1350
+ record.officeHolders = [...held].map(([office, holders]) => ({
1351
+ office,
1352
+ holders: holders.map((being) =>
1353
+ readCanonicalKey(canonicalKey(being.package, "note", "being", being.shortcode)),
1354
+ ),
1355
+ }));
1356
+ }
1357
+
1336
1358
  // Automatic lexical sections require only the recorded frontmatter. Their
1337
1359
  // published anchors and journal targets therefore use the same pure body
1338
1360
  // projection as the surfaces, without making the index emitter async.
@@ -1924,6 +1946,7 @@ export function emitContentIndex({ contentBase, outDir, config, publication } =
1924
1946
  record.data?.events?.length,
1925
1947
  ) ||
1926
1948
  derivedSections(notes, {
1949
+ rawOf: (file) => parsed.get(file)?.raw,
1927
1950
  fileOf: (record) => noteFile(tree, record),
1928
1951
  config: resolved,
1929
1952
  foreignIndex: loadForeignIndexes(resolved, [contentPackage]).index,
@@ -56,6 +56,7 @@ import { readWikilink } from "./wikilink-syntax.mjs";
56
56
  import { encodeAddresses } from "./address-values.mjs";
57
57
  import fs from "node:fs";
58
58
  import { cultureRecordProblems, cultureInheritedFields } from "./culture-parents.mjs";
59
+ import { governmentInherited } from "./holdings.mjs";
59
60
  import path from "node:path";
60
61
 
61
62
  import { matchAllOutsideCode } from "./code-fences.mjs";
@@ -248,6 +249,13 @@ export function buildLinkIndex(
248
249
  if (typeof record.languageCode === "string") note.languageCode = record.languageCode;
249
250
  const hierarchyProblems = cultureRecordProblems(record);
250
251
  note.inheritedCultureFields = cultureInheritedFields(record);
252
+ // A district's government taken from its parent is the parent's
253
+ // authored value, checked where it is written, not here.
254
+ if (governmentInherited(record))
255
+ note.inheritedCultureFields = new Set([
256
+ ...(note.inheritedCultureFields ?? []),
257
+ "government",
258
+ ]);
251
259
  if (hierarchyProblems)
252
260
  note.reportedCultureParents = new Set(
253
261
  hierarchyProblems
@@ -28,6 +28,7 @@
28
28
  * | quotes, dashes and ellipses | as written | typographic | as written | as written, set by Typst |
29
29
  * | raw HTML in a note | passed through | passed through | shown as text | shown as text |
30
30
  * | a name's first use | bold | bold | bold, with an index | bold |
31
+ * | `--- {.page-break}` | omitted | omitted | omitted | a weak page break |
31
32
  * | the output | HTML | HTML | HTML | the token stream, for the Typst stage |
32
33
  *
33
34
  * A target differs from the others only in that table. The shared plugins are
@@ -39,6 +40,8 @@
39
40
  *
40
41
  * A note reaches {@link Renderer#render} with its index-dependent constructs
41
42
  * already resolved by {@link module:engine/content-expand.expandContent}.
43
+ * Valid authored page breaks are omitted before these stages.
44
+ *
42
45
  * {@link Renderer#prepare} then writes the constructs markdown-it has no
43
46
  * syntax for as HTML between blank lines, in this order:
44
47
  *
@@ -64,6 +67,7 @@
64
67
  * @module
65
68
  */
66
69
 
70
+ import { omitPageBreaks, pageBreakPlugin } from "./page-breaks.mjs";
67
71
  import MarkdownIt from "markdown-it";
68
72
  import footnotePlugin from "markdown-it-footnote";
69
73
  import deflistPlugin from "markdown-it-deflist";
@@ -122,7 +126,14 @@ function parseVerseContent(token, md, env, region) {
122
126
  const inline = token.type === "verse_inline";
123
127
  const paired = inline ? undefined : env.verses?.get(body);
124
128
  const footnotes = new Set(Object.keys(env.footnotes?.refs ?? {}).map((key) => key.slice(1)));
125
- const model = paired?.model ?? region.model ?? parseVerse(body, { inline, footnotes });
129
+ const model =
130
+ paired?.model ??
131
+ region.model ??
132
+ parseVerse(region.logicalBody ?? body, {
133
+ inline,
134
+ footnotes,
135
+ sourceOffsets: region.bodyOffsets,
136
+ });
126
137
  token.meta ??= {};
127
138
  token.meta.verse = region;
128
139
  token.meta.versePaired = paired;
@@ -395,6 +406,7 @@ export function createRenderer(target, options = {}) {
395
406
 
396
407
  use("footnotes", footnotePlugin);
397
408
  if (book) {
409
+ use("page-breaks", pageBreakPlugin);
398
410
  use("alerts", alertMarkdownPlugin);
399
411
  use("spans", spanMarkdownPlugin, { verseInline: quotedVerse });
400
412
  use("verse-div", verseDivPlugin);
@@ -442,7 +454,7 @@ export function createRenderer(target, options = {}) {
442
454
 
443
455
  /** Write the constructs markdown-it has no syntax for as HTML. */
444
456
  const prepare = (markdown, env = {}) => {
445
- let text = String(markdown ?? "");
457
+ let text = omitPageBreaks(String(markdown ?? ""));
446
458
  if (preview) text = sourceMalformedAlerts(text);
447
459
  let verses = env.verses;
448
460
  if (env.runs) {
@@ -74,6 +74,9 @@ import { plainRomanVerse } from "./roman-verse.mjs";
74
74
  import { encodeText } from "./encoding.mjs";
75
75
  import { WIKILINK } from "./wikilink-syntax.mjs";
76
76
  import { matchAllOutsideCode } from "./code-fences.mjs";
77
+ import { noteAddressOf } from "./word-rows.mjs";
78
+ import { isNoteRecord } from "./index-records.mjs";
79
+ import { parseAddress, isAddressTuple, renderAddress } from "./address.mjs";
77
80
  import { parseExtensionAttributes } from "./extension-attributes.mjs";
78
81
 
79
82
  /** The keys this module reads, which no construct passes through as written. */
@@ -118,6 +121,7 @@ const CONFIG = "package-build.config.yaml";
118
121
  * @property {ReadonlyMap<string, import("./verse-metre.mjs").MetreBook>} [metreBooks] -
119
122
  * What each configured language's verse is counted by, from
120
123
  * {@link module:engine/verse-metre.metreBooksFromRecords}.
124
+ * @property {(target: string) => string|undefined} [referenceName] - An indexed wikilink target's visible name.
121
125
  * @property {ReadonlyMap<string, import("./roman-characters.mjs").RomanInventory>} [romanInventories] -
122
126
  * Exact Roman source inventories by configured alias and BCP 47 tag.
123
127
  * @property {ReadonlyMap<string, import("./script-emphasis.mjs").Emphasis>} [emphases] -
@@ -164,12 +168,16 @@ const withEncodings = new WeakMap();
164
168
  * resolved configuration.
165
169
  * @param {Iterable<Record<string, any>>} [records] - The content index's
166
170
  * records, whose language notes supply lexical writings and script choices.
171
+ * @param {{referenceName?: (target: string) => string|undefined}} [options] -
172
+ * The rendering or validation index's own target-name lookup.
167
173
  * @returns {RunContext} Its languages, the faces installed where it sits, and
168
174
  * the encodings. A fonts package that cannot be read gives no faces; a run naming
169
175
  * one is reported.
170
176
  */
171
- export function runContext(config, records) {
172
- if (!config) return {};
177
+ export function runContext(config, records, { referenceName: indexedName } = {}) {
178
+ const withReferences = (context) =>
179
+ indexedName ? Object.freeze({ ...context, referenceName: indexedName }) : context;
180
+ if (!config) return withReferences({});
173
181
  let base = contexts.get(config);
174
182
  if (!base) {
175
183
  let faces;
@@ -192,14 +200,41 @@ export function runContext(config, records) {
192
200
  });
193
201
  contexts.set(config, base);
194
202
  }
195
- if (!records || typeof records !== "object") return base;
203
+ if (!records || typeof records !== "object") return withReferences(base);
196
204
  let byRecords = withEncodings.get(config);
197
205
  if (!byRecords) withEncodings.set(config, (byRecords = new WeakMap()));
198
206
  const known = byRecords.get(records);
199
- if (known) return known;
207
+ if (known) return withReferences(known);
200
208
  const loaded = encodingsFromRecords(records, config, base.fonts);
201
209
  const encodings = loaded.encodings;
202
210
  const languageRecords = languageRecordsByName(records, config);
211
+ const referenceNames = new Map();
212
+ for (const record of records) {
213
+ if (!isNoteRecord(record)) continue;
214
+ const name = record.name?.full ?? record.name;
215
+ if (typeof name !== "string") continue;
216
+ const note = noteAddressOf(record);
217
+ if (note) referenceNames.set(note, name);
218
+ const canonical = record.address?.canonical;
219
+ if (canonical)
220
+ referenceNames.set(
221
+ isAddressTuple(canonical) ? renderAddress(canonical) : canonical,
222
+ name,
223
+ );
224
+ }
225
+ const referenceTypes = new Set([...records].map((record) => record.type));
226
+ const referenceName = (target) => {
227
+ const address = parseAddress(
228
+ target,
229
+ {
230
+ package: config.contentPackage,
231
+ system: "note",
232
+ types: referenceTypes,
233
+ },
234
+ { declared: true },
235
+ );
236
+ return isAddressTuple(address) ? referenceNames.get(renderAddress(address)) : undefined;
237
+ };
203
238
  const context = Object.freeze({
204
239
  ...base,
205
240
  encodings,
@@ -211,12 +246,13 @@ export function runContext(config, records) {
211
246
  wordBooks: wordBooksFromRecords(records, config),
212
247
  metres: metresFromRecords(records),
213
248
  metreBooks: metreBooksFromRecords(records, config),
249
+ referenceName,
214
250
  emphases: emphasesFromRecords(records),
215
251
  languageEmphases: languageEmphasesOf(languageRecords),
216
252
  directions: directionsFromRecords(records),
217
253
  });
218
254
  byRecords.set(records, context);
219
- return context;
255
+ return withReferences(context);
220
256
  }
221
257
 
222
258
  /**
@@ -2056,6 +2092,7 @@ export function scanRuns(source, ctx = {}, syntax) {
2056
2092
  languages: ctx.languages,
2057
2093
  metreBooks: ctx.metreBooks,
2058
2094
  lexicons: ctx.lexicons,
2095
+ referenceName: ctx.referenceName,
2059
2096
  attributeColumns,
2060
2097
  });
2061
2098
  const merged = (found, extra) =>
@@ -2715,7 +2752,8 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
2715
2752
  ...(detail.face?.direction === "rtl" ? { dir: "rtl" } : {}),
2716
2753
  ...(search ? { "data-pagefind-ignore": "true" } : {}),
2717
2754
  };
2718
- const marker = `<!--verse-pair:${verses.size + 1}-->${poem.inline ? "" : "\n"}`;
2755
+ const prefix = poem.inline ? "" : block.body.slice(0, block.bodyOffsets[0]);
2756
+ const marker = `${prefix}<!--verse-pair:${verses.size + 1}-->${poem.inline ? "" : "\n"}`;
2719
2757
  const body = marker + block.body;
2720
2758
  verses.set(body, { model, classes, attributes, inline: poem.inline });
2721
2759
  pairedEdits.push({ from: block.bodyStart, to: block.bodyStart, text: marker });
@@ -25,6 +25,7 @@ import { captureContentSources } from "./content-sources.mjs";
25
25
  import { buildLinkIndex } from "./content-links.mjs";
26
26
  import { buildContentLinkIndex } from "./helpers.mjs";
27
27
  import { runContext } from "./content-scripts.mjs";
28
+ import { resolveReference } from "./wikilinks.mjs";
28
29
  import { loadForeignIndexes } from "./metadata-index.mjs";
29
30
  import { loadPackConfig } from "./pack-config.mjs";
30
31
  import { routerFor } from "./pack-router.mjs";
@@ -273,19 +274,22 @@ export async function createContentSession({
273
274
  async compileCorpus() {
274
275
  return memo("compile", async () => {
275
276
  const sqlTables = await session.sqlTables("all");
277
+ const linkIndex = buildContentLinkIndex(tree, router, {
278
+ config,
279
+ records,
280
+ sqlTables,
281
+ foreign,
282
+ });
276
283
  return {
277
- runs: runContext(config, records),
284
+ runs: runContext(config, records, {
285
+ referenceName: (target) => resolveReference(linkIndex, target)?.name,
286
+ }),
278
287
  records,
279
288
  parsed,
280
289
  problems,
281
290
  sqlTables,
282
291
  syntax,
283
- linkIndex: buildContentLinkIndex(tree, router, {
284
- config,
285
- records,
286
- sqlTables,
287
- foreign,
288
- }),
292
+ linkIndex,
289
293
  };
290
294
  });
291
295
  },
@@ -318,12 +318,11 @@ export function scanSpans(source) {
318
318
  return mapping.absolute + pos - mapping.local;
319
319
  });
320
320
  }
321
- // Verse fences preserve inline Markdown even though the block parser
322
- // normally treats every code fence as literal text.
321
+ // Verse divs retain inline syntax while their container prefixes stay layout.
323
322
  for (const poem of scanVerse(text, { checkWorks: false }).blocks) {
324
323
  const children = [];
325
- parser.inline.parse(poem.body, parser, env, children);
326
- collect(children, (pos) => poem.bodyStart + pos);
324
+ parser.inline.parse(poem.logicalBody, parser, env, children);
325
+ collect(children, (pos) => poem.bodyStart + poem.bodyOffsets[pos]);
327
326
  }
328
327
  const raw = text.split("");
329
328
  for (const region of codeRegions(text))
@@ -14,6 +14,7 @@
14
14
  import { FENCE_LINE } from "./code-fences.mjs";
15
15
  import { PAGE_LIST_LANGUAGE } from "./page-lists.mjs";
16
16
  import { appendGeneratedSections } from "./generated-sections.mjs";
17
+ import { insertDeclaredSections } from "./declared-sections.mjs";
17
18
  import { insertLexicalSections } from "./lexical-sections.mjs";
18
19
 
19
20
  /**
@@ -129,8 +130,8 @@ export function expandContentTables(markdown, { source = "", sqlTables, pageList
129
130
  source,
130
131
  directive: block.join("\n"),
131
132
  reason: failure,
132
- line: i,
133
- column: indent.length + 1,
133
+ line: prepared?.line ?? i,
134
+ column: prepared?.column ?? indent.length + 1,
134
135
  });
135
136
  block.forEach((value, offset) => emit(value, i + offset));
136
137
  i = close;
@@ -149,11 +150,17 @@ export function expandContentTables(markdown, { source = "", sqlTables, pageList
149
150
  if (close + 1 < lines.length && lines[close + 1].trim()) emit("", i, true);
150
151
  i = close;
151
152
  }
153
+ const declared = insertDeclaredSections(out.join("\n"), sqlTables?.declared, {
154
+ authored,
155
+ lineMap,
156
+ generatedFrom,
157
+ syntax: sqlTables?.syntax,
158
+ });
152
159
  return {
153
- ...insertLexicalSections(out.join("\n"), sqlTables?.lexical, {
160
+ ...insertLexicalSections(declared.markdown, sqlTables?.lexical, {
154
161
  authored,
155
- lineMap,
156
- generatedFrom,
162
+ lineMap: declared.lineMap,
163
+ generatedFrom: declared.generatedFrom,
157
164
  syntax: sqlTables?.syntax,
158
165
  }),
159
166
  errors,
@@ -140,7 +140,7 @@ export function scanVerse(source, { checkWorks = true } = {}) {
140
140
  for (const message of [...region.problems, ...refusedAttributes(region.attributes)])
141
141
  errors.push({ ...at, message });
142
142
  if (!region.closed) errors.push({ ...at, message: "verse div needs a closing ::: line" });
143
- if (!region.body.trim()) errors.push({ ...at, message: "verse div is empty" });
143
+ if (!region.logicalBody.trim()) errors.push({ ...at, message: "verse div is empty" });
144
144
  for (const key of Object.keys(region.attributes))
145
145
  if (!VERSE_KEYS.includes(key))
146
146
  errors.push({
@@ -150,7 +150,10 @@ export function scanVerse(source, { checkWorks = true } = {}) {
150
150
  });
151
151
  if (region.attributes.work !== undefined && !region.attributes.work.trim())
152
152
  errors.push({ ...at, message: "work= needs a nonempty page-local key" });
153
- const model = parseVerse(region.body, { footnotes: definedFootnotes });
153
+ const model = parseVerse(region.logicalBody, {
154
+ footnotes: definedFootnotes,
155
+ sourceOffsets: region.bodyOffsets,
156
+ });
154
157
  for (const finding of model.errors) {
155
158
  const preceding = text.slice(0, region.bodyStart + finding.offset);
156
159
  errors.push({
@@ -254,6 +257,8 @@ function keyColumn(opening, key) {
254
257
  * @property {string[]} classes - Authored presentation classes.
255
258
  * @property {string} indent - Opening div's leading spaces.
256
259
  * @property {string} body - Authored verse body.
260
+ * @property {string} logicalBody - Body without Markdown container prefixes.
261
+ * @property {number[]} bodyOffsets - Logical UTF-16 offsets mapped to authored body offsets, including the end.
257
262
  * @property {VerseModel} model - Logical structure relative to the body.
258
263
  */
259
264
 
@@ -291,10 +296,10 @@ function cantoLine(line) {
291
296
  * Offsets are UTF-16 positions relative to body. Delimiters and indentation are
292
297
  * layout ranges; source newlines inside a logical line contribute one space.
293
298
  * @param {string} body - Authored verse body.
294
- * @param {{inline?: boolean, footnotes?: ReadonlySet<string>}} [options] - Inline quotations have no cantos; footnotes are same-note definition labels.
299
+ * @param {{inline?: boolean, footnotes?: ReadonlySet<string>, sourceOffsets?: number[]}} [options] - Inline quotations have no cantos; footnotes are same-note definition labels; sourceOffsets maps projected body positions to authored offsets, including the end.
295
300
  * @returns {VerseModel} The shared structure with offsets relative to the body.
296
301
  */
297
- export function parseVerse(body, { inline = false, footnotes = new Set() } = {}) {
302
+ export function parseVerse(body, { inline = false, footnotes = new Set(), sourceOffsets } = {}) {
298
303
  const source = String(body ?? "");
299
304
  const groups = [],
300
305
  boundaries = [],
@@ -594,6 +599,35 @@ export function parseVerse(body, { inline = false, footnotes = new Set() } = {})
594
599
  for (const group of groups)
595
600
  if (!group.cantoBreak)
596
601
  group.label = multi ? `${group.canto}:${group.stanza}` : String(group.stanza);
602
+ if (sourceOffsets) {
603
+ const position = (offset) => sourceOffsets[offset];
604
+ const bounds = (item) => {
605
+ item.from = position(item.from);
606
+ item.to = position(item.to);
607
+ };
608
+ for (const line of new Set([
609
+ ...cantos.flatMap((canto) => canto.stanzas.flatMap((stanza) => stanza.lines)),
610
+ ...pendingNotes,
611
+ ])) {
612
+ bounds(line);
613
+ line.offsets = line.offsets.map(position);
614
+ }
615
+ for (const canto of cantos) for (const stanza of canto.stanzas) bounds(stanza);
616
+ for (const group of groups) bounds(group);
617
+ for (const boundary of boundaries) boundary.sourceOffset = position(boundary.sourceOffset);
618
+ for (const range of layout) {
619
+ range[0] = position(range[0]);
620
+ range[1] = position(range[1]);
621
+ }
622
+ for (const finding of errors) finding.offset = position(finding.offset);
623
+ // Container bytes remain layout in projections that read the authored body.
624
+ let previous = 0;
625
+ for (const offset of sourceOffsets) {
626
+ if (offset > previous) layout.push([previous, offset]);
627
+ previous = offset + 1;
628
+ }
629
+ layout.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
630
+ }
597
631
  return { cantos, groups, boundaries, layout, errors, notes: pendingNotes };
598
632
  }
599
633
 
@@ -816,16 +850,24 @@ export function renderVerse(source, { verses = new Map() } = {}) {
816
850
  !lines.slice(0, block.start).some((line) => /^ *(?:[-+*]|\d+[.)]) +/.test(line))
817
851
  )
818
852
  indent = "";
853
+ const openingPrefix = lines[block.start].slice(0, block.column - 1);
854
+ // A list marker belongs only to the opening line; its continuation is spaces.
855
+ const prefix = openingPrefix.replace(/(?:[-+*]|\d+[.)])[ \t]+/g, (mark) =>
856
+ " ".repeat(mark.length),
857
+ );
858
+ const container = prefix.includes(">") || openingPrefix !== prefix;
859
+ const continuation = container ? prefix : indent;
860
+ const blank = continuation.trimEnd();
819
861
  output.push(
820
862
  "",
821
- `${indent}<div ${attrs.join(" ")}>`,
822
- "",
863
+ `${container ? openingPrefix : indent}<div ${attrs.join(" ")}>`,
864
+ blank,
823
865
  ...verseMarkdown(block.body, { labelled, model, presentation: transported })
824
866
  .split("\n")
825
- .map((line) => (line ? indent + line : line)),
826
- "",
827
- `${indent}</div>`,
828
- "",
867
+ .map((line) => (line ? continuation + line : blank)),
868
+ blank,
869
+ `${continuation}</div>`,
870
+ blank,
829
871
  );
830
872
  cursor = block.end + 1;
831
873
  }