@heroiclands/package-build 25.1.2 → 25.1.3
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 +7 -0
- package/docs/encoding-core.md +11 -0
- package/docs/reference/format-details.md +30 -2
- package/docs/reference/note-types.md +258 -257
- package/engine/encoding-lexicon.mjs +12 -2
- package/engine/language-names.mjs +67 -8
- package/engine/note-vocabulary.mjs +22 -2
- package/package.json +1 -1
- package/types/engine/language-names.d.mts +3 -0
|
@@ -22,6 +22,7 @@ import { isLanguageRecord, lineageChain, noteAddressOf } from "./word-rows.mjs";
|
|
|
22
22
|
import { derivedForms, foldWord } from "./word-forms.mjs";
|
|
23
23
|
import { WRITTEN_ENDINGS, RESERVED_TONGUE } from "./language-terms.mjs";
|
|
24
24
|
import { LANGUAGE_SUBTYPE, glossText } from "./word-terms.mjs";
|
|
25
|
+
import { ENGLISH_GROUP } from "./language-names.mjs";
|
|
25
26
|
import { writingsOf } from "./language-writings.mjs";
|
|
26
27
|
import { encodeText, validateSource, EncodingError } from "./encoding.mjs";
|
|
27
28
|
import { encodeWithLanguage } from "./encoding-language.mjs";
|
|
@@ -642,7 +643,10 @@ export function checkEncodingLexicon(note, ctx) {
|
|
|
642
643
|
...new Map(
|
|
643
644
|
[...(ctx.encodings?.values() ?? [])].map((encoding) => [encoding.name, encoding]),
|
|
644
645
|
).values(),
|
|
645
|
-
]
|
|
646
|
+
]
|
|
647
|
+
.filter((encoding) => aliases.has(encoding.language))
|
|
648
|
+
// A codes-only script accepts no letter, so the roman forms are not written in it.
|
|
649
|
+
.filter((encoding) => [...encoding.accepted].some((char) => /\p{L}/u.test(char)));
|
|
646
650
|
if (!encodings.length) return [];
|
|
647
651
|
const forms = [],
|
|
648
652
|
data = note.fm.data ?? {};
|
|
@@ -653,6 +657,7 @@ export function checkEncodingLexicon(note, ctx) {
|
|
|
653
657
|
add(word?.form, ["words", i, "form"]);
|
|
654
658
|
listOf(word?.variants).forEach((form, j) => add(form, ["words", i, "variants", j]));
|
|
655
659
|
});
|
|
660
|
+
// `en` holds English names, which no encoding of the tongue sees.
|
|
656
661
|
const names = (value, keys) => {
|
|
657
662
|
if (Array.isArray(value))
|
|
658
663
|
value.forEach((item, i) =>
|
|
@@ -660,7 +665,12 @@ export function checkEncodingLexicon(note, ctx) {
|
|
|
660
665
|
);
|
|
661
666
|
else if (isMap(value))
|
|
662
667
|
for (const [key, item] of Object.entries(value))
|
|
663
|
-
if (
|
|
668
|
+
if (
|
|
669
|
+
key !== "pattern" &&
|
|
670
|
+
key !== "metadata" &&
|
|
671
|
+
!(keys.length === 1 && key === ENGLISH_GROUP)
|
|
672
|
+
)
|
|
673
|
+
names(item, [...keys, key]);
|
|
664
674
|
};
|
|
665
675
|
names(data.names, ["names"]);
|
|
666
676
|
listOf(data.elements).forEach((element, i) => {
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
* person's gender's list, or from every given list together where that gender
|
|
34
34
|
* has none; `{given.<gender>}` draws from that gender's list whatever the
|
|
35
35
|
* person's gender, as a patronymic draws a father's name.
|
|
36
|
+
* - `en` — English names, in groups of lists: `en.epithet` is a list a pattern
|
|
37
|
+
* draws from as `{en.epithet}`, as `"{given} the {en.epithet}"` does. English
|
|
38
|
+
* names are never encoded in the tongue's script or checked against its
|
|
39
|
+
* spelling.
|
|
36
40
|
* - every other key — a list of names the pattern draws from by that key:
|
|
37
41
|
* `clan`, `family`, `place`. A list name is letters, digits and underscores,
|
|
38
42
|
* beginning with a letter.
|
|
@@ -55,13 +59,16 @@ import { nearest } from "./near-miss.mjs";
|
|
|
55
59
|
/** The `skill` subType whose notes may carry name lists. */
|
|
56
60
|
export const NAMES_SUBTYPE = "language";
|
|
57
61
|
|
|
62
|
+
/** The key whose groups of lists hold English names. */
|
|
63
|
+
export const ENGLISH_GROUP = "en";
|
|
64
|
+
|
|
58
65
|
/** The placeholder that draws from the given-name list for a gender. */
|
|
59
66
|
export const GIVEN_LIST = "given";
|
|
60
67
|
|
|
61
68
|
/** What a list name may be spelled with. */
|
|
62
69
|
const LIST_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
63
70
|
|
|
64
|
-
/** A placeholder's text: a list name, and for `given`
|
|
71
|
+
/** A placeholder's text: a list name, and after a dot a gender for `given` or a list for `en`. */
|
|
65
72
|
const PLACEHOLDER = /^([A-Za-z][A-Za-z0-9_]*)(?:\.([A-Za-z][A-Za-z0-9_]*))?$/;
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -80,11 +87,12 @@ export function requiredGivenGenders() {
|
|
|
80
87
|
* One piece of a parsed pattern: literal text, or a placeholder naming a list
|
|
81
88
|
* and, for `given`, the gender whose list it draws from.
|
|
82
89
|
*
|
|
83
|
-
* @typedef {{literal: string} | {list: string, gender?: string}} PatternPart
|
|
90
|
+
* @typedef {{literal: string} | {list: string, gender?: string, sub?: string}} PatternPart
|
|
84
91
|
*/
|
|
85
92
|
|
|
86
93
|
/** A placeholder as written, without its braces. */
|
|
87
94
|
function placeholderText(part) {
|
|
95
|
+
if (part.sub !== undefined) return `${part.list}.${part.sub}`;
|
|
88
96
|
return part.gender ? `${part.list}.${part.gender}` : part.list;
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -126,17 +134,22 @@ export function parsePattern(pattern) {
|
|
|
126
134
|
`has the placeholder \`{${text}}\`; a placeholder names a list in letters, ` +
|
|
127
135
|
"digits and underscores, beginning with a letter",
|
|
128
136
|
);
|
|
129
|
-
else if (match[
|
|
137
|
+
else if (match[1] === ENGLISH_GROUP && match[2] === undefined)
|
|
138
|
+
problems.push(
|
|
139
|
+
`has the placeholder \`{${text}}\`; \`en\` holds groups of English names, ` +
|
|
140
|
+
"so a placeholder names one as `{en.<list>}`",
|
|
141
|
+
);
|
|
142
|
+
else if (match[2] !== undefined && match[1] !== GIVEN_LIST && match[1] !== ENGLISH_GROUP)
|
|
130
143
|
problems.push(
|
|
131
144
|
`has the placeholder \`{${text}}\`; only \`given\` takes a gender after a dot, ` +
|
|
132
|
-
"as `{given.male}` does",
|
|
145
|
+
"as `{given.male}` does, and `en` a list, as `{en.epithet}` does",
|
|
133
146
|
);
|
|
134
147
|
else {
|
|
135
148
|
if (literal) parts.push({ literal });
|
|
136
149
|
literal = "";
|
|
137
|
-
parts.push(
|
|
138
|
-
|
|
139
|
-
);
|
|
150
|
+
if (match[2] === undefined) parts.push({ list: match[1] });
|
|
151
|
+
else if (match[1] === ENGLISH_GROUP) parts.push({ list: match[1], sub: match[2] });
|
|
152
|
+
else parts.push({ list: match[1], gender: match[2] });
|
|
140
153
|
}
|
|
141
154
|
i = close + 1;
|
|
142
155
|
}
|
|
@@ -263,7 +276,30 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
263
276
|
});
|
|
264
277
|
};
|
|
265
278
|
|
|
266
|
-
const lists = Object.keys(names).filter((key) => key !== "pattern");
|
|
279
|
+
const lists = Object.keys(names).filter((key) => key !== "pattern" && key !== ENGLISH_GROUP);
|
|
280
|
+
const english = names[ENGLISH_GROUP];
|
|
281
|
+
const englishLists = [];
|
|
282
|
+
if (english !== undefined && english !== null) {
|
|
283
|
+
if (!isPlainMap(english))
|
|
284
|
+
shape(
|
|
285
|
+
[ENGLISH_GROUP],
|
|
286
|
+
`\`${label([ENGLISH_GROUP])}\` should be a map of names to lists of English names`,
|
|
287
|
+
);
|
|
288
|
+
else
|
|
289
|
+
for (const key of Object.keys(english)) {
|
|
290
|
+
if (!LIST_NAME.test(key)) {
|
|
291
|
+
problem(
|
|
292
|
+
[ENGLISH_GROUP, key],
|
|
293
|
+
`"${key}" is not a list name; a list of \`${label([ENGLISH_GROUP])}\` is ` +
|
|
294
|
+
"named in letters, digits and underscores, beginning with a letter",
|
|
295
|
+
true,
|
|
296
|
+
);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
englishLists.push(key);
|
|
300
|
+
checkList([ENGLISH_GROUP, key]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
267
303
|
for (const key of lists) {
|
|
268
304
|
if (key === GIVEN_LIST) continue;
|
|
269
305
|
if (!LIST_NAME.test(key)) {
|
|
@@ -376,6 +412,19 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
376
412
|
if (parsed.problems.length) unreadable = true;
|
|
377
413
|
for (const part of parsed.parts) {
|
|
378
414
|
if (!("list" in part)) continue;
|
|
415
|
+
if (part.sub !== undefined) {
|
|
416
|
+
named.add(placeholderText(part));
|
|
417
|
+
if (englishLists.includes(part.sub)) continue;
|
|
418
|
+
const guess = nearest(part.sub, englishLists);
|
|
419
|
+
problem(
|
|
420
|
+
path,
|
|
421
|
+
`\`{${placeholderText(part)}}\` in \`${label(path)}\` names no list of ` +
|
|
422
|
+
`\`${label([ENGLISH_GROUP])}\`` +
|
|
423
|
+
(englishLists.length ? `; the lists are ${englishLists.join(", ")}` : "") +
|
|
424
|
+
(guess ? `. Did you mean "{en.${guess}}"?` : ""),
|
|
425
|
+
);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
379
428
|
named.add(part.list);
|
|
380
429
|
if (part.gender !== undefined) {
|
|
381
430
|
if (givenMap[part.gender] === undefined || givenMap[part.gender] === null)
|
|
@@ -397,6 +446,15 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
397
446
|
}
|
|
398
447
|
}
|
|
399
448
|
if (unreadable) return problems;
|
|
449
|
+
for (const list of englishLists) {
|
|
450
|
+
if (named.has(`${ENGLISH_GROUP}.${list}`)) continue;
|
|
451
|
+
problem(
|
|
452
|
+
[ENGLISH_GROUP, list],
|
|
453
|
+
`\`${label([ENGLISH_GROUP, list])}\` is never drawn from: no pattern of ` +
|
|
454
|
+
`\`${label(["pattern"])}\` has \`{${ENGLISH_GROUP}.${list}}\``,
|
|
455
|
+
true,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
400
458
|
for (const list of lists) {
|
|
401
459
|
if (named.has(list)) continue;
|
|
402
460
|
problem(
|
|
@@ -490,6 +548,7 @@ export function givenNames(names, gender) {
|
|
|
490
548
|
|
|
491
549
|
/** The names one placeholder draws from, for a person of `gender`. */
|
|
492
550
|
function listFor(names, part, gender) {
|
|
551
|
+
if (part.sub !== undefined) return names[part.list][part.sub];
|
|
493
552
|
if (part.list !== GIVEN_LIST) return names[part.list];
|
|
494
553
|
if (part.gender !== undefined) return names[GIVEN_LIST][part.gender];
|
|
495
554
|
return givenNames(names, gender);
|
|
@@ -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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "25.1.
|
|
3
|
+
"version": "25.1.3",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -104,6 +104,8 @@ export function drawName(names: object, gender: string, next: () => number): str
|
|
|
104
104
|
export function nameMisfit(full: string, names: object, gender: string): string | undefined;
|
|
105
105
|
/** The `skill` subType whose notes may carry name lists. */
|
|
106
106
|
export const NAMES_SUBTYPE: "language";
|
|
107
|
+
/** The key whose groups of lists hold English names. */
|
|
108
|
+
export const ENGLISH_GROUP: "en";
|
|
107
109
|
/** The placeholder that draws from the given-name list for a gender. */
|
|
108
110
|
export const GIVEN_LIST: "given";
|
|
109
111
|
/**
|
|
@@ -115,6 +117,7 @@ export type PatternPart = {
|
|
|
115
117
|
} | {
|
|
116
118
|
list: string;
|
|
117
119
|
gender?: string;
|
|
120
|
+
sub?: string;
|
|
118
121
|
};
|
|
119
122
|
/**
|
|
120
123
|
* One thing wrong with a language's name lists.
|