@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.
@@ -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
- ].filter((encoding) => aliases.has(encoding.language));
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 (key !== "pattern" && key !== "metadata") names(item, [...keys, key]);
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` an optional gender. */
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[2] !== undefined && match[1] !== GIVEN_LIST)
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
- match[2] === undefined ? { list: match[1] } : { list: match[1], gender: match[2] },
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 placeholder of the same name draws from. 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, and the text " +
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.2",
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.