@heroiclands/package-build 23.12.8 → 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 (64) hide show
  1. package/CHANGELOG.md +48 -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/links-and-markup.md +156 -22
  9. package/docs/commands.md +22 -7
  10. package/docs/configuration.md +1 -1
  11. package/docs/content-format.md +3 -3
  12. package/docs/diagnostics.md +42 -0
  13. package/docs/reference/format-details.md +99 -22
  14. package/docs/reference/note-types.md +8 -1
  15. package/docs/reference/theme-pages.md +51 -0
  16. package/docs/source-preview.md +1 -1
  17. package/engine/anchors.mjs +2 -2
  18. package/engine/code-fences.mjs +3 -3
  19. package/engine/content-blocks.mjs +3 -3
  20. package/engine/content-figures.mjs +5 -5
  21. package/engine/content-index.mjs +1 -1
  22. package/engine/content-scripts.mjs +55 -21
  23. package/engine/content-spans.mjs +2 -2
  24. package/engine/content-verse.mjs +750 -0
  25. package/engine/dictionary-rows.mjs +15 -4
  26. package/engine/field-reference.mjs +4 -4
  27. package/engine/first-use.mjs +17 -27
  28. package/engine/key-case-collisions.mjs +136 -0
  29. package/engine/language-data.mjs +203 -6
  30. package/engine/language-terms.mjs +74 -5
  31. package/engine/map-lexicon.mjs +42 -23
  32. package/engine/names.mjs +36 -9
  33. package/engine/note-matter.mjs +66 -0
  34. package/engine/pdf-build.mjs +1 -0
  35. package/engine/pdf-render.mjs +107 -18
  36. package/engine/read-stdin.mjs +29 -0
  37. package/engine/run-rows.mjs +2 -2
  38. package/engine/run-words.mjs +4 -111
  39. package/engine/site-build.mjs +4 -4
  40. package/engine/site-preview.mjs +14 -14
  41. package/engine/sql-tables.mjs +20 -3
  42. package/engine/web-wikilinks.mjs +1 -1
  43. package/engine/word-forms.mjs +349 -0
  44. package/engine/word-terms.mjs +9 -0
  45. package/hugo-theme/static/css/style.css +92 -14
  46. package/package.json +2 -2
  47. package/types/engine/code-fences.d.mts +3 -3
  48. package/types/engine/content-scripts.d.mts +8 -4
  49. package/types/engine/content-verse.d.mts +240 -0
  50. package/types/engine/dictionary-rows.d.mts +10 -0
  51. package/types/engine/field-reference.d.mts +1 -1
  52. package/types/engine/key-case-collisions.d.mts +28 -0
  53. package/types/engine/language-data.d.mts +0 -2
  54. package/types/engine/map-lexicon.d.mts +19 -6
  55. package/types/engine/names.d.mts +34 -3
  56. package/types/engine/note-matter.d.mts +48 -0
  57. package/types/engine/read-stdin.d.mts +11 -0
  58. package/types/engine/run-rows.d.mts +2 -2
  59. package/types/engine/run-words.d.mts +0 -15
  60. package/types/engine/sql-tables.d.mts +4 -1
  61. package/types/engine/web-wikilinks.d.mts +1 -1
  62. package/types/engine/word-forms.d.mts +82 -0
  63. package/engine/content-poetry.mjs +0 -240
  64. package/types/engine/content-poetry.d.mts +0 -58
@@ -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
+ };
@@ -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[];