@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
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
import { isAddressTuple } from "./address.mjs";
|
|
56
56
|
import { listed, nearest } from "./near-miss.mjs";
|
|
57
57
|
import { checkDataKeys } from "./data-keys.mjs";
|
|
58
|
+
import { FALLBACK_LANGUAGE, normalizeName } from "./name-forms.mjs";
|
|
58
59
|
import {
|
|
59
60
|
authoredNoteKeys,
|
|
60
61
|
CHARACTER_NAME_KEYS,
|
|
@@ -1165,6 +1166,9 @@ function checkEmbeddedShortcodes(note, blockName) {
|
|
|
1165
1166
|
* configuration through {@link systemBlocksFor}, and this module states no
|
|
1166
1167
|
* system name of its own. See {@link DEFAULT_SYSTEM_BLOCKS} for the fallback.
|
|
1167
1168
|
* @param {object} [opts.addressContext] - Builder package and Address type vocabulary.
|
|
1169
|
+
* @param {Iterable<string>} [opts.languages] - The language codes the
|
|
1170
|
+
* configuration's `languages:` declares. A `name` key that is one of them, or
|
|
1171
|
+
* `en`, is a list of forms; absent, only `en` is.
|
|
1168
1172
|
* @param {readonly string[]} [opts.packs] - The pack names this package
|
|
1169
1173
|
* declares, for a `data:` field whose map is keyed by pack. Supplied by the
|
|
1170
1174
|
* caller like `schemas` and `vocabulary`, and absent it no claim is made
|
|
@@ -1181,6 +1185,7 @@ export function lintNote(
|
|
|
1181
1185
|
packs,
|
|
1182
1186
|
emittedArt,
|
|
1183
1187
|
addressContext,
|
|
1188
|
+
languages,
|
|
1184
1189
|
systems = DEFAULT_SYSTEM_BLOCKS,
|
|
1185
1190
|
},
|
|
1186
1191
|
) {
|
|
@@ -1254,15 +1259,17 @@ export function lintNote(
|
|
|
1254
1259
|
if (type === "being" && fm.subType === "character") {
|
|
1255
1260
|
for (const key of CHARACTER_NAME_KEYS) allowed.add(key);
|
|
1256
1261
|
}
|
|
1262
|
+
const codes = new Set([FALLBACK_LANGUAGE, ...(languages ?? [])]);
|
|
1257
1263
|
for (const key of Object.keys(name)) {
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1264
|
+
if (allowed.has(key) || codes.has(key)) continue;
|
|
1265
|
+
findings.push({
|
|
1266
|
+
file: note.file,
|
|
1267
|
+
...nameAt([key]),
|
|
1268
|
+
severity: "error",
|
|
1269
|
+
message:
|
|
1270
|
+
`\`name.${key}\` is not valid for this note: a name lists its forms ` +
|
|
1271
|
+
`under a language code \`languages:\` declares, or \`${FALLBACK_LANGUAGE}\``,
|
|
1272
|
+
});
|
|
1266
1273
|
}
|
|
1267
1274
|
if (typeof name.full !== "string" || !name.full.trim()) {
|
|
1268
1275
|
findings.push({
|
|
@@ -1272,18 +1279,34 @@ export function lintNote(
|
|
|
1272
1279
|
message: "`name.full` must be a nonempty string",
|
|
1273
1280
|
});
|
|
1274
1281
|
}
|
|
1275
|
-
|
|
1282
|
+
for (const code of Object.keys(name)) {
|
|
1283
|
+
if (!codes.has(code) || allowed.has(code)) continue;
|
|
1284
|
+
const forms = name[code];
|
|
1276
1285
|
if (
|
|
1277
|
-
!Array.isArray(
|
|
1278
|
-
|
|
1286
|
+
!Array.isArray(forms) ||
|
|
1287
|
+
forms.some((form) => typeof form !== "string" || !form.trim())
|
|
1279
1288
|
) {
|
|
1280
1289
|
findings.push({
|
|
1281
1290
|
file: note.file,
|
|
1282
|
-
...nameAt([
|
|
1291
|
+
...nameAt([code]),
|
|
1283
1292
|
severity: "error",
|
|
1284
|
-
message:
|
|
1293
|
+
message: `\`name.${code}\` must be a list of nonempty strings`,
|
|
1285
1294
|
});
|
|
1295
|
+
continue;
|
|
1286
1296
|
}
|
|
1297
|
+
if (code !== FALLBACK_LANGUAGE || typeof name.full !== "string") continue;
|
|
1298
|
+
const full = normalizeName(name.full);
|
|
1299
|
+
forms.forEach((form, i) => {
|
|
1300
|
+
if (normalizeName(form) !== full) return;
|
|
1301
|
+
findings.push({
|
|
1302
|
+
file: note.file,
|
|
1303
|
+
...positionOfFrontmatterPath(raw(), ["name", code, i]),
|
|
1304
|
+
severity: "error",
|
|
1305
|
+
message:
|
|
1306
|
+
`\`name.${code}\` repeats \`name.full\` (${form}); \`full\` is ` +
|
|
1307
|
+
"already an English form, so leave it out",
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1287
1310
|
}
|
|
1288
1311
|
if (
|
|
1289
1312
|
Object.hasOwn(name, "sort") &&
|
|
@@ -1738,6 +1761,8 @@ export function lintNote(
|
|
|
1738
1761
|
* @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
|
|
1739
1762
|
* The system blocks to check; see {@link lintNote} and
|
|
1740
1763
|
* {@link systemBlocksFor}.
|
|
1764
|
+
* @param {Iterable<string>} [opts.languages] - The configured language codes,
|
|
1765
|
+
* which a note's `name` lists its forms under; see {@link lintNote}.
|
|
1741
1766
|
* @param {readonly string[]} [opts.packs] - The declared pack names; see
|
|
1742
1767
|
* {@link lintNote}.
|
|
1743
1768
|
* @param {(type: string) => {document: string|null, art: readonly string[]}|null} [opts.emittedArt]
|
|
@@ -1747,7 +1772,7 @@ export function lintNote(
|
|
|
1747
1772
|
*/
|
|
1748
1773
|
export function lintFrontmatter(
|
|
1749
1774
|
index,
|
|
1750
|
-
{ schemas, vocabulary, packs, emittedArt, references = true, systems, currentDate },
|
|
1775
|
+
{ schemas, vocabulary, packs, emittedArt, references = true, systems, currentDate, languages },
|
|
1751
1776
|
) {
|
|
1752
1777
|
const findings = [];
|
|
1753
1778
|
const notes = [...index.notes].sort((a, b) =>
|
|
@@ -1763,6 +1788,7 @@ export function lintFrontmatter(
|
|
|
1763
1788
|
currentDate,
|
|
1764
1789
|
packs,
|
|
1765
1790
|
emittedArt,
|
|
1791
|
+
languages,
|
|
1766
1792
|
index: references ? index : undefined,
|
|
1767
1793
|
addressContext: {
|
|
1768
1794
|
package: index.contentPackage,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
noteTerms,
|
|
52
52
|
} from "./culture-reach.mjs";
|
|
53
53
|
import { pronunciationProblem } from "./word-terms.mjs";
|
|
54
|
+
import { noteLang } from "./glossary-language.mjs";
|
|
54
55
|
|
|
55
56
|
/** Whether a value is a plain map. */
|
|
56
57
|
const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -126,11 +127,10 @@ export function checkGlossary(note, { index } = {}) {
|
|
|
126
127
|
[i],
|
|
127
128
|
"error",
|
|
128
129
|
`\`${entry.name}\` resolves to no language: the entry states no ` +
|
|
129
|
-
"`language`, and " +
|
|
130
|
-
(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"; state `language` on the entry or `data.language` on the note",
|
|
130
|
+
"`language`, and the note's language" +
|
|
131
|
+
(noteLang(note) ? ` ${noteLang(note)}` : "") +
|
|
132
|
+
" has no `note` in `languages:`; state `language` on the entry, or " +
|
|
133
|
+
"`data.lang` on the note naming a language with a note",
|
|
134
134
|
),
|
|
135
135
|
);
|
|
136
136
|
const senses = noteTerms(index, note, entry).get(name);
|
|
@@ -16,16 +16,11 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 1. the entry's own `language`;
|
|
18
18
|
* 2. the note it is written on, when that note is a language;
|
|
19
|
-
* 3.
|
|
20
|
-
*
|
|
21
|
-
* culture's `data.language`;
|
|
22
|
-
* 4. on a culture, its own `data.language`.
|
|
19
|
+
* 3. the language note of the code the content index derived for the note it
|
|
20
|
+
* is written on, its `lang` — see {@link module:engine/note-language}.
|
|
23
21
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* inherits from its parent cultures. The lint and the `glossary` relation both
|
|
27
|
-
* resolve through here, each handing over how it reads an Address and looks a
|
|
28
|
-
* note up.
|
|
22
|
+
* The lint and the `glossary` relation both resolve through here, each handing
|
|
23
|
+
* over how it reads an Address, looks a note up and finds a code's note.
|
|
29
24
|
*
|
|
30
25
|
* A leaf module, importing nothing.
|
|
31
26
|
*
|
|
@@ -35,12 +30,6 @@
|
|
|
35
30
|
/** A note's parsed frontmatter, whichever shape the caller hands over. */
|
|
36
31
|
const payload = (note) => note?.fm ?? note ?? {};
|
|
37
32
|
|
|
38
|
-
/** A note's `data`, or an empty map. */
|
|
39
|
-
const dataOf = (note) => {
|
|
40
|
-
const data = payload(note).data;
|
|
41
|
-
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
42
|
-
};
|
|
43
|
-
|
|
44
33
|
/** A note's type or subType, lowercased. */
|
|
45
34
|
const lower = (value) => String(value ?? "").toLowerCase();
|
|
46
35
|
|
|
@@ -51,11 +40,22 @@ const lower = (value) => String(value ?? "").toLowerCase();
|
|
|
51
40
|
* @property {(value: unknown, type: string) => string|undefined} keyOf - A
|
|
52
41
|
* value's canonical Address in the `note` system, read with `type` as the
|
|
53
42
|
* default type, or `undefined` where it is no Address.
|
|
54
|
-
* @property {(
|
|
55
|
-
*
|
|
43
|
+
* @property {(code: string) => string|undefined} noteOf - The Address of a
|
|
44
|
+
* language code's note, or `undefined` where no note is configured for it.
|
|
56
45
|
* @property {string} [self] - The owning note's own Address, for a language.
|
|
57
46
|
*/
|
|
58
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The language a note is in, as the content index derived it.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} note - An index record, or a link-index note carrying it.
|
|
52
|
+
* @returns {string|undefined} The code.
|
|
53
|
+
*/
|
|
54
|
+
export function noteLang(note) {
|
|
55
|
+
const lang = note?.lang;
|
|
56
|
+
return typeof lang === "string" && lang ? lang : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
59
|
/**
|
|
60
60
|
* The language a glossary entry resolves to.
|
|
61
61
|
*
|
|
@@ -66,34 +66,11 @@ const lower = (value) => String(value ?? "").toLowerCase();
|
|
|
66
66
|
* @returns {string|undefined} The language's Address, or `undefined` where the
|
|
67
67
|
* chain finds none.
|
|
68
68
|
*/
|
|
69
|
-
export function glossaryLanguage(entry, owner, { keyOf,
|
|
69
|
+
export function glossaryLanguage(entry, owner, { keyOf, noteOf, self }) {
|
|
70
70
|
const own = keyOf(entry?.language, "skill");
|
|
71
71
|
if (own) return own;
|
|
72
72
|
const fm = payload(owner);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return keyOf(dataOf(owner).language, "skill");
|
|
77
|
-
if (type !== "affiliation") return undefined;
|
|
78
|
-
|
|
79
|
-
const seen = new Set();
|
|
80
|
-
let level = [owner];
|
|
81
|
-
while (level.length) {
|
|
82
|
-
const next = [];
|
|
83
|
-
for (const body of level) {
|
|
84
|
-
if (seen.has(body)) continue;
|
|
85
|
-
seen.add(body);
|
|
86
|
-
const language = keyOf(dataOf(body).language, "skill");
|
|
87
|
-
if (language) return language;
|
|
88
|
-
for (const parent of Array.isArray(dataOf(body).parents) ? dataOf(body).parents : []) {
|
|
89
|
-
const key = keyOf(parent, "affiliation");
|
|
90
|
-
const target = key ? lookup(key) : undefined;
|
|
91
|
-
if (target && lower(payload(target).type) === "affiliation") next.push(target);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
level = next;
|
|
95
|
-
}
|
|
96
|
-
const culture = keyOf(dataOf(owner).culture, "lore");
|
|
97
|
-
const target = culture ? lookup(culture) : undefined;
|
|
98
|
-
return target ? keyOf(dataOf(target).language, "skill") : undefined;
|
|
73
|
+
if (lower(fm.type) === "skill" && lower(fm.subType) === "language") return self;
|
|
74
|
+
const code = noteLang(owner);
|
|
75
|
+
return code ? noteOf(code) : undefined;
|
|
99
76
|
}
|
package/engine/homepage.mjs
CHANGED
|
@@ -355,9 +355,6 @@ export function homepageTitle(fm, config) {
|
|
|
355
355
|
* **No `url` and no `slug`.** The homepage publishes at the package root, so
|
|
356
356
|
* the page has no address to state.
|
|
357
357
|
*
|
|
358
|
-
* An authored `aliases` is dropped, as it is on every other page: this build
|
|
359
|
-
* emits no redirects.
|
|
360
|
-
*
|
|
361
358
|
* `related` is derived like every other page's — what links to the homepage
|
|
362
359
|
* is a fact about the rest of the tree — so an authored value is dropped and
|
|
363
360
|
* the caller's block, where it has one, is set in its place.
|
|
@@ -372,7 +369,6 @@ export function homepageTitle(fm, config) {
|
|
|
372
369
|
*/
|
|
373
370
|
export function homepageFrontmatter(fm, { contentPackage, title, related }) {
|
|
374
371
|
const data = { ...fm, package: contentPackage, title };
|
|
375
|
-
delete data.aliases;
|
|
376
372
|
delete data.related;
|
|
377
373
|
if (related) data.related = related;
|
|
378
374
|
return data;
|
package/engine/index-records.mjs
CHANGED
package/engine/infobox.mjs
CHANGED
|
@@ -129,6 +129,7 @@ import {
|
|
|
129
129
|
} from "./standings.mjs";
|
|
130
130
|
import { AddressEntries } from "./address-values.mjs";
|
|
131
131
|
import { noteCalendar } from "./note-calendars.mjs";
|
|
132
|
+
import { nameFormsOf } from "./name-forms.mjs";
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
135
|
* How a section arranges what it holds.
|
|
@@ -288,6 +289,9 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
288
289
|
withheld: "template machinery, not a fact about the subject",
|
|
289
290
|
}),
|
|
290
291
|
id: Object.freeze({ withheld: "document identity machinery, not a fact about the subject" }),
|
|
292
|
+
lang: Object.freeze({
|
|
293
|
+
withheld: "language machinery, a code rather than a fact a reader follows",
|
|
294
|
+
}),
|
|
291
295
|
pack: Object.freeze({ withheld: "compendium routing machinery, not a fact about the subject" }),
|
|
292
296
|
packFolder: Object.freeze({
|
|
293
297
|
withheld: "compendium routing machinery, not a fact about the subject",
|
|
@@ -1040,9 +1044,8 @@ function noteBox(
|
|
|
1040
1044
|
const shown = new Set();
|
|
1041
1045
|
const name = fm?.name?.full;
|
|
1042
1046
|
if (hasValue(name)) rows.push({ label: "Name", kind: "text", value: String(name) });
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1047
|
+
const forms = nameFormsOf(fm?.name);
|
|
1048
|
+
if (forms.length) rows.push({ label: "Other names", kind: "list", value: forms });
|
|
1046
1049
|
if (fm?.type === "being" && Object.hasOwn(BEING_SUBTYPE_LABELS, fm.subType)) {
|
|
1047
1050
|
rows.push({ label: "Type", kind: "text", value: BEING_SUBTYPE_LABELS[fm.subType] });
|
|
1048
1051
|
}
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
*
|
|
18
18
|
* A `skill` note of `subType: language` states them as `data.names`, and
|
|
19
19
|
* `package-build npc new` draws a person's name from them, or checks a name it
|
|
20
|
-
* is given against them. A culture reaches them through
|
|
21
|
-
* cultures sharing a tongue
|
|
20
|
+
* is given against them. A culture reaches them through its language, the
|
|
21
|
+
* note `languages:` names for its `data.lang`, so cultures sharing a tongue
|
|
22
|
+
* share its lists.
|
|
22
23
|
*
|
|
23
24
|
* - `pattern` — the name's shape: placeholders such as `{given}` and `{clan}`,
|
|
24
25
|
* each naming a list, with literal text around them or directly against
|
|
@@ -33,6 +34,10 @@
|
|
|
33
34
|
* person's gender's list, or from every given list together where that gender
|
|
34
35
|
* has none; `{given.<gender>}` draws from that gender's list whatever the
|
|
35
36
|
* person's gender, as a patronymic draws a father's name.
|
|
37
|
+
* - `en` — English names, in groups of lists: `en.epithet` is a list a pattern
|
|
38
|
+
* draws from as `{en.epithet}`, as `"{given} the {en.epithet}"` does. English
|
|
39
|
+
* names are never encoded in the tongue's script or checked against its
|
|
40
|
+
* spelling.
|
|
36
41
|
* - every other key — a list of names the pattern draws from by that key:
|
|
37
42
|
* `clan`, `family`, `place`. A list name is letters, digits and underscores,
|
|
38
43
|
* beginning with a letter.
|
|
@@ -55,13 +60,16 @@ import { nearest } from "./near-miss.mjs";
|
|
|
55
60
|
/** The `skill` subType whose notes may carry name lists. */
|
|
56
61
|
export const NAMES_SUBTYPE = "language";
|
|
57
62
|
|
|
63
|
+
/** The key whose groups of lists hold English names. */
|
|
64
|
+
export const ENGLISH_GROUP = "en";
|
|
65
|
+
|
|
58
66
|
/** The placeholder that draws from the given-name list for a gender. */
|
|
59
67
|
export const GIVEN_LIST = "given";
|
|
60
68
|
|
|
61
69
|
/** What a list name may be spelled with. */
|
|
62
70
|
const LIST_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
63
71
|
|
|
64
|
-
/** A placeholder's text: a list name, and for `given`
|
|
72
|
+
/** A placeholder's text: a list name, and after a dot a gender for `given` or a list for `en`. */
|
|
65
73
|
const PLACEHOLDER = /^([A-Za-z][A-Za-z0-9_]*)(?:\.([A-Za-z][A-Za-z0-9_]*))?$/;
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -80,11 +88,12 @@ export function requiredGivenGenders() {
|
|
|
80
88
|
* One piece of a parsed pattern: literal text, or a placeholder naming a list
|
|
81
89
|
* and, for `given`, the gender whose list it draws from.
|
|
82
90
|
*
|
|
83
|
-
* @typedef {{literal: string} | {list: string, gender?: string}} PatternPart
|
|
91
|
+
* @typedef {{literal: string} | {list: string, gender?: string, sub?: string}} PatternPart
|
|
84
92
|
*/
|
|
85
93
|
|
|
86
94
|
/** A placeholder as written, without its braces. */
|
|
87
95
|
function placeholderText(part) {
|
|
96
|
+
if (part.sub !== undefined) return `${part.list}.${part.sub}`;
|
|
88
97
|
return part.gender ? `${part.list}.${part.gender}` : part.list;
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -126,17 +135,22 @@ export function parsePattern(pattern) {
|
|
|
126
135
|
`has the placeholder \`{${text}}\`; a placeholder names a list in letters, ` +
|
|
127
136
|
"digits and underscores, beginning with a letter",
|
|
128
137
|
);
|
|
129
|
-
else if (match[
|
|
138
|
+
else if (match[1] === ENGLISH_GROUP && match[2] === undefined)
|
|
139
|
+
problems.push(
|
|
140
|
+
`has the placeholder \`{${text}}\`; \`en\` holds groups of English names, ` +
|
|
141
|
+
"so a placeholder names one as `{en.<list>}`",
|
|
142
|
+
);
|
|
143
|
+
else if (match[2] !== undefined && match[1] !== GIVEN_LIST && match[1] !== ENGLISH_GROUP)
|
|
130
144
|
problems.push(
|
|
131
145
|
`has the placeholder \`{${text}}\`; only \`given\` takes a gender after a dot, ` +
|
|
132
|
-
"as `{given.male}` does",
|
|
146
|
+
"as `{given.male}` does, and `en` a list, as `{en.epithet}` does",
|
|
133
147
|
);
|
|
134
148
|
else {
|
|
135
149
|
if (literal) parts.push({ literal });
|
|
136
150
|
literal = "";
|
|
137
|
-
parts.push(
|
|
138
|
-
|
|
139
|
-
);
|
|
151
|
+
if (match[2] === undefined) parts.push({ list: match[1] });
|
|
152
|
+
else if (match[1] === ENGLISH_GROUP) parts.push({ list: match[1], sub: match[2] });
|
|
153
|
+
else parts.push({ list: match[1], gender: match[2] });
|
|
140
154
|
}
|
|
141
155
|
i = close + 1;
|
|
142
156
|
}
|
|
@@ -263,7 +277,30 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
263
277
|
});
|
|
264
278
|
};
|
|
265
279
|
|
|
266
|
-
const lists = Object.keys(names).filter((key) => key !== "pattern");
|
|
280
|
+
const lists = Object.keys(names).filter((key) => key !== "pattern" && key !== ENGLISH_GROUP);
|
|
281
|
+
const english = names[ENGLISH_GROUP];
|
|
282
|
+
const englishLists = [];
|
|
283
|
+
if (english !== undefined && english !== null) {
|
|
284
|
+
if (!isPlainMap(english))
|
|
285
|
+
shape(
|
|
286
|
+
[ENGLISH_GROUP],
|
|
287
|
+
`\`${label([ENGLISH_GROUP])}\` should be a map of names to lists of English names`,
|
|
288
|
+
);
|
|
289
|
+
else
|
|
290
|
+
for (const key of Object.keys(english)) {
|
|
291
|
+
if (!LIST_NAME.test(key)) {
|
|
292
|
+
problem(
|
|
293
|
+
[ENGLISH_GROUP, key],
|
|
294
|
+
`"${key}" is not a list name; a list of \`${label([ENGLISH_GROUP])}\` is ` +
|
|
295
|
+
"named in letters, digits and underscores, beginning with a letter",
|
|
296
|
+
true,
|
|
297
|
+
);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
englishLists.push(key);
|
|
301
|
+
checkList([ENGLISH_GROUP, key]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
267
304
|
for (const key of lists) {
|
|
268
305
|
if (key === GIVEN_LIST) continue;
|
|
269
306
|
if (!LIST_NAME.test(key)) {
|
|
@@ -376,6 +413,19 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
376
413
|
if (parsed.problems.length) unreadable = true;
|
|
377
414
|
for (const part of parsed.parts) {
|
|
378
415
|
if (!("list" in part)) continue;
|
|
416
|
+
if (part.sub !== undefined) {
|
|
417
|
+
named.add(placeholderText(part));
|
|
418
|
+
if (englishLists.includes(part.sub)) continue;
|
|
419
|
+
const guess = nearest(part.sub, englishLists);
|
|
420
|
+
problem(
|
|
421
|
+
path,
|
|
422
|
+
`\`{${placeholderText(part)}}\` in \`${label(path)}\` names no list of ` +
|
|
423
|
+
`\`${label([ENGLISH_GROUP])}\`` +
|
|
424
|
+
(englishLists.length ? `; the lists are ${englishLists.join(", ")}` : "") +
|
|
425
|
+
(guess ? `. Did you mean "{en.${guess}}"?` : ""),
|
|
426
|
+
);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
379
429
|
named.add(part.list);
|
|
380
430
|
if (part.gender !== undefined) {
|
|
381
431
|
if (givenMap[part.gender] === undefined || givenMap[part.gender] === null)
|
|
@@ -397,6 +447,15 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
397
447
|
}
|
|
398
448
|
}
|
|
399
449
|
if (unreadable) return problems;
|
|
450
|
+
for (const list of englishLists) {
|
|
451
|
+
if (named.has(`${ENGLISH_GROUP}.${list}`)) continue;
|
|
452
|
+
problem(
|
|
453
|
+
[ENGLISH_GROUP, list],
|
|
454
|
+
`\`${label([ENGLISH_GROUP, list])}\` is never drawn from: no pattern of ` +
|
|
455
|
+
`\`${label(["pattern"])}\` has \`{${ENGLISH_GROUP}.${list}}\``,
|
|
456
|
+
true,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
400
459
|
for (const list of lists) {
|
|
401
460
|
if (named.has(list)) continue;
|
|
402
461
|
problem(
|
|
@@ -490,6 +549,7 @@ export function givenNames(names, gender) {
|
|
|
490
549
|
|
|
491
550
|
/** The names one placeholder draws from, for a person of `gender`. */
|
|
492
551
|
function listFor(names, part, gender) {
|
|
552
|
+
if (part.sub !== undefined) return names[part.list][part.sub];
|
|
493
553
|
if (part.list !== GIVEN_LIST) return names[part.list];
|
|
494
554
|
if (part.gender !== undefined) return names[GIVEN_LIST][part.gender];
|
|
495
555
|
return givenNames(names, gender);
|
package/engine/language-rows.mjs
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
SOUND_CHANGE_STEP_KEYS,
|
|
69
69
|
TONGUE_KEYS,
|
|
70
70
|
} from "./language-terms.mjs";
|
|
71
|
+
import { nameFormsOf } from "./name-forms.mjs";
|
|
71
72
|
|
|
72
73
|
/** The columns every list relation opens with. */
|
|
73
74
|
const IDENTITY = Object.freeze(["language", "position"]);
|
|
@@ -354,7 +355,7 @@ const soundsOf = (data) => (isMap(data.sounds) ? data.sounds : {});
|
|
|
354
355
|
|
|
355
356
|
/**
|
|
356
357
|
* Every name a note of the package carries, with the Addresses carrying it:
|
|
357
|
-
* each string of a note's `name`, its
|
|
358
|
+
* each string of a note's `name`, its listed forms, an affiliation's rank and
|
|
358
359
|
* office titles, and the name of each anchor, cited with the anchor.
|
|
359
360
|
*
|
|
360
361
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
@@ -371,12 +372,10 @@ function carriedNames(records) {
|
|
|
371
372
|
for (const record of records) {
|
|
372
373
|
const address = noteAddressOf(record);
|
|
373
374
|
if (!address || record.documents || record.type === "folder") continue;
|
|
374
|
-
if (isMap(record.name))
|
|
375
|
-
for (const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
else carry(value, address);
|
|
379
|
-
}
|
|
375
|
+
if (isMap(record.name)) {
|
|
376
|
+
for (const value of Object.values(record.name)) carry(value, address);
|
|
377
|
+
for (const form of nameFormsOf(record.name)) carry(form, address);
|
|
378
|
+
}
|
|
380
379
|
const governance = isMap(record.data?.governance) ? record.data.governance : {};
|
|
381
380
|
for (const rank of Array.isArray(governance.ranks) ? governance.ranks : [])
|
|
382
381
|
if (isMap(rank)) carry(rank.title, address);
|
|
@@ -26,6 +26,7 @@ import { scanBlocks } from "./content-blocks.mjs";
|
|
|
26
26
|
import { renderDictionary, renderGlossary } from "./dictionary-layout.mjs";
|
|
27
27
|
import { DICTIONARY_COLUMNS, TERM_COLUMNS, dictionaryAndTermRows } from "./dictionary-rows.mjs";
|
|
28
28
|
import { glossaryLanguage } from "./glossary-language.mjs";
|
|
29
|
+
import { languageNotesByCode } from "./note-language.mjs";
|
|
29
30
|
import { scanHeadingAttributes } from "./heading-attributes.mjs";
|
|
30
31
|
import { isNoteRecord } from "./index-records.mjs";
|
|
31
32
|
|
|
@@ -117,6 +118,7 @@ export function lexicalSectionsFor(records, all = records) {
|
|
|
117
118
|
const visible = encodeAddresses(records).filter(isNoteRecord);
|
|
118
119
|
const complete = encodeAddresses(all).filter(isNoteRecord);
|
|
119
120
|
const byKey = new Map(visible.map((record) => [keyOf(record), record]));
|
|
121
|
+
const languageNotes = languageNotesByCode(complete, keyOf);
|
|
120
122
|
const addressContext = {
|
|
121
123
|
types: new Set(complete.map((record) => record.type)),
|
|
122
124
|
packages: new Set(complete.map((record) => record.package)),
|
|
@@ -136,7 +138,7 @@ export function lexicalSectionsFor(records, all = records) {
|
|
|
136
138
|
const pageLanguage = glossaryLanguage(undefined, record, {
|
|
137
139
|
self: key,
|
|
138
140
|
keyOf: (value) => (typeof value === "string" ? value : undefined),
|
|
139
|
-
|
|
141
|
+
noteOf: (code) => languageNotes.get(code),
|
|
140
142
|
});
|
|
141
143
|
sections.set(key, [
|
|
142
144
|
{
|
|
@@ -17,10 +17,8 @@
|
|
|
17
17
|
* `data.culture`, is a field every lore note but a culture note may state,
|
|
18
18
|
* declared in `culture-choice.mjs`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `package-build npc new` draws from, and an affiliation the trade, temple or
|
|
23
|
-
* order tongue it keeps.
|
|
20
|
+
* The tongue a work is composed in is its `data.lang`, which every note may
|
|
21
|
+
* state.
|
|
24
22
|
*
|
|
25
23
|
* `DataFieldSpec` declares the keys a type accepts, not the subType that may
|
|
26
24
|
* write them, so {@link checkLiteratureNote} scopes these keys to the subTypes
|
|
@@ -29,74 +27,13 @@
|
|
|
29
27
|
* @module
|
|
30
28
|
*/
|
|
31
29
|
|
|
32
|
-
import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
|
|
33
30
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
34
31
|
|
|
35
32
|
/** The `lore` subType whose notes are works of literature. */
|
|
36
33
|
export const LITERATURE_SUBTYPE = "literature";
|
|
37
34
|
|
|
38
|
-
/** The `lore` subTypes that may name a language. */
|
|
39
|
-
export const LANGUAGE_SUBTYPES = Object.freeze([LITERATURE_SUBTYPE, "culture"]);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Require `data.language` to address a `skill` note whose subType is
|
|
43
|
-
* `language`.
|
|
44
|
-
*
|
|
45
|
-
* An Address that does not resolve is the reference check's to report, so
|
|
46
|
-
* this speaks only about a target that exists and is the wrong kind.
|
|
47
|
-
*
|
|
48
|
-
* @param {object} note
|
|
49
|
-
* @param {{index?: object}} [options]
|
|
50
|
-
* @returns {object[]} The findings.
|
|
51
|
-
*/
|
|
52
|
-
export function checkLanguageChoice(note, { index } = {}) {
|
|
53
|
-
const value = note.fm?.data?.language;
|
|
54
|
-
if (value === undefined || value === null || !index?.addressHit) return [];
|
|
55
|
-
const tuple = parseAddress(value, {
|
|
56
|
-
package: index.contentPackage,
|
|
57
|
-
system: "note",
|
|
58
|
-
type: "skill",
|
|
59
|
-
types: index.types,
|
|
60
|
-
packages: index.packages,
|
|
61
|
-
});
|
|
62
|
-
if (tuple.reason) return [];
|
|
63
|
-
const target = index.addressHit(renderAddress(tuple));
|
|
64
|
-
if (!target) return [];
|
|
65
|
-
const fm = target.fm ?? target;
|
|
66
|
-
if (tuple.type === "skill" && fm.subType === "language") return [];
|
|
67
|
-
return [
|
|
68
|
-
{
|
|
69
|
-
file: note.file,
|
|
70
|
-
...positionOfFrontmatterPath(note.raw ?? "", ["data", "language"]),
|
|
71
|
-
severity: "error",
|
|
72
|
-
message: `data.language ${isAddressTuple(value) ? renderAddress(value) : value} names a note that is not a skill whose subType is language`,
|
|
73
|
-
},
|
|
74
|
-
];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* `data.language`: the tongue a work is composed in, the tongue a culture's
|
|
79
|
-
* people speak, or the tongue an affiliation keeps. Optional on all three.
|
|
80
|
-
*
|
|
81
|
-
* @type {object}
|
|
82
|
-
*/
|
|
83
|
-
export const LANGUAGE_FIELD = Object.freeze({
|
|
84
|
-
name: "language",
|
|
85
|
-
shape: "an Address",
|
|
86
|
-
kind: "address",
|
|
87
|
-
ref: "skill",
|
|
88
|
-
accepts: Object.freeze(["skill"]),
|
|
89
|
-
check: checkLanguageChoice,
|
|
90
|
-
describe:
|
|
91
|
-
"The tongue the note is in, as a language skill note: the tongue a work is composed " +
|
|
92
|
-
"in, a culture's people speak, or an affiliation keeps as its trade, temple or order " +
|
|
93
|
-
"tongue. `package-build npc new` draws a culture's names from its language's " +
|
|
94
|
-
"`data.names`. A `literature` or `culture` lore note, or an affiliation, only.",
|
|
95
|
-
});
|
|
96
|
-
|
|
97
35
|
/**
|
|
98
|
-
* The fields only a `literature` note may state
|
|
99
|
-
* {@link LANGUAGE_FIELD}. Every one is optional.
|
|
36
|
+
* The fields only a `literature` note may state. Every one is optional.
|
|
100
37
|
*
|
|
101
38
|
* @type {ReadonlyArray<object>}
|
|
102
39
|
*/
|
|
@@ -124,8 +61,7 @@ export const LITERATURE_FIELDS = Object.freeze([
|
|
|
124
61
|
const LITERATURE_KEYS = Object.freeze(LITERATURE_FIELDS.map((field) => field.name));
|
|
125
62
|
|
|
126
63
|
/**
|
|
127
|
-
* Refuse a literature field on a lore note of any other subType
|
|
128
|
-
* `data.language` on a lore note that is neither a work nor a culture.
|
|
64
|
+
* Refuse a literature field on a lore note of any other subType.
|
|
129
65
|
*
|
|
130
66
|
* @param {object} note
|
|
131
67
|
* @returns {object[]} One error per key written off the subTypes that take it.
|
|
@@ -153,13 +89,5 @@ export function checkLiteratureNote(note) {
|
|
|
153
89
|
`\`subType\` is not \`literature\``,
|
|
154
90
|
),
|
|
155
91
|
);
|
|
156
|
-
if (!LANGUAGE_SUBTYPES.includes(subType) && stated(LANGUAGE_FIELD.name))
|
|
157
|
-
findings.push(
|
|
158
|
-
finding(
|
|
159
|
-
LANGUAGE_FIELD.name,
|
|
160
|
-
"`data.language` names the tongue of a work of literature or of a culture, " +
|
|
161
|
-
"and this note's `subType` is neither `literature` nor `culture`",
|
|
162
|
-
),
|
|
163
|
-
);
|
|
164
92
|
return findings;
|
|
165
93
|
}
|