@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 25.6.0
4
+
5
+ **Changes to act on**
6
+
7
+ - Note queries and a language note's `scope` select notes by property, tag or address; a query that reads `file.path`, `file.folder` or `file.name`, or a scope that lists `paths`, is an error at its position.
8
+ - A scenario no longer takes `data.status`, and a note that still writes it fails the build; mark an unfinished scenario with the `draft` tag instead.
9
+ - A region's settlement table comes from its frontmatter on the website, in Foundry and in books, and a table still written in the body must move there. It needs no holding-model tag, and shows a Held by column only where a row names its holder by address.
10
+
11
+ **New**
12
+
13
+ - Places can be districts, such as a city's quarter or a kingdom's shire, standing within a settlement or a region and falling under its government unless they name their own.
14
+ - Places can be routes, such as a road, a pass or a sea lane, with the journeys along them.
15
+ - An affiliation's page lists who holds each of its offices, drawn from the living beings that name the office, and the content index carries the same list.
16
+ - A being can hold several posts in one body by listing them under `office`.
17
+ - Gear can be marked `unique: true` for a one-of-a-kind item; such an item's note must link a place or a being.
18
+ - A language or spelling guide can declare a written mark of kind `quality`, for a mark that changes which vowel is said rather than how long it is held.
19
+ - A quotation typed with `<<` and `>>` draws no raw-HTML warning.
20
+
21
+ ## 25.5.5
22
+
23
+ **Languages and scripts**
24
+
25
+ - Ch'alish P'ilam Ceremonial text passes character checks and is recognized as native script alongside Common.
26
+ - Core vocabulary reports name the languages left unmeasured, including when no language opts into coverage.
27
+ - Generated NPC names capitalize their name words for English prose while keeping connecting words as written.
28
+ - Distinct source names keep their authored spelling under the culture's language code, except when the selected pattern draws English words.
29
+ - The language reference explains that Roman inventories list language additions and refuse shared punctuation and whitespace in the list.
30
+ - A language can supply one list of given names for everyone; NPC generation draws from it for every gender.
31
+
32
+ **Verse**
33
+
34
+ - Metre checks allow multi-syllable function words to go unstressed, so lines can scan without placing a beat on every particle or pronoun.
35
+ - Verse inside blockquotes and nested lists keeps its line breaks, stanza boundaries and indentation, with findings located at the authored text.
36
+ - Verse metre checks count a linked name's visible words without adding beats from its address.
37
+
38
+ **Rendering and sites**
39
+
40
+ - A book can break the page at a chosen point with `--- {.page-break}`.
41
+ - Website previews start immediately, keep the last successful pages available during rebuilds, and report progress while building pages and search.
42
+
3
43
  ## 25.5.4
4
44
 
5
45
  **Builds and commands**
package/README.md CHANGED
@@ -53,6 +53,13 @@ repository’s setup installs the two canonical shell hooks; it installs no
53
53
  pre-push hook. Run `npm test` and the other checks directly, or use
54
54
  `package-build ci` for a container run.
55
55
 
