@heroiclands/package-build 23.2.1 → 23.2.2
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 +8 -0
- package/content-config.mjs +15 -0
- package/docs/configuration.md +10 -0
- package/docs/guides/book.md +10 -0
- package/engine/pdf-build.mjs +10 -9
- package/engine/pdf-index.mjs +15 -2
- package/engine/pdf-pages.mjs +29 -6
- package/hugo-theme/layouts/_default/baseof.html +1 -1
- package/package.json +1 -1
- package/types/engine/pdf-index.d.mts +5 -1
- package/types/engine/pdf-pages.d.mts +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
A book author can list leading words such as "Kingdom of" or "Ritual:" under `pdf.indexHeads`, and the index also files those notes under the rest of the name, so "Kingdom of Targud" is found under Targud.
|
|
8
|
+
|
|
9
|
+
A book whose Typst is killed, most often by the memory limit, now fails naming the signal instead of an empty reason, and a Typst that exits silently reports its exit code.
|
|
10
|
+
|
|
3
11
|
## 23.2.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/content-config.mjs
CHANGED
|
@@ -776,6 +776,7 @@ const PDF_KEYS = [
|
|
|
776
776
|
"unprinted",
|
|
777
777
|
"coverage",
|
|
778
778
|
"indexSeeAlso",
|
|
779
|
+
"indexHeads",
|
|
779
780
|
"copyright",
|
|
780
781
|
"license",
|
|
781
782
|
];
|
|
@@ -1920,6 +1921,17 @@ function normalizePdf(value, rootDir, contentPackage) {
|
|
|
1920
1921
|
fail("pdf.copyright", "must be one line — the licence statement is the one that runs on");
|
|
1921
1922
|
}
|
|
1922
1923
|
|
|
1924
|
+
/** @type {string[]} */
|
|
1925
|
+
const indexHeads = [];
|
|
1926
|
+
if (input.indexHeads !== undefined) {
|
|
1927
|
+
if (!Array.isArray(input.indexHeads)) fail("pdf.indexHeads", "must be a list of strings");
|
|
1928
|
+
input.indexHeads.forEach((head, i) => {
|
|
1929
|
+
const field = `pdf.indexHeads[${i}]`;
|
|
1930
|
+
if (typeof head !== "string" || !head.trim()) fail(field, "must be a non-empty string");
|
|
1931
|
+
indexHeads.push(head);
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1923
1935
|
return Object.freeze({
|
|
1924
1936
|
books,
|
|
1925
1937
|
out: input.out === undefined ? "" : requireNonEmptyString(input.out, "pdf.out"),
|
|
@@ -1939,6 +1951,9 @@ function normalizePdf(value, rootDir, contentPackage) {
|
|
|
1939
1951
|
// Whether an index names a note another book prints, as
|
|
1940
1952
|
// `Name: Book title, p. N`.
|
|
1941
1953
|
indexSeeAlso: optionalBoolean(input.indexSeeAlso, "pdf.indexSeeAlso", false),
|
|
1954
|
+
// Leading words an index inverts: `Kingdom of ` files `Kingdom of
|
|
1955
|
+
// Targud` again as `Targud, Kingdom of`.
|
|
1956
|
+
indexHeads: Object.freeze(indexHeads),
|
|
1942
1957
|
copyright: copyright.trim(),
|
|
1943
1958
|
license:
|
|
1944
1959
|
input.license === undefined ?
|
package/docs/configuration.md
CHANGED
|
@@ -1161,6 +1161,7 @@ The keys of `pdf` itself are shared by every book:
|
|
|
1161
1161
|
| `pdf.unprinted` | string (`WHERE` clause) | no | `""` |
|
|
1162
1162
|
| `pdf.coverage` | `warning` \| `error` \| `off` | no | `warning` |
|
|
1163
1163
|
| `pdf.indexSeeAlso` | boolean | no | `false` |
|
|
1164
|
+
| `pdf.indexHeads` | string[] | no | `[]` — none inverted |
|
|
1164
1165
|
| `pdf.copyright` | string (one line) | no | `""` |
|
|
1165
1166
|
| `pdf.license` | string (one or more lines) | no | `""` |
|
|
1166
1167
|
|
|
@@ -1185,6 +1186,15 @@ The keys of `pdf` itself are shared by every book:
|
|
|
1185
1186
|
`Name: _Book title_, p. N` with the page the note's entry starts on in its
|
|
1186
1187
|
home book. It is `false` by default, and the index then lists only the notes
|
|
1187
1188
|
the book is home to. See [The index](guides/book.md#the-index).
|
|
1189
|
+
- `pdf.indexHeads` lists leading words that open many names, such as
|
|
1190
|
+
`Kingdom of ` or `Ritual:`. Each is matched literally against the start of
|
|
1191
|
+
a note's `name.full`, so a head that ends in a space includes it. A note
|
|
1192
|
+
whose name opens with one is also filed in the index under the rest of its
|
|
1193
|
+
name, followed by the head without its trailing space or colon: `Kingdom of
|
|
1194
|
+
Targud` is also listed as `Targud, Kingdom of`, with the same pages as the
|
|
1195
|
+
note's own entry. Where several heads match, the longest applies. A blank
|
|
1196
|
+
entry, or a value that is not a list of strings, is an error at the offending
|
|
1197
|
+
key. See [The index](guides/book.md#the-index).
|
|
1188
1198
|
- A note several books select prints in each of them. Its **home book** is the
|
|
1189
1199
|
first book in `pdf.books` that selects it; a note a book selects twice is
|
|
1190
1200
|
still one book's, and prints at each position. After the books are built the
|
package/docs/guides/book.md
CHANGED
|
@@ -409,9 +409,19 @@ notes the book prints.
|
|
|
409
409
|
| Term | Source | Shown as |
|
|
410
410
|
| ------------------------ | --------------------- | ---------------------------------------------------------------------------------- |
|
|
411
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` |
|
|
412
413
|
| An alias | `name.aliases` | _see_ the note's name |
|
|
413
414
|
| An event's in-world name | `data.events[].names` | _see_ the name of the note that holds the event |
|
|
414
415
|
|
|
416
|
+
A name that opens with a generic word a reader would not look under, such as
|
|
417
|
+
`Kingdom of Targud` or `Ritual: Blessing`, is also filed under the rest of the
|
|
418
|
+
name when [`pdf.indexHeads`](../configuration.md) lists that opening: with
|
|
419
|
+
`indexHeads: ["Kingdom of ", "Ritual:"]` the index lists `Targud, Kingdom of`
|
|
420
|
+
and `Blessing, Ritual`, each with the pages of the note's entry. No alias is
|
|
421
|
+
needed to find the note under its distinctive word. An inverted entry that would
|
|
422
|
+
print the same as another note's term is told apart by the note's subtype, then
|
|
423
|
+
its parent place, as any two equal terms are.
|
|
424
|
+
|
|
415
425
|
To get a term into the index, give the note a `name.full`, add an alias or an
|
|
416
426
|
event name, or link to the note from prose with a wikilink. A wikilink to a note
|
|
417
427
|
the book prints adds the page it appears on. A term mentioned several times on
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -86,7 +86,14 @@ import { publishesContentPages } from "../content-config.mjs";
|
|
|
86
86
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
87
87
|
import { noteFile } from "./index-records.mjs";
|
|
88
88
|
import { coverage, isPrintable, recordKey } from "./pdf-coverage.mjs";
|
|
89
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
elsewhereFor,
|
|
91
|
+
homeEntries,
|
|
92
|
+
movedPages,
|
|
93
|
+
pageTable,
|
|
94
|
+
readEntryPages,
|
|
95
|
+
typstFailure,
|
|
96
|
+
} from "./pdf-pages.mjs";
|
|
90
97
|
import { cachedDependencies, openNotesDatabase, prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
91
98
|
import { parseDocumentTree, runTreeFilters, planDocument } from "./pdf-toc.mjs";
|
|
92
99
|
import {
|
|
@@ -1166,6 +1173,7 @@ async function buildBook(book, shared) {
|
|
|
1166
1173
|
// another book is a reprint, and its term is in that book's index.
|
|
1167
1174
|
const index = {
|
|
1168
1175
|
owns: (entry) => (shared.homes?.get(recordKey(entry.record)) ?? book.id) === book.id,
|
|
1176
|
+
heads: resolved.pdf.indexHeads,
|
|
1169
1177
|
...(elsewhere?.seeAlso ? { foreign: elsewhere.listed } : {}),
|
|
1170
1178
|
};
|
|
1171
1179
|
|
|
@@ -2054,14 +2062,7 @@ export function compileTypst(typPath, pdfPath, pdf = {}) {
|
|
|
2054
2062
|
};
|
|
2055
2063
|
}
|
|
2056
2064
|
if (result.status !== 0) {
|
|
2057
|
-
|
|
2058
|
-
.trim()
|
|
2059
|
-
.split("\n")[0];
|
|
2060
|
-
return {
|
|
2061
|
-
ok: false,
|
|
2062
|
-
message: `\`${binary} compile\` failed: ${detail}`,
|
|
2063
|
-
findings: [],
|
|
2064
|
-
};
|
|
2065
|
+
return { ok: false, message: typstFailure(result, binary, "compile"), findings: [] };
|
|
2065
2066
|
}
|
|
2066
2067
|
return { ok: true, message: "", findings: typstWarnings(result.stderr) };
|
|
2067
2068
|
}
|
package/engine/pdf-index.mjs
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
*
|
|
26
26
|
* - A note's own entry lists its name, with the entry's page in bold.
|
|
27
27
|
* - An authored mention of a note in the book adds the page it appears on.
|
|
28
|
+
* - A note whose name opens with one of the book's `pdf.indexHeads` is also
|
|
29
|
+
* filed under the rest of its name, with the head after a comma
|
|
30
|
+
* (`Targud, Kingdom of`), and that entry carries the note's pages.
|
|
28
31
|
* - An alias (`name.aliases`) and an event's in-world name
|
|
29
32
|
* (`data.events[].names`) list a cross-reference to the note's name.
|
|
30
33
|
*
|
|
@@ -96,6 +99,9 @@ function letterOf(key) {
|
|
|
96
99
|
* @param {object} [opts] - Options.
|
|
97
100
|
* @param {(entry: object) => boolean} [opts.owns] - Whether this book is the
|
|
98
101
|
* entry's home. Every entry when absent.
|
|
102
|
+
* @param {string[]} [opts.heads] - Leading words (`pdf.indexHeads`) matched
|
|
103
|
+
* literally against the start of a name; a name that opens with one is also
|
|
104
|
+
* listed as `<rest>, <head>`, the head without its trailing space or colon. None when absent.
|
|
99
105
|
* @param {Array<{slug: string, name: string, title: string, page: string|number}>} [opts.foreign] -
|
|
100
106
|
* Notes another book prints, listed as `Name: Book title, p. N` once this
|
|
101
107
|
* book mentions them.
|
|
@@ -103,7 +109,8 @@ function letterOf(key) {
|
|
|
103
109
|
* foreign?: {title: string, page: string}}>} Terms with `see` set for a
|
|
104
110
|
* cross-reference to the main name it points at.
|
|
105
111
|
*/
|
|
106
|
-
export function indexTerms(entries, { owns = () => true, foreign = [] } = {}) {
|
|
112
|
+
export function indexTerms(entries, { owns = () => true, foreign = [], heads = [] } = {}) {
|
|
113
|
+
const byLength = [...heads].sort((a, b) => b.length - a.length);
|
|
107
114
|
const terms = [];
|
|
108
115
|
const info = new Map();
|
|
109
116
|
const nameByCanonical = new Map();
|
|
@@ -130,6 +137,12 @@ export function indexTerms(entries, { owns = () => true, foreign = [] } = {}) {
|
|
|
130
137
|
});
|
|
131
138
|
terms.push({ name, slug, main: true });
|
|
132
139
|
const taken = new Set([sortKey(name)]);
|
|
140
|
+
const head = byLength.find((one) => name.startsWith(one) && name.slice(one.length).trim());
|
|
141
|
+
if (head) {
|
|
142
|
+
const inverted = `${name.slice(head.length).trim()}, ${head.replace(/[\s:]+$/, "")}`;
|
|
143
|
+
taken.add(sortKey(inverted));
|
|
144
|
+
terms.push({ name: inverted, slug, main: false, inverted: true });
|
|
145
|
+
}
|
|
133
146
|
const names = [
|
|
134
147
|
...(Array.isArray(entry.record?.name?.aliases) ? entry.record.name.aliases : []),
|
|
135
148
|
...(Array.isArray(entry.record?.data?.events) ? entry.record.data.events : []).flatMap(
|
|
@@ -174,7 +187,7 @@ export function indexTerms(entries, { owns = () => true, foreign = [] } = {}) {
|
|
|
174
187
|
const keyed = terms.map((term, i) => ({
|
|
175
188
|
name: shown[i],
|
|
176
189
|
slug: term.slug,
|
|
177
|
-
...(term.main ? {} : { see: mainShown.get(term.slug) }),
|
|
190
|
+
...(term.main || term.inverted ? {} : { see: mainShown.get(term.slug) }),
|
|
178
191
|
key: sortKey(shown[i]),
|
|
179
192
|
}));
|
|
180
193
|
const listed = new Set(terms.map((term) => term.slug));
|
package/engine/pdf-pages.mjs
CHANGED
|
@@ -66,6 +66,34 @@ export function evalArgs(typPath, fontArgs) {
|
|
|
66
66
|
return ["eval", "--in", typPath, ...fontArgs, PAGES_EXPRESSION];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Say why a finished Typst process failed.
|
|
71
|
+
*
|
|
72
|
+
* A process killed by a signal has no exit status and prints nothing, so the
|
|
73
|
+
* signal is named; `SIGKILL` is the kernel's out-of-memory kill. Otherwise the
|
|
74
|
+
* first line the compiler printed is the reason, and the exit code stands in
|
|
75
|
+
* when it printed none.
|
|
76
|
+
*
|
|
77
|
+
* @param {{status: number | null, signal: string | null, stderr?: string | null, stdout?: string | null}} result -
|
|
78
|
+
* The finished process.
|
|
79
|
+
* @param {string} binary - The Typst binary.
|
|
80
|
+
* @param {string} subcommand - The Typst subcommand that ran.
|
|
81
|
+
* @returns {string} The reason, ready to follow the failing command's name.
|
|
82
|
+
*/
|
|
83
|
+
export function typstFailure(result, binary, subcommand) {
|
|
84
|
+
const command = `\`${binary} ${subcommand}\``;
|
|
85
|
+
if (result.signal) {
|
|
86
|
+
const hint = result.signal === "SIGKILL" ? " (most often the memory limit)" : "";
|
|
87
|
+
return `${command} was killed by ${result.signal}${hint}`;
|
|
88
|
+
}
|
|
89
|
+
const detail = String(result.stderr || result.stdout || "")
|
|
90
|
+
.trim()
|
|
91
|
+
.split("\n")[0];
|
|
92
|
+
return detail ?
|
|
93
|
+
`${command} failed: ${detail}`
|
|
94
|
+
: `${command} exited with code ${result.status} and printed nothing`;
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
/**
|
|
70
98
|
* Measure the page each labelled entry of a book starts on.
|
|
71
99
|
*
|
|
@@ -87,12 +115,7 @@ export function readEntryPages(typPath, fontArgs, binary = "typst") {
|
|
|
87
115
|
return fail(`could not run \`${binary}\`: ${err.message}`);
|
|
88
116
|
}
|
|
89
117
|
if (result.error) return fail(`could not run \`${binary}\`: ${result.error.message}`);
|
|
90
|
-
if (result.status !== 0)
|
|
91
|
-
const detail = String(result.stderr || result.stdout || "")
|
|
92
|
-
.trim()
|
|
93
|
-
.split("\n")[0];
|
|
94
|
-
return fail(`\`${binary} eval\` failed: ${detail}`);
|
|
95
|
-
}
|
|
118
|
+
if (result.status !== 0) return fail(typstFailure(result, binary, "eval"));
|
|
96
119
|
let rows;
|
|
97
120
|
try {
|
|
98
121
|
rows = JSON.parse(result.stdout);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
enough — the system deliberately uses no Pro-only icons — and the
|
|
16
16
|
game-icons webfont is subset to just the glyphs the docs reference
|
|
17
17
|
(~3 KB), generated by the SoHL repo. */}}
|
|
18
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.
|
|
18
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|
19
19
|
<link rel="stylesheet" href="{{ "css/sohl-icons.css" | relURL }}">
|
|
20
20
|
</head>
|
|
21
21
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "23.2.
|
|
3
|
+
"version": "23.2.2",
|
|
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",
|
|
@@ -17,6 +17,9 @@ export function indexMarker(slug: string, main?: boolean): string;
|
|
|
17
17
|
* @param {object} [opts] - Options.
|
|
18
18
|
* @param {(entry: object) => boolean} [opts.owns] - Whether this book is the
|
|
19
19
|
* entry's home. Every entry when absent.
|
|
20
|
+
* @param {string[]} [opts.heads] - Leading words (`pdf.indexHeads`) matched
|
|
21
|
+
* literally against the start of a name; a name that opens with one is also
|
|
22
|
+
* listed as `<rest>, <head>`, the head without its trailing space or colon. None when absent.
|
|
20
23
|
* @param {Array<{slug: string, name: string, title: string, page: string|number}>} [opts.foreign] -
|
|
21
24
|
* Notes another book prints, listed as `Name: Book title, p. N` once this
|
|
22
25
|
* book mentions them.
|
|
@@ -24,8 +27,9 @@ export function indexMarker(slug: string, main?: boolean): string;
|
|
|
24
27
|
* foreign?: {title: string, page: string}}>} Terms with `see` set for a
|
|
25
28
|
* cross-reference to the main name it points at.
|
|
26
29
|
*/
|
|
27
|
-
export function indexTerms(entries: object[], { owns, foreign }?: {
|
|
30
|
+
export function indexTerms(entries: object[], { owns, foreign, heads }?: {
|
|
28
31
|
owns?: ((entry: object) => boolean) | undefined;
|
|
32
|
+
heads?: string[] | undefined;
|
|
29
33
|
foreign?: {
|
|
30
34
|
slug: string;
|
|
31
35
|
name: string;
|
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
* @returns {string[]} The arguments, in order.
|
|
11
11
|
*/
|
|
12
12
|
export function evalArgs(typPath: string, fontArgs: string[]): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Say why a finished Typst process failed.
|
|
15
|
+
*
|
|
16
|
+
* A process killed by a signal has no exit status and prints nothing, so the
|
|
17
|
+
* signal is named; `SIGKILL` is the kernel's out-of-memory kill. Otherwise the
|
|
18
|
+
* first line the compiler printed is the reason, and the exit code stands in
|
|
19
|
+
* when it printed none.
|
|
20
|
+
*
|
|
21
|
+
* @param {{status: number | null, signal: string | null, stderr?: string | null, stdout?: string | null}} result -
|
|
22
|
+
* The finished process.
|
|
23
|
+
* @param {string} binary - The Typst binary.
|
|
24
|
+
* @param {string} subcommand - The Typst subcommand that ran.
|
|
25
|
+
* @returns {string} The reason, ready to follow the failing command's name.
|
|
26
|
+
*/
|
|
27
|
+
export function typstFailure(result: {
|
|
28
|
+
status: number | null;
|
|
29
|
+
signal: string | null;
|
|
30
|
+
stderr?: string | null;
|
|
31
|
+
stdout?: string | null;
|
|
32
|
+
}, binary: string, subcommand: string): string;
|
|
13
33
|
/**
|
|
14
34
|
* Measure the page each labelled entry of a book starts on.
|
|
15
35
|
*
|