@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.
@@ -24,9 +24,12 @@
24
24
  * binds.
25
25
  *
26
26
  * A word row holds the words its own note states, and nothing is copied for a
27
- * daughter language. `language_lineage` is one row per language and each of
28
- * its ancestors through `data.family`, itself included at depth `0`, so a
29
- * language's words with its inherited ones are a join of the two.
27
+ * daughter language. Its `gloss` is the word's senses joined by `; `, its
28
+ * `senses` each sense in order, and its `attested` the notes whose prose cites
29
+ * it ({@link module:engine/word-citations}), or `NULL` where none does.
30
+ * `language_lineage` is one row per language and each of its ancestors through
31
+ * `data.family`, itself included at depth `0`, so a language's words with its
32
+ * inherited ones are a join of the two.
30
33
  *
31
34
  * @module
32
35
  */
@@ -35,17 +38,20 @@ import { renderAddress } from "./address.mjs";
35
38
  import { glossaryLanguage } from "./glossary-language.mjs";
36
39
  import {
37
40
  AFFIX_KEYS,
41
+ ZERO_AFFIX,
38
42
  FIELD_KEYS,
39
43
  GLOSSARY_ENTRY_KEYS,
40
44
  LANGUAGE_SUBTYPE,
41
45
  WORD_KEYS,
46
+ glossSenses,
47
+ glossText,
42
48
  } from "./word-terms.mjs";
43
49
 
44
50
  /** The columns a word row carries before the word's own keys. */
45
51
  const WORD_IDENTITY_COLUMNS = Object.freeze(["language", "position"]);
46
52
 
47
53
  /** The columns a word row derives after the word's own keys. */
48
- const WORD_DERIVED_COLUMNS = Object.freeze(["built_text", "built_parts"]);
54
+ const WORD_DERIVED_COLUMNS = Object.freeze(["built_text", "built_parts", "senses", "attested"]);
49
55
 
50
56
  /** Every column of the `words` relation, in order. */
51
57
  export const WORD_COLUMNS = Object.freeze([
@@ -58,7 +64,7 @@ export const WORD_COLUMNS = Object.freeze([
58
64
  export const GLOSSARY_COLUMNS = Object.freeze(["note", "position", ...GLOSSARY_ENTRY_KEYS]);
59
65
 
60
66
  /** Every column of the `affixes` relation, in order. */
61
- export const AFFIX_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...AFFIX_KEYS]);
67
+ export const AFFIX_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...AFFIX_KEYS, "written"]);
62
68
 
63
69
  /** Every column of the `fields` relation, in order. */
64
70
  export const FIELD_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...FIELD_KEYS]);