56
+ The repository's `vitest.config.ts` limits both `npm test` and `npm run
57
+ test:watch` to two workers, with Vitest's isolated fork pool. CI runs the same
58
+ `npm test` command. This bounds concurrent test files on the shared runner,
59
+ including files that start CLI subprocesses, load SQL databases, or emit
60
+ TypeScript declarations. Keep this bound when reproducing CI failures locally;
61
+ a percentage of the host's CPUs does not reserve resources for other jobs.
62
+
56
63
  ## Documentation
57
64
 
58
65
  The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md). This toolchain does not build its own site or book.
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * It carries what every package's compiled content needs to read in Foundry as
19
19
  * it does in the preview and on the site: definition lists, verse, alerts,
20
- * inline spoilers, marked names, unresolved links, captioned blocks and
20
+ * inline spoilers, unresolved links, captioned blocks and
21
21
  * highlighted code. Every
22
22
  * class the Foundry renderer emits is styled here or exempted by name in
23
23
  * `tests/base-styles.test.ts`. A package keeps its own stylesheet for its own
@@ -1289,7 +1289,12 @@ function lintCommand() {
1289
1289
  // Text marked with a language, a font or a map, checked against
1290
1290
  // the languages this package declares, the faces it installed
1291
1291
  // and the maps its notes declare.
1292
- const runs = runContext(config, records);
1292
+ const runs = runContext(config, records, {
1293
+ referenceName: (target) => {
1294
+ const hit = index.resolveAddress(target) ?? index.manifestHit(target);
1295
+ return hit?.fm?.name?.full ?? hit?.name;
1296
+ },
1297
+ });
1293
1298
  const loaderLocations = new Set(
1294
1299
  (runs.encodingFindings ?? []).map((one) =>
1295
1300
  JSON.stringify([one.file, one.line, one.column, one.message]),
@@ -2346,7 +2351,11 @@ function siteBuildCommand() {
2346
2351
  const config = argv[CONTENT_CONFIG] ?? loadPackConfig();
2347
2352
  const root = path.resolve(config.rootDir, argv.out ?? DEPLOY_ROOT);
2348
2353
  const out = siteDirectory(config, root);
2349
- const result = await buildPackageSite({ config, out });
2354
+ const result = await buildPackageSite({
2355
+ config,
2356
+ out,
2357
+ progress: (message) => log.info(message),
2358
+ });
2350
2359
  // A note in error is written as its page shows it, and the
2351
2360
  // build fails without indexing a site that is not to be
2352
2361
  // deployed.
@@ -2374,6 +2383,7 @@ function siteBuildCommand() {
2374
2383
  base: siteBase(config),
2375
2384
  out: root,
2376
2385
  search: config.site.search,
2386
+ progress: (message) => log.info(message),
2377
2387
  });
2378
2388
  if (search) {
2379
2389
  log.info(
@@ -2429,8 +2439,10 @@ function siteServeCommand() {
2429
2439
  host: argv.host,
2430
2440
  port: argv.port,
2431
2441
  report: (event) => {
2432
- if (event.kind === "error") reportFailure(event.error);
2433
- else if (event.kind === "failed") {
2442
+ if (event.kind === "progress") log.info(event.message);
2443
+ else if (event.kind === "error") {
2444
+ if (!event.fatal) reportFailure(event.error);
2445
+ } else if (event.kind === "failed") {
2434
2446
  if (event.built) reportSiteBuild(event.built);
2435
2447
  for (const finding of event.findings ?? []) emitDiagnostic(finding);
2436
2448
  } else {
@@ -2444,11 +2456,22 @@ function siteServeCommand() {
2444
2456
  },
2445
2457
  });
2446
2458
  log.info(`serving ${served.url} — interrupt to stop`);
2447
- await new Promise((resolve) => {
2448
- process.once("SIGINT", resolve);
2449
- process.once("SIGTERM", resolve);
2459
+ let interrupt;
2460
+ const interrupted = new Promise((resolve) => {
2461
+ interrupt = resolve;
2462
+ process.once("SIGINT", interrupt);
2463
+ process.once("SIGTERM", interrupt);
2450
2464
  });
2451
- await served.close();
2465
+ try {
2466
+ await Promise.race([interrupted, served.done]);
2467
+ } catch (err) {
2468
+ reportFailure(err);
2469
+ process.exitCode = 1;
2470
+ } finally {
2471
+ process.removeListener("SIGINT", interrupt);
2472
+ process.removeListener("SIGTERM", interrupt);
2473
+ await served.close();
2474
+ }
2452
2475
  } catch (err) {
2453
2476
  reportFailure(err);
2454
2477
  process.exitCode = 1;
@@ -3467,6 +3490,7 @@ function newNpc(config, argv) {
3467
3490
  profile,
3468
3491
  names,
3469
3492
  language: languageKey,
3493
+ languageCode: code,
3470
3494
  seed: argv.seed,
3471
3495
  currentDate: config.currentDate,
3472
3496
  dateContext: reckoningContext(index),
package/docs/api.md CHANGED
@@ -496,13 +496,15 @@ What a place is next to (`data.borders`) and reachable from (`data.routes`): the
496
496
 
497
497
  What lies within a place and which affiliation explicitly governs it — read off `data.parents` and resolved explicit `data.government` across this package and every fetched index, inverted into the lists a note's generated **Within**, **Governed by** and **Governed places** sections show — and the population-based government advisory `package-build lint` runs on a place.
498
498
 
499
- | Export | Signature | Returns | Use it when |
500
- | ---------------------- | -------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
501
- | `holdingsNode` | `holdingsNode(fm, { title, url, package, system, canonical })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
502
- | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
503
- | `holdingsPages` | `holdingsPages(nodes, { governmentNodes })` | `Map<string, Holdings>` | inverting `parents` and resolved explicit `government` into each node's sorted lists, keyed by its `url`, for every node with a `url` and an entry |
504
- | `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` value names — bare or qualified Addresses — empties dropped and repeats collapsed |
505
- | `checkGovernment` | `checkGovernment(note)` | `object[]` | warning at `data.population` for positive population with no own `data.government` key; explicit null means anarchy |
499
+ | Export | Signature | Returns | Use it when |
500
+ | ---------------------------- | -------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
501
+ | `holdingsNode` | `holdingsNode(fm, { title, url, package, system, canonical })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
502
+ | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
503
+ | `holdingsPages` | `holdingsPages(nodes, { governmentNodes })` | `Map<string, Holdings>` | inverting `parents` and resolved explicit `government` into each node's sorted lists, keyed by its `url`, for every node with a `url` and an entry |
504
+ | `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` value names — bare or qualified Addresses — empties dropped and repeats collapsed |
505
+ | `checkGovernment` | `checkGovernment(note)` | `object[]` | warning at `data.population` for positive population with no `data.government` key, own or inherited; explicit null means anarchy |
506
+ | `inheritDistrictGovernments` | `inheritDistrictGovernments(records, { foreignRecord })` | `void` | giving each district record that states no `data.government` the value of its nearest parent that states one, as the content index does |
507
+ | `governmentInherited` | `governmentInherited(record)` | `boolean` | asking whether a district record's `data.government` came from a parent rather than its own note |
506
508
 
507
509
  An entry carries the listed note's `title`, its `address` in the `note` system, its `type` and `subType`, and its `url` where the note has one. A node with no `url` — a stub — is listed by others and carries no list of its own. Records passed as `governmentNodes` take part in government alone, never in geography.
508
510
 
@@ -531,16 +533,18 @@ What trade a settlement supports (`data.market`): the six-step scale, stated onc
531
533
 
532
534
  ### `engine.settlementTables`
533
535
 
534
- A region's settlements table — a markdown table under `## Settlements` on a `place` note, one row per settlement with its market, its people and its holder — held to its declared columns, to the holding model the region's tags name, and to the notes its rows link. `package-build lint` runs it as part of the `place` check; a note with no such table is not read.
536
+ A region declares its rows in indexed `data.settlements`. The guard checks populations, markets, holders and regional coverage, and refuses authored body tables. Every publishing surface uses the same generated table, retaining the canonical `#settlements` heading or filling the author's existing section.
535
537
 
