@heroiclands/package-build 23.12.3 → 23.12.5

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 (55) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/ci/ci-steps.mjs +5 -5
  3. package/content-config.mjs +19 -0
  4. package/docs/api.md +1 -0
  5. package/docs/authoring/frontmatter.md +1 -1
  6. package/docs/authoring/links-and-markup.md +4 -4
  7. package/docs/commands.md +5 -4
  8. package/docs/configuration.md +29 -12
  9. package/docs/guides/book.md +15 -13
  10. package/docs/guides/release-and-deploy.md +6 -9
  11. package/docs/project-setup.md +11 -11
  12. package/docs/reference/format-details.md +28 -16
  13. package/docs/reference/note-types.md +1 -1
  14. package/engine/content-index.mjs +2 -39
  15. package/engine/event-fields.mjs +24 -1
  16. package/engine/event-rows.mjs +37 -2
  17. package/engine/event-views.mjs +58 -14
  18. package/engine/frontmatter-lint.mjs +11 -0
  19. package/engine/generated-sections.mjs +8 -1
  20. package/engine/holdings.mjs +8 -2
  21. package/engine/infobox-render.mjs +16 -5
  22. package/engine/infobox.mjs +23 -2
  23. package/engine/literature-works.mjs +8 -2
  24. package/engine/metadata-index.mjs +3 -0
  25. package/engine/name-sort.mjs +91 -0
  26. package/engine/note-frontmatter.mjs +1 -1
  27. package/engine/note-vocabulary.mjs +1 -1
  28. package/engine/page-lists.mjs +3 -2
  29. package/engine/pdf-build.mjs +1 -0
  30. package/engine/pdf-index.mjs +33 -12
  31. package/engine/pdf-pages.mjs +13 -3
  32. package/engine/pdf-render.mjs +60 -33
  33. package/engine/pdf-toc.mjs +4 -3
  34. package/engine/project-init.mjs +2 -2
  35. package/engine/related-pages.mjs +12 -2
  36. package/engine/site-build.mjs +3 -0
  37. package/engine/sql-tables.mjs +20 -5
  38. package/githooks/pre-push +5 -5
  39. package/package.json +1 -1
  40. package/types/content-config.d.mts +5 -0
  41. package/types/engine/content-index.d.mts +2 -31
  42. package/types/engine/event-fields.d.mts +11 -0
  43. package/types/engine/event-rows.d.mts +4 -1
  44. package/types/engine/event-views.d.mts +7 -3
  45. package/types/engine/generated-sections.d.mts +6 -0
  46. package/types/engine/holdings.d.mts +7 -1
  47. package/types/engine/infobox.d.mts +11 -0
  48. package/types/engine/literature-works.d.mts +5 -0
  49. package/types/engine/name-sort.d.mts +49 -0
  50. package/types/engine/pdf-index.d.mts +7 -3
  51. package/types/engine/pdf-pages.d.mts +2 -1
  52. package/types/engine/pdf-render.d.mts +16 -5
  53. package/types/engine/pdf-toc.d.mts +1 -1
  54. package/types/engine/related-pages.d.mts +5 -0
  55. package/ci/confirm-npm-version.mjs +0 -135
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.12.5
4
+
5
+ ### Patch Changes
6
+
7
+ In PDF books, the page number and section name at the foot of each page are set at the size of the body text.
8
+
9
+ In PDF books, list items sit apart from one another and carry a bullet large enough to see at book size.
10
+
11
+ In PDF books, a reference to another book that already sits in brackets joins them, reading (The Crossing, Thal. p524) rather than nesting a second pair.
12
+
13
+ Chronology sections, and the other event sections, list each event's date first, written in the setting's calendar. A chronology shows its Felt here column only when some event is felt in the place, and prints across the full page in PDF books. A captioned table can ask to span the page in a PDF book with `.full-width`. A wide table floats to where it fits, and the text runs on past its heading rather than leaving an empty page.
14
+
15
+ In PDF books, a profile box too long for the rest of a page continues on the next one between its rows, instead of running off the page over the footer.
16
+
17
+ A note's name may give a `sort` form, the way an index would write it: `sort: Black Flame, The` for _The Black Flame_, or `sort: Hilvan, Doran`. It files the note in the book index, page lists and other name-ordered lists, and it is the note's entry text in the book index; every other list still shows the full name. Authored `sql` fences can order by the new `nameSort` column of `entries`, which is the `sort` form or, without one, the full name.
18
+
19
+ Profile figures read as numbers do: 12,500 under a million, and 11 million or 1.2 million above it, on the website, in Foundry and in PDF books.
20
+
21
+ ## 23.12.4
22
+
23
+ ### Patch Changes
24
+
25
+ A PDF book may declare a short `abbreviation`, so references into it read `(Aelw. p307)` instead of naming the full title, and the other books' indexes list its notes the same way.
26
+
27
+ In PDF books, a definition list sets its term in bold at the start of the line with the description running on after it, indented on later lines, and no space appears between a linked name and the punctuation that follows it.
28
+
29
+ `project init` scaffolds the build workflow at `.gitea/workflows/build.yml`, and `project check` and `package-build ci` read it from there.
30
+
3
31
  ## 23.12.3
4
32
 
5
33
  ### Patch Changes
package/ci/ci-steps.mjs CHANGED
@@ -18,7 +18,7 @@
18
18
  * of the command list is a second statement of one thing, and the copy is the
19
19
  * one that goes stale — silently, because a pre-push check that runs four of
20
20
  * five steps still exits 0. So the steps are parsed out of
