@heroiclands/package-build 23.12.7 → 23.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/assets/preview/gitea.ini +2 -2
  4. package/assets/styles/content-base.css +143 -23
  5. package/bin/content-commands.mjs +15 -4
  6. package/bin/package-build-preview.mjs +2 -2
  7. package/docs/api.md +18 -18
  8. package/docs/authoring/assets.md +1 -1
  9. package/docs/authoring/links-and-markup.md +156 -22
  10. package/docs/commands.md +32 -10
  11. package/docs/configuration.md +8 -1
  12. package/docs/content-format.md +3 -3
  13. package/docs/diagnostics.md +42 -0
  14. package/docs/reference/format-details.md +105 -22
  15. package/docs/reference/note-types.md +8 -1
  16. package/docs/reference/theme-pages.md +51 -0
  17. package/docs/source-preview.md +1 -1
  18. package/e2e.mjs +1 -1
  19. package/engine/anchors.mjs +2 -2
  20. package/engine/asset-shipping.mjs +22 -8
  21. package/engine/code-fences.mjs +3 -3
  22. package/engine/content-blocks.mjs +3 -3
  23. package/engine/content-figures.mjs +5 -5
  24. package/engine/content-index.mjs +1 -1
  25. package/engine/content-scripts.mjs +55 -21
  26. package/engine/content-spans.mjs +2 -2
  27. package/engine/content-verse.mjs +750 -0
  28. package/engine/dictionary-rows.mjs +15 -4
  29. package/engine/field-reference.mjs +4 -4
  30. package/engine/first-use.mjs +17 -27
  31. package/engine/key-case-collisions.mjs +136 -0
  32. package/engine/language-data.mjs +203 -6
  33. package/engine/language-terms.mjs +74 -5
  34. package/engine/map-lexicon.mjs +42 -23
  35. package/engine/names.mjs +36 -9
  36. package/engine/note-matter.mjs +66 -0
  37. package/engine/pdf-build.mjs +1 -0
  38. package/engine/pdf-render.mjs +107 -18
  39. package/engine/read-stdin.mjs +29 -0
  40. package/engine/run-rows.mjs +2 -2
  41. package/engine/run-words.mjs +4 -111
  42. package/engine/site-build.mjs +4 -4
  43. package/engine/site-preview.mjs +14 -14
  44. package/engine/sql-tables.mjs +20 -3
  45. package/engine/web-wikilinks.mjs +1 -1
  46. package/engine/word-forms.mjs +349 -0
  47. package/engine/word-terms.mjs +9 -0
  48. package/hugo-theme/static/css/style.css +92 -14
  49. package/package.json +3 -3
  50. package/types/e2e.d.mts +2 -2
  51. package/types/engine/address-fields.d.mts +1 -1
  52. package/types/engine/address-values.d.mts +1 -1
  53. package/types/engine/asset-shipping.d.mts +5 -0
  54. package/types/engine/code-fences.d.mts +3 -3
  55. package/types/engine/content-format-check.d.mts +1 -1
  56. package/types/engine/content-scripts.d.mts +8 -4
  57. package/types/engine/content-verse.d.mts +240 -0
  58. package/types/engine/dictionary-rows.d.mts +10 -0
  59. package/types/engine/field-reference.d.mts +1 -1
  60. package/types/engine/key-case-collisions.d.mts +28 -0
  61. package/types/engine/language-data.d.mts +0 -2
  62. package/types/engine/map-lexicon.d.mts +19 -6
  63. package/types/engine/names.d.mts +34 -3
  64. package/types/engine/note-matter.d.mts +48 -0
  65. package/types/engine/note-vocabulary.d.mts +1 -1
  66. package/types/engine/pdf-coverage.d.mts +1 -1
  67. package/types/engine/read-stdin.d.mts +11 -0
  68. package/types/engine/run-rows.d.mts +2 -2
  69. package/types/engine/run-words.d.mts +0 -15
  70. package/types/engine/sql-tables.d.mts +4 -1
  71. package/types/engine/text-maps.d.mts +1 -1
  72. package/types/engine/web-wikilinks.d.mts +1 -1
  73. package/types/engine/word-forms.d.mts +82 -0
  74. package/types/release.d.mts +1 -1
  75. package/engine/content-poetry.mjs +0 -240
  76. package/types/engine/content-poetry.d.mts +0 -58
@@ -2,7 +2,7 @@
2
2
  * One run as an index record carries it.
3
3
  *
4
4
  * @typedef {object} RecordRun
