@heroiclands/package-build 23.6.1 → 23.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1718 @@
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 keys a language note states beside its words: the register of the names
16
+ * the setting gives in the language's reach, the tags the register uses, the
17
+ * forms it does not write, the notes the register covers, the letters and sound
18
+ * rules, the sound changes, the name elements, the roots, the cognates, the
19
+ * forms older than the rules and the negative particle.
20
+ *
21
+ * A leaf module, importing nothing: the note vocabulary reads these
22
+ * declarations as it loads, and `language-data.mjs` checks what the values
23
+ * mean. Every closed list here — the edges, element positions, register
24
+ * kinds, stress rules, joins, marks, onsets, harmony sets, cluster uses and
25
+ * calling-name uses — holds the labels the setting's own tables use, one word
26
+ * for each.
27
+ *
28
+ * @module
29
+ */
30
+
31
+ /** The `data` keys a language note states its sounds, names and history in. */
32
+ export const LANGUAGE_DATA_KEYS = Object.freeze([
33
+ "tongues",
34
+ "register",
35
+ "retired",
36
+ "scope",
37
+ "sounds",
38
+ "soundChanges",
39
+ "elements",
40
+ "roots",
41
+ "cognates",
42
+ "callingNames",
43
+ "older",
44
+ "negation",
45
+ "naming",
46
+ ]);
47
+
48
+ /**
49
+ * The tongue tag every language accepts without declaring it: a name given in
50
+ * the language of the pages.
51
+ */
52
+ export const RESERVED_TONGUE = "translation";
53
+
54
+ /**
55
+ * The placeholder a tongue tag closes on to declare a family of tags, one per
56
+ * tongue named after the colon: `exonym:<tongue>` declares `exonym:vylari`.
57
+ */
58
+ export const TONGUE_PLACEHOLDER = "<tongue>";
59
+
60
+ /**
61
+ * The edges of a word a sound rule names: what may open it, close it, stand
62
+ * inside it, and what counts as a syllable.
63
+ */
64
+ export const EDGE_KINDS = Object.freeze([
65
+ "openConsonant",
66
+ "openCluster",
67
+ "closeConsonant",
68
+ "closeCluster",
69
+ "innerCluster",
70
+ "vowelPair",
71
+ "glide",
72
+ "syllabic",
73
+ ]);
74
+
75
+ /** What each edge kind holds, for the reference and for a finding. */
76
+ export const EDGE_MEANINGS = Object.freeze({
77
+ openConsonant: "the single consonants that may open a word, where not every one may",
78
+ openCluster: "the clusters that may open a word",
79
+ closeConsonant: "the consonants that may close a word",
80
+ closeCluster: "the clusters that may close a word",
81
+ innerCluster: "the pairs that may stand together inside a word",
82
+ vowelPair: "the vowels that may stand together",
83
+ glide: "the glides that may follow a consonant",
84
+ syllabic: "the consonants that may carry a syllable",
85
+ });
86
+
87
+ /** The word a sound rule's `sounds` takes in place of a list: every sound. */
88
+ export const ANY_SOUND = "any";
89
+
90
+ /**
91
+ * Where in a name an element stands: `opening` opens a name or stands inside
92
+ * it, `closing` closes it, `either` does both, and `link` joins two parts and
93
+ * never begins or ends a name.
94
+ */
95
+ export const ELEMENT_POSITIONS = Object.freeze(["opening", "closing", "either", "link"]);
96
+
97
+ /**
98
+ * What a register name is, where a language sorts its names by kind, and the
99
+ * kind of name an element builds.
100
+ */
101
+ export const REGISTER_KINDS = Object.freeze(["byname", "given", "place"]);
102
+
103
+ /**
104
+ * The class of name a naming shape or edge holds for: every name, a given or a
105
+ * clan name, or a name by how it is built — `bound` a stem and an ending,
106
+ * `compound` elements alone.
107
+ */
108
+ export const NAME_CLASSES = Object.freeze(["name", "given", "clan", "bound", "compound"]);
109
+
110
+ /** The class of name a naming edge holds for where it states none. */
111
+ export const ANY_NAME = "name";
112
+
113
+ /**
114
+ * What a place name takes first from what it is held from: a name-stem, a
115
+ * god's name, a founder's given name, or one of the listed elements.
116
+ */
117
+ export const HELD_TAKES = Object.freeze(["stem", "godName", "givenName", "element"]);
118
+
119
+ /** The `takes` values that are a name a place name shapes. */
120
+ export const HELD_NAMES = Object.freeze(["godName", "givenName"]);
121
+
122
+ /** How a name is clipped where a place name takes it. */
123
+ export const HELD_CLIPS = Object.freeze(["first"]);
124
+
125
+ /** How often a group of clusters stands inside a word. */
126
+ export const CLUSTER_USES = Object.freeze(["common", "sparing"]);
127
+
128
+ /** Where a language's stress falls. */
129
+ export const STRESS_RULES = Object.freeze([
130
+ "first syllable",
131
+ "penultimate or final",
132
+ "by weight",
133
+ "as marked",
134
+ ]);
135
+
136
+ /** Which sounds meet where two parts of a word join. */
137
+ export const JOIN_MEETINGS = Object.freeze(["C+C", "V+C", "V+V"]);
138
+
139
+ /**
140
+ * What happens where they meet: `merge` writes one, `insert` sets the join's
141
+ * letters between, `drop` loses the first, `double` doubles the consonant.
142
+ */
143
+ export const JOIN_RULES = Object.freeze(["merge", "insert", "drop", "double"]);
144
+
145
+ /** What a written mark says of its letter. */
146
+ export const MARK_KINDS = Object.freeze(["stress", "length"]);
147
+
148
+ /** How many of a mark a word may carry: `one` at most, or `any` number. */
149
+ export const MARK_LIMITS = Object.freeze(["one", "any"]);
150
+
151
+ /** What a syllable may open on, where the rule is stated. */
152
+ export const ONSET_RULES = Object.freeze(["single"]);
153
+
154
+ /** Which harmony a neutral vowel counts as. */
155
+ export const HARMONY_SETS = Object.freeze(["back", "front"]);
156
+
157
+ /** Whose name a calling-name closing usually makes. */
158
+ export const CALLING_USES = Object.freeze(["man", "woman", "either"]);
159
+
160
+ /** A list of strings, as an inner key's `entries`. */
161
+ const STRINGS = Object.freeze({ kind: "string", shape: "a string" });
162
+
163
+ /**
164
+ * The keys of one `data.tongues` entry.
165
+ *
166
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
167
+ */
168
+ export const TONGUE_FIELDS = Object.freeze([
169
+ Object.freeze({
170
+ name: "tag",
171
+ kind: "string",
172
+ required: true,
173
+ shape: "a tag such as `dunhari`, or `exonym:<tongue>`",
174
+ describe:
175
+ "The tag a register row's `tongue` names: lowercase letters and digits, or a tag " +
176
+ "closing on `:<tongue>`, which declares one tag per tongue named after the colon.",
177
+ }),
178
+ Object.freeze({
179
+ name: "meaning",
180
+ kind: "string",
181
+ required: true,
182
+ shape: "a string",
183
+ describe: "What a name of this tongue is.",
184
+ }),
185
+ Object.freeze({
186
+ name: "language",
187
+ kind: "address",
188
+ shape: "a language skill Address",
189
+ describe: "The language note the tag names, where it is one.",
190
+ }),
191
+ ]);
192
+
193
+ /**
194
+ * The keys of one `data.register` entry.
195
+ *
196
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
197
+ */
198
+ export const REGISTER_FIELDS = Object.freeze([
199
+ Object.freeze({
200
+ name: "name",
201
+ kind: "string",
202
+ required: true,
203
+ shape: "a name as the setting writes it",
204
+ describe: "A name the setting gives, as it is written.",
205
+ }),
206
+ Object.freeze({
207
+ name: "tongue",
208
+ kind: "string",
209
+ shape: "a tag of `data.tongues`, or `translation`",
210
+ describe:
211
+ "The tongue the name is in, as a tag `data.tongues` declares, or `translation`. A row " +
212
+ "states a `tongue`, a `kind`, or both.",
213
+ }),
214
+ Object.freeze({
215
+ name: "kind",
216
+ kind: "string",
217
+ allowed: REGISTER_KINDS,
218
+ shape: `one of ${REGISTER_KINDS.join(", ")}`,
219
+ describe: "What kind of name it is, in a language that sorts its names by kind.",
220
+ }),
221
+ Object.freeze({
222
+ name: "built",
223
+ kind: "list",
224
+ shape: "a list of pieces",
225
+ entries: STRINGS,
226
+ describe: "The pieces the name is built from, in order, as a word's `built` writes them.",
227
+ }),
228
+ Object.freeze({
229
+ name: "gloss",
230
+ kind: "string",
231
+ shape: "a string",
232
+ describe: "What the name means, where the pages give its sense.",
233
+ }),
234
+ Object.freeze({
235
+ name: "why",
236
+ kind: "string",
237
+ shape: "a string",
238
+ describe: "Why the name stands outside the pattern the language's names are built by.",
239
+ }),
240
+ ]);
241
+
242
+ /**
243
+ * The keys of one `data.retired` entry.
244
+ *
245
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
246
+ */
247
+ export const RETIRED_FIELDS = Object.freeze([
248
+ Object.freeze({
249
+ name: "form",
250
+ kind: "string",
251
+ required: true,
252
+ shape: "a string",
253
+ describe: "The form the setting does not write.",
254
+ }),
255
+ Object.freeze({
256
+ name: "instead",
257
+ kind: "string-or-list",
258
+ shape: "a string, or a list of them",
259
+ entries: STRINGS,
260
+ describe: "What is written in its place, one form or several; absent where nothing is.",
261
+ }),
262
+ Object.freeze({
263
+ name: "why",
264
+ kind: "string",
265
+ required: true,
266
+ shape: "a string",
267
+ describe: "Why the setting does not write it.",
268
+ }),
269
+ ]);
270
+
271
+ /**
272
+ * The keys of a language's `data.scope`: the notes its register covers. A note
273
+ * is in scope when any one of them catches it and `skip` does not name it.
274
+ *
275
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
276
+ */
277
+ export const SCOPE_FIELDS = Object.freeze([
278
+ Object.freeze({
279
+ name: "folders",
280
+ kind: "list",
281
+ shape: "a list of folder Addresses",
282
+ entries: Object.freeze({ kind: "address", shape: "a folder Address" }),
283
+ describe: "Every note whose `data.packFolder` is one of these folders.",
284
+ }),
285
+ Object.freeze({
286
+ name: "cultures",
287
+ kind: "list",
288
+ shape: "a list of culture Addresses",
289
+ entries: Object.freeze({ kind: "address", shape: "a culture Address" }),
290
+ describe: "Every note whose `data.culture` is one of these cultures.",
291
+ }),
292
+ Object.freeze({
293
+ name: "lore",
294
+ kind: "list",
295
+ shape: "a list of lore Addresses",
296
+ entries: Object.freeze({ kind: "address", shape: "a lore Address" }),
297
+ describe: "Every note whose `data.lore` names one of these notes.",
298
+ }),
299
+ Object.freeze({
300
+ name: "parents",
301
+ kind: "list",
302
+ shape: "a list of Addresses",
303
+ entries: Object.freeze({ kind: "address", shape: "a place or affiliation Address" }),
304
+ describe: "Every note whose `data.parents` names one of these notes.",
305
+ }),
306
+ Object.freeze({
307
+ name: "addresses",
308
+ kind: "list",
309
+ shape: "a list of Addresses",
310
+ entries: Object.freeze({ kind: "address", shape: "an Address" }),
311
+ describe: "These notes themselves.",
312
+ }),
313
+ Object.freeze({
314
+ name: "paths",
315
+ kind: "list",
316
+ shape: "a list of content paths",
317
+ entries: Object.freeze({ kind: "string", shape: "a path such as `Regions/Aelwyth/`" }),
318
+ describe: "Every note whose file sits below one of these paths of the content tree.",
319
+ }),
320
+ Object.freeze({
321
+ name: "types",
322
+ kind: "list",
323
+ shape: "a list of note types",
324
+ entries: Object.freeze({ kind: "string", shape: "a note type" }),
325
+ describe:
326
+ "The note types the register covers; where stated, a note of another type is out " +
327
+ "of scope whatever else catches it.",
328
+ }),
329
+ Object.freeze({
330
+ name: "skip",
331
+ kind: "list",
332
+ shape: "a list of Addresses",
333
+ entries: Object.freeze({ kind: "address", shape: "an Address" }),
334
+ describe: "Notes out of scope whatever else catches them.",
335
+ }),
336
+ ]);
337
+
338
+ /**
339
+ * The keys of one `data.sounds.letters` entry.
340
+ *
341
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
342
+ */
343
+ export const LETTER_FIELDS = Object.freeze([
344
+ Object.freeze({
345
+ name: "letters",
346
+ kind: "list",
347
+ required: true,
348
+ shape: "a list of letters",
349
+ entries: STRINGS,
350
+ describe: "The letters or digraphs the row describes, as written.",
351
+ }),
352
+ Object.freeze({
353
+ name: "kind",
354
+ kind: "string",
355
+ required: true,
356
+ shape: "a string, such as `consonant` or `long vowel`",
357
+ describe: "What kind of sound they write.",
358
+ }),
359
+ Object.freeze({
360
+ name: "like",
361
+ kind: "string",
362
+ shape: "a string",
363
+ describe: "What they sound like; absent where the row states only their kind and place.",
364
+ }),
365
+ Object.freeze({
366
+ name: "place",
367
+ kind: "string",
368
+ shape: "a string, such as `lips` or `tongue-tip`",
369
+ describe: "Where in the mouth they are made.",
370
+ }),
371
+ Object.freeze({
372
+ name: "tongue",
373
+ kind: "string-or-list",
374
+ shape: "a tag, or a list of them",
375
+ entries: STRINGS,
376
+ describe:
377
+ "The tongues that write them, as tags `data.tongues` declares; every one where absent.",
378
+ }),
379
+ ]);
380
+
381
+ /**
382
+ * The keys of one `data.sounds.edges` entry.
383
+ *
384
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
385
+ */
386
+ export const EDGE_FIELDS = Object.freeze([
387
+ Object.freeze({
388
+ name: "edge",
389
+ kind: "string",
390
+ required: true,
391
+ allowed: EDGE_KINDS,
392
+ shape: `one of ${EDGE_KINDS.join(", ")}`,
393
+ describe: "Which edge of a word the sounds stand at.",
394
+ }),
395
+ Object.freeze({
396
+ name: "sounds",
397
+ kind: "string-or-list",
398
+ required: true,
399
+ allowed: Object.freeze([ANY_SOUND]),
400
+ shape: `a list of sounds, or \`${ANY_SOUND}\``,
401
+ entries: STRINGS,
402
+ describe: `The sounds, as written; \`${ANY_SOUND}\` for every one, and an empty list for none.`,
403
+ }),
404
+ ]);
405
+
406
+ /**
407
+ * The keys of one `data.sounds.clusters.groups` entry: clusters of one kind,
408
+ * and how often they stand.
409
+ *
410
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
411
+ */
412
+ export const CLUSTER_GROUP_FIELDS = Object.freeze([
413
+ Object.freeze({
414
+ name: "what",
415
+ kind: "string",
416
+ required: true,
417
+ shape: "a string, such as `a nasal and a fricative`",
418
+ describe: "What the clusters of the group are made of.",
419
+ }),
420
+ Object.freeze({
421
+ name: "clusters",
422
+ kind: "list",
423
+ required: true,
424
+ shape: "a list of clusters",
425
+ entries: STRINGS,
426
+ describe: "The clusters, as written; a digraph counts as one consonant.",
427
+ }),
428
+ Object.freeze({
429
+ name: "use",
430
+ kind: "string",
431
+ required: true,
432
+ allowed: CLUSTER_USES,
433
+ shape: `one of ${CLUSTER_USES.join(", ")}`,
434
+ describe: "How often the group's clusters stand inside a word.",
435
+ }),
436
+ ]);
437
+
438
+ /**
439
+ * The keys of a language's `data.sounds.clusters`.
440
+ *
441
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
442
+ */
443
+ export const CLUSTER_FIELDS = Object.freeze([
444
+ Object.freeze({
445
+ name: "max",
446
+ kind: "integer",
447
+ required: true,
448
+ shape: "a whole number",
449
+ describe: "The most consonants that stand together inside a word.",
450
+ }),
451
+ Object.freeze({
452
+ name: "groups",
453
+ kind: "list",
454
+ shape: "list of `{ what, clusters, use }`",
455
+ entries: Object.freeze({
456
+ kind: "map",
457
+ shape: "a map — `{ what, clusters, use }`",
458
+ fields: CLUSTER_GROUP_FIELDS,
459
+ }),
460
+ describe:
461
+ "The clusters that may stand inside a word, in groups; a language stating them " +
462
+ "states no `innerCluster` edge.",
463
+ }),
464
+ ]);
465
+
466
+ /**
467
+ * The keys of one `data.sounds.readPast` entry.
468
+ *
469
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
470
+ */
471
+ export const READ_PAST_FIELDS = Object.freeze([
472
+ Object.freeze({
473
+ name: "written",
474
+ kind: "string",
475
+ required: true,
476
+ shape: "a string",
477
+ describe: "A mark or spelling the pages write.",
478
+ }),
479
+ Object.freeze({
480
+ name: "readAs",
481
+ kind: "string",
482
+ shape: "a string",
483
+ describe: "What a word is read with in its place; absent where it is read as nothing.",
484
+ }),
485
+ Object.freeze({
486
+ name: "what",
487
+ kind: "string",
488
+ required: true,
489
+ shape: "a string",
490
+ describe: "What the mark or spelling is.",
491
+ }),
492
+ ]);
493
+
494
+ /** A list of sounds, or `any` for every one. */
495
+ const SOUNDS_OR_ANY = Object.freeze({
496
+ kind: "string-or-list",
497
+ allowed: Object.freeze([ANY_SOUND]),
498
+ shape: `a list of sounds, or \`${ANY_SOUND}\``,
499
+ entries: STRINGS,
500
+ });
501
+
502
+ /**
503
+ * The keys of one `data.sounds.finals` entry: a syllable final, for a
504
+ * language whose syllables are parsed by them.
505
+ *
506
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
507
+ */
508
+ export const FINAL_FIELDS = Object.freeze([
509
+ Object.freeze({
510
+ name: "final",
511
+ kind: "string",
512
+ required: true,
513
+ shape: "a final as written, such as `ang`",
514
+ describe: "The final.",
515
+ }),
516
+ Object.freeze({
517
+ name: "like",
518
+ kind: "string",
519
+ required: true,
520
+ shape: "a string",
521
+ describe: "What it sounds like.",
522
+ }),
523
+ ]);
524
+
525
+ /**
526
+ * The keys of one `data.sounds.joining` entry: how two parts are written
527
+ * where the first closes and the second opens on the given sounds.
528
+ *
529
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
530
+ */
531
+ export const JOINING_FIELDS = Object.freeze([
532
+ Object.freeze({
533
+ name: "first",
534
+ kind: "list",
535
+ required: true,
536
+ shape: "a list of sounds",
537
+ entries: STRINGS,
538
+ describe: "The sounds the first part closes on.",
539
+ }),
540
+ Object.freeze({
541
+ name: "second",
542
+ kind: "list",
543
+ required: true,
544
+ shape: "a list of sounds",
545
+ entries: STRINGS,
546
+ describe: "The sounds the second part opens on.",
547
+ }),
548
+ Object.freeze({
549
+ name: "written",
550
+ kind: "string",
551
+ required: true,
552
+ shape: "a string, such as `â` or `y+`",
553
+ describe:
554
+ "What is written where they meet: the sound that stands for both, or a join " +
555
+ "closing on `+`, which keeps both and sets it between.",
556
+ }),
557
+ Object.freeze({
558
+ name: "example",
559
+ kind: "string",
560
+ shape: "a string",
561
+ describe: "A word joined so.",
562
+ }),
563
+ ]);
564
+
565
+ /**
566
+ * The keys of one `data.sounds.joins` entry: what happens where two parts of a
567
+ * word meet.
568
+ *
569
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
570
+ */
571
+ export const JOIN_FIELDS = Object.freeze([
572
+ Object.freeze({
573
+ name: "meeting",
574
+ kind: "string",
575
+ required: true,
576
+ allowed: JOIN_MEETINGS,
577
+ shape: `one of ${JOIN_MEETINGS.join(", ")}`,
578
+ describe:
579
+ "Which sounds meet: a consonant `C` or a vowel `V` closing the first part, then opening the second.",
580
+ }),
581
+ Object.freeze({
582
+ name: "rule",
583
+ kind: "string",
584
+ required: true,
585
+ allowed: JOIN_RULES,
586
+ shape: `one of ${JOIN_RULES.join(", ")}`,
587
+ describe: "What happens where they meet.",
588
+ }),
589
+ Object.freeze({
590
+ name: "letters",
591
+ kind: "list",
592
+ shape: "a list of letters",
593
+ entries: STRINGS,
594
+ describe:
595
+ "The letters the rule takes: what `insert` sets between, or what `double` doubles.",
596
+ }),
597
+ Object.freeze({
598
+ name: "where",
599
+ kind: "string",
600
+ shape: "a string, such as `between stems`",
601
+ describe: "Where the rule holds; everywhere where absent.",
602
+ }),
603
+ ]);
604
+
605
+ /**
606
+ * The keys of one `data.sounds.marks` entry: a mark the language writes on a
607
+ * letter.
608
+ *
609
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
610
+ */
611
+ export const MARK_FIELDS = Object.freeze([
612
+ Object.freeze({
613
+ name: "mark",
614
+ kind: "string",
615
+ required: true,
616
+ shape: "a string, such as `acute`",
617
+ describe: "The mark's name.",
618
+ }),
619
+ Object.freeze({
620
+ name: "letters",
621
+ kind: "list",
622
+ required: true,
623
+ shape: "a list of letters",
624
+ entries: STRINGS,
625
+ describe: "The letters that carry it.",
626
+ }),
627
+ Object.freeze({
628
+ name: "kind",
629
+ kind: "string",
630
+ required: true,
631
+ allowed: MARK_KINDS,
632
+ shape: `one of ${MARK_KINDS.join(", ")}`,
633
+ describe: "What the mark says of its letter.",
634
+ }),
635
+ Object.freeze({
636
+ name: "limit",
637
+ kind: "string",
638
+ required: true,
639
+ allowed: MARK_LIMITS,
640
+ shape: `one of ${MARK_LIMITS.join(", ")}`,
641
+ describe: "How many a word may carry: `one` at most, or `any`.",
642
+ }),
643
+ ]);
644
+
645
+ /**
646
+ * The keys of a language's `data.sounds.syllables.harmony`.
647
+ *
648
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
649
+ */
650
+ export const HARMONY_FIELDS = Object.freeze([
651
+ Object.freeze({
652
+ name: "back",
653
+ kind: "list",
654
+ required: true,
655
+ shape: "a list of vowels",
656
+ entries: STRINGS,
657
+ describe: "The back vowels.",
658
+ }),
659
+ Object.freeze({
660
+ name: "front",
661
+ kind: "list",
662
+ required: true,
663
+ shape: "a list of vowels",
664
+ entries: STRINGS,
665
+ describe: "The front vowels.",
666
+ }),
667
+ Object.freeze({
668
+ name: "neutral",
669
+ kind: "list",
670
+ shape: "a list of vowels",
671
+ entries: STRINGS,
672
+ describe: "The vowels of neither set.",
673
+ }),
674
+ Object.freeze({
675
+ name: "neutralAs",
676
+ kind: "string",
677
+ allowed: HARMONY_SETS,
678
+ shape: `one of ${HARMONY_SETS.join(", ")}`,
679
+ describe: "The set a word of neutral vowels alone counts as.",
680
+ }),
681
+ Object.freeze({
682
+ name: "partners",
683
+ kind: "map",
684
+ shape: "a map of vowels to front vowels",
685
+ values: Object.freeze({ kind: "string", shape: "a front vowel" }),
686
+ describe:
687
+ "The front vowel each vowel turns to in an affix's front form, keyed by the vowel; " +
688
+ "every key is a vowel of the harmony and every value one of its `front` vowels.",
689
+ }),
690
+ ]);
691
+
692
+ /**
693
+ * The keys of a language's `data.sounds.syllables`: what a syllable may open
694
+ * and close on, and what a word may.
695
+ *
696
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
697
+ */
698
+ export const SYLLABLE_FIELDS = Object.freeze([
699
+ Object.freeze({
700
+ name: "onsets",
701
+ kind: "string",
702
+ allowed: ONSET_RULES,
703
+ shape: `one of ${ONSET_RULES.join(", ")}`,
704
+ describe: "What a syllable opens on: `single`, a single consonant.",
705
+ }),
706
+ Object.freeze({
707
+ name: "codas",
708
+ ...SOUNDS_OR_ANY,
709
+ describe: "The consonants that may close a syllable; `any` for every one.",
710
+ }),
711
+ Object.freeze({
712
+ name: "finals",
713
+ ...SOUNDS_OR_ANY,
714
+ describe: "The consonants that may close a word; `any` for every one.",
715
+ }),
716
+ Object.freeze({
717
+ name: "doubled",
718
+ kind: "list",
719
+ shape: "a list of letters",
720
+ entries: STRINGS,
721
+ describe: "The doubled consonants a word may hold; none where empty.",
722
+ }),
723
+ Object.freeze({
724
+ name: "vowelPairs",
725
+ kind: "list",
726
+ shape: "a list of vowel pairs",
727
+ entries: STRINGS,
728
+ describe: "The vowels that may stand together; none where empty.",
729
+ }),
730
+ Object.freeze({
731
+ name: "initialVowel",
732
+ kind: "boolean",
733
+ shape: "`true` or `false`",
734
+ describe: "Whether a word may open on a vowel.",
735
+ }),
736
+ Object.freeze({
737
+ name: "finalVowel",
738
+ kind: "boolean",
739
+ shape: "`true` or `false`",
740
+ describe: "Whether a word may close on a vowel; `true` where unstated.",
741
+ }),
742
+ Object.freeze({
743
+ name: "neverInitial",
744
+ kind: "list",
745
+ shape: "a list of letters",
746
+ entries: STRINGS,
747
+ describe: "The letters that never open a word.",
748
+ }),
749
+ Object.freeze({
750
+ name: "signature",
751
+ kind: "list",
752
+ shape: "a list of letters",
753
+ entries: STRINGS,
754
+ describe: "The letters that mark a word as the language's own.",
755
+ }),
756
+ Object.freeze({
757
+ name: "harmony",
758
+ kind: "map",
759
+ shape: "a map — `{ back, front, neutral?, neutralAs?, partners? }`",
760
+ fields: HARMONY_FIELDS,
761
+ describe: "The vowel harmony a word keeps.",
762
+ }),
763
+ ]);
764
+
765
+ /**
766
+ * The keys of one `data.sounds.skeleton` entry: the vowel set between a
767
+ * root's second and third consonants.
768
+ *
769
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
770
+ */
771
+ export const SKELETON_FIELDS = Object.freeze([
772
+ Object.freeze({
773
+ name: "second",
774
+ ...SOUNDS_OR_ANY,
775
+ required: true,
776
+ describe: "The second consonants of a root the row holds for; `any` for every other.",
777
+ }),
778
+ Object.freeze({
779
+ name: "between",
780
+ kind: "string",
781
+ shape: "a vowel, such as `a`",
782
+ describe: "The vowel set between the second and third consonants; nothing where absent.",
783
+ }),
784
+ ]);
785
+
786
+ /**
787
+ * The keys of one `data.sounds.frames` entry: a pattern a root is set in.
788
+ *
789
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
790
+ */
791
+ export const FRAME_FIELDS = Object.freeze([
792
+ Object.freeze({
793
+ name: "frame",
794
+ kind: "string",
795
+ required: true,
796
+ shape: "a string, such as `bare`",
797
+ describe: "The frame's name, which a word's `frame` names.",
798
+ }),
799
+ Object.freeze({
800
+ name: "shape",
801
+ kind: "string",
802
+ required: true,
803
+ shape: "a pattern, such as `K1aK2K3`",
804
+ describe: "The pattern, the root's consonants numbered `K1`, `K2`, `K3`.",
805
+ }),
806
+ Object.freeze({
807
+ name: "class",
808
+ kind: "list",
809
+ required: true,
810
+ shape: "a list of word class bases",
811
+ entries: STRINGS,
812
+ describe: "The classes of word the frame makes, each a base of the master word classes.",
813
+ }),
814
+ Object.freeze({
815
+ name: "makes",
816
+ kind: "string",
817
+ required: true,
818
+ shape: "a string",
819
+ describe: "What the frame makes of a root.",
820
+ }),
821
+ Object.freeze({
822
+ name: "examples",
823
+ kind: "map",
824
+ shape: "a map of roots to forms",
825
+ values: Object.freeze({ kind: "string", shape: "a form, bare" }),
826
+ describe: "The form a root takes in the frame, keyed by the root.",
827
+ }),
828
+ ]);
829
+
830
+ /**
831
+ * The keys of one `data.sounds.mutations` entry: a sound and what it becomes
832
+ * where a named mutation holds.
833
+ *
834
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
835
+ */
836
+ export const MUTATION_FIELDS = Object.freeze([
837
+ Object.freeze({
838
+ name: "mutation",
839
+ kind: "string",
840
+ required: true,
841
+ shape: "a string, such as `wearing`",
842
+ describe: "The mutation's name; its rows share it, and `data.negation` names it.",
843
+ }),
844
+ Object.freeze({
845
+ name: "sound",
846
+ kind: "string",
847
+ required: true,
848
+ shape: "a sound as written, such as `p`",
849
+ describe: "The sound as it stands where the mutation does not hold; once per mutation.",
850
+ }),
851
+ Object.freeze({
852
+ name: "becomes",
853
+ kind: "string",
854
+ required: true,
855
+ shape: "a sound as written, such as `v`",
856
+ describe: "What the sound becomes where the mutation holds.",
857
+ }),
858
+ Object.freeze({
859
+ name: "example",
860
+ kind: "string",
861
+ shape: "a string",
862
+ describe: "A word that shows it.",
863
+ }),
864
+ ]);
865
+
866
+ /**
867
+ * The keys of a language's `data.sounds`.
868
+ *
869
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
870
+ */
871
+ export const SOUND_FIELDS = Object.freeze([
872
+ Object.freeze({
873
+ name: "letters",
874
+ kind: "list",
875
+ shape: "list of `{ letters, kind, like, tongue? }`",
876
+ entries: Object.freeze({
877
+ kind: "map",
878
+ shape: "a map — `{ letters, kind, like, tongue? }`",
879
+ fields: LETTER_FIELDS,
880
+ }),
881
+ describe: "The letters the language writes, and the sounds they stand for.",
882
+ }),
883
+ Object.freeze({
884
+ name: "edges",
885
+ kind: "list",
886
+ shape: "list of `{ edge, sounds }`",
887
+ entries: Object.freeze({
888
+ kind: "map",
889
+ shape: "a map — `{ edge, sounds }`",
890
+ fields: EDGE_FIELDS,
891
+ }),
892
+ describe: "The sounds that may stand at each edge of a word, one entry per edge.",
893
+ }),
894
+ Object.freeze({
895
+ name: "clusters",
896
+ kind: "map",
897
+ shape: "a map — `{ max, groups? }`",
898
+ fields: CLUSTER_FIELDS,
899
+ describe: "How many consonants may stand together.",
900
+ }),
901
+ Object.freeze({
902
+ name: "stress",
903
+ kind: "string",
904
+ allowed: STRESS_RULES,
905
+ shape: `one of ${STRESS_RULES.join(", ")}`,
906
+ describe: "Where a word's stress falls.",
907
+ }),
908
+ Object.freeze({
909
+ name: "finals",
910
+ kind: "list",
911
+ shape: "list of `{ final, like }`",
912
+ entries: Object.freeze({
913
+ kind: "map",
914
+ shape: "a map — `{ final, like }`",
915
+ fields: FINAL_FIELDS,
916
+ }),
917
+ describe: "The syllable finals, for a language whose syllables are parsed by them.",
918
+ }),
919
+ Object.freeze({
920
+ name: "syllables",
921
+ kind: "map",
922
+ shape: "a map of syllable rules",
923
+ fields: SYLLABLE_FIELDS,
924
+ describe: "What a syllable and a word may open and close on.",
925
+ }),
926
+ Object.freeze({
927
+ name: "joining",
928
+ kind: "list",
929
+ shape: "list of `{ first, second, written, example? }`",
930
+ entries: Object.freeze({
931
+ kind: "map",
932
+ shape: "a map — `{ first, second, written, example? }`",
933
+ fields: JOINING_FIELDS,
934
+ }),
935
+ describe:
936
+ "How two parts are written where the first closes and the second opens on the given sounds.",
937
+ }),
938
+ Object.freeze({
939
+ name: "joins",
940
+ kind: "list",
941
+ shape: "list of `{ meeting, rule, letters?, where? }`",
942
+ entries: Object.freeze({
943
+ kind: "map",
944
+ shape: "a map — `{ meeting, rule, letters?, where? }`",
945
+ fields: JOIN_FIELDS,
946
+ }),
947
+ describe: "What happens where two parts of a word meet, one entry per meeting.",
948
+ }),
949
+ Object.freeze({
950
+ name: "marks",
951
+ kind: "list",
952
+ shape: "list of `{ mark, letters, kind, limit }`",
953
+ entries: Object.freeze({
954
+ kind: "map",
955
+ shape: "a map — `{ mark, letters, kind, limit }`",
956
+ fields: MARK_FIELDS,
957
+ }),
958
+ describe: "The marks the language writes on its letters.",
959
+ }),
960
+ Object.freeze({
961
+ name: "skeleton",
962
+ kind: "list",
963
+ shape: "list of `{ second, between? }`",
964
+ entries: Object.freeze({
965
+ kind: "map",
966
+ shape: "a map — `{ second, between? }`",
967
+ fields: SKELETON_FIELDS,
968
+ }),
969
+ describe: "The vowel set between a root's second and third consonants.",
970
+ }),
971
+ Object.freeze({
972
+ name: "frames",
973
+ kind: "list",
974
+ shape: "list of `{ frame, shape, class, makes, examples? }`",
975
+ entries: Object.freeze({
976
+ kind: "map",
977
+ shape: "a map — `{ frame, shape, class, makes, examples? }`",
978
+ fields: FRAME_FIELDS,
979
+ }),
980
+ describe: "The patterns a root is set in.",
981
+ }),
982
+ Object.freeze({
983
+ name: "readPast",
984
+ kind: "list",
985
+ shape: "list of `{ written, readAs?, what }`",
986
+ entries: Object.freeze({
987
+ kind: "map",
988
+ shape: "a map — `{ written, readAs?, what }`",
989
+ fields: READ_PAST_FIELDS,
990
+ }),
991
+ describe: "Marks and spellings a word is read through before the sound rules hold it.",
992
+ }),
993
+ Object.freeze({
994
+ name: "mutations",
995
+ kind: "list",
996
+ shape: "list of `{ mutation, sound, becomes, example? }`",
997
+ entries: Object.freeze({
998
+ kind: "map",
999
+ shape: "a map — `{ mutation, sound, becomes, example? }`",
1000
+ fields: MUTATION_FIELDS,
1001
+ }),
1002
+ describe:
1003
+ "The sounds a word changes where a named mutation holds, one entry per sound, such " +
1004
+ "as a stop worn to a fricative or a consonant voiced inside a compound.",
1005
+ }),
1006
+ ]);
1007
+
1008
+ /**
1009
+ * The keys of one `data.soundChanges` entry.
1010
+ *
1011
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1012
+ */
1013
+ export const SOUND_CHANGE_STEP_FIELDS = Object.freeze([
1014
+ Object.freeze({
1015
+ name: "step",
1016
+ kind: "string",
1017
+ required: true,
1018
+ shape: 'a string, such as `A1` or `"1"`',
1019
+ describe: "The step's label, in the order the changes apply.",
1020
+ }),
1021
+ Object.freeze({
1022
+ name: "change",
1023
+ kind: "string",
1024
+ required: true,
1025
+ shape: "a change in arrow notation, such as `` `ph` > `f`; `kh` > `h` ``",
1026
+ describe:
1027
+ "The change, in arrow notation: one or more `` `sources` > `targets` `` rules " +
1028
+ "separated by `;`, or the operation a step names in words.",
1029
+ }),
1030
+ Object.freeze({
1031
+ name: "where",
1032
+ kind: "string",
1033
+ shape: "a string, such as `word end`",
1034
+ describe: "Where the change holds; everywhere where absent.",
1035
+ }),
1036
+ ]);
1037
+
1038
+ /**
1039
+ * The keys of one `data.soundChanges` set: an ordered run of steps, named, and
1040
+ * where they lead from and to.
1041
+ *
1042
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1043
+ */
1044
+ export const SOUND_CHANGE_FIELDS = Object.freeze([
1045
+ Object.freeze({
1046
+ name: "name",
1047
+ kind: "string",
1048
+ required: true,
1049
+ shape: "a string, such as `From Helonic to Byzarian`",
1050
+ describe: "The set's name, which no other set of the note shares.",
1051
+ }),
1052
+ Object.freeze({
1053
+ name: "from",
1054
+ kind: "address",
1055
+ shape: "a language skill Address",
1056
+ describe: "The language whose words the steps start from.",
1057
+ }),
1058
+ Object.freeze({
1059
+ name: "to",
1060
+ kind: "address",
1061
+ shape: "a language skill Address",
1062
+ describe: "The language whose words the steps make.",
1063
+ }),
1064
+ Object.freeze({
1065
+ name: "takes",
1066
+ kind: "string",
1067
+ shape: "a case, such as `accusative`",
1068
+ describe:
1069
+ "The form of the older word the steps start from; its citation form where absent.",
1070
+ }),
1071
+ Object.freeze({
1072
+ name: "steps",
1073
+ kind: "list",
1074
+ required: true,
1075
+ shape: "list of `{ step, change, where? }`",
1076
+ entries: Object.freeze({
1077
+ kind: "map",
1078
+ shape: "a map — `{ step, change, where? }`",
1079
+ fields: SOUND_CHANGE_STEP_FIELDS,
1080
+ }),
1081
+ describe: "The steps, in the order they apply; no step label stands twice in a set.",
1082
+ }),
1083
+ ]);
1084
+
1085
+ /**
1086
+ * The keys of one `data.elements` entry.
1087
+ *
1088
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1089
+ */
1090
+ export const ELEMENT_FIELDS = Object.freeze([
1091
+ Object.freeze({
1092
+ name: "form",
1093
+ kind: "string",
1094
+ required: true,
1095
+ shape: "the element as written, such as `arn-`",
1096
+ describe: "The element a name is built from.",
1097
+ }),
1098
+ Object.freeze({
1099
+ name: "variants",
1100
+ kind: "list",
1101
+ shape: "a list of forms",
1102
+ entries: STRINGS,
1103
+ describe: "The element's other forms, such as `aldr-` beside `ald-`.",
1104
+ }),
1105
+ Object.freeze({
1106
+ name: "position",
1107
+ kind: "string",
1108
+ allowed: ELEMENT_POSITIONS,
1109
+ shape: `one of ${ELEMENT_POSITIONS.join(", ")}`,
1110
+ describe: "Where in a name the element stands.",
1111
+ }),
1112
+ Object.freeze({
1113
+ name: "kind",
1114
+ kind: "string",
1115
+ allowed: REGISTER_KINDS,
1116
+ shape: `one of ${REGISTER_KINDS.join(", ")}`,
1117
+ describe:
1118
+ "The kind of name the element builds, in a language that keeps one set of elements " +
1119
+ "per kind; every kind where absent.",
1120
+ }),
1121
+ Object.freeze({
1122
+ name: "sense",
1123
+ kind: "string",
1124
+ required: true,
1125
+ shape: "a string",
1126
+ describe: "What the element means.",
1127
+ }),
1128
+ Object.freeze({
1129
+ name: "examples",
1130
+ kind: "list",
1131
+ shape: "a list of names",
1132
+ entries: STRINGS,
1133
+ describe: "The names the element is read from.",
1134
+ }),
1135
+ Object.freeze({
1136
+ name: "word",
1137
+ kind: "string",
1138
+ shape: "a form of the language's words",
1139
+ describe:
1140
+ "The word the element is the stem of, a form of `data.words` of the language or an " +
1141
+ "ancestor.",
1142
+ }),
1143
+ Object.freeze({
1144
+ name: "genitive",
1145
+ kind: "string",
1146
+ shape: "the element as written, such as `-borgar`",
1147
+ describe: "The element in the genitive, where a name governing another word takes it.",
1148
+ }),
1149
+ ]);
1150
+
1151
+ /**
1152
+ * The keys of one `data.roots` entry.
1153
+ *
1154
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1155
+ */
1156
+ export const ROOT_FIELDS = Object.freeze([
1157
+ Object.freeze({
1158
+ name: "root",
1159
+ kind: "string",
1160
+ required: true,
1161
+ shape: "a consonantal skeleton, such as `d-l-k`",
1162
+ describe: "The root.",
1163
+ }),
1164
+ Object.freeze({
1165
+ name: "sense",
1166
+ kind: "string",
1167
+ required: true,
1168
+ shape: "a string",
1169
+ describe: "What the root means.",
1170
+ }),
1171
+ Object.freeze({
1172
+ name: "field",
1173
+ kind: "string",
1174
+ shape: "a string, such as `stone`",
1175
+ describe: "The field the root belongs to.",
1176
+ }),
1177
+ ]);
1178
+
1179
+ /**
1180
+ * The keys of one `data.cognates` entry.
1181
+ *
1182
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1183
+ */
1184
+ export const COGNATE_FIELDS = Object.freeze([
1185
+ Object.freeze({
1186
+ name: "forms",
1187
+ kind: "map",
1188
+ required: true,
1189
+ shape: "a map of language Addresses to forms",
1190
+ values: Object.freeze({ kind: "string", shape: "a form, bare" }),
1191
+ describe: "The form the set takes in each language, keyed by the language note's Address.",
1192
+ }),
1193
+ Object.freeze({
1194
+ name: "proto",
1195
+ kind: "string",
1196
+ shape: "a form, bare",
1197
+ describe:
1198
+ "The reconstructed form the set descends from, in no attested language; printed with " +
1199
+ "an asterisk.",
1200
+ }),
1201
+ Object.freeze({
1202
+ name: "gloss",
1203
+ kind: "string",
1204
+ required: true,
1205
+ shape: "a string",
1206
+ describe: "What the forms mean.",
1207
+ }),
1208
+ ]);
1209
+
1210
+ /**
1211
+ * The keys of one `data.callingNames.closings` entry: an ending, or the first
1212
+ * syllable doubled, that a calling name closes on.
1213
+ *
1214
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1215
+ */
1216
+ export const CALLING_CLOSING_FIELDS = Object.freeze([
1217
+ Object.freeze({
1218
+ name: "form",
1219
+ kind: "string",
1220
+ shape: "an ending, such as `-u`",
1221
+ describe: "The ending; a row states `form` or `doubled`, never both.",
1222
+ }),
1223
+ Object.freeze({
1224
+ name: "doubled",
1225
+ kind: "boolean",
1226
+ shape: "`true`",
1227
+ describe: "The calling name doubles the first syllable rather than taking an ending.",
1228
+ }),
1229
+ Object.freeze({
1230
+ name: "usual",
1231
+ kind: "string",
1232
+ required: true,
1233
+ allowed: CALLING_USES,
1234
+ shape: `one of ${CALLING_USES.join(", ")}`,
1235
+ describe: "Whose name it usually makes.",
1236
+ }),
1237
+ ]);
1238
+
1239
+ /**
1240
+ * The keys of a language's `data.callingNames`: the short name a long given
1241
+ * name is called by.
1242
+ *
1243
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1244
+ */
1245
+ export const CALLING_NAME_FIELDS = Object.freeze([
1246
+ Object.freeze({
1247
+ name: "closings",
1248
+ kind: "list",
1249
+ required: true,
1250
+ shape: "list of `{ form?, doubled?, usual }`",
1251
+ entries: Object.freeze({
1252
+ kind: "map",
1253
+ shape: "a map — `{ form?, doubled?, usual }`",
1254
+ fields: CALLING_CLOSING_FIELDS,
1255
+ }),
1256
+ describe: "What a calling name closes on.",
1257
+ }),
1258
+ Object.freeze({
1259
+ name: "minSyllables",
1260
+ kind: "integer",
1261
+ required: true,
1262
+ shape: "a whole number",
1263
+ describe: "The fewest syllables a given name has that the pages give a calling name.",
1264
+ }),
1265
+ ]);
1266
+
1267
+ /**
1268
+ * The keys of one `data.older` entry.
1269
+ *
1270
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1271
+ */
1272
+ export const OLDER_FIELDS = Object.freeze([
1273
+ Object.freeze({
1274
+ name: "form",
1275
+ kind: "string",
1276
+ required: true,
1277
+ shape: "a form as the setting writes it",
1278
+ describe: "A word or name whose shape the language's rules do not make.",
1279
+ }),
1280
+ Object.freeze({
1281
+ name: "gloss",
1282
+ kind: "string",
1283
+ shape: "a string",
1284
+ describe: "What it means.",
1285
+ }),
1286
+ Object.freeze({
1287
+ name: "layer",
1288
+ kind: "string",
1289
+ shape: "a string, such as `Primordial`",
1290
+ describe: "The historical layer it belongs to.",
1291
+ }),
1292
+ ]);
1293
+
1294
+ /**
1295
+ * The keys of a language's `data.negation`.
1296
+ *
1297
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1298
+ */
1299
+ export const NEGATION_FIELDS = Object.freeze([
1300
+ Object.freeze({
1301
+ name: "particle",
1302
+ kind: "string",
1303
+ required: true,
1304
+ shape: "a form of the language's words",
1305
+ describe:
1306
+ "The particle that stands before a verb to negate it, a form of `data.words` of the " +
1307
+ "language or an ancestor.",
1308
+ }),
1309
+ Object.freeze({
1310
+ name: "mutation",
1311
+ kind: "string",
1312
+ shape: "a mutation of `data.sounds.mutations`",
1313
+ describe: "The mutation the negated verb takes, named in `data.sounds.mutations`.",
1314
+ }),
1315
+ ]);
1316
+
1317
+ /**
1318
+ * The keys of a range: the least and the most.
1319
+ *
1320
+ * @param {string} kind - `integer` or `number`.
1321
+ * @param {string} what - What the range counts.
1322
+ * @returns {readonly import("./data-keys.mjs").InnerKeySpec[]}
1323
+ */
1324
+ const rangeFields = (kind, what) =>
1325
+ Object.freeze([
1326
+ Object.freeze({
1327
+ name: "min",
1328
+ kind,
1329
+ required: true,
1330
+ shape: kind === "integer" ? "a whole number" : "a number",
1331
+ describe: `The fewest ${what}.`,
1332
+ }),
1333
+ Object.freeze({
1334
+ name: "max",
1335
+ kind,
1336
+ required: true,
1337
+ shape: kind === "integer" ? "a whole number" : "a number",
1338
+ describe: `The most ${what}; never below \`min\`.`,
1339
+ }),
1340
+ ]);
1341
+
1342
+ /**
1343
+ * The keys of one `data.naming.shapes` entry: how long a class of name runs.
1344
+ *
1345
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1346
+ */
1347
+ export const NAME_SHAPE_FIELDS = Object.freeze([
1348
+ Object.freeze({
1349
+ name: "of",
1350
+ kind: "string",
1351
+ required: true,
1352
+ allowed: NAME_CLASSES,
1353
+ shape: `one of ${NAME_CLASSES.join(", ")}`,
1354
+ describe: "The class of name the shape holds for; once per class.",
1355
+ }),
1356
+ Object.freeze({
1357
+ name: "syllables",
1358
+ kind: "map",
1359
+ shape: "a map — `{ min, max }`",
1360
+ fields: rangeFields("integer", "syllables a name of the class runs to"),
1361
+ describe: "How many syllables a name of the class runs to.",
1362
+ }),
1363
+ Object.freeze({
1364
+ name: "perVowel",
1365
+ kind: "map",
1366
+ shape: "a map — `{ min, max }`",
1367
+ fields: rangeFields("number", "consonants a name of the class carries to each vowel"),
1368
+ describe:
1369
+ "How many consonants a name of the class carries to each vowel; a shape states this, " +
1370
+ "`syllables`, or both.",
1371
+ }),
1372
+ ]);
1373
+
1374
+ /**
1375
+ * The keys of one `data.naming.edges` entry: the sounds a name may open or
1376
+ * close on, where a name keeps edges ordinary words do not.
1377
+ *
1378
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1379
+ */
1380
+ export const NAME_EDGE_FIELDS = Object.freeze([
1381
+ Object.freeze({
1382
+ name: "edge",
1383
+ kind: "string",
1384
+ required: true,
1385
+ allowed: EDGE_KINDS,
1386
+ shape: `one of ${EDGE_KINDS.join(", ")}`,
1387
+ describe: "Which edge of a name the sounds stand at.",
1388
+ }),
1389
+ Object.freeze({
1390
+ name: "sounds",
1391
+ kind: "string-or-list",
1392
+ required: true,
1393
+ allowed: Object.freeze([ANY_SOUND]),
1394
+ shape: `a list of sounds, or \`${ANY_SOUND}\``,
1395
+ entries: STRINGS,
1396
+ describe: `The sounds, as written; \`${ANY_SOUND}\` for every one, and an empty list for none.`,
1397
+ }),
1398
+ Object.freeze({
1399
+ name: "vowel",
1400
+ kind: "boolean",
1401
+ shape: "`true` or `false`",
1402
+ describe: "Whether a vowel may stand at the edge too; `false` where unstated.",
1403
+ }),
1404
+ Object.freeze({
1405
+ name: "of",
1406
+ kind: "string",
1407
+ allowed: NAME_CLASSES,
1408
+ shape: `one of ${NAME_CLASSES.join(", ")}`,
1409
+ describe: `The class of name the edge holds for; \`${ANY_NAME}\`, every name, where absent.`,
1410
+ }),
1411
+ ]);
1412
+
1413
+ /**
1414
+ * The keys of one `data.naming.draws` entry: the element kinds a kind of name
1415
+ * is built from.
1416
+ *
1417
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1418
+ */
1419
+ export const NAME_DRAW_FIELDS = Object.freeze([
1420
+ Object.freeze({
1421
+ name: "kind",
1422
+ kind: "string",
1423
+ required: true,
1424
+ allowed: REGISTER_KINDS,
1425
+ shape: `one of ${REGISTER_KINDS.join(", ")}`,
1426
+ describe: "The kind of name; once per kind.",
1427
+ }),
1428
+ Object.freeze({
1429
+ name: "elements",
1430
+ kind: "list",
1431
+ required: true,
1432
+ shape: "a list of element kinds",
1433
+ entries: Object.freeze({
1434
+ kind: "string",
1435
+ allowed: REGISTER_KINDS,
1436
+ shape: `one of ${REGISTER_KINDS.join(", ")}`,
1437
+ }),
1438
+ describe: "The kinds of element, as `data.elements` states them, the name is built from.",
1439
+ }),
1440
+ ]);
1441
+
1442
+ /**
1443
+ * The keys of one `data.naming.heldFrom` entry: what a place name takes first
1444
+ * from what the place is held from.
1445
+ *
1446
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1447
+ */
1448
+ export const HELD_FROM_FIELDS = Object.freeze([
1449
+ Object.freeze({
1450
+ name: "source",
1451
+ kind: "string",
1452
+ required: true,
1453
+ shape: "a string, such as `a founder`",
1454
+ describe: "What the place is held from.",
1455
+ }),
1456
+ Object.freeze({
1457
+ name: "takes",
1458
+ kind: "string",
1459
+ required: true,
1460
+ allowed: HELD_TAKES,
1461
+ shape: `one of ${HELD_TAKES.join(", ")}`,
1462
+ describe:
1463
+ "What stands first: a name-stem in any grade, a god's name, a founder's given name, " +
1464
+ "or one of `elements`.",
1465
+ }),
1466
+ Object.freeze({
1467
+ name: "clip",
1468
+ kind: "string",
1469
+ allowed: HELD_CLIPS,
1470
+ shape: `one of ${HELD_CLIPS.join(", ")}`,
1471
+ describe: "How the name is clipped: `first`, to its first element; whole where absent.",
1472
+ }),
1473
+ Object.freeze({
1474
+ name: "genitive",
1475
+ kind: "list",
1476
+ shape: "a list of endings",
1477
+ entries: STRINGS,
1478
+ describe: "The genitive endings, one of which stands at the seam after the name.",
1479
+ }),
1480
+ Object.freeze({
1481
+ name: "elements",
1482
+ kind: "list",
1483
+ shape: "a list of elements",
1484
+ entries: STRINGS,
1485
+ describe: "The elements that may stand first, for a row that takes an `element`.",
1486
+ }),
1487
+ ]);
1488
+
1489
+ /**
1490
+ * The keys of one `data.naming.nearName.examples` entry.
1491
+ *
1492
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1493
+ */
1494
+ export const NEAR_NAME_EXAMPLE_FIELDS = Object.freeze([
1495
+ Object.freeze({
1496
+ name: "near",
1497
+ kind: "string",
1498
+ required: true,
1499
+ shape: "a near name, such as `Anlâ`",
1500
+ describe: "The near name.",
1501
+ }),
1502
+ Object.freeze({
1503
+ name: "given",
1504
+ kind: "string",
1505
+ required: true,
1506
+ shape: "a given name",
1507
+ describe: "The given name it is the near name of.",
1508
+ }),
1509
+ ]);
1510
+
1511
+ /**
1512
+ * The keys of a language's `data.naming.nearName`.
1513
+ *
1514
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1515
+ */
1516
+ export const NEAR_NAME_FIELDS = Object.freeze([
1517
+ Object.freeze({
1518
+ name: "minSyllables",
1519
+ kind: "integer",
1520
+ required: true,
1521
+ shape: "a whole number",
1522
+ describe: "The fewest syllables a given name has that carries a near name.",
1523
+ }),
1524
+ Object.freeze({
1525
+ name: "examples",
1526
+ kind: "list",
1527
+ shape: "list of `{ near, given }`",
1528
+ entries: Object.freeze({
1529
+ kind: "map",
1530
+ shape: "a map — `{ near, given }`",
1531
+ fields: NEAR_NAME_EXAMPLE_FIELDS,
1532
+ }),
1533
+ describe: "Worked near names, each beside its given name.",
1534
+ }),
1535
+ ]);
1536
+
1537
+ /** A list of maps, as an inner key's declaration. */
1538
+ const innerList = (name, fields, describe) => {
1539
+ const keys = fields.map((field) => (field.required ? field.name : `${field.name}?`)).join(", ");
1540
+ return Object.freeze({
1541
+ name,
1542
+ kind: "list",
1543
+ shape: `list of \`{ ${keys} }\``,
1544
+ entries: Object.freeze({ kind: "map", shape: `a map — \`{ ${keys} }\``, fields }),
1545
+ describe,
1546
+ });
1547
+ };
1548
+
1549
+ /**
1550
+ * The keys of a language's `data.naming`: the parameters its names keep
1551
+ * beyond the rules its words keep.
1552
+ *
1553
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1554
+ */
1555
+ export const NAMING_FIELDS = Object.freeze([
1556
+ innerList("shapes", NAME_SHAPE_FIELDS, "How long each class of name runs."),
1557
+ innerList(
1558
+ "edges",
1559
+ NAME_EDGE_FIELDS,
1560
+ "The sounds a name may open and close on, where names keep edges ordinary words do not; " +
1561
+ "an edge stands once for each class of name.",
1562
+ ),
1563
+ innerList("draws", NAME_DRAW_FIELDS, "The element kinds each kind of name is built from."),
1564
+ innerList(
1565
+ "heldFrom",
1566
+ HELD_FROM_FIELDS,
1567
+ "What a place name takes first, by what the place is held from.",
1568
+ ),
1569
+ Object.freeze({
1570
+ name: "nearName",
1571
+ kind: "map",
1572
+ shape: "a map — `{ minSyllables, examples? }`",
1573
+ fields: NEAR_NAME_FIELDS,
1574
+ describe: "The near name a long given name carries.",
1575
+ }),
1576
+ ]);
1577
+
1578
+ /**
1579
+ * A list of maps, as a `data` field's declaration.
1580
+ *
1581
+ * @param {string} name - The key.
1582
+ * @param {readonly import("./data-keys.mjs").InnerKeySpec[]} fields - One entry's keys.
1583
+ * @param {string} describe - What the list holds.
1584
+ * @returns {object} The declaration.
1585
+ */
1586
+ function listOfMaps(name, fields, describe) {
1587
+ const keys = fields.map((field) => (field.required ? field.name : `${field.name}?`)).join(", ");
1588
+ return Object.freeze({
1589
+ name,
1590
+ kind: "list",
1591
+ shape: `list of \`{ ${keys} }\``,
1592
+ entries: Object.freeze({ kind: "map", shape: `a map — \`{ ${keys} }\``, fields }),
1593
+ describe: `${describe} A language skill note only.`,
1594
+ });
1595
+ }
1596
+
1597
+ /**
1598
+ * The `data` fields this module declares, in the order the vocabulary lists
1599
+ * them. {@link module:engine/language-data.checkLanguageData} checks what the
1600
+ * values mean.
1601
+ *
1602
+ * @type {readonly object[]}
1603
+ */
1604
+ export const LANGUAGE_DATA_FIELDS = Object.freeze([
1605
+ listOfMaps(
1606
+ "tongues",
1607
+ TONGUE_FIELDS,
1608
+ "The tags a register row's `tongue` may name, each with what it means.",
1609
+ ),
1610
+ listOfMaps(
1611
+ "register",
1612
+ REGISTER_FIELDS,
1613
+ "Every name the setting gives in the language's reach, with its tongue and pieces; the " +
1614
+ "notes carrying each name are derived.",
1615
+ ),
1616
+ listOfMaps("retired", RETIRED_FIELDS, "The forms the setting does not write, and why."),
1617
+ Object.freeze({
1618
+ name: "scope",
1619
+ kind: "map",
1620
+ shape: "a map — `{ folders?, cultures?, lore?, parents?, addresses?, paths?, types?, skip? }`",
1621
+ fields: SCOPE_FIELDS,
1622
+ describe: "The notes the register covers. A language skill note only.",
1623
+ }),
1624
+ Object.freeze({
1625
+ name: "sounds",
1626
+ kind: "map",
1627
+ shape:
1628
+ "a map — `{ letters?, edges?, clusters?, stress?, finals?, syllables?, joining?, " +
1629
+ "joins?, marks?, skeleton?, frames?, readPast?, mutations? }`",
1630
+ fields: SOUND_FIELDS,
1631
+ describe:
1632
+ "The letters the language writes and the rules its words keep. A language skill " +
1633
+ "note only.",
1634
+ }),
1635
+ listOfMaps(
1636
+ "soundChanges",
1637
+ SOUND_CHANGE_FIELDS,
1638
+ "Named sets of ordered changes, each making one tongue's words from another's.",
1639
+ ),
1640
+ listOfMaps("elements", ELEMENT_FIELDS, "The elements the language's names are built from."),
1641
+ listOfMaps("roots", ROOT_FIELDS, "The consonantal roots the language's words are set from."),
1642
+ listOfMaps(
1643
+ "cognates",
1644
+ COGNATE_FIELDS,
1645
+ "Sets of forms one older word takes in several languages, stated on the eldest.",
1646
+ ),
1647
+ Object.freeze({
1648
+ name: "callingNames",
1649
+ kind: "map",
1650
+ shape: "a map — `{ closings, minSyllables }`",
1651
+ fields: CALLING_NAME_FIELDS,
1652
+ describe: "The short name a long given name is called by. A language skill note only.",
1653
+ }),
1654
+ listOfMaps(
1655
+ "older",
1656
+ OLDER_FIELDS,
1657
+ "The words and names older than the language's rules, which no sound or name rule holds.",
1658
+ ),
1659
+ Object.freeze({
1660
+ name: "negation",
1661
+ kind: "map",
1662
+ shape: "a map — `{ particle, mutation? }`",
1663
+ fields: NEGATION_FIELDS,
1664
+ describe: "How a verb is negated. A language skill note only.",
1665
+ }),
1666
+ Object.freeze({
1667
+ name: "naming",
1668
+ kind: "map",
1669
+ shape: "a map — `{ shapes?, edges?, draws?, heldFrom?, nearName? }`",
1670
+ fields: NAMING_FIELDS,
1671
+ describe:
1672
+ "The shapes, edges and sources the language's names keep beyond its words' rules. A " +
1673
+ "language skill note only.",
1674
+ }),
1675
+ ]);
1676
+
1677
+ /** The keys of {@link TONGUE_FIELDS}, in declared order. */
1678
+ export const TONGUE_KEYS = Object.freeze(TONGUE_FIELDS.map((field) => field.name));
1679
+ /** The keys of {@link REGISTER_FIELDS}, in declared order. */
1680
+ export const REGISTER_KEYS = Object.freeze(REGISTER_FIELDS.map((field) => field.name));
1681
+ /** The keys of {@link RETIRED_FIELDS}, in declared order. */
1682
+ export const RETIRED_KEYS = Object.freeze(RETIRED_FIELDS.map((field) => field.name));
1683
+ /** The keys of {@link LETTER_FIELDS}, in declared order. */
1684
+ export const LETTER_KEYS = Object.freeze(LETTER_FIELDS.map((field) => field.name));
1685
+ /** The keys of {@link EDGE_FIELDS}, in declared order. */
1686
+ export const EDGE_KEYS = Object.freeze(EDGE_FIELDS.map((field) => field.name));
1687
+ /** The keys of {@link READ_PAST_FIELDS}, in declared order. */
1688
+ export const READ_PAST_KEYS = Object.freeze(READ_PAST_FIELDS.map((field) => field.name));
1689
+ /** The keys of {@link SOUND_CHANGE_STEP_FIELDS}, in declared order. */
1690
+ export const SOUND_CHANGE_STEP_KEYS = Object.freeze(
1691
+ SOUND_CHANGE_STEP_FIELDS.map((field) => field.name),
1692
+ );
1693
+ /** The keys of {@link FINAL_FIELDS}, in declared order. */
1694
+ export const FINAL_KEYS = Object.freeze(FINAL_FIELDS.map((field) => field.name));
1695
+ /** The keys of {@link JOINING_FIELDS}, in declared order. */
1696
+ export const JOINING_KEYS = Object.freeze(JOINING_FIELDS.map((field) => field.name));
1697
+ /** The keys of {@link JOIN_FIELDS}, in declared order. */
1698
+ export const JOIN_KEYS = Object.freeze(JOIN_FIELDS.map((field) => field.name));
1699
+ /** The keys of {@link MARK_FIELDS}, in declared order. */
1700
+ export const MARK_KEYS = Object.freeze(MARK_FIELDS.map((field) => field.name));
1701
+ /** The keys of {@link SKELETON_FIELDS}, in declared order. */
1702
+ export const SKELETON_KEYS = Object.freeze(SKELETON_FIELDS.map((field) => field.name));
1703
+ /** The keys of {@link CALLING_CLOSING_FIELDS}, in declared order. */
1704
+ export const CALLING_CLOSING_KEYS = Object.freeze(
1705
+ CALLING_CLOSING_FIELDS.map((field) => field.name),
1706
+ );
1707
+ /** The keys of {@link ELEMENT_FIELDS}, in declared order. */
1708
+ export const ELEMENT_KEYS = Object.freeze(ELEMENT_FIELDS.map((field) => field.name));
1709
+ /** The keys of {@link ROOT_FIELDS}, in declared order. */
1710
+ export const ROOT_KEYS = Object.freeze(ROOT_FIELDS.map((field) => field.name));
1711
+ /** The keys of {@link MUTATION_FIELDS}, in declared order. */
1712
+ export const MUTATION_KEYS = Object.freeze(MUTATION_FIELDS.map((field) => field.name));
1713
+ /** The keys of {@link CLUSTER_GROUP_FIELDS}, in declared order. */
1714
+ export const CLUSTER_GROUP_KEYS = Object.freeze(CLUSTER_GROUP_FIELDS.map((field) => field.name));
1715
+ /** The keys of {@link OLDER_FIELDS}, in declared order. */
1716
+ export const OLDER_KEYS = Object.freeze(OLDER_FIELDS.map((field) => field.name));
1717
+ /** The keys of {@link SCOPE_FIELDS}, in declared order. */
1718
+ export const SCOPE_KINDS = Object.freeze(SCOPE_FIELDS.map((field) => field.name));