@@ -94,10 +100,11 @@ export function pieceGlosses(...datas) {
94
100
  const loose = new Map();
95
101
  const take = (entries) => {
96
102
  for (const entry of Array.isArray(entries) ? entries : []) {
97
- if (!isMap(entry) || typeof entry.form !== "string" || typeof entry.gloss !== "string")
98
- continue;
99
- if (!exact.has(entry.form)) exact.set(entry.form, entry.gloss);
100
- if (!loose.has(bare(entry.form))) loose.set(bare(entry.form), entry.gloss);
103
+ if (!isMap(entry) || typeof entry.form !== "string") continue;
104
+ const gloss = glossText(entry.gloss);
105
+ if (gloss === null) continue;
106
+ if (!exact.has(entry.form)) exact.set(entry.form, gloss);
107
+ if (!loose.has(bare(entry.form))) loose.set(bare(entry.form), gloss);
101
108
  }
102
109
  };
103
110
  for (const data of datas) {
@@ -175,7 +182,7 @@ export function isLanguageRecord(record) {
175
182
  * @param {Record<string, any>} record - An index record.
176
183
  * @returns {string|null} The Address, or `null` for a record naming no note.
177
184
  */
178
- function noteAddressOf(record) {
185
+ export function noteAddressOf(record) {
179
186
  if (!record?.package || !record.type || !record.shortcode) return null;
180
187
  return renderAddress({
181
188
  package: String(record.package),
@@ -185,6 +192,31 @@ function noteAddressOf(record) {
185
192
  });
186
193
  }
187
194
 
195
+ /**
196
+ * A lookup of the notes citing a word, from records that carry `citations`.
197
+ *
198
+ * @param {Iterable<Record<string, any>>} records - Index records.
199
+ * @returns {(language: string, form: string) => string[]} The citing notes;
200
+ * empty where none cites the word.
201
+ */
202
+ export function citationLookup(records) {
203
+ const byLanguage = new Map();
204
+ for (const record of records) {
205
+ if (!isLanguageRecord(record) || !Array.isArray(record.citations)) continue;
206
+ const language = noteAddressOf(record);
207
+ if (!language) continue;
208
+ const words = new Map();
209
+ for (const entry of record.citations)
210
+ if (isMap(entry) && typeof entry.form === "string" && Array.isArray(entry.notes))
211
+ words.set(
212
+ entry.form,
213
+ entry.notes.filter((note) => typeof note === "string"),
214
+ );
215
+ byLanguage.set(language, words);
216
+ }
217
+ return (language, form) => byLanguage.get(language)?.get(form) ?? [];
218
+ }
219
+
188
220
  /**
189
221
  * The `words` rows of a set of index records.
190
222
  *
@@ -198,6 +230,17 @@ function noteAddressOf(record) {
198
230
  */
199
231
  export function wordRows(records, all = records) {
200
232
  const { languages, chain } = languageLineages(all);
233
+ const citedBy = citationLookup(all);
234
+ const visible = new Set();
235
+ for (const record of records) {
236
+ const key = noteAddressOf(record);
237
+ if (key) visible.add(key);
238
+ }
239
+ const known = new Set();
240
+ for (const record of all) {
241
+ const key = noteAddressOf(record);
242
+ if (key) known.add(key);
243
+ }
201
244
  const rows = [];
202
245
  for (const record of records) {
203
246
  if (!isLanguageRecord(record)) continue;
@@ -215,6 +258,13 @@ export function wordRows(records, all = records) {
215
258
  row.built_text = built.length ? built.join(" + ") : null;
216
259
  row.built_parts =
217
260
  built.length ? built.map((form) => ({ form, gloss: glossOf(form) })) : null;
261
+ row.gloss = glossText(entry.gloss);
262
+ row.senses = glossSenses(entry.gloss);
263
+ // A citing note the audience does not see is left out, as the note is.
264
+ const attested = citedBy(language, entry.form).filter(
265
+ (note) => visible.has(note) || !known.has(note),
266
+ );
267
+ row.attested = attested.length ? attested : null;
218
268
  rows.push(row);
219
269
  });
220
270
  }
@@ -250,14 +300,18 @@ function languageListRows(records, list, keys) {
250
300
 
251
301
  /**
252
302
  * The `affixes` rows of a set of index records: one per `data.affixes` entry
253
- * of a language note, which is the row's `language`.
303
+ * of a language note, which is the row's `language`, with `written`, the form
304
+ * as a table prints it: `∅` for a zero affix, whose `form` is `NULL`.
254
305
  *
255
306
  * @param {Iterable<Record<string, any>>} records - Index records, every
256
307
  * Address a string, already narrowed to the audience being published to.
257
308
  * @returns {object[]} One row per affix, in record then entry order.
258
309
  */
259
310
  export function affixRows(records) {
260
- return languageListRows(records, "affixes", AFFIX_KEYS);
311
+ return languageListRows(records, "affixes", AFFIX_KEYS).map((row) => ({
312
+ ...row,
313
+ written: row.zero === true ? ZERO_AFFIX : row.form,
314
+ }));
261
315
  }
262
316
 
263
317
  /**
@@ -44,6 +44,30 @@ export const WORD_SOURCE_KINDS = Object.freeze([
44
44
  "island",
45
45
  ]);
46
46
 
47
+ /**
48
+ * What an affix makes: a case, a word derived from another, a verb's tense,
49
+ * aspect, mood or voice, a given name's ending, or a lineage name's ending.
50
+ */
51
+ export const AFFIX_KINDS = Object.freeze([
52
+ "case",
53
+ "derivation",
54
+ "tense",
55
+ "aspect",
56
+ "mood",
57
+ "voice",
58
+ "name",
59
+ "lineage",
60
+ ]);
61
+
62
+ /** The affix kind whose ending says who the name is given to. */
63
+ export const NAME_AFFIX_KIND = "name";
64
+
65
+ /** How a zero affix is written where a table prints it. */
66
+ export const ZERO_AFFIX = "∅";
67
+
68
+ /** Who a given name's ending is given to. */
69
+ export const AFFIX_GIVEN = Object.freeze(["man", "woman", "either"]);
70
+
47
71
  /** The source kind that requires `language`. */
48
72
  export const BORROWED_SOURCE_KIND = "borrowed";
49
73
 
@@ -88,6 +112,15 @@ export const WORD_FIELDS = Object.freeze([
88
112
  shape: "the word as cited, bare",
89
113
  describe: "The word as cited, with no backticks.",
90
114
  }),
115
+ Object.freeze({
116
+ name: "variants",
117
+ kind: "list",
118
+ shape: "a list of forms",
119
+ entries: Object.freeze({ kind: "string", shape: "a form, bare" }),
120
+ describe:
121
+ "The word's other forms, such as a long form beside a short one; a marked use of " +
122
+ "one cites the word.",
123
+ }),
91
124
  Object.freeze({
92
125
  name: "name_sort",
93
126
  kind: "string",
@@ -112,10 +145,12 @@ export const WORD_FIELDS = Object.freeze([
112
145
  }),
113
146
  Object.freeze({
114
147
  name: "gloss",
115
- kind: "string",
148
+ kind: "string-or-list",
116
149
  required: true,
117
- shape: "a string",
118
- describe: "The meaning, in brief.",
150
+ shape: "a string, or a list of senses",
151
+ entries: Object.freeze({ kind: "string", shape: "a sense" }),
152
+ describe:
153
+ "The meaning, in brief; a list states one sense per entry, which a dictionary numbers.",
119
154
  }),
120
155
  Object.freeze({
121
156
  name: "source",
@@ -134,11 +169,11 @@ export const WORD_FIELDS = Object.freeze([
134
169
  "write them; absent for a root word.",
135
170
  }),
136
171
  Object.freeze({
137
- name: "attested",
138
- kind: "list",
139
- shape: "a list of Addresses",
140
- entries: Object.freeze({ kind: "address", shape: "an Address" }),
141
- describe: "The notes the word is cited from.",
172
+ name: "note",
173
+ kind: "string",
174
+ shape: "a string",
175
+ describe:
176
+ "What a source says of the word beyond its pieces, such as a cognate it is read with.",
142
177
  }),
143
178
  Object.freeze({
144
179
  name: "pronunciation",
@@ -163,14 +198,41 @@ export const WORD_FIELDS = Object.freeze([
163
198
  Object.freeze({
164
199
  name: "frame",
165
200
  kind: "string",
166
- shape: "a string",
167
- describe: "The frame a root is set in.",
201
+ shape: "a frame of the language's `sounds.frames`",
202
+ describe: "The frame a root is set in, named in the language's `sounds.frames`.",
168
203
  }),
169
204
  ]);
170
205
 
171
206
  /** The keys of {@link WORD_FIELDS}, in declared order. */
172
207
  export const WORD_KEYS = Object.freeze(WORD_FIELDS.map((field) => field.name));
173
208
 
209
+ /**
210
+ * A word's senses: one for a string gloss, one per entry for a list, each
211
+ * trimmed, blanks dropped.
212
+ *
213
+ * @param {unknown} gloss - The word's `gloss`.
214
+ * @returns {string[]} The senses, in order.
215
+ */
216
+ export function glossSenses(gloss) {
217
+ const list = Array.isArray(gloss) ? gloss : [gloss];
218
+ return list
219
+ .filter((sense) => typeof sense === "string")
220
+ .map((sense) => sense.trim())
221
+ .filter(Boolean);
222
+ }
223
+
224
+ /**
225
+ * A word's gloss as one line of text: its senses joined by `; `, or `null`
226
+ * where it states none.
227
+ *
228
+ * @param {unknown} gloss - The word's `gloss`.
229
+ * @returns {string|null}
230
+ */
231
+ export function glossText(gloss) {
232
+ const senses = glossSenses(gloss);
233
+ return senses.length ? senses.join("; ") : null;
234
+ }
235
+
174
236
  /**
175
237
  * The keys one entry of a language note's `data.fields` carries.
176
238
  *
@@ -202,9 +264,36 @@ export const AFFIX_FIELDS = Object.freeze([
202
264
  Object.freeze({
203
265
  name: "form",
204
266
  kind: "string",
205
- required: true,
206
267
  shape: "the affix as written, such as `-rith`",
207
- describe: "The affix as a word's `built` names it.",
268
+ describe: "The affix as a word's `built` names it; absent on a zero affix.",
269
+ }),
270
+ Object.freeze({
271
+ name: "zero",
272
+ kind: "boolean",
273
+ shape: "`true`",
274
+ describe:
275
+ "The affix is written as nothing, such as a case with no ending; it states no " +
276
+ `\`form\`, prints as \`${ZERO_AFFIX}\`, and no \`built\` piece names it.`,
277
+ }),
278
+ Object.freeze({
279
+ name: "kind",
280
+ kind: "string",
281
+ allowed: AFFIX_KINDS,
282
+ shape: `one of ${AFFIX_KINDS.join(", ")}`,
283
+ describe: "What the affix makes.",
284
+ }),
285
+ Object.freeze({
286
+ name: "given",
287
+ kind: "string",
288
+ allowed: AFFIX_GIVEN,
289
+ shape: `one of ${AFFIX_GIVEN.join(", ")}`,
290
+ describe: `Who a given name closing on it belongs to; an affix of kind \`${NAME_AFFIX_KIND}\` only.`,
291
+ }),
292
+ Object.freeze({
293
+ name: "where",
294
+ kind: "string",
295
+ shape: "a string, such as `before a stop`",
296
+ describe: "Where this form stands, for an affix written two ways; everywhere where absent.",
208
297
  }),
209
298
  Object.freeze({
210
299
  name: "gloss",
@@ -217,7 +306,29 @@ export const AFFIX_FIELDS = Object.freeze([
217
306
  name: "attaches",
218
307
  kind: "string",
219
308
  shape: "a string",
220
- describe: "How it attaches, such as `replaces a closing vowel`.",
309
+ describe: "How it attaches, in words, such as `replaces a closing vowel`.",
310
+ }),
311
+ Object.freeze({
312
+ name: "replaces",
313
+ kind: "string",
314
+ shape: "`vowel`, or the letters, such as `a`",
315
+ describe:
316
+ "What it takes the place of at the close of the stem: `vowel` for any closing " +
317
+ "vowel, or the letters it replaces; added to the whole stem where absent.",
318
+ }),
319
+ Object.freeze({
320
+ name: "lengthens",
321
+ kind: "boolean",
322
+ shape: "`true` or `false`",
323
+ describe:
324
+ "Whether the stem's last vowel lengthens before the affix; `false` where unstated. An " +
325
+ "affix that `replaces` the stem's close does not also lengthen it.",
326
+ }),
327
+ Object.freeze({
328
+ name: "front",
329
+ kind: "string",
330
+ shape: "the affix as written, such as `-ür`",
331
+ describe: "Its form after front vowels, where the language keeps vowel harmony.",
221
332
  }),
222
333
  Object.freeze({
223
334
  name: "examples",
@@ -226,6 +337,13 @@ export const AFFIX_FIELDS = Object.freeze([
226
337
  entries: Object.freeze({ kind: "string", shape: "a string" }),
227
338
  describe: "The names the affix is read from.",
228
339
  }),
340
+ Object.freeze({
341
+ name: "barredAfter",
342
+ kind: "list",
343
+ shape: "a list of sounds",
344
+ entries: Object.freeze({ kind: "string", shape: "a sound as written" }),
345
+ describe: "The sounds a stem may not close on to take the affix.",
346
+ }),
229
347
  Object.freeze({
230
348
  name: "tones",
231
349
  kind: "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.6.1",
3
+ "version": "23.7.1",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -6,7 +6,8 @@
6
6
  * @property {object} [addressContext] - Address resolution context.
7
7
  * @property {(address: string) => string|undefined} [labelOf] - A note's
8
8
  * `name.full`, by its Address.
9
- * @property {boolean} [showKey=true] - Whether the key is printed.
9
+ * @property {boolean|"only"} [showKey=true] - Whether the key is printed;
10
+ * `"only"` prints the key and nothing else.
10
11
  * @property {string} [pageLanguage] - The language of the page the fence is
11
12
  * on, which no entry is tagged with.
12
13
  */
@@ -63,9 +64,10 @@ export type LayoutOptions = {
63
64
  */
64
65
  labelOf?: ((address: string) => string | undefined) | undefined;
65
66
  /**
66
- * - Whether the key is printed.
67
+ * - Whether the key is printed;
68
+ * `"only"` prints the key and nothing else.
67
69
  */
68
- showKey?: boolean | undefined;
70
+ showKey?: boolean | "only" | undefined;
69
71
  /**
70
72
  * - The language of the page the fence is
71
73
  * on, which no entry is tagged with.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Whether a tag a row names is one `data.tongues` declares.
3
+ *
4
+ * @param {string} tag - The tag as the row writes it.
5
+ * @param {Set<string>} declared - The declared tags.
6
+ * @returns {boolean}
7
+ */
8
+ export function tongueDeclared(tag: string, declared: Set<string>): boolean;
9
+ /**
10
+ * Check a `skill` note's language data against what it means.
11
+ *
12
+ * @param {object} note - A `skill` note: `file`, `raw` and parsed `fm`.
13
+ * @param {object} [context]
14
+ * @param {object} [context.index] - The index, for whether an Address
15
+ * resolves. Without one, only what the note says of itself is checked.
16
+ * @param {readonly string[]} [context.types] - The note types a scope may name.
17
+ * @returns {object[]} The findings.
18
+ */
19
+ export function checkLanguageData(note: object, { index, types }?: {
20
+ index?: object | undefined;
21
+ types?: readonly string[] | undefined;
22
+ }): object[];