@heroiclands/package-build 25.1.3 → 25.2.1
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 +36 -0
- package/assets/styles/content-base.css +0 -9
- package/bin/content-commands.mjs +22 -16
- package/bin/package-build.mjs +24 -25
- package/content-config.mjs +7 -34
- package/docs/api.md +38 -21
- package/docs/authoring/first-note.md +3 -3
- package/docs/authoring/frontmatter.md +30 -2
- package/docs/authoring/links-and-markup.md +92 -31
- package/docs/commands.md +103 -59
- package/docs/configuration.md +10 -45
- package/docs/content-format.md +8 -6
- package/docs/diagnostics.md +75 -0
- package/docs/guides/book.md +3 -3
- package/docs/guides/module-project.md +1 -1
- package/docs/guides/reference-pages.md +7 -3
- package/docs/project-setup.md +3 -0
- package/docs/reference/format-details.md +299 -185
- package/docs/reference/note-types.md +16 -17
- package/docs/source-preview.md +2 -1
- package/engine/changelog-group.mjs +84 -173
- package/engine/changelog-labels.mjs +73 -0
- package/engine/changelog-lint.mjs +168 -44
- package/engine/content-expand.mjs +58 -38
- package/engine/content-format.yaml +0 -2
- package/engine/content-index.mjs +49 -23
- package/engine/content-links.mjs +4 -0
- package/engine/content-lint.mjs +2 -3
- package/engine/content-render.mjs +6 -2
- package/engine/culture-parents.mjs +1 -1
- package/engine/culture-reach.mjs +10 -1
- package/engine/diagnostics.mjs +3 -2
- package/engine/dictionary-rows.mjs +3 -1
- package/engine/first-use.mjs +301 -87
- package/engine/frontmatter-lint.mjs +40 -14
- package/engine/glossary-entries.mjs +5 -5
- package/engine/glossary-language.mjs +21 -44
- package/engine/homepage.mjs +0 -4
- package/engine/index-records.mjs +3 -1
- package/engine/infobox.mjs +6 -3
- package/engine/journals.mjs +14 -14
- package/engine/language-names.mjs +3 -2
- package/engine/language-rows.mjs +6 -7
- package/engine/lexical-sections.mjs +3 -1
- package/engine/literature-notes.mjs +4 -76
- package/engine/metadata-index.mjs +15 -8
- package/engine/name-forms.mjs +99 -0
- package/engine/name-refs.mjs +143 -0
- package/engine/name-rows.mjs +11 -1
- package/engine/names.mjs +27 -22
- package/engine/note-addresses.mjs +0 -3
- package/engine/note-frontmatter.mjs +3 -5
- package/engine/note-language.mjs +348 -0
- package/engine/note-vocabulary.mjs +13 -11
- package/engine/pdf-index.mjs +3 -2
- package/engine/pdf-render.mjs +2 -4
- package/engine/preview-source.mjs +15 -8
- package/engine/settlement-tables.mjs +4 -3
- package/engine/site-build.mjs +2 -7
- package/engine/site-search.mjs +3 -5
- package/engine/site-theme.mjs +1 -1
- package/engine/sql-tables.mjs +29 -14
- package/engine/wikilink-syntax.mjs +2 -0
- package/engine/word-citations.mjs +3 -3
- package/engine/word-rows.mjs +3 -1
- package/engine/word-terms.mjs +2 -2
- package/package.json +1 -1
- package/site-theme/static/css/style.css +0 -4
- package/types/engine/changelog-group.d.mts +6 -10
- package/types/engine/changelog-labels.d.mts +10 -0
- package/types/engine/changelog-lint.d.mts +11 -6
- package/types/engine/content-index.d.mts +16 -16
- package/types/engine/diagnostics.d.mts +1 -1
- package/types/engine/first-use.d.mts +97 -1
- package/types/engine/frontmatter-lint.d.mts +9 -2
- package/types/engine/glossary-language.d.mts +13 -6
- package/types/engine/homepage.d.mts +0 -3
- package/types/engine/journals.d.mts +3 -3
- package/types/engine/literature-notes.d.mts +2 -27
- package/types/engine/name-forms.d.mts +61 -0
- package/types/engine/name-refs.d.mts +51 -0
- package/types/engine/name-rows.d.mts +4 -0
- package/types/engine/names.d.mts +12 -92
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-language.d.mts +47 -0
- package/types/engine/settlement-tables.d.mts +1 -1
- package/types/engine/site-build.d.mts +2 -6
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
* **The language a note is in**, a code `languages:` declares or `en`. A
|
|
16
|
+
* note's language is the first of:
|
|
17
|
+
*
|
|
18
|
+
* 1. its own `data.lang`;
|
|
19
|
+
* 2. its `data.culture`'s language: the culture note it names, resolved by
|
|
20
|
+
* these same rules;
|
|
21
|
+
* 3. its `data.parents`, nearest first, each resolved by these same rules;
|
|
22
|
+
* 4. `en`.
|
|
23
|
+
*
|
|
24
|
+
* Steps 2 and 3 take only a language some note states: a culture or parent
|
|
25
|
+
* that itself falls through to `en` contributes nothing, and the chain goes on.
|
|
26
|
+
* A parent in another package contributes the language its published entry
|
|
27
|
+
* carries.
|
|
28
|
+
*
|
|
29
|
+
* The content index writes the result onto every note record as `lang`, and
|
|
30
|
+
* every reader of a note's language reads it from there. The configured
|
|
31
|
+
* language note of each code carries that code as `languageCode`, so a reader
|
|
32
|
+
* holding only records maps a code to its note.
|
|
33
|
+
*
|
|
34
|
+
* Findings, each an error:
|
|
35
|
+
*
|
|
36
|
+
* - a `data.lang` naming no declared code, at its value;
|
|
37
|
+
* - a `data.culture` whose culture resolves to no language, at its value;
|
|
38
|
+
* - every `data.parents` entry on a cycle, at the entry, whether or not a walk
|
|
39
|
+
* reaches it. A cycle among culture notes is reported by the culture
|
|
40
|
+
* hierarchy check instead.
|
|
41
|
+
*
|
|
42
|
+
* @module
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import fs from "node:fs";
|
|
46
|
+
|
|
47
|
+
import { canonicalKey, isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
|
|
48
|
+
import { isAddressSegment } from "./address-charset.mjs";
|
|
49
|
+
import { AddressLink } from "./address-values.mjs";
|
|
50
|
+
import { formatDiagnostic, positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
51
|
+
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
52
|
+
import { FALLBACK_LANGUAGE } from "./name-forms.mjs";
|
|
53
|
+
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The canonical key an Address names, without any anchor.
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown} value - A tuple, an {@link AddressLink}, or anything else.
|
|
59
|
+
* @returns {string|undefined} The key, or `undefined` for anything else.
|
|
60
|
+
*/
|
|
61
|
+
function addressKey(value) {
|
|
62
|
+
const tuple = value instanceof AddressLink ? value.target : value;
|
|
63
|
+
return isAddressTuple(tuple) ? renderAddress(tuple) : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a record is a culture note.
|
|
68
|
+
*
|
|
69
|
+
* @param {Record<string, any>|undefined} record - An index record.
|
|
70
|
+
* @returns {boolean} Whether it is a `lore` note with `subType: culture`.
|
|
71
|
+
*/
|
|
72
|
+
function isCultureNote(record) {
|
|
73
|
+
return record?.type === "lore" && String(record.subType ?? "").toLowerCase() === "culture";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Write each note's language onto its record as `lang`, and each configured
|
|
78
|
+
* language note's code onto its record as `languageCode`.
|
|
79
|
+
*
|
|
80
|
+
* Reads each note's authored `data.lang`, `data.culture` and `data.parents`,
|
|
81
|
+
* so it runs before the index writes a resolved culture over `data.culture`.
|
|
82
|
+
*
|
|
83
|
+
* @param {Array<Record<string, any>>} records - Index records, every Address a
|
|
84
|
+
* tuple. Records that are not notes are passed over.
|
|
85
|
+
* @param {object} options - Options.
|
|
86
|
+
* @param {string} options.contentPackage - The package the records belong to.
|
|
87
|
+
* @param {Readonly<Record<string, {note?: string}>>} [options.languages] - The
|
|
88
|
+
* configured languages, by code.
|
|
89
|
+
* @param {string} [options.contentBase] - The content tree, for opening a note
|
|
90
|
+
* to locate a finding.
|
|
91
|
+
* @param {(key: string) => Record<string, any>|undefined} [options.foreignRecord] -
|
|
92
|
+
* Another package's entry for a canonical address.
|
|
93
|
+
* @param {object[]} [options.problems] - Receives each finding. Omitted, the
|
|
94
|
+
* findings are collected and one Error lists them all.
|
|
95
|
+
* @param {Map<string, {raw: string}>} [options.parsed] - Notes already read, keyed by file.
|
|
96
|
+
* @returns {Array<Record<string, any>>} The same records.
|
|
97
|
+
* @throws {Error} When `problems` is omitted and anything is reported.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveNoteLanguages(
|
|
100
|
+
records,
|
|
101
|
+
{ contentPackage, languages, contentBase, foreignRecord, problems, parsed },
|
|
102
|
+
) {
|
|
103
|
+
const codes = new Set([FALLBACK_LANGUAGE, ...Object.keys(languages ?? {})]);
|
|
104
|
+
const notes = new Map();
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
if (!isNoteRecord(record) || !isAddressSegment(String(record.shortcode ?? ""))) continue;
|
|
107
|
+
const key = canonicalKey(contentPackage, NOTE_SYSTEM, record.type, record.shortcode);
|
|
108
|
+
if (!notes.has(key)) notes.set(key, record);
|
|
109
|
+
}
|
|
110
|
+
const localOf = (value) => {
|
|
111
|
+
const tuple = value instanceof AddressLink ? value.target : value;
|
|
112
|
+
if (!isAddressTuple(tuple) || tuple.package !== contentPackage) return undefined;
|
|
113
|
+
return notes.get(canonicalKey(contentPackage, NOTE_SYSTEM, tuple.type, tuple.shortcode));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const findings = [];
|
|
117
|
+
/**
|
|
118
|
+
* Report one finding on a note, at a frontmatter value.
|
|
119
|
+
*
|
|
120
|
+
* @param {Record<string, any>} record - The note.
|
|
121
|
+
* @param {Array<string|number>} keyPath - Where the value is written.
|
|
122
|
+
* @param {string} message - What is wrong.
|
|
123
|
+
*/
|
|
124
|
+
const report = (record, keyPath, message) => {
|
|
125
|
+
const file = contentBase ? noteFile(contentBase, record) : record.file?.path;
|
|
126
|
+
const raw =
|
|
127
|
+
parsed?.get(file)?.raw ??
|
|
128
|
+
(contentBase && fs.existsSync(file) ? fs.readFileSync(file, "utf8") : undefined);
|
|
129
|
+
findings.push({
|
|
130
|
+
file,
|
|
131
|
+
...positionOfFrontmatterPath(raw, keyPath),
|
|
132
|
+
severity: "error",
|
|
133
|
+
message,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const memo = new Map();
|
|
138
|
+
const inProgress = new Set();
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The language a source Address states: a local note's, resolved, or the
|
|
142
|
+
* one a foreign entry publishes.
|
|
143
|
+
*
|
|
144
|
+
* @param {unknown} value - A `data.culture` or `data.parents` Address.
|
|
145
|
+
* @returns {string|undefined} The code.
|
|
146
|
+
*/
|
|
147
|
+
const statedThrough = (value) => {
|
|
148
|
+
const key = addressKey(value);
|
|
149
|
+
if (!key) return undefined;
|
|
150
|
+
const local = localOf(value);
|
|
151
|
+
if (local) return resolve(local);
|
|
152
|
+
const tuple = value instanceof AddressLink ? value.target : value;
|
|
153
|
+
if (tuple.package === contentPackage) return undefined;
|
|
154
|
+
const lang = foreignRecord?.(key)?.lang;
|
|
155
|
+
return typeof lang === "string" && lang ? lang : undefined;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The language a note states through its own field, its culture or its
|
|
160
|
+
* parents, ignoring the fallback.
|
|
161
|
+
*
|
|
162
|
+
* @param {Record<string, any>} record - The note.
|
|
163
|
+
* @returns {string|undefined} The code.
|
|
164
|
+
*/
|
|
165
|
+
const resolveRecord = (record) => {
|
|
166
|
+
const own = record.data?.lang;
|
|
167
|
+
if (own !== undefined && own !== null) {
|
|
168
|
+
if (typeof own === "string" && codes.has(own)) return own;
|
|
169
|
+
report(
|
|
170
|
+
record,
|
|
171
|
+
["data", "lang"],
|
|
172
|
+
`data.lang ${typeof own === "string" ? own : JSON.stringify(own)} names no ` +
|
|
173
|
+
`language: state a code \`languages:\` declares, or ${FALLBACK_LANGUAGE} ` +
|
|
174
|
+
`(${[...codes].sort().join(", ")})`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const culture = record.data?.culture;
|
|
178
|
+
const cultureRecord = localOf(culture);
|
|
179
|
+
if (addressKey(culture) && cultureRecord !== record) {
|
|
180
|
+
const lang = statedThrough(culture);
|
|
181
|
+
if (lang) return lang;
|
|
182
|
+
if (isCultureNote(cultureRecord) && !inProgress.has(cultureRecord))
|
|
183
|
+
report(
|
|
184
|
+
record,
|
|
185
|
+
["data", "culture"],
|
|
186
|
+
`data.culture ${cultureRecord.type}-${cultureRecord.shortcode} resolves to ` +
|
|
187
|
+
"no language: state data.lang on the culture or on a parent culture",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const parents = Array.isArray(record.data?.parents) ? record.data.parents : [];
|
|
191
|
+
for (const [position, parent] of parents.entries()) {
|
|
192
|
+
const target = localOf(parent);
|
|
193
|
+
if (target && inProgress.has(target)) continue;
|
|
194
|
+
const lang = statedThrough(parent);
|
|
195
|
+
if (lang) return lang;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A local note's stated language, memoised; a note already being resolved
|
|
202
|
+
* is a cycle and states nothing.
|
|
203
|
+
*
|
|
204
|
+
* @param {Record<string, any>} record - The note.
|
|
205
|
+
* @returns {string|undefined} The code.
|
|
206
|
+
*/
|
|
207
|
+
const resolve = (record) => {
|
|
208
|
+
if (memo.has(record)) return memo.get(record);
|
|
209
|
+
if (inProgress.has(record)) return undefined;
|
|
210
|
+
inProgress.add(record);
|
|
211
|
+
const result = resolveRecord(record);
|
|
212
|
+
inProgress.delete(record);
|
|
213
|
+
memo.set(record, result);
|
|
214
|
+
return result;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Walked in address order, so which note of a cycle is met first is a fact
|
|
218
|
+
// about the content rather than about directory-read order.
|
|
219
|
+
for (const key of [...notes.keys()].sort()) resolve(notes.get(key));
|
|
220
|
+
|
|
221
|
+
// Every `data.parents` entry on a cycle, whether or not a walk reached it.
|
|
222
|
+
for (const { record, position, target } of cyclicParents(notes, localOf))
|
|
223
|
+
if (!(isCultureNote(record) && isCultureNote(target)))
|
|
224
|
+
report(
|
|
225
|
+
record,
|
|
226
|
+
["data", "parents", position],
|
|
227
|
+
`data.parents.${position} names ${target.type}-${target.shortcode}, ` +
|
|
228
|
+
"which closes a parents cycle; a note is never its own ancestor",
|
|
229
|
+
);
|
|
230
|
+
for (const record of records) {
|
|
231
|
+
if (!isNoteRecord(record) || !isAddressSegment(String(record.shortcode ?? ""))) continue;
|
|
232
|
+
const own = notes.get(
|
|
233
|
+
canonicalKey(contentPackage, NOTE_SYSTEM, record.type, record.shortcode),
|
|
234
|
+
);
|
|
235
|
+
record.lang = memo.get(own) ?? FALLBACK_LANGUAGE;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Each configured language note carries its code.
|
|
239
|
+
const types = new Set(["skill", ...records.map((record) => String(record.type ?? ""))]);
|
|
240
|
+
for (const [code, spec] of Object.entries(languages ?? {})) {
|
|
241
|
+
if (typeof spec?.note !== "string") continue;
|
|
242
|
+
const tuple = parseAddress(spec.note, {
|
|
243
|
+
package: contentPackage,
|
|
244
|
+
system: NOTE_SYSTEM,
|
|
245
|
+
type: "skill",
|
|
246
|
+
types,
|
|
247
|
+
});
|
|
248
|
+
const record = tuple.reason ? undefined : localOf(tuple);
|
|
249
|
+
if (record) record.languageCode = code;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
findings.sort(
|
|
253
|
+
(a, b) =>
|
|
254
|
+
String(a.file).localeCompare(String(b.file), "en") ||
|
|
255
|
+
(a.line ?? 0) - (b.line ?? 0) ||
|
|
256
|
+
(a.column ?? 0) - (b.column ?? 0),
|
|
257
|
+
);
|
|
258
|
+
if (problems) problems.push(...findings);
|
|
259
|
+
else if (findings.length)
|
|
260
|
+
throw Object.assign(new Error(findings.map(formatDiagnostic).join("\n")), {
|
|
261
|
+
located: true,
|
|
262
|
+
});
|
|
263
|
+
return records;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Every local `data.parents` entry that lies on a cycle: its target reaches
|
|
268
|
+
* its note again through `data.parents`.
|
|
269
|
+
*
|
|
270
|
+
* @param {Map<string, Record<string, any>>} notes - Local notes by key.
|
|
271
|
+
* @param {(value: unknown) => Record<string, any>|undefined} localOf - The
|
|
272
|
+
* local note an Address names.
|
|
273
|
+
* @returns {Array<{record: Record<string, any>, position: number, target: Record<string, any>}>}
|
|
274
|
+
* The entries, in note-key then list order.
|
|
275
|
+
*/
|
|
276
|
+
function cyclicParents(notes, localOf) {
|
|
277
|
+
const edges = new Map();
|
|
278
|
+
for (const record of notes.values()) {
|
|
279
|
+
const parents = Array.isArray(record.data?.parents) ? record.data.parents : [];
|
|
280
|
+
edges.set(
|
|
281
|
+
record,
|
|
282
|
+
parents
|
|
283
|
+
.map((parent, position) => ({ position, target: localOf(parent) }))
|
|
284
|
+
.filter((edge) => edge.target),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
// Tarjan's strongly connected components: an edge inside one component,
|
|
288
|
+
// or from a note to itself, lies on a cycle.
|
|
289
|
+
const index = new Map();
|
|
290
|
+
const low = new Map();
|
|
291
|
+
const component = new Map();
|
|
292
|
+
const stack = [];
|
|
293
|
+
const onStack = new Set();
|
|
294
|
+
let counter = 0;
|
|
295
|
+
const visit = (record) => {
|
|
296
|
+
index.set(record, counter);
|
|
297
|
+
low.set(record, counter);
|
|
298
|
+
counter++;
|
|
299
|
+
stack.push(record);
|
|
300
|
+
onStack.add(record);
|
|
301
|
+
for (const { target } of edges.get(record) ?? []) {
|
|
302
|
+
if (!index.has(target)) {
|
|
303
|
+
visit(target);
|
|
304
|
+
low.set(record, Math.min(low.get(record), low.get(target)));
|
|
305
|
+
} else if (onStack.has(target))
|
|
306
|
+
low.set(record, Math.min(low.get(record), index.get(target)));
|
|
307
|
+
}
|
|
308
|
+
if (low.get(record) !== index.get(record)) return;
|
|
309
|
+
let member;
|
|
310
|
+
do {
|
|
311
|
+
member = stack.pop();
|
|
312
|
+
onStack.delete(member);
|
|
313
|
+
component.set(member, record);
|
|
314
|
+
} while (member !== record);
|
|
315
|
+
};
|
|
316
|
+
const keys = [...notes.keys()].sort();
|
|
317
|
+
for (const key of keys) if (!index.has(notes.get(key))) visit(notes.get(key));
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const key of keys) {
|
|
320
|
+
const record = notes.get(key);
|
|
321
|
+
for (const { position, target } of edges.get(record) ?? [])
|
|
322
|
+
if (target === record || component.get(target) === component.get(record))
|
|
323
|
+
out.push({ record, position, target });
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Each language code's note, read from records alone: the Address of every
|
|
330
|
+
* record carrying a `languageCode`.
|
|
331
|
+
*
|
|
332
|
+
* @param {Iterable<Record<string, any>>} records - Index records, local and
|
|
333
|
+
* any dependency's, or link-index notes carrying them.
|
|
334
|
+
* @param {(record: Record<string, any>) => string|undefined} addressOf - A
|
|
335
|
+
* record's Address, as the caller keys notes.
|
|
336
|
+
* @returns {Map<string, string>} Code to the language note's Address; the first
|
|
337
|
+
* record naming a code wins.
|
|
338
|
+
*/
|
|
339
|
+
export function languageNotesByCode(records, addressOf) {
|
|
340
|
+
const out = new Map();
|
|
341
|
+
for (const record of records) {
|
|
342
|
+
const code = record?.languageCode;
|
|
343
|
+
if (typeof code !== "string" || out.has(code)) continue;
|
|
344
|
+
const address = addressOf(record);
|
|
345
|
+
if (address) out.set(code, address);
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
@@ -125,7 +125,7 @@ import {
|
|
|
125
125
|
requiredHeightGenders,
|
|
126
126
|
} from "./appearance-profile.mjs";
|
|
127
127
|
import { checkExtraFeatures } from "./appearance-features.mjs";
|
|
128
|
-
import {
|
|
128
|
+
import { LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
|
|
129
129
|
import {
|
|
130
130
|
AFFIX_FIELDS,
|
|
131
131
|
GLOSSARY_ENTRY_FIELDS,
|
|
@@ -1035,7 +1035,9 @@ export const NOTE_TOP_LEVEL_FIELDS = Object.freeze([
|
|
|
1035
1035
|
}),
|
|
1036
1036
|
Object.freeze({
|
|
1037
1037
|
name: "name",
|
|
1038
|
-
describe:
|
|
1038
|
+
describe:
|
|
1039
|
+
"The display names — a required `full`, the English name; the other names, " +
|
|
1040
|
+
"listed under the code of their language; and any `sort`.",
|
|
1039
1041
|
}),
|
|
1040
1042
|
Object.freeze({
|
|
1041
1043
|
name: "type",
|
|
@@ -1099,6 +1101,14 @@ export const NOTE_TOP_LEVEL_KEY_SET = Object.freeze(new Set(NOTE_TOP_LEVEL_KEYS)
|
|
|
1099
1101
|
* @type {readonly DataFieldSpec[]}
|
|
1100
1102
|
*/
|
|
1101
1103
|
export const SHARED_DATA_FIELDS = Object.freeze([
|
|
1104
|
+
Object.freeze({
|
|
1105
|
+
name: "lang",
|
|
1106
|
+
kind: "string",
|
|
1107
|
+
shape: "a language code `languages:` declares, or `en`",
|
|
1108
|
+
describe:
|
|
1109
|
+
"The language the note is in. Absent, the content index derives it: the " +
|
|
1110
|
+
"note's culture's language, then its `data.parents`' nearest first, then `en`.",
|
|
1111
|
+
}),
|
|
1102
1112
|
Object.freeze({
|
|
1103
1113
|
name: "seeAlso",
|
|
1104
1114
|
kind: "list",
|
|
@@ -1916,13 +1926,6 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1916
1926
|
...CULTURE_FIELD,
|
|
1917
1927
|
describe: "The people this affiliation belongs to, as a culture lore note.",
|
|
1918
1928
|
},
|
|
1919
|
-
{
|
|
1920
|
-
...LANGUAGE_FIELD,
|
|
1921
|
-
describe:
|
|
1922
|
-
"The trade, temple or order tongue the affiliation keeps, as a language " +
|
|
1923
|
-
"skill note. A glossary entry that states no `language` is in it. The " +
|
|
1924
|
-
"affiliation chooses its own common vocabulary and context meanings.",
|
|
1925
|
-
},
|
|
1926
1929
|
GLOSSARY_FIELD,
|
|
1927
1930
|
{
|
|
1928
1931
|
name: "relations",
|
|
@@ -2465,7 +2468,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2465
2468
|
accepts: ["lore"],
|
|
2466
2469
|
check: checkCultureParents,
|
|
2467
2470
|
describe:
|
|
2468
|
-
"Culture only: parent culture lore notes, searched in listed depth-first order for omitted
|
|
2471
|
+
"Culture only: parent culture lore notes, searched in listed depth-first order for an omitted appearance profile, and nearest first for an omitted `data.lang`.",
|
|
2469
2472
|
},
|
|
2470
2473
|
{
|
|
2471
2474
|
...CULTURE_FIELD,
|
|
@@ -2475,7 +2478,6 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2475
2478
|
"culture, and states the field only as itself.",
|
|
2476
2479
|
},
|
|
2477
2480
|
...LITERATURE_FIELDS,
|
|
2478
|
-
LANGUAGE_FIELD,
|
|
2479
2481
|
EVENTS_FIELD,
|
|
2480
2482
|
APPEARANCE_PROFILE_FIELD,
|
|
2481
2483
|
{
|
package/engine/pdf-index.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* - A note whose name opens with one of the book's `pdf.indexHeads` is also
|
|
29
29
|
* filed under the rest of its name, with the head after a comma
|
|
30
30
|
* (`Targud, Kingdom of`), and that entry carries the note's pages.
|
|
31
|
-
* -
|
|
31
|
+
* - A listed name form (`name.<code>`) and an event's in-world name
|
|
32
32
|
* (`data.events[].names`) list a cross-reference to the note's name.
|
|
33
33
|
*
|
|
34
34
|
* Terms sort on the ASCII fold the content index derives for names
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { asciiName, nameSortOf } from "./name-sort.mjs";
|
|
45
|
+
import { nameFormsOf } from "./name-forms.mjs";
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* The marker an entry heading or an authored mention carries.
|
|
@@ -153,7 +154,7 @@ export function indexTerms(entries, { owns = () => true, foreign = [], heads = [
|
|
|
153
154
|
terms.push({ name: inverted, slug, main: false, inverted: true });
|
|
154
155
|
}
|
|
155
156
|
const names = [
|
|
156
|
-
...(
|
|
157
|
+
...nameFormsOf(entry.record?.name),
|
|
157
158
|
...(Array.isArray(entry.record?.data?.events) ? entry.record.data.events : []).flatMap(
|
|
158
159
|
(event) => (Array.isArray(event?.names) ? event.names.map((n) => n?.name) : []),
|
|
159
160
|
),
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -2012,11 +2012,9 @@ export function bookTypstPreamble(runs = {}, families = {}) {
|
|
|
2012
2012
|
" }\n" +
|
|
2013
2013
|
" if lang != none { text(lang: lang, styled) } else { styled }\n" +
|
|
2014
2014
|
"}",
|
|
2015
|
-
// Span classes reach the book intact
|
|
2016
|
-
// weight so it remains bold inside an ordinary strong phrase.
|
|
2015
|
+
// Span classes reach the book intact.
|
|
2017
2016
|
"#let book-span(body, classes: (), lang: none) = {\n" +
|
|
2018
|
-
|
|
2019
|
-
" box(book-script-text(styled, classes: classes, lang: lang))\n" +
|
|
2017
|
+
" box(book-script-text(body, classes: classes, lang: lang))\n" +
|
|
2020
2018
|
"}",
|
|
2021
2019
|
// Use the file's natural measure for auto. Every requested width is
|
|
2022
2020
|
// bounded by the available measure and by the height of a page.
|
|
@@ -67,7 +67,8 @@ export const PREVIEW_HELPERS = Object.freeze({
|
|
|
67
67
|
*/
|
|
68
68
|
export const INERT_BRACKET = "\uE000";
|
|
69
69
|
|
|
70
|
-
const
|
|
70
|
+
const syntax = new MarkdownIt();
|
|
71
|
+
const escapeHtml = syntax.utils.escapeHtml;
|
|
71
72
|
// An authored `<` stands as a private-use character until the HTML is written;
|
|
72
73
|
// source is shown as it was authored.
|
|
73
74
|
const escape = (value) => escapeHtml(String(value ?? "").replaceAll(INERT_BRACKET, "<"));
|
|
@@ -185,16 +186,16 @@ function withTrailingAttributes(rest, raw) {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
/**
|
|
188
|
-
* The cells of one table row, keeping
|
|
189
|
-
*
|
|
189
|
+
* The cells of one table row, keeping pipes inside balanced brackets, code
|
|
190
|
+
* spans and extension braces in their cell.
|
|
190
191
|
*
|
|
191
192
|
* @param {string} line - One table line.
|
|
192
193
|
* @returns {string[]} The trimmed cells, without the empty edges.
|
|
193
194
|
*/
|
|
194
195
|
function tableCells(line) {
|
|
195
196
|
const result = [];
|
|
197
|
+
const state = new syntax.inline.State(line, syntax, {}, []);
|
|
196
198
|
let cell = "",
|
|
197
|
-
wiki = false,
|
|
198
199
|
code = "",
|
|
199
200
|
quote = false,
|
|
200
201
|
braces = 0;
|
|
@@ -204,7 +205,15 @@ function tableCells(line) {
|
|
|
204
205
|
cell += char + line[++at];
|
|
205
206
|
continue;
|
|
206
207
|
}
|
|
207
|
-
if (char === "
|
|
208
|
+
if (char === "[" && !code && !braces) {
|
|
209
|
+
const end = syntax.helpers.parseLinkLabel(state, at, false);
|
|
210
|
+
if (end >= 0) {
|
|
211
|
+
cell += line.slice(at, end + 1);
|
|
212
|
+
at = end;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (char === "`") {
|
|
208
217
|
const marks = /^`+/.exec(line.slice(at))[0];
|
|
209
218
|
if (!code) code = marks;
|
|
210
219
|
else if (code === marks) code = "";
|
|
@@ -213,13 +222,11 @@ function tableCells(line) {
|
|
|
213
222
|
continue;
|
|
214
223
|
}
|
|
215
224
|
if (!code) {
|
|
216
|
-
if (line.slice(at, at + 2) === "[[") wiki = true;
|
|
217
|
-
if (line.slice(at - 1, at + 1) === "]]") wiki = false;
|
|
218
225
|
if (char === '"' && braces) quote = !quote;
|
|
219
226
|
if (!quote && char === "{") braces++;
|
|
220
227
|
if (!quote && char === "}" && braces) braces--;
|
|
221
228
|
}
|
|
222
|
-
if (char === "|" && !
|
|
229
|
+
if (char === "|" && !code && !braces) {
|
|
223
230
|
result.push(cell.trim());
|
|
224
231
|
cell = "";
|
|
225
232
|
} else cell += char;
|
|
@@ -43,6 +43,7 @@ import { MARKET_CLASSES, marketClass } from "./market-class.mjs";
|
|
|
43
43
|
import { hasTag } from "./note-vocabulary.mjs";
|
|
44
44
|
import { shortcodesOf } from "./holdings.mjs";
|
|
45
45
|
import { WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
|
|
46
|
+
import { nameFormsOf } from "./name-forms.mjs";
|
|
46
47
|
|
|
47
48
|
/** The heading text a settlements table stands under, at level two. */
|
|
48
49
|
export const SETTLEMENTS_HEADING = "Settlements";
|
|
@@ -332,7 +333,7 @@ export function findSettlementTables(raw) {
|
|
|
332
333
|
* @property {string} shortcode - Lower case.
|
|
333
334
|
* @property {string} subType
|
|
334
335
|
* @property {string} title
|
|
335
|
-
* @property {Set<string>} names - Its name and
|
|
336
|
+
* @property {Set<string>} names - Its name and listed forms, lower case.
|
|
336
337
|
* @property {string[]} parents - Enclosing places' shortcodes.
|
|
337
338
|
* @property {{heading: number, tables: SettlementTable[]}} section
|
|
338
339
|
*/
|
|
@@ -353,7 +354,7 @@ export function findSettlementTables(raw) {
|
|
|
353
354
|
const CORPUS = new WeakMap();
|
|
354
355
|
|
|
355
356
|
/**
|
|
356
|
-
* A note's name and
|
|
357
|
+
* A note's name and listed forms, lower case.
|
|
357
358
|
*
|
|
358
359
|
* @param {object} fm - The frontmatter.
|
|
359
360
|
* @returns {Set<string>} The names.
|
|
@@ -367,7 +368,7 @@ function namesOf(fm) {
|
|
|
367
368
|
if (typeof name === "string") add(name);
|
|
368
369
|
else if (name && typeof name === "object") {
|
|
369
370
|
add(name.full);
|
|
370
|
-
for (const
|
|
371
|
+
for (const form of nameFormsOf(name)) add(form);
|
|
371
372
|
}
|
|
372
373
|
return out;
|
|
373
374
|
}
|
package/engine/site-build.mjs
CHANGED
|
@@ -578,9 +578,6 @@ export function tableUniverse(pages) {
|
|
|
578
578
|
/**
|
|
579
579
|
* The frontmatter a page publishes with.
|
|
580
580
|
*
|
|
581
|
-
* An authored `aliases` is a list of names a reader might call the note. It is
|
|
582
|
-
* dropped: this build emits no redirects.
|
|
583
|
-
*
|
|
584
581
|
* A content page carries the package the build **derived**. No note
|
|
585
582
|
* declares one, so the note's frontmatter alone would publish a page that does
|
|
586
583
|
* not say which package it belongs to. Writing the derived value keeps a page
|
|
@@ -589,9 +586,8 @@ export function tableUniverse(pages) {
|
|
|
589
586
|
*
|
|
590
587
|
* **`related` is derived, never authored.** What links to a page is a fact
|
|
591
588
|
* about every other page, known only once the whole tree has resolved — see
|
|
592
|
-
* {@link module:engine/related-pages} — so an authored value is dropped
|
|
593
|
-
*
|
|
594
|
-
* holds the graph.
|
|
589
|
+
* {@link module:engine/related-pages} — so an authored value is dropped, and
|
|
590
|
+
* {@link renderPages} sets the derived block once it holds the graph.
|
|
595
591
|
*
|
|
596
592
|
* @param {object} page - The page.
|
|
597
593
|
* @param {object} options
|
|
@@ -621,7 +617,6 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
|
621
617
|
kbfolder: page.folder,
|
|
622
618
|
};
|
|
623
619
|
if (decorate) decorate(data, page);
|
|
624
|
-
delete data.aliases;
|
|
625
620
|
delete data.related;
|
|
626
621
|
if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
|
|
627
622
|
return data;
|
package/engine/site-search.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* the world. A note reached by more than one path carries every continent and
|
|
31
31
|
* region it reaches, so a being with homes in two regions is found under both.
|
|
32
32
|
*
|
|
33
|
-
* The note's name and its
|
|
33
|
+
* The note's name and its listed forms are weighted, so a search for a name finds
|
|
34
34
|
* its note first. A note's tags and its draft state are not filters.
|
|
35
35
|
*
|
|
36
36
|
* @module
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import { placeShortcodes } from "./map-places.mjs";
|
|
40
40
|
import { resolveInfoboxRef } from "./site-index.mjs";
|
|
41
|
+
import { nameFormsOf } from "./name-forms.mjs";
|
|
41
42
|
|
|
42
43
|
/** The filters a note's page carries, in the order a reader is offered them. */
|
|
43
44
|
export const SEARCH_FILTERS = Object.freeze([
|
|
@@ -153,9 +154,6 @@ export function searchMetadata(fm, { geography, index }) {
|
|
|
153
154
|
values: values[name].filter(Boolean),
|
|
154
155
|
})).filter((filter) => filter.values.length);
|
|
155
156
|
const name = isMapping(fm?.name) ? fm.name : {};
|
|
156
|
-
const names = [
|
|
157
|
-
text(name.full),
|
|
158
|
-
...(Array.isArray(name.aliases) ? name.aliases.map(text) : []),
|
|
159
|
-
].filter(Boolean);
|
|
157
|
+
const names = [text(name.full), ...nameFormsOf(name).map(text)].filter(Boolean);
|
|
160
158
|
return { filters, names: [...new Set(names)] };
|
|
161
159
|
}
|
package/engine/site-theme.mjs
CHANGED
|
@@ -261,7 +261,7 @@ export function tableOfContents(headings) {
|
|
|
261
261
|
return lines.join("\n");
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
/** How much more a page's name and
|
|
264
|
+
/** How much more a page's name and listed forms count in a search than a word of its body. */
|
|
265
265
|
const NAME_WEIGHT = 10;
|
|
266
266
|
|
|
267
267
|
/** The most `h2` headings a page may have before it gets a contents list. */
|