@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.
- package/CHANGELOG.md +39 -0
- package/docs/authoring/links-and-markup.md +418 -24
- package/docs/content-format.md +1 -1
- package/docs/reference/format-details.md +680 -73
- package/docs/reference/note-types.md +208 -39
- package/engine/content-format.yaml +13 -0
- package/engine/content-index.mjs +21 -14
- package/engine/dictionary-layout.mjs +7 -3
- package/engine/dictionary-rows.mjs +6 -13
- package/engine/index-records.mjs +1 -0
- package/engine/language-data.mjs +613 -0
- package/engine/language-rows.mjs +993 -0
- package/engine/language-terms.mjs +1718 -0
- package/engine/language-words.mjs +115 -6
- package/engine/note-addresses.mjs +38 -4
- package/engine/note-vocabulary.mjs +9 -2
- package/engine/sql-tables.mjs +45 -12
- package/engine/word-citations.mjs +206 -0
- package/engine/word-rows.mjs +66 -12
- package/engine/word-terms.mjs +131 -13
- package/package.json +1 -1
- package/types/engine/dictionary-layout.d.mts +5 -3
- package/types/engine/language-data.d.mts +22 -0
- package/types/engine/language-rows.d.mts +313 -0
- package/types/engine/language-terms.d.mts +367 -0
- package/types/engine/note-addresses.d.mts +8 -10
- package/types/engine/sql-tables.d.mts +2 -2
- package/types/engine/word-citations.d.mts +43 -0
- package/types/engine/word-rows.d.mts +18 -1
- package/types/engine/word-terms.d.mts +27 -0
|
@@ -0,0 +1,993 @@
|
|
|
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 rows of the SQL relations a language note's register, sounds, history
|
|
16
|
+
* and names are read through, from index records whose Addresses are already
|
|
17
|
+
* strings.
|
|
18
|
+
*
|
|
19
|
+
* Every relation is keyed by `language`, the note that states the entry, and
|
|
20
|
+
* a list's entries by `position`, their order in it. **Every row has every
|
|
21
|
+
* column**, so a fence naming a key no entry writes still binds. A key that
|
|
22
|
+
* would share a name with `language` or `position` takes a prefix:
|
|
23
|
+
* a tongue's `language` is `tongue_language`, an element's `position` is
|
|
24
|
+
* `element_position`, and a cognate form's language is `form_language`.
|
|
25
|
+
*
|
|
26
|
+
* - `register` — one row per `data.register` entry, with `built_text` and
|
|
27
|
+
* `notes`, every note in the language's `data.scope` carrying the name — of
|
|
28
|
+
* the whole package, for a language stating no scope: as its full name,
|
|
29
|
+
* an alias or a part of its name, as a rank or an office title, or as the
|
|
30
|
+
* name of an anchor, which is cited with its anchor. A folder note carries
|
|
31
|
+
* no name;
|
|
32
|
+
* - `retired`, `tongues`, `letters`, `edges`, `read_past`, `sound_changes`,
|
|
33
|
+
* `elements` and `roots` — one row per entry, the list keys always lists;
|
|
34
|
+
* - `scope` — one row per value, with the `kind` of `data.scope` it is under;
|
|
35
|
+
* - `clusters` — one row per language stating `data.sounds.clusters`;
|
|
36
|
+
* - `cluster_groups` and `mutations` — one row per entry of
|
|
37
|
+
* `data.sounds.clusters.groups` and `data.sounds.mutations`;
|
|
38
|
+
* - `older` — one row per `data.older` entry;
|
|
39
|
+
* - `negation` — one row per language stating `data.negation`;
|
|
40
|
+
* - `name_shapes`, `name_edges`, `name_draws` and `held_from` — one row per
|
|
41
|
+
* entry of the lists of `data.naming`, and `near_names` one per worked
|
|
42
|
+
* near name, with the language's `min_syllables`;
|
|
43
|
+
* - `cognates` — one row per form of each set, with the set's `gloss`.
|
|
44
|
+
*
|
|
45
|
+
* @module
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { isLanguageRecord, noteAddressOf } from "./word-rows.mjs";
|
|
49
|
+
import {
|
|
50
|
+
ANY_NAME,
|
|
51
|
+
CALLING_CLOSING_KEYS,
|
|
52
|
+
CLUSTER_GROUP_KEYS,
|
|
53
|
+
EDGE_KEYS,
|
|
54
|
+
ELEMENT_KEYS,
|
|
55
|
+
FINAL_KEYS,
|
|
56
|
+
JOINING_KEYS,
|
|
57
|
+
JOIN_KEYS,
|
|
58
|
+
LETTER_KEYS,
|
|
59
|
+
MARK_KEYS,
|
|
60
|
+
MUTATION_KEYS,
|
|
61
|
+
OLDER_KEYS,
|
|
62
|
+
READ_PAST_KEYS,
|
|
63
|
+
REGISTER_KEYS,
|
|
64
|
+
RETIRED_KEYS,
|
|
65
|
+
ROOT_KEYS,
|
|
66
|
+
SCOPE_KINDS,
|
|
67
|
+
SKELETON_KEYS,
|
|
68
|
+
SOUND_CHANGE_STEP_KEYS,
|
|
69
|
+
TONGUE_KEYS,
|
|
70
|
+
} from "./language-terms.mjs";
|
|
71
|
+
|
|
72
|
+
/** The columns every list relation opens with. */
|
|
73
|
+
const IDENTITY = Object.freeze(["language", "position"]);
|
|
74
|
+
|
|
75
|
+
/** A key's column, prefixed where it would share an identity column's name. */
|
|
76
|
+
const column = (prefix, key) => (IDENTITY.includes(key) ? `${prefix}_${key}` : key);
|
|
77
|
+
|
|
78
|
+
/** Every column of the `register` relation, in order. */
|
|
79
|
+
export const REGISTER_COLUMNS = Object.freeze([
|
|
80
|
+
...IDENTITY,
|
|
81
|
+
...REGISTER_KEYS,
|
|
82
|
+
"built_text",
|
|
83
|
+
"notes",
|
|
84
|
+
]);
|
|
85
|
+
/** Every column of the `retired` relation, in order. */
|
|
86
|
+
export const RETIRED_COLUMNS = Object.freeze([...IDENTITY, ...RETIRED_KEYS]);
|
|
87
|
+
/** Every column of the `tongues` relation, in order. */
|
|
88
|
+
export const TONGUE_COLUMNS = Object.freeze([
|
|
89
|
+
...IDENTITY,
|
|
90
|
+
...TONGUE_KEYS.map((key) => column("tongue", key)),
|
|
91
|
+
]);
|
|
92
|
+
/** Every column of the `scope` relation, in order. */
|
|
93
|
+
export const SCOPE_COLUMNS = Object.freeze(["language", "kind", "value"]);
|
|
94
|
+
/** Every column of the `letters` relation, in order. */
|
|
95
|
+
export const LETTER_COLUMNS = Object.freeze([...IDENTITY, ...LETTER_KEYS]);
|
|
96
|
+
/** Every column of the `edges` relation, in order. */
|
|
97
|
+
export const EDGE_COLUMNS = Object.freeze([...IDENTITY, ...EDGE_KEYS]);
|
|
98
|
+
/** Every column of the `clusters` relation, in order. */
|
|
99
|
+
export const CLUSTER_COLUMNS = Object.freeze(["language", "max"]);
|
|
100
|
+
/** Every column of the `cluster_groups` relation, in order. */
|
|
101
|
+
export const CLUSTER_GROUP_COLUMNS = Object.freeze([...IDENTITY, ...CLUSTER_GROUP_KEYS]);
|
|
102
|
+
/** Every column of the `mutations` relation, in order. */
|
|
103
|
+
export const MUTATION_COLUMNS = Object.freeze([...IDENTITY, ...MUTATION_KEYS]);
|
|
104
|
+
/** Every column of the `older` relation, in order. */
|
|
105
|
+
export const OLDER_COLUMNS = Object.freeze([...IDENTITY, ...OLDER_KEYS]);
|
|
106
|
+
/** Every column of the `name_shapes` relation, in order. */
|
|
107
|
+
export const NAME_SHAPE_COLUMNS = Object.freeze([
|
|
108
|
+
...IDENTITY,
|
|
109
|
+
"of",
|
|
110
|
+
"syllables_min",
|
|
111
|
+
"syllables_max",
|
|
112
|
+
"per_vowel_min",
|
|
113
|
+
"per_vowel_max",
|
|
114
|
+
]);
|
|
115
|
+
/** Every column of the `name_edges` relation, in order. */
|
|
116
|
+
export const NAME_EDGE_COLUMNS = Object.freeze([...IDENTITY, "edge", "sounds", "vowel", "of"]);
|
|
117
|
+
/** Every column of the `name_draws` relation, in order. */
|
|
118
|
+
export const NAME_DRAW_COLUMNS = Object.freeze([...IDENTITY, "kind", "elements"]);
|
|
119
|
+
/** Every column of the `held_from` relation, in order. */
|
|
120
|
+
export const HELD_FROM_COLUMNS = Object.freeze([
|
|
121
|
+
...IDENTITY,
|
|
122
|
+
"source",
|
|
123
|
+
"takes",
|
|
124
|
+
"clip",
|
|
125
|
+
"genitive",
|
|
126
|
+
"elements",
|
|
127
|
+
]);
|
|
128
|
+
/** Every column of the `near_names` relation, in order. */
|
|
129
|
+
export const NEAR_NAME_COLUMNS = Object.freeze([...IDENTITY, "near", "given", "min_syllables"]);
|
|
130
|
+
/** Every column of the `negation` relation, in order: one row per language. */
|
|
131
|
+
export const NEGATION_COLUMNS = Object.freeze(["language", "particle", "mutation"]);
|
|
132
|
+
/** Every column of the `stress` relation, in order. */
|
|
133
|
+
export const STRESS_COLUMNS = Object.freeze(["language", "stress"]);
|
|
134
|
+
/** Every column of the `finals` relation, in order. */
|
|
135
|
+
export const FINAL_COLUMNS = Object.freeze([...IDENTITY, ...FINAL_KEYS]);
|
|
136
|
+
/** Every column of the `syllables` relation, in order: one row per language. */
|
|
137
|
+
export const SYLLABLE_COLUMNS = Object.freeze([
|
|
138
|
+
"language",
|
|
139
|
+
"onsets",
|
|
140
|
+
"codas",
|
|
141
|
+
"finals",
|
|
142
|
+
"doubled",
|
|
143
|
+
"vowelPairs",
|
|
144
|
+
"initialVowel",
|
|
145
|
+
"finalVowel",
|
|
146
|
+
"neverInitial",
|
|
147
|
+
"signature",
|
|
148
|
+
"harmony_back",
|
|
149
|
+
"harmony_front",
|
|
150
|
+
"harmony_neutral",
|
|
151
|
+
"harmony_neutralAs",
|
|
152
|
+
"harmony_partners",
|
|
153
|
+
]);
|
|
154
|
+
/** Every column of the `joining` relation, in order. */
|
|
155
|
+
export const JOINING_COLUMNS = Object.freeze([...IDENTITY, ...JOINING_KEYS]);
|
|
156
|
+
/** Every column of the `joins` relation, in order. */
|
|
157
|
+
export const JOIN_COLUMNS = Object.freeze([...IDENTITY, ...JOIN_KEYS]);
|
|
158
|
+
/** Every column of the `marks` relation, in order. */
|
|
159
|
+
export const MARK_COLUMNS = Object.freeze([...IDENTITY, ...MARK_KEYS]);
|
|
160
|
+
/** Every column of the `skeleton` relation, in order. */
|
|
161
|
+
export const SKELETON_COLUMNS = Object.freeze([...IDENTITY, ...SKELETON_KEYS]);
|
|
162
|
+
/** Every column of the `frames` relation, in order. */
|
|
163
|
+
export const FRAME_COLUMNS = Object.freeze([
|
|
164
|
+
...IDENTITY,
|
|
165
|
+
"frame",
|
|
166
|
+
"shape",
|
|
167
|
+
"class",
|
|
168
|
+
"makes",
|
|
169
|
+
"examples",
|
|
170
|
+
]);
|
|
171
|
+
/** Every column of the `read_past` relation, in order. */
|
|
172
|
+
export const READ_PAST_COLUMNS = Object.freeze([...IDENTITY, ...READ_PAST_KEYS]);
|
|
173
|
+
/** Every column of the `sound_changes` relation, in order: one row per step. */
|
|
174
|
+
export const SOUND_CHANGE_COLUMNS = Object.freeze([
|
|
175
|
+
...IDENTITY,
|
|
176
|
+
"name",
|
|
177
|
+
"from_language",
|
|
178
|
+
"to_language",
|
|
179
|
+
"takes",
|
|
180
|
+
"step_position",
|
|
181
|
+
...SOUND_CHANGE_STEP_KEYS,
|
|
182
|
+
]);
|
|
183
|
+
/** Every column of the `elements` relation, in order. */
|
|
184
|
+
export const ELEMENT_COLUMNS = Object.freeze([
|
|
185
|
+
...IDENTITY,
|
|
186
|
+
...ELEMENT_KEYS.map((key) => column("element", key)),
|
|
187
|
+
]);
|
|
188
|
+
/** Every column of the `roots` relation, in order. */
|
|
189
|
+
export const ROOT_COLUMNS = Object.freeze([...IDENTITY, ...ROOT_KEYS]);
|
|
190
|
+
/** Every column of the `cognates` relation, in order. */
|
|
191
|
+
export const COGNATE_COLUMNS = Object.freeze([
|
|
192
|
+
...IDENTITY,
|
|
193
|
+
"gloss",
|
|
194
|
+
"proto",
|
|
195
|
+
"form_language",
|
|
196
|
+
"form",
|
|
197
|
+
]);
|
|
198
|
+
/** Every column of the `calling_names` relation, in order. */
|
|
199
|
+
export const CALLING_NAME_COLUMNS = Object.freeze([
|
|
200
|
+
...IDENTITY,
|
|
201
|
+
...CALLING_CLOSING_KEYS,
|
|
202
|
+
"min_syllables",
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
/** A list of strings, as a column type. */
|
|
206
|
+
const LIST = "VARCHAR[]";
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The type of each column that holds no text, by relation, so a relation
|
|
210
|
+
* with no row still has the type a fence's list or number function binds to.
|
|
211
|
+
*
|
|
212
|
+
* @type {Readonly<Record<string, Readonly<Record<string, string>>>>}
|
|
213
|
+
*/
|
|
214
|
+
export const LANGUAGE_COLUMN_TYPES = Object.freeze({
|
|
215
|
+
register: { position: "BIGINT", built: LIST, notes: LIST },
|
|
216
|
+
retired: { position: "BIGINT", instead: LIST },
|
|
217
|
+
tongues: { position: "BIGINT" },
|
|
218
|
+
scope: {},
|
|
219
|
+
letters: { position: "BIGINT", letters: LIST, tongue: LIST },
|
|
220
|
+
edges: { position: "BIGINT", sounds: LIST },
|
|
221
|
+
clusters: { max: "BIGINT" },
|
|
222
|
+
cluster_groups: { position: "BIGINT", clusters: LIST },
|
|
223
|
+
mutations: { position: "BIGINT" },
|
|
224
|
+
older: { position: "BIGINT" },
|
|
225
|
+
negation: {},
|
|
226
|
+
name_shapes: {
|
|
227
|
+
position: "BIGINT",
|
|
228
|
+
syllables_min: "BIGINT",
|
|
229
|
+
syllables_max: "BIGINT",
|
|
230
|
+
per_vowel_min: "DOUBLE",
|
|
231
|
+
per_vowel_max: "DOUBLE",
|
|
232
|
+
},
|
|
233
|
+
name_edges: { position: "BIGINT", sounds: LIST, vowel: "BOOLEAN" },
|
|
234
|
+
name_draws: { position: "BIGINT", elements: LIST },
|
|
235
|
+
held_from: { position: "BIGINT", genitive: LIST, elements: LIST },
|
|
236
|
+
near_names: { position: "BIGINT", min_syllables: "BIGINT" },
|
|
237
|
+
stress: {},
|
|
238
|
+
finals: { position: "BIGINT" },
|
|
239
|
+
syllables: {
|
|
240
|
+
codas: LIST,
|
|
241
|
+
finals: LIST,
|
|
242
|
+
doubled: LIST,
|
|
243
|
+
vowelPairs: LIST,
|
|
244
|
+
initialVowel: "BOOLEAN",
|
|
245
|
+
finalVowel: "BOOLEAN",
|
|
246
|
+
neverInitial: LIST,
|
|
247
|
+
signature: LIST,
|
|
248
|
+
harmony_back: LIST,
|
|
249
|
+
harmony_front: LIST,
|
|
250
|
+
harmony_neutral: LIST,
|
|
251
|
+
harmony_partners: "STRUCT(vowel VARCHAR, partner VARCHAR)[]",
|
|
252
|
+
},
|
|
253
|
+
joining: { position: "BIGINT", first: LIST, second: LIST },
|
|
254
|
+
joins: { position: "BIGINT", letters: LIST },
|
|
255
|
+
marks: { position: "BIGINT", letters: LIST },
|
|
256
|
+
skeleton: { position: "BIGINT", second: LIST },
|
|
257
|
+
frames: {
|
|
258
|
+
position: "BIGINT",
|
|
259
|
+
class: LIST,
|
|
260
|
+
examples: "STRUCT(root VARCHAR, form VARCHAR)[]",
|
|
261
|
+
},
|
|
262
|
+
read_past: { position: "BIGINT" },
|
|
263
|
+
sound_changes: { position: "BIGINT", step_position: "BIGINT" },
|
|
264
|
+
elements: { position: "BIGINT", variants: LIST, examples: LIST },
|
|
265
|
+
roots: { position: "BIGINT" },
|
|
266
|
+
cognates: { position: "BIGINT" },
|
|
267
|
+
calling_names: { position: "BIGINT", doubled: "BOOLEAN", min_syllables: "BIGINT" },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/** Whether a value is a plain map. */
|
|
271
|
+
const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
272
|
+
|
|
273
|
+
/** Code-point order, so a list is the same on every machine. */
|
|
274
|
+
const compare = (a, b) =>
|
|
275
|
+
a < b ? -1
|
|
276
|
+
: a > b ? 1
|
|
277
|
+
: 0;
|
|
278
|
+
|
|
279
|
+
/** A value that may be one string or a list of them, as a list; `null` for none. */
|
|
280
|
+
const asList = (value) => {
|
|
281
|
+
if (value === undefined || value === null) return null;
|
|
282
|
+
return (Array.isArray(value) ? value : [value]).map(String);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Each language record of a set, with its Address.
|
|
287
|
+
*
|
|
288
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
289
|
+
* @returns {Array<[string, Record<string, any>]>}
|
|
290
|
+
*/
|
|
291
|
+
function languagesOf(records) {
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const record of records) {
|
|
294
|
+
if (!isLanguageRecord(record) || record.documents) continue;
|
|
295
|
+
const language = noteAddressOf(record);
|
|
296
|
+
if (language) out.push([language, record]);
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The rows of one list a language note states: one per entry, every key a
|
|
303
|
+
* column.
|
|
304
|
+
*
|
|
305
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
306
|
+
* @param {(data: object) => unknown} read - The list, from a record's `data`.
|
|
307
|
+
* @param {readonly string[]} keys - The keys an entry carries.
|
|
308
|
+
* @param {object} [options]
|
|
309
|
+
* @param {string} [options.prefix] - The prefix of a key sharing an identity
|
|
310
|
+
* column's name.
|
|
311
|
+
* @param {readonly string[]} [options.lists] - The keys always set as lists.
|
|
312
|
+
* @returns {object[]} One row per entry, in record then entry order.
|
|
313
|
+
*/
|
|
314
|
+
function listRows(records, read, keys, { prefix = "", lists = [] } = {}) {
|
|
315
|
+
const rows = [];
|
|
316
|
+
for (const [language, record] of languagesOf(records)) {
|
|
317
|
+
const entries = read(isMap(record.data) ? record.data : {});
|
|
318
|
+
if (!Array.isArray(entries)) continue;
|
|
319
|
+
entries.forEach((entry, position) => {
|
|
320
|
+
if (!isMap(entry)) return;
|
|
321
|
+
const row = { language, position };
|
|
322
|
+
for (const key of keys)
|
|
323
|
+
row[column(prefix, key)] =
|
|
324
|
+
lists.includes(key) ? asList(entry[key]) : (entry[key] ?? null);
|
|
325
|
+
rows.push(row);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return rows;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** A language's `data.sounds`, or an empty map. */
|
|
332
|
+
const soundsOf = (data) => (isMap(data.sounds) ? data.sounds : {});
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Every name a note of the package carries, with the Addresses carrying it:
|
|
336
|
+
* each string of a note's `name`, its aliases, an affiliation's rank and
|
|
337
|
+
* office titles, and the name of each anchor, cited with the anchor.
|
|
338
|
+
*
|
|
339
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
340
|
+
* @returns {Map<string, Set<string>>} By name as written, NFC.
|
|
341
|
+
*/
|
|
342
|
+
function carriedNames(records) {
|
|
343
|
+
const names = new Map();
|
|
344
|
+
const carry = (name, address) => {
|
|
345
|
+
if (typeof name !== "string" || !name.trim()) return;
|
|
346
|
+
const key = name.normalize("NFC");
|
|
347
|
+
if (!names.has(key)) names.set(key, new Set());
|
|
348
|
+
names.get(key).add(address);
|
|
349
|
+
};
|
|
350
|
+
for (const record of records) {
|
|
351
|
+
const address = noteAddressOf(record);
|
|
352
|
+
if (!address || record.documents || record.type === "folder") continue;
|
|
353
|
+
if (isMap(record.name))
|
|
354
|
+
for (const [key, value] of Object.entries(record.name)) {
|
|
355
|
+
if (key === "aliases" && Array.isArray(value))
|
|
356
|
+
for (const alias of value) carry(alias, address);
|
|
357
|
+
else carry(value, address);
|
|
358
|
+
}
|
|
359
|
+
const governance = isMap(record.data?.governance) ? record.data.governance : {};
|
|
360
|
+
for (const rank of Array.isArray(governance.ranks) ? governance.ranks : [])
|
|
361
|
+
if (isMap(rank)) carry(rank.title, address);
|
|
362
|
+
if (isMap(governance.offices))
|
|
363
|
+
for (const title of Object.keys(governance.offices)) carry(title, address);
|
|
364
|
+
for (const anchor of Array.isArray(record.anchors) ? record.anchors : [])
|
|
365
|
+
if (isMap(anchor) && typeof anchor.slug === "string")
|
|
366
|
+
carry(anchor.name, `${address}#${anchor.slug}`);
|
|
367
|
+
}
|
|
368
|
+
return names;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** A list of strings from a record value, or an empty one. */
|
|
372
|
+
const strings = (value) =>
|
|
373
|
+
(Array.isArray(value) ? value : [value]).filter((one) => typeof one === "string" && one);
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Whether a note falls in a language's `data.scope`: one of its kinds catches
|
|
377
|
+
* the note, `types` (where stated) admits its type, and `skip` does not name
|
|
378
|
+
* it. A language that states no scope covers every note.
|
|
379
|
+
*
|
|
380
|
+
* @param {Record<string, any>} record - A note's index record.
|
|
381
|
+
* @param {string} address - The note's Address.
|
|
382
|
+
* @param {object|undefined} scope - The language's `data.scope`.
|
|
383
|
+
* @returns {boolean}
|
|
384
|
+
*/
|
|
385
|
+
export function inScope(record, address, scope) {
|
|
386
|
+
if (!isMap(scope)) return true;
|
|
387
|
+
const values = (kind) => new Set(strings(scope[kind]));
|
|
388
|
+
if (values("skip").has(address)) return false;
|
|
389
|
+
const types = values("types");
|
|
390
|
+
if (types.size && !types.has(record.type)) return false;
|
|
391
|
+
const data = isMap(record.data) ? record.data : {};
|
|
392
|
+
const any = (kind, list) => {
|
|
393
|
+
const wanted = values(kind);
|
|
394
|
+
return strings(list).some((one) => wanted.has(one));
|
|
395
|
+
};
|
|
396
|
+
const file = typeof record.file?.path === "string" ? record.file.path : "";
|
|
397
|
+
return (
|
|
398
|
+
values("addresses").has(address) ||
|
|
399
|
+
any("folders", data.packFolder) ||
|
|
400
|
+
any("cultures", data.culture) ||
|
|
401
|
+
any("lore", data.lore) ||
|
|
402
|
+
any("parents", data.parents) ||
|
|
403
|
+
strings(scope.paths).some((prefix) => file.startsWith(prefix))
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The `register` rows of a set of index records.
|
|
409
|
+
*
|
|
410
|
+
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
411
|
+
* Address a string, already narrowed to the audience being published to.
|
|
412
|
+
* @param {Iterable<Record<string, any>>} [all] - Every record of the package;
|
|
413
|
+
* a note carrying a name is cited only where the audience sees it.
|
|
414
|
+
* @returns {object[]} One row per register entry, in record then entry order.
|
|
415
|
+
*/
|
|
416
|
+
export function registerRows(records, all = records) {
|
|
417
|
+
const visible = new Set();
|
|
418
|
+
for (const record of records) {
|
|
419
|
+
const key = noteAddressOf(record);
|
|
420
|
+
if (key) visible.add(key);
|
|
421
|
+
}
|
|
422
|
+
const every = [...all];
|
|
423
|
+
// The language note's own record, never the journal sharing its Address.
|
|
424
|
+
const languages = new Map(languagesOf(every));
|
|
425
|
+
const byLanguage = new Map();
|
|
426
|
+
const namesOf = (language) => {
|
|
427
|
+
if (!byLanguage.has(language)) {
|
|
428
|
+
const scope = languages.get(language)?.data?.scope;
|
|
429
|
+
byLanguage.set(
|
|
430
|
+
language,
|
|
431
|
+
carriedNames(
|
|
432
|
+
every.filter((one) => {
|
|
433
|
+
const address = noteAddressOf(one);
|
|
434
|
+
return address !== null && inScope(one, address, scope);
|
|
435
|
+
}),
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return byLanguage.get(language);
|
|
440
|
+
};
|
|
441
|
+
return listRows(records, (data) => data.register, REGISTER_KEYS, { lists: ["built"] }).map(
|
|
442
|
+
(row) => {
|
|
443
|
+
const built = row.built ?? [];
|
|
444
|
+
const names = namesOf(row.language);
|
|
445
|
+
const notes = [...(names.get(String(row.name ?? "").normalize("NFC")) ?? [])]
|
|
446
|
+
.filter((address) => visible.has(address.split("#")[0]))
|
|
447
|
+
.sort(compare);
|
|
448
|
+
return {
|
|
449
|
+
...row,
|
|
450
|
+
built_text: built.length ? built.join(" + ") : null,
|
|
451
|
+
notes: notes.length ? notes : null,
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* The `retired` rows: one per `data.retired` entry, `instead` a list.
|
|
459
|
+
*
|
|
460
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
461
|
+
* @returns {object[]}
|
|
462
|
+
*/
|
|
463
|
+
export function retiredRows(records) {
|
|
464
|
+
return listRows(records, (data) => data.retired, RETIRED_KEYS, { lists: ["instead"] });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The `tongues` rows: one per `data.tongues` entry.
|
|
469
|
+
*
|
|
470
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
471
|
+
* @returns {object[]}
|
|
472
|
+
*/
|
|
473
|
+
export function tongueRows(records) {
|
|
474
|
+
return listRows(records, (data) => data.tongues, TONGUE_KEYS, { prefix: "tongue" });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The `scope` rows: one per value of each kind of `data.scope`, in the order
|
|
479
|
+
* the kinds are declared.
|
|
480
|
+
*
|
|
481
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
482
|
+
* @returns {object[]}
|
|
483
|
+
*/
|
|
484
|
+
export function scopeRows(records) {
|
|
485
|
+
const rows = [];
|
|
486
|
+
for (const [language, record] of languagesOf(records)) {
|
|
487
|
+
const scope = isMap(record.data?.scope) ? record.data.scope : {};
|
|
488
|
+
for (const kind of SCOPE_KINDS)
|
|
489
|
+
for (const value of Array.isArray(scope[kind]) ? scope[kind] : [])
|
|
490
|
+
if (value !== null && value !== undefined)
|
|
491
|
+
rows.push({ language, kind, value: String(value) });
|
|
492
|
+
}
|
|
493
|
+
return rows;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The `letters` rows: one per `data.sounds.letters` entry, `letters` and
|
|
498
|
+
* `tongue` lists.
|
|
499
|
+
*
|
|
500
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
501
|
+
* @returns {object[]}
|
|
502
|
+
*/
|
|
503
|
+
export function letterRows(records) {
|
|
504
|
+
return listRows(records, (data) => soundsOf(data).letters, LETTER_KEYS, {
|
|
505
|
+
lists: ["letters", "tongue"],
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* The `edges` rows: one per `data.sounds.edges` entry, `sounds` a list.
|
|
511
|
+
*
|
|
512
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
513
|
+
* @returns {object[]}
|
|
514
|
+
*/
|
|
515
|
+
export function edgeRows(records) {
|
|
516
|
+
return listRows(records, (data) => soundsOf(data).edges, EDGE_KEYS, { lists: ["sounds"] });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The `clusters` rows: one per language stating `data.sounds.clusters`.
|
|
521
|
+
*
|
|
522
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
523
|
+
* @returns {object[]}
|
|
524
|
+
*/
|
|
525
|
+
export function clusterRows(records) {
|
|
526
|
+
const rows = [];
|
|
527
|
+
for (const [language, record] of languagesOf(records)) {
|
|
528
|
+
const clusters = soundsOf(isMap(record.data) ? record.data : {}).clusters;
|
|
529
|
+
if (isMap(clusters)) rows.push({ language, max: clusters.max ?? null });
|
|
530
|
+
}
|
|
531
|
+
return rows;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* The `cluster_groups` rows: one per `data.sounds.clusters.groups` entry,
|
|
536
|
+
* `clusters` a list.
|
|
537
|
+
*
|
|
538
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
539
|
+
* @returns {object[]}
|
|
540
|
+
*/
|
|
541
|
+
export function clusterGroupRows(records) {
|
|
542
|
+
return listRows(records, (data) => soundsOf(data).clusters?.groups, CLUSTER_GROUP_KEYS, {
|
|
543
|
+
lists: ["clusters"],
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* The `mutations` rows: one per `data.sounds.mutations` entry.
|
|
549
|
+
*
|
|
550
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
551
|
+
* @returns {object[]}
|
|
552
|
+
*/
|
|
553
|
+
export function mutationRows(records) {
|
|
554
|
+
return listRows(records, (data) => soundsOf(data).mutations, MUTATION_KEYS);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The `older` rows: one per `data.older` entry.
|
|
559
|
+
*
|
|
560
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
561
|
+
* @returns {object[]}
|
|
562
|
+
*/
|
|
563
|
+
export function olderRows(records) {
|
|
564
|
+
return listRows(records, (data) => data.older, OLDER_KEYS);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The `negation` rows: one per language stating `data.negation`.
|
|
569
|
+
*
|
|
570
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
571
|
+
* @returns {object[]}
|
|
572
|
+
*/
|
|
573
|
+
export function negationRows(records) {
|
|
574
|
+
const rows = [];
|
|
575
|
+
for (const [language, record] of languagesOf(records)) {
|
|
576
|
+
const negation = record.data?.negation;
|
|
577
|
+
if (isMap(negation))
|
|
578
|
+
rows.push({
|
|
579
|
+
language,
|
|
580
|
+
particle: negation.particle ?? null,
|
|
581
|
+
mutation: negation.mutation ?? null,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return rows;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** A language's `data.naming`, or an empty map. */
|
|
588
|
+
const namingOf = (data) => (isMap(data.naming) ? data.naming : {});
|
|
589
|
+
|
|
590
|
+
/** A range's bound, or `null`. */
|
|
591
|
+
const bound = (range, key) => (isMap(range) && typeof range[key] === "number" ? range[key] : null);
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The `name_shapes` rows: one per `data.naming.shapes` entry, each range as
|
|
595
|
+
* its two bounds.
|
|
596
|
+
*
|
|
597
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
598
|
+
* @returns {object[]}
|
|
599
|
+
*/
|
|
600
|
+
export function nameShapeRows(records) {
|
|
601
|
+
const rows = [];
|
|
602
|
+
for (const [language, record] of languagesOf(records)) {
|
|
603
|
+
const shapes = namingOf(isMap(record.data) ? record.data : {}).shapes;
|
|
604
|
+
if (!Array.isArray(shapes)) continue;
|
|
605
|
+
shapes.forEach((shape, position) => {
|
|
606
|
+
if (!isMap(shape)) return;
|
|
607
|
+
rows.push({
|
|
608
|
+
language,
|
|
609
|
+
position,
|
|
610
|
+
of: shape.of ?? null,
|
|
611
|
+
syllables_min: bound(shape.syllables, "min"),
|
|
612
|
+
syllables_max: bound(shape.syllables, "max"),
|
|
613
|
+
per_vowel_min: bound(shape.perVowel, "min"),
|
|
614
|
+
per_vowel_max: bound(shape.perVowel, "max"),
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return rows;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The `name_edges` rows: one per `data.naming.edges` entry, `sounds` a list,
|
|
623
|
+
* `vowel` `false` and `of` `name` where unstated.
|
|
624
|
+
*
|
|
625
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
626
|
+
* @returns {object[]}
|
|
627
|
+
*/
|
|
628
|
+
export function nameEdgeRows(records) {
|
|
629
|
+
return listRows(records, (data) => namingOf(data).edges, ["edge", "sounds", "vowel", "of"], {
|
|
630
|
+
lists: ["sounds"],
|
|
631
|
+
}).map((row) => ({ ...row, vowel: row.vowel === true, of: row.of ?? ANY_NAME }));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* The `name_draws` rows: one per `data.naming.draws` entry.
|
|
636
|
+
*
|
|
637
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
638
|
+
* @returns {object[]}
|
|
639
|
+
*/
|
|
640
|
+
export function nameDrawRows(records) {
|
|
641
|
+
return listRows(records, (data) => namingOf(data).draws, ["kind", "elements"], {
|
|
642
|
+
lists: ["elements"],
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* The `held_from` rows: one per `data.naming.heldFrom` entry.
|
|
648
|
+
*
|
|
649
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
650
|
+
* @returns {object[]}
|
|
651
|
+
*/
|
|
652
|
+
export function heldFromRows(records) {
|
|
653
|
+
return listRows(
|
|
654
|
+
records,
|
|
655
|
+
(data) => namingOf(data).heldFrom,
|
|
656
|
+
["source", "takes", "clip", "genitive", "elements"],
|
|
657
|
+
{ lists: ["genitive", "elements"] },
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* The `near_names` rows: one per worked example of `data.naming.nearName`,
|
|
663
|
+
* each with the language's `min_syllables`.
|
|
664
|
+
*
|
|
665
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
666
|
+
* @returns {object[]}
|
|
667
|
+
*/
|
|
668
|
+
export function nearNameRows(records) {
|
|
669
|
+
const rows = [];
|
|
670
|
+
for (const [language, record] of languagesOf(records)) {
|
|
671
|
+
const near = namingOf(isMap(record.data) ? record.data : {}).nearName;
|
|
672
|
+
if (!isMap(near) || !Array.isArray(near.examples)) continue;
|
|
673
|
+
near.examples.forEach((example, position) => {
|
|
674
|
+
if (!isMap(example)) return;
|
|
675
|
+
rows.push({
|
|
676
|
+
language,
|
|
677
|
+
position,
|
|
678
|
+
near: example.near ?? null,
|
|
679
|
+
given: example.given ?? null,
|
|
680
|
+
min_syllables: near.minSyllables ?? null,
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
return rows;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The `read_past` rows: one per `data.sounds.readPast` entry.
|
|
689
|
+
*
|
|
690
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
691
|
+
* @returns {object[]}
|
|
692
|
+
*/
|
|
693
|
+
export function readPastRows(records) {
|
|
694
|
+
return listRows(records, (data) => soundsOf(data).readPast, READ_PAST_KEYS);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* The `sound_changes` rows: one per step of each `data.soundChanges` set, with
|
|
699
|
+
* the set's `position`, `name`, `from_language`, `to_language` and `takes`,
|
|
700
|
+
* and the step's `step_position` within it.
|
|
701
|
+
*
|
|
702
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
703
|
+
* @returns {object[]}
|
|
704
|
+
*/
|
|
705
|
+
export function soundChangeRows(records) {
|
|
706
|
+
const rows = [];
|
|
707
|
+
for (const [language, record] of languagesOf(records)) {
|
|
708
|
+
const sets = record.data?.soundChanges;
|
|
709
|
+
if (!Array.isArray(sets)) continue;
|
|
710
|
+
sets.forEach((set, position) => {
|
|
711
|
+
if (!isMap(set) || !Array.isArray(set.steps)) return;
|
|
712
|
+
set.steps.forEach((step, stepPosition) => {
|
|
713
|
+
if (!isMap(step)) return;
|
|
714
|
+
const row = {
|
|
715
|
+
language,
|
|
716
|
+
position,
|
|
717
|
+
name: set.name ?? null,
|
|
718
|
+
from_language: set.from ?? null,
|
|
719
|
+
to_language: set.to ?? null,
|
|
720
|
+
takes: set.takes ?? null,
|
|
721
|
+
step_position: stepPosition,
|
|
722
|
+
};
|
|
723
|
+
for (const key of SOUND_CHANGE_STEP_KEYS) row[key] = step[key] ?? null;
|
|
724
|
+
rows.push(row);
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return rows;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* The `elements` rows: one per `data.elements` entry, `examples` a list.
|
|
733
|
+
*
|
|
734
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
735
|
+
* @returns {object[]}
|
|
736
|
+
*/
|
|
737
|
+
export function elementRows(records) {
|
|
738
|
+
return listRows(records, (data) => data.elements, ELEMENT_KEYS, {
|
|
739
|
+
prefix: "element",
|
|
740
|
+
lists: ["variants", "examples"],
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* The `roots` rows: one per `data.roots` entry.
|
|
746
|
+
*
|
|
747
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
748
|
+
* @returns {object[]}
|
|
749
|
+
*/
|
|
750
|
+
export function rootRows(records) {
|
|
751
|
+
return listRows(records, (data) => data.roots, ROOT_KEYS);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* The `cognates` rows: one per form of each `data.cognates` set, in the order
|
|
756
|
+
* the set writes them, with the set's `position`, `gloss` and `proto`.
|
|
757
|
+
*
|
|
758
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
759
|
+
* @returns {object[]}
|
|
760
|
+
*/
|
|
761
|
+
export function cognateRows(records) {
|
|
762
|
+
const rows = [];
|
|
763
|
+
for (const [language, record] of languagesOf(records)) {
|
|
764
|
+
const sets = record.data?.cognates;
|
|
765
|
+
if (!Array.isArray(sets)) continue;
|
|
766
|
+
sets.forEach((set, position) => {
|
|
767
|
+
if (!isMap(set) || !isMap(set.forms)) return;
|
|
768
|
+
for (const [formLanguage, form] of Object.entries(set.forms))
|
|
769
|
+
rows.push({
|
|
770
|
+
language,
|
|
771
|
+
position,
|
|
772
|
+
gloss: set.gloss ?? null,
|
|
773
|
+
proto: set.proto ?? null,
|
|
774
|
+
form_language: formLanguage,
|
|
775
|
+
form: form ?? null,
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return rows;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* The `stress` rows: one per language stating `data.sounds.stress`.
|
|
784
|
+
*
|
|
785
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
786
|
+
* @returns {object[]}
|
|
787
|
+
*/
|
|
788
|
+
export function stressRows(records) {
|
|
789
|
+
const rows = [];
|
|
790
|
+
for (const [language, record] of languagesOf(records)) {
|
|
791
|
+
const stress = soundsOf(isMap(record.data) ? record.data : {}).stress;
|
|
792
|
+
if (typeof stress === "string") rows.push({ language, stress });
|
|
793
|
+
}
|
|
794
|
+
return rows;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* The `finals` rows: one per `data.sounds.finals` entry.
|
|
799
|
+
*
|
|
800
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
801
|
+
* @returns {object[]}
|
|
802
|
+
*/
|
|
803
|
+
export function finalRows(records) {
|
|
804
|
+
return listRows(records, (data) => soundsOf(data).finals, FINAL_KEYS);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* The `syllables` rows: one per language stating `data.sounds.syllables`, a
|
|
809
|
+
* list rule always a list (`any` a list of one), the harmony's sets each a
|
|
810
|
+
* column of its own, its partners a list of `{ vowel, partner }` in the order
|
|
811
|
+
* stated.
|
|
812
|
+
*
|
|
813
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
814
|
+
* @returns {object[]}
|
|
815
|
+
*/
|
|
816
|
+
export function syllableRows(records) {
|
|
817
|
+
const rows = [];
|
|
818
|
+
for (const [language, record] of languagesOf(records)) {
|
|
819
|
+
const rules = soundsOf(isMap(record.data) ? record.data : {}).syllables;
|
|
820
|
+
if (!isMap(rules)) continue;
|
|
821
|
+
const harmony = isMap(rules.harmony) ? rules.harmony : {};
|
|
822
|
+
rows.push({
|
|
823
|
+
language,
|
|
824
|
+
onsets: rules.onsets ?? null,
|
|
825
|
+
codas: asList(rules.codas),
|
|
826
|
+
finals: asList(rules.finals),
|
|
827
|
+
doubled: asList(rules.doubled),
|
|
828
|
+
vowelPairs: asList(rules.vowelPairs),
|
|
829
|
+
initialVowel: typeof rules.initialVowel === "boolean" ? rules.initialVowel : null,
|
|
830
|
+
finalVowel: typeof rules.finalVowel === "boolean" ? rules.finalVowel : true,
|
|
831
|
+
neverInitial: asList(rules.neverInitial),
|
|
832
|
+
signature: asList(rules.signature),
|
|
833
|
+
harmony_back: asList(harmony.back),
|
|
834
|
+
harmony_front: asList(harmony.front),
|
|
835
|
+
harmony_neutral: asList(harmony.neutral),
|
|
836
|
+
harmony_neutralAs: harmony.neutralAs ?? null,
|
|
837
|
+
harmony_partners:
|
|
838
|
+
isMap(harmony.partners) && Object.keys(harmony.partners).length ?
|
|
839
|
+
Object.entries(harmony.partners).map(([vowel, partner]) => ({
|
|
840
|
+
vowel,
|
|
841
|
+
partner: partner == null ? null : String(partner),
|
|
842
|
+
}))
|
|
843
|
+
: null,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
return rows;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* The `joining` rows: one per `data.sounds.joining` entry.
|
|
851
|
+
*
|
|
852
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
853
|
+
* @returns {object[]}
|
|
854
|
+
*/
|
|
855
|
+
export function joiningRows(records) {
|
|
856
|
+
return listRows(records, (data) => soundsOf(data).joining, JOINING_KEYS, {
|
|
857
|
+
lists: ["first", "second"],
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* The `joins` rows: one per `data.sounds.joins` entry.
|
|
863
|
+
*
|
|
864
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
865
|
+
* @returns {object[]}
|
|
866
|
+
*/
|
|
867
|
+
export function joinRows(records) {
|
|
868
|
+
return listRows(records, (data) => soundsOf(data).joins, JOIN_KEYS, { lists: ["letters"] });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* The `marks` rows: one per `data.sounds.marks` entry.
|
|
873
|
+
*
|
|
874
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
875
|
+
* @returns {object[]}
|
|
876
|
+
*/
|
|
877
|
+
export function markRows(records) {
|
|
878
|
+
return listRows(records, (data) => soundsOf(data).marks, MARK_KEYS, { lists: ["letters"] });
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* The `skeleton` rows: one per `data.sounds.skeleton` entry, `second` a list.
|
|
883
|
+
*
|
|
884
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
885
|
+
* @returns {object[]}
|
|
886
|
+
*/
|
|
887
|
+
export function skeletonRows(records) {
|
|
888
|
+
return listRows(records, (data) => soundsOf(data).skeleton, SKELETON_KEYS, {
|
|
889
|
+
lists: ["second"],
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* The `frames` rows: one per `data.sounds.frames` entry, `examples` a list of
|
|
895
|
+
* `{ root, form }` in the order the frame states them.
|
|
896
|
+
*
|
|
897
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
898
|
+
* @returns {object[]}
|
|
899
|
+
*/
|
|
900
|
+
export function frameRows(records) {
|
|
901
|
+
const rows = [];
|
|
902
|
+
for (const [language, record] of languagesOf(records)) {
|
|
903
|
+
const frames = soundsOf(isMap(record.data) ? record.data : {}).frames;
|
|
904
|
+
if (!Array.isArray(frames)) continue;
|
|
905
|
+
frames.forEach((frame, position) => {
|
|
906
|
+
if (!isMap(frame)) return;
|
|
907
|
+
const examples =
|
|
908
|
+
isMap(frame.examples) ?
|
|
909
|
+
Object.entries(frame.examples).map(([root, form]) => ({
|
|
910
|
+
root,
|
|
911
|
+
form: form == null ? null : String(form),
|
|
912
|
+
}))
|
|
913
|
+
: [];
|
|
914
|
+
rows.push({
|
|
915
|
+
language,
|
|
916
|
+
position,
|
|
917
|
+
frame: frame.frame ?? null,
|
|
918
|
+
shape: frame.shape ?? null,
|
|
919
|
+
class: asList(frame.class),
|
|
920
|
+
makes: frame.makes ?? null,
|
|
921
|
+
examples: examples.length ? examples : null,
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return rows;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* The `calling_names` rows: one per `data.callingNames.closings` entry, each
|
|
930
|
+
* with the language's `min_syllables`.
|
|
931
|
+
*
|
|
932
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
933
|
+
* @returns {object[]}
|
|
934
|
+
*/
|
|
935
|
+
export function callingNameRows(records) {
|
|
936
|
+
const rows = [];
|
|
937
|
+
for (const [language, record] of languagesOf(records)) {
|
|
938
|
+
const calling = record.data?.callingNames;
|
|
939
|
+
if (!isMap(calling) || !Array.isArray(calling.closings)) continue;
|
|
940
|
+
calling.closings.forEach((closing, position) => {
|
|
941
|
+
if (!isMap(closing)) return;
|
|
942
|
+
rows.push({
|
|
943
|
+
language,
|
|
944
|
+
position,
|
|
945
|
+
form: closing.form ?? null,
|
|
946
|
+
doubled: closing.doubled === true,
|
|
947
|
+
usual: closing.usual ?? null,
|
|
948
|
+
min_syllables: calling.minSyllables ?? null,
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return rows;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Every relation of a language's register, sounds, history and names: its
|
|
957
|
+
* name, its rows from the records the audience sees and every record of the
|
|
958
|
+
* package, and its columns. {@link LANGUAGE_COLUMN_TYPES} types its columns.
|
|
959
|
+
*
|
|
960
|
+
* @type {ReadonlyArray<[string, (visible: object[], all: object[]) => object[], readonly string[]]>}
|
|
961
|
+
*/
|
|
962
|
+
export const LANGUAGE_RELATIONS = Object.freeze([
|
|
963
|
+
["register", registerRows, REGISTER_COLUMNS],
|
|
964
|
+
["retired", retiredRows, RETIRED_COLUMNS],
|
|
965
|
+
["tongues", tongueRows, TONGUE_COLUMNS],
|
|
966
|
+
["scope", scopeRows, SCOPE_COLUMNS],
|
|
967
|
+
["letters", letterRows, LETTER_COLUMNS],
|
|
968
|
+
["edges", edgeRows, EDGE_COLUMNS],
|
|
969
|
+
["clusters", clusterRows, CLUSTER_COLUMNS],
|
|
970
|
+
["cluster_groups", clusterGroupRows, CLUSTER_GROUP_COLUMNS],
|
|
971
|
+
["mutations", mutationRows, MUTATION_COLUMNS],
|
|
972
|
+
["stress", stressRows, STRESS_COLUMNS],
|
|
973
|
+
["finals", finalRows, FINAL_COLUMNS],
|
|
974
|
+
["syllables", syllableRows, SYLLABLE_COLUMNS],
|
|
975
|
+
["joining", joiningRows, JOINING_COLUMNS],
|
|
976
|
+
["joins", joinRows, JOIN_COLUMNS],
|
|
977
|
+
["marks", markRows, MARK_COLUMNS],
|
|
978
|
+
["skeleton", skeletonRows, SKELETON_COLUMNS],
|
|
979
|
+
["frames", frameRows, FRAME_COLUMNS],
|
|
980
|
+
["read_past", readPastRows, READ_PAST_COLUMNS],
|
|
981
|
+
["sound_changes", soundChangeRows, SOUND_CHANGE_COLUMNS],
|
|
982
|
+
["elements", elementRows, ELEMENT_COLUMNS],
|
|
983
|
+
["roots", rootRows, ROOT_COLUMNS],
|
|
984
|
+
["cognates", cognateRows, COGNATE_COLUMNS],
|
|
985
|
+
["calling_names", callingNameRows, CALLING_NAME_COLUMNS],
|
|
986
|
+
["older", olderRows, OLDER_COLUMNS],
|
|
987
|
+
["negation", negationRows, NEGATION_COLUMNS],
|
|
988
|
+
["name_shapes", nameShapeRows, NAME_SHAPE_COLUMNS],
|
|
989
|
+
["name_edges", nameEdgeRows, NAME_EDGE_COLUMNS],
|
|
990
|
+
["name_draws", nameDrawRows, NAME_DRAW_COLUMNS],
|
|
991
|
+
["held_from", heldFromRows, HELD_FROM_COLUMNS],
|
|
992
|
+
["near_names", nearNameRows, NEAR_NAME_COLUMNS],
|
|
993
|
+
]);
|