@heroiclands/package-build 23.7.0 → 23.8.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +32 -0
  3. package/bin/content-commands.mjs +20 -6
  4. package/ci/check-banners.mjs +37 -19
  5. package/container.mjs +48 -10
  6. package/docs/api.md +87 -43
  7. package/docs/authoring/frontmatter.md +30 -1
  8. package/docs/authoring/links-and-markup.md +229 -36
  9. package/docs/commands.md +12 -2
  10. package/docs/content-format.md +24 -1
  11. package/docs/design.md +31 -0
  12. package/docs/guides/e2e.md +37 -1
  13. package/docs/project-setup.md +30 -15
  14. package/docs/reference/format-details.md +418 -220
  15. package/docs/reference/note-types.md +90 -31
  16. package/e2e.mjs +89 -2
  17. package/engine/base-compiler.mjs +11 -18
  18. package/engine/compile-corpus.mjs +2 -1
  19. package/engine/content-alerts.mjs +4 -2
  20. package/engine/content-blocks.mjs +18 -36
  21. package/engine/content-format.yaml +3 -0
  22. package/engine/content-index.mjs +35 -22
  23. package/engine/content-poetry.mjs +9 -0
  24. package/engine/diagnostics.mjs +24 -11
  25. package/engine/frontmatter-lint.mjs +1 -1
  26. package/engine/generate.mjs +12 -22
  27. package/engine/helpers.mjs +8 -6
  28. package/engine/infobox.mjs +3 -0
  29. package/engine/journals.mjs +11 -1
  30. package/engine/language-data.mjs +236 -1
  31. package/engine/language-rows.mjs +235 -3
  32. package/engine/language-terms.mjs +524 -9
  33. package/engine/language-words.mjs +75 -0
  34. package/engine/markdownlint-run.mjs +12 -2
  35. package/engine/markup-validation.mjs +34 -0
  36. package/engine/note-addresses.mjs +1 -0
  37. package/engine/note-claims.mjs +19 -10
  38. package/engine/note-package.mjs +3 -5
  39. package/engine/note-vocabulary.mjs +29 -1
  40. package/engine/pdf-build.mjs +8 -1
  41. package/engine/prose-lint.mjs +12 -6
  42. package/engine/related-pages.mjs +22 -3
  43. package/engine/see-also.mjs +150 -0
  44. package/engine/site-build.mjs +5 -1
  45. package/engine/site-preview.mjs +4 -2
  46. package/engine/sql-tables.mjs +37 -12
  47. package/engine/standings.mjs +14 -12
  48. package/engine/word-rows.mjs +8 -3
  49. package/engine/word-terms.mjs +60 -2
  50. package/hugo-theme/layouts/partials/related.html +25 -8
  51. package/package.json +2 -1
  52. package/types/container.d.mts +4 -3
  53. package/types/e2e.d.mts +28 -0
  54. package/types/engine/base-compiler.d.mts +2 -2
  55. package/types/engine/compile-corpus.d.mts +2 -1
  56. package/types/engine/content-index.d.mts +20 -7
  57. package/types/engine/diagnostics.d.mts +7 -6
  58. package/types/engine/helpers.d.mts +7 -4
  59. package/types/engine/language-rows.d.mts +89 -3
  60. package/types/engine/language-terms.d.mts +106 -4
  61. package/types/engine/markdownlint-run.d.mts +3 -1
  62. package/types/engine/markup-validation.d.mts +14 -0
  63. package/types/engine/note-addresses.d.mts +10 -0
  64. package/types/engine/note-claims.d.mts +7 -1
  65. package/types/engine/note-package.d.mts +3 -1
  66. package/types/engine/note-vocabulary.d.mts +1 -0
  67. package/types/engine/related-pages.d.mts +10 -1
  68. package/types/engine/see-also.d.mts +55 -0
  69. package/types/engine/sql-tables.d.mts +7 -3
  70. package/types/engine/word-rows.d.mts +2 -1
  71. package/types/engine/word-terms.d.mts +11 -0
@@ -25,6 +25,11 @@
25
25
  * `data.tongues`, `translation` apart: `exonym:vylari` is declared by
26
26
  * `exonym:<tongue>`;
27
27
  * - an edge stands once in `data.sounds.edges`;
28
+ * - a sound stands once in each mutation of `data.sounds.mutations`;
29
+ * - a cluster stands in one group of `data.sounds.clusters.groups`, and a
30
+ * language stating groups states no `innerCluster` edge;
31
+ * - every vowel `data.sounds.syllables.harmony.partners` keys is a vowel of
32
+ * the harmony, and every partner one of its `front` vowels;
28
33
  * - every Address in `data.scope` resolves, and every type it names is a note
