@heroiclands/package-build 23.5.1 → 23.6.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 (43) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/content-commands.mjs +23 -5
  3. package/docs/api.md +9 -0
  4. package/docs/authoring/links-and-markup.md +77 -12
  5. package/docs/content-format.md +16 -0
  6. package/docs/guides/reference-pages.md +89 -0
  7. package/docs/reference/format-details.md +533 -84
  8. package/docs/reference/note-types.md +191 -149
  9. package/engine/base-compiler.mjs +21 -0
  10. package/engine/content-format.yaml +9 -0
  11. package/engine/content-index.mjs +19 -5
  12. package/engine/culture-reach.mjs +330 -0
  13. package/engine/dictionary-layout.mjs +340 -0
  14. package/engine/dictionary-rows.mjs +608 -0
  15. package/engine/glossary-entries.mjs +182 -0
  16. package/engine/glossary-language.mjs +99 -0
  17. package/engine/index.mjs +1 -0
  18. package/engine/infobox.mjs +8 -0
  19. package/engine/language-words.mjs +360 -0
  20. package/engine/literature-notes.mjs +10 -8
  21. package/engine/note-addresses.mjs +25 -0
  22. package/engine/note-vocabulary.mjs +161 -1
  23. package/engine/path-charset.mjs +120 -0
  24. package/engine/sql-tables.mjs +311 -21
  25. package/engine/word-classes.mjs +167 -0
  26. package/engine/word-rows.mjs +233 -0
  27. package/engine/word-terms.mjs +306 -0
  28. package/package.json +1 -1
  29. package/types/engine/content-index.d.mts +15 -5
  30. package/types/engine/culture-reach.d.mts +66 -0
  31. package/types/engine/dictionary-layout.d.mts +69 -0
  32. package/types/engine/dictionary-rows.d.mts +39 -0
  33. package/types/engine/glossary-entries.d.mts +12 -0
  34. package/types/engine/glossary-language.d.mts +42 -0
  35. package/types/engine/index.d.mts +1 -0
  36. package/types/engine/language-words.d.mts +33 -0
  37. package/types/engine/literature-notes.d.mts +2 -2
  38. package/types/engine/note-addresses.d.mts +17 -0
  39. package/types/engine/path-charset.d.mts +38 -0
  40. package/types/engine/sql-tables.d.mts +7 -2
  41. package/types/engine/word-classes.d.mts +87 -0
  42. package/types/engine/word-rows.d.mts +64 -0
  43. package/types/engine/word-terms.d.mts +68 -0