21
- * `.github/workflows/build.yml` at run time and there is nothing to keep in
21
+ * `.gitea/workflows/build.yml` at run time and there is nothing to keep in
22
22
  * step.
23
23
  *
24
24
  * **It refuses rather than assumes.** If the workflow's shape changes so that
@@ -28,7 +28,7 @@
28
28
  *
29
29
  * **What it cannot run**, and says so: a `uses:` step is a published action, not
30
30
  * a command — the forbidden-marker check, the coverage upload. Those stay
31
- * GitHub's to run, and the summary names them so the gap is visible rather than
31
+ * CI's to run, and the summary names them so the gap is visible rather than
32
32
  * assumed away.
33
33
  *
34
34
  * **It parses the workflow itself rather than importing a YAML library**, for a
@@ -64,7 +64,7 @@ const ROOT = process.cwd();
64
64
  * failed*. Mirrors the same constant in `ci-docker.mjs`.
65
65
  */
66
66
  export const UNAVAILABLE = 2;
67
- const WORKFLOW_DIR = path.join(ROOT, ".github/workflows");
67
+ const WORKFLOW_DIR = path.join(ROOT, ".gitea/workflows");
68
68
 
69
69
  /**
70
70
  * The workflow's steps, split into what can be run here and what cannot.
@@ -77,7 +77,7 @@ export function ciSteps() {
77
77
  const files = prTriggeredWorkflows();
78
78
  if (!files.length) {
79
79
  throw new NoWorkflow(
80
- `no workflow under .github/workflows is triggered by \`pull_request\`, ` +
80
+ `no workflow under .gitea/workflows is triggered by \`pull_request\`, ` +
81
81
  `so there is nothing here that a pull request would run.`,
82
82
  );
83
83
  }
@@ -236,7 +236,7 @@ function main() {
236
236
  });
237
237
  if (result.status !== 0) {
238
238
  console.error(
239
- `\nci-steps: FAILED at "${step.name}" — this is what GitHub would report.\n` +
239
+ `\nci-steps: FAILED at "${step.name}" — this is what CI would report.\n` +
240
240
  ` Fix it, or push with --no-verify if you mean to.`,
241
241
  );
242
242
  return result.status ?? 1;
@@ -787,6 +787,7 @@ const PDF_KEYS = [
787
787
  const PDF_BOOK_KEYS = [
788
788
  "id",
789
789
  "title",
790
+ "abbreviation",
790
791
  "subtitle",
791
792
  "coverImage",
792
793
  "front",
@@ -1813,6 +1814,8 @@ function normalizeSite(value) {
1813
1814
  * @typedef {object} ResolvedPdfBook
1814
1815
  * @property {string} id Its name on the command line, `--book <id>`.
1815
1816
  * @property {string} title
1817
+ * @property {string} abbreviation Names the book in another book's
1818
+ * references; `""` when none is declared.
1816
1819
  * @property {string} subtitle `""` when none is declared.
1817
1820
  * @property {string} coverImage Absolute; `""` when none is declared.
1818
1821
  * Whether the file exists is checked when the
@@ -1984,6 +1987,8 @@ function normalizePdfBooks(value, rootDir, contentPackage) {
1984
1987
  const ids = new Set();
1985
1988
  /** @type {Map<string, number>} Stem → the book that took it. */
1986
1989
  const stems = new Map();