29
34
  * type;
30
35
  * - every language a cognate's `forms` names is a language note;
@@ -32,7 +37,15 @@
32
37
  * - a sound-change set's name stands once in the note, a step's label once
33
38
  * in its set, and its `from` and `to` name language notes;
34
39
  * - a frame's classes are bases of the master word classes;
35
- * - a calling-name closing states a `form` or `doubled: true`, not both.
40
+ * - a calling-name closing states a `form` or `doubled: true`, not both;
41
+ * - a form stands once in `data.older`;
42
+ * - the mutation `data.negation` names is one of `data.sounds.mutations`;
43
+ * - in `data.naming`, a class of name has one shape, which states its
44
+ * syllables, its consonants per vowel or both, each range's `min` no
45
+ * higher than its `max`; an edge stands once for each class of name; a kind
46
+ * of name has one set of draws; and a held-from row takes `elements` only
47
+ * where it takes an `element`, and is clipped or given a genitive only
48
+ * where it takes a name.
36
49
  *
37
50
  * @module
38
51
  */
@@ -43,6 +56,9 @@ import { addressKeyOf } from "./culture-reach.mjs";
43
56
  import { AddressEntries } from "./address-values.mjs";
44
57
  import { isAddressTuple, renderAddress } from "./address.mjs";
45
58
  import {
59
+ ANY_NAME,
60
+ HELD_NAMES,
61
+ HELD_TAKES,
46
62
  LANGUAGE_DATA_KEYS,
47
63
  RESERVED_TONGUE,
48
64
  SCOPE_KINDS,
@@ -287,6 +303,80 @@ export function checkLanguageData(note, { index, types } = {}) {
287
303
  else edgeAt.set(row.edge, i);
288
304
  });
289
305
 