536
- | Export | Signature | Returns | Use it when |
537
- | ------------------------ | ---------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
538
- | `HOLDING_MODELS` | `const HOLDING_MODELS` | `readonly object[]` | the holding models in order, each its `tag`, the `holder` its Held by cell names, and whether a `person` may hold |
539
- | `SETTLEMENT_COLUMNS` | `const SETTLEMENT_COLUMNS` | `readonly object[]` | the table's columns in header order, each its `name` and whether it is `required` |
540
- | `SETTLEMENTS_HEADING` | `const SETTLEMENTS_HEADING` | `string` | the level-two heading text a settlements table stands under |
541
- | `SETTLEMENT_TABLE_RULES` | `const SETTLEMENT_TABLE_RULES` | `readonly object[]` | the rules in order, each its `name` — the prefix its findings carry — and what it `describe`s |
542
- | `findSettlementTables` | `findSettlementTables(raw)` | `{ heading: number, tables: object[] }` | reading a note's settlements tables, each with its header, its rows' cells and their 1-based lines and columns |
543
- | `checkSettlementTables` | `checkSettlementTables(note, { index })` | `object[]` | linting a place note's settlements tables against the corpus the index holds |
538
+ | Export | Signature | Returns | Use it when |
539
+ | ------------------------- | ------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
540
+ | `SETTLEMENT_COLUMNS` | `const SETTLEMENT_COLUMNS` | `readonly object[]` | the table's columns in header order, each its `name` and whether it is `required`; an optional one appears when a row states it |
541
+ | `SETTLEMENTS_HEADING` | `const SETTLEMENTS_HEADING` | `string` | the generated settlements heading |
542
+ | `SETTLEMENT_TABLE_RULES` | `const SETTLEMENT_TABLE_RULES` | `readonly object[]` | the rules in order, each its `name` — the prefix its findings carry — and what it `describe`s |
543
+ | `settlementMarkdown` | `settlementMarkdown(record)` | `string` | rendering indexed rows as the shared table |
544
+ | `assertSettlementRecords` | `assertSettlementRecords(records, { fileOf, rawOf, foreignIndex })` | `void` | refusing invalid indexed rows before publication |
545
+ | `SETTLEMENTS_FIELD` | `const SETTLEMENTS_FIELD` | `object` | the vocabulary declaration for rows and tail |
546
+ | `SETTLEMENT_ROW_FIELDS` | `const SETTLEMENT_ROW_FIELDS` | `readonly object[]` | the closed row declaration |
547
+ | `checkSettlementTables` | `checkSettlementTables(note, { index })` | `object[]` | linting a place note's settlements tables against the corpus the index holds |
544
548
 
