@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
package/engine/content-spans.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { slugify } from "./content-slug.mjs";
|
|
|
17
17
|
import { scanPoetry } from "./content-poetry.mjs";
|
|
18
18
|
import footnotePlugin from "markdown-it-footnote";
|
|
19
19
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
20
|
+
import { inlineText } from "./name-markup.mjs";
|
|
20
21
|
|
|
21
22
|
/** Add bracket spans after recognized Markdown links and footnotes.
|
|
22
23
|
* @param {import("markdown-it")} md - Markdown parser to extend.
|
|
@@ -57,7 +58,9 @@ export function spanMarkdownPlugin(md) {
|
|
|
57
58
|
if (close === state.posMax)
|
|
58
59
|
parsed.problems.push("span attributes need closing {…} braces");
|
|
59
60
|
else {
|
|
60
|
-
parsed = parseExtensionAttributes(state.src.slice(finish + 1, close)
|
|
61
|
+
parsed = parseExtensionAttributes(state.src.slice(finish + 1, close), {
|
|
62
|
+
positions: true,
|
|
63
|
+
});
|
|
61
64
|
finish = close + 1;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -73,6 +76,17 @@ export function spanMarkdownPlugin(md) {
|
|
|
73
76
|
end: finish,
|
|
74
77
|
text: state.src.slice(start + 1, end),
|
|
75
78
|
problems,
|
|
79
|
+
attributePositions: Object.fromEntries(
|
|
80
|
+
Object.entries(parsed.positions ?? {}).map(([key, positions]) => [
|
|
81
|
+
key,
|
|
82
|
+
Object.fromEntries(
|
|
83
|
+
Object.entries(positions).map(([part, range]) => [
|
|
84
|
+
part,
|
|
85
|
+
{ start: range.start + end + 2, end: range.end + end + 2 },
|
|
86
|
+
]),
|
|
87
|
+
),
|
|
88
|
+
]),
|
|
89
|
+
),
|
|
76
90
|
};
|
|
77
91
|
if (parsed.id) token.attrSet("id", slugify(parsed.id));
|
|
78
92
|
if (parsed.classes.length) token.attrSet("class", parsed.classes.join(" "));
|
|
@@ -84,6 +98,11 @@ export function spanMarkdownPlugin(md) {
|
|
|
84
98
|
if (child.meta?.span) {
|
|
85
99
|
child.meta.start += start + 1;
|
|
86
100
|
child.meta.end += start + 1;
|
|
101
|
+
for (const positions of Object.values(child.meta.attributePositions ?? {}))
|
|
102
|
+
for (const range of Object.values(positions)) {
|
|
103
|
+
range.start += start + 1;
|
|
104
|
+
range.end += start + 1;
|
|
105
|
+
}
|
|
87
106
|
}
|
|
88
107
|
state.tokens.push(...nested);
|
|
89
108
|
state.push("span_close", "span", -1);
|
|
@@ -98,6 +117,7 @@ const parser = new MarkdownIt({ html: true })
|
|
|
98
117
|
.use(spanMarkdownPlugin);
|
|
99
118
|
/** Read span anchors and located attribute diagnostics outside literal code.
|
|
100
119
|
* @param {string} source - Authored Markdown.
|
|
120
|
+
* @returns {import("./name-markup.mjs").SpanScan} Actual spans, explicit names and located diagnostics.
|
|
101
121
|
*/
|
|
102
122
|
export function scanSpans(source) {
|
|
103
123
|
const text = String(source ?? ""),
|
|
@@ -112,14 +132,28 @@ export function scanSpans(source) {
|
|
|
112
132
|
starts.push(offset);
|
|
113
133
|
offset += line.length + 1;
|
|
114
134
|
}
|
|
135
|
+
const located = (start, end) => {
|
|
136
|
+
let lo = 0,
|
|
137
|
+
hi = starts.length;
|
|
138
|
+
while (lo + 1 < hi) {
|
|
139
|
+
const mid = (lo + hi) >>> 1;
|
|
140
|
+
if (starts[mid] <= start) lo = mid;
|
|
141
|
+
else hi = mid;
|
|
142
|
+
}
|
|
143
|
+
return { start, end, line: lo + 1, column: start - starts[lo] + 1 };
|
|
144
|
+
};
|
|
115
145
|
const collect = (children, position) => {
|
|
116
|
-
for (const child of children ?? []) {
|
|
146
|
+
for (const [index, child] of (children ?? []).entries()) {
|
|
117
147
|
if (!child.meta?.span) continue;
|
|
118
148
|
const start = position(child.meta.start),
|
|
119
149
|
end = position(child.meta.end);
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
150
|
+
const { line, column } = located(start, end);
|
|
151
|
+
let depth = 1,
|
|
152
|
+
close = index + 1;
|
|
153
|
+
for (; close < children.length; close++) {
|
|
154
|
+
if (children[close].type === "span_open") depth++;
|
|
155
|
+
if (children[close].type === "span_close" && --depth === 0) break;
|
|
156
|
+
}
|
|
123
157
|
spans.push({
|
|
124
158
|
id: child.attrGet("id") ?? "",
|
|
125
159
|
line,
|
|
@@ -127,13 +161,50 @@ export function scanSpans(source) {
|
|
|
127
161
|
start,
|
|
128
162
|
end,
|
|
129
163
|
text: child.meta.text,
|
|
164
|
+
contentEnd: position(child.meta.start + 1 + child.meta.text.length),
|
|
165
|
+
visible: inlineText(children.slice(index + 1, close)),
|
|
130
166
|
attrs: child.attrs ?? [],
|
|
167
|
+
attributePositions: Object.fromEntries(
|
|
168
|
+
Object.entries(child.meta.attributePositions ?? {}).map(([key, positions]) => [
|
|
169
|
+
key,
|
|
170
|
+
Object.fromEntries(
|
|
171
|
+
Object.entries(positions).map(([part, range]) => [
|
|
172
|
+
part,
|
|
173
|
+
located(position(range.start), position(range.end)),
|
|
174
|
+
]),
|
|
175
|
+
),
|
|
176
|
+
]),
|
|
177
|
+
),
|
|
131
178
|
});
|
|
132
179
|
for (const message of child.meta.problems) errors.push({ line, column, message });
|
|
133
180
|
}
|
|
134
181
|
};
|
|
182
|
+
let row;
|
|
135
183
|
for (const token of tokens) {
|
|
136
|
-
if (token.type
|
|
184
|
+
if (token.type === "tr_open" && token.map) {
|
|
185
|
+
const line = lines[token.map[0]],
|
|
186
|
+
offsets = [];
|
|
187
|
+
let content = "";
|
|
188
|
+
// The existing table rule removes a backslash immediately before
|
|
189
|
+
// a pipe. Retain that projection's original character positions;
|
|
190
|
+
// actual row/cell tokens still own all table parsing and trimming.
|
|
191
|
+
for (let i = 0; i < line.length; i++) {
|
|
192
|
+
if (line[i] === "\\" && line[i + 1] === "|") continue;
|
|
193
|
+
offsets.push(i);
|
|
194
|
+
content += line[i];
|
|
195
|
+
}
|
|
196
|
+
offsets.push(line.length);
|
|
197
|
+
row = { content, offsets, line: token.map[0], cursor: 0 };
|
|
198
|
+
} else if (token.type === "tr_close") row = undefined;
|
|
199
|
+
if (token.type !== "inline") continue;
|
|
200
|
+
if (!token.map) {
|
|
201
|
+
if (!row) continue;
|
|
202
|
+
const column = row.content.indexOf(token.content, row.cursor);
|
|
203
|
+
if (column < 0) continue;
|
|
204
|
+
row.cursor = column + token.content.length;
|
|
205
|
+
collect(token.children, (pos) => starts[row.line] + row.offsets[column + pos]);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
137
208
|
const contentLines = token.content.split("\n"),
|
|
138
209
|
positions = [];
|
|
139
210
|
let local = 0;
|
|
@@ -159,7 +230,15 @@ export function scanSpans(source) {
|
|
|
159
230
|
}
|
|
160
231
|
spans.sort((a, b) => a.start - b.start);
|
|
161
232
|
errors.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
162
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
spans,
|
|
235
|
+
names: spans.filter((span) =>
|
|
236
|
+
span.attrs.some(
|
|
237
|
+
([key, value]) => key === "class" && value.split(/\s+/).includes("name"),
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
errors,
|
|
241
|
+
};
|
|
163
242
|
}
|
|
164
243
|
|
|
165
244
|
/** Emit span tags while leaving their inline content as Markdown for Hugo.
|
|
@@ -182,7 +261,7 @@ export function renderSpans(source) {
|
|
|
182
261
|
const attrs = span.attrs.map(([key, value]) => ` ${key}="${escape(value)}"`).join("");
|
|
183
262
|
markdown =
|
|
184
263
|
markdown.slice(0, span.start) +
|
|
185
|
-
`<span${attrs}>${renderSpans(span.
|
|
264
|
+
`<span${attrs}>${renderSpans(markdown.slice(span.start + 1, span.contentEnd)).markdown}</span>` +
|
|
186
265
|
markdown.slice(span.end);
|
|
187
266
|
}
|
|
188
267
|
return { markdown, errors };
|
|
@@ -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,13 @@ 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
|
+
syntax: sqlTables?.syntax,
|
|
158
|
+
}),
|
|
152
159
|
errors,
|
|
153
160
|
warnings,
|
|
154
|
-
lineMap,
|
|
155
|
-
...(generatedFrom === undefined ? {} : { generatedFrom }),
|
|
156
161
|
};
|
|
157
162
|
}
|
|
@@ -43,9 +43,10 @@
|
|
|
43
43
|
|
|
44
44
|
import fs from "node:fs";
|
|
45
45
|
import path from "node:path";
|
|
46
|
+
import { capturedSources } from "./content-sources.mjs";
|
|
46
47
|
|
|
47
48
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
48
|
-
import { positionInBody } from "./diagnostics.mjs";
|
|
49
|
+
import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* A list item's marker, immediately followed by `[ ]` or `[x]` and a space —
|
|
@@ -114,15 +115,30 @@ export function checkTaskLists(body, file, { bodyLine = 1, bodyColumn = 1 } = {}
|
|
|
114
115
|
* @param {object} [opts]
|
|
115
116
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
|
|
116
117
|
* in addition to the dot-directories always skipped.
|
|
118
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
|
|
117
119
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
118
120
|
* severity: "warning", message: string}>, files: number}} The findings, and
|
|
119
121
|
* how many files were read.
|
|
120
122
|
*/
|
|
121
|
-
export function lintContentTaskLists(contentBase, { skipDirectories = [] } = {}) {
|
|
123
|
+
export function lintContentTaskLists(contentBase, { skipDirectories = [], sources } = {}) {
|
|
122
124
|
const skip = new Set(skipDirectories);
|
|
123
125
|
/** @type {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} */
|
|
124
126
|
const findings = [];
|
|
125
127
|
let files = 0;
|
|
128
|
+
if (sources) {
|
|
129
|
+
for (const [full, content] of capturedSources(
|
|
130
|
+
contentBase,
|
|
131
|
+
sources,
|
|
132
|
+
skipDirectories,
|
|
133
|
+
new Set([".md", ".markdown"]),
|
|
134
|
+
)) {
|
|
135
|
+
files++;
|
|
136
|
+
findings.push(
|
|
137
|
+
...checkTaskLists(...bodyOf(content, path.relative(process.cwd(), full))),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return { findings, files };
|
|
141
|
+
}
|
|
126
142
|
|
|
127
143
|
/** @param {string} dir - Directory to descend into. */
|
|
128
144
|
const walk = (dir) => {
|
|
@@ -171,10 +187,10 @@ export function lintContentTaskLists(contentBase, { skipDirectories = [] } = {})
|
|
|
171
187
|
* arguments {@link checkTaskLists} takes.
|
|
172
188
|
*/
|
|
173
189
|
function bodyOf(content, file) {
|
|
174
|
-
const match = content
|
|
190
|
+
const match = splitFrontmatter(content);
|
|
175
191
|
if (!match) return [content, file, { bodyLine: 1, bodyColumn: 1 }];
|
|
176
192
|
|
|
177
|
-
const raw = match
|
|
193
|
+
const raw = match.body;
|
|
178
194
|
const body = raw.trim();
|
|
179
195
|
const bodyStart = content.length - raw.length + (raw.length - raw.trimStart().length);
|
|
180
196
|
const before = content.slice(0, bodyStart);
|
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
|
|
package/engine/diagnostics.mjs
CHANGED
|
@@ -46,6 +46,17 @@ import fs from "node:fs";
|
|
|
46
46
|
import path from "node:path";
|
|
47
47
|
import YAML, { LineCounter } from "yaml";
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Split opening YAML frontmatter from its untrimmed body without parsing it.
|
|
51
|
+
* LF and CRLF delimiters retain the exact source strings and their offsets.
|
|
52
|
+
* @param {string} raw - Complete Markdown source.
|
|
53
|
+
* @returns {{yaml: string, body: string}|null} Source slices, or no fence.
|
|
54
|
+
*/
|
|
55
|
+
export function splitFrontmatter(raw) {
|
|
56
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n)?([\s\S]*)$/);
|
|
57
|
+
return match ? { yaml: match[1], body: match[2] } : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* The `file:line:column` locator, with whatever is known.
|
|
51
62
|
*
|
|
@@ -201,9 +212,9 @@ export function positionInBody(body, offset, { bodyLine = 1, bodyColumn = 1, lin
|
|
|
201
212
|
*/
|
|
202
213
|
export function positionInFrontmatter(raw, key, value = undefined, { topLevel = false } = {}) {
|
|
203
214
|
if (typeof raw !== "string" || !key) return {};
|
|
204
|
-
const fence = raw
|
|
215
|
+
const fence = splitFrontmatter(raw);
|
|
205
216
|
if (!fence) return {};
|
|
206
|
-
const block = fence
|
|
217
|
+
const block = fence.yaml;
|
|
207
218
|
const lines = block.split("\n");
|
|
208
219
|
|
|
209
220
|
// A value locates the exact entry; the key alone locates its declaration.
|
|
@@ -437,10 +448,10 @@ export function yamlKeyPath(field) {
|
|
|
437
448
|
export function frontmatterValueAt(raw, keyPath) {
|
|
438
449
|
if (typeof raw !== "string" || !Array.isArray(keyPath) || keyPath.length === 0)
|
|
439
450
|
return undefined;
|
|
440
|
-
const fence = raw
|
|
451
|
+
const fence = splitFrontmatter(raw);
|
|
441
452
|
if (!fence) return undefined;
|
|
442
453
|
try {
|
|
443
|
-
const node = YAML.parseDocument(fence
|
|
454
|
+
const node = YAML.parseDocument(fence.yaml).getIn(keyPath, true);
|
|
444
455
|
const value = node?.value;
|
|
445
456
|
return ["string", "number", "boolean"].includes(typeof value) ? value : undefined;
|
|
446
457
|
} catch {
|
|
@@ -473,9 +484,9 @@ export function frontmatterValueAt(raw, keyPath) {
|
|
|
473
484
|
*/
|
|
474
485
|
export function positionOfFrontmatterPath(raw, keyPath, { key = false } = {}) {
|
|
475
486
|
if (typeof raw !== "string") return {};
|
|
476
|
-
const fence = raw
|
|
487
|
+
const fence = splitFrontmatter(raw);
|
|
477
488
|
if (!fence) return {};
|
|
478
|
-
const position = positionOfYamlPath(fence
|
|
489
|
+
const position = positionOfYamlPath(fence.yaml, keyPath, { key });
|
|
479
490
|
// +1: the fence's line 1 is the file's line 2, the opening `---` being the
|
|
480
491
|
// first. `positionOfYamlPath` already counts from 1 within the block.
|
|
481
492
|
return position.line === undefined ? {} : { ...position, line: position.line + 1 };
|