@@ -0,0 +1,233 @@
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 { GLOSSARY_ENTRY_KEYS, LANGUAGE_SUBTYPE, WORD_KEYS } from "./word-terms.mjs";
37
+
38
+ /** The columns a word row carries before the word's own keys. */
39
+ const WORD_IDENTITY_COLUMNS = Object.freeze(["language", "position"]);
40
+
41
+ /** The columns a word row derives after the word's own keys. */
42
+ const WORD_DERIVED_COLUMNS = Object.freeze(["built_text", "built_parts"]);
43
+
44
+ /** Every column of the `words` relation, in order. */
45
+ export const WORD_COLUMNS = Object.freeze([
46
+ ...WORD_IDENTITY_COLUMNS,
47
+ ...WORD_KEYS,
48
+ ...WORD_DERIVED_COLUMNS,
49
+ ]);
50
+
51
+ /** Every column of the `glossary` relation, in order. */
52
+ export const GLOSSARY_COLUMNS = Object.freeze(["note", "position", ...GLOSSARY_ENTRY_KEYS]);
53
+
54
+ /** Every column of the `language_lineage` relation, in order. */
55
+ export const LINEAGE_COLUMNS = Object.freeze(["language", "ancestor", "depth"]);
56
+
57
+ /** The columns of any of these relations that hold a whole number. */
58
+ export const ROW_INTEGER_COLUMNS = Object.freeze(["position", "depth"]);
59
+
60
+ /** Whether a value is a plain map. */
61
+ const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
62
+
63
+ /** A piece's form with its joining hyphens taken off, for a second match. */
64
+ const bare = (form) => String(form).replace(/^-+|-+$/g, "");
65
+
66
+ /**
67
+ * A lookup from a built piece to its gloss, over one language's words (stems
68
+ * included) and affixes.
69
+ *
70
+ * A piece matches a word or an affix written exactly as it is, and failing
71
+ * that one written with the same letters and different joining hyphens, so
72
+ * `wulf-` finds the word `wulf`. A word's own gloss wins over an affix's.
73
+ *
74
+ * @param {object} data - The language note's `data`.
75
+ * @returns {(piece: string) => string|null} The gloss, or `null` where none resolves.
76
+ */
77
+ export function pieceGlosses(data) {
78
+ const exact = new Map();
79
+ const loose = new Map();
80
+ const take = (entries) => {
81
+ for (const entry of Array.isArray(entries) ? entries : []) {
82
+ if (!isMap(entry) || typeof entry.form !== "string" || typeof entry.gloss !== "string")
83
+ continue;
84
+ if (!exact.has(entry.form)) exact.set(entry.form, entry.gloss);
85
+ if (!loose.has(bare(entry.form))) loose.set(bare(entry.form), entry.gloss);
86
+ }
87
+ };
88
+ take(data?.words);
89
+ take(data?.affixes);
90
+ return (piece) => exact.get(piece) ?? loose.get(bare(piece)) ?? null;
91
+ }
92
+
93
+ /**
94
+ * Whether a record is a language note's.
95
+ *
96
+ * @param {Record<string, any>} record - An index record.
97
+ * @returns {boolean}
98
+ */
99
+ export function isLanguageRecord(record) {
100
+ return record?.type === "skill" && record?.subType === LANGUAGE_SUBTYPE;
101
+ }
102
+
103
+ /**
104
+ * A record's Address as a reference to it is written: in the `note` system,
105
+ * so it compares equal to the Addresses other notes state.
106
+ *
107
+ * @param {Record<string, any>} record - An index record.
108
+ * @returns {string|null} The Address, or `null` for a record naming no note.
109
+ */
110
+ function noteAddressOf(record) {
111
+ if (!record?.package || !record.type || !record.shortcode) return null;
112
+ return renderAddress({
113
+ package: String(record.package),
114
+ system: "note",
115
+ type: String(record.type),
116
+ shortcode: String(record.shortcode),
117
+ });
118
+ }
119
+
120
+ /**
121
+ * The `words` rows of a set of index records.
122
+ *
123
+ * A row's `language` is the note that states it.
124
+ *
125
+ * @param {Iterable<Record<string, any>>} records - Index records, every
126
+ * Address a string, already narrowed to the audience being published to.
127
+ * @returns {object[]} One row per word, in record then entry order.
128
+ */
129
+ export function wordRows(records) {
130
+ const rows = [];
131
+ for (const record of records) {
132
+ if (!isLanguageRecord(record)) continue;
133
+ const words = record.data?.words;
134
+ if (!Array.isArray(words) || !words.length) continue;
135
+ const language = noteAddressOf(record);
136
+ if (!language) continue;
137
+ const glossOf = pieceGlosses(record.data);
138
+ words.forEach((entry, position) => {
139
+ if (!isMap(entry)) return;
140
+ const row = { language, position };
141
+ for (const key of WORD_KEYS) row[key] = entry[key] ?? null;
142
+ if (row.name_sort === null) row.name_sort = row.form;
143
+ const built = Array.isArray(entry.built) ? entry.built.map(String) : [];
144
+ row.built_text = built.length ? built.join(" + ") : null;
145
+ row.built_parts =
146
+ built.length ? built.map((form) => ({ form, gloss: glossOf(form) })) : null;
147
+ rows.push(row);
148
+ });
149
+ }
150
+ return rows;
151
+ }
152
+
153
+ /**
154
+ * The `glossary` rows of a set of index records: one per entry of a culture's
155
+ * or an affiliation's `data.glossary`, with `name_sort` the name and `common`
156
+ * `false` where the entry leaves them unstated, and `language` the language
157
+ * the entry resolves to, or `NULL` where it resolves to none.
158
+ *
159
+ * @param {Iterable<Record<string, any>>} records - Index records, every
160
+ * Address a string, already narrowed to the audience being published to.
161
+ * @param {Iterable<Record<string, any>>} [all] - Every record of the package,
162
+ * for resolving a language through a note the audience does not see.
163
+ * @returns {object[]} One row per entry, in record then entry order.
164
+ */
165
+ export function glossaryRows(records, all = records) {
166
+ const byAddress = new Map();
167
+ for (const record of all) {
168
+ const key = noteAddressOf(record);
169
+ if (key && !byAddress.has(key)) byAddress.set(key, record);
170
+ }
171
+ const reader = {
172
+ keyOf: (value) => (typeof value === "string" && value ? value : undefined),
173
+ lookup: (key) => byAddress.get(key),
174
+ };
175
+ const rows = [];
176
+ for (const record of records) {
177
+ const owner =
178
+ record?.type === "affiliation" ||
179
+ (record?.type === "lore" && record?.subType === "culture");
180
+ if (!owner) continue;
181
+ const entries = record.data?.glossary;
182
+ if (!Array.isArray(entries) || !entries.length) continue;
183
+ const note = noteAddressOf(record);
184
+ if (!note) continue;
185
+ entries.forEach((entry, position) => {
186
+ if (!isMap(entry) || typeof entry.name !== "string") return;
187
+ const row = { note, position };
188
+ for (const key of GLOSSARY_ENTRY_KEYS) row[key] = entry[key] ?? null;
189
+ if (row.name_sort === null) row.name_sort = row.name;
190
+ if (row.common === null) row.common = false;
191
+ row.language = glossaryLanguage(entry, record, { ...reader, self: note }) ?? null;
192
+ rows.push(row);
193
+ });
194
+ }
195
+ return rows;
196
+ }
197
+
198
+ /**
199
+ * The `language_lineage` rows of a set of index records: for each language
200
+ * note, itself at depth `0`, its `data.family` at depth `1`, and so on up the
201
+ * chain. The chain stops at a parent that is no language note of the package,
202
+ * and before a language it has already reached, so a cycle the lint refuses
203
+ * still yields finite rows.
204
+ *
205
+ * @param {Iterable<Record<string, any>>} records - Index records, every
206
+ * Address a string, already narrowed to the audience being published to.
207
+ * @param {Iterable<Record<string, any>>} [all] - Every record of the package,
208
+ * for following a chain through a note the audience does not see.
209
+ * @returns {object[]} One row per language and ancestor, in record then depth order.
210
+ */
211
+ export function lineageRows(records, all = records) {
212
+ const languages = new Map();
213
+ for (const record of all)
214
+ if (isLanguageRecord(record)) {
215
+ const key = noteAddressOf(record);
216
+ if (key && !languages.has(key)) languages.set(key, record);
217
+ }
218
+ const rows = [];
219
+ for (const record of records) {
220
+ if (!isLanguageRecord(record)) continue;
221
+ const language = noteAddressOf(record);
222
+ if (!language) continue;
223
+ const seen = new Set();
224
+ let current = language;
225
+ for (let depth = 0; current && languages.has(current) && !seen.has(current); depth++) {
226
+ seen.add(current);
227
+ rows.push({ language, ancestor: current, depth });
228
+ const family = languages.get(current).data?.family;
229
+ current = typeof family === "string" && family ? family : undefined;
230
+ }
231
+ }
232
+ return rows;
233
+ }
@@ -0,0 +1,306 @@
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
+ ]);
230
+
231
+ /**
232
+ * The keys one entry of a culture's or an affiliation's `data.glossary`
233
+ * carries.
234
+ *
235
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
236
+ */
237
+ export const GLOSSARY_ENTRY_FIELDS = Object.freeze([
238
+ Object.freeze({
239
+ name: "name",
240
+ kind: "string",
241
+ required: true,
242
+ shape: "a string",
243
+ describe: "The headword.",
244
+ }),
245
+ Object.freeze({
246
+ name: "name_sort",
247
+ kind: "string",
248
+ shape: "a string",
249
+ describe: "Its sort key; the name when unstated.",
250
+ }),
251
+ Object.freeze({
252
+ name: "common",
253
+ kind: "boolean",
254
+ shape: "`true` or `false`",
255
+ describe: "A word a player is expected to know by heart; `false` when unstated.",
256
+ }),
257
+ Object.freeze({
258
+ name: "definition",
259
+ kind: "string",
260
+ shape: "a string",
261
+ describe:
262
+ "The sense text. Stated, it creates the entry or replaces every sense the " +
263
+ "entry's other sources give it.",
264
+ }),
265
+ Object.freeze({
266
+ name: "pronunciation",
267
+ kind: "string",
268
+ shape: "a respelling such as `sol-MEH-reh`",
269
+ describe: "A respelling, held to the same rule as a word's.",
270
+ }),
271
+ Object.freeze({
272
+ name: "language",
273
+ kind: "address",
274
+ shape: "a language skill Address",
275
+ describe:
276
+ "The language the term is in. Unstated, it is the affiliation's language, its " +
277
+ "parents', or its culture's, or the culture's own.",
278
+ }),
279
+ ]);
280
+
281
+ /** The keys of {@link GLOSSARY_ENTRY_FIELDS}, in declared order. */
282
+ export const GLOSSARY_ENTRY_KEYS = Object.freeze(GLOSSARY_ENTRY_FIELDS.map((field) => field.name));
283
+
284
+ /**
285
+ * Why a pronunciation is not a respelling, or `undefined` when it is one.
286
+ *
287
+ * A respelling is letters and apostrophes in hyphen-separated syllables, with
288
+ * at most one syllable in capitals.
289
+ *
290
+ * @param {string} text
291
+ * @returns {string|undefined}
292
+ */
293
+ export function pronunciationProblem(text) {
294
+ const value = String(text ?? "");
295
+ if (!/^[\p{L}\p{M}'’]+(?:-[\p{L}\p{M}'’]+)*$/u.test(value))
296
+ return "a respelling holds letters, apostrophes and hyphens only, as `sol-MEH-reh`";
297
+ const stressed = value
298
+ .split(/-/)
299
+ .filter(
300
+ (syllable) =>
301
+ syllable !== syllable.toLowerCase() && syllable === syllable.toUpperCase(),
302
+ );
303
+ if (stressed.length > 1)
304
+ return `a respelling stresses one syllable, and this capitalises ${stressed.length}`;
305
+ return undefined;
306
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.5.1",
3
+ "version": "23.6.0",
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",
@@ -316,11 +316,13 @@ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectori
316
316
  * `paths.contentIndex`.