545
549
  ### `engine.mapPlaces`
546
550
 
@@ -1329,12 +1333,30 @@ content tree can also compile into Foundry compendium packs.
1329
1333
  | `renderPages` | `function renderPages(pages, options)` | `{{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}}` | Renders every page. |
1330
1334
  | `resolveSitePass` | `function resolveSitePass(name, options)` | `{{beforeLinks?: Function}}` The bundle. | Resolves `site.pass` to its bundle. |
1331
1335
  | `sitePageDecorator` | `sitePageDecorator(config, index)` | Function | Add the site's infobox and being metadata to a rendered page. |
1332
- | `buildSite` | `buildSite({ config, sqlTables })` | `{{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], mapFindings: object[], manifests: object\|null, site?: object}}` | Collects, checks and renders a content tree's pages, and reports what it found; returns each page for the site's renderer. |
1336
+ | `buildSite` | `buildSite({ config, sqlTables, progress })` | `{{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], mapFindings: object[], manifests: object\|null, site?: object}}` | Collects, checks and renders a content tree's pages, and reports what it found; returns each page for the site's renderer. |
1333
1337
 
1334
1338
  `siteGates` takes `{ config, records, sqlTables? }`. Pass the prepared
1335
1339
  `sqlTables` map so anchors emitted by automatic dictionary sections are present in the
1336
1340
  index for checking page links.
1337
1341
 
