@heroiclands/package-build 25.1.2 → 25.2.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.
- 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 +7 -5
- package/docs/diagnostics.md +75 -0
- package/docs/encoding-core.md +11 -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 +329 -187
- package/docs/reference/note-types.md +274 -274
- 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/encoding-lexicon.mjs +12 -2
- 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/language-names.mjs +70 -10
- 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 +35 -13
- package/engine/pdf-index.mjs +3 -2
- package/engine/pdf-render.mjs +2 -4
- 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/language-names.d.mts +3 -0
- 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,
|
|
@@ -773,7 +773,8 @@ const NAMES_FIELD = Object.freeze({
|
|
|
773
773
|
'builds a name from placeholders such as "{given} {clan}" and literal text, `given` ' +
|
|
774
774
|
"maps a gender to a list of given names, " +
|
|
775
775
|
`${wordList([...requiredGivenGenders()]).replace(" or ", " and ")} required, and ` +
|
|
776
|
-
"every other key is a list of names a
|
|
776
|
+
"`en` holds groups of English names, and every other key is a list of names a " +
|
|
777
|
+
"placeholder of the same name draws from. A " +
|
|
777
778
|
"language skill note only. See [name lists](format-details.md#name-lists).",
|
|
778
779
|
fields: Object.freeze([
|
|
779
780
|
Object.freeze({
|
|
@@ -783,7 +784,8 @@ const NAMES_FIELD = Object.freeze({
|
|
|
783
784
|
shape: 'a pattern such as "{given} {clan}", a list of them, or a map of genders to either',
|
|
784
785
|
describe:
|
|
785
786
|
"How a name is built: each `{list}` placeholder is filled from the list of " +
|
|
786
|
-
"that name, `{given.<gender>}` from that gender's given names,
|
|
787
|
+
"that name, `{given.<gender>}` from that gender's given names, `{en.<list>}` from a " +
|
|
788
|
+
"list of English names, and the text " +
|
|
787
789
|
"around them is written as it stands. A list states every form a name may " +
|
|
788
790
|
"take; a map keyed by gender states each gender's forms, and a gender with " +
|
|
789
791
|
"none takes every pattern stated.",
|
|
@@ -822,6 +824,24 @@ const NAMES_FIELD = Object.freeze({
|
|
|
822
824
|
}),
|
|
823
825
|
}),
|
|
824
826
|
}),
|
|
827
|
+
Object.freeze({
|
|
828
|
+
name: "en",
|
|
829
|
+
kind: "map",
|
|
830
|
+
shape: "a map of list names to lists of English names",
|
|
831
|
+
describe:
|
|
832
|
+
"English names, never encoded in the tongue's script. A pattern draws from one " +
|
|
833
|
+
"list as `{en.<list>}`, as `{en.epithet}` does.",
|
|
834
|
+
values: Object.freeze({
|
|
835
|
+
kind: "list",
|
|
836
|
+
shape: "a list of English names",
|
|
837
|
+
describe: "A list of English names a placeholder draws from.",
|
|
838
|
+
entries: Object.freeze({
|
|
839
|
+
kind: "string",
|
|
840
|
+
shape: "a name",
|
|
841
|
+
describe: "One English name, as written.",
|
|
842
|
+
}),
|
|
843
|
+
}),
|
|
844
|
+
}),
|
|
825
845
|
]),
|
|
826
846
|
values: Object.freeze({
|
|
827
847
|
kind: "list",
|
|
@@ -1015,7 +1035,9 @@ export const NOTE_TOP_LEVEL_FIELDS = Object.freeze([
|
|
|
1015
1035
|
}),
|
|
1016
1036
|
Object.freeze({
|
|
1017
1037
|
name: "name",
|
|
1018
|
-
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`.",
|
|
1019
1041
|
}),
|
|
1020
1042
|
Object.freeze({
|
|
1021
1043
|
name: "type",
|
|
@@ -1079,6 +1101,14 @@ export const NOTE_TOP_LEVEL_KEY_SET = Object.freeze(new Set(NOTE_TOP_LEVEL_KEYS)
|
|
|
1079
1101
|
* @type {readonly DataFieldSpec[]}
|
|
1080
1102
|
*/
|
|
1081
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
|
+
}),
|
|
1082
1112
|
Object.freeze({
|
|
1083
1113
|
name: "seeAlso",
|
|
1084
1114
|
kind: "list",
|
|
@@ -1896,13 +1926,6 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1896
1926
|
...CULTURE_FIELD,
|
|
1897
1927
|
describe: "The people this affiliation belongs to, as a culture lore note.",
|
|
1898
1928
|
},
|
|
1899
|
-
{
|
|
1900
|
-
...LANGUAGE_FIELD,
|
|
1901
|
-
describe:
|
|
1902
|
-
"The trade, temple or order tongue the affiliation keeps, as a language " +
|
|
1903
|
-
"skill note. A glossary entry that states no `language` is in it. The " +
|
|
1904
|
-
"affiliation chooses its own common vocabulary and context meanings.",
|
|
1905
|
-
},
|
|
1906
1929
|
GLOSSARY_FIELD,
|
|
1907
1930
|
{
|
|
1908
1931
|
name: "relations",
|
|
@@ -2445,7 +2468,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2445
2468
|
accepts: ["lore"],
|
|
2446
2469
|
check: checkCultureParents,
|
|
2447
2470
|
describe:
|
|
2448
|
-
"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`.",
|
|
2449
2472
|
},
|
|
2450
2473
|
{
|
|
2451
2474
|
...CULTURE_FIELD,
|
|
@@ -2455,7 +2478,6 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2455
2478
|
"culture, and states the field only as itself.",
|
|
2456
2479
|
},
|
|
2457
2480
|
...LITERATURE_FIELDS,
|
|
2458
|
-
LANGUAGE_FIELD,
|
|
2459
2481
|
EVENTS_FIELD,
|
|
2460
2482
|
APPEARANCE_PROFILE_FIELD,
|
|
2461
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.
|
|
@@ -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. */
|