1990
+ /** @type {Map<string, string>} Abbreviation → the id of the book that took it. */
1991
+ const abbreviations = new Map();
1987
1992
  return Object.freeze(
1988
1993
  value.map((entry, i) => {
1989
1994
  const where = `pdf.books[${i}]`;
@@ -2003,6 +2008,19 @@ function normalizePdfBooks(value, rootDir, contentPackage) {
2003
2008
  if (ids.has(id)) fail(`${where}.id`, `is \`${id}\`, which names a book more than once`);
2004
2009
  ids.add(id);
2005
2010
  const title = requireNonEmptyString(book.title, `${where}.title`);
2011
+ let abbreviation = "";
2012
+ if (book.abbreviation !== undefined) {
2013
+ abbreviation = requireNonEmptyString(book.abbreviation, `${where}.abbreviation`);
2014
+ const holder = abbreviations.get(abbreviation);
2015
+ if (holder !== undefined) {
2016
+ fail(
2017
+ `${where}.abbreviation`,
2018
+ `is \`${abbreviation}\`, which books \`${holder}\` and \`${id}\` both ` +
2019
+ "declare — a reader could not tell which book to open",
2020
+ );
2021
+ }
2022
+ abbreviations.set(abbreviation, id);
2023
+ }
2006
2024
 
2007
2025
  // One tree, from exactly one place: two would leave the reader to
2008
2026
  // guess which is printed, and none prints nothing.
@@ -2059,6 +2077,7 @@ function normalizePdfBooks(value, rootDir, contentPackage) {
2059
2077
  return Object.freeze({
2060
2078
  id,
2061
2079
  title,
2080
+ abbreviation,
2062
2081
  subtitle:
2063
2082
  book.subtitle === undefined ?
2064
2083
  ""
package/docs/api.md CHANGED
@@ -384,6 +384,7 @@ The declared infobox: what a note's summary panel holds, decided once and render
384
384
  | `assertInfoboxSet` | `assertInfoboxSet(boxes, fm, options)` | `readonly object[]`, throws | refusing a page that carries anything but the boxes its type maps to |
385
385
  | `buildInfoboxes` | `buildInfoboxes(fm, options)` | `object[]` | building every box a note carries, against a given set of maps and declarations |
386
386
  | `defineInfobox` | `defineInfobox(declaration)` | `object` | declaring one system's half of the infobox |
387
+ | `formatQuantity` | `formatQuantity(value)` | `string` | writing a number as a profile shows it: `12,500` under a million, `11 million` above |
387
388
  | `hasRenderableValue` | `hasRenderableValue(kind, value)` | `boolean` | deciding whether a built value is worth a row |
388
389
  | `hasValue` | `hasValue(value)` | `boolean` | deciding whether an authored value is worth a row |
389
390
  | `humanizeFieldName` | `humanizeFieldName(name)` | `string` | turning a declared key into the label a reader sees |
@@ -21,7 +21,7 @@ data: {}
21
21
  ---
22
22
  ```
23
23
 
24
- `name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names. The note infobox lists aliases directly beneath the full name. For a `being` with `subType: character`, `given` and `clan` are optional name components. The first alias of a character or NPC is its nickname. Omit optional keys when they do not apply; `given`, `clan`, and alias entries must be nonempty strings when present. No other keys are accepted under `name`.
24
+ `name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names, and `sort` for the form the name takes in an index, such as `Black Flame, The` or `Hilvan, Doran`. The note infobox lists aliases directly beneath the full name. `sort` files the note wherever notes are ordered by name — the book index, page lists, generated lists of related notes, and the `nameSort` column of the `entries` relation — and is the note's entry text in the book index. Every other list still displays the `full` name. A note with no `sort` files and indexes by its `full` name. For `name: {full: The Black Flame, sort: "Black Flame, The"}`, the book index prints _Black Flame, The_ under **B**, and a page list prints _The Black Flame_ at the same place.
25
25
 
26
26
  `description` is the short page summary. `tags` hold draft state and descriptive labels. The note type's classification belongs in `subType` where it is declared. `data` holds facts about the subject shared by systems. A YAML comment is the place for an author-only note: commented values remain valid YAML but do not enter the content index or generated documents.
27
27
 
@@ -197,7 +197,7 @@ The caption attaches to exactly the next supported block:
197
197
 
198
198
  The optional `type` attribute overrides inference and accepts `figure`, `table`, `poetry`, `code`, `prose`, and `example`. `type=example` uses **Example 1**, **Example 2**, and so on. It does not allow an unsupported target. A heading, list, thematic break, another caption or end of document after a caption is an error. Caption markers without separating spaces or caption text are errors too.
199
199
 
200
- Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
200
+ Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. On a captioned table or SQL fence, `.full-width` sets the table across the page in a PDF book instead of in one column (`: Regional trade routes {.full-width}`); the other surfaces ignore it. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
201
201
 
202
202
  Each numbered type has its own counter. Websites and Foundry number within a note; books number across their reading order. Unnumbered captions neither display nor consume a number. A map image can retain its asset-derived **Map** classification when no type is specified.
203
203
 
@@ -543,7 +543,7 @@ A column's heading is its SQL alias, so `AS "Name"` is what a reader sees. A cel
543
543
  | `market` | The market scale as three columns — `value`, `name`, `trade` — so a table can print `village` beside the number a note wrote. |
544
544
  | `<package>.notes` | The same relations — `notes`, `entries`, `events`, `names`, `words`, `affixes`, `fields`, `glossary`, `language_lineage`, `dictionary`, `terms`, `register`, `tongues`, `retired`, `scope`, `letters`, `edges`, `clusters`, `stress`, `finals`, `syllables`, `joining`, `joins`, `marks`, `skeleton`, `frames`, `read_past`, `mutations`, `cluster_groups`, `sound_changes`, `elements`, `roots`, `cognates`, `calling_names`, `older`, `negation`, `name_shapes`, `name_edges`, `name_draws`, `held_from` and `near_names` — for each package this one declares a dependency on, in a schema named for it. |
545
545
 
546
- Nested frontmatter is addressed exactly as a note writes it: `name.full`, `sohl.weight`, `data.born`. Every Address is a string — the full canonical Address, as in `list_contains(data.parents, 'thalorna-note-place-xerathia')` — whichever package published it. Every note carries a computed `state` column of `full`, `draft` or `stub`. Notes tagged `gm` are absent from the relations on public surfaces.
546
+ Nested frontmatter is addressed exactly as a note writes it: `name.full`, `sohl.weight`, `data.born`. Every Address is a string — the full canonical Address, as in `list_contains(data.parents, 'thalorna-note-place-xerathia')` — whichever package published it. Every note carries a computed `state` column of `full`, `draft` or `stub`, and a `nameSort` column holding its `name.sort` or, without one, its `name.full`, to `ORDER BY`. Notes tagged `gm` are absent from the relations on public surfaces.
547
547
 
548
548
  ### Sections the build adds
549
549
 
@@ -567,7 +567,7 @@ The vale holds three villages, of which only [[place-ford|Ford]] is walled.
567
567
 
568
568
  ### Sections the build adds about events
569
569
 
570
- A note's events are listed for you on the notes they concern: a place gets a **Chronology** of what happened there and below it, with every `depth: world` event as context, a being, affiliation or people the **Events** it took part in, an affiliation, place or people the **Accounts** it gives, and a note holding events **What followed** them. Each is a section appended after everything you wrote, with a fixed anchor — `chronology`, `events`, `accounts`, `followed` — and it appears only where it has something to list.
570
+ A note's events are listed for you on the notes they concern: a place gets a **Chronology** of what happened there and below it, with every `depth: world` event as context, a being, affiliation or people the **Events** it took part in, an affiliation, place or people the **Accounts** it gives, and a note holding events **What followed** them. Each is a section appended after everything you wrote, with a fixed anchor — `chronology`, `events`, `accounts`, `followed` — and it appears only where it has something to list. Each lists the date first, written in the setting's calendar, followed by what happened; a Chronology adds a **Felt here** column only where some event is felt in the place, and a PDF book prints it across the full page width.
571
571
 
572
572
  To replace one, write the section yourself with the same anchor. The build then adds nothing for it and leaves yours exactly as written:
573
573
 
@@ -577,7 +577,7 @@ To replace one, write the section yourself with the same anchor. The build then
577
577
  The fells keep no reckoning before the raising.
578
578
 
579
579
  ```sql
580
- SELECT summary AS "What happened", "when" AS "When"
580
+ SELECT summary AS "What happened", whenText AS "When"
581
581
  FROM events
582
582
  WHERE list_contains("where".locus, 'thalorna-note-place-ironfells')
583
583
  ORDER BY whenSort
package/docs/commands.md CHANGED
@@ -128,16 +128,16 @@ package-build ci [--native]
128
128
 
129
129
  The default command runs in Docker on Linux against a clean export of the
130
130
  current commit. It reads the repository's pull-request workflows under
131
- `.github/workflows/` and runs their `run:` steps in workflow order. A failed
131
+ `.gitea/workflows/` and runs their `run:` steps in workflow order. A failed
132
132
  step stops the replay. Uncommitted changes are absent from the clean export.
133
133
 
134
134
  `--native` runs the same steps on the host in the current working tree. It
135
135
  needs no Docker and includes uncommitted changes, but its operating system and
136
- existing files can differ from a clean GitHub runner. The steps themselves
136
+ existing files can differ from a clean CI runner. The steps themselves
137
137
  still come from the workflow files.
138
138
 
139
139
  The command lists `uses:` steps that it cannot run locally. A successful replay
140
- covers the listed `run:` steps; GitHub runs the published actions separately.
140
+ covers the listed `run:` steps; the forge runs the published actions separately.
141
141
  Workflow steps may install dependencies, generate files, or perform other
142
142
  declared build actions.
143
143
 
@@ -2632,7 +2632,8 @@ Typst compiler; `--no-compile` emits the Typst source and stops there,
2632
2632
  which is what lets the source be inspected or compiled by hand.
2633
2633
 
2634
2634
  A reference to a note that another book is home to prints that book and page,
2635
- `Name (_Book title_, p. 12)`. A compile renders and lays out every book, writes
2635
+ `Name (_Book title_, p. 12)`, or `Name (Aelw. p12)` when that book declares an
2636
+ `abbreviation`. A compile renders and lays out every book, writes
2636
2637
  `pages.json` (each note's home book and the page its entry starts on) into the
2637
2638
  output directory, and renders again with those pages; `--no-compile` runs
2638
2639
  neither pass and prints `p. ?`. See the
@@ -1156,6 +1156,7 @@ pdf:
1156
1156
  books:
1157
1157
  - id: core
1158
1158
  title: The World of Thalorna
1159
+ abbreviation: Thal.
1159
1160
  subtitle: A Song of Heroic Lands Sourcebook
1160
1161
  coverImage: assets/images/covers/core.webp
1161
1162
  document: book.yaml
@@ -1209,7 +1210,8 @@ The keys of `pdf` itself are shared by every book:
1209
1210
 
1210
1211
  - `pdf.indexSeeAlso` adds a line to a book's index for each note that another
1211
1212
  book prints and this book mentions in authored prose, written
1212
- `Name: _Book title_, p. N` with the page the note's entry starts on in its
1213
+ `Name: _Book title_, p. N` (or `Name: Aelw. p307` when that book declares an
1214
+ `abbreviation`) with the page the note's entry starts on in its
1213
1215
  home book. It is `false` by default, and the index then lists only the notes
1214
1216
  the book is home to. See [The index](guides/book.md#the-index).
1215
1217
  - `pdf.indexHeads` lists leading words that open many names, such as
@@ -1236,20 +1238,31 @@ Targud` is also listed as `Targud, Kingdom of`, with the same pages as the
1236
1238
 
1237
1239
  Each entry of `pdf.books` is one PDF:
1238
1240
 
1239
- | Key | Type | Required | Default |
1240
- | ------------------------ | ------------------------- | -------------- | ----------------------- |
1241
- | `pdf.books[].id` | string | yes | — |
1242
- | `pdf.books[].title` | string | yes | — |
1243
- | `pdf.books[].subtitle` | string | no | `""` |
1244
- | `pdf.books[].coverImage` | string (path) | no | `""` — no cover |
1245
- | `pdf.books[].front` | string[] (markdown files) | no | `[]` |
1246
- | `pdf.books[].out` | string (file stem) | no | `<contentPackage>-<id>` |
1247
- | `pdf.books[].contents` | list (the tree, inline) | one of the two | — |
1248
- | `pdf.books[].document` | string (path to the tree) | one of the two | — |
1241
+ | Key | Type | Required | Default |
1242
+ | -------------------------- | ------------------------- | -------------- | ------------------------- |
1243
+ | `pdf.books[].id` | string | yes | — |
1244
+ | `pdf.books[].title` | string | yes | — |
1245
+ | `pdf.books[].abbreviation` | string | no | `""` — the title names it |
1246
+ | `pdf.books[].subtitle` | string | no | `""` |
1247
+ | `pdf.books[].coverImage` | string (path) | no | `""` — no cover |
1248
+ | `pdf.books[].front` | string[] (markdown files) | no | `[]` |
1249
+ | `pdf.books[].out` | string (file stem) | no | `<contentPackage>-<id>` |
1250
+ | `pdf.books[].contents` | list (the tree, inline) | one of the two | — |
1251
+ | `pdf.books[].document` | string (path to the tree) | one of the two | — |
1249
1252
 
1250
1253
  - `id` is lowercase letters, digits and hyphens, starting with a letter or
1251
1254
  digit. It is what `--book <id>` names and the tail of the default file stem.
1252
1255
  Each `id` is used once.
1256
+ - `abbreviation` is a short name for the book, such as `Aelw.`. It is used only
1257
+ where another book refers to this one: a reference to a note this book
1258
+ prints reads `Name (Aelw. p307)`, with a non-breaking space between the
1259
+ abbreviation and the page, and the other book's index, when
1260
+ `pdf.indexSeeAlso` is on, lists the note as `Name: Aelw. p307`. A book
1261
+ with no abbreviation is named by its full title in italics, `Name (_Book
1262
+ title_, p. 307)`. A link the text closes a bracket on takes the reference
1263
+ inside that bracket after a comma, so `(see The Crossing)` prints
1264
+ `(see The Crossing, Thal. p524)` rather than nesting brackets. Each abbreviation is used once, since a reader could not
1265
+ tell which book to open from one shared by two.
1253
1266
  - `contents` is the tree written inline — the same `sectionName` /
1254
1267
  `contents` / `filter` / `file` shape a tree file holds under its own
1255
1268
  `contents:` key — and `document` names the file holding it. A book declares
@@ -1296,6 +1309,10 @@ file, line and column the key is written at:
1296
1309
 
1297
1310
  > ``package-build config: `pdf.books[0].title` must be a non-empty string.``
1298
1311
 
1312
+ > ``package-build config: `pdf.books[0].abbreviation` must be a non-empty string.``
1313
+
1314
+ > ``package-build config: `pdf.books[1].abbreviation` is `Thal.`, which books `core` and `vedyara` both declare — a reader could not tell which book to open.``
1315
+
1299
1316
  > ``package-build config: `pdf.books[0]` must declare exactly one of `contents` and `document` — the tree written inline, or the file holding it.``
1300
1317
 
1301
1318
  > ``package-build config: `pdf.books[0].out` is `dist/core`, which is not a file stem — letters, digits, `.`, `_` and `-`, with no directory; the directory is `pdf.out`.``
@@ -1304,7 +1321,7 @@ file, line and column the key is written at:
1304
1321
 
1305
1322
  > ``package-build config: `pdf.books[0].front` must be a list of markdown files.``
1306
1323
 
1307
- > ``package-build config: `pdf.books[0].<key>` is not a recognized option (expected one of: id, title, subtitle, coverImage, front, out, contents, document).``
1324
+ > ``package-build config: `pdf.books[0].<key>` is not a recognized option (expected one of: id, title, abbreviation, subtitle, coverImage, front, out, contents, document).``
1308
1325
 
1309
1326
  > ``package-build config: `pdf.coverage` must be one of: warning, error, off.``
1310
1327
 
@@ -174,11 +174,11 @@ the Lyrebird Inn (The Tenebrae Volume, p. 214)
174
174
 
175
175
  Which form a wikilink takes depends on where its target is printed:
176
176
 
177
- | The target is | The link prints |
178
- | ------------------------------------------------------------- | ----------------------------------------- |
179
- | An entry of the same book, including a note the book reprints | An internal destination |
180
- | An entry of another book, and not of this one | `Name (_Book title_, p. N)`, with no link |
181
- | A note in no book | The web address of the note's page |
177
+ | The target is | The link prints |
178
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
179
+ | An entry of the same book, including a note the book reprints | An internal destination |
180
+ | An entry of another book, and not of this one | `Name (_Book title_, p. N)`, or `Name (Aelw. pN)` with an abbreviation, with no link; inside a bracket the text closes, `(Name, Aelw. pN)` |
181
+ | A note in no book | The web address of the note's page |
182
182
 
183
183
  The page is the page the PDF itself numbers, which is the number printed in the
184
184
  footer and used by the contents and the index, so the printed `p. N` is the
@@ -406,12 +406,12 @@ A book that prints at least one note ends with an **Index** chapter, grouped
406
406
  under letter headings. Nothing configures it: the build derives it from the
407
407
  notes the book prints.
408
408
 
409
- | Term | Source | Shown as |
410
- | ------------------------ | --------------------- | ---------------------------------------------------------------------------------- |
411
- | A note's name | `name.full` | the page of the note's entry in **bold**, then the pages of every authored mention |
412
- | A name without its head | `name.full` | the same pages as the note's entry, filed as `Targud, Kingdom of` |
413
- | An alias | `name.aliases` | _see_ the note's name |
414
- | An event's in-world name | `data.events[].names` | _see_ the name of the note that holds the event |
409
+ | Term | Source | Shown as |
410
+ | ------------------------ | ----------------------------- | ---------------------------------------------------------------------------------- |
411
+ | A note's name | `name.sort`, else `name.full` | the page of the note's entry in **bold**, then the pages of every authored mention |
412
+ | A name without its head | `name.full` | the same pages as the note's entry, filed as `Targud, Kingdom of` |
413
+ | An alias | `name.aliases` | _see_ the note's name |
414
+ | An event's in-world name | `data.events[].names` | _see_ the name of the note that holds the event |
415
415
 
416
416
  A name that opens with a generic word a reader would not look under, such as
417
417
  `Kingdom of Targud` or `Ritual: Blessing`, is also filed under the rest of the
@@ -438,7 +438,7 @@ first book in `pdf.books` that selects it. In a package of several books:
438
438
  as the cross-book references (`pages.json`), so the page a reference prints
439
439
  for a note is the page its home book's index lists for the note's own entry.
440
440
  - A mention of a note another book prints is not a term of the mentioning
441
- book. The mention reads `Name (_Book title_, p. N)` in the text, and the note's
441
+ book. The mention reads `Name (_Book title_, p. N)` in the text (`Name (Aelw. pN)` when the home book declares an `abbreviation`), and the note's
442
442
  entry and its page are in the other book's index.
443
443
 
444
444
  Set [`pdf.indexSeeAlso`](../configuration.md) to `true` to list those mentions
@@ -447,6 +447,8 @@ then has a line, filed under its name:
447
447
 
448
448
  > Sword: _Book A_, p. 12
449
449
 
450
+ A home book with an `abbreviation` files the line as `Sword: BkA. p12`.
451
+
450
452
  Two kinds of link add nothing:
451
453
 
452
454
  - **Links in generated sections** such as "Within" or "Governed by". They name
@@ -458,7 +460,7 @@ Two kinds of link add nothing:
458
460
 
459
461
  Terms sort on the ASCII fold the content index derives for names (`nameAscii`),
460
462
  ignoring case, so an accented name files under its base letter and the order
461
- matches the table of contents. A term that does not begin with a letter files
463
+ matches the table of contents. A note's own entry reads as its `name.sort` when it declares one, in index form such as `Black Flame, The` or `Hilvan, Doran`, and as its `name.full` otherwise; it files, and takes its letter heading, from that form. A note that declares a `sort` is not also filed under `pdf.indexHeads`. A term that does not begin with a letter files
462
464
  first, under `#`. An alias or event name that folds to the same text as the
463
465
  note's own name is not listed again.
464
466
 
@@ -182,17 +182,14 @@ release runs in two halves, one on each:
182
182
  pushes the tag `v<version>` at the merged version commit. Releasing from
183
183
  Gitea is switched on by the repository variable `HL_RELEASE_ENABLED`.
184
184
  2. **Publish, on GitHub.** The push mirror carries the tag to GitHub, where it
185
- starts the **Publish to npm** workflow, `.github/workflows/release.yml`. It
186
- refuses a tag that names a commit outside `main` or that is not
187
- `v<version>` of the tagged `package.json`. When npm does not have the
188
- version yet, it runs the tests and publishes through npm Trusted Publishing
189
- with provenance. It then creates the GitHub Release from the version's
190
- changelog section, unless one exists.
185
+ starts the **Publish to npm** workflow, `.github/workflows/release.yml`, the
186
+ only workflow GitHub runs. Every check has already run on Gitea before the
187
+ tag exists, so the workflow installs, publishes through npm Trusted
188
+ Publishing with provenance, and creates the GitHub Release from the
189
+ version's changelog section.
191
190
 
192
191
  Nothing on Gitea publishes to npm, and nothing on GitHub runs for a branch
193
- push. Every step of the GitHub half is safe to repeat: re-running it, or
194
- dispatching it with the tag as its ref, finishes a release whose publish or
195
- Release step failed and changes nothing for one that completed. The
192
+ push. The
196
193
  [npm package page](https://www.npmjs.com/package/@heroiclands/package-build)
197
194
  shows the published version and README.
198
195
 
@@ -513,7 +513,7 @@ tool:
513
513
  organisation-level and defined in `HeroicLands/.github`, so the repository
514
514
  carries no label registry of its own.
515
515
 
516
- `.github/workflows/` then carries the repository's own CI. The org-wide
516
+ `.gitea/workflows/` carries the repository's own CI. The org-wide
517
517
  `HeroicLands/.github` repository supplies the shared composite actions those
518
518
  workflows `uses:` — `labels`, `no-attribution` and `todos` — plus the reusable
519
519
  site-deploy workflow, so a fix to any of them reaches every repository at once.
@@ -522,16 +522,16 @@ site-deploy workflow, so a fix to any of them reaches every repository at once.
522
522
 
523
523
  A summary, because "where does this come from" is the question that recurs.
524
524
 
525
- | Read | By |
526
- | ----------------------------- | --------------------------------------------------------------------------------------------------------- |
527
- | `package.json` | The Foundry package id, the version, the release addresses; the site's `baseURL`, description and author. |
528
- | `package-build.config.yaml` | Everything else about the build. |
529
- | `assets/content/**/*.md` | Every content command. |
530
- | `.gitignore` | `package-build format`, `package-build markdown`. |
531
- | `.prettierignore` | `package-build format`. |
532
- | `lang/*.json` | `package-build lang`. |
533
- | `.env.local` | `package-build deploy`. |
534
- | `.github/workflows/build.yml` | The `pre-push` hook, for its step list. |
525
+ | Read | By |
526
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
527
+ | `package.json` | The Foundry package id, the version, the release addresses; the site's `baseURL`, description and author. |
528
+ | `package-build.config.yaml` | Everything else about the build. |
529
+ | `assets/content/**/*.md` | Every content command. |
530
+ | `.gitignore` | `package-build format`, `package-build markdown`. |
531
+ | `.prettierignore` | `package-build format`. |
532
+ | `lang/*.json` | `package-build lang`. |
533
+ | `.env.local` | `package-build deploy`. |
534
+ | `.gitea/workflows/build.yml` | The `pre-push` hook, for its step list. |
535
535
 
536
536
  Everything written goes under `build/`. Nothing the toolchain generates is
537
537
  committed.
@@ -379,7 +379,13 @@ content, comment out its YAML lines with `#`. Comments remain valid YAML text
379
379
  and do not become data in the JSONL index, generated pages, or Foundry documents.
380
380
 
381
381
  `name` is a map with a required nonempty `full` string when present. It may
382
- also contain `aliases`, an ordered list of nonempty strings. A being with
382
+ also contain `aliases`, an ordered list of nonempty strings, and `sort`, a nonempty
383
+ string giving the form the name takes in an index, such as `Black Flame, The` or
384
+ `Hilvan, Doran`. `sort` files the note wherever notes are ordered by name (page
385
+ lists, generated lists of related notes, the book index, the `nameSort` column)
386
+ and is the note's entry text in the book index; every other list still displays
387
+ `full`. Without `sort`, the note files and indexes by `full`. Ordering compares
388
+ the ASCII fold of the key (`Árdavon` files beside `Ardavon`). A being with
383
389
  `subType: character` may additionally contain nonempty `given` and `clan`
384
390
  strings. Other keys under `name` are errors. For a character or NPC, the first
385
391
  alias is its nickname. A being's homes belong in `data.homes`.
@@ -2916,7 +2922,9 @@ reads `NULL` rather than failing. `data.culture` is the one field that reads as
2916
2922
  a derived value: the note's own culture, or the one it inherits. See
2917
2923
  [Culture in the index](#culture-in-the-index).
2918
2924
 
2919
- **`notes` leaves out stubs; `entries` does not.** Both carry a derived `state`
2925
+ **`notes` leaves out stubs; `entries` does not.** Both carry a derived `nameSort`
2926
+ column — the note's `name.sort` when it declares one, otherwise its `name.full`,
2927
+ unfolded — for `ORDER BY`, and a derived `state`
2920
2928
  column — `stub`, `draft` or `full` — and `notes` is exactly
2921
2929
  `SELECT * FROM entries WHERE state <> 'stub'`. So every table already written
2922
2930
  selects what it always selected, and gains `state` for free; a table that should
@@ -2957,20 +2965,21 @@ query rather than a list kept by hand:
2957
2965
 
2958
2966
  ````markdown
2959
2967
  ```sql
2960
- SELECT note AS _ref, "when" AS "When", summary AS "What happened"
2968
+ SELECT note AS _ref, whenText AS "When", summary AS "What happened"
2961
2969
  FROM events
2962
2970
  WHERE kind IN ('war', 'siege', 'battle')
2963
2971
  ORDER BY whenSort
2964
2972
  ```
2965
2973
  ````
2966
2974
 
2967
- | Column | What it holds |
2968
- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2969
- | `note` | The canonical Address of the note the event is written on. |
2970
- | `address` | The event's own Address: `note#id` where the event has an `id`, the note's Address alone where it has none. |
2971
- | `id`, `kind`, `depth`, … | Every key of an event, each its own column under its own name — `id`, `kind`, `depth`, `when`, `until`, `recurs`, `summary`, `standing`, `names`, `where`, `who`, `follows`, `accounts`, `unresolved`, `sources`, `stated`. |
2972
- | `whenSort`, `untilSort` | The date's position on the canonical axis: the canonical year, with a day as its fraction of the year where the calendar states the year's length. `ORDER BY whenSort` is date order. |
2973
- | `whenYear`, `untilYear` | The canonical year alone, for a filter such as `whenYear BETWEEN 200 AND 299`. |
2975
+ | Column | What it holds |
2976
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2977
+ | `note` | The canonical Address of the note the event is written on. |
2978
+ | `address` | The event's own Address: `note#id` where the event has an `id`, the note's Address alone where it has none. |
2979
+ | `id`, `kind`, `depth`, … | Every key of an event, each its own column under its own name — `id`, `kind`, `depth`, `when`, `until`, `recurs`, `summary`, `standing`, `names`, `where`, `who`, `follows`, `accounts`, `unresolved`, `sources`, `stated`. |
2980
+ | `whenSort`, `untilSort` | The date's position on the canonical axis: the canonical year, with a day as its fraction of the year where the calendar states the year's length. `ORDER BY whenSort` is date order. |
2981
+ | `whenText`, `untilText` | The date as a reader sees it, printed as `dateformat` prints it in the calendar the owning note names in `data.calendar`, else the one the world note names, else in the era the date itself names; `~` marks an approximate date. A date that does not resolve keeps the text its note wrote. |
2982
+ | `whenYear`, `untilYear` | The canonical year alone, for a filter such as `whenYear BETWEEN 200 AND 299`. |
2974
2983
 
2975
2984
  `when` and `until` read as the text the note wrote, `~` included. The sort
2976
2985
  keys and years are positions on the canonical axis, which counts a year `0`
@@ -3498,7 +3507,7 @@ The caption attaches to exactly the next supported block:
3498
3507
 
3499
3508
  The optional `type` attribute overrides inference and accepts `figure`, `table`, `poetry`, `code`, `prose`, and `example`. `type=example` uses **Example 1**, **Example 2**, and so on. It does not allow an unsupported target. A heading, list, thematic break, another caption or end of document after a caption is an error. Caption markers without separating spaces or caption text are errors too.
3500
3509
 
3501
- Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
3510
+ Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. On a captioned table or SQL fence, `.full-width` sets the table across the page in a PDF book instead of in one column (`: Regional trade routes {.full-width}`); the other surfaces ignore it. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
3502
3511
 
3503
3512
  Each numbered type has its own counter. Websites and Foundry number within a note; books number across their reading order. Unnumbered captions neither display nor consume a number. A map image can retain its asset-derived **Map** classification when no type is specified.
3504
3513
 
@@ -3763,11 +3772,14 @@ The build appends sections to the end of a note's body that list the events conc
3763
3772
 
3764
3773
  | View | Anchor | On | Rows | Columns |
3765
3774
  | ------------- | ------------ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
3766
- | Chronology | `chronology` | a `place` | events whose `where.locus` names the place or a place below it in `parents`; events whose `where.reach` names one, with that entry's `how`; every `depth: world` event as context | What happened, When, Felt here |
3767
- | Events | `events` | a `being`, an `affiliation`, a `lore` note of `subType: culture` | events whose `who[].ref` names the note, with the role it played | What happened, When, Role |
3768
- | Accounts | `accounts` | an `affiliation`, a `place`, a `lore` note of `subType: culture` | each `accounts[]` entry the note gives, with the event it concerns | Event, When, What they say, Agrees |
3769
- | What followed | `followed` | a note holding events | events whose `follows[].event` names one of this note's events, with how | What followed, When, How |
3770
-
3775
+ | Chronology | `chronology` | a `place` | events whose `where.locus` names the place or a place below it in `parents`; events whose `where.reach` names one, with that entry's `how`; every `depth: world` event as context | When, What happened, Felt here |
3776
+ | Events | `events` | a `being`, an `affiliation`, a `lore` note of `subType: culture` | events whose `who[].ref` names the note, with the role it played | When, What happened, Role |
3777
+ | Accounts | `accounts` | an `affiliation`, a `place`, a `lore` note of `subType: culture` | each `accounts[]` entry the note gives, with the event it concerns | When, Event, What they say, Agrees |
3778
+ | What followed | `followed` | a note holding events | events whose `follows[].event` names one of this note's events, with how | When, What followed, How |
3779
+
3780
+ - **Every view's first column is When**, the event's date written in the setting's calendar — the `events` relation's `whenText` column. An event whose date does not resolve shows the text its note wrote.
3781
+ - **A chronology's Felt here column appears only where some row is felt in the place.** A place whose rows are all events that happened there, or world events, has the When and What happened columns alone.
3782
+ - **A chronology is always set across the full page width in a PDF book**, whatever its columns.
3771
3783
  - **The views follow everything the author wrote**, in the order of the [generated sections](#generated-sections).
3772
3784
  - **A view with no rows is not generated.** A chronology is generated only where the place has rows of its own; the world events beside them are context and make no chronology by themselves.
3773
3785
  - **An author's own section replaces a view.** A note whose body declares a prose anchor with the view's slug — `# Chronology {#chronology}`, or a block or span with that `id` — gets no generated section for it, and its own section is left exactly as written. This is the one way to turn a view off, and the way to choose a different table: write the section and its own `sql` fence.
@@ -18,7 +18,7 @@ These are the only keys a note writes at its top level, in the order the formatt
18
18
  | Key | Meaning |
19
19
  | ------------- | ----------------------------------------------------------------------------------------- |
20
20
  | `shortcode` | The note's own address segment, unique within its type. |
21
- | `name` | The display names — a required `full`, and any `aliases`. |
21
+ | `name` | The display names — a required `full`, and any `aliases` and `sort`. |
22
22
  | `type` | What the note is about, which decides its vocabulary and its document. |
23
23
  | `subType` | The type's own genre, where it declares one. |
24
24
  | `description` | The short page summary. |
@@ -108,8 +108,9 @@ import { analyzeNameSpans } from "./names.mjs";
108
108
  import fs from "node:fs";
109
109
  import path from "node:path";
110
110
 
111
- import unidecode from "unidecode";
111
+ import { asciiName } from "./name-sort.mjs";
112
112
 
113
+ export { asciiName };
113
114
  import { loadForeignIndexes, metadataFileName } from "./metadata-index.mjs";
114
115
  import { collectAssetRecords } from "./asset-index.mjs";
115
116
  import { checkForeignAssetBindings } from "./asset-bindings.mjs";
@@ -245,44 +246,6 @@ export function noteAddress(frontmatter, contentPackage) {
245
246
  };
246
247
  }
247
248
 
248
- /**
249
- * A note's display name reduced to printable 7-bit ASCII.
250
- *
251
- * Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
252
- * `Kèthîra` — and nobody types them. A reader searching the index, or an editor
253
- * completing a wikilink, needs a form that matches what a keyboard produces, so
254
- * the record states one rather than leaving every consumer to invent it (and to
255
- * invent a *different* one, which is how two searches over the same data come
256
- * to disagree).
257
- *
258
- * **Transliterated, not stripped.** `unidecode` — the same table
259
- * {@link slugify} already runs, so an ASCII name and a slug can never disagree
260
- * about a character — carries a letter across rather than deleting it:
261
- * diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
262
- * `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
263
- * even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
264
- * would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
265
- *
266
- * Anything still outside printable ASCII after that becomes a space, and runs
267
- * of whitespace collapse — a space rather than nothing, so a character that
268
- * transliterates away cannot silently weld two words together.
269
- *
270
- * The value is emitted even when it equals the name, so a consumer matching on
271
- * it never has to branch on whether the name happened to be ASCII already.
272
- *
273
- * @param {unknown} name - The note's `name.full`.
274
- * @returns {string|null} The ASCII form, or `null` when there is no name, or
275
- * nothing printable survives.
276
- */
277
- export function asciiName(name) {
278
- if (typeof name !== "string") return null;
279
- const folded = unidecode(name)
280
- .replace(/[^\x20-\x7E]/g, " ")
281
- .replace(/\s+/g, " ")
282
- .trim();
283
- return folded === "" ? null : folded;
284
- }
285
-
286
249
  /**
287
250
  * A note's `name.aliases` reduced to printable 7-bit ASCII, in order.
288
251
  *