@heroiclands/package-build 23.6.1 → 23.7.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.
@@ -0,0 +1,1203 @@
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 and the cognates.
19
+ *
20
+ * A leaf module, importing nothing: the note vocabulary reads these
21
+ * declarations as it loads, and `language-data.mjs` checks what the values
22
+ * mean. Every closed list here — the edges, element positions, register
23
+ * kinds, stress rules, joins, marks, onsets, harmony sets and calling-name
24
+ * uses — holds the labels the setting's own tables use, one word for each.
25
+ *
26
+ * @module
27
+ */
28
+
29
+ /** The `data` keys a language note states its sounds, names and history in. */
30
+ export const LANGUAGE_DATA_KEYS = Object.freeze([
31
+ "tongues",
32
+ "register",
33
+ "retired",
34
+ "scope",
35
+ "sounds",
36
+ "soundChanges",
37
+ "elements",
38
+ "roots",
39
+ "cognates",
40
+ "callingNames",
41
+ ]);
42
+
43
+ /**
44
+ * The tongue tag every language accepts without declaring it: a name given in
45
+ * the language of the pages.
46
+ */
47
+ export const RESERVED_TONGUE = "translation";
48
+
49
+ /**
50
+ * The placeholder a tongue tag closes on to declare a family of tags, one per
51
+ * tongue named after the colon: `exonym:<tongue>` declares `exonym:vylari`.
52
+ */
53
+ export const TONGUE_PLACEHOLDER = "<tongue>";
54
+
55
+ /**
56
+ * The edges of a word a sound rule names: what may open it, close it, stand
57
+ * inside it, and what counts as a syllable.
58
+ */
59
+ export const EDGE_KINDS = Object.freeze([
60
+ "openConsonant",
61
+ "openCluster",
62
+ "closeConsonant",
63
+ "closeCluster",
64
+ "innerCluster",
65
+ "vowelPair",
66
+ "glide",
67
+ "syllabic",
68
+ ]);
69
+
70
+ /** What each edge kind holds, for the reference and for a finding. */
71
+ export const EDGE_MEANINGS = Object.freeze({
72
+ openConsonant: "the single consonants that may open a word, where not every one may",
73
+ openCluster: "the clusters that may open a word",
74
+ closeConsonant: "the consonants that may close a word",
75
+ closeCluster: "the clusters that may close a word",
76
+ innerCluster: "the pairs that may stand together inside a word",
77
+ vowelPair: "the vowels that may stand together",
78
+ glide: "the glides that may follow a consonant",
79
+ syllabic: "the consonants that may carry a syllable",
80
+ });
81
+
82
+ /** The word a sound rule's `sounds` takes in place of a list: every sound. */
83
+ export const ANY_SOUND = "any";
84
+
85
+ /**
86
+ * Where in a name an element stands: `opening` opens a name or stands inside
87
+ * it, `closing` closes it, `either` does both, and `link` joins two parts and
88
+ * never begins or ends a name.
89
+ */
90
+ export const ELEMENT_POSITIONS = Object.freeze(["opening", "closing", "either", "link"]);
91
+
92
+ /** What a register name is, where a language sorts its names by kind. */
93
+ export const REGISTER_KINDS = Object.freeze(["byname", "given", "place"]);
94
+
95
+ /** Where a language's stress falls. */
96
+ export const STRESS_RULES = Object.freeze([
97
+ "first syllable",
98
+ "penultimate or final",
99
+ "by weight",
100
+ "as marked",
101
+ ]);
102
+
103
+ /** Which sounds meet where two parts of a word join. */
104
+ export const JOIN_MEETINGS = Object.freeze(["C+C", "V+C", "V+V"]);
105
+
106
+ /**
107
+ * What happens where they meet: `merge` writes one, `insert` sets the join's
108
+ * letters between, `drop` loses the first, `double` doubles the consonant.
109
+ */
110
+ export const JOIN_RULES = Object.freeze(["merge", "insert", "drop", "double"]);
111
+
112
+ /** What a written mark says of its letter. */
113
+ export const MARK_KINDS = Object.freeze(["stress", "length"]);
114
+
115
+ /** How many of a mark a word may carry: `one` at most, or `any` number. */
116
+ export const MARK_LIMITS = Object.freeze(["one", "any"]);
117
+
118
+ /** What a syllable may open on, where the rule is stated. */
119
+ export const ONSET_RULES = Object.freeze(["single"]);
120
+
121
+ /** Which harmony a neutral vowel counts as. */
122
+ export const HARMONY_SETS = Object.freeze(["back", "front"]);
123
+
124
+ /** Whose name a calling-name closing usually makes. */
125
+ export const CALLING_USES = Object.freeze(["man", "woman", "either"]);
126
+
127
+ /** A list of strings, as an inner key's `entries`. */
128
+ const STRINGS = Object.freeze({ kind: "string", shape: "a string" });
129
+
130
+ /**
131
+ * The keys of one `data.tongues` entry.
132
+ *
133
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
134
+ */
135
+ export const TONGUE_FIELDS = Object.freeze([
136
+ Object.freeze({
137
+ name: "tag",
138
+ kind: "string",
139
+ required: true,
140
+ shape: "a tag such as `dunhari`, or `exonym:<tongue>`",
141
+ describe:
142
+ "The tag a register row's `tongue` names: lowercase letters and digits, or a tag " +
143
+ "closing on `:<tongue>`, which declares one tag per tongue named after the colon.",
144
+ }),
145
+ Object.freeze({
146
+ name: "meaning",
147
+ kind: "string",
148
+ required: true,
149
+ shape: "a string",
150
+ describe: "What a name of this tongue is.",
151
+ }),
152
+ Object.freeze({
153
+ name: "language",
154
+ kind: "address",
155
+ shape: "a language skill Address",
156
+ describe: "The language note the tag names, where it is one.",
157
+ }),
158
+ ]);
159
+
160
+ /**
161
+ * The keys of one `data.register` entry.
162
+ *
163
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
164
+ */
165
+ export const REGISTER_FIELDS = Object.freeze([
166
+ Object.freeze({
167
+ name: "name",
168
+ kind: "string",
169
+ required: true,
170
+ shape: "a name as the setting writes it",
171
+ describe: "A name the setting gives, as it is written.",
172
+ }),
173
+ Object.freeze({
174
+ name: "tongue",
175
+ kind: "string",
176
+ shape: "a tag of `data.tongues`, or `translation`",
177
+ describe:
178
+ "The tongue the name is in, as a tag `data.tongues` declares, or `translation`. A row " +
179
+ "states a `tongue`, a `kind`, or both.",
180
+ }),
181
+ Object.freeze({
182
+ name: "kind",
183
+ kind: "string",
184
+ allowed: REGISTER_KINDS,
185
+ shape: `one of ${REGISTER_KINDS.join(", ")}`,
186
+ describe: "What kind of name it is, in a language that sorts its names by kind.",
187
+ }),
188
+ Object.freeze({
189
+ name: "built",
190
+ kind: "list",
191
+ shape: "a list of pieces",
192
+ entries: STRINGS,
193
+ describe: "The pieces the name is built from, in order, as a word's `built` writes them.",
194
+ }),
195
+ Object.freeze({
196
+ name: "why",
197
+ kind: "string",
198
+ shape: "a string",
199
+ describe: "Why the name stands outside the pattern the language's names are built by.",
200
+ }),
201
+ ]);
202
+
203
+ /**
204
+ * The keys of one `data.retired` entry.
205
+ *
206
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
207
+ */
208
+ export const RETIRED_FIELDS = Object.freeze([
209
+ Object.freeze({
210
+ name: "form",
211
+ kind: "string",
212
+ required: true,
213
+ shape: "a string",
214
+ describe: "The form the setting does not write.",
215
+ }),
216
+ Object.freeze({
217
+ name: "instead",
218
+ kind: "string-or-list",
219
+ shape: "a string, or a list of them",
220
+ entries: STRINGS,
221
+ describe: "What is written in its place, one form or several; absent where nothing is.",
222
+ }),
223
+ Object.freeze({
224
+ name: "why",
225
+ kind: "string",
226
+ required: true,
227
+ shape: "a string",
228
+ describe: "Why the setting does not write it.",
229
+ }),
230
+ ]);
231
+
232
+ /**
233
+ * The keys of a language's `data.scope`: the notes its register covers. A note
234
+ * is in scope when any one of them catches it and `skip` does not name it.
235
+ *
236
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
237
+ */
238
+ export const SCOPE_FIELDS = Object.freeze([
239
+ Object.freeze({
240
+ name: "folders",
241
+ kind: "list",
242
+ shape: "a list of folder Addresses",
243
+ entries: Object.freeze({ kind: "address", shape: "a folder Address" }),
244
+ describe: "Every note whose `data.packFolder` is one of these folders.",
245
+ }),
246
+ Object.freeze({
247
+ name: "cultures",
248
+ kind: "list",
249
+ shape: "a list of culture Addresses",
250
+ entries: Object.freeze({ kind: "address", shape: "a culture Address" }),
251
+ describe: "Every note whose `data.culture` is one of these cultures.",
252
+ }),
253
+ Object.freeze({
254
+ name: "lore",
255
+ kind: "list",
256
+ shape: "a list of lore Addresses",
257
+ entries: Object.freeze({ kind: "address", shape: "a lore Address" }),
258
+ describe: "Every note whose `data.lore` names one of these notes.",
259
+ }),
260
+ Object.freeze({
261
+ name: "parents",
262
+ kind: "list",
263
+ shape: "a list of Addresses",
264
+ entries: Object.freeze({ kind: "address", shape: "a place or affiliation Address" }),
265
+ describe: "Every note whose `data.parents` names one of these notes.",
266
+ }),
267
+ Object.freeze({
268
+ name: "addresses",
269
+ kind: "list",
270
+ shape: "a list of Addresses",
271
+ entries: Object.freeze({ kind: "address", shape: "an Address" }),
272
+ describe: "These notes themselves.",
273
+ }),
274
+ Object.freeze({
275
+ name: "paths",
276
+ kind: "list",
277
+ shape: "a list of content paths",
278
+ entries: Object.freeze({ kind: "string", shape: "a path such as `Regions/Aelwyth/`" }),
279
+ describe: "Every note whose file sits below one of these paths of the content tree.",
280
+ }),
281
+ Object.freeze({
282
+ name: "types",
283
+ kind: "list",
284
+ shape: "a list of note types",
285
+ entries: Object.freeze({ kind: "string", shape: "a note type" }),
286
+ describe:
287
+ "The note types the register covers; where stated, a note of another type is out " +
288
+ "of scope whatever else catches it.",
289
+ }),
290
+ Object.freeze({
291
+ name: "skip",
292
+ kind: "list",
293
+ shape: "a list of Addresses",
294
+ entries: Object.freeze({ kind: "address", shape: "an Address" }),
295
+ describe: "Notes out of scope whatever else catches them.",
296
+ }),
297
+ ]);
298
+
299
+ /**
300
+ * The keys of one `data.sounds.letters` entry.
301
+ *
302
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
303
+ */
304
+ export const LETTER_FIELDS = Object.freeze([
305
+ Object.freeze({
306
+ name: "letters",
307
+ kind: "list",
308
+ required: true,
309
+ shape: "a list of letters",
310
+ entries: STRINGS,
311
+ describe: "The letters or digraphs the row describes, as written.",
312
+ }),
313
+ Object.freeze({
314
+ name: "kind",
315
+ kind: "string",
316
+ required: true,
317
+ shape: "a string, such as `consonant` or `long vowel`",
318
+ describe: "What kind of sound they write.",
319
+ }),
320
+ Object.freeze({
321
+ name: "like",
322
+ kind: "string",
323
+ required: true,
324
+ shape: "a string",
325
+ describe: "What they sound like.",
326
+ }),
327
+ Object.freeze({
328
+ name: "tongue",
329
+ kind: "string-or-list",
330
+ shape: "a tag, or a list of them",
331
+ entries: STRINGS,
332
+ describe:
333
+ "The tongues that write them, as tags `data.tongues` declares; every one where absent.",
334
+ }),
335
+ ]);
336
+
337
+ /**
338
+ * The keys of one `data.sounds.edges` entry.
339
+ *
340
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
341
+ */
342
+ export const EDGE_FIELDS = Object.freeze([
343
+ Object.freeze({
344
+ name: "edge",
345
+ kind: "string",
346
+ required: true,
347
+ allowed: EDGE_KINDS,
348
+ shape: `one of ${EDGE_KINDS.join(", ")}`,
349
+ describe: "Which edge of a word the sounds stand at.",
350
+ }),
351
+ Object.freeze({
352
+ name: "sounds",
353
+ kind: "string-or-list",
354
+ required: true,
355
+ allowed: Object.freeze([ANY_SOUND]),
356
+ shape: `a list of sounds, or \`${ANY_SOUND}\``,
357
+ entries: STRINGS,
358
+ describe: `The sounds, as written; \`${ANY_SOUND}\` for every one, and an empty list for none.`,
359
+ }),
360
+ ]);
361
+
362
+ /**
363
+ * The keys of a language's `data.sounds.clusters`.
364
+ *
365
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
366
+ */
367
+ export const CLUSTER_FIELDS = Object.freeze([
368
+ Object.freeze({
369
+ name: "max",
370
+ kind: "integer",
371
+ required: true,
372
+ shape: "a whole number",
373
+ describe: "The most consonants that stand together inside a word.",
374
+ }),
375
+ ]);
376
+
377
+ /**
378
+ * The keys of one `data.sounds.readPast` entry.
379
+ *
380
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
381
+ */
382
+ export const READ_PAST_FIELDS = Object.freeze([
383
+ Object.freeze({
384
+ name: "written",
385
+ kind: "string",
386
+ required: true,
387
+ shape: "a string",
388
+ describe: "A mark or spelling the pages write.",
389
+ }),
390
+ Object.freeze({
391
+ name: "readAs",
392
+ kind: "string",
393
+ shape: "a string",
394
+ describe: "What a word is read with in its place; absent where it is read as nothing.",
395
+ }),
396
+ Object.freeze({
397
+ name: "what",
398
+ kind: "string",
399
+ required: true,
400
+ shape: "a string",
401
+ describe: "What the mark or spelling is.",
402
+ }),
403
+ ]);
404
+
405
+ /** A list of sounds, or `any` for every one. */
406
+ const SOUNDS_OR_ANY = Object.freeze({
407
+ kind: "string-or-list",
408
+ allowed: Object.freeze([ANY_SOUND]),
409
+ shape: `a list of sounds, or \`${ANY_SOUND}\``,
410
+ entries: STRINGS,
411
+ });
412
+
413
+ /**
414
+ * The keys of one `data.sounds.finals` entry: a syllable final, for a
415
+ * language whose syllables are parsed by them.
416
+ *
417
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
418
+ */
419
+ export const FINAL_FIELDS = Object.freeze([
420
+ Object.freeze({
421
+ name: "final",
422
+ kind: "string",
423
+ required: true,
424
+ shape: "a final as written, such as `ang`",
425
+ describe: "The final.",
426
+ }),
427
+ Object.freeze({
428
+ name: "like",
429
+ kind: "string",
430
+ required: true,
431
+ shape: "a string",
432
+ describe: "What it sounds like.",
433
+ }),
434
+ ]);
435
+
436
+ /**
437
+ * The keys of one `data.sounds.joining` entry: how two parts are written
438
+ * where the first closes and the second opens on the given sounds.
439
+ *
440
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
441
+ */
442
+ export const JOINING_FIELDS = Object.freeze([
443
+ Object.freeze({
444
+ name: "first",
445
+ kind: "list",
446
+ required: true,
447
+ shape: "a list of sounds",
448
+ entries: STRINGS,
449
+ describe: "The sounds the first part closes on.",
450
+ }),
451
+ Object.freeze({
452
+ name: "second",
453
+ kind: "list",
454
+ required: true,
455
+ shape: "a list of sounds",
456
+ entries: STRINGS,
457
+ describe: "The sounds the second part opens on.",
458
+ }),
459
+ Object.freeze({
460
+ name: "written",
461
+ kind: "string",
462
+ required: true,
463
+ shape: "a string, such as `â` or `y+`",
464
+ describe:
465
+ "What is written where they meet: the sound that stands for both, or a join " +
466
+ "closing on `+`, which keeps both and sets it between.",
467
+ }),
468
+ Object.freeze({
469
+ name: "example",
470
+ kind: "string",
471
+ shape: "a string",
472
+ describe: "A word joined so.",
473
+ }),
474
+ ]);
475
+
476
+ /**
477
+ * The keys of one `data.sounds.joins` entry: what happens where two parts of a
478
+ * word meet.
479
+ *
480
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
481
+ */
482
+ export const JOIN_FIELDS = Object.freeze([
483
+ Object.freeze({
484
+ name: "meeting",
485
+ kind: "string",
486
+ required: true,
487
+ allowed: JOIN_MEETINGS,
488
+ shape: `one of ${JOIN_MEETINGS.join(", ")}`,
489
+ describe:
490
+ "Which sounds meet: a consonant `C` or a vowel `V` closing the first part, then opening the second.",
491
+ }),
492
+ Object.freeze({
493
+ name: "rule",
494
+ kind: "string",
495
+ required: true,
496
+ allowed: JOIN_RULES,
497
+ shape: `one of ${JOIN_RULES.join(", ")}`,
498
+ describe: "What happens where they meet.",
499
+ }),
500
+ Object.freeze({
501
+ name: "letters",
502
+ kind: "list",
503
+ shape: "a list of letters",
504
+ entries: STRINGS,
505
+ describe:
506
+ "The letters the rule takes: what `insert` sets between, or what `double` doubles.",
507
+ }),
508
+ Object.freeze({
509
+ name: "where",
510
+ kind: "string",
511
+ shape: "a string, such as `between stems`",
512
+ describe: "Where the rule holds; everywhere where absent.",
513
+ }),
514
+ ]);
515
+
516
+ /**
517
+ * The keys of one `data.sounds.marks` entry: a mark the language writes on a
518
+ * letter.
519
+ *
520
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
521
+ */
522
+ export const MARK_FIELDS = Object.freeze([
523
+ Object.freeze({
524
+ name: "mark",
525
+ kind: "string",
526
+ required: true,
527
+ shape: "a string, such as `acute`",
528
+ describe: "The mark's name.",
529
+ }),
530
+ Object.freeze({
531
+ name: "letters",
532
+ kind: "list",
533
+ required: true,
534
+ shape: "a list of letters",
535
+ entries: STRINGS,
536
+ describe: "The letters that carry it.",
537
+ }),
538
+ Object.freeze({
539
+ name: "kind",
540
+ kind: "string",
541
+ required: true,
542
+ allowed: MARK_KINDS,
543
+ shape: `one of ${MARK_KINDS.join(", ")}`,
544
+ describe: "What the mark says of its letter.",
545
+ }),
546
+ Object.freeze({
547
+ name: "limit",
548
+ kind: "string",
549
+ required: true,
550
+ allowed: MARK_LIMITS,
551
+ shape: `one of ${MARK_LIMITS.join(", ")}`,
552
+ describe: "How many a word may carry: `one` at most, or `any`.",
553
+ }),
554
+ ]);
555
+
556
+ /**
557
+ * The keys of a language's `data.sounds.syllables.harmony`.
558
+ *
559
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
560
+ */
561
+ export const HARMONY_FIELDS = Object.freeze([
562
+ Object.freeze({
563
+ name: "back",
564
+ kind: "list",
565
+ required: true,
566
+ shape: "a list of vowels",
567
+ entries: STRINGS,
568
+ describe: "The back vowels.",
569
+ }),
570
+ Object.freeze({
571
+ name: "front",
572
+ kind: "list",
573
+ required: true,
574
+ shape: "a list of vowels",
575
+ entries: STRINGS,
576
+ describe: "The front vowels.",
577
+ }),
578
+ Object.freeze({
579
+ name: "neutral",
580
+ kind: "list",
581
+ shape: "a list of vowels",
582
+ entries: STRINGS,
583
+ describe: "The vowels of neither set.",
584
+ }),
585
+ Object.freeze({
586
+ name: "neutralAs",
587
+ kind: "string",
588
+ allowed: HARMONY_SETS,
589
+ shape: `one of ${HARMONY_SETS.join(", ")}`,
590
+ describe: "The set a word of neutral vowels alone counts as.",
591
+ }),
592
+ ]);
593
+
594
+ /**
595
+ * The keys of a language's `data.sounds.syllables`: what a syllable may open
596
+ * and close on, and what a word may.
597
+ *
598
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
599
+ */
600
+ export const SYLLABLE_FIELDS = Object.freeze([
601
+ Object.freeze({
602
+ name: "onsets",
603
+ kind: "string",
604
+ allowed: ONSET_RULES,
605
+ shape: `one of ${ONSET_RULES.join(", ")}`,
606
+ describe: "What a syllable opens on: `single`, a single consonant.",
607
+ }),
608
+ Object.freeze({
609
+ name: "codas",
610
+ ...SOUNDS_OR_ANY,
611
+ describe: "The consonants that may close a syllable; `any` for every one.",
612
+ }),
613
+ Object.freeze({
614
+ name: "finals",
615
+ ...SOUNDS_OR_ANY,
616
+ describe: "The consonants that may close a word; `any` for every one.",
617
+ }),
618
+ Object.freeze({
619
+ name: "doubled",
620
+ kind: "list",
621
+ shape: "a list of letters",
622
+ entries: STRINGS,
623
+ describe: "The doubled consonants a word may hold; none where empty.",
624
+ }),
625
+ Object.freeze({
626
+ name: "vowelPairs",
627
+ kind: "list",
628
+ shape: "a list of vowel pairs",
629
+ entries: STRINGS,
630
+ describe: "The vowels that may stand together; none where empty.",
631
+ }),
632
+ Object.freeze({
633
+ name: "initialVowel",
634
+ kind: "boolean",
635
+ shape: "`true` or `false`",
636
+ describe: "Whether a word may open on a vowel.",
637
+ }),
638
+ Object.freeze({
639
+ name: "finalVowel",
640
+ kind: "boolean",
641
+ shape: "`true` or `false`",
642
+ describe: "Whether a word may close on a vowel; `true` where unstated.",
643
+ }),
644
+ Object.freeze({
645
+ name: "neverInitial",
646
+ kind: "list",
647
+ shape: "a list of letters",
648
+ entries: STRINGS,
649
+ describe: "The letters that never open a word.",
650
+ }),
651
+ Object.freeze({
652
+ name: "signature",
653
+ kind: "list",
654
+ shape: "a list of letters",
655
+ entries: STRINGS,
656
+ describe: "The letters that mark a word as the language's own.",
657
+ }),
658
+ Object.freeze({
659
+ name: "harmony",
660
+ kind: "map",
661
+ shape: "a map — `{ back, front, neutral?, neutralAs? }`",
662
+ fields: HARMONY_FIELDS,
663
+ describe: "The vowel harmony a word keeps.",
664
+ }),
665
+ ]);
666
+
667
+ /**
668
+ * The keys of one `data.sounds.skeleton` entry: the vowel set between a
669
+ * root's second and third consonants.
670
+ *
671
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
672
+ */
673
+ export const SKELETON_FIELDS = Object.freeze([
674
+ Object.freeze({
675
+ name: "second",
676
+ ...SOUNDS_OR_ANY,
677
+ required: true,
678
+ describe: "The second consonants of a root the row holds for; `any` for every other.",
679
+ }),
680
+ Object.freeze({
681
+ name: "between",
682
+ kind: "string",
683
+ shape: "a vowel, such as `a`",
684
+ describe: "The vowel set between the second and third consonants; nothing where absent.",
685
+ }),
686
+ ]);
687
+
688
+ /**
689
+ * The keys of one `data.sounds.frames` entry: a pattern a root is set in.
690
+ *
691
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
692
+ */
693
+ export const FRAME_FIELDS = Object.freeze([
694
+ Object.freeze({
695
+ name: "frame",
696
+ kind: "string",
697
+ required: true,
698
+ shape: "a string, such as `bare`",
699
+ describe: "The frame's name, which a word's `frame` names.",
700
+ }),
701
+ Object.freeze({
702
+ name: "shape",
703
+ kind: "string",
704
+ required: true,
705
+ shape: "a pattern, such as `K1aK2K3`",
706
+ describe: "The pattern, the root's consonants numbered `K1`, `K2`, `K3`.",
707
+ }),
708
+ Object.freeze({
709
+ name: "class",
710
+ kind: "list",
711
+ required: true,
712
+ shape: "a list of word class bases",
713
+ entries: STRINGS,
714
+ describe: "The classes of word the frame makes, each a base of the master word classes.",
715
+ }),
716
+ Object.freeze({
717
+ name: "makes",
718
+ kind: "string",
719
+ required: true,
720
+ shape: "a string",
721
+ describe: "What the frame makes of a root.",
722
+ }),
723
+ Object.freeze({
724
+ name: "examples",
725
+ kind: "map",
726
+ shape: "a map of roots to forms",
727
+ values: Object.freeze({ kind: "string", shape: "a form, bare" }),
728
+ describe: "The form a root takes in the frame, keyed by the root.",
729
+ }),
730
+ ]);
731
+
732
+ /**
733
+ * The keys of a language's `data.sounds`.
734
+ *
735
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
736
+ */
737
+ export const SOUND_FIELDS = Object.freeze([
738
+ Object.freeze({
739
+ name: "letters",
740
+ kind: "list",
741
+ shape: "list of `{ letters, kind, like, tongue? }`",
742
+ entries: Object.freeze({
743
+ kind: "map",
744
+ shape: "a map — `{ letters, kind, like, tongue? }`",
745
+ fields: LETTER_FIELDS,
746
+ }),
747
+ describe: "The letters the language writes, and the sounds they stand for.",
748
+ }),
749
+ Object.freeze({
750
+ name: "edges",
751
+ kind: "list",
752
+ shape: "list of `{ edge, sounds }`",
753
+ entries: Object.freeze({
754
+ kind: "map",
755
+ shape: "a map — `{ edge, sounds }`",
756
+ fields: EDGE_FIELDS,
757
+ }),
758
+ describe: "The sounds that may stand at each edge of a word, one entry per edge.",
759
+ }),
760
+ Object.freeze({
761
+ name: "clusters",
762
+ kind: "map",
763
+ shape: "a map — `{ max }`",
764
+ fields: CLUSTER_FIELDS,
765
+ describe: "How many consonants may stand together.",
766
+ }),
767
+ Object.freeze({
768
+ name: "stress",
769
+ kind: "string",
770
+ allowed: STRESS_RULES,
771
+ shape: `one of ${STRESS_RULES.join(", ")}`,
772
+ describe: "Where a word's stress falls.",
773
+ }),
774
+ Object.freeze({
775
+ name: "finals",
776
+ kind: "list",
777
+ shape: "list of `{ final, like }`",
778
+ entries: Object.freeze({
779
+ kind: "map",
780
+ shape: "a map — `{ final, like }`",
781
+ fields: FINAL_FIELDS,
782
+ }),
783
+ describe: "The syllable finals, for a language whose syllables are parsed by them.",
784
+ }),
785
+ Object.freeze({
786
+ name: "syllables",
787
+ kind: "map",
788
+ shape: "a map of syllable rules",
789
+ fields: SYLLABLE_FIELDS,
790
+ describe: "What a syllable and a word may open and close on.",
791
+ }),
792
+ Object.freeze({
793
+ name: "joining",
794
+ kind: "list",
795
+ shape: "list of `{ first, second, written, example? }`",
796
+ entries: Object.freeze({
797
+ kind: "map",
798
+ shape: "a map — `{ first, second, written, example? }`",
799
+ fields: JOINING_FIELDS,
800
+ }),
801
+ describe:
802
+ "How two parts are written where the first closes and the second opens on the given sounds.",
803
+ }),
804
+ Object.freeze({
805
+ name: "joins",
806
+ kind: "list",
807
+ shape: "list of `{ meeting, rule, letters?, where? }`",
808
+ entries: Object.freeze({
809
+ kind: "map",
810
+ shape: "a map — `{ meeting, rule, letters?, where? }`",
811
+ fields: JOIN_FIELDS,
812
+ }),
813
+ describe: "What happens where two parts of a word meet, one entry per meeting.",
814
+ }),
815
+ Object.freeze({
816
+ name: "marks",
817
+ kind: "list",
818
+ shape: "list of `{ mark, letters, kind, limit }`",
819
+ entries: Object.freeze({
820
+ kind: "map",
821
+ shape: "a map — `{ mark, letters, kind, limit }`",
822
+ fields: MARK_FIELDS,
823
+ }),
824
+ describe: "The marks the language writes on its letters.",
825
+ }),
826
+ Object.freeze({
827
+ name: "skeleton",
828
+ kind: "list",
829
+ shape: "list of `{ second, between? }`",
830
+ entries: Object.freeze({
831
+ kind: "map",
832
+ shape: "a map — `{ second, between? }`",
833
+ fields: SKELETON_FIELDS,
834
+ }),
835
+ describe: "The vowel set between a root's second and third consonants.",
836
+ }),
837
+ Object.freeze({
838
+ name: "frames",
839
+ kind: "list",
840
+ shape: "list of `{ frame, shape, class, makes, examples? }`",
841
+ entries: Object.freeze({
842
+ kind: "map",
843
+ shape: "a map — `{ frame, shape, class, makes, examples? }`",
844
+ fields: FRAME_FIELDS,
845
+ }),
846
+ describe: "The patterns a root is set in.",
847
+ }),
848
+ Object.freeze({
849
+ name: "readPast",
850
+ kind: "list",
851
+ shape: "list of `{ written, readAs?, what }`",
852
+ entries: Object.freeze({
853
+ kind: "map",
854
+ shape: "a map — `{ written, readAs?, what }`",
855
+ fields: READ_PAST_FIELDS,
856
+ }),
857
+ describe: "Marks and spellings a word is read through before the sound rules hold it.",
858
+ }),
859
+ ]);
860
+
861
+ /**
862
+ * The keys of one `data.soundChanges` entry.
863
+ *
864
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
865
+ */
866
+ export const SOUND_CHANGE_STEP_FIELDS = Object.freeze([
867
+ Object.freeze({
868
+ name: "step",
869
+ kind: "string",
870
+ required: true,
871
+ shape: 'a string, such as `A1` or `"1"`',
872
+ describe: "The step's label, in the order the changes apply.",
873
+ }),
874
+ Object.freeze({
875
+ name: "change",
876
+ kind: "string",
877
+ required: true,
878
+ shape: "a change in arrow notation, such as `` `ph` > `f`; `kh` > `h` ``",
879
+ describe:
880
+ "The change, in arrow notation: one or more `` `sources` > `targets` `` rules " +
881
+ "separated by `;`, or the operation a step names in words.",
882
+ }),
883
+ Object.freeze({
884
+ name: "where",
885
+ kind: "string",
886
+ shape: "a string, such as `word end`",
887
+ describe: "Where the change holds; everywhere where absent.",
888
+ }),
889
+ ]);
890
+
891
+ /**
892
+ * The keys of one `data.soundChanges` set: an ordered run of steps, named, and
893
+ * where they lead from and to.
894
+ *
895
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
896
+ */
897
+ export const SOUND_CHANGE_FIELDS = Object.freeze([
898
+ Object.freeze({
899
+ name: "name",
900
+ kind: "string",
901
+ required: true,
902
+ shape: "a string, such as `From Helonic to Byzarian`",
903
+ describe: "The set's name, which no other set of the note shares.",
904
+ }),
905
+ Object.freeze({
906
+ name: "from",
907
+ kind: "address",
908
+ shape: "a language skill Address",
909
+ describe: "The language whose words the steps start from.",
910
+ }),
911
+ Object.freeze({
912
+ name: "to",
913
+ kind: "address",
914
+ shape: "a language skill Address",
915
+ describe: "The language whose words the steps make.",
916
+ }),
917
+ Object.freeze({
918
+ name: "steps",
919
+ kind: "list",
920
+ required: true,
921
+ shape: "list of `{ step, change, where? }`",
922
+ entries: Object.freeze({
923
+ kind: "map",
924
+ shape: "a map — `{ step, change, where? }`",
925
+ fields: SOUND_CHANGE_STEP_FIELDS,
926
+ }),
927
+ describe: "The steps, in the order they apply; no step label stands twice in a set.",
928
+ }),
929
+ ]);
930
+
931
+ /**
932
+ * The keys of one `data.elements` entry.
933
+ *
934
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
935
+ */
936
+ export const ELEMENT_FIELDS = Object.freeze([
937
+ Object.freeze({
938
+ name: "form",
939
+ kind: "string",
940
+ required: true,
941
+ shape: "the element as written, such as `arn-`",
942
+ describe: "The element a name is built from.",
943
+ }),
944
+ Object.freeze({
945
+ name: "variants",
946
+ kind: "list",
947
+ shape: "a list of forms",
948
+ entries: STRINGS,
949
+ describe: "The element's other forms, such as `aldr-` beside `ald-`.",
950
+ }),
951
+ Object.freeze({
952
+ name: "position",
953
+ kind: "string",
954
+ allowed: ELEMENT_POSITIONS,
955
+ shape: `one of ${ELEMENT_POSITIONS.join(", ")}`,
956
+ describe: "Where in a name the element stands.",
957
+ }),
958
+ Object.freeze({
959
+ name: "sense",
960
+ kind: "string",
961
+ required: true,
962
+ shape: "a string",
963
+ describe: "What the element means.",
964
+ }),
965
+ Object.freeze({
966
+ name: "examples",
967
+ kind: "list",
968
+ shape: "a list of names",
969
+ entries: STRINGS,
970
+ describe: "The names the element is read from.",
971
+ }),
972
+ ]);
973
+
974
+ /**
975
+ * The keys of one `data.roots` entry.
976
+ *
977
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
978
+ */
979
+ export const ROOT_FIELDS = Object.freeze([
980
+ Object.freeze({
981
+ name: "root",
982
+ kind: "string",
983
+ required: true,
984
+ shape: "a consonantal skeleton, such as `d-l-k`",
985
+ describe: "The root.",
986
+ }),
987
+ Object.freeze({
988
+ name: "sense",
989
+ kind: "string",
990
+ required: true,
991
+ shape: "a string",
992
+ describe: "What the root means.",
993
+ }),
994
+ Object.freeze({
995
+ name: "field",
996
+ kind: "string",
997
+ shape: "a string, such as `stone`",
998
+ describe: "The field the root belongs to.",
999
+ }),
1000
+ ]);
1001
+
1002
+ /**
1003
+ * The keys of one `data.cognates` entry.
1004
+ *
1005
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1006
+ */
1007
+ export const COGNATE_FIELDS = Object.freeze([
1008
+ Object.freeze({
1009
+ name: "forms",
1010
+ kind: "map",
1011
+ required: true,
1012
+ shape: "a map of language Addresses to forms",
1013
+ values: Object.freeze({ kind: "string", shape: "a form, bare" }),
1014
+ describe: "The form the set takes in each language, keyed by the language note's Address.",
1015
+ }),
1016
+ Object.freeze({
1017
+ name: "proto",
1018
+ kind: "string",
1019
+ shape: "a form, bare",
1020
+ describe:
1021
+ "The reconstructed form the set descends from, in no attested language; printed with " +
1022
+ "an asterisk.",
1023
+ }),
1024
+ Object.freeze({
1025
+ name: "gloss",
1026
+ kind: "string",
1027
+ required: true,
1028
+ shape: "a string",
1029
+ describe: "What the forms mean.",
1030
+ }),
1031
+ ]);
1032
+
1033
+ /**
1034
+ * The keys of one `data.callingNames.closings` entry: an ending, or the first
1035
+ * syllable doubled, that a calling name closes on.
1036
+ *
1037
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1038
+ */
1039
+ export const CALLING_CLOSING_FIELDS = Object.freeze([
1040
+ Object.freeze({
1041
+ name: "form",
1042
+ kind: "string",
1043
+ shape: "an ending, such as `-u`",
1044
+ describe: "The ending; a row states `form` or `doubled`, never both.",
1045
+ }),
1046
+ Object.freeze({
1047
+ name: "doubled",
1048
+ kind: "boolean",
1049
+ shape: "`true`",
1050
+ describe: "The calling name doubles the first syllable rather than taking an ending.",
1051
+ }),
1052
+ Object.freeze({
1053
+ name: "usual",
1054
+ kind: "string",
1055
+ required: true,
1056
+ allowed: CALLING_USES,
1057
+ shape: `one of ${CALLING_USES.join(", ")}`,
1058
+ describe: "Whose name it usually makes.",
1059
+ }),
1060
+ ]);
1061
+
1062
+ /**
1063
+ * The keys of a language's `data.callingNames`: the short name a long given
1064
+ * name is called by.
1065
+ *
1066
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
1067
+ */
1068
+ export const CALLING_NAME_FIELDS = Object.freeze([
1069
+ Object.freeze({
1070
+ name: "closings",
1071
+ kind: "list",
1072
+ required: true,
1073
+ shape: "list of `{ form?, doubled?, usual }`",
1074
+ entries: Object.freeze({
1075
+ kind: "map",
1076
+ shape: "a map — `{ form?, doubled?, usual }`",
1077
+ fields: CALLING_CLOSING_FIELDS,
1078
+ }),
1079
+ describe: "What a calling name closes on.",
1080
+ }),
1081
+ Object.freeze({
1082
+ name: "minSyllables",
1083
+ kind: "integer",
1084
+ required: true,
1085
+ shape: "a whole number",
1086
+ describe: "The fewest syllables a given name has that the pages give a calling name.",
1087
+ }),
1088
+ ]);
1089
+
1090
+ /**
1091
+ * A list of maps, as a `data` field's declaration.
1092
+ *
1093
+ * @param {string} name - The key.
1094
+ * @param {readonly import("./data-keys.mjs").InnerKeySpec[]} fields - One entry's keys.
1095
+ * @param {string} describe - What the list holds.
1096
+ * @returns {object} The declaration.
1097
+ */
1098
+ function listOfMaps(name, fields, describe) {
1099
+ const keys = fields.map((field) => (field.required ? field.name : `${field.name}?`)).join(", ");
1100
+ return Object.freeze({
1101
+ name,
1102
+ kind: "list",
1103
+ shape: `list of \`{ ${keys} }\``,
1104
+ entries: Object.freeze({ kind: "map", shape: `a map — \`{ ${keys} }\``, fields }),
1105
+ describe: `${describe} A language skill note only.`,
1106
+ });
1107
+ }
1108
+
1109
+ /**
1110
+ * The `data` fields this module declares, in the order the vocabulary lists
1111
+ * them. {@link module:engine/language-data.checkLanguageData} checks what the
1112
+ * values mean.
1113
+ *
1114
+ * @type {readonly object[]}
1115
+ */
1116
+ export const LANGUAGE_DATA_FIELDS = Object.freeze([
1117
+ listOfMaps(
1118
+ "tongues",
1119
+ TONGUE_FIELDS,
1120
+ "The tags a register row's `tongue` may name, each with what it means.",
1121
+ ),
1122
+ listOfMaps(
1123
+ "register",
1124
+ REGISTER_FIELDS,
1125
+ "Every name the setting gives in the language's reach, with its tongue and pieces; the " +
1126
+ "notes carrying each name are derived.",
1127
+ ),
1128
+ listOfMaps("retired", RETIRED_FIELDS, "The forms the setting does not write, and why."),
1129
+ Object.freeze({
1130
+ name: "scope",
1131
+ kind: "map",
1132
+ shape: "a map — `{ folders?, cultures?, lore?, parents?, addresses?, paths?, types?, skip? }`",
1133
+ fields: SCOPE_FIELDS,
1134
+ describe: "The notes the register covers. A language skill note only.",
1135
+ }),
1136
+ Object.freeze({
1137
+ name: "sounds",
1138
+ kind: "map",
1139
+ shape:
1140
+ "a map — `{ letters?, edges?, clusters?, stress?, finals?, syllables?, joining?, " +
1141
+ "joins?, marks?, skeleton?, frames?, readPast? }`",
1142
+ fields: SOUND_FIELDS,
1143
+ describe:
1144
+ "The letters the language writes and the rules its words keep. A language skill " +
1145
+ "note only.",
1146
+ }),
1147
+ listOfMaps(
1148
+ "soundChanges",
1149
+ SOUND_CHANGE_FIELDS,
1150
+ "Named sets of ordered changes, each making one tongue's words from another's.",
1151
+ ),
1152
+ listOfMaps("elements", ELEMENT_FIELDS, "The elements the language's names are built from."),
1153
+ listOfMaps("roots", ROOT_FIELDS, "The consonantal roots the language's words are set from."),
1154
+ listOfMaps(
1155
+ "cognates",
1156
+ COGNATE_FIELDS,
1157
+ "Sets of forms one older word takes in several languages, stated on the eldest.",
1158
+ ),
1159
+ Object.freeze({
1160
+ name: "callingNames",
1161
+ kind: "map",
1162
+ shape: "a map — `{ closings, minSyllables }`",
1163
+ fields: CALLING_NAME_FIELDS,
1164
+ describe: "The short name a long given name is called by. A language skill note only.",
1165
+ }),
1166
+ ]);
1167
+
1168
+ /** The keys of {@link TONGUE_FIELDS}, in declared order. */
1169
+ export const TONGUE_KEYS = Object.freeze(TONGUE_FIELDS.map((field) => field.name));
1170
+ /** The keys of {@link REGISTER_FIELDS}, in declared order. */
1171
+ export const REGISTER_KEYS = Object.freeze(REGISTER_FIELDS.map((field) => field.name));
1172
+ /** The keys of {@link RETIRED_FIELDS}, in declared order. */
1173
+ export const RETIRED_KEYS = Object.freeze(RETIRED_FIELDS.map((field) => field.name));
1174
+ /** The keys of {@link LETTER_FIELDS}, in declared order. */
1175
+ export const LETTER_KEYS = Object.freeze(LETTER_FIELDS.map((field) => field.name));
1176
+ /** The keys of {@link EDGE_FIELDS}, in declared order. */
1177
+ export const EDGE_KEYS = Object.freeze(EDGE_FIELDS.map((field) => field.name));
1178
+ /** The keys of {@link READ_PAST_FIELDS}, in declared order. */
1179
+ export const READ_PAST_KEYS = Object.freeze(READ_PAST_FIELDS.map((field) => field.name));
1180
+ /** The keys of {@link SOUND_CHANGE_STEP_FIELDS}, in declared order. */
1181
+ export const SOUND_CHANGE_STEP_KEYS = Object.freeze(
1182
+ SOUND_CHANGE_STEP_FIELDS.map((field) => field.name),
1183
+ );
1184
+ /** The keys of {@link FINAL_FIELDS}, in declared order. */
1185
+ export const FINAL_KEYS = Object.freeze(FINAL_FIELDS.map((field) => field.name));
1186
+ /** The keys of {@link JOINING_FIELDS}, in declared order. */
1187
+ export const JOINING_KEYS = Object.freeze(JOINING_FIELDS.map((field) => field.name));
1188
+ /** The keys of {@link JOIN_FIELDS}, in declared order. */
1189
+ export const JOIN_KEYS = Object.freeze(JOIN_FIELDS.map((field) => field.name));
1190
+ /** The keys of {@link MARK_FIELDS}, in declared order. */
1191
+ export const MARK_KEYS = Object.freeze(MARK_FIELDS.map((field) => field.name));
1192
+ /** The keys of {@link SKELETON_FIELDS}, in declared order. */
1193
+ export const SKELETON_KEYS = Object.freeze(SKELETON_FIELDS.map((field) => field.name));
1194
+ /** The keys of {@link CALLING_CLOSING_FIELDS}, in declared order. */
1195
+ export const CALLING_CLOSING_KEYS = Object.freeze(
1196
+ CALLING_CLOSING_FIELDS.map((field) => field.name),
1197
+ );
1198
+ /** The keys of {@link ELEMENT_FIELDS}, in declared order. */
1199
+ export const ELEMENT_KEYS = Object.freeze(ELEMENT_FIELDS.map((field) => field.name));
1200
+ /** The keys of {@link ROOT_FIELDS}, in declared order. */
1201
+ export const ROOT_KEYS = Object.freeze(ROOT_FIELDS.map((field) => field.name));
1202
+ /** The keys of {@link SCOPE_FIELDS}, in declared order. */
1203
+ export const SCOPE_KINDS = Object.freeze(SCOPE_FIELDS.map((field) => field.name));