@heroiclands/package-build 23.12.3 → 23.12.4

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.12.4
4
+
5
+ ### Patch Changes
6
+
7
+ 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.
8
+
9
+ 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.
10
+
11
+ `project init` scaffolds the build workflow at `.gitea/workflows/build.yml`, and `project check` and `package-build ci` read it from there.
12
+
3
13
  ## 23.12.3
4
14
 
5
15
  ### 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/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,29 @@ 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)`. Each abbreviation is used once, since a reader could not
1263
+ tell which book to open from one shared by two.
1253
1264
  - `contents` is the tree written inline — the same `sectionName` /
1254
1265
  `contents` / `filter` / `file` shape a tree file holds under its own
1255
1266
  `contents:` key — and `document` names the file holding it. A book declares
@@ -1296,6 +1307,10 @@ file, line and column the key is written at:
1296
1307
 
1297
1308
  > ``package-build config: `pdf.books[0].title` must be a non-empty string.``
1298
1309
 
1310
+ > ``package-build config: `pdf.books[0].abbreviation` must be a non-empty string.``
1311
+
1312
+ > ``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.``
1313
+
1299
1314
  > ``package-build config: `pdf.books[0]` must declare exactly one of `contents` and `document` — the tree written inline, or the file holding it.``
1300
1315
 
1301
1316
  > ``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 +1319,7 @@ file, line and column the key is written at:
1304
1319
 
1305
1320
  > ``package-build config: `pdf.books[0].front` must be a list of markdown files.``
1306
1321
 
1307
- > ``package-build config: `pdf.books[0].<key>` is not a recognized option (expected one of: id, title, subtitle, coverImage, front, out, contents, document).``
1322
+ > ``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
1323
 
1309
1324
  > ``package-build config: `pdf.coverage` must be one of: warning, error, off.``
1310
1325
 