5
- * @property {"span"|"block"|"poetry"} kind - The construct.
5
+ * @property {"span"|"block"|"verse"} kind - The construct.
6
6
  * @property {string|null} lang - The language it is set in, as written: its
7
7
  * own `lang=`, else the one the div or fence around it states.
8
8
  * @property {string|null} tag - The BCP 47 tag it resolves to, or null.
@@ -57,7 +57,7 @@ export type RecordRun = {
57
57
  /**
58
58
  * - The construct.
59
59
  */
60
- kind: "span" | "block" | "poetry";
60
+ kind: "span" | "block" | "verse";
61
61
  /**
62
62
  * - The language it is set in, as written: its
63
63
  * own `lang=`, else the one the div or fence around it states.
@@ -1,18 +1,3 @@
1
- /**
2
- * A word as it is compared: NFC, lowercase, `’` read as `'`.
3
- *
4
- * @param {string} word - The word.
5
- * @returns {string} Its folded form.
6
- */
7
- export function foldWord(word: string): string;
8
- /**
9
- * The words a language note accepts of its own: its forms, and what its
10
- * plural rows, verb pattern and replacing suffixes derive from them.
11
- *
12
- * @param {Record<string, any>} data - The language note's `data`.
13
- * @returns {Set<string>} The words, folded.
14
- */
15
- export function lexiconWords(data: Record<string, any>): Set<string>;
16
1
  /**
17
2
  * The words of every name a language note registers under a tongue other than
18
3
  * `translation`.
@@ -65,10 +65,12 @@ export function findSqlBlocks(markdown: string): Array<{
65
65
  * @param {object} [opts.addressContext] - Address resolution context.
66
66
  * @param {"all"|"public"} [opts.audience] - Whether to exclude GM notes from
67
67
  * local and dependency relations.
68
+ * @param {string} [opts.contentBase] - The directory the records' paths are
69
+ * relative to, so a refused key is reported at its file and line.
68
70
  * @returns {Promise<{query: (sql: string) => Promise<object[]>,
69
71
  * close: () => Promise<void>}>} The open database.
70
72
  */
71
- export function openNotesDatabase(records: object[], { dir, dependencies, addressContext, audience }?: {
73
+ export function openNotesDatabase(records: object[], { dir, dependencies, addressContext, audience, contentBase }?: {
72
74
  dir?: string | undefined;
73
75
  dependencies?: {
74
76
  id: string;
@@ -76,6 +78,7 @@ export function openNotesDatabase(records: object[], { dir, dependencies, addres
76
78
  }[] | undefined;
77
79
  addressContext?: object | undefined;
78
80
  audience?: "all" | "public" | undefined;
81
+ contentBase?: string | undefined;
79
82
  }): Promise<{
80
83
  query: (sql: string) => Promise<object[]>;
81
84
  close: () => Promise<void>;
@@ -126,7 +126,7 @@ export type MapRow = {
126
126
  /**
127
127
  * - Where the match must sit.
128
128
  */
129
- at?: "end" | "start" | undefined;
129
+ at?: "start" | "end" | undefined;
130
130
  };
131
131
  /**
132
132
  * A map ready to apply.
@@ -19,7 +19,7 @@
19
19
  * Values are read from the *parsed* frontmatter, so a `[[` inside a YAML comment
20
20
  * is not a hit, and every hit can be named by the path a reader would look at.
21
21
  *
22
- * @param {unknown} fm - Parsed frontmatter, as `gray-matter` returns it.
22
+ * @param {unknown} fm - Parsed frontmatter, as `readMatter` returns it.
23
23
  * @returns {Array<{path: string, link: string}>} In reading order; `path` is the
24
24
  * dotted key path of the offending value (`government.summary`, `aliases.1`).
25
25
  */
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A word as it is compared: NFC, lowercase, `’` read as `'`.
3
+ *
4
+ * @param {string} word - The word.
5
+ * @returns {string} Its folded form.
6
+ */
7
+ export function foldWord(word: string): string;
8
+ /**
9
+ * Add a text's words to a set, each whole and each part of a compound.
10
+ *
11
+ * @param {Set<string>} set - The set.
12
+ * @param {string} text - A form or a name.
13
+ */
14
+ export function addWords(set: Set<string>, text: string): void;
15
+ /**
16
+ * One form a rule derives from a lexicon word.
17
+ *
18
+ * @typedef {object} DerivedForm
19
+ * @property {string} form - The form, folded.
20
+ * @property {Record<string, any>} word - The `data.words` entry it comes from.
21
+ */
22
+ /**
23
+ * Every form the language's rules derive from its lexicon words, in
24
+ * `data.words` order, each once, leaving out a form that is a lexicon word.
25
+ *
26
+ * @param {Record<string, any>|undefined} data - The language note's `data`.
27
+ * @returns {DerivedForm[]} The forms.
28
+ */
29
+ export function derivedForms(data: Record<string, any> | undefined): DerivedForm[];
30
+ /**
31
+ * The words a language note accepts of its own: its forms, and what its rules
32
+ * derive from them (see {@link derivedForms}), each whole and each part of a
33
+ * compound.
34
+ *
35
+ * @param {Record<string, any>} data - The language note's `data`.
36
+ * @returns {Set<string>} The words, folded.
37
+ */
38
+ export function lexiconWords(data: Record<string, any>): Set<string>;
39
+ /**
40
+ * A language's agreement rule, as derivation reads it.
41
+ */
42
+ export type Agreement = {
43
+ /**
44
+ * - The class bases that agree.
45
+ */
46
+ classes: ReadonlySet<string>;
47
+ /**
48
+ * - The endings, folded, longest first.
49
+ */
50
+ endings: readonly string[];
51
+ };
52
+ /**
53
+ * A language's participle rule, as derivation reads it.
54
+ */
55
+ export type Participles = {
56
+ /**
57
+ * - The root endings, folded, longest
58
+ * first.
59
+ */
60
+ root: readonly string[];
61
+ /**
62
+ * -
63
+ * The participle suffixes, folded.
64
+ */
65
+ suffixes: Array<{
66
+ suffix: string;
67
+ replaces: string | undefined;
68
+ }>;
69
+ };
70
+ /**
71
+ * One form a rule derives from a lexicon word.
72
+ */
73
+ export type DerivedForm = {
74
+ /**
75
+ * - The form, folded.
76
+ */
77
+ form: string;
78
+ /**
79
+ * - The `data.words` entry it comes from.
80
+ */
81
+ word: Record<string, any>;
82
+ };
@@ -38,7 +38,7 @@
38
38
  export function packRelease({ stageDir, outDir, artifact, metadataDir, pdf, }?: {
39
39
  stageDir?: string | undefined;
40
40
  outDir?: string | undefined;
41
- artifact?: "module" | "system" | undefined;
41
+ artifact?: "system" | "module" | undefined;
42
42
  metadataDir?: string | undefined;
43
43
  pdf?: boolean | undefined;
44
44
  }): Promise<{
@@ -1,240 +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
- /** The authored verse lines, excluding stanza separators. */
15
- export function verseLines(body) {
16
- return String(body ?? "")
17
- .split("\n")
18
- .filter((line) => line.trim());
19
- }
20
-
21
- /**
22
- * The least-indented verse is level zero. Every two additional leading spaces
23
- * add one level; a leftover space is discarded and the level stops at eight.
24
- * Blank lines divide stanzas rather than contributing to the baseline.
25
- */
26
- export function poetryStanzas(body) {
27
- const lines = String(body ?? "").split("\n");
28
- const verses = verseLines(body);
29
- if (!verses.length) return [];
30
- const baseline = Math.min(...verses.map((line) => /^ */.exec(line)[0].length));
31
- const stanzas = [];
32
- let stanza = [];
33
- for (const line of lines) {
34
- if (!line.trim()) {
35
- if (stanza.length) stanzas.push(stanza);
36
- stanza = [];
37
- continue;
38
- }
39
- const spaces = /^ */.exec(line)[0].length;
40
- stanza.push({
41
- text: line.slice(spaces).trimEnd(),
42
- level: Math.min(8, Math.floor((spaces - baseline) / 2)),
43
- });
44
- }
45
- if (stanza.length) stanzas.push(stanza);
46
- return stanzas;
47
- }
48
-
49
- /** A verse is inline Markdown even when it starts with block syntax. */
50
- export function inlineVerse(text) {
51
- return String(text)
52
- .replace(/^(#{1,6}\s|>\s|[-+*]\s|:{1,2}\s|`{3,}|~{3,}|-{3,}\s*$)/, "\\$1")
53
- .replace(/^(\d+)([.)])\s/, "$1\\$2 ");
54
- }
55
-
56
- /**
57
- * Keep stanzas as Markdown paragraphs and make every verse line a hard break.
58
- * The surrounding renderer still sees the entire note, so links and footnote
59
- * definitions remain in scope. The span carries the indent for each line.
60
- */
61
- export function poetryMarkdown(body) {
62
- return poetryStanzas(body)
63
- .map((stanza) =>
64
- stanza
65
- .map(
66
- ({ text, level }) =>
67
- `<span class="poetry-line${level ? ` i${level}` : ""}">${inlineVerse(text)}</span>`,
68
- )
69
- .join(" \n"),
70
- )
71
- .join("\n\n");
72
- }
73
-
74
- import MarkdownIt from "markdown-it";
75
- import { slugify } from "./content-slug.mjs";
76
- import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
77
-
78
- const structureParser = new MarkdownIt({ html: true });
79
-
80
- /**
81
- * The keys a poetry fence's attribute block takes. `lang`, `font` and `map`
82
- * are read by `scanRuns` in content-scripts.mjs; `dir`, and on the site
83
- * `data-pagefind-ignore`, are written by its rewrite of a fence set in one of
84
- * the faces, and reach the element as they are written.
85
- */
86
- export const POETRY_KEYS = Object.freeze([
87
- "form",
88
- "meter",
89
- "rhyme",
90
- "syllables",
91
- "lang",
92
- "font",
93
- "map",
94
- "dir",
95
- "data-pagefind-ignore",
96
- ]);
97
-
98
- /** Read poetry code fences; start/end are inclusive zero-based source lines.
99
- * @param {string} source - Authored Markdown.
100
- */
101
- export function scanPoetry(source) {
102
- const lines = String(source ?? "").split("\n"),
103
- blocks = [],
104
- errors = [];
105
- const literalLines = new Set();
106
- for (const token of structureParser.parse(String(source ?? ""), {})) {
107
- if (!token.map || !["blockquote_open", "html_block"].includes(token.type)) continue;
108
- for (let line = token.map[0]; line < token.map[1]; line++) literalLines.add(line);
109
- }
110
- let opening = null;
111
- for (let i = 0; i < lines.length; i++) {
112
- if (literalLines.has(i)) continue;
113
- const match = /^ *(`{3,}|~{3,})(.*)$/.exec(lines[i]);
114
- if (opening) {
115
- if (
116
- match &&
117
- match[1][0] === opening.fence[0] &&
118
- match[1].length >= opening.fence.length &&
119
- !match[2].trim()
120
- ) {
121
- if (opening.name === "poetry") {
122
- const body = lines.slice(opening.start + 1, i).join("\n");
123
- if (!body.trim())
124
- errors.push({
125
- line: opening.start + 1,
126
- column: 1,
127
- message: "poetry block is empty",
128
- });
129
- for (let n = opening.start + 1; n < i; n++) {
130
- const col = lines[n].indexOf("\t");
131
- if (col >= 0)
132
- errors.push({
133
- line: n + 1,
134
- column: col + 1,
135
- message: "tabs are not allowed in poetry",
136
- });
137
- }
138
- if (
139
- opening.attributes.syllables &&
140
- opening.attributes.syllables.split(",").length !== verseLines(body).length
141
- )
142
- errors.push({
143
- line: opening.start + 1,
144
- column: 1,
145
- message: `poetry syllables= gives ${opening.attributes.syllables.split(",").length} counts for ${verseLines(body).length} verse lines`,
146
- });
147
- blocks.push({ ...opening, end: i, body });
148
- }
149
- opening = null;
150
- }
151
- continue;
152
- }
153
- if (!match) continue;
154
- const info = match[2].trim();
155
- opening = {
156
- start: i,
157
- fence: match[1],
158
- name: info.split(/\s/)[0],
159
- attributes: {},
160
- id: "",
161
- classes: [],
162
- indent: /^ */.exec(lines[i])[0],
163
- };
164
- if (opening.name !== "poetry") continue;
165
- const raw = info.slice(6).trim();
166
- const at = { line: i + 1, column: 1 };
167
- if (raw && !/^\{[^}\n]*\}$/.test(raw)) {
168
- errors.push({ ...at, message: "poetry attributes need {…} braces" });
169
- continue;
170
- }
171
- const parsed = parseExtensionAttributes(raw ? raw.slice(1, -1) : "");
172
- opening.id = parsed.id;
173
- opening.classes = parsed.classes;
174
- opening.attributes = parsed.values;
175
- for (const message of [...parsed.problems, ...refusedAttributes(parsed.values)])
176
- errors.push({ ...at, message });
177
- for (const [key, value] of Object.entries(parsed.values)) {
178
- if (!POETRY_KEYS.includes(key))
179
- errors.push({ ...at, message: `${key}= is not a poetry attribute` });
180
- else if (key === "syllables" && !/^[1-9]\d*(?:,[1-9]\d*)*$/.test(value))
181
- errors.push({
182
- ...at,
183
- message: "poetry syllables= needs positive comma-separated counts",
184
- });
185
- }
186
- }
187
- if (opening?.name === "poetry")
188
- errors.push({
189
- line: opening.start + 1,
190
- column: 1,
191
- message: "poetry block needs a closing code fence",
192
- });
193
- return { blocks, errors };
194
- }
195
-
196
- /** Replace verse fences with HTML wrappers whose body remains inline Markdown.
197
- * @param {string} source - Authored Markdown.
198
- */
199
- export function renderPoetry(source) {
200
- const { blocks, errors } = scanPoetry(source),
201
- lines = String(source ?? "").split("\n"),
202
- output = [];
203
- let cursor = 0;
204
- const escape = (value) =>
205
- String(value)
206
- .replace(/&/g, "&amp;")
207
- .replace(/"/g, "&quot;")
208
- .replace(/</g, "&lt;")
209
- .replace(/>/g, "&gt;");
210
- for (const block of blocks) {
211
- output.push(...lines.slice(cursor, block.start));
212
- const attrs = [`class="${escape(["poetry", ...block.classes].join(" "))}"`];
213
- if (block.id) attrs.push(`id="${escape(slugify(block.id))}"`);
214
- for (const [key, value] of Object.entries(block.attributes))
215
- if (!refusedAttributes({ [key]: value }).length)
216
- attrs.push(
217
- `${["lang", "dir", "data-pagefind-ignore"].includes(key) ? key : `data-${key}`}="${escape(value)}"`,
218
- );
219
- let indent = block.indent;
220
- if (
221
- indent.length >= 4 &&
222
- !lines.slice(0, block.start).some((line) => /^ *(?:[-+*]|\d+[.)]) +/.test(line))
223
- )
224
- indent = "";
225
- output.push(
226
- "",
227
- `${indent}<div ${attrs.join(" ")}>`,
228
- "",
229
- ...poetryMarkdown(block.body)
230
- .split("\n")
231
- .map((line) => (line ? indent + line : line)),
232
- "",
233
- `${indent}</div>`,
234
- "",
235
- );
236
- cursor = block.end + 1;
237
- }
238
- output.push(...lines.slice(cursor));
239
- return { markdown: output.join("\n"), errors };
240
- }
@@ -1,58 +0,0 @@
1
- /** The authored verse lines, excluding stanza separators. */
2
- export function verseLines(body: any): string[];
3
- /**
4
- * The least-indented verse is level zero. Every two additional leading spaces
5
- * add one level; a leftover space is discarded and the level stops at eight.
6
- * Blank lines divide stanzas rather than contributing to the baseline.
7
- */
8
- export function poetryStanzas(body: any): {
9
- text: string;
10
- level: number;
11
- }[][];
12
- /** A verse is inline Markdown even when it starts with block syntax. */
13
- export function inlineVerse(text: any): string;
14
- /**
15
- * Keep stanzas as Markdown paragraphs and make every verse line a hard break.
16
- * The surrounding renderer still sees the entire note, so links and footnote
17
- * definitions remain in scope. The span carries the indent for each line.
18
- */
19
- export function poetryMarkdown(body: any): string;
20
- /** Read poetry code fences; start/end are inclusive zero-based source lines.
21
- * @param {string} source - Authored Markdown.
22
- */
23
- export function scanPoetry(source: string): {
24
- blocks: {
25
- end: number;
26
- body: string;
27
- start: number;
28
- fence: string;
29
- name: string;
30
- attributes: {};
31
- id: string;
32
- classes: never[];
33
- indent: string;
34
- }[];
35
- errors: {
36
- line: number;
37
- column: number;
38
- message: string;
39
- }[];
40
- };
41
- /** Replace verse fences with HTML wrappers whose body remains inline Markdown.
42
- * @param {string} source - Authored Markdown.
43
- */
44
- export function renderPoetry(source: string): {
45
- markdown: string;
46
- errors: {
47
- line: number;
48
- column: number;
49
- message: string;
50
- }[];
51
- };
52
- /**
53
- * The keys a poetry fence's attribute block takes. `lang`, `font` and `map`
54
- * are read by `scanRuns` in content-scripts.mjs; `dir`, and on the site
55
- * `data-pagefind-ignore`, are written by its rewrite of a fence set in one of
56
- * the faces, and reach the element as they are written.
57
- */
58
- export const POETRY_KEYS: readonly string[];