@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
|
@@ -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) =>
|
|
@@ -78,6 +78,8 @@ import { isHomepage } from "./homepage.mjs";
|
|
|
78
78
|
import { declaresNoPack } from "./pack-router.mjs";
|
|
79
79
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
80
80
|
import { collectAnchors } from "./anchors.mjs";
|
|
81
|
+
import { scanSpans } from "./content-spans.mjs";
|
|
82
|
+
import { DICTIONARY_ENTRY_CLASS } from "./dictionary-layout.mjs";
|
|
81
83
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
82
84
|
import { routerFor } from "./pack-router.mjs";
|
|
83
85
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -94,7 +96,9 @@ import { publishesContentPages } from "../content-config.mjs";
|
|
|
94
96
|
export const LEAD_ANCHOR = "$lead";
|
|
95
97
|
|
|
96
98
|
/**
|
|
97
|
-
* Every
|
|
99
|
+
* Every anchor of a note's journal, as `anchorName → Foundry target`.
|
|
100
|
+
* A dictionary word's native heading carries its fragment after the
|
|
101
|
+
* containing page's UUID.
|
|
98
102
|
*
|
|
99
103
|
* Whole, not a fragment appended to the entry's UUID: nothing owns a page
|
|
100
104
|
* address, so a complete link restates no fact, and it keeps the page-id hash
|
|
@@ -112,9 +116,20 @@ export function anchorsOf(entryUuid, entryId, body, name) {
|
|
|
112
116
|
const anchors = {};
|
|
113
117
|
splitPages(body, name).forEach((page, index) => {
|
|
114
118
|
const uuid = pageUuid(entryUuid, journalPageId(entryId, page));
|
|
119
|
+
const words = new Set(
|
|
120
|
+
scanSpans(page.markdown)
|
|
121
|
+
.spans.filter((span) =>
|
|
122
|
+
span.attrs.some(
|
|
123
|
+
([key, value]) =>
|
|
124
|
+
key === "class" && value.split(/\s+/).includes(DICTIONARY_ENTRY_CLASS),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
.map((span) => span.id),
|
|
128
|
+
);
|
|
115
129
|
if (index === 0) anchors[LEAD_ANCHOR] = uuid;
|
|
116
130
|
if (page.anchorSlug) anchors[page.anchorSlug] = uuid;
|
|
117
|
-
for (const anchor of collectAnchors(page.markdown))
|
|
131
|
+
for (const anchor of collectAnchors(page.markdown))
|
|
132
|
+
anchors[anchor.slug] = words.has(anchor.slug) ? `${uuid}#${anchor.slug}` : uuid;
|
|
118
133
|
});
|
|
119
134
|
return anchors;
|
|
120
135
|
}
|
|
@@ -290,7 +305,10 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
290
305
|
id: fm.id,
|
|
291
306
|
uuid: own,
|
|
292
307
|
systemUuids: systemUuidsFor(fm.type, fm.id),
|
|
293
|
-
anchors:
|
|
308
|
+
anchors:
|
|
309
|
+
own && packForType(fm.type)?.docType === "JournalEntry" ?
|
|
310
|
+
anchorsOf(own, fm.id, body ?? "", name)
|
|
311
|
+
: undefined,
|
|
294
312
|
},
|
|
295
313
|
];
|
|
296
314
|
}
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
* `lore` note of `subType: culture` or on an `affiliation`, a list of
|
|
17
17
|
* `{ name, name_sort?, common?, definition?, pronunciation?, language? }`.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* An entry chooses a common word, gives a dictionary word a contextual
|
|
20
|
+
* meaning, or defines a phrase, compound or term of art. An entry naming a
|
|
21
|
+
* word of its resolved language carries that word's senses and class unless
|
|
22
|
+
* its own `definition` supplies the sense. A rank or office can also be named.
|
|
23
|
+
* Common marks, sort keys and pronunciation overrides belong to the entry's
|
|
24
|
+
* own glossary.
|
|
25
25
|
*
|
|
26
26
|
* The shape is the inner-key check's; this check owns what an entry means:
|
|
27
27
|
*
|
|
@@ -88,7 +88,7 @@ export function checkGlossary(note, { index } = {}) {
|
|
|
88
88
|
if (!Array.isArray(data.glossary)) return [];
|
|
89
89
|
|
|
90
90
|
const findings = [];
|
|
91
|
-
const
|
|
91
|
+
const hasTerms = Boolean(index?.notes);
|
|
92
92
|
const seen = new Set();
|
|
93
93
|
data.glossary.forEach((entry, i) => {
|
|
94
94
|
if (!isMap(entry) || typeof entry.name !== "string" || !entry.name.trim()) return;
|
|
@@ -116,7 +116,7 @@ export function checkGlossary(note, { index } = {}) {
|
|
|
116
116
|
),
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
|
-
if (!
|
|
119
|
+
if (!hasTerms) return;
|
|
120
120
|
const stated = entry.language !== undefined && entry.language !== null;
|
|
121
121
|
const named = stated ? languageProblem(index, entry.language) : undefined;
|
|
122
122
|
if (named) findings.push(at([i, "language"], "error", named));
|
|
@@ -133,15 +133,15 @@ export function checkGlossary(note, { index } = {}) {
|
|
|
133
133
|
"; state `language` on the entry or `data.language` on the note",
|
|
134
134
|
),
|
|
135
135
|
);
|
|
136
|
-
const senses =
|
|
136
|
+
const senses = noteTerms(index, note, entry).get(name);
|
|
137
137
|
const defined = typeof entry.definition === "string" && entry.definition.trim() !== "";
|
|
138
138
|
if (!defined && !senses)
|
|
139
139
|
findings.push(
|
|
140
140
|
at(
|
|
141
141
|
[i, "name"],
|
|
142
142
|
"error",
|
|
143
|
-
`\`${entry.name}\` names nothing: it is no word of
|
|
144
|
-
"
|
|
143
|
+
`\`${entry.name}\` names nothing: it is no word of its resolved language ` +
|
|
144
|
+
"and no rank or office that reaches this note, and the entry gives no " +
|
|
145
145
|
"`definition` to make it a term",
|
|
146
146
|
),
|
|
147
147
|
);
|
|
@@ -28,12 +28,26 @@
|
|
|
28
28
|
* **A leaf, deliberately.** `anchors.mjs` is shared by the compilers, the index
|
|
29
29
|
* and the link checker and cannot import any of them; the figure pass is
|
|
30
30
|
* imported by `anchors.mjs` and so cannot import it back. A module that imports
|
|
31
|
-
*
|
|
31
|
+
* the attribute grammar and Markdown literal ranges can be asked by all of them.
|
|
32
32
|
*
|
|
33
33
|
* @module
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
37
|
+
import MarkdownIt from "markdown-it";
|
|
38
|
+
|
|
39
|
+
/** Literal block ranges use the renderer's Markdown grammar. */
|
|
40
|
+
const structureParser = new MarkdownIt({ html: true });
|
|
41
|
+
|
|
42
|
+
/** Body lines occupied by code or raw HTML, including comment examples. */
|
|
43
|
+
function literalHeadingLines(source) {
|
|
44
|
+
const lines = new Set();
|
|
45
|
+
for (const token of structureParser.parse(String(source ?? ""), {})) {
|
|
46
|
+
if (!token.map || !["fence", "code_block", "html_block"].includes(token.type)) continue;
|
|
47
|
+
for (let line = token.map[0]; line < token.map[1]; line++) lines.add(line);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
37
51
|
|
|
38
52
|
/**
|
|
39
53
|
* A heading line: its hashes and the text after them.
|
|
@@ -134,8 +148,8 @@ export function opensPage({ level, id }) {
|
|
|
134
148
|
/**
|
|
135
149
|
* Every heading in a body, with what its attribute block declares.
|
|
136
150
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
151
|
+
* Code and raw HTML blocks are skipped: an example heading in a document is
|
|
152
|
+
* not a heading of it, including an example in an HTML comment.
|
|
139
153
|
*
|
|
140
154
|
* @param {string} source - A note's markdown body.
|
|
141
155
|
* @param {number} [bodyLine=1] - The 1-based file line the body starts on, so a
|
|
@@ -148,18 +162,9 @@ export function scanHeadingAttributes(source, bodyLine = 1) {
|
|
|
148
162
|
const lines = String(source ?? "").split("\n");
|
|
149
163
|
const headings = [];
|
|
150
164
|
const errors = [];
|
|
151
|
-
|
|
165
|
+
const literal = literalHeadingLines(source);
|
|
152
166
|
for (let i = 0; i < lines.length; i++) {
|
|
153
|
-
|
|
154
|
-
if (codeFence) {
|
|
155
|
-
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length)
|
|
156
|
-
codeFence = null;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (fence) {
|
|
160
|
-
codeFence = fence[1];
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
167
|
+
if (literal.has(i)) continue;
|
|
163
168
|
const heading = HEADING_LINE.exec(lines[i]);
|
|
164
169
|
if (!heading) continue;
|
|
165
170
|
const parsed = splitHeadingAttributes(heading[2]);
|
|
@@ -267,21 +272,9 @@ export function parseHeadingLine(line) {
|
|
|
267
272
|
export function pageOpenings(source, blockLines = new Set()) {
|
|
268
273
|
const lines = String(source ?? "").split("\n");
|
|
269
274
|
const openings = new Map();
|
|
270
|
-
|
|
275
|
+
const literal = literalHeadingLines(source);
|
|
271
276
|
for (const [index, line] of lines.entries()) {
|
|
272
|
-
|
|
273
|
-
// be at least as long, same as {@link scanHeadingAttributes} tracks
|
|
274
|
-
// it below — a longer fence is what lets a fenced example carry a
|
|
275
|
-
// shorter fence of its own as literal content.
|
|
276
|
-
const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
277
|
-
if (codeFence) {
|
|
278
|
-
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length) {
|
|
279
|
-
codeFence = null;
|
|
280
|
-
}
|
|
281
|
-
} else if (fence) {
|
|
282
|
-
codeFence = fence[1];
|
|
283
|
-
}
|
|
284
|
-
if (codeFence !== null || blockLines.has(index)) continue;
|
|
277
|
+
if (literal.has(index) || blockLines.has(index)) continue;
|
|
285
278
|
const heading = parseHeadingLine(line);
|
|
286
279
|
if (!heading?.startsPage) continue;
|
|
287
280
|
openings.set(index, { ...heading, line: index + 1 });
|
package/engine/helpers.mjs
CHANGED
|
@@ -74,6 +74,7 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
74
74
|
import { DERIVED_PACKED_TYPES, NEVER_PACKED_TYPES } from "./note-claims.mjs";
|
|
75
75
|
import { expandContentTables } from "./content-tables.mjs";
|
|
76
76
|
import { spanMarkdownPlugin } from "./content-spans.mjs";
|
|
77
|
+
import { dictionaryEntryPlugin } from "./dictionary-layout.mjs";
|
|
77
78
|
import { renderBlocks } from "./content-blocks.mjs";
|
|
78
79
|
import { headingAttributesPlugin } from "./heading-attributes.mjs";
|
|
79
80
|
import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
|
|
@@ -108,6 +109,7 @@ export const md = markdownit({ html: true })
|
|
|
108
109
|
.use(footnotePlugin)
|
|
109
110
|
.use(deflistPlugin)
|
|
110
111
|
.use(spanMarkdownPlugin)
|
|
112
|
+
.use(dictionaryEntryPlugin)
|
|
111
113
|
// A heading's attribute block, written onto the heading element rather than
|
|
112
114
|
// typeset into it — the reading the website's renderer does for itself.
|
|
113
115
|
.use(headingAttributesPlugin)
|
|
@@ -781,12 +783,14 @@ import { collectAnchors } from "./anchors.mjs";
|
|
|
781
783
|
* omitted.
|
|
782
784
|
* @param {readonly object[]} [opts.records] - The corpus, derived once per
|
|
783
785
|
* compile and handed in. Required: see {@link assertSuppliedCorpus}.
|
|
786
|
+
* @param {Map<string, object[]>} [opts.sqlTables] - Prepared directives,
|
|
787
|
+
* including the anchors and Foundry page maps of their expanded bodies.
|
|
784
788
|
* @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
|
|
785
789
|
*/
|
|
786
790
|
export function buildContentLinkIndex(
|
|
787
791
|
contentBase,
|
|
788
792
|
router = packRouter(),
|
|
789
|
-
{ config, records } = {},
|
|
793
|
+
{ config, records, sqlTables } = {},
|
|
790
794
|
) {
|
|
791
795
|
const docs = [];
|
|
792
796
|
/** The files this package ships, by canonical address. */
|
|
@@ -812,6 +816,7 @@ export function buildContentLinkIndex(
|
|
|
812
816
|
// authored fields, and neither may be handed the keys the index derived.
|
|
813
817
|
const fm = authoredFrontmatter(record);
|
|
814
818
|
const absPath = noteFile(contentBase, record);
|
|
819
|
+
const prepared = sqlTables?.get(absPath);
|
|
815
820
|
// The id a note's document is filed under: its authored pin, or the
|
|
816
821
|
// one derived from its canonical address. Derived by the index
|
|
817
822
|
// against the configuration this build resolved — never here through
|
|
@@ -861,11 +866,12 @@ export function buildContentLinkIndex(
|
|
|
861
866
|
// discarded here — the walk yields the body and nothing read it.
|
|
862
867
|
// Read from the record rather than from a second reading of the
|
|
863
868
|
// note's headings — the one-anchor-reader rule.
|
|
864
|
-
anchors: new Set(
|
|
865
|
-
(record.anchors ?? [])
|
|
869
|
+
anchors: new Set([
|
|
870
|
+
...(record.anchors ?? [])
|
|
866
871
|
.filter((anchor) => anchor.kind === "prose")
|
|
867
872
|
.map((anchor) => anchor.slug),
|
|
868
|
-
|
|
873
|
+
...(prepared?.anchors ?? []).map((anchor) => anchor.slug),
|
|
874
|
+
]),
|
|
869
875
|
// The note's event ids, which a wikilink may not name: a link to
|
|
870
876
|
// one is refused with its own message rather than as unknown.
|
|
871
877
|
eventAnchors: new Set(
|
|
@@ -873,8 +879,8 @@ export function buildContentLinkIndex(
|
|
|
873
879
|
.filter((anchor) => anchor.kind === "event")
|
|
874
880
|
.map((anchor) => anchor.slug),
|
|
875
881
|
),
|
|
876
|
-
anchorUuids: record.foundry?.note?.anchors,
|
|
877
|
-
docAnchorUuids: documentation?.foundry?.note?.anchors,
|
|
882
|
+
anchorUuids: prepared?.anchorUuids ?? record.foundry?.note?.anchors,
|
|
883
|
+
docAnchorUuids: prepared?.docAnchorUuids ?? documentation?.foundry?.note?.anchors,
|
|
878
884
|
});
|
|
879
885
|
}
|
|
880
886
|
// Packages this build links *into* but does not publish. Each publishes
|
package/engine/journals.mjs
CHANGED
|
@@ -73,6 +73,7 @@ import { reckoningContext } from "./reckoning-markers.mjs";
|
|
|
73
73
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
74
74
|
import { JOURNAL_TYPES } from "./ids.mjs";
|
|
75
75
|
import { journalHasContent } from "./note-state.mjs";
|
|
76
|
+
import { hasLexicalEntries } from "./lexical-sections.mjs";
|
|
76
77
|
import { draftNoticeFor } from "./draft-notice.mjs";
|
|
77
78
|
import { scanFigures } from "./content-figures.mjs";
|
|
78
79
|
import { separateFootnotes } from "./content-footnotes.mjs";
|
|
@@ -760,13 +761,15 @@ export class Journals extends BasePackCompiler {
|
|
|
760
761
|
* An entry holding only its infobox is empty by this test. The panel is a
|
|
761
762
|
* rendering of `data:` that the content index already carries, so a
|
|
762
763
|
* compendium entry of nothing else tells a reader what the index told them.
|
|
764
|
+
* A frontmatter dictionary or glossary supplies pages of its own, so it
|
|
765
|
+
* qualifies before the shared body expansion inserts it.
|
|
763
766
|
*
|
|
764
767
|
* @param {object} fm - The note's frontmatter.
|
|
765
768
|
* @param {string} body - The note body, frontmatter stripped.
|
|
766
769
|
* @returns {boolean} True to skip the note.
|
|
767
770
|
*/
|
|
768
771
|
skipNote(fm, body) {
|
|
769
|
-
return !journalHasContent(body);
|
|
772
|
+
return !journalHasContent(body) && !hasLexicalEntries(fm);
|
|
770
773
|
}
|
|
771
774
|
|
|
772
775
|
/**
|