@heroiclands/package-build 23.8.0 → 23.9.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 +27 -0
- package/bin/content-commands.mjs +12 -4
- package/docs/api.md +328 -28
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/links-and-markup.md +140 -56
- package/docs/commands.md +19 -10
- package/docs/content-format.md +2 -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 +269 -123
- package/docs/reference/note-types.md +184 -170
- package/engine/anchors.mjs +7 -20
- package/engine/compile-corpus.mjs +3 -2
- package/engine/content-format.yaml +1 -0
- package/engine/content-index.mjs +412 -11
- package/engine/content-links.mjs +43 -15
- package/engine/content-tables.mjs +8 -4
- package/engine/culture-reach.mjs +61 -34
- package/engine/data-keys.mjs +3 -1
- package/engine/dictionary-layout.mjs +151 -19
- package/engine/dictionary-rows.mjs +129 -79
- package/engine/foundry-entries.mjs +21 -3
- package/engine/glossary-entries.mjs +11 -11
- package/engine/heading-attributes.mjs +21 -28
- package/engine/helpers.mjs +12 -6
- package/engine/journals.mjs +4 -1
- package/engine/language-data.mjs +53 -0
- package/engine/language-terms.mjs +86 -0
- package/engine/language-words.mjs +2 -2
- package/engine/lexical-sections.mjs +247 -0
- package/engine/note-claims.mjs +2 -0
- package/engine/note-state.mjs +9 -3
- package/engine/note-vocabulary.mjs +7 -6
- package/engine/pdf-build.mjs +9 -9
- package/engine/site-build.mjs +16 -2
- package/engine/sql-tables.mjs +262 -43
- package/engine/stub-lint.mjs +6 -2
- package/engine/web-wikilinks.mjs +8 -6
- package/engine/word-rows.mjs +20 -12
- package/engine/word-terms.mjs +15 -6
- package/package.json +1 -1
- package/types/engine/content-index.d.mts +65 -1
- package/types/engine/content-tables.d.mts +2 -1
- package/types/engine/culture-reach.d.mts +22 -2
- package/types/engine/data-keys.d.mts +7 -0
- package/types/engine/dictionary-layout.d.mts +35 -4
- package/types/engine/dictionary-rows.d.mts +9 -0
- package/types/engine/foundry-entries.d.mts +3 -1
- package/types/engine/heading-attributes.d.mts +2 -2
- package/types/engine/helpers.d.mts +4 -1
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/lexical-sections.d.mts +95 -0
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/site-build.d.mts +4 -1
- package/types/engine/sql-tables.d.mts +33 -3
- package/types/engine/word-rows.d.mts +9 -7
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { FENCE_LINE } from "./code-fences.mjs";
|
|
15
15
|
import { PAGE_LIST_LANGUAGE } from "./page-lists.mjs";
|
|
16
16
|
import { appendGeneratedSections } from "./generated-sections.mjs";
|
|
17
|
+
import { insertLexicalSections } from "./lexical-sections.mjs";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Replace prepared SQL fences and page lists with Markdown while retaining
|
|
@@ -29,7 +30,8 @@ import { appendGeneratedSections } from "./generated-sections.mjs";
|
|
|
29
30
|
* Prepared results.
|
|
30
31
|
* A note's generated sections — see {@link module:engine/generated-sections} —
|
|
31
32
|
* are appended to the body first, from the `generated` text the preparation
|
|
32
|
-
* set on its results, and expanded with it.
|
|
33
|
+
* set on its results, and expanded with it. Automatic lexical lists, carried
|
|
34
|
+
* on `lexical`, then fill their authored sections or append a new section.
|
|
33
35
|
*
|
|
34
36
|
* @returns {{markdown: string, errors: object[], warnings: object[], lineMap: object[],
|
|
35
37
|
* generatedFrom?: number}} `generatedFrom` is the 0-based line of the
|
|
@@ -148,10 +150,12 @@ export function expandContentTables(markdown, { source = "", sqlTables, pageList
|
|
|
148
150
|
i = close;
|
|
149
151
|
}
|
|
150
152
|
return {
|
|
151
|
-
|
|
153
|
+
...insertLexicalSections(out.join("\n"), sqlTables?.lexical, {
|
|
154
|
+
authored,
|
|
155
|
+
lineMap,
|
|
156
|
+
generatedFrom,
|
|
157
|
+
}),
|
|
152
158
|
errors,
|
|
153
159
|
warnings,
|
|
154
|
-
lineMap,
|
|
155
|
-
...(generatedFrom === undefined ? {} : { generatedFrom }),
|
|
156
160
|
};
|
|
157
161
|
}
|
package/engine/culture-reach.mjs
CHANGED
|
@@ -15,17 +15,14 @@
|
|
|
15
15
|
* Which terms reach a culture or an affiliation, and whether an affiliation
|
|
16
16
|
* reaches a culture at all.
|
|
17
17
|
*
|
|
18
|
-
* A culture
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
18
|
+
* A culture can name the ranks and offices of every affiliation that reaches
|
|
19
|
+
* it through `data.culture` or `data.parents`. An affiliation can name its own
|
|
20
|
+
* ranks and offices. Each glossary entry can also name a word of its resolved
|
|
21
|
+
* language ({@link module:engine/glossary-language}). A language's words include
|
|
22
|
+
* those it inherits through `data.family`, and a word is a term only where
|
|
26
23
|
* its class's base is a glossary base. The `dictionary` relation
|
|
27
|
-
* ({@link module:engine/dictionary-rows})
|
|
28
|
-
*
|
|
24
|
+
* ({@link module:engine/dictionary-rows}) uses the same resolved-language and
|
|
25
|
+
* governance sources.
|
|
29
26
|
*
|
|
30
27
|
* **An affiliation that reaches no culture is a warning**, at its `type`, in a
|
|
31
28
|
* tree that describes peoples, which is a tree holding at least one culture
|
|
@@ -42,7 +39,7 @@ import { AddressLink } from "./address-values.mjs";
|
|
|
42
39
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
43
40
|
import { glossaryLanguage } from "./glossary-language.mjs";
|
|
44
41
|
import { parseWordClass, WORD_CLASS_BASES } from "./word-classes.mjs";
|
|
45
|
-
import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
|
|
42
|
+
import { glossSenses, LANGUAGE_SUBTYPE } from "./word-terms.mjs";
|
|
46
43
|
|
|
47
44
|
/** The bases whose words are glossary terms. */
|
|
48
45
|
const GLOSSARY_BASES = new Set(
|
|
@@ -189,6 +186,11 @@ export function entryLanguage(index, note, entry) {
|
|
|
189
186
|
* @property {Map<string, Map<string, string[]>>} terms - Each culture's and
|
|
190
187
|
* affiliation's terms by NFC name, each with the sense texts its sources
|
|
191
188
|
* give it.
|
|
189
|
+
* @property {Map<string, Map<string, string[]>>} governance - The ranks and
|
|
190
|
+
* offices each owner can name.
|
|
191
|
+
* @property {Map<string, Array<{form: string, gloss: unknown}>>} words - Each
|
|
192
|
+
* language's dictionary words.
|
|
193
|
+
* @property {Map<string, string|undefined>} families - Each language's parent.
|
|
192
194
|
*/
|
|
193
195
|
|
|
194
196
|
/** @type {WeakMap<object, GlossaryCorpus>} */
|
|
@@ -239,6 +241,23 @@ function reachedCultures(index, note) {
|
|
|
239
241
|
return [...out];
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
/** Add the nearest dictionary definition of each form in a language's lineage. */
|
|
245
|
+
function addLanguageWords(terms, key, language, words, families) {
|
|
246
|
+
const seen = new Set();
|
|
247
|
+
const forms = new Set();
|
|
248
|
+
for (let current = language; current && !seen.has(current); current = families.get(current)) {
|
|
249
|
+
seen.add(current);
|
|
250
|
+
for (const word of words.get(current) ?? []) {
|
|
251
|
+
const form = glossaryName(word.form);
|
|
252
|
+
if (forms.has(form)) continue;
|
|
253
|
+
forms.add(form);
|
|
254
|
+
const senses = glossSenses(word.gloss);
|
|
255
|
+
if (!senses.length) addTerm(terms, key, form, undefined);
|
|
256
|
+
for (const sense of senses) addTerm(terms, key, form, sense);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
242
261
|
/**
|
|
243
262
|
* The glossary corpus of an index.
|
|
244
263
|
*
|
|
@@ -257,18 +276,7 @@ export function glossaryCorpus(index) {
|
|
|
257
276
|
if (key) families.set(key, addressKeyOf(dataOf(note).family, index, "skill"));
|
|
258
277
|
}
|
|
259
278
|
const terms = new Map();
|
|
260
|
-
|
|
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
|
-
};
|
|
279
|
+
const governance = new Map();
|
|
272
280
|
for (const note of notes) {
|
|
273
281
|
const culture = isCultureNote(note);
|
|
274
282
|
if (!culture && typeOf(note) !== "affiliation") continue;
|
|
@@ -276,18 +284,22 @@ export function glossaryCorpus(index) {
|
|
|
276
284
|
if (!key) continue;
|
|
277
285
|
if (!terms.has(key)) terms.set(key, new Map());
|
|
278
286
|
const data = dataOf(note);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
287
|
+
addLanguageWords(terms, key, entryLanguage(index, note, undefined), words, families);
|
|
288
|
+
if (culture) continue;
|
|
283
289
|
addGovernance(terms, key, data);
|
|
284
|
-
|
|
290
|
+
addGovernance(governance, key, data);
|
|
285
291
|
for (const home of reachedCultures(index, note)) {
|
|
286
292
|
addGovernance(terms, home, data);
|
|
287
|
-
|
|
293
|
+
addGovernance(governance, home, data);
|
|
288
294
|
}
|
|
289
295
|
}
|
|
290
|
-
const corpus = {
|
|
296
|
+
const corpus = {
|
|
297
|
+
describesPeoples: notes.some(isCultureNote),
|
|
298
|
+
terms,
|
|
299
|
+
governance,
|
|
300
|
+
words,
|
|
301
|
+
families,
|
|
302
|
+
};
|
|
291
303
|
CORPUS.set(index, corpus);
|
|
292
304
|
return corpus;
|
|
293
305
|
}
|
|
@@ -298,16 +310,31 @@ export function glossaryCorpus(index) {
|
|
|
298
310
|
*
|
|
299
311
|
* @param {object} index - The link index.
|
|
300
312
|
* @param {object} note - The culture or affiliation note.
|
|
313
|
+
* @param {object} [entry] - A glossary entry, whose resolved language selects
|
|
314
|
+
* its dictionary words. Absent, the note's own language is used.
|
|
301
315
|
* @returns {Map<string, string[]>} The terms; empty for a note no source reaches.
|
|
302
316
|
*/
|
|
303
|
-
export function noteTerms(index, note) {
|
|
317
|
+
export function noteTerms(index, note, entry) {
|
|
304
318
|
const key = ownKey(note, index);
|
|
305
|
-
|
|
319
|
+
const corpus = glossaryCorpus(index);
|
|
320
|
+
if (!entry) return (key && corpus.terms.get(key)) || new Map();
|
|
321
|
+
const own = new Map(
|
|
322
|
+
[...(corpus.governance.get(key) ?? [])].map(([name, senses]) => [name, [...senses]]),
|
|
323
|
+
);
|
|
324
|
+
addLanguageWords(
|
|
325
|
+
new Map([[key, own]]),
|
|
326
|
+
key,
|
|
327
|
+
entryLanguage(index, note, entry),
|
|
328
|
+
corpus.words,
|
|
329
|
+
corpus.families,
|
|
330
|
+
);
|
|
331
|
+
return own;
|
|
306
332
|
}
|
|
307
333
|
|
|
308
334
|
/**
|
|
309
335
|
* Warn of an affiliation that reaches no culture, its own or one it inherits,
|
|
310
|
-
* in a tree that describes peoples.
|
|
336
|
+
* in a tree that describes peoples. Its ranks and offices then have no
|
|
337
|
+
* culture associations for term queries.
|
|
311
338
|
*
|
|
312
339
|
* @param {object} note - An `affiliation` note.
|
|
313
340
|
* @param {{index?: object}} [context]
|
|
@@ -323,7 +350,7 @@ export function checkAffiliationReach(note, { index } = {}) {
|
|
|
323
350
|
severity: "warning",
|
|
324
351
|
message:
|
|
325
352
|
"this affiliation reaches no culture, its own or one inherited through " +
|
|
326
|
-
"`data.parents`, so its ranks
|
|
353
|
+
"`data.parents`, so its ranks and offices have no culture associations for term queries; " +
|
|
327
354
|
"state `data.culture`",
|
|
328
355
|
},
|
|
329
356
|
];
|
package/engine/data-keys.mjs
CHANGED
|
@@ -70,6 +70,8 @@ import { nearest } from "./near-miss.mjs";
|
|
|
70
70
|
* every surface shows it exactly as written, never recased.
|
|
71
71
|
* @property {string} shape - The shape in words, for a finding and the reference.
|
|
72
72
|
* @property {boolean} [required] - The key must be stated, with a value.
|
|
73
|
+
* @property {boolean} [allowEmpty] - A required string may be empty where
|
|
74
|
+
* empty text is a stated value.
|
|
73
75
|
* @property {boolean} [nullable] - An explicit `null` states the key, for a
|
|
74
76
|
* required key whose null has a meaning of its own.
|
|
75
77
|
* @property {string} [label] - The infobox row label, where humanising the key
|
|
@@ -234,7 +236,7 @@ export function pathLabel(path) {
|
|
|
234
236
|
function stated(value, spec) {
|
|
235
237
|
if (value === undefined) return false;
|
|
236
238
|
if (value === null) return Boolean(spec.nullable);
|
|
237
|
-
if (typeof value === "string") return value.trim() !== "";
|
|
239
|
+
if (typeof value === "string") return Boolean(spec.allowEmpty) || value.trim() !== "";
|
|
238
240
|
return true;
|
|
239
241
|
}
|
|
240
242
|
|
|
@@ -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
|
}
|