317
317
  * @param {object} [options.config] - A resolved configuration; loaded when omitted.
318
318
  * @returns {{file: string, notes: number, assets: number, records: number,
319
- * bytes: number, full: number, draft: number, stub: number}} Where it was
320
- * written, how many notes and how many assets it holds, how many records that
321
- * is in all, its size, and the ladder those notes sit on — so the ratio of
322
- * written to unwritten is visible on every build rather than discovered in a
323
- * year.
319
+ * bytes: number, full: number, draft: number, stub: number,
320
+ * dictionary: {file: string, rows: number}, terms: {file: string, rows: number}}}
321
+ * Where it was written, how many notes and how many assets it holds, how many
322
+ * records that is in all, its size, and the ladder those notes sit on — so
323
+ * the ratio of written to unwritten is visible on every build rather than
324
+ * discovered in a year — and where the `dictionary` and `terms` datafiles
325
+ * were written beside it, with their row counts.
324
326
  * @throws {Error} When the content tree is absent, or when it yields no note at
325
327
  * all — an empty index is indistinguishable from a mis-pointed tree, and a
326
328
  * reader would take it as the authoritative statement that this package has
@@ -339,6 +341,14 @@ export function emitContentIndex({ contentBase, outDir, config }?: {
339
341
  full: number;
340
342
  draft: number;
341
343
  stub: number;
344
+ dictionary: {
345
+ file: string;
346
+ rows: number;
347
+ };
348
+ terms: {
349
+ file: string;
350
+ rows: number;
351
+ };
342
352
  };
343
353
  import { collectAnchors } from "./anchors.mjs";
344
354
  import { authoredFrontmatter } from "./index-records.mjs";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A value's canonical Address as a string, whether the note holds it parsed or
3
+ * as written, or `undefined` where it is no Address.
4
+ *
5
+ * @param {unknown} value - The value.
6
+ * @param {object|undefined} index - The index, for the package and the types.
7
+ * @param {string} type - The type a bare shortcode defaults to.
8
+ * @returns {string|undefined} The Address.
9
+ */
10
+ export function addressKeyOf(value: unknown, index: object | undefined, type: string): string | undefined;
11
+ /** A note's own Address, keyed as a reference to it is: in the `note` system. */
12
+ export function ownKey(note: any, index: any): string | undefined;
13
+ /**
14
+ * The language a glossary entry on a note resolves to.
15
+ *
16
+ * @param {object} index - The link index.
17
+ * @param {object} note - The culture or affiliation the entry is written on.
18
+ * @param {object|undefined} entry - The entry, or nothing for the note's own.
19
+ * @returns {string|undefined} The language's Address.
20
+ */
21
+ export function entryLanguage(index: object, note: object, entry: object | undefined): string | undefined;
22
+ /**
23
+ * The glossary corpus of an index.
24
+ *
25
+ * @param {object} index - The link index: `notes`, `contentPackage`, `types`.
26
+ * @returns {GlossaryCorpus}
27
+ */
28
+ export function glossaryCorpus(index: object): GlossaryCorpus;
29
+ /**
30
+ * A culture's or an affiliation's terms, by NFC name, each with the sense
31
+ * texts its sources give.
32
+ *
33
+ * @param {object} index - The link index.
34
+ * @param {object} note - The culture or affiliation note.
35
+ * @returns {Map<string, string[]>} The terms; empty for a note no source reaches.
36
+ */
37
+ export function noteTerms(index: object, note: object): Map<string, string[]>;
38
+ /**
39
+ * Warn of an affiliation that reaches no culture, its own or one it inherits,
40
+ * in a tree that describes peoples.
41
+ *
42
+ * @param {object} note - An `affiliation` note.
43
+ * @param {{index?: object}} [context]
44
+ * @returns {object[]} One warning, at the note's `type`, or none.
45
+ */
46
+ export function checkAffiliationReach(note: object, { index }?: {
47
+ index?: object;
48
+ }): object[];
49
+ export function isCultureNote(note: any): boolean;
50
+ export function isLanguageNote(note: any): boolean;
51
+ export function glossaryName(name: any): string;
52
+ /**
53
+ * What the index says about each culture's and affiliation's terms, read once.
54
+ */
55
+ export type GlossaryCorpus = {
56
+ /**
57
+ * - The tree holds a culture note.
58
+ */
59
+ describesPeoples: boolean;
60
+ /**
61
+ * - Each culture's and
62
+ * affiliation's terms by NFC name, each with the sense texts its sources
63
+ * give it.
64
+ */
65
+ terms: Map<string, Map<string, string[]>>;
66
+ };