@@ -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 |
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
@@ -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
@@ -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.
@@ -780,6 +780,7 @@ export async function buildPdf({
780
780
  books: buildable.map((b) => ({
781
781
  id: b.entry.id,
782
782
  title: b.entry.title,
783
+ abbreviation: b.entry.abbreviation,
783
784
  plan: b.selection.plan,
784
785
  })),
785
786
  homes,
@@ -102,11 +102,13 @@ function letterOf(key) {
102
102
  * @param {string[]} [opts.heads] - Leading words (`pdf.indexHeads`) matched
103
103
  * literally against the start of a name; a name that opens with one is also
104
104
  * listed as `<rest>, <head>`, the head without its trailing space or colon. None when absent.
105
- * @param {Array<{slug: string, name: string, title: string, page: string|number}>} [opts.foreign] -
105
+ * @param {Array<{slug: string, name: string, title: string, abbreviation?: string,
106
+ * page: string|number}>} [opts.foreign] -
106
107
  * Notes another book prints, listed as `Name: Book title, p. N` once this
107
- * book mentions them.
108
+ * book mentions them, or `Name: Aelw. p12` when the home book has an
109
+ * abbreviation.
108
110
  * @returns {Array<{letter: string, name: string, slug: string, see?: string,
109
- * foreign?: {title: string, page: string}}>} Terms with `see` set for a
111
+ * foreign?: {title: string, abbreviation?: string, page: string}}>} Terms with `see` set for a
110
112
  * cross-reference to the main name it points at.
111
113
  */
112
114
  export function indexTerms(entries, { owns = () => true, foreign = [], heads = [] } = {}) {
@@ -198,7 +200,11 @@ export function indexTerms(entries, { owns = () => true, foreign = [], heads = [
198
200
  keyed.push({
199
201
  name,
200
202
  slug: other.slug,
201
- foreign: { title: String(other.title), page: String(other.page) },
203
+ foreign: {
204
+ title: String(other.title),
205
+ ...(other.abbreviation ? { abbreviation: String(other.abbreviation) } : {}),
206
+ page: String(other.page),
207
+ },
202
208
  key: sortKey(name),
203
209
  });
204
210
  }
@@ -234,7 +240,7 @@ export function indexTerms(entries, { owns = () => true, foreign = [], heads = [
234
240
  export function indexPreamble({ seeAlso = false } = {}) {
235
241
  return [
236
242
  // Zero-size and invisible; the label is what the chapter queries.
237
- "#let book-ix(key, main: false) = [#metadata((key: key, main: main)) <book-index>]",
243
+ "#let book-ix(key, main: false) = [#metadata((key: key, main: main))<book-index>]",
238
244
  "#let book-index(terms) = context {",
239
245
  " set par(justify: false, first-line-indent: 0em, hanging-indent: 1em, spacing: 0.45em)",
240
246
  " set text(size: 8.8pt)",
@@ -271,7 +277,12 @@ export function indexPreamble({ seeAlso = false } = {}) {
271
277
  ...(seeAlso ?
272
278
  [
273
279
  " if away != none {",
274
- " par[#term.name: #emph[#away.title], p. #away.page]",
280
+ ' let short = away.at("abbreviation", default: none)',
281
+ " if short != none {",
282
+ " par[#term.name: #short~p#away.page]",
283
+ " } else {",
284
+ " par[#term.name: #emph[#away.title], p. #away.page]",
285
+ " }",
275
286
  " continue",
276
287
  " }",
277
288
  ]
@@ -309,8 +320,11 @@ export function indexChapter(entries, title, opts = {}) {
309
320
  }${
310
321
  term.foreign === undefined ?
311
322
  ""
312
- : `, foreign: (title: "${escapeString(term.foreign.title)}", ` +
313
- `page: "${escapeString(term.foreign.page)}")`
323
+ : `, foreign: (title: "${escapeString(term.foreign.title)}", ${
324
+ term.foreign.abbreviation === undefined ?
325
+ ""
326
+ : `abbreviation: "${escapeString(term.foreign.abbreviation)}", `
327
+ }` + `page: "${escapeString(term.foreign.page)}")`
314
328
  })`,
315
329
  );
316
330
  return [
@@ -142,7 +142,7 @@ export function readEntryPages(typPath, fontArgs, binary = "typst") {
142
142
  * The books that render, in configuration order.
143
143
  * @param {Map<string, string>} input.homes - Each note's key → its home book's
144
144
  * id, from {@link module:engine/pdf-coverage.coverage}.
145
- * @returns {Array<{key: string, book: string, title: string, anchor: string,
145
+ * @returns {Array<{key: string, book: string, title: string, abbreviation?: string, anchor: string,
146
146
  * record: object, slug: string, aliases: string[]}>} One per homed note, in book order and
147
147
  * then entry order.
148
148
  */
@@ -160,6 +160,7 @@ export function homeEntries({ books, homes }) {
160
160
  key,
161
161
  book: book.id,
162
162
  title: book.title,
163
+ abbreviation: book.abbreviation,
163
164
  anchor: entry.anchor,
164
165
  record,
165
166
  slug: String(record?.address?.slug ?? record?.shortcode ?? ""),
@@ -246,13 +247,21 @@ export function elsewhereFor({ entries, book, printed, table, seeAlso = false })
246
247
  const keys = [entry.slug, ...entry.aliases].filter((key) => key && !printed.has(key));
247
248
  const page = table?.[entry.key]?.page ?? UNMEASURED_PAGE;
248
249
  for (const key of keys) {
249
- if (!map.has(key)) map.set(key, { slug: entry.slug, title: entry.title, page });
250
+ if (!map.has(key)) {
251
+ map.set(key, {
252
+ slug: entry.slug,
253
+ title: entry.title,
254
+ abbreviation: entry.abbreviation,
255
+ page,
256
+ });
257
+ }
250
258
  }
251
259
  if (seeAlso && entry.slug && !printed.has(entry.slug)) {
252
260
  listed.push({
253
261
  slug: entry.slug,
254
262
  name: String(entry.record?.name?.full ?? "").trim(),
255
263
  title: entry.title,
264
+ abbreviation: entry.abbreviation,
256
265
  page,
257
266
  });
258
267
  }
@@ -841,12 +841,10 @@ function renderBlock(tokens, i, out, ctx) {
841
841
  }
842
842
 
843
843
  /**
844
- * Self-contained dictionary rows with the website's 150 CSS-pixel hanging indent.
845
- *
846
- * Each row remains a breakable block; only the bold term is an inline box. The
847
- * negative first-line space lets its label start at the original margin while
848
- * subsequent paragraphs, lists and blocks retain the definition's inset. A long
849
- * term enlarges its slot without changing the continuation margin.
844
+ * Definition rows set as a book sets them: the term in bold, run in at the
845
+ * margin, and the definition following on the same line, with its continuation
846
+ * lines and any further paragraphs, lists and blocks indented 1em. A long term
847
+ * takes no more room than a short one. Rows are spaced as paragraphs are.
850
848
  *
851
849
  * Scope the helper to this list so exported Markdown fragments also work without
852
850
  * a book preamble, including nested definition lists.
@@ -854,18 +852,12 @@ function renderBlock(tokens, i, out, ctx) {
854
852
  * @returns {string} Typst markup.
855
853
  */
856
854
  function renderDefinitionList(entries) {
857
- return `#context {
858
- let book-definition(term, body) = {
859
- let indent = 112.5pt
860
- let gap = (0.75em).to-absolute()
861
- let label = text(weight: "bold", term)
862
- let slot = calc.max(indent - gap, measure(label).width)
863
- block(width: 100%, breakable: true, inset: (left: indent), above: 0pt, below: 0.5em)[
864
- #set par(first-line-indent: 0pt, hanging-indent: 0pt)
865
- #set text(weight: "regular")
866
- #h(-indent)#box(width: slot, label)#h(gap)#body
867
- ]
868
- }
855
+ return `#{
856
+ let book-definition(term, body) = block(width: 100%, breakable: true, inset: (left: 1em))[
857
+ #set par(first-line-indent: 0pt, hanging-indent: 0pt)
858
+ #set text(weight: "regular")
859
+ #h(-1em)#text(weight: "bold", term)#h(0.5em)#body
860
+ ]
869
861
  ${entries.join("\n ")}
870
862
  }`;
871
863
  }
@@ -1457,17 +1449,22 @@ function renderLink(href, inner, ctx) {
1457
1449
 
1458
1450
  /**
1459
1451
  * A reference to a note another book prints, as Typst: the words the link
1460
- * carried, then the book and the page, `Name (_Book title_, p. 12)`.
1452
+ * carried, then the book and the page. A book with an abbreviation reads
1453
+ * `Name (Aelw. p12)`; one without reads `Name (_Book title_, p. 12)`.
1461
1454
  *
1462
- * The space before the page number does not break, so a number never starts a
1463
- * line of its own.
1455
+ * The space before the page number, or between the abbreviation and the page,
1456
+ * does not break, so a number never starts a line of its own.
1464
1457
  *
1465
1458
  * @param {string} inner - The already-rendered link text.
1466
- * @param {{title: string, page: string|number}} away - The home book's title
1467
- * and the page the entry starts on.
1459
+ * @param {{title: string, abbreviation?: string, page: string|number}} away -
1460
+ * The home book's title, its abbreviation when it declares one, and the page
1461
+ * the entry starts on.
1468
1462
  * @returns {string} Typst markup.
1469
1463
  */
1470
1464
  export function crossBookReference(inner, away) {
1465
+ if (away.abbreviation) {
1466
+ return `${inner} (${escapeTypst(away.abbreviation)}~p${away.page})`;
1467
+ }
1471
1468
  return `${inner} (#emph[${escapeTypst(away.title)}], p.~${away.page})`;
1472
1469
  }
1473
1470
 
@@ -308,7 +308,7 @@ function projectText(answers) {
308
308
  "Labels are organization-level, defined in `HeroicLands/.github`.",
309
309
  "",
310
310
  ].join("\n"),
311
- ".github/workflows/build.yml": [
311
+ ".gitea/workflows/build.yml": [
312
312
  "# SPDX-License-Identifier: GPL-3.0-or-later",
313
313
  "name: Build and Test",
314
314
  "on:",
@@ -424,7 +424,7 @@ export function checkProject(rootDir) {
424
424
  "LICENSE.md",
425
425
  ".changeset/config.json",
426
426
  ".github/ISSUE_REPORTING.md",
427
- ".github/workflows/build.yml",
427
+ ".gitea/workflows/build.yml",
428
428
  ];
429
429
  for (const file of required) {
430
430
  if (!fs.existsSync(path.join(root, file))) add(file, "required project file is missing");
package/githooks/pre-push CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env sh
2
2
  #
3
3
  # Before pushing, run exactly what the Build & Test workflow runs — in a
4
- # container, over a clean export of HEAD — so a red GitHub check is something
4
+ # container, over a clean export of HEAD — so a red CI check is something
5
5
  # you chose rather than something you discover.
6
6
  #
7
7
  # The steps are not listed here. `ci/ci-steps.mjs` reads them out of
8
- # `.github/workflows/build.yml`, because a hook holding its own copy of the
8
+ # `.gitea/workflows/build.yml`, because a hook holding its own copy of the
9
9
  # command list is a second statement of one thing, and the copy is the one that
10
10
  # goes stale — silently, since running four of five steps still exits 0.
11
11
  #
12
12
  # **On push rather than at `gh pr create`**, because a push is where this
13
- # repository's work reaches GitHub: normally one, just before the pull request.
13
+ # repository's work reaches the forge: normally one, just before the pull request.
14
14
  # Nothing to remember, no wrapper command, and no entry in package.json — the
15
15
  # hook is the whole mechanism.
16
16
  #
@@ -92,7 +92,7 @@ status=$?
92
92
  # 2 means the check could not run — no Docker — which is not the same as a
93
93
  # failure and must not stop the push. Whoever is pushing may not be the
94
94
  # maintainer, may be on a machine without Docker, and is entitled to open a
95
- # pull request; GitHub is what enforces the workflow. Said loudly, though: a
95
+ # pull request; CI is what enforces the workflow. Said loudly, though: a
96
96
  # skipped check that reads like a passed one is how a guard loses its meaning.
97
97
  # 127 is "could not execute it at all" — no node on PATH, package not
98
98
  # installed. Infrastructure, not a verdict, so it is handled the same way.
@@ -104,6 +104,6 @@ fi
104
104
 
105
105
  if [ "$status" -ne 0 ]; then
106
106
  echo ""
107
- echo "pre-push: refusing to push — GitHub would report the same failure."
107
+ echo "pre-push: refusing to push — CI would report the same failure."
108
108
  exit 1
109
109
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.12.3",
3
+ "version": "23.12.4",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -159,6 +159,11 @@ export type ResolvedPdfBook = {
159
159
  */
160
160
  id: string;
161
161
  title: string;
162
+ /**
163
+ * Names the book in another book's
164
+ * references; `""` when none is declared.
165
+ */
166
+ abbreviation: string;
162
167
  /**
163
168
  * `""` when none is declared.
164
169
  */
@@ -20,11 +20,13 @@ export function indexMarker(slug: string, main?: boolean): string;
20
20
  * @param {string[]} [opts.heads] - Leading words (`pdf.indexHeads`) matched
21
21
  * literally against the start of a name; a name that opens with one is also
22
22
  * listed as `<rest>, <head>`, the head without its trailing space or colon. None when absent.
23
- * @param {Array<{slug: string, name: string, title: string, page: string|number}>} [opts.foreign] -
23
+ * @param {Array<{slug: string, name: string, title: string, abbreviation?: string,
24
+ * page: string|number}>} [opts.foreign] -
24
25
  * Notes another book prints, listed as `Name: Book title, p. N` once this
25
- * book mentions them.
26
+ * book mentions them, or `Name: Aelw. p12` when the home book has an
27
+ * abbreviation.
26
28
  * @returns {Array<{letter: string, name: string, slug: string, see?: string,
27
- * foreign?: {title: string, page: string}}>} Terms with `see` set for a
29
+ * foreign?: {title: string, abbreviation?: string, page: string}}>} Terms with `see` set for a
28
30
  * cross-reference to the main name it points at.
29
31
  */
30
32
  export function indexTerms(entries: object[], { owns, foreign, heads }?: {
@@ -34,6 +36,7 @@ export function indexTerms(entries: object[], { owns, foreign, heads }?: {
34
36
  slug: string;
35
37
  name: string;
36
38
  title: string;
39
+ abbreviation?: string;
37
40
  page: string | number;
38
41
  }[] | undefined;
39
42
  }): Array<{
@@ -43,6 +46,7 @@ export function indexTerms(entries: object[], { owns, foreign, heads }?: {
43
46
  see?: string;
44
47
  foreign?: {
45
48
  title: string;
49
+ abbreviation?: string;
46
50
  page: string;
47
51
  };
48
52
  }>;
@@ -57,7 +57,7 @@ export function readEntryPages(typPath: string, fontArgs: string[], binary?: str
57
57
  * The books that render, in configuration order.
58
58
  * @param {Map<string, string>} input.homes - Each note's key → its home book's
59
59
  * id, from {@link module:engine/pdf-coverage.coverage}.
60
- * @returns {Array<{key: string, book: string, title: string, anchor: string,
60
+ * @returns {Array<{key: string, book: string, title: string, abbreviation?: string, anchor: string,
61
61
  * record: object, slug: string, aliases: string[]}>} One per homed note, in book order and
62
62
  * then entry order.
63
63
  */
@@ -74,6 +74,7 @@ export function homeEntries({ books, homes }: {
74
74
  key: string;
75
75
  book: string;
76
76
  title: string;
77
+ abbreviation?: string;
77
78
  anchor: string;
78
79
  record: object;
79
80
  slug: string;
@@ -166,18 +166,21 @@ export function markdownToTypst(markdown: string, opts?: {
166
166
  export function absolutePageUrl(url: string, site?: string): string;
167
167
  /**
168
168
  * A reference to a note another book prints, as Typst: the words the link
169
- * carried, then the book and the page, `Name (_Book title_, p. 12)`.
169
+ * carried, then the book and the page. A book with an abbreviation reads
170
+ * `Name (Aelw. p12)`; one without reads `Name (_Book title_, p. 12)`.
170
171
  *
171
- * The space before the page number does not break, so a number never starts a
172
- * line of its own.
172
+ * The space before the page number, or between the abbreviation and the page,
173
+ * does not break, so a number never starts a line of its own.
173
174
  *
174
175
  * @param {string} inner - The already-rendered link text.
175
- * @param {{title: string, page: string|number}} away - The home book's title
176
- * and the page the entry starts on.
176
+ * @param {{title: string, abbreviation?: string, page: string|number}} away -
177
+ * The home book's title, its abbreviation when it declares one, and the page
178
+ * the entry starts on.
177
179
  * @returns {string} Typst markup.
178
180
  */
179
181
  export function crossBookReference(inner: string, away: {
180
182
  title: string;
183
+ abbreviation?: string;
181
184
  page: string | number;
182
185
  }): string;
183
186
  /**
@@ -1,135 +0,0 @@
1
- /*
2
- * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
- * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
- *
5
- * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
- * You may copy, modify, and distribute it under the terms of that license.
7
- *
8
- * For full terms, see the LICENSE.md file in the project root or visit:
9
- * https://www.gnu.org/licenses/gpl-3.0.html
10
- *
11
- * SPDX-License-Identifier: GPL-3.0-or-later
12
- */
13
-
14
- /**
15
- * Asserts that npm serves an exact package version.
16
- *
17
- * `npm publish` returns before the registry serves the new version, and a
18
- * version that is accepted but never served is indistinguishable from success
19
- * by the publish alone. This polls `npm view <name>@<version> version` until
20
- * it prints the version, then exits 0. When the time budget runs out it exits 1
21
- * with an `::error::` annotation naming the package, the version and the last
22
- * registry answer.
23
- *
24
- * Usage: `node ci/confirm-npm-version.mjs <name> <version> [--interval <s>]
25
- * [--timeout <s>]`. The defaults are 15 seconds between queries and 600 seconds
26
- * overall.
27
- */
28
-
29
- import { spawnSync } from "node:child_process";
30
- import { fileURLToPath } from "node:url";
31
-
32
- /** Seconds between registry queries. */
33
- export const DEFAULT_INTERVAL_SECONDS = 15;
34
- /** Seconds the registry has to serve the version. */
35
- export const DEFAULT_TIMEOUT_SECONDS = 600;
36
-
37
- /**
38
- * Ask the registry which version `name@version` resolves to.
39
- *
40
- * @param {string} spec - `<name>@<version>`.
41
- * @returns {string} The version npm printed, or its error text when the query
42
- * fails (an unpublished version answers E404).
43
- */
44
- export function queryNpm(spec) {
45
- const result = spawnSync("npm", ["view", spec, "version"], { encoding: "utf8" });
46
- if (result.error) return result.error.message;
47
- const out = (result.stdout ?? "").trim();
48
- if (result.status === 0) return out;
49
- const err = (result.stderr ?? "").trim().replace(/\s*\n\s*/g, " | ");
50
- return err || out || `npm exited ${result.status}`;
51
- }
52
-
53
- /**
54
- * Poll until the registry serves `version` of `name`.
55
- *
56
- * @param {object} options
57
- * @param {string} options.name - Package name.
58
- * @param {string} options.version - Exact version expected.
59
- * @param {(spec: string) => string} [options.query] - Registry query; returns
60
- * the registry's answer for `<name>@<version>`.
61
- * @param {(ms: number) => Promise<void>} [options.sleep] - Waits between queries.
62
- * @param {number} [options.intervalSeconds] - Seconds between queries.
63
- * @param {number} [options.timeoutSeconds] - Seconds before giving up.
64
- * @param {(line: string) => void} [options.log] - Progress output.
65
- * @returns {Promise<{ok: true, attempts: number} | {ok: false, attempts: number, last: string}>}
66
- */
67
- export async function confirmNpmVersion({
68
- name,
69
- version,
70
- query = queryNpm,
71
- sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
72
- intervalSeconds = DEFAULT_INTERVAL_SECONDS,
73
- timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
74
- log = () => {},
75
- }) {
76
- const spec = `${name}@${version}`;
77
- const attemptLimit = Math.floor(timeoutSeconds / intervalSeconds) + 1;
78
- let last = "";
79
- for (let attempts = 1; attempts <= attemptLimit; attempts++) {
80
- last = query(spec);
81
- if (last === version) {
82
- log(`${spec} is served by npm.`);
83
- return { ok: true, attempts };
84
- }
85
- log(`${spec} is not served yet (${last || "empty answer"}).`);
86
- if (attempts < attemptLimit) await sleep(intervalSeconds * 1000);
87
- }
88
- return { ok: false, attempts: attemptLimit, last };
89
- }
90
-
91
- function parseArguments(argv) {
92
- const positional = [];
93
- const options = {};
94
- for (let i = 0; i < argv.length; i++) {
95
- const arg = argv[i];
96
- if (arg === "--interval" || arg === "--timeout") {
97
- const value = Number(argv[++i]);
98
- if (!Number.isFinite(value) || value <= 0) {
99
- throw new Error(`${arg} takes a positive number of seconds.`);
100
- }
101
- options[arg === "--interval" ? "intervalSeconds" : "timeoutSeconds"] = value;
102
- } else if (arg.startsWith("--")) {
103
- throw new Error(`Unknown option ${arg}.`);
104
- } else {
105
- positional.push(arg);
106
- }
107
- }
108
- if (positional.length !== 2) {
109
- throw new Error(
110
- "Usage: confirm-npm-version.mjs <name> <version> [--interval <s>] [--timeout <s>]",
111
- );
112
- }
113
- return { name: positional[0], version: positional[1], ...options };
114
- }
115
-
116
- async function main(argv) {
117
- let options;
118
- try {
119
- options = parseArguments(argv);
120
- } catch (error) {
121
- console.error(`::error::${error.message}`);
122
- return 2;
123
- }
124
- const outcome = await confirmNpmVersion({ ...options, log: (line) => console.log(line) });
125
- if (outcome.ok) return 0;
126
- const timeout = options.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
127
- console.error(
128
- `::error::npm does not serve ${options.name}@${options.version} after ${timeout}s and ${outcome.attempts} queries. Last registry answer: ${outcome.last || "(empty)"}`,
129
- );
130
- return 1;
131
- }
132
-
133
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
134
- process.exitCode = await main(process.argv.slice(2));
135
- }