@heroiclands/package-build 23.8.0 → 23.10.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 +53 -0
- package/README.md +2 -0
- package/bin/content-commands.mjs +207 -70
- package/docs/api.md +646 -78
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/links-and-markup.md +253 -102
- package/docs/commands.md +111 -13
- package/docs/content-format.md +7 -2
- package/docs/diagnostics.md +1 -1
- package/docs/guides/packs.md +2 -1
- package/docs/guides/reference-pages.md +86 -63
- package/docs/reference/format-details.md +398 -137
- package/docs/reference/note-types.md +188 -174
- package/engine/anchors.mjs +15 -26
- package/engine/base-compiler.mjs +6 -4
- package/engine/changelog-group.mjs +58 -34
- package/engine/changelog-lint.mjs +6 -2
- package/engine/compendiums.mjs +15 -1
- package/engine/compile-corpus.mjs +17 -3
- package/engine/content-charset.mjs +13 -1
- package/engine/content-format.yaml +1 -0
- package/engine/content-html.mjs +18 -4
- package/engine/content-icons.mjs +15 -1
- package/engine/content-images.mjs +28 -9
- package/engine/content-index.mjs +496 -16
- package/engine/content-links.mjs +70 -27
- package/engine/content-session.mjs +247 -0
- package/engine/content-sources.mjs +110 -0
- package/engine/content-spans.mjs +87 -8
- package/engine/content-tables.mjs +9 -4
- package/engine/content-tasklists.mjs +20 -4
- package/engine/culture-reach.mjs +61 -34
- package/engine/data-keys.mjs +3 -1
- package/engine/diagnostics.mjs +17 -6
- package/engine/dictionary-layout.mjs +151 -19
- package/engine/dictionary-rows.mjs +129 -79
- package/engine/extension-attributes.mjs +28 -5
- package/engine/first-use.mjs +371 -0
- package/engine/foundry-entries.mjs +29 -8
- package/engine/generate.mjs +13 -4
- package/engine/glossary-entries.mjs +11 -11
- package/engine/heading-attributes.mjs +21 -28
- package/engine/helpers.mjs +54 -19
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +3 -0
- package/engine/journals.mjs +13 -6
- package/engine/language-data.mjs +55 -2
- package/engine/language-rows.mjs +4 -4
- package/engine/language-terms.mjs +90 -4
- package/engine/language-words.mjs +2 -2
- package/engine/lexical-sections.mjs +247 -0
- package/engine/markup-validation.mjs +7 -2
- package/engine/metadata-index.mjs +1 -0
- package/engine/name-markup.mjs +114 -0
- package/engine/name-rows.mjs +70 -0
- package/engine/names.mjs +460 -0
- package/engine/note-addresses.mjs +25 -4
- package/engine/note-claims.mjs +2 -0
- package/engine/note-state.mjs +9 -3
- package/engine/note-vocabulary.mjs +7 -6
- package/engine/path-charset.mjs +24 -1
- package/engine/pdf-build.mjs +9 -9
- package/engine/pdf-render.mjs +15 -1
- package/engine/site-build.mjs +16 -2
- package/engine/site-index.mjs +5 -1
- package/engine/sql-tables.mjs +324 -44
- package/engine/stub-lint.mjs +10 -5
- package/engine/web-wikilinks.mjs +8 -6
- package/engine/word-rows.mjs +20 -12
- package/engine/word-terms.mjs +15 -6
- package/engine/yaml-lint.mjs +3 -5
- package/hugo-theme/static/css/style.css +4 -0
- package/package.json +1 -1
- package/types/engine/anchors.d.mts +4 -2
- package/types/engine/compendiums.d.mts +5 -1
- package/types/engine/compile-corpus.d.mts +5 -2
- package/types/engine/content-charset.d.mts +5 -1
- package/types/engine/content-html.d.mts +5 -1
- package/types/engine/content-icons.d.mts +5 -1
- package/types/engine/content-images.d.mts +5 -1
- package/types/engine/content-index.d.mts +102 -18
- package/types/engine/content-links.d.mts +22 -8
- package/types/engine/content-session.d.mts +108 -0
- package/types/engine/content-sources.d.mts +32 -0
- package/types/engine/content-spans.d.mts +3 -5
- package/types/engine/content-tables.d.mts +2 -1
- package/types/engine/content-tasklists.d.mts +5 -1
- package/types/engine/culture-reach.d.mts +22 -2
- package/types/engine/data-keys.d.mts +7 -0
- package/types/engine/diagnostics.d.mts +10 -0
- package/types/engine/dictionary-layout.d.mts +35 -4
- package/types/engine/dictionary-rows.d.mts +9 -0
- package/types/engine/extension-attributes.d.mts +16 -2
- package/types/engine/first-use.d.mts +31 -0
- package/types/engine/foundry-entries.d.mts +14 -5
- package/types/engine/generate.d.mts +5 -1
- package/types/engine/heading-attributes.d.mts +2 -2
- package/types/engine/helpers.d.mts +33 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/journals.d.mts +5 -2
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/lexical-sections.d.mts +96 -0
- package/types/engine/markup-validation.d.mts +2 -1
- package/types/engine/name-markup.d.mts +176 -0
- package/types/engine/name-rows.d.mts +35 -0
- package/types/engine/names.d.mts +356 -0
- package/types/engine/note-addresses.d.mts +3 -3
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/path-charset.d.mts +16 -1
- package/types/engine/site-build.d.mts +6 -3
- package/types/engine/site-index.d.mts +3 -1
- package/types/engine/sql-tables.d.mts +39 -4
- package/types/engine/stub-lint.d.mts +3 -1
- package/types/engine/word-rows.d.mts +9 -7
|
@@ -22,18 +22,21 @@
|
|
|
22
22
|
* surface sets it as it sets a note:
|
|
23
23
|
*
|
|
24
24
|
* - **the key**, once, at the head unless the fence sets `key=false`: the abbreviations this rendering uses,
|
|
25
|
-
* each with its expansion, then
|
|
25
|
+
* each with its expansion, then a glossary's common mark where an entry carries it.
|
|
26
26
|
* A fence setting `key=only` prints the key alone;
|
|
27
27
|
* - **a heading per letter**, from `letter`, at the fence's `section-level`;
|
|
28
28
|
* - **one paragraph per entry**: the common mark, the bold headword, the
|
|
29
|
-
* pronunciation between slashes, the language tag
|
|
30
|
-
*
|
|
29
|
+
* pronunciation between slashes, the language tag and word class in
|
|
30
|
+
* italics, a dictionary word's field tag and etymology in brackets, the senses, and the
|
|
31
31
|
* citations as links.
|
|
32
32
|
*
|
|
33
33
|
* A glossary sets the rows that share a headword and a language as one entry:
|
|
34
34
|
* their senses numbered in the order the rows arrive, each tagged with the
|
|
35
|
-
* bodies it comes from, a
|
|
36
|
-
*
|
|
35
|
+
* bodies it comes from, except a sole owner's label on its own page. A sense
|
|
36
|
+
* several rows give is printed once with all its bodies. A dictionary word's
|
|
37
|
+
* senses carry no bodies; a glossary's contextual
|
|
38
|
+
* and governance senses name their owners. A glossary headword naming a
|
|
39
|
+
* dictionary word links to that word's anchored entry on its language page.
|
|
37
40
|
*
|
|
38
41
|
* The entries are ordered here rather than by the query: by letter, then by
|
|
39
42
|
* `name_sort` with its accents taken off and its case folded, then as written.
|
|
@@ -43,8 +46,9 @@
|
|
|
43
46
|
* @module
|
|
44
47
|
*/
|
|
45
48
|
|
|
46
|
-
import { parseAddress, renderAddress } from "./address.mjs";
|
|
47
|
-
import { dictionaryLetter } from "./dictionary-rows.mjs";
|
|
49
|
+
import { parseAddress, readCanonicalKey, renderAddress, splitAnchor } from "./address.mjs";
|
|
50
|
+
import { dictionaryEntryAnchor, dictionaryLetter } from "./dictionary-rows.mjs";
|
|
51
|
+
import { VERB_PERSON_FIELDS } from "./language-terms.mjs";
|
|
48
52
|
import {
|
|
49
53
|
WORD_CLASS_BASES,
|
|
50
54
|
WORD_CLASS_MODIFIERS,
|
|
@@ -61,6 +65,50 @@ export const LAYOUT_REQUIRED_COLUMNS = Object.freeze({
|
|
|
61
65
|
/** The mark a common entry opens with. */
|
|
62
66
|
export const COMMON_MARK = "•";
|
|
63
67
|
|
|
68
|
+
/** The word-entry span class whose Foundry rendering is a native TOC heading. */
|
|
69
|
+
export const DICTIONARY_ENTRY_CLASS = "dictionary-entry";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render a dictionary entry's marked headword as an inline native heading in
|
|
73
|
+
* Foundry. Its paragraph becomes a div, so the heading has a valid container.
|
|
74
|
+
* The shared Markdown keeps its span and its containing journal page.
|
|
75
|
+
*
|
|
76
|
+
* @param {import("markdown-it")} md - The Foundry Markdown parser.
|
|
77
|
+
*/
|
|
78
|
+
export function dictionaryEntryPlugin(md) {
|
|
79
|
+
md.core.ruler.after("inline", "dictionary_entry_headings", (state) => {
|
|
80
|
+
for (let i = 1; i < state.tokens.length - 1; i++) {
|
|
81
|
+
const inline = state.tokens[i];
|
|
82
|
+
const opening = inline.children?.[0];
|
|
83
|
+
if (
|
|
84
|
+
inline.type !== "inline" ||
|
|
85
|
+
state.tokens[i - 1].type !== "paragraph_open" ||
|
|
86
|
+
state.tokens[i + 1].type !== "paragraph_close" ||
|
|
87
|
+
opening?.type !== "span_open" ||
|
|
88
|
+
!opening.attrGet("id") ||
|
|
89
|
+
!String(opening.attrGet("class") ?? "")
|
|
90
|
+
.split(/\s+/)
|
|
91
|
+
.includes(DICTIONARY_ENTRY_CLASS)
|
|
92
|
+
)
|
|
93
|
+
continue;
|
|
94
|
+
let depth = 0;
|
|
95
|
+
const closing = inline.children.find((token) => {
|
|
96
|
+
if (token.type === "span_open") depth++;
|
|
97
|
+
if (token.type === "span_close") return --depth === 0;
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
if (!closing) continue;
|
|
101
|
+
opening.tag = closing.tag = "h6";
|
|
102
|
+
opening.attrSet(
|
|
103
|
+
"style",
|
|
104
|
+
"display: inline; margin: 0; padding: 0; border: 0; font: inherit; text-transform: none",
|
|
105
|
+
);
|
|
106
|
+
state.tokens[i - 1].tag = state.tokens[i + 1].tag = "div";
|
|
107
|
+
state.tokens[i - 1].attrSet("style", "margin: 0 0 0.5em");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
64
112
|
/** What the key says of the common mark. */
|
|
65
113
|
const COMMON_KEY = "a common word, which a player is expected to know by heart";
|
|
66
114
|
|
|
@@ -93,10 +141,16 @@ function sentence(text) {
|
|
|
93
141
|
* @property {object} [addressContext] - Address resolution context.
|
|
94
142
|
* @property {(address: string) => string|undefined} [labelOf] - A note's
|
|
95
143
|
* `name.full`, by its Address.
|
|
96
|
-
* @property {boolean|"only"} [showKey=true] - Whether the key
|
|
97
|
-
* `"only"` prints the key and nothing else.
|
|
144
|
+
* @property {boolean|"only"} [showKey=true] - Whether the key and dictionary
|
|
145
|
+
* head tables are printed; `"only"` prints the key and nothing else.
|
|
98
146
|
* @property {string} [pageLanguage] - The language of the page the fence is
|
|
99
147
|
* on, which no entry is tagged with.
|
|
148
|
+
* @property {string} [pageOwner] - The containing page's canonical note Address;
|
|
149
|
+
* a sense attributed only to its own row's owner omits that label on this page.
|
|
150
|
+
* @property {boolean} [entryAnchors=true] - Whether dictionary headwords carry
|
|
151
|
+
* their canonical targets; custom SQL views leave those to the automatic list.
|
|
152
|
+
* @property {object} [verbs] - The owning language's stated pattern, including
|
|
153
|
+
* when its automatic dictionary has no eligible word entries.
|
|
100
154
|
*/
|
|
101
155
|
|
|
102
156
|
/**
|
|
@@ -114,9 +168,56 @@ export function renderDictionary(result, opts = {}) {
|
|
|
114
168
|
result.rows.map((row) => ({ ...row, rows: [row] })),
|
|
115
169
|
opts,
|
|
116
170
|
true,
|
|
171
|
+
verbTables(result.rows, opts),
|
|
117
172
|
);
|
|
118
173
|
}
|
|
119
174
|
|
|
175
|
+
/** A table cell cannot carry a literal pipe or a line break. */
|
|
176
|
+
const verbCell = (text) =>
|
|
177
|
+
String(text)
|
|
178
|
+
.replace(/\|/g, "\\|")
|
|
179
|
+
.replace(/[\r\n]+/g, " ");
|
|
180
|
+
|
|
181
|
+
/** One head table per represented language, with no forms invented for a word. */
|
|
182
|
+
function verbTables(rows, opts) {
|
|
183
|
+
const patterns = new Map();
|
|
184
|
+
for (const row of rows)
|
|
185
|
+
if (row.verbs && typeof row.verbs === "object")
|
|
186
|
+
patterns.set(row.language ?? opts.pageLanguage ?? "", {
|
|
187
|
+
verbs: row.verbs,
|
|
188
|
+
label: row.language_tag,
|
|
189
|
+
});
|
|
190
|
+
if (opts.verbs && !patterns.has(opts.pageLanguage ?? ""))
|
|
191
|
+
patterns.set(opts.pageLanguage ?? "", { verbs: opts.verbs });
|
|
192
|
+
const tables = [];
|
|
193
|
+
for (const [language, { verbs, label }] of [...patterns].sort(([a], [b]) => compare(a, b))) {
|
|
194
|
+
let endings;
|
|
195
|
+
if (verbs.invariant === true) endings = [["All persons", "unchanged"]];
|
|
196
|
+
else if (
|
|
197
|
+
VERB_PERSON_FIELDS.every((person) => typeof verbs.persons?.[person.name] === "string")
|
|
198
|
+
)
|
|
199
|
+
endings = VERB_PERSON_FIELDS.map((person) => [
|
|
200
|
+
person.label,
|
|
201
|
+
verbs.persons[person.name] ? `-${verbs.persons[person.name]}` : "unchanged",
|
|
202
|
+
]);
|
|
203
|
+
else continue;
|
|
204
|
+
const title =
|
|
205
|
+
patterns.size > 1 ?
|
|
206
|
+
`**Verbs (${String(opts.labelOf?.(language) ?? label ?? language).replace(/[\r\n]+/g, " ")})**`
|
|
207
|
+
: "**Verbs**";
|
|
208
|
+
tables.push(
|
|
209
|
+
[
|
|
210
|
+
title,
|
|
211
|
+
"",
|
|
212
|
+
"| Person | Ending added to citation form |",
|
|
213
|
+
"| --- | --- |",
|
|
214
|
+
...endings.map((ending) => `| ${ending.map(verbCell).join(" | ")} |`),
|
|
215
|
+
].join("\n"),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return tables.join("\n\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
120
221
|
/**
|
|
121
222
|
* Render a result of terms as a glossary.
|
|
122
223
|
*
|
|
@@ -141,10 +242,18 @@ export function renderGlossary(result, opts = {}) {
|
|
|
141
242
|
for (const sense of Array.isArray(row.senses) ? row.senses : []) {
|
|
142
243
|
const same = senses.find((one) => one.text === sense?.text);
|
|
143
244
|
const bodies = Array.isArray(sense?.bodies) ? sense.bodies : [];
|
|
144
|
-
|
|
145
|
-
|
|
245
|
+
const omitBody = Boolean(
|
|
246
|
+
opts.pageOwner &&
|
|
247
|
+
row.owner === opts.pageOwner &&
|
|
248
|
+
bodies.length === 1 &&
|
|
249
|
+
bodies[0] === row.owner,
|
|
250
|
+
);
|
|
251
|
+
if (!same) senses.push({ text: sense?.text, bodies: [...bodies], omitBody });
|
|
252
|
+
else {
|
|
253
|
+
same.omitBody &&= omitBody;
|
|
146
254
|
for (const body of bodies)
|
|
147
255
|
if (!same.bodies.includes(body)) same.bodies.push(body);
|
|
256
|
+
}
|
|
148
257
|
}
|
|
149
258
|
for (const sense of senses) sense.bodies.sort(compare);
|
|
150
259
|
const first = (column) =>
|
|
@@ -157,7 +266,8 @@ export function renderGlossary(result, opts = {}) {
|
|
|
157
266
|
pronunciation: first("pronunciation"),
|
|
158
267
|
name_sort: first("name_sort"),
|
|
159
268
|
letter: first("letter"),
|
|
160
|
-
class:
|
|
269
|
+
class: first("class"),
|
|
270
|
+
word: first("word"),
|
|
161
271
|
etymology: undefined,
|
|
162
272
|
};
|
|
163
273
|
}),
|
|
@@ -181,8 +291,9 @@ function requireColumns(result, layout) {
|
|
|
181
291
|
*
|
|
182
292
|
* @param {object[]} entries - One per entry, with the row columns.
|
|
183
293
|
* @param {LayoutOptions} opts
|
|
184
|
-
* @param {boolean} words - Whether the entries are words, with
|
|
185
|
-
* etymology.
|
|
294
|
+
* @param {boolean} words - Whether the entries are dictionary words, with
|
|
295
|
+
* their own anchors and etymology.
|
|
296
|
+
* @param {string} [head] - Optional dictionary head tables.
|
|
186
297
|
* @returns {string}
|
|
187
298
|
*/
|
|
188
299
|
function render(
|
|
@@ -194,14 +305,17 @@ function render(
|
|
|
194
305
|
labelOf = () => undefined,
|
|
195
306
|
showKey = true,
|
|
196
307
|
pageLanguage,
|
|
308
|
+
entryAnchors = true,
|
|
197
309
|
},
|
|
198
310
|
words,
|
|
311
|
+
head = "",
|
|
199
312
|
) {
|
|
200
313
|
const label = (address) => labelOf(address) ?? address;
|
|
201
314
|
const usedTags = new Map();
|
|
202
315
|
const usedBases = new Set();
|
|
203
316
|
const usedModifiers = new Set();
|
|
204
317
|
let anyCommon = false;
|
|
318
|
+
let anyPlural = false;
|
|
205
319
|
|
|
206
320
|
const cite = (address) => {
|
|
207
321
|
const text = label(address);
|
|
@@ -236,25 +350,40 @@ function render(
|
|
|
236
350
|
paragraphs.push(`${"#".repeat(sectionLevel)} ${letter}`);
|
|
237
351
|
}
|
|
238
352
|
const parts = [];
|
|
239
|
-
if (row.common === true) {
|
|
353
|
+
if (!words && row.common === true) {
|
|
240
354
|
anyCommon = true;
|
|
241
355
|
parts.push(COMMON_MARK);
|
|
242
356
|
}
|
|
243
|
-
|
|
357
|
+
let headword = `**${row.name}**`;
|
|
358
|
+
if (words && entryAnchors && typeof row.language === "string")
|
|
359
|
+
headword = `[${headword}]{#${dictionaryEntryAnchor(row.language, row.name)} .${DICTIONARY_ENTRY_CLASS}}`;
|
|
360
|
+
else if (!words && typeof row.word === "string") {
|
|
361
|
+
const { address, anchor } = splitAnchor(row.word);
|
|
362
|
+
const ref = readCanonicalKey(address);
|
|
363
|
+
if (ref && anchor && linkable(ref))
|
|
364
|
+
headword = `**[[${renderAddress(ref)}#${anchor}|${String(row.name).replace(/\|/g, "/")}]]**`;
|
|
365
|
+
}
|
|
366
|
+
parts.push(headword);
|
|
244
367
|
if (row.pronunciation) parts.push(`/${row.pronunciation}/`);
|
|
245
368
|
|
|
246
369
|
const italic = [];
|
|
247
370
|
if (row.language_tag && row.language !== pageLanguage)
|
|
248
371
|
italic.push(tag(row.language, row.language_tag));
|
|
249
|
-
if (
|
|
372
|
+
if (row.class) {
|
|
250
373
|
const parsed = parseWordClass(row.class);
|
|
251
374
|
if (!parsed.reason) {
|
|
252
375
|
usedBases.add(parsed.base);
|
|
253
376
|
for (const token of parsed.modifiers) usedModifiers.add(token);
|
|
254
|
-
|
|
377
|
+
let classification = abbreviateWordClass(parsed);
|
|
378
|
+
if (words && parsed.base === "n" && typeof row.plural === "string" && row.plural) {
|
|
379
|
+
classification += `, pl. ${row.plural}`;
|
|
380
|
+
anyPlural = true;
|
|
381
|
+
}
|
|
382
|
+
italic.push(classification);
|
|
255
383
|
} else italic.push(String(row.class));
|
|
256
384
|
}
|
|
257
385
|
if (italic.length) parts.push(`_${italic.join(" ")}_`);
|
|
386
|
+
if (words && row.field) parts.push(`(_${row.field}_)`);
|
|
258
387
|
|
|
259
388
|
if (words) {
|
|
260
389
|
const bracket = etymologyText(row.etymology, row.language, tag);
|
|
@@ -264,7 +393,8 @@ function render(
|
|
|
264
393
|
const senses = Array.isArray(row.senses) ? row.senses : [];
|
|
265
394
|
const senseText = (sense) => {
|
|
266
395
|
const bodies = Array.isArray(sense?.bodies) ? sense.bodies : [];
|
|
267
|
-
const by =
|
|
396
|
+
const by =
|
|
397
|
+
bodies.length && !sense.omitBody ? `(_${bodies.map(label).join(", ")}_) ` : "";
|
|
268
398
|
return `${by}${sentence(sense?.text)}`;
|
|
269
399
|
};
|
|
270
400
|
if (senses.length === 1) parts.push(senseText(senses[0]));
|
|
@@ -287,9 +417,11 @@ function render(
|
|
|
287
417
|
for (const modifier of WORD_CLASS_MODIFIERS)
|
|
288
418
|
if (usedModifiers.has(modifier.token))
|
|
289
419
|
key.push(`_${modifier.abbreviation}_ ${modifier.expansion}`);
|
|
420
|
+
if (anyPlural) key.push("_pl._ plural");
|
|
290
421
|
if (anyCommon) key.push(`${COMMON_MARK} ${COMMON_KEY}`);
|
|
291
422
|
const line = key.length ? `**Key:** ${key.join("; ")}.` : "";
|
|
292
423
|
if (showKey === "only") return line;
|
|
424
|
+
if (showKey && head) paragraphs.unshift(head);
|
|
293
425
|
if (showKey && line) paragraphs.unshift(line);
|
|
294
426
|
return paragraphs.join("\n\n");
|
|
295
427
|
}
|
|
@@ -36,19 +36,19 @@
|
|
|
36
36
|
* where it is a word of its language compared without case. A title of
|
|
37
37
|
* several words, joined by a space or a hyphen, always is.
|
|
38
38
|
*
|
|
39
|
-
* **A glossary entry
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* office's row.
|
|
39
|
+
* **A glossary entry naming a dictionary word** carries the word's class and
|
|
40
|
+
* a link to its entry in the resolved language's dictionary. Without a
|
|
41
|
+
* `definition` it carries the word's senses; with one it carries that sense
|
|
42
|
+
* instead. An owner's own rank or office contributes its descriptions after
|
|
43
|
+
* the word's senses, unless the entry states a definition. An entry naming
|
|
44
|
+
* another body's rank or office carries its senses on the entry's own row.
|
|
45
|
+
* `common`, `name_sort` and `pronunciation` belong to each owner's entry;
|
|
46
|
+
* nothing a glossary states changes a dictionary word.
|
|
48
47
|
*
|
|
49
48
|
* @module
|
|
50
49
|
*/
|
|
51
50
|
|
|
51
|
+
import { createHash } from "node:crypto";
|
|
52
52
|
import { parseAddress, renderAddress } from "./address.mjs";
|
|
53
53
|
import { glossaryLanguage } from "./glossary-language.mjs";
|
|
54
54
|
import { isNoteRecord } from "./index-records.mjs";
|
|
@@ -65,10 +65,12 @@ export const DICTIONARY_COLUMNS = Object.freeze([
|
|
|
65
65
|
"letter",
|
|
66
66
|
"pronunciation",
|
|
67
67
|
"class",
|
|
68
|
+
"plural",
|
|
69
|
+
"field",
|
|
68
70
|
"etymology",
|
|
69
71
|
"senses",
|
|
70
72
|
"usage",
|
|
71
|
-
"
|
|
73
|
+
"verbs",
|
|
72
74
|
"depth",
|
|
73
75
|
]);
|
|
74
76
|
|
|
@@ -82,6 +84,8 @@ export const TERM_COLUMNS = Object.freeze([
|
|
|
82
84
|
"name_sort",
|
|
83
85
|
"letter",
|
|
84
86
|
"pronunciation",
|
|
87
|
+
"class",
|
|
88
|
+
"word",
|
|
85
89
|
"senses",
|
|
86
90
|
"usage",
|
|
87
91
|
"common",
|
|
@@ -126,6 +130,34 @@ const isSingleWord = (title) => !/[\s\-‐‑]/u.test(title);
|
|
|
126
130
|
/** Whether a glossary entry states a definition. */
|
|
127
131
|
const defines = (entry) => typeof entry.definition === "string" && entry.definition.trim() !== "";
|
|
128
132
|
|
|
133
|
+
/** A noun's stated plural, or the first literal ending replacement that fits. */
|
|
134
|
+
function pluralOf(word, grammar) {
|
|
135
|
+
if (parseWordClass(word.class).base !== "n") return null;
|
|
136
|
+
if (typeof word.plural === "string" && word.plural) return word.plural;
|
|
137
|
+
for (const rule of Array.isArray(grammar?.plural) ? grammar.plural : []) {
|
|
138
|
+
if (typeof rule?.ending !== "string" || typeof rule.becomes !== "string") continue;
|
|
139
|
+
if (word.form.endsWith(rule.ending))
|
|
140
|
+
return (
|
|
141
|
+
(rule.ending ? word.form.slice(0, -rule.ending.length) : word.form) + rule.becomes
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The stable prose anchor of a word in one language's dictionary. The exact
|
|
149
|
+
* language and NFC form identify it, including case and accents.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} language - The language's canonical note Address.
|
|
152
|
+
* @param {string} name - The dictionary headword.
|
|
153
|
+
* @returns {string} The anchor slug.
|
|
154
|
+
*/
|
|
155
|
+
export function dictionaryEntryAnchor(language, name) {
|
|
156
|
+
return `word-${createHash("sha256")
|
|
157
|
+
.update(JSON.stringify([language, nfc(name)]))
|
|
158
|
+
.digest("hex")}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
/**
|
|
130
162
|
* The heading letter of a sort key: its first letter or digit, accents taken
|
|
131
163
|
* off and apostrophes skipped, in capitals; `#` where it has none.
|
|
@@ -259,10 +291,11 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
259
291
|
byForm.set(key, exact);
|
|
260
292
|
byLowerForm.set(key, lower);
|
|
261
293
|
}
|
|
262
|
-
/**
|
|
294
|
+
/** A form's nearest visible definition, traversing the complete lineage. */
|
|
263
295
|
const wordIn = (language, name) => {
|
|
264
296
|
const chain = lineage(language);
|
|
265
297
|
for (let depth = 0; depth < chain.length; depth++) {
|
|
298
|
+
if (!visible.has(chain[depth])) continue;
|
|
266
299
|
const word = byForm.get(chain[depth]).get(name);
|
|
267
300
|
if (word) return { word, statedIn: chain[depth], depth };
|
|
268
301
|
}
|
|
@@ -377,7 +410,7 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
377
410
|
}
|
|
378
411
|
const allGovernance = [...governance.values()].flatMap((rows) => [...rows.values()]);
|
|
379
412
|
|
|
380
|
-
// ── Word rows
|
|
413
|
+
// ── Word rows ────────────────────────────────────────────────────────────
|
|
381
414
|
const wordRows = new Map();
|
|
382
415
|
const wordKey = (language, name) => `${language}\0${name}`;
|
|
383
416
|
for (const [language] of languages) {
|
|
@@ -388,7 +421,7 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
388
421
|
const name = nfc(word.form);
|
|
389
422
|
const key = wordKey(language, name);
|
|
390
423
|
if (wordRows.has(key)) continue;
|
|
391
|
-
wordRows.set(key, { language, name, found: wordIn(language, name)
|
|
424
|
+
wordRows.set(key, { language, name, found: wordIn(language, name) });
|
|
392
425
|
}
|
|
393
426
|
}
|
|
394
427
|
}
|
|
@@ -404,48 +437,35 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
404
437
|
culture ?
|
|
405
438
|
allGovernance.filter((row) => row.cultures.includes(note))
|
|
406
439
|
: [...(governance.get(note)?.values() ?? [])];
|
|
407
|
-
const reachLanguages = new Set([
|
|
408
|
-
languageOf(record, note, undefined),
|
|
409
|
-
...(culture ? reach.map((row) => row.language) : []),
|
|
410
|
-
]);
|
|
411
|
-
reachLanguages.delete(null);
|
|
412
440
|
glossaryOf(note).forEach((entry, position) => {
|
|
413
441
|
const name = nfc(entry.name);
|
|
414
442
|
const language = languageOf(record, note, entry);
|
|
415
443
|
const mark = { note, position, entry };
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
owner: note,
|
|
425
|
-
ownerKind: culture ? "culture" : "affiliation",
|
|
426
|
-
language,
|
|
427
|
-
name,
|
|
428
|
-
cultures: culture ? [note] : culturesOf(note),
|
|
429
|
-
mark,
|
|
430
|
-
});
|
|
444
|
+
const found = wordRows.get(wordKey(language, name))?.found;
|
|
445
|
+
const own = !culture ? governance.get(note)?.get(name) : undefined;
|
|
446
|
+
if (own) {
|
|
447
|
+
own.language = language;
|
|
448
|
+
own.found = found;
|
|
449
|
+
if (defines(entry)) own.definition = mark;
|
|
450
|
+
own.marks.push(mark);
|
|
451
|
+
own.named = true;
|
|
431
452
|
return;
|
|
432
453
|
}
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
for (const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
454
|
+
const reached = reach.filter((row) => row.name === name);
|
|
455
|
+
const preferred = reached.filter((row) => row.language === language);
|
|
456
|
+
const providers = preferred.length ? preferred : reached;
|
|
457
|
+
if (!defines(entry)) for (const row of providers) row.named = true;
|
|
458
|
+
if (defines(entry) || found || providers.length)
|
|
459
|
+
owned.push({
|
|
460
|
+
owner: note,
|
|
461
|
+
ownerKind: culture ? "culture" : "affiliation",
|
|
462
|
+
language,
|
|
463
|
+
name,
|
|
464
|
+
cultures: culture ? [note] : culturesOf(note),
|
|
465
|
+
found,
|
|
466
|
+
providers: found || defines(entry) ? [] : providers,
|
|
467
|
+
mark,
|
|
468
|
+
});
|
|
449
469
|
});
|
|
450
470
|
}
|
|
451
471
|
|
|
@@ -484,38 +504,50 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
484
504
|
|
|
485
505
|
const citedBy = citationLookup(byKey.values());
|
|
486
506
|
const dictionary = [];
|
|
487
|
-
|
|
488
|
-
|
|
507
|
+
const wordUsage = ({ word, statedIn }) =>
|
|
508
|
+
[...new Set(citedBy(statedIn, word.form).filter(citable))].sort(compare);
|
|
509
|
+
for (const { language, name, found } of wordRows.values()) {
|
|
510
|
+
const { word, depth } = found;
|
|
511
|
+
const grammar = languages.get(language)?.data?.grammar;
|
|
489
512
|
const nameSort =
|
|
490
|
-
|
|
491
|
-
(typeof word.name_sort === "string" && word.name_sort ? word.name_sort : name);
|
|
513
|
+
typeof word.name_sort === "string" && word.name_sort ? word.name_sort : name;
|
|
492
514
|
dictionary.push({
|
|
493
515
|
language,
|
|
494
516
|
language_tag: tagOf(language),
|
|
495
517
|
name,
|
|
496
518
|
name_sort: nameSort,
|
|
497
519
|
letter: dictionaryLetter(nameSort),
|
|
498
|
-
pronunciation:
|
|
499
|
-
marked(marks, "pronunciation") ??
|
|
500
|
-
(typeof word.pronunciation === "string" ? word.pronunciation : null),
|
|
520
|
+
pronunciation: typeof word.pronunciation === "string" ? word.pronunciation : null,
|
|
501
521
|
class: typeof word.class === "string" ? word.class : null,
|
|
522
|
+
plural: pluralOf(word, grammar),
|
|
523
|
+
field: typeof word.field === "string" ? word.field : null,
|
|
502
524
|
etymology: etymologyOf(found),
|
|
503
525
|
senses: glossSenses(word.gloss).map((text) => ({ text, bodies: [] })),
|
|
504
|
-
usage:
|
|
505
|
-
|
|
526
|
+
usage: wordUsage(found),
|
|
527
|
+
verbs: isMap(grammar?.verbs) ? grammar.verbs : null,
|
|
506
528
|
depth,
|
|
507
529
|
});
|
|
508
530
|
}
|
|
509
531
|
|
|
510
532
|
const terms = [];
|
|
511
533
|
const termRow = (
|
|
512
|
-
{ owner, ownerKind, language, name, cultures },
|
|
534
|
+
{ owner, ownerKind, language, name, cultures, found },
|
|
513
535
|
senses,
|
|
514
536
|
usage,
|
|
515
537
|
marks,
|
|
516
538
|
origins,
|
|
517
539
|
) => {
|
|
518
|
-
const
|
|
540
|
+
const word = found?.word;
|
|
541
|
+
const wordLanguage =
|
|
542
|
+
(
|
|
543
|
+
Array.isArray(languages.get(language)?.data?.words) &&
|
|
544
|
+
languages.get(language).data.words.length
|
|
545
|
+
) ?
|
|
546
|
+
language
|
|
547
|
+
: found?.statedIn;
|
|
548
|
+
const nameSort =
|
|
549
|
+
marked(marks, "name_sort") ??
|
|
550
|
+
(typeof word?.name_sort === "string" && word.name_sort ? word.name_sort : name);
|
|
519
551
|
terms.push({
|
|
520
552
|
owner,
|
|
521
553
|
owner_kind: ownerKind,
|
|
@@ -524,7 +556,9 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
524
556
|
name,
|
|
525
557
|
name_sort: nameSort,
|
|
526
558
|
letter: dictionaryLetter(nameSort),
|
|
527
|
-
pronunciation: marked(marks, "pronunciation") ?? null,
|
|
559
|
+
pronunciation: marked(marks, "pronunciation") ?? word?.pronunciation ?? null,
|
|
560
|
+
class: word?.class ?? null,
|
|
561
|
+
word: found ? `${wordLanguage}#${dictionaryEntryAnchor(wordLanguage, name)}` : null,
|
|
528
562
|
senses,
|
|
529
563
|
usage: [...new Set(usage)].filter(citable).sort(compare),
|
|
530
564
|
common: marks.some((one) => one.entry.common === true),
|
|
@@ -532,34 +566,50 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
532
566
|
cultures,
|
|
533
567
|
});
|
|
534
568
|
};
|
|
569
|
+
const combineSenses = (senses) => {
|
|
570
|
+
const combined = [];
|
|
571
|
+
for (const sense of senses) {
|
|
572
|
+
const same = combined.find((one) => one.text === sense.text);
|
|
573
|
+
if (!same) combined.push({ ...sense, bodies: [...sense.bodies] });
|
|
574
|
+
else
|
|
575
|
+
for (const body of sense.bodies)
|
|
576
|
+
if (!same.bodies.includes(body)) same.bodies.push(body);
|
|
577
|
+
}
|
|
578
|
+
return combined;
|
|
579
|
+
};
|
|
580
|
+
const governanceSenses = (row) => {
|
|
581
|
+
if (row.definition) return [{ text: row.definition.entry.definition, bodies: [row.owner] }];
|
|
582
|
+
return combineSenses([
|
|
583
|
+
...glossSenses(row.found?.word.gloss).map((text) => ({ text, bodies: [] })),
|
|
584
|
+
...[...row.pieces]
|
|
585
|
+
.sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind])
|
|
586
|
+
.filter((piece) => piece.text?.trim())
|
|
587
|
+
.map((piece) => ({ text: piece.text, bodies: [row.owner] })),
|
|
588
|
+
]);
|
|
589
|
+
};
|
|
535
590
|
for (const row of allGovernance) {
|
|
536
591
|
const enters =
|
|
537
592
|
!isSingleWord(row.name) ||
|
|
538
593
|
row.named ||
|
|
539
594
|
(row.language !== null && hasWordIgnoringCase(row.language, row.name));
|
|
540
595
|
if (!enters) continue;
|
|
541
|
-
|
|
542
|
-
if (row.definition)
|
|
543
|
-
senses.push({ text: row.definition.entry.definition, bodies: [row.owner] });
|
|
544
|
-
else
|
|
545
|
-
for (const piece of [...row.pieces].sort(
|
|
546
|
-
(a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind],
|
|
547
|
-
))
|
|
548
|
-
if (piece.text?.trim() && !senses.some((sense) => sense.text === piece.text))
|
|
549
|
-
senses.push({ text: piece.text, bodies: [row.owner] });
|
|
550
|
-
termRow(row, senses, [...row.usage], row.marks, [
|
|
596
|
+
termRow(row, governanceSenses(row), [...row.usage], row.marks, [
|
|
551
597
|
...row.pieces.map((piece) => piece.kind),
|
|
552
598
|
...(row.marks.length ? ["glossary"] : []),
|
|
553
599
|
]);
|
|
554
600
|
}
|
|
555
|
-
for (const one of owned)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
[{ text: one.mark.entry.definition, bodies: [one.owner] }]
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
601
|
+
for (const one of owned) {
|
|
602
|
+
const defined = defines(one.mark.entry);
|
|
603
|
+
const senses =
|
|
604
|
+
defined ? [{ text: one.mark.entry.definition, bodies: [one.owner] }]
|
|
605
|
+
: one.found ? glossSenses(one.found.word.gloss).map((text) => ({ text, bodies: [] }))
|
|
606
|
+
: combineSenses(one.providers.flatMap(governanceSenses));
|
|
607
|
+
const usage =
|
|
608
|
+
defined ? [one.owner]
|
|
609
|
+
: one.found ? wordUsage(one.found)
|
|
610
|
+
: [one.owner, ...one.providers.flatMap((row) => [...row.usage])];
|
|
611
|
+
termRow(one, senses, usage, [one.mark], ["glossary"]);
|
|
612
|
+
}
|
|
563
613
|
|
|
564
614
|
dictionary.sort(
|
|
565
615
|
(a, b) =>
|