306
+ if (isMap(sounds.clusters) && Array.isArray(sounds.clusters.groups))
307
+ listOf(sounds.edges).forEach((row, i) => {
308
+ if (!isMap(row) || row.edge !== "innerCluster") return;
309
+ findings.push(
310
+ finding(
311
+ ["data", "sounds", "edges", i, "edge"],
312
+ "error",
313
+ "`data.sounds.clusters.groups` states the clusters inside a word, so no " +
314
+ "`innerCluster` edge does",
315
+ ),
316
+ );
317
+ });
318
+ const clusterAt = new Map();
319
+ listOf(sounds.clusters?.groups).forEach((group, i) => {
320
+ if (!isMap(group)) return;
321
+ listOf(group.clusters).forEach((cluster, j) => {
322
+ if (typeof cluster !== "string") return;
323
+ if (clusterAt.has(cluster))
324
+ findings.push(
325
+ finding(
326
+ ["data", "sounds", "clusters", "groups", i, "clusters", j],
327
+ "error",
328
+ `the cluster \`${cluster}\` stands twice, first in ` +
329
+ `\`data.sounds.clusters.groups[${clusterAt.get(cluster)}]\``,
330
+ ),
331
+ );
332
+ else clusterAt.set(cluster, i);
333
+ });
334
+ });
335
+
336
+ const mutationAt = new Map();
337
+ listOf(sounds.mutations).forEach((row, i) => {
338
+ if (!isMap(row) || typeof row.mutation !== "string" || typeof row.sound !== "string")
339
+ return;
340
+ const key = JSON.stringify([row.mutation, row.sound]);
341
+ if (mutationAt.has(key))
342
+ findings.push(
343
+ finding(
344
+ ["data", "sounds", "mutations", i, "sound"],
345
+ "error",
346
+ `the sound \`${row.sound}\` stands twice in the mutation \`${row.mutation}\`, ` +
347
+ `first at \`data.sounds.mutations[${mutationAt.get(key)}]\``,
348
+ ),
349
+ );
350
+ else mutationAt.set(key, i);
351
+ });
352
+
353
+ const harmony = isMap(sounds.syllables?.harmony) ? sounds.syllables.harmony : {};
354
+ if (isMap(harmony.partners)) {
355
+ const front = new Set(listOf(harmony.front));
356
+ const vowels = new Set([...listOf(harmony.back), ...front, ...listOf(harmony.neutral)]);
357
+ for (const [vowel, partner] of Object.entries(harmony.partners)) {
358
+ const path = ["data", "sounds", "syllables", "harmony", "partners", vowel];
359
+ if (!vowels.has(vowel))
360
+ findings.push(
361
+ finding(
362
+ path,
363
+ "error",
364
+ `\`${vowel}\` is not a vowel of \`data.sounds.syllables.harmony\``,
365
+ true,
366
+ ),
367
+ );
368
+ else if (typeof partner === "string" && !front.has(partner))
369
+ findings.push(
370
+ finding(
371
+ path,
372
+ "error",
373
+ `the partner \`${partner}\` of \`${vowel}\` is not one of the harmony's ` +
374
+ "`front` vowels",
375
+ ),
376
+ );
377
+ }
378
+ }
379
+
290
380
  listOf(sounds.frames).forEach((row, i) => {
291
381
  if (!isMap(row)) return;
292
382
  listOf(row.class).forEach((base, j) => {
@@ -351,6 +441,151 @@ export function checkLanguageData(note, { index, types } = {}) {
351
441
  );
352
442
  });
353
443
 
444
+ // ── Older forms and negation ─────────────────────────────────────────────
445
+ const olderAt = new Map();
446
+ listOf(data.older).forEach((row, i) => {
447
+ if (!isMap(row) || typeof row.form !== "string") return;
448
+ if (olderAt.has(row.form))
449
+ findings.push(
450
+ finding(
451
+ ["data", "older", i, "form"],
452
+ "error",
453
+ `the form \`${row.form}\` stands twice, first at \`data.older[${olderAt.get(row.form)}]\``,
454
+ ),
455
+ );
456
+ else olderAt.set(row.form, i);
457
+ });
458
+ const negation = isMap(data.negation) ? data.negation : {};
459
+ if (typeof negation.mutation === "string") {
460
+ const named = new Set(
461
+ listOf(sounds.mutations)
462
+ .filter((row) => isMap(row) && typeof row.mutation === "string")
463
+ .map((row) => row.mutation),
464
+ );
465
+ if (!named.has(negation.mutation))
466
+ findings.push(
467
+ finding(
468
+ ["data", "negation", "mutation"],
469
+ "error",
470
+ `the mutation \`${negation.mutation}\` is not one of \`data.sounds.mutations\``,
471
+ ),
472
+ );
473
+ }
474
+
475
+ // ── Naming ───────────────────────────────────────────────────────────────
476
+ const naming = isMap(data.naming) ? data.naming : {};
477
+ /**
478
+ * A finding at the second of two entries sharing a key.
479
+ *
480
+ * @param {string} list - The list under `data.naming`.
481
+ * @param {(row: object) => string|undefined} keyOf - The entry's key.
482
+ * @param {(row: object, first: number) => [Array<string|number>, string]} report
483
+ * - Where the second sits, relative to its entry, and what is said.
484
+ */
485
+ const once = (list, keyOf, report) => {
486
+ const seen = new Map();
487
+ listOf(naming[list]).forEach((row, i) => {
488
+ if (!isMap(row)) return;
489
+ const key = keyOf(row);
490
+ if (key === undefined) return;
491
+ if (seen.has(key)) {
492
+ const [rest, message] = report(row, seen.get(key));
493
+ findings.push(finding(["data", "naming", list, i, ...rest], "error", message));
494
+ } else seen.set(key, i);
495
+ });
496
+ };
497
+ once(
498
+ "shapes",
499
+ (row) => (typeof row.of === "string" ? row.of : undefined),
500
+ (row, first) => [
501
+ ["of"],
502
+ `the class of name \`${row.of}\` has its shape stated twice, first at ` +
503
+ `\`data.naming.shapes[${first}]\``,
504
+ ],
505
+ );
506
+ once(
507
+ "edges",
508
+ (row) =>
509
+ typeof row.edge === "string" ?
510
+ JSON.stringify([row.edge, row.of ?? ANY_NAME])
511
+ : undefined,
512
+ (row, first) => [
513
+ ["edge"],
514
+ `the edge \`${row.edge}\` of \`${row.of ?? ANY_NAME}\` stands twice, first at ` +
515
+ `\`data.naming.edges[${first}]\``,
516
+ ],
517
+ );
518
+ once(
519
+ "draws",
520
+ (row) => (typeof row.kind === "string" ? row.kind : undefined),
521
+ (row, first) => [
522
+ ["kind"],
523
+ `the kind \`${row.kind}\` has its draws stated twice, first at ` +
524
+ `\`data.naming.draws[${first}]\``,
525
+ ],
526
+ );
527
+ listOf(naming.shapes).forEach((row, i) => {
528
+ if (!isMap(row)) return;
529
+ const at = (...rest) => ["data", "naming", "shapes", i, ...rest];
530
+ if (row.syllables == null && row.perVowel == null)
531
+ findings.push(
532
+ finding(
533
+ at(),
534
+ "error",
535
+ `\`data.naming.shapes[${i}]\` states neither \`syllables\` nor \`perVowel\``,
536
+ ),
537
+ );
538
+ for (const key of ["syllables", "perVowel"]) {
539
+ const range = row[key];
540
+ if (!isMap(range) || typeof range.min !== "number" || typeof range.max !== "number")
541
+ continue;
542
+ if (range.min > range.max)
543
+ findings.push(
544
+ finding(
545
+ at(key),
546
+ "error",
547
+ `\`data.naming.shapes[${i}].${key}\` runs from ${range.min} to ${range.max}; ` +
548
+ "its `min` is above its `max`",
549
+ ),
550
+ );
551
+ }
552
+ });
553
+ listOf(naming.heldFrom).forEach((row, i) => {
554
+ if (!isMap(row) || !HELD_TAKES.includes(row.takes)) return;
555
+ const at = (...rest) => ["data", "naming", "heldFrom", i, ...rest];
556
+ const label = `\`data.naming.heldFrom[${i}]\``;
557
+ if (row.takes === "element") {
558
+ if (!listOf(row.elements).length)
559
+ findings.push(
560
+ finding(
561
+ at(),
562
+ "error",
563
+ `${label} takes an \`element\` and names no \`elements\``,
564
+ ),
565
+ );
566
+ } else if (row.elements != null)
567
+ findings.push(
568
+ finding(
569
+ at("elements"),
570
+ "error",
571
+ `\`elements\` lists what a place takes first, and ${label} takes a ` +
572
+ `\`${row.takes}\``,
573
+ true,
574
+ ),
575
+ );
576
+ if (!HELD_NAMES.includes(row.takes))
577
+ for (const key of ["clip", "genitive"])
578
+ if (row[key] != null)
579
+ findings.push(
580
+ finding(
581
+ at(key),
582
+ "error",
583
+ `\`${key}\` shapes a name, and ${label} takes a \`${row.takes}\``,
584
+ true,
585
+ ),
586
+ );
587
+ });
588
+
354
589
  // ── Scope ────────────────────────────────────────────────────────────────
355
590
  const scope = isMap(data.scope) ? data.scope : {};
356
591
  for (const kind of SCOPE_KINDS) {
@@ -33,6 +33,13 @@
33
33
  * `elements` and `roots` — one row per entry, the list keys always lists;
34
34
  * - `scope` — one row per value, with the `kind` of `data.scope` it is under;
35
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`;
36
43
  * - `cognates` — one row per form of each set, with the set's `gloss`.
37
44
  *
38
45
  * @module
@@ -40,7 +47,9 @@
40
47
 
41
48
  import { isLanguageRecord, noteAddressOf } from "./word-rows.mjs";
42
49
  import {
50
+ ANY_NAME,
43
51
  CALLING_CLOSING_KEYS,
52
+ CLUSTER_GROUP_KEYS,
44
53
  EDGE_KEYS,
45
54
  ELEMENT_KEYS,
46
55
  FINAL_KEYS,
@@ -48,6 +57,8 @@ import {
48
57
  JOIN_KEYS,
49
58
  LETTER_KEYS,
50
59
  MARK_KEYS,
60
+ MUTATION_KEYS,
61
+ OLDER_KEYS,
51
62
  READ_PAST_KEYS,
52
63
  REGISTER_KEYS,
53
64
  RETIRED_KEYS,
@@ -86,6 +97,38 @@ export const LETTER_COLUMNS = Object.freeze([...IDENTITY, ...LETTER_KEYS]);
86
97
  export const EDGE_COLUMNS = Object.freeze([...IDENTITY, ...EDGE_KEYS]);
87
98
  /** Every column of the `clusters` relation, in order. */
88
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"]);
89
132
  /** Every column of the `stress` relation, in order. */
90
133
  export const STRESS_COLUMNS = Object.freeze(["language", "stress"]);
91
134
  /** Every column of the `finals` relation, in order. */
@@ -106,6 +149,7 @@ export const SYLLABLE_COLUMNS = Object.freeze([
106
149
  "harmony_front",
107
150
  "harmony_neutral",
108
151
  "harmony_neutralAs",
152
+ "harmony_partners",
109
153
  ]);
110
154
  /** Every column of the `joining` relation, in order. */
111
155
  export const JOINING_COLUMNS = Object.freeze([...IDENTITY, ...JOINING_KEYS]);
@@ -132,6 +176,7 @@ export const SOUND_CHANGE_COLUMNS = Object.freeze([
132
176
  "name",
133
177
  "from_language",
134
178
  "to_language",
179
+ "takes",
135
180
  "step_position",
136
181
  ...SOUND_CHANGE_STEP_KEYS,
137
182
  ]);
@@ -174,6 +219,21 @@ export const LANGUAGE_COLUMN_TYPES = Object.freeze({
174
219
  letters: { position: "BIGINT", letters: LIST, tongue: LIST },
175
220
  edges: { position: "BIGINT", sounds: LIST },
176
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" },
177
237
  stress: {},
178
238
  finals: { position: "BIGINT" },
179
239
  syllables: {
@@ -188,6 +248,7 @@ export const LANGUAGE_COLUMN_TYPES = Object.freeze({
188
248
  harmony_back: LIST,
189
249
  harmony_front: LIST,
190
250
  harmony_neutral: LIST,
251
+ harmony_partners: "STRUCT(vowel VARCHAR, partner VARCHAR)[]",
191
252
  },
192
253
  joining: { position: "BIGINT", first: LIST, second: LIST },
193
254
  joins: { position: "BIGINT", letters: LIST },
@@ -470,6 +531,159 @@ export function clusterRows(records) {
470
531
  return rows;
471
532
  }
472
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
+
473
687
  /**
474
688
  * The `read_past` rows: one per `data.sounds.readPast` entry.
475
689
  *
@@ -482,8 +696,8 @@ export function readPastRows(records) {
482
696
 
483
697
  /**
484
698
  * The `sound_changes` rows: one per step of each `data.soundChanges` set, with
485
- * the set's `position`, `name`, `from_language` and `to_language`, and the
486
- * step's `step_position` within it.
699
+ * the set's `position`, `name`, `from_language`, `to_language` and `takes`,
700
+ * and the step's `step_position` within it.
487
701
  *
488
702
  * @param {Iterable<Record<string, any>>} records - Index records.
489
703
  * @returns {object[]}
@@ -503,6 +717,7 @@ export function soundChangeRows(records) {
503
717
  name: set.name ?? null,
504
718
  from_language: set.from ?? null,
505
719
  to_language: set.to ?? null,
720
+ takes: set.takes ?? null,
506
721
  step_position: stepPosition,
507
722
  };
508
723
  for (const key of SOUND_CHANGE_STEP_KEYS) row[key] = step[key] ?? null;
@@ -592,7 +807,8 @@ export function finalRows(records) {
592
807
  /**
593
808
  * The `syllables` rows: one per language stating `data.sounds.syllables`, a
594
809
  * list rule always a list (`any` a list of one), the harmony's sets each a
595
- * column of its own.
810
+ * column of its own, its partners a list of `{ vowel, partner }` in the order
811
+ * stated.
596
812
  *
597
813
  * @param {Iterable<Record<string, any>>} records - Index records.
598
814
  * @returns {object[]}
@@ -618,6 +834,13 @@ export function syllableRows(records) {
618
834
  harmony_front: asList(harmony.front),
619
835
  harmony_neutral: asList(harmony.neutral),
620
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,
621
844
  });
622
845
  }
623
846
  return rows;
@@ -744,6 +967,8 @@ export const LANGUAGE_RELATIONS = Object.freeze([
744
967
  ["letters", letterRows, LETTER_COLUMNS],
745
968
  ["edges", edgeRows, EDGE_COLUMNS],
746
969
  ["clusters", clusterRows, CLUSTER_COLUMNS],
970
+ ["cluster_groups", clusterGroupRows, CLUSTER_GROUP_COLUMNS],
971
+ ["mutations", mutationRows, MUTATION_COLUMNS],
747
972
  ["stress", stressRows, STRESS_COLUMNS],
748
973
  ["finals", finalRows, FINAL_COLUMNS],
749
974
  ["syllables", syllableRows, SYLLABLE_COLUMNS],
@@ -758,4 +983,11 @@ export const LANGUAGE_RELATIONS = Object.freeze([
758
983
  ["roots", rootRows, ROOT_COLUMNS],
759
984
  ["cognates", cognateRows, COGNATE_COLUMNS],
760
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],
761
993
  ]);