@heroiclands/package-build 23.5.1 → 23.6.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 +41 -0
- package/bin/content-commands.mjs +23 -5
- package/docs/api.md +9 -0
- package/docs/authoring/links-and-markup.md +108 -12
- package/docs/content-format.md +16 -0
- package/docs/guides/reference-pages.md +89 -0
- package/docs/reference/format-details.md +549 -88
- package/docs/reference/note-types.md +192 -149
- package/engine/base-compiler.mjs +21 -0
- package/engine/content-format.yaml +9 -0
- package/engine/content-index.mjs +19 -5
- package/engine/culture-reach.mjs +330 -0
- package/engine/dictionary-layout.mjs +342 -0
- package/engine/dictionary-rows.mjs +605 -0
- package/engine/glossary-entries.mjs +182 -0
- package/engine/glossary-language.mjs +99 -0
- package/engine/index.mjs +1 -0
- package/engine/infobox.mjs +8 -0
- package/engine/language-words.mjs +374 -0
- package/engine/literature-notes.mjs +10 -8
- package/engine/note-addresses.mjs +25 -0
- package/engine/note-vocabulary.mjs +161 -1
- package/engine/pack-config.mjs +6 -1
- package/engine/path-charset.mjs +120 -0
- package/engine/sql-tables.mjs +352 -21
- package/engine/word-classes.mjs +167 -0
- package/engine/word-rows.mjs +343 -0
- package/engine/word-terms.mjs +318 -0
- package/package.json +1 -1
- package/types/engine/content-index.d.mts +15 -5
- package/types/engine/culture-reach.d.mts +66 -0
- package/types/engine/dictionary-layout.d.mts +74 -0
- package/types/engine/dictionary-rows.d.mts +39 -0
- package/types/engine/glossary-entries.d.mts +12 -0
- package/types/engine/glossary-language.d.mts +42 -0
- package/types/engine/index.d.mts +1 -0
- package/types/engine/language-words.d.mts +33 -0
- package/types/engine/literature-notes.d.mts +2 -2
- package/types/engine/note-addresses.d.mts +17 -0
- package/types/engine/path-charset.d.mts +38 -0
- package/types/engine/sql-tables.d.mts +8 -2
- package/types/engine/word-classes.d.mts +87 -0
- package/types/engine/word-rows.d.mts +119 -0
- package/types/engine/word-terms.d.mts +72 -0
|
@@ -0,0 +1,343 @@
|
|
|
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 `words` and `glossary` relations, read from index
|
|
16
|
+
* records whose Addresses are already strings.
|
|
17
|
+
*
|
|
18
|
+
* `words` is one row per `data.words` entry of every language note, keyed by
|
|
19
|
+
* that note, and `glossary` one row per `data.glossary` entry of every culture
|
|
20
|
+
* and affiliation, with the language the entry resolves to
|
|
21
|
+
* ({@link module:engine/glossary-language}). **Every row has every column**:
|
|
22
|
+
* the keys come from {@link module:engine/word-terms}, so a key no entry in a
|
|
23
|
+
* corpus writes is still a column, `NULL` in every row, and a fence naming it
|
|
24
|
+
* binds.
|
|
25
|
+
*
|
|
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.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { renderAddress } from "./address.mjs";
|
|
35
|
+
import { glossaryLanguage } from "./glossary-language.mjs";
|
|
36
|
+
import {
|
|
37
|
+
AFFIX_KEYS,
|
|
38
|
+
FIELD_KEYS,
|
|
39
|
+
GLOSSARY_ENTRY_KEYS,
|
|
40
|
+
LANGUAGE_SUBTYPE,
|
|
41
|
+
WORD_KEYS,
|
|
42
|
+
} from "./word-terms.mjs";
|
|
43
|
+
|
|
44
|
+
/** The columns a word row carries before the word's own keys. */
|
|
45
|
+
const WORD_IDENTITY_COLUMNS = Object.freeze(["language", "position"]);
|
|
46
|
+
|
|
47
|
+
/** The columns a word row derives after the word's own keys. */
|
|
48
|
+
const WORD_DERIVED_COLUMNS = Object.freeze(["built_text", "built_parts"]);
|
|
49
|
+
|
|
50
|
+
/** Every column of the `words` relation, in order. */
|
|
51
|
+
export const WORD_COLUMNS = Object.freeze([
|
|
52
|
+
...WORD_IDENTITY_COLUMNS,
|
|
53
|
+
...WORD_KEYS,
|
|
54
|
+
...WORD_DERIVED_COLUMNS,
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** Every column of the `glossary` relation, in order. */
|
|
58
|
+
export const GLOSSARY_COLUMNS = Object.freeze(["note", "position", ...GLOSSARY_ENTRY_KEYS]);
|
|
59
|
+
|
|
60
|
+
/** Every column of the `affixes` relation, in order. */
|
|
61
|
+
export const AFFIX_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...AFFIX_KEYS]);
|
|
62
|
+
|
|
63
|
+
/** Every column of the `fields` relation, in order. */
|
|
64
|
+
export const FIELD_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...FIELD_KEYS]);
|
|
65
|
+
|
|
66
|
+
/** Every column of the `language_lineage` relation, in order. */
|
|
67
|
+
export const LINEAGE_COLUMNS = Object.freeze(["language", "ancestor", "depth"]);
|
|
68
|
+
|
|
69
|
+
/** The columns of any of these relations that hold a whole number. */
|
|
70
|
+
export const ROW_INTEGER_COLUMNS = Object.freeze(["position", "depth"]);
|
|
71
|
+
|
|
72
|
+
/** Whether a value is a plain map. */
|
|
73
|
+
const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
74
|
+
|
|
75
|
+
/** A piece's form with its joining hyphens taken off, for a second match. */
|
|
76
|
+
const bare = (form) => String(form).replace(/^-+|-+$/g, "");
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A lookup from a built piece to its gloss, over the words (stems included)
|
|
80
|
+
* and affixes of one or more languages.
|
|
81
|
+
*
|
|
82
|
+
* Languages are given nearest first: a language's own words, then its own
|
|
83
|
+
* affixes, then the next language's, so a daughter's entry wins over its
|
|
84
|
+
* parent's. A piece matches an entry written exactly as it is, and failing
|
|
85
|
+
* that one written with the same letters and different joining hyphens, so
|
|
86
|
+
* `wulf-` finds the word `wulf`. Within a language a word's own gloss wins over
|
|
87
|
+
* an affix's.
|
|
88
|
+
*
|
|
89
|
+
* @param {...object} datas - Each language note's `data`, nearest first.
|
|
90
|
+
* @returns {(piece: string) => string|null} The gloss, or `null` where none resolves.
|
|
91
|
+
*/
|
|
92
|
+
export function pieceGlosses(...datas) {
|
|
93
|
+
const exact = new Map();
|
|
94
|
+
const loose = new Map();
|
|
95
|
+
const take = (entries) => {
|
|
96
|
+
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);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
for (const data of datas) {
|
|
104
|
+
take(data?.words);
|
|
105
|
+
take(data?.affixes);
|
|
106
|
+
}
|
|
107
|
+
return (piece) => exact.get(piece) ?? loose.get(bare(piece)) ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The chain of languages from one up through `data.family`: the language
|
|
112
|
+
* itself, then its parent, and so on. The chain stops at a parent that is no
|
|
113
|
+
* language, and before a language it has already reached, so a cycle the lint
|
|
114
|
+
* refuses still yields a finite chain. `language_lineage`, the dictionary and
|
|
115
|
+
* the resolution of built pieces all walk it.
|
|
116
|
+
*
|
|
117
|
+
* @param {string|undefined} start - The language's key.
|
|
118
|
+
* @param {object} reader
|
|
119
|
+
* @param {(key: string) => boolean} reader.has - Whether a key is a language.
|
|
120
|
+
* @param {(key: string) => string|undefined} reader.parentOf - A language's
|
|
121
|
+
* parent key, where it states one.
|
|
122
|
+
* @returns {string[]} The keys, nearest first.
|
|
123
|
+
*/
|
|
124
|
+
export function lineageChain(start, { has, parentOf }) {
|
|
125
|
+
const chain = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (let current = start; current && has(current) && !seen.has(current);) {
|
|
128
|
+
seen.add(current);
|
|
129
|
+
chain.push(current);
|
|
130
|
+
current = parentOf(current) || undefined;
|
|
131
|
+
}
|
|
132
|
+
return chain;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A language record's `data.family` where it is a plain Address string. */
|
|
136
|
+
const familyKey = (record) => {
|
|
137
|
+
const family = record?.data?.family;
|
|
138
|
+
return typeof family === "string" && family ? family : undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A lookup of language records by Address, and the chain walk over them.
|
|
143
|
+
*
|
|
144
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
145
|
+
* @returns {{languages: Map<string, object>, chain: (key: string) => string[]}}
|
|
146
|
+
*/
|
|
147
|
+
export function languageLineages(records) {
|
|
148
|
+
const languages = new Map();
|
|
149
|
+
for (const record of records)
|
|
150
|
+
if (isLanguageRecord(record)) {
|
|
151
|
+
const key = noteAddressOf(record);
|
|
152
|
+
if (key && !languages.has(key)) languages.set(key, record);
|
|
153
|
+
}
|
|
154
|
+
const reader = {
|
|
155
|
+
has: (key) => languages.has(key),
|
|
156
|
+
parentOf: (key) => familyKey(languages.get(key)),
|
|
157
|
+
};
|
|
158
|
+
return { languages, chain: (key) => lineageChain(key, reader) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether a record is a language note's.
|
|
163
|
+
*
|
|
164
|
+
* @param {Record<string, any>} record - An index record.
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
export function isLanguageRecord(record) {
|
|
168
|
+
return record?.type === "skill" && record?.subType === LANGUAGE_SUBTYPE;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A record's Address as a reference to it is written: in the `note` system,
|
|
173
|
+
* so it compares equal to the Addresses other notes state.
|
|
174
|
+
*
|
|
175
|
+
* @param {Record<string, any>} record - An index record.
|
|
176
|
+
* @returns {string|null} The Address, or `null` for a record naming no note.
|
|
177
|
+
*/
|
|
178
|
+
function noteAddressOf(record) {
|
|
179
|
+
if (!record?.package || !record.type || !record.shortcode) return null;
|
|
180
|
+
return renderAddress({
|
|
181
|
+
package: String(record.package),
|
|
182
|
+
system: "note",
|
|
183
|
+
type: String(record.type),
|
|
184
|
+
shortcode: String(record.shortcode),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The `words` rows of a set of index records.
|
|
190
|
+
*
|
|
191
|
+
* A row's `language` is the note that states it.
|
|
192
|
+
*
|
|
193
|
+
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
194
|
+
* Address a string, already narrowed to the audience being published to.
|
|
195
|
+
* @param {Iterable<Record<string, any>>} [all] - Every record of the package,
|
|
196
|
+
* for resolving a built piece through an ancestor the audience does not see.
|
|
197
|
+
* @returns {object[]} One row per word, in record then entry order.
|
|
198
|
+
*/
|
|
199
|
+
export function wordRows(records, all = records) {
|
|
200
|
+
const { languages, chain } = languageLineages(all);
|
|
201
|
+
const rows = [];
|
|
202
|
+
for (const record of records) {
|
|
203
|
+
if (!isLanguageRecord(record)) continue;
|
|
204
|
+
const words = record.data?.words;
|
|
205
|
+
if (!Array.isArray(words) || !words.length) continue;
|
|
206
|
+
const language = noteAddressOf(record);
|
|
207
|
+
if (!language) continue;
|
|
208
|
+
const glossOf = pieceGlosses(...chain(language).map((key) => languages.get(key).data));
|
|
209
|
+
words.forEach((entry, position) => {
|
|
210
|
+
if (!isMap(entry)) return;
|
|
211
|
+
const row = { language, position };
|
|
212
|
+
for (const key of WORD_KEYS) row[key] = entry[key] ?? null;
|
|
213
|
+
if (row.name_sort === null) row.name_sort = row.form;
|
|
214
|
+
const built = Array.isArray(entry.built) ? entry.built.map(String) : [];
|
|
215
|
+
row.built_text = built.length ? built.join(" + ") : null;
|
|
216
|
+
row.built_parts =
|
|
217
|
+
built.length ? built.map((form) => ({ form, gloss: glossOf(form) })) : null;
|
|
218
|
+
rows.push(row);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return rows;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The rows of one list a language note states beside its words: one per entry,
|
|
226
|
+
* keyed by the note, with every key of the entry a column.
|
|
227
|
+
*
|
|
228
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
229
|
+
* @param {string} list - The `data` key holding the entries.
|
|
230
|
+
* @param {readonly string[]} keys - The keys an entry carries.
|
|
231
|
+
* @returns {object[]} One row per entry, in record then entry order.
|
|
232
|
+
*/
|
|
233
|
+
function languageListRows(records, list, keys) {
|
|
234
|
+
const rows = [];
|
|
235
|
+
for (const record of records) {
|
|
236
|
+
if (!isLanguageRecord(record)) continue;
|
|
237
|
+
const entries = record.data?.[list];
|
|
238
|
+
if (!Array.isArray(entries) || !entries.length) continue;
|
|
239
|
+
const language = noteAddressOf(record);
|
|
240
|
+
if (!language) continue;
|
|
241
|
+
entries.forEach((entry, position) => {
|
|
242
|
+
if (!isMap(entry)) return;
|
|
243
|
+
const row = { language, position };
|
|
244
|
+
for (const key of keys) row[key] = entry[key] ?? null;
|
|
245
|
+
rows.push(row);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return rows;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 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`.
|
|
254
|
+
*
|
|
255
|
+
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
256
|
+
* Address a string, already narrowed to the audience being published to.
|
|
257
|
+
* @returns {object[]} One row per affix, in record then entry order.
|
|
258
|
+
*/
|
|
259
|
+
export function affixRows(records) {
|
|
260
|
+
return languageListRows(records, "affixes", AFFIX_KEYS);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The `fields` rows of a set of index records: one per `data.fields` entry of
|
|
265
|
+
* a language note, which is the row's `language`.
|
|
266
|
+
*
|
|
267
|
+
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
268
|
+
* Address a string, already narrowed to the audience being published to.
|
|
269
|
+
* @returns {object[]} One row per field, in record then entry order.
|
|
270
|
+
*/
|
|
271
|
+
export function fieldRows(records) {
|
|
272
|
+
return languageListRows(records, "fields", FIELD_KEYS);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The `glossary` rows of a set of index records: one per entry of a culture's
|
|
277
|
+
* or an affiliation's `data.glossary`, with `name_sort` the name and `common`
|
|
278
|
+
* `false` where the entry leaves them unstated, and `language` the language
|
|
279
|
+
* the entry resolves to, or `NULL` where it resolves to none.
|
|
280
|
+
*
|
|
281
|
+
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
282
|
+
* Address a string, already narrowed to the audience being published to.
|
|
283
|
+
* @param {Iterable<Record<string, any>>} [all] - Every record of the package,
|
|
284
|
+
* for resolving a language through a note the audience does not see.
|
|
285
|
+
* @returns {object[]} One row per entry, in record then entry order.
|
|
286
|
+
*/
|
|
287
|
+
export function glossaryRows(records, all = records) {
|
|
288
|
+
const byAddress = new Map();
|
|
289
|
+
for (const record of all) {
|
|
290
|
+
const key = noteAddressOf(record);
|
|
291
|
+
if (key && !byAddress.has(key)) byAddress.set(key, record);
|
|
292
|
+
}
|
|
293
|
+
const reader = {
|
|
294
|
+
keyOf: (value) => (typeof value === "string" && value ? value : undefined),
|
|
295
|
+
lookup: (key) => byAddress.get(key),
|
|
296
|
+
};
|
|
297
|
+
const rows = [];
|
|
298
|
+
for (const record of records) {
|
|
299
|
+
const owner =
|
|
300
|
+
record?.type === "affiliation" ||
|
|
301
|
+
(record?.type === "lore" && record?.subType === "culture");
|
|
302
|
+
if (!owner) continue;
|
|
303
|
+
const entries = record.data?.glossary;
|
|
304
|
+
if (!Array.isArray(entries) || !entries.length) continue;
|
|
305
|
+
const note = noteAddressOf(record);
|
|
306
|
+
if (!note) continue;
|
|
307
|
+
entries.forEach((entry, position) => {
|
|
308
|
+
if (!isMap(entry) || typeof entry.name !== "string") return;
|
|
309
|
+
const row = { note, position };
|
|
310
|
+
for (const key of GLOSSARY_ENTRY_KEYS) row[key] = entry[key] ?? null;
|
|
311
|
+
if (row.name_sort === null) row.name_sort = row.name;
|
|
312
|
+
if (row.common === null) row.common = false;
|
|
313
|
+
row.language = glossaryLanguage(entry, record, { ...reader, self: note }) ?? null;
|
|
314
|
+
rows.push(row);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return rows;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The `language_lineage` rows of a set of index records: for each language
|
|
322
|
+
* note, itself at depth `0`, its `data.family` at depth `1`, and so on up the
|
|
323
|
+
* chain. The chain stops at a parent that is no language note of the package,
|
|
324
|
+
* and before a language it has already reached, so a cycle the lint refuses
|
|
325
|
+
* still yields finite rows.
|
|
326
|
+
*
|
|
327
|
+
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
328
|
+
* Address a string, already narrowed to the audience being published to.
|
|
329
|
+
* @param {Iterable<Record<string, any>>} [all] - Every record of the package,
|
|
330
|
+
* for following a chain through a note the audience does not see.
|
|
331
|
+
* @returns {object[]} One row per language and ancestor, in record then depth order.
|
|
332
|
+
*/
|
|
333
|
+
export function lineageRows(records, all = records) {
|
|
334
|
+
const { chain } = languageLineages(all);
|
|
335
|
+
const rows = [];
|
|
336
|
+
for (const record of records) {
|
|
337
|
+
if (!isLanguageRecord(record)) continue;
|
|
338
|
+
const language = noteAddressOf(record);
|
|
339
|
+
if (!language) continue;
|
|
340
|
+
chain(language).forEach((ancestor, depth) => rows.push({ language, ancestor, depth }));
|
|
341
|
+
}
|
|
342
|
+
return rows;
|
|
343
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
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 inner keys of a language note's words, fields and affixes, and of a
|
|
16
|
+
* culture's or an affiliation's glossary entries, as inner-key declarations.
|
|
17
|
+
*
|
|
18
|
+
* A leaf module, importing nothing: the note vocabulary reads these
|
|
19
|
+
* declarations as it loads, and the checks that judge what the values mean —
|
|
20
|
+
* a class against the master word classes, a field against the note's own
|
|
21
|
+
* fields — live in `language-words.mjs` and `glossary-entries.mjs`.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The `skill` subType whose notes are languages, and the only one with words. */
|
|
27
|
+
export const LANGUAGE_SUBTYPE = "language";
|
|
28
|
+
|
|
29
|
+
/** The `data` keys a language note states its words in, beside `names`. */
|
|
30
|
+
export const WORD_DATA_KEYS = Object.freeze(["classModifiers", "fields", "words", "affixes"]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Where a word comes from. `borrowed` names the tongue it was taken from;
|
|
34
|
+
* `inherited`, `kept` and `worn` name the ancient form where the note gives
|
|
35
|
+
* one; `coined`, `substrate` and `island` name no form.
|
|
36
|
+
*/
|
|
37
|
+
export const WORD_SOURCE_KINDS = Object.freeze([
|
|
38
|
+
"inherited",
|
|
39
|
+
"borrowed",
|
|
40
|
+
"kept",
|
|
41
|
+
"worn",
|
|
42
|
+
"coined",
|
|
43
|
+
"substrate",
|
|
44
|
+
"island",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** The source kind that requires `language`. */
|
|
48
|
+
export const BORROWED_SOURCE_KIND = "borrowed";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The keys of a word's `source`.
|
|
52
|
+
*
|
|
53
|
+
* @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
|
|
54
|
+
*/
|
|
55
|
+
export const WORD_SOURCE_FIELDS = Object.freeze([
|
|
56
|
+
Object.freeze({
|
|
57
|
+
name: "kind",
|
|
58
|
+
kind: "string",
|
|
59
|
+
required: true,
|
|
60
|
+
allowed: WORD_SOURCE_KINDS,
|
|
61
|
+
shape: `one of ${WORD_SOURCE_KINDS.join(", ")}`,
|
|
62
|
+
describe: "How the word came into the tongue.",
|
|
63
|
+
}),
|
|
64
|
+
Object.freeze({
|
|
65
|
+
name: "language",
|
|
66
|
+
kind: "address",
|
|
67
|
+
shape: "a language skill Address",
|
|
68
|
+
describe: "The tongue the word was borrowed from; required for `borrowed`.",
|
|
69
|
+
}),
|
|
70
|
+
Object.freeze({
|
|
71
|
+
name: "form",
|
|
72
|
+
kind: "string",
|
|
73
|
+
shape: "a string",
|
|
74
|
+
describe: "The form the word had in its source, bare.",
|
|
75
|
+
}),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The keys one word of a language note's `data.words` carries.
|
|
80
|
+
*
|
|
81
|
+
* @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
|
|
82
|
+
*/
|
|
83
|
+
export const WORD_FIELDS = Object.freeze([
|
|
84
|
+
Object.freeze({
|
|
85
|
+
name: "form",
|
|
86
|
+
kind: "string",
|
|
87
|
+
required: true,
|
|
88
|
+
shape: "the word as cited, bare",
|
|
89
|
+
describe: "The word as cited, with no backticks.",
|
|
90
|
+
}),
|
|
91
|
+
Object.freeze({
|
|
92
|
+
name: "name_sort",
|
|
93
|
+
kind: "string",
|
|
94
|
+
shape: "a string",
|
|
95
|
+
describe: "The sort key; the form when unstated.",
|
|
96
|
+
}),
|
|
97
|
+
Object.freeze({
|
|
98
|
+
name: "class",
|
|
99
|
+
kind: "string",
|
|
100
|
+
required: true,
|
|
101
|
+
shape: "a word class, base first, such as `n I f`",
|
|
102
|
+
describe:
|
|
103
|
+
"A class from the master word classes: a base, then the modifiers the language " +
|
|
104
|
+
"declares in `classModifiers`.",
|
|
105
|
+
}),
|
|
106
|
+
Object.freeze({
|
|
107
|
+
name: "field",
|
|
108
|
+
kind: "string",
|
|
109
|
+
required: true,
|
|
110
|
+
shape: "one of the language's `fields` names",
|
|
111
|
+
describe: "The field the word is grouped under, named in the language's `fields`.",
|
|
112
|
+
}),
|
|
113
|
+
Object.freeze({
|
|
114
|
+
name: "gloss",
|
|
115
|
+
kind: "string",
|
|
116
|
+
required: true,
|
|
117
|
+
shape: "a string",
|
|
118
|
+
describe: "The meaning, in brief.",
|
|
119
|
+
}),
|
|
120
|
+
Object.freeze({
|
|
121
|
+
name: "source",
|
|
122
|
+
kind: "map",
|
|
123
|
+
shape: "a map — `{ kind, language?, form? }`",
|
|
124
|
+
fields: WORD_SOURCE_FIELDS,
|
|
125
|
+
describe: "Where the word comes from.",
|
|
126
|
+
}),
|
|
127
|
+
Object.freeze({
|
|
128
|
+
name: "built",
|
|
129
|
+
kind: "list",
|
|
130
|
+
shape: "a list of pieces",
|
|
131
|
+
entries: Object.freeze({ kind: "string", shape: "a string" }),
|
|
132
|
+
describe:
|
|
133
|
+
"The pieces the word is built from, in order, as the language's words and affixes " +
|
|
134
|
+
"write them; absent for a root word.",
|
|
135
|
+
}),
|
|
136
|
+
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.",
|
|
142
|
+
}),
|
|
143
|
+
Object.freeze({
|
|
144
|
+
name: "pronunciation",
|
|
145
|
+
kind: "string",
|
|
146
|
+
shape: "a respelling such as `sol-MEH-reh`",
|
|
147
|
+
describe:
|
|
148
|
+
"A respelling in hyphen-separated syllables, the stressed one in capitals: letters, " +
|
|
149
|
+
"apostrophes and hyphens only, and at most one capitalised syllable.",
|
|
150
|
+
}),
|
|
151
|
+
Object.freeze({
|
|
152
|
+
name: "tones",
|
|
153
|
+
kind: "string",
|
|
154
|
+
shape: "a string",
|
|
155
|
+
describe: "A tone pattern, such as `LHL`.",
|
|
156
|
+
}),
|
|
157
|
+
Object.freeze({
|
|
158
|
+
name: "root",
|
|
159
|
+
kind: "string",
|
|
160
|
+
shape: "a string",
|
|
161
|
+
describe: "A consonantal skeleton, such as `d-l-k`.",
|
|
162
|
+
}),
|
|
163
|
+
Object.freeze({
|
|
164
|
+
name: "frame",
|
|
165
|
+
kind: "string",
|
|
166
|
+
shape: "a string",
|
|
167
|
+
describe: "The frame a root is set in.",
|
|
168
|
+
}),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
/** The keys of {@link WORD_FIELDS}, in declared order. */
|
|
172
|
+
export const WORD_KEYS = Object.freeze(WORD_FIELDS.map((field) => field.name));
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The keys one entry of a language note's `data.fields` carries.
|
|
176
|
+
*
|
|
177
|
+
* @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
|
|
178
|
+
*/
|
|
179
|
+
export const WORD_FIELD_FIELDS = Object.freeze([
|
|
180
|
+
Object.freeze({
|
|
181
|
+
name: "name",
|
|
182
|
+
kind: "string",
|
|
183
|
+
required: true,
|
|
184
|
+
shape: "a string",
|
|
185
|
+
describe: "The field's name, which a word's `field` names.",
|
|
186
|
+
}),
|
|
187
|
+
Object.freeze({
|
|
188
|
+
name: "holds",
|
|
189
|
+
kind: "string",
|
|
190
|
+
required: true,
|
|
191
|
+
shape: "a string",
|
|
192
|
+
describe: "What the field holds.",
|
|
193
|
+
}),
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The keys one entry of a language note's `data.affixes` carries.
|
|
198
|
+
*
|
|
199
|
+
* @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
|
|
200
|
+
*/
|
|
201
|
+
export const AFFIX_FIELDS = Object.freeze([
|
|
202
|
+
Object.freeze({
|
|
203
|
+
name: "form",
|
|
204
|
+
kind: "string",
|
|
205
|
+
required: true,
|
|
206
|
+
shape: "the affix as written, such as `-rith`",
|
|
207
|
+
describe: "The affix as a word's `built` names it.",
|
|
208
|
+
}),
|
|
209
|
+
Object.freeze({
|
|
210
|
+
name: "gloss",
|
|
211
|
+
kind: "string",
|
|
212
|
+
required: true,
|
|
213
|
+
shape: "a string",
|
|
214
|
+
describe: "What the affix means.",
|
|
215
|
+
}),
|
|
216
|
+
Object.freeze({
|
|
217
|
+
name: "attaches",
|
|
218
|
+
kind: "string",
|
|
219
|
+
shape: "a string",
|
|
220
|
+
describe: "How it attaches, such as `replaces a closing vowel`.",
|
|
221
|
+
}),
|
|
222
|
+
Object.freeze({
|
|
223
|
+
name: "examples",
|
|
224
|
+
kind: "list",
|
|
225
|
+
shape: "a list of strings",
|
|
226
|
+
entries: Object.freeze({ kind: "string", shape: "a string" }),
|
|
227
|
+
describe: "The names the affix is read from.",
|
|
228
|
+
}),
|
|
229
|
+
Object.freeze({
|
|
230
|
+
name: "tones",
|
|
231
|
+
kind: "string",
|
|
232
|
+
shape: "a tone pattern, such as `L`",
|
|
233
|
+
describe: "The tone pattern the affix carries, written as a word's `tones` is.",
|
|
234
|
+
}),
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
/** The keys of {@link WORD_FIELD_FIELDS}, in declared order. */
|
|
238
|
+
export const FIELD_KEYS = Object.freeze(WORD_FIELD_FIELDS.map((field) => field.name));
|
|
239
|
+
|
|
240
|
+
/** The keys of {@link AFFIX_FIELDS}, in declared order. */
|
|
241
|
+
export const AFFIX_KEYS = Object.freeze(AFFIX_FIELDS.map((field) => field.name));
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The keys one entry of a culture's or an affiliation's `data.glossary`
|
|
245
|
+
* carries.
|
|
246
|
+
*
|
|
247
|
+
* @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
|
|
248
|
+
*/
|
|
249
|
+
export const GLOSSARY_ENTRY_FIELDS = Object.freeze([
|
|
250
|
+
Object.freeze({
|
|
251
|
+
name: "name",
|
|
252
|
+
kind: "string",
|
|
253
|
+
required: true,
|
|
254
|
+
shape: "a string",
|
|
255
|
+
describe: "The headword.",
|
|
256
|
+
}),
|
|
257
|
+
Object.freeze({
|
|
258
|
+
name: "name_sort",
|
|
259
|
+
kind: "string",
|
|
260
|
+
shape: "a string",
|
|
261
|
+
describe: "Its sort key; the name when unstated.",
|
|
262
|
+
}),
|
|
263
|
+
Object.freeze({
|
|
264
|
+
name: "common",
|
|
265
|
+
kind: "boolean",
|
|
266
|
+
shape: "`true` or `false`",
|
|
267
|
+
describe: "A word a player is expected to know by heart; `false` when unstated.",
|
|
268
|
+
}),
|
|
269
|
+
Object.freeze({
|
|
270
|
+
name: "definition",
|
|
271
|
+
kind: "string",
|
|
272
|
+
shape: "a string",
|
|
273
|
+
describe:
|
|
274
|
+
"The sense text. Stated, it creates the entry or replaces every sense the " +
|
|
275
|
+
"entry's other sources give it.",
|
|
276
|
+
}),
|
|
277
|
+
Object.freeze({
|
|
278
|
+
name: "pronunciation",
|
|
279
|
+
kind: "string",
|
|
280
|
+
shape: "a respelling such as `sol-MEH-reh`",
|
|
281
|
+
describe: "A respelling, held to the same rule as a word's.",
|
|
282
|
+
}),
|
|
283
|
+
Object.freeze({
|
|
284
|
+
name: "language",
|
|
285
|
+
kind: "address",
|
|
286
|
+
shape: "a language skill Address",
|
|
287
|
+
describe:
|
|
288
|
+
"The language the term is in. Unstated, it is the affiliation's language, its " +
|
|
289
|
+
"parents', or its culture's, or the culture's own.",
|
|
290
|
+
}),
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
/** The keys of {@link GLOSSARY_ENTRY_FIELDS}, in declared order. */
|
|
294
|
+
export const GLOSSARY_ENTRY_KEYS = Object.freeze(GLOSSARY_ENTRY_FIELDS.map((field) => field.name));
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Why a pronunciation is not a respelling, or `undefined` when it is one.
|
|
298
|
+
*
|
|
299
|
+
* A respelling is letters and apostrophes in hyphen-separated syllables, with
|
|
300
|
+
* at most one syllable in capitals.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} text
|
|
303
|
+
* @returns {string|undefined}
|
|
304
|
+
*/
|
|
305
|
+
export function pronunciationProblem(text) {
|
|
306
|
+
const value = String(text ?? "");
|
|
307
|
+
if (!/^[\p{L}\p{M}'’]+(?:-[\p{L}\p{M}'’]+)*$/u.test(value))
|
|
308
|
+
return "a respelling holds letters, apostrophes and hyphens only, as `sol-MEH-reh`";
|
|
309
|
+
const stressed = value
|
|
310
|
+
.split(/-/)
|
|
311
|
+
.filter(
|
|
312
|
+
(syllable) =>
|
|
313
|
+
syllable !== syllable.toLowerCase() && syllable === syllable.toUpperCase(),
|
|
314
|
+
);
|
|
315
|
+
if (stressed.length > 1)
|
|
316
|
+
return `a respelling stresses one syllable, and this capitalises ${stressed.length}`;
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.6.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",
|