@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.
- package/CHANGELOG.md +25 -0
- package/bin/content-commands.mjs +23 -5
- package/docs/api.md +9 -0
- package/docs/authoring/links-and-markup.md +77 -12
- package/docs/content-format.md +16 -0
- package/docs/guides/reference-pages.md +89 -0
- package/docs/reference/format-details.md +533 -84
- package/docs/reference/note-types.md +191 -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 +340 -0
- package/engine/dictionary-rows.mjs +608 -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 +360 -0
- package/engine/literature-notes.mjs +10 -8
- package/engine/note-addresses.mjs +25 -0
- package/engine/note-vocabulary.mjs +161 -1
- package/engine/path-charset.mjs +120 -0
- package/engine/sql-tables.mjs +311 -21
- package/engine/word-classes.mjs +167 -0
- package/engine/word-rows.mjs +233 -0
- package/engine/word-terms.mjs +306 -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 +69 -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 +7 -2
- package/types/engine/word-classes.d.mts +87 -0
- package/types/engine/word-rows.d.mts +64 -0
- package/types/engine/word-terms.d.mts +68 -0
|
@@ -56,6 +56,8 @@ types:
|
|
|
56
56
|
- lore
|
|
57
57
|
- parents
|
|
58
58
|
- culture
|
|
59
|
+
- language
|
|
60
|
+
- glossary
|
|
59
61
|
- relations
|
|
60
62
|
subTypes:
|
|
61
63
|
- guild
|
|
@@ -203,6 +205,12 @@ types:
|
|
|
203
205
|
- masteryLevel
|
|
204
206
|
- parentSkill
|
|
205
207
|
- names
|
|
208
|
+
- family
|
|
209
|
+
- abbreviation
|
|
210
|
+
- classModifiers
|
|
211
|
+
- fields
|
|
212
|
+
- words
|
|
213
|
+
- affixes
|
|
206
214
|
subTypes:
|
|
207
215
|
- social
|
|
208
216
|
- nature
|
|
@@ -261,6 +269,7 @@ types:
|
|
|
261
269
|
- language
|
|
262
270
|
- events
|
|
263
271
|
- appearanceProfile
|
|
272
|
+
- glossary
|
|
264
273
|
subTypes:
|
|
265
274
|
- cosmology
|
|
266
275
|
- deity
|
package/engine/content-index.mjs
CHANGED
|
@@ -112,6 +112,7 @@ import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
|
112
112
|
import { addressSlug, canonicalKey } from "./content-address.mjs";
|
|
113
113
|
import { isAddressTuple, ownDocumentSystem, readCanonicalKey, renderAddress } from "./address.mjs";
|
|
114
114
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
115
|
+
import { dictionaryAndTermRows, serializeRows } from "./dictionary-rows.mjs";
|
|
115
116
|
import { assetAddressIndex } from "./art-fields.mjs";
|
|
116
117
|
import { embedRole } from "./content-embeds.mjs";
|
|
117
118
|
// One reader for a note's anchors, shared with the link checker and with the
|
|
@@ -1396,11 +1397,13 @@ export function indexRecordsFor({
|
|
|
1396
1397
|
* `paths.contentIndex`.
|
|
1397
1398
|
* @param {object} [options.config] - A resolved configuration; loaded when omitted.
|
|
1398
1399
|
* @returns {{file: string, notes: number, assets: number, records: number,
|
|
1399
|
-
* bytes: number, full: number, draft: number, stub: number
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1400
|
+
* bytes: number, full: number, draft: number, stub: number,
|
|
1401
|
+
* dictionary: {file: string, rows: number}, terms: {file: string, rows: number}}}
|
|
1402
|
+
* Where it was written, how many notes and how many assets it holds, how many
|
|
1403
|
+
* records that is in all, its size, and the ladder those notes sit on — so
|
|
1404
|
+
* the ratio of written to unwritten is visible on every build rather than
|
|
1405
|
+
* discovered in a year — and where the `dictionary` and `terms` datafiles
|
|
1406
|
+
* were written beside it, with their row counts.
|
|
1404
1407
|
* @throws {Error} When the content tree is absent, or when it yields no note at
|
|
1405
1408
|
* all — an empty index is indistinguishable from a mis-pointed tree, and a
|
|
1406
1409
|
* reader would take it as the authoritative statement that this package has
|
|
@@ -1443,6 +1446,16 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
1443
1446
|
fs.mkdirSync(dir, { recursive: true });
|
|
1444
1447
|
fs.writeFileSync(file, text);
|
|
1445
1448
|
|
|
1449
|
+
// The `dictionary` and `terms` relations as datafiles beside the index, for
|
|
1450
|
+
// `jq`, an editor or another package. Nothing in the build reads them back.
|
|
1451
|
+
const derived = dictionaryAndTermRows(records.map((record) => encodeAddresses(record)));
|
|
1452
|
+
const datafiles = {};
|
|
1453
|
+
for (const relation of ["dictionary", "terms"]) {
|
|
1454
|
+
const rowsFile = path.join(dir, `${contentPackage}-${relation}.jsonl`);
|
|
1455
|
+
fs.writeFileSync(rowsFile, serializeRows(derived[relation]));
|
|
1456
|
+
datafiles[relation] = { file: rowsFile, rows: derived[relation].length };
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1446
1459
|
// Counted separately because they are genuinely different numbers: an item
|
|
1447
1460
|
// note yields a second record for its documentation journal and a file
|
|
1448
1461
|
// yields an asset record, so reporting records as notes would overstate how
|
|
@@ -1463,5 +1476,6 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
1463
1476
|
full: noteRecords.length - stub - draft,
|
|
1464
1477
|
draft,
|
|
1465
1478
|
stub,
|
|
1479
|
+
...datafiles,
|
|
1466
1480
|
};
|
|
1467
1481
|
}
|
|
@@ -0,0 +1,330 @@
|
|
|
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
|
+
* Which terms reach a culture or an affiliation, and whether an affiliation
|
|
16
|
+
* reaches a culture at all.
|
|
17
|
+
*
|
|
18
|
+
* A culture's terms are the words of its language, and the ranks, offices and
|
|
19
|
+
* language words of every affiliation that reaches it — through its own
|
|
20
|
+
* `data.culture`, the one the index resolves for it, or the culture of an
|
|
21
|
+
* affiliation above it through `data.parents` —
|
|
22
|
+
* each affiliation's language being the one its glossary resolves to
|
|
23
|
+
* ({@link module:engine/glossary-language}). An affiliation's terms are its own
|
|
24
|
+
* ranks and offices and the words of that language. A language's words include
|
|
25
|
+
* the words it inherits through `data.family`, and a word is a term only where
|
|
26
|
+
* its class's base is a glossary base. The `dictionary` relation
|
|
27
|
+
* ({@link module:engine/dictionary-rows}) names a glossary entry's term from
|
|
28
|
+
* the same reach.
|
|
29
|
+
*
|
|
30
|
+
* **An affiliation that reaches no culture is a warning**, at its `type`, in a
|
|
31
|
+
* tree that describes peoples, which is a tree holding at least one culture
|
|
32
|
+
* note. A tree holding none describes no peoples, and the rule does not speak
|
|
33
|
+
* there.
|
|
34
|
+
*
|
|
35
|
+
* The corpus is read once per index and kept beside it.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
|
|
41
|
+
import { AddressLink } from "./address-values.mjs";
|
|
42
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
43
|
+
import { glossaryLanguage } from "./glossary-language.mjs";
|
|
44
|
+
import { parseWordClass, WORD_CLASS_BASES } from "./word-classes.mjs";
|
|
45
|
+
import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
|
|
46
|
+
|
|
47
|
+
/** The bases whose words are glossary terms. */
|
|
48
|
+
const GLOSSARY_BASES = new Set(
|
|
49
|
+
WORD_CLASS_BASES.filter((item) => item.glossary).map((item) => item.token),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/** A note's parsed frontmatter, whichever shape the index hands over. */
|
|
53
|
+
const payload = (note) => note?.fm ?? note ?? {};
|
|
54
|
+
|
|
55
|
+
/** A note's `data`, or an empty map. */
|
|
56
|
+
const dataOf = (note) => {
|
|
57
|
+
const data = payload(note).data;
|
|
58
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** A note's type, lowercased. */
|
|
62
|
+
const typeOf = (note) => String(note?.type ?? payload(note).type ?? "").toLowerCase();
|
|
63
|
+
|
|
64
|
+
/** A note's subType, lowercased. */
|
|
65
|
+
const subTypeOf = (note) => String(payload(note).subType ?? "").toLowerCase();
|
|
66
|
+
|
|
67
|
+
/** Whether a note is a culture. */
|
|
68
|
+
export const isCultureNote = (note) => typeOf(note) === "lore" && subTypeOf(note) === "culture";
|
|
69
|
+
|
|
70
|
+
/** Whether a note is a language. */
|
|
71
|
+
export const isLanguageNote = (note) =>
|
|
72
|
+
typeOf(note) === "skill" && subTypeOf(note) === LANGUAGE_SUBTYPE;
|
|
73
|
+
|
|
74
|
+
/** A name as the glossary compares it: NFC, case kept. */
|
|
75
|
+
export const glossaryName = (name) => String(name ?? "").normalize("NFC");
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A value's canonical Address as a string, whether the note holds it parsed or
|
|
79
|
+
* as written, or `undefined` where it is no Address.
|
|
80
|
+
*
|
|
81
|
+
* @param {unknown} value - The value.
|
|
82
|
+
* @param {object|undefined} index - The index, for the package and the types.
|
|
83
|
+
* @param {string} type - The type a bare shortcode defaults to.
|
|
84
|
+
* @returns {string|undefined} The Address.
|
|
85
|
+
*/
|
|
86
|
+
export function addressKeyOf(value, index, type) {
|
|
87
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
88
|
+
if (isAddressTuple(value)) return renderAddress(value);
|
|
89
|
+
if (value instanceof AddressLink) return renderAddress(value.target);
|
|
90
|
+
if (typeof value !== "string") return undefined;
|
|
91
|
+
const tuple = parseAddress(value, {
|
|
92
|
+
package: index?.contentPackage,
|
|
93
|
+
system: "note",
|
|
94
|
+
type,
|
|
95
|
+
types: index?.types,
|
|
96
|
+
packages: index?.packages,
|
|
97
|
+
});
|
|
98
|
+
return tuple.reason ? undefined : renderAddress(tuple);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A note's own Address, keyed as a reference to it is: in the `note` system. */
|
|
102
|
+
export function ownKey(note, index) {
|
|
103
|
+
const shortcode = payload(note).shortcode;
|
|
104
|
+
if (typeof shortcode !== "string" || !shortcode || !index?.contentPackage) return undefined;
|
|
105
|
+
return renderAddress({
|
|
106
|
+
package: index.contentPackage,
|
|
107
|
+
system: "note",
|
|
108
|
+
type: typeOf(note),
|
|
109
|
+
shortcode,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add a sense to a culture's or an affiliation's term.
|
|
115
|
+
*
|
|
116
|
+
* @param {Map<string, Map<string, string[]>>} terms - Note to name to senses.
|
|
117
|
+
* @param {string} culture - The note's Address.
|
|
118
|
+
* @param {unknown} name - The term.
|
|
119
|
+
* @param {unknown} sense - Its sense text.
|
|
120
|
+
*/
|
|
121
|
+
function addTerm(terms, culture, name, sense) {
|
|
122
|
+
if (typeof name !== "string" || !name.trim()) return;
|
|
123
|
+
if (!terms.has(culture)) terms.set(culture, new Map());
|
|
124
|
+
const byName = terms.get(culture);
|
|
125
|
+
const key = glossaryName(name);
|
|
126
|
+
if (!byName.has(key)) byName.set(key, []);
|
|
127
|
+
if (typeof sense === "string") byName.get(key).push(sense);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The glossary-base words of every local language note, keyed by the note.
|
|
132
|
+
*
|
|
133
|
+
* @param {readonly object[]} notes - The index's notes.
|
|
134
|
+
* @param {object} index - The index.
|
|
135
|
+
* @returns {Map<string, Array<{form: string, gloss: unknown}>>} Language to its
|
|
136
|
+
* glossary-base words.
|
|
137
|
+
*/
|
|
138
|
+
function languageWords(notes, index) {
|
|
139
|
+
const out = new Map();
|
|
140
|
+
for (const note of notes) {
|
|
141
|
+
if (!isLanguageNote(note)) continue;
|
|
142
|
+
const words = dataOf(note).words;
|
|
143
|
+
if (!Array.isArray(words)) continue;
|
|
144
|
+
const language = ownKey(note, index);
|
|
145
|
+
if (!language) continue;
|
|
146
|
+
for (const word of words) {
|
|
147
|
+
if (!word || typeof word !== "object" || typeof word.form !== "string") continue;
|
|
148
|
+
const parsed = parseWordClass(word.class);
|
|
149
|
+
if (parsed.reason || !GLOSSARY_BASES.has(parsed.base)) continue;
|
|
150
|
+
if (!out.has(language)) out.set(language, []);
|
|
151
|
+
out.get(language).push({ form: word.form, gloss: word.gloss });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* How the lint reads Addresses and notes for {@link glossaryLanguage}.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} index - The link index.
|
|
161
|
+
* @param {object} note - The owning note.
|
|
162
|
+
* @returns {import("./glossary-language.mjs").GlossaryLanguageReader}
|
|
163
|
+
*/
|
|
164
|
+
function readerOf(index, note) {
|
|
165
|
+
return {
|
|
166
|
+
keyOf: (value, type) => addressKeyOf(value, index, type),
|
|
167
|
+
lookup: (key) => index.addressHit?.(key),
|
|
168
|
+
self: ownKey(note, index),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The language a glossary entry on a note resolves to.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} index - The link index.
|
|
176
|
+
* @param {object} note - The culture or affiliation the entry is written on.
|
|
177
|
+
* @param {object|undefined} entry - The entry, or nothing for the note's own.
|
|
178
|
+
* @returns {string|undefined} The language's Address.
|
|
179
|
+
*/
|
|
180
|
+
export function entryLanguage(index, note, entry) {
|
|
181
|
+
return glossaryLanguage(entry, note, readerOf(index, note));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* What the index says about each culture's and affiliation's terms, read once.
|
|
186
|
+
*
|
|
187
|
+
* @typedef {object} GlossaryCorpus
|
|
188
|
+
* @property {boolean} describesPeoples - The tree holds a culture note.
|
|
189
|
+
* @property {Map<string, Map<string, string[]>>} terms - Each culture's and
|
|
190
|
+
* affiliation's terms by NFC name, each with the sense texts its sources
|
|
191
|
+
* give it.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/** @type {WeakMap<object, GlossaryCorpus>} */
|
|
195
|
+
const CORPUS = new WeakMap();
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Add an affiliation's ranks and offices to a note's terms.
|
|
199
|
+
*
|
|
200
|
+
* @param {Map<string, Map<string, string[]>>} terms - Note to name to senses.
|
|
201
|
+
* @param {string} key - The note the terms belong to.
|
|
202
|
+
* @param {object} data - The affiliation's `data`.
|
|
203
|
+
*/
|
|
204
|
+
function addGovernance(terms, key, data) {
|
|
205
|
+
const ranks = data.governance?.ranks;
|
|
206
|
+
for (const rung of Array.isArray(ranks) ? ranks : [])
|
|
207
|
+
if (rung && typeof rung === "object") addTerm(terms, key, rung.title, rung.description);
|
|
208
|
+
const offices = data.governance?.offices;
|
|
209
|
+
if (offices && typeof offices === "object" && !Array.isArray(offices))
|
|
210
|
+
for (const [title, office] of Object.entries(offices))
|
|
211
|
+
addTerm(terms, key, title, typeof office === "string" ? office : office?.description);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Every culture an affiliation's terms reach: its own, and the culture of each
|
|
216
|
+
* affiliation above it through `data.parents`.
|
|
217
|
+
*
|
|
218
|
+
* @param {object} index - The link index.
|
|
219
|
+
* @param {object} note - The affiliation.
|
|
220
|
+
* @returns {string[]} The cultures' Addresses.
|
|
221
|
+
*/
|
|
222
|
+
function reachedCultures(index, note) {
|
|
223
|
+
const out = new Set();
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
const queue = [note];
|
|
226
|
+
while (queue.length) {
|
|
227
|
+
const body = queue.shift();
|
|
228
|
+
if (!body || seen.has(body)) continue;
|
|
229
|
+
seen.add(body);
|
|
230
|
+
const data = dataOf(body);
|
|
231
|
+
const culture = addressKeyOf(data.culture, index, "lore");
|
|
232
|
+
if (culture) out.add(culture);
|
|
233
|
+
for (const parent of Array.isArray(data.parents) ? data.parents : []) {
|
|
234
|
+
const key = addressKeyOf(parent, index, "affiliation");
|
|
235
|
+
const target = key ? index.addressHit?.(key) : undefined;
|
|
236
|
+
if (target && typeOf(target) === "affiliation") queue.push(target);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return [...out];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The glossary corpus of an index.
|
|
244
|
+
*
|
|
245
|
+
* @param {object} index - The link index: `notes`, `contentPackage`, `types`.
|
|
246
|
+
* @returns {GlossaryCorpus}
|
|
247
|
+
*/
|
|
248
|
+
export function glossaryCorpus(index) {
|
|
249
|
+
const cached = CORPUS.get(index);
|
|
250
|
+
if (cached) return cached;
|
|
251
|
+
const notes = index.notes ?? [];
|
|
252
|
+
const words = languageWords(notes, index);
|
|
253
|
+
const families = new Map();
|
|
254
|
+
for (const note of notes)
|
|
255
|
+
if (isLanguageNote(note)) {
|
|
256
|
+
const key = ownKey(note, index);
|
|
257
|
+
if (key) families.set(key, addressKeyOf(dataOf(note).family, index, "skill"));
|
|
258
|
+
}
|
|
259
|
+
const terms = new Map();
|
|
260
|
+
// A language's words include those it inherits through `data.family`.
|
|
261
|
+
const addWords = (key, language) => {
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
for (
|
|
264
|
+
let current = language;
|
|
265
|
+
current && !seen.has(current);
|
|
266
|
+
current = families.get(current)
|
|
267
|
+
) {
|
|
268
|
+
seen.add(current);
|
|
269
|
+
for (const word of words.get(current) ?? []) addTerm(terms, key, word.form, word.gloss);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
for (const note of notes) {
|
|
273
|
+
const culture = isCultureNote(note);
|
|
274
|
+
if (!culture && typeOf(note) !== "affiliation") continue;
|
|
275
|
+
const key = ownKey(note, index);
|
|
276
|
+
if (!key) continue;
|
|
277
|
+
if (!terms.has(key)) terms.set(key, new Map());
|
|
278
|
+
const data = dataOf(note);
|
|
279
|
+
if (culture) {
|
|
280
|
+
addWords(key, addressKeyOf(data.language, index, "skill"));
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
addGovernance(terms, key, data);
|
|
284
|
+
addWords(key, entryLanguage(index, note, undefined));
|
|
285
|
+
for (const home of reachedCultures(index, note)) {
|
|
286
|
+
addGovernance(terms, home, data);
|
|
287
|
+
addWords(home, entryLanguage(index, note, undefined));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const corpus = { describesPeoples: notes.some(isCultureNote), terms };
|
|
291
|
+
CORPUS.set(index, corpus);
|
|
292
|
+
return corpus;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* A culture's or an affiliation's terms, by NFC name, each with the sense
|
|
297
|
+
* texts its sources give.
|
|
298
|
+
*
|
|
299
|
+
* @param {object} index - The link index.
|
|
300
|
+
* @param {object} note - The culture or affiliation note.
|
|
301
|
+
* @returns {Map<string, string[]>} The terms; empty for a note no source reaches.
|
|
302
|
+
*/
|
|
303
|
+
export function noteTerms(index, note) {
|
|
304
|
+
const key = ownKey(note, index);
|
|
305
|
+
return (key && glossaryCorpus(index).terms.get(key)) || new Map();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Warn of an affiliation that reaches no culture, its own or one it inherits,
|
|
310
|
+
* in a tree that describes peoples.
|
|
311
|
+
*
|
|
312
|
+
* @param {object} note - An `affiliation` note.
|
|
313
|
+
* @param {{index?: object}} [context]
|
|
314
|
+
* @returns {object[]} One warning, at the note's `type`, or none.
|
|
315
|
+
*/
|
|
316
|
+
export function checkAffiliationReach(note, { index } = {}) {
|
|
317
|
+
if (!index?.notes || !glossaryCorpus(index).describesPeoples) return [];
|
|
318
|
+
if (addressKeyOf(dataOf(note).culture, index, "lore")) return [];
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
file: note.file,
|
|
322
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["type"]),
|
|
323
|
+
severity: "warning",
|
|
324
|
+
message:
|
|
325
|
+
"this affiliation reaches no culture, its own or one inherited through " +
|
|
326
|
+
"`data.parents`, so its ranks, offices and language enter no culture's glossary; " +
|
|
327
|
+
"state `data.culture`",
|
|
328
|
+
},
|
|
329
|
+
];
|
|
330
|
+
}
|