1342
+ `buildSite`, `engine/site-render.mjs`'s `buildPackageSite` and `writeSite`, and
1343
+ `engine/site-root.mjs`'s `writeSiteRoot` accept an optional `progress(message)`
1344
+ callback for phase summaries. It receives prose, independently of findings.
1345
+
1346
+ `engine/site-serve.mjs`'s `serveSite({ config, out, root?, host?, port?, search?,
1347
+ year?, report? })` resolves with `{ url, settled, close, done }` when HTTP listens,
1348
+ before the initial build. Await `settled()` to wait for that build and queued
1349
+ edits, and `close()` to stop watching and serving. `done` resolves on close;
1350
+ an unexpected HTTP failure rejects it and stops the watchers. Its `report` callback
1351
+ receives `progress` events with a `message`, alongside `built`, `page`,
1352
+ `theme`, `failed` and `error` events; a fatal HTTP error has `fatal: true`.
1353
+ Failed builds are reported through that
1354
+ callback and leave the previous output in service. HTTP runs in a separate
1355
+ thread so synchronous rendering does not stall requests; the resolved
1356
+ configuration and incremental render state stay in the calling thread. Every
1357
+ update is prepared in a separate output tree, then installed between HTTP
1358
+ requests so readers receive complete files.
1359
+
1338
1360
  ### `engine.contentRender`
1339
1361
 
1340
1362
  `createRenderer(target, options?)` builds the one markdown-it configuration every target renders through. `target` is `"foundry"`, `"web"`, `"preview"` or `"book"`. The renderer holds the configured parser (`md`), the names of the plugins installed (`plugins`), and three functions: `prepare` writes the constructs markdown-it has no syntax for — marked text, captioned items, named blocks, verse and alerts — as HTML between blank lines; `render` prepares and renders to HTML; `renderInline` renders one inline run. The `book` target parses only, and its Typst stage reads the tokens.
@@ -1362,6 +1384,16 @@ complete source. Reference-only positions contribute no work shape. A wholly
1362
1384
  reference-only body carries its references in `model.notes`, rendered once
1363
1385
  outside native-font wrappers without a verse row or label.
1364
1386
 
1387
+ Verse regions retain the authored `body` and its absolute `bodyStart` and
1388
+ `bodyEnd`. Their `logicalBody` excludes Markdown list and blockquote prefixes;
1389
+ `bodyOffsets` maps each logical UTF-16 position, including the exclusive end,
1390
+ back to an authored body offset. `parseVerse(logicalBody, {sourceOffsets:
1391
+ bodyOffsets, footnotes})` returns the whole model in authored body coordinates:
1392
+ line bounds and character offsets, stanza and canto bounds, boundaries, layout
1393
+ ranges and findings all use that map. Omitted prefixes are layout ranges, so
1394
+ Roman and native projections exclude them. Add `bodyStart` to a model offset to
1395
+ locate it in the complete source. LF and CRLF retain their authored lengths.
1396
+
1365
1397
  | Export | Signature | Returns | Use it when |
1366
1398
  | ---------------- | ---------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
1367
1399
  | `createRenderer` | `createRenderer(target, options?)` | `{ target, md, plugins, prepare, render, renderInline }` | Rendering a note body for one target with the rules every target shares. |
@@ -1446,17 +1478,17 @@ The charset authored content is held to, so a book can choose its face. The pack
1446
1478
 
1447
1479
  ### `engine.romanCharacters`
1448
1480
 
1449
- Exact permitted Roman source characters for configured languages, before script
1450
- encoding. Inventories come from `data.roman` on each language's own note.
1481
+ A configured language's Roman additions to the shared punctuation standard,
1482
+ before script encoding. Inventories come from `data.roman` on each language's own note.
1451
1483
 
1452
- | Export | Signature | Returns | Use it when |
1453
- | ------------------------- | -------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1454
- | `isRomanWhitespace` | `isRomanWhitespace(char)` | `boolean` | recognizing standard spaces and ASCII tab or line whitespace |
1455
- | `romanInventoryProblems` | `romanInventoryProblems(value)` | `string[]` | checking NFC, repeated characters and forbidden whitespace or controls in a literal inventory |
1456
- | `romanInventoriesOf` | `romanInventoriesOf(records, languages)` | `Map<string, RomanInventory>` | looking up a configured language's own inventory by name or BCP 47 tag |
1457
- | `romanTextProblems` | `romanTextProblems(text, inventory, stream)` | `{offset, message}[]` | validating standard Roman punctuation and language additions with exact UTF-16 offsets; pass a `stream` state to continue sentences and quotations from earlier runs |
1458
- | `romanPunctuationState` | `romanPunctuationState()` | `{sentenceStart, quotes}` | starting a stream of runs whose sentences and quotations continue from one run to the next |
1459
- | `romanUnclosedQuotations` | `romanUnclosedQuotations(quotes)` | `{offset, message}[]` | reporting the quotations still open where a stream of runs ends |
1484
+ | Export | Signature | Returns | Use it when |
1485
+ | ------------------------- | -------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1486
+ | `isRomanWhitespace` | `isRomanWhitespace(char)` | `boolean` | recognizing standard spaces and ASCII tab or line whitespace |
1487
+ | `romanInventoryProblems` | `romanInventoryProblems(value)` | `string[]` | checking NFC, repeats and forbidden whitespace, controls, standard punctuation, literal markers, semicolons, colons, English double quotes and setting-script characters |
1488
+ | `romanInventoriesOf` | `romanInventoriesOf(records, languages)` | `Map<string, RomanInventory>` | looking up a configured language's own inventory by name or BCP 47 tag |
1489
+ | `romanTextProblems` | `romanTextProblems(text, inventory, stream)` | `{offset, message}[]` | validating standard Roman punctuation and language additions with exact UTF-16 offsets; pass a `stream` state to continue sentences and quotations from earlier runs |
1490
+ | `romanPunctuationState` | `romanPunctuationState()` | `{sentenceStart, quotes}` | starting a stream of runs whose sentences and quotations continue from one run to the next |
1491
+ | `romanUnclosedQuotations` | `romanUnclosedQuotations(quotes)` | `{offset, message}[]` | reporting the quotations still open where a stream of runs ends |
1460
1492
 
1461
1493
  ### `engine.pathCharset`
1462
1494
 
@@ -1491,6 +1523,10 @@ Naming an interface icon in a note, without drawing it there. The user guide des
1491
1523
 
1492
1524
  Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cannot say, a note does not say — it gets a construct every surface can render, the way `:icon …:` replaces a pasted glyph.
1493
1525
 
1526
+ `HTML_TAG` excludes the doubled `<<` that opens a typed quotation. A quotation
1527
+ written with `<<` and `>>` draws no raw-HTML warning; adjacent real tags retain
1528
+ their findings and positions.
1529
+
1494
1530
  | Export | Signature | Returns | Use it when |
1495
1531
  | ----------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
1496
1532
  | `HTML_TAG` | `const HTML_TAG` | — | A raw HTML tag, opening, closing or self-closing. |
@@ -2573,6 +2609,16 @@ points derive their own inputs. Generation and pack compilation accept
2573
2609
  `reportPreparation` (default true); a coordinating caller may set it false
2574
2610
  only after reporting the shared source findings itself.
2575
2611
 
2612
+ `engine/content-scripts` exposes `runContext(config, records?, {referenceName}?)`.
2613
+ It returns the language, encoding and metre context used by run scanning.
2614
+ `referenceName(target)` supplies the rendering or validation index's current
2615
+ name for an Address, including dependency targets, or `undefined` when none
2616
+ resolves. This lookup takes precedence over the local-record fallback and
2617
+ belongs only to the returned context; shared cached lexical data does not
2618
+ share a lookup between page or audience views. A metre line whose empty link
2619
+ label has no resolved spoken text stays unscanned, while the link validator
2620
+ reports invalid targets independently.
2621
+
2576
2622
  `buildLinkIndex` and `buildContentLinkIndex` accept an optional `foreign` view
2577
2623
  from the same session; their diagnostic and routed resolver contracts remain
2578
2624
  distinct. `collectAnchors`, `markupAnchorFindings`, `markupFindings`, journal
@@ -2922,10 +2968,13 @@ const inferred = encode(data, null, "say", { enc: "aelwsemaronenc" });
2922
2968
 
2923
2969
  Both functions take `(data, language, text, options?)` and return
2924
2970
  `{text, findings}`; encoding with a language also returns source-positioned
2925
- `pieces`. A language key selects its encoding, face and lexical inputs.
2926
- `options.enc` and `options.font` override the encoding and face; `options.verse`
2927
- asks the encoder to interpret verse structure. With `language: null`, `enc` is
2928
- required and only the encoding's rows apply.
2971
+ `pieces`. `options.enc` selects a consumer-authored encoding document, which
2972
+ supplies its language and font. An explicit language must agree with that
2973
+ document; a null or omitted language infers it and uses its lexical inputs.
2974
+ `options.font` must agree with the encoding's font; `options.verse` asks the
2975
+ encoder to interpret verse structure. An omitted `enc`, or `enc: "default"`,
2976
+ preserves the Roman text. Bare conversion without lexical inputs uses the
2977
+ pure `encodeText` and `decodeText` functions in `engine/encoding.mjs`.
2929
2978
 
2930
2979
  Language conversion uses the same pure `encodeWithLanguage` and
2931
2980
  `decodeWithLanguage` functions as the build. Recorded native writings and
@@ -2933,7 +2982,7 @@ parts precede encoding rows, and decoding can recover an unmarked word from
2933
2982
  the lexicon. An unknown word gives a positioned finding. A bare refusal
2934
2983
  returns empty text and an error finding. Findings carry `severity`, `message`,
2935
2984
  `offset` (UTF-16), and one-based Unicode-code-point `line` and `column`.
2936
- Invalid data, unknown languages and missing encoding selections throw an
2985
+ Invalid data, unknown languages and unknown encoding selections throw an
2937
2986
  error before conversion. `validateData(data)` returns the validated data or
2938
2987
  throws; it opens no files.
2939
2988
 
@@ -117,7 +117,7 @@ Write `government: null` for **complete anarchy**. Omitting the key leaves gover
117
117
  | Any | Explicit `null` | None; complete anarchy |
118
118
  | Any | Affiliation Address | None; the reference must resolve |
119
119
 
120
- These rules apply to every place subtype, including settlements, sites, structures, features, regions and worlds. Empty strings, numbers, lists, maps, references to another note type and unresolved references are errors. They do not declare anarchy. `parents`, containment and capitals do not supply a government. The generated **Governed by** and **Governed places** sections use only explicit government references.
120
+ These rules apply to every place subtype, including settlements, sites, structures, features, regions and worlds. Empty strings, numbers, lists, maps, references to another note type and unresolved references are errors. They do not declare anarchy. `parents`, containment and capitals do not supply a government, with one exception: a `district` that omits the key falls under the government of its nearest parent that states one, and is treated as stating that value, so it receives no advisory. An explicit value on the district, `null` included, is its own. The generated **Governed by** and **Governed places** sections use the explicit references and these inherited ones.
121
121
 
122
122
  ## Publication and summaries
123
123
 
@@ -237,6 +237,30 @@ A captioned item inside the section is withheld with it: in Foundry the item's o
237
237
 
238
238
  The special Actor anchors `{#appearance}` and `{#dossier}` feed the same authored sections to each system's Actor fields, and take an H1 only — not the lower-level heading a journal page may otherwise start from. The section runs to the next H1, nested headings included. An anchor declared on a lower heading is still found, and is a build error rather than a quietly empty field: move the heading to the top level, or drop the anchor if the section is meant to stay unaddressed prose.
239
239
 
240
+ ## Book page breaks
241
+
242
+ Write `--- {.page-break}` on its own line between blocks, with blank lines around it:
243
+
244
+ ```markdown
245
+ The first passage ends here.
246
+
247
+ --- {.page-break}
248
+
249
+ The next passage starts on a fresh book page.
250
+ ```
251
+
252
+ The book starts a fresh page without drawing a rule. If the marker already
253
+ falls at the top of a page, it adds no blank page. The website, Foundry and
254
+ both editor preview views omit the marker. Plain `---`, `***` and `___` keep
255
+ their ordinary horizontal rules. Code blocks and verse divs keep the marker
256
+ as literal text.
257
+
258
+ Only `---` accepts `.page-break`; `*** {.page-break}` and `___ {.page-break}`
259
+ are findings. Other classes, ids and attributes on an attributed `---` line
260
+ are findings too. A page break stands between blocks at the top level of a
261
+ note: putting one inside a list, block quote, alert, footnote definition,
262
+ secret block or fenced div is a finding at the marker's line and column.
263
+
240
264
  ## Captions and numbered references
241
265
 
242
266
  Write a caption on its own line **before** the item it describes, with a blank line before and after the caption. `:` produces an unnumbered caption; `:@` produces a numbered caption. One or more spaces separate the marker from its text. An optional attribute block at the end accepts an identifier, classes and named attributes:
@@ -424,6 +448,20 @@ Periods inside the first inline span can set indentation; periods in later spans
424
448
  are literal. Tabs inside a verse div are errors. The div body is opaque to Markdown block syntax:
425
449
  lines beginning `- `, `1. `, `# `, `> ` or four spaces stay verse text.
426
450
 
451
+ A verse div may sit inside a blockquote, a list item, or nested combinations of
452
+ both. Write the surrounding container prefix on each line, including blank
453
+ stanza lines. Container markers stay outside the verse text; logical lines,
454
+ indentation and stanza boundaries follow the same rules as a standalone div.
455
+ Findings point to the authored line and column, including its container prefix.
456
+
457
+ ```markdown
458
+ > ::: {.verse}
459
+ > One/..Two
460
+ >
461
+ > Three
462
+ > :::
463
+ ```
464
+
427
465
  These two bodies have the same lines and indentation:
428
466
 
429
467
  ```markdown
@@ -491,13 +529,23 @@ language's stress rules; it reports an error at the line:
491
529
  Speaker labels and stage directions (`.speaker` and `.direction`) are presentation
492
530
  only: their text is not scanned, and a line containing only those spans is skipped.
493
531
 
532
+ Wikilinks contribute only their visible labels to the beat count. An empty label
533
+ (`[[place-ashkarad|]]`) contributes the indexed target's name, including a name
534
+ from a dependency. Address segments, anchors and link attributes contribute no
535
+ beats. If an empty label has no resolved spoken text, the line is not scanned;
536
+ the link check reports invalid targets separately. Empty same-page caption
537
+ references depend on page or book numbering and leave the line unscanned when
538
+ that display text is unavailable.
539
+
494
540
  Beats are counted this way:
495
541
 
496
542
  - **Syllables** are vowel groups. A vowel is a letter whose base is `a`, `e`,
497
543
  `i`, `o` or `u`, and a pair the language lists in
498
544
  `data.sounds.syllables.vowelPairs` is one syllable.
499
- - **Every word of two or more syllables has one beat**, on its stressed
500
- syllable. The language's stress rule places it: `two` for a word of two
545
+ - **A word of two or more syllables has one beat**, on its stressed
546
+ syllable, but may go unstressed when every lexicon class is a function class:
547
+ any base other than `n`, `v`, `adj`, `num` or `name`. A word with any content
548
+ class, or no lexicon entry, keeps its beat. The language's stress rule places it: `two` for a word of two
501
549
  syllables, `longer` for a word of three or more, and a vowel carrying the
502
550
  rule's `mark` is stressed wherever it sits.
503
551
  - **A one-syllable noun, verb, adjective, numeral or name has a beat.** A
@@ -506,7 +554,7 @@ Beats are counted this way:
506
554
  through `lang=` as it is for the word check, including words inherited from
507
555
  its family. All entries for a spelling in the nearest lexicon supply their
508
556
  classes. A form the
509
- language's own rules derive takes its word's class. A word whose entries differ in class is a
557
+ language's own rules derive takes its word's class. A one-syllable word with both content and function classes has a
510
558
  beat that may be left out.
511
559
  - **A one-syllable word the lexicon does not hold is a content word** and has a
512
560
  beat. The word check reports the word separately.
@@ -1490,13 +1538,14 @@ Nested frontmatter is addressed exactly as a note writes it: `name.full`, `sohl.
1490
1538
 
1491
1539
  The build adds sections to the end of a note, after everything you wrote, listing what other notes say about it. Each appears only where it has something to list, under a fixed anchor:
1492
1540
 
1493
- | Section | Anchor | Lists |
1494
- | ----------------- | ---------------- | -------------------------------------------------------------------- |
1495
- | Within | `within` | on a place, the places whose `parents` name it, grouped by kind |
1496
- | Governed by | `governedby` | on a place, the affiliation its `government` names |
1497
- | Governed places | `governedplaces` | on an affiliation, the places naming it as their `government` |
1498
- | In song and story | `insongandstory` | the works of literature whose `subjects` name the note or its events |
1499
- | From here | `fromhere` | on a place with borders or routes, the map drawn from it |
1541
+ | Section | Anchor | Lists |
1542
+ | ----------------- | ---------------- | ----------------------------------------------------------------------------- |
1543
+ | Within | `within` | on a place, the places whose `parents` name it, grouped by kind |
1544
+ | Governed by | `governedby` | on a place, the affiliation its `government` names |
1545
+ | Governed places | `governedplaces` | on an affiliation, the places naming it as their `government` |
1546
+ | Office holders | `officeholders` | on an affiliation, each office's living holders, from the beings that name it |
1547
+ | In song and story | `insongandstory` | the works of literature whose `subjects` name the note or its events |
1548
+ | From here | `fromhere` | on a place with borders or routes, the map drawn from it |
1500
1549
 
1501
1550
  The event sections below sit among them, and the [format reference](../reference/format-details.md#generated-sections) gives the order. Each entry links to the note it names; a note with no page of its own is named in plain text. Replace a section the way you replace an event view, by writing your own with the same anchor:
1502
1551