@heroiclands/package-build 23.7.1 → 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 +41 -0
- package/README.md +32 -0
- package/bin/content-commands.mjs +32 -10
- package/ci/check-banners.mjs +37 -19
- package/container.mjs +48 -10
- package/docs/api.md +402 -58
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/frontmatter.md +30 -1
- package/docs/authoring/links-and-markup.md +154 -56
- package/docs/commands.md +31 -12
- package/docs/content-format.md +25 -2
- package/docs/design.md +31 -0
- package/docs/diagnostics.md +1 -1
- package/docs/guides/e2e.md +37 -1
- package/docs/guides/packs.md +2 -1
- package/docs/guides/reference-pages.md +86 -63
- package/docs/project-setup.md +30 -15
- package/docs/reference/format-details.md +313 -132
- package/docs/reference/note-types.md +191 -174
- package/e2e.mjs +89 -2
- package/engine/anchors.mjs +7 -20
- package/engine/base-compiler.mjs +11 -18
- package/engine/compile-corpus.mjs +5 -3
- package/engine/content-alerts.mjs +4 -2
- package/engine/content-blocks.mjs +18 -36
- package/engine/content-format.yaml +1 -0
- package/engine/content-index.mjs +447 -33
- package/engine/content-links.mjs +43 -15
- package/engine/content-poetry.mjs +9 -0
- package/engine/content-tables.mjs +8 -4
- package/engine/culture-reach.mjs +61 -34
- package/engine/data-keys.mjs +3 -1
- package/engine/diagnostics.mjs +24 -11
- package/engine/dictionary-layout.mjs +151 -19
- package/engine/dictionary-rows.mjs +129 -79
- package/engine/foundry-entries.mjs +21 -3
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/generate.mjs +12 -22
- package/engine/glossary-entries.mjs +11 -11
- package/engine/heading-attributes.mjs +21 -28
- package/engine/helpers.mjs +20 -12
- package/engine/infobox.mjs +3 -0
- package/engine/journals.mjs +15 -2
- 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/markdownlint-run.mjs +12 -2
- package/engine/markup-validation.mjs +34 -0
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-claims.mjs +21 -10
- package/engine/note-package.mjs +3 -5
- package/engine/note-state.mjs +9 -3
- package/engine/note-vocabulary.mjs +36 -7
- package/engine/pdf-build.mjs +17 -10
- package/engine/prose-lint.mjs +12 -6
- package/engine/related-pages.mjs +22 -3
- package/engine/see-also.mjs +150 -0
- package/engine/site-build.mjs +21 -3
- package/engine/site-preview.mjs +4 -2
- package/engine/sql-tables.mjs +287 -53
- package/engine/standings.mjs +14 -12
- 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/hugo-theme/layouts/partials/related.html +25 -8
- package/package.json +2 -1
- package/types/container.d.mts +4 -3
- package/types/e2e.d.mts +28 -0
- package/types/engine/base-compiler.d.mts +2 -2
- package/types/engine/compile-corpus.d.mts +2 -1
- package/types/engine/content-index.d.mts +85 -8
- 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/diagnostics.d.mts +7 -6
- 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 +11 -5
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/lexical-sections.d.mts +95 -0
- package/types/engine/markdownlint-run.d.mts +3 -1
- package/types/engine/markup-validation.d.mts +14 -0
- package/types/engine/note-addresses.d.mts +10 -0
- package/types/engine/note-claims.d.mts +7 -1
- package/types/engine/note-package.d.mts +3 -1
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +1 -0
- package/types/engine/related-pages.d.mts +10 -1
- package/types/engine/see-also.d.mts +55 -0
- package/types/engine/site-build.d.mts +4 -1
- package/types/engine/sql-tables.d.mts +40 -6
- package/types/engine/word-rows.d.mts +9 -7
package/engine/content-links.mjs
CHANGED
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
|
|
91
91
|
import { frontmatterWikilinks, slugify } from "./web-wikilinks.mjs";
|
|
92
92
|
import { homepageAddresses, isHomepage } from "./homepage.mjs";
|
|
93
|
-
import { parseWikilink, WIKILINK } from "./wikilink-syntax.mjs";
|
|
93
|
+
import { isEventAnchor, parseWikilink, WIKILINK } from "./wikilink-syntax.mjs";
|
|
94
94
|
import { readQualifier } from "./wikilinks.mjs";
|
|
95
95
|
|
|
96
96
|
/**
|
|
@@ -243,7 +243,10 @@ export function buildLinkIndex(
|
|
|
243
243
|
// same headings — the disagreement the anchor half removes.
|
|
244
244
|
anchors.set(
|
|
245
245
|
note,
|
|
246
|
-
new Set(
|
|
246
|
+
new Set([
|
|
247
|
+
...(record.anchors ?? []).filter((a) => a.kind === "prose").map((a) => a.slug),
|
|
248
|
+
...(sqlTables?.get(absPath)?.anchors ?? []).map((a) => a.slug),
|
|
249
|
+
]),
|
|
247
250
|
);
|
|
248
251
|
eventAnchorsOf.set(
|
|
249
252
|
note,
|
|
@@ -331,7 +334,13 @@ export function buildLinkIndex(
|
|
|
331
334
|
*/
|
|
332
335
|
function expandedBody(note) {
|
|
333
336
|
const body = note.body;
|
|
334
|
-
|
|
337
|
+
const prepared = sqlTables?.get(note.file);
|
|
338
|
+
if (
|
|
339
|
+
!prepared?.generated &&
|
|
340
|
+
!prepared?.lexical?.length &&
|
|
341
|
+
!/^[ \t]*(?:`{3,}|~{3,})[ \t]*(?:sql|pagelist)\b/im.test(body)
|
|
342
|
+
)
|
|
343
|
+
return body;
|
|
335
344
|
return expandContentTables(body, {
|
|
336
345
|
source: note.file,
|
|
337
346
|
// A generated link is checked like an authored one, so both
|
|
@@ -940,18 +949,37 @@ export function auditLinks(index) {
|
|
|
940
949
|
for (const note of notes) {
|
|
941
950
|
for (const { target, rawTarget, anchor, text, occurrence, labelled } of linksOf(note)) {
|
|
942
951
|
if (!anchor || !labelled) continue;
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
if (!
|
|
949
|
-
|
|
952
|
+
const local = target ? resolve(target) : note;
|
|
953
|
+
const foreign = !local && target ? manifestHit(target) : null;
|
|
954
|
+
// An unresolved address is reported below. A foreign sheet opens
|
|
955
|
+
// its system document, so only the readable note view names prose
|
|
956
|
+
// sections. A public note's GM target is an audience refusal below.
|
|
957
|
+
if (!local) {
|
|
958
|
+
if (!foreign || (foreign.gm && !isGmNote(note.fm))) continue;
|
|
959
|
+
const qualified = readQualifier(
|
|
960
|
+
target,
|
|
961
|
+
index.types,
|
|
962
|
+
index.packages,
|
|
963
|
+
index.noIndexPackages,
|
|
964
|
+
);
|
|
965
|
+
if (qualified?.system !== NOTE_SYSTEM) continue;
|
|
966
|
+
}
|
|
967
|
+
const dest = local ?? foreign;
|
|
968
|
+
const slug = local ? slugify(anchor) : anchor;
|
|
969
|
+
const event =
|
|
970
|
+
local ?
|
|
971
|
+
(index.eventAnchorsOf?.get(local) ?? new Set()).has(anchor)
|
|
972
|
+
: isEventAnchor(foreign, anchor);
|
|
973
|
+
// Fetched note anchors are the publisher's declaration, independent
|
|
974
|
+
// of a site URL or a Foundry UUID. An asset has no body and therefore
|
|
975
|
+
// declares no sections; an absent foreign anchor list does too.
|
|
976
|
+
const declared =
|
|
977
|
+
local ?
|
|
978
|
+
(anchors.get(local) ?? new Set()).has(slug)
|
|
979
|
+
: foreign.noteAnchors?.some((one) => one.slug === anchor && one.kind === "prose");
|
|
980
|
+
if (!declared || (!local && event)) {
|
|
950
981
|
deadAnchors.push({
|
|
951
|
-
reason:
|
|
952
|
-
(index.eventAnchorsOf?.get(dest) ?? new Set()).has(anchor) ?
|
|
953
|
-
"event-anchor"
|
|
954
|
-
: "unknown-anchor",
|
|
982
|
+
reason: event ? "event-anchor" : "unknown-anchor",
|
|
955
983
|
note,
|
|
956
984
|
link: `${rawTarget}#${anchor}`,
|
|
957
985
|
// The two the shared `unknown-anchor` wording reads, in the
|
|
@@ -960,7 +988,7 @@ export function auditLinks(index) {
|
|
|
960
988
|
// declare. Carrying them is what lets one sentence serve
|
|
961
989
|
// the checker and the build.
|
|
962
990
|
target: rawTarget,
|
|
963
|
-
anchor:
|
|
991
|
+
anchor: slug,
|
|
964
992
|
dest,
|
|
965
993
|
text,
|
|
966
994
|
occurrence,
|
|
@@ -71,9 +71,12 @@ export function poetryMarkdown(body) {
|
|
|
71
71
|
.join("\n\n");
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
import MarkdownIt from "markdown-it";
|
|
74
75
|
import { slugify } from "./content-slug.mjs";
|
|
75
76
|
import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
|
|
76
77
|
|
|
78
|
+
const structureParser = new MarkdownIt({ html: true });
|
|
79
|
+
|
|
77
80
|
/** Read poetry code fences; start/end are inclusive zero-based source lines.
|
|
78
81
|
* @param {string} source - Authored Markdown.
|
|
79
82
|
*/
|
|
@@ -81,8 +84,14 @@ export function scanPoetry(source) {
|
|
|
81
84
|
const lines = String(source ?? "").split("\n"),
|
|
82
85
|
blocks = [],
|
|
83
86
|
errors = [];
|
|
87
|
+
const literalLines = new Set();
|
|
88
|
+
for (const token of structureParser.parse(String(source ?? ""), {})) {
|
|
89
|
+
if (!token.map || !["blockquote_open", "html_block"].includes(token.type)) continue;
|
|
90
|
+
for (let line = token.map[0]; line < token.map[1]; line++) literalLines.add(line);
|
|
91
|
+
}
|
|
84
92
|
let opening = null;
|
|
85
93
|
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
if (literalLines.has(i)) continue;
|
|
86
95
|
const match = /^ *(`{3,}|~{3,})(.*)$/.exec(lines[i]);
|
|
87
96
|
if (opening) {
|
|
88
97
|
if (
|
|
@@ -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
|
|
package/engine/diagnostics.mjs
CHANGED
|
@@ -249,15 +249,20 @@ export function positionInFrontmatter(raw, key, value = undefined, { topLevel =
|
|
|
249
249
|
* {@link positionInFrontmatter}.
|
|
250
250
|
* @param {boolean} [options.topLevel=false] - Require the key at column 1, so
|
|
251
251
|
* an identically named nested key cannot answer for it.
|
|
252
|
+
* @param {string} [options.raw] - Source text already read by the caller.
|
|
252
253
|
* @returns {{line?: number, column?: number}|undefined} Spreadable position
|
|
253
254
|
* fields, dropped rather than guessed when the file cannot be read or the key
|
|
254
255
|
* cannot be found — as `formatDiagnostic` requires.
|
|
255
256
|
*/
|
|
256
|
-
export function locateFrontmatterKey(
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
export function locateFrontmatterKey(
|
|
258
|
+
absPath,
|
|
259
|
+
key,
|
|
260
|
+
value = undefined,
|
|
261
|
+
{ topLevel = false, raw } = {},
|
|
262
|
+
) {
|
|
263
|
+
if (!absPath && raw === undefined) return undefined;
|
|
259
264
|
try {
|
|
260
|
-
raw
|
|
265
|
+
raw ??= fs.readFileSync(absPath, "utf8");
|
|
261
266
|
} catch {
|
|
262
267
|
return undefined;
|
|
263
268
|
}
|
|
@@ -306,6 +311,9 @@ export function positionOfLiteral(text, needle, occurrence = 1) {
|
|
|
306
311
|
};
|
|
307
312
|
}
|
|
308
313
|
|
|
314
|
+
/** The most recently located text; retaining one document bounds memory. */
|
|
315
|
+
let locatedYaml;
|
|
316
|
+
|
|
309
317
|
/**
|
|
310
318
|
* Where a **key or value in a YAML document** sits, addressed by its path.
|
|
311
319
|
*
|
|
@@ -315,11 +323,10 @@ export function positionOfLiteral(text, needle, occurrence = 1) {
|
|
|
315
323
|
* first occurrence is routinely not the one the finding is about — which is the
|
|
316
324
|
* one thing the located form exists to prevent.
|
|
317
325
|
*
|
|
318
|
-
* A path resolves the exact node
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
* representation of the same file to keep in step.
|
|
326
|
+
* A path resolves the exact node. The most recently located text retains its
|
|
327
|
+
* document and line counter, so several findings in one note share a parse.
|
|
328
|
+
* A different text replaces that entry, including an edited copy of the same
|
|
329
|
+
* file. The loader returns plain data without source ranges.
|
|
323
330
|
*
|
|
324
331
|
* Every failure — unparseable text, an `.mjs` configuration, a path that
|
|
325
332
|
* resolves to nothing — yields `{}`, so a caller spreads the result and the
|
|
@@ -348,9 +355,15 @@ export function positionOfYamlPath(text, keyPath, { key = false } = {}) {
|
|
|
348
355
|
if (!Array.isArray(keyPath) || keyPath.length === 0) return {};
|
|
349
356
|
|
|
350
357
|
let node;
|
|
351
|
-
|
|
358
|
+
let counter;
|
|
352
359
|
try {
|
|
353
|
-
|
|
360
|
+
if (locatedYaml?.text !== text) {
|
|
361
|
+
const lineCounter = new LineCounter();
|
|
362
|
+
const doc = YAML.parseDocument(text, { lineCounter });
|
|
363
|
+
locatedYaml = { text, doc, counter: lineCounter };
|
|
364
|
+
}
|
|
365
|
+
const { doc } = locatedYaml;
|
|
366
|
+
counter = locatedYaml.counter;
|
|
354
367
|
// `keepScalar` returns the Scalar node rather than its value, which is
|
|
355
368
|
// the only form carrying a range.
|
|
356
369
|
node = doc.getIn(keyPath, true);
|