@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
|
@@ -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
|
}
|
|
@@ -1541,7 +1541,7 @@ export function lintNote(
|
|
|
1541
1541
|
for (const field of fields) {
|
|
1542
1542
|
if (note.inheritedCultureFields?.has(field.name)) continue;
|
|
1543
1543
|
if (typeof field.check !== "function") continue;
|
|
1544
|
-
findings.push(...field.check(note, { index }));
|
|
1544
|
+
findings.push(...field.check(note, { index, addressContext: referenceContext }));
|
|
1545
1545
|
}
|
|
1546
1546
|
// A check the type declares of the whole note, with the same index.
|
|
1547
1547
|
if (typeof entry.check === "function") findings.push(...entry.check(note, { index }));
|
package/engine/generate.mjs
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* The HeroicLands vault is not a build input for SoHL content.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { noteAddressContext } from "./note-addresses.mjs";
|
|
41
|
+
import { noteAddressContext, decodeNoteAddresses } from "./note-addresses.mjs";
|
|
42
42
|
import { GENERATED_DRAWING_DIR } from "./generated-sections.mjs";
|
|
43
43
|
import fs from "fs";
|
|
44
44
|
import path from "path";
|
|
@@ -54,7 +54,7 @@ import { Hm3Actors } from "../hm3/actors.mjs";
|
|
|
54
54
|
import { Macros } from "./macros.mjs";
|
|
55
55
|
import { Scenes } from "./scenes.mjs";
|
|
56
56
|
import { Bundles } from "./bundles.mjs";
|
|
57
|
-
import { statsForPack,
|
|
57
|
+
import { statsForPack, folderFilename } from "./helpers.mjs";
|
|
58
58
|
import {
|
|
59
59
|
buildFolderNoteIndex,
|
|
60
60
|
collectFolderNotes,
|
|
@@ -800,7 +800,10 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
|
|
|
800
800
|
// every type a system deliberately does not map, which is exactly the
|
|
801
801
|
// silence the rule requires. Independent of `only`, since it is a fact about the
|
|
802
802
|
// configured pack list rather than about which passes this run executes.
|
|
803
|
-
const unclaimed = unclaimedNoteFindings(config, undefined, {
|
|
803
|
+
const unclaimed = unclaimedNoteFindings(config, undefined, {
|
|
804
|
+
records: corpus.records,
|
|
805
|
+
parsed: corpus.parsed,
|
|
806
|
+
});
|
|
804
807
|
for (const finding of unclaimed) emitDiagnostic(finding);
|
|
805
808
|
|
|
806
809
|
fs.mkdirSync(config.paths.packJson, { recursive: true });
|
|
@@ -831,22 +834,8 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
|
|
|
831
834
|
try {
|
|
832
835
|
folderNotes = buildFolderNoteIndex(
|
|
833
836
|
collectFolderNotes(
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
// places where that distinction is load-bearing rather than
|
|
837
|
-
// incidental.
|
|
838
|
-
//
|
|
839
|
-
// `collectFolderNotes` treats `fm.id` as an **authored pin**,
|
|
840
|
-
// which wins over the id it derives under the folder namespace.
|
|
841
|
-
// A record's `id` is not that: the index fills it in for every
|
|
842
|
-
// addressable note, so handing records straight over
|
|
843
|
-
// would make every folder look pinned. The *value*
|
|
844
|
-
// would be right either way — the index derives a folder's id
|
|
845
|
-
// under the folder namespace, so the two agree — but `derivedId`
|
|
846
|
-
// would not, and it is what tells an author whether a duplicate
|
|
847
|
-
// id was two pins or two addresses. The index cannot tell a pin
|
|
848
|
-
// from a derivation, so the note is read — and only folder notes
|
|
849
|
-
// are, 79 of `sohl`'s 1,685 rather than all of them.
|
|
837
|
+
// Folder pins are authored ids; index-derived ids must not
|
|
838
|
+
// turn an unpinned folder into a pinned one.
|
|
850
839
|
corpus.records
|
|
851
840
|
.filter(
|
|
852
841
|
(record) =>
|
|
@@ -856,9 +845,10 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
|
|
|
856
845
|
.map((record) => {
|
|
857
846
|
const absPath = noteFile(contentBase, record);
|
|
858
847
|
return {
|
|
859
|
-
frontmatter:
|
|
860
|
-
|
|
861
|
-
|
|
848
|
+
frontmatter: decodeNoteAddresses(
|
|
849
|
+
structuredClone(corpus.parsed.get(absPath).frontmatter),
|
|
850
|
+
noteAddressContext(config),
|
|
851
|
+
),
|
|
862
852
|
absPath,
|
|
863
853
|
};
|
|
864
854
|
}),
|
|
@@ -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)
|
|
@@ -233,17 +235,18 @@ export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, res
|
|
|
233
235
|
/**
|
|
234
236
|
* Parses a markdown file with YAML frontmatter.
|
|
235
237
|
*
|
|
236
|
-
* Returns `{ frontmatter, body, description, bodyLine, bodyColumn }` where
|
|
238
|
+
* Returns `{ frontmatter, body, description, bodyLine, bodyColumn, raw }` where
|
|
237
239
|
* `body` is the trimmed raw markdown after the frontmatter block, and
|
|
238
|
-
* `description` is `body` rendered to HTML.
|
|
240
|
+
* `description` is `body` rendered to HTML. The full source is retained as
|
|
241
|
+
* `raw` for diagnostic locations. `bodyLine` / `bodyColumn` are the
|
|
239
242
|
* 1-based **file** position of the body's first character, which is what turns
|
|
240
243
|
* an offset within `body` into a position a diagnostic can name — see
|
|
241
244
|
* {@link positionInBody}. If the file has no frontmatter block, returns
|
|
242
|
-
* `{ frontmatter: null, body: "", description: "" }`
|
|
245
|
+
* `{ frontmatter: null, body: "", description: "", raw }` and no
|
|
243
246
|
* position: there is no body to have one.
|
|
244
247
|
* @param {string} filePath - Markdown file to read.
|
|
245
248
|
* @param {{addressContext?: object}} [opts] - Address defaults for decoded fields.
|
|
246
|
-
* @returns {{frontmatter: object|null, body: string, description: string,
|
|
249
|
+
* @returns {{frontmatter: object|null, body: string, description: string, raw: string,
|
|
247
250
|
* bodyLine?: number, bodyColumn?: number, parseError?: {message: string,
|
|
248
251
|
* line?: number, column?: number}}} Parsed note and body position.
|
|
249
252
|
*/
|
|
@@ -251,7 +254,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
251
254
|
const content = fs.readFileSync(filePath, "utf8");
|
|
252
255
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
253
256
|
if (!fmMatch) {
|
|
254
|
-
return { frontmatter: null, body: "", description: "" };
|
|
257
|
+
return { frontmatter: null, body: "", description: "", raw: content };
|
|
255
258
|
}
|
|
256
259
|
let frontmatter;
|
|
257
260
|
try {
|
|
@@ -264,6 +267,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
264
267
|
frontmatter: null,
|
|
265
268
|
body: "",
|
|
266
269
|
description: "",
|
|
270
|
+
raw: content,
|
|
267
271
|
parseError: {
|
|
268
272
|
message: `YAML parse error: ${message}`,
|
|
269
273
|
...(Number.isInteger(location?.line) ? { line: location.line + 1 } : {}),
|
|
@@ -295,7 +299,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
295
299
|
const before = content.slice(0, bodyStart);
|
|
296
300
|
const bodyLine = before.split("\n").length;
|
|
297
301
|
const bodyColumn = bodyStart - before.lastIndexOf("\n");
|
|
298
|
-
return { frontmatter, body, description, bodyLine, bodyColumn };
|
|
302
|
+
return { frontmatter, body, description, bodyLine, bodyColumn, raw: content };
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
/**
|
|
@@ -779,12 +783,14 @@ import { collectAnchors } from "./anchors.mjs";
|
|
|
779
783
|
* omitted.
|
|
780
784
|
* @param {readonly object[]} [opts.records] - The corpus, derived once per
|
|
781
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.
|
|
782
788
|
* @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
|
|
783
789
|
*/
|
|
784
790
|
export function buildContentLinkIndex(
|
|
785
791
|
contentBase,
|
|
786
792
|
router = packRouter(),
|
|
787
|
-
{ config, records } = {},
|
|
793
|
+
{ config, records, sqlTables } = {},
|
|
788
794
|
) {
|
|
789
795
|
const docs = [];
|
|
790
796
|
/** The files this package ships, by canonical address. */
|
|
@@ -810,6 +816,7 @@ export function buildContentLinkIndex(
|
|
|
810
816
|
// authored fields, and neither may be handed the keys the index derived.
|
|
811
817
|
const fm = authoredFrontmatter(record);
|
|
812
818
|
const absPath = noteFile(contentBase, record);
|
|
819
|
+
const prepared = sqlTables?.get(absPath);
|
|
813
820
|
// The id a note's document is filed under: its authored pin, or the
|
|
814
821
|
// one derived from its canonical address. Derived by the index
|
|
815
822
|
// against the configuration this build resolved — never here through
|
|
@@ -859,11 +866,12 @@ export function buildContentLinkIndex(
|
|
|
859
866
|
// discarded here — the walk yields the body and nothing read it.
|
|
860
867
|
// Read from the record rather than from a second reading of the
|
|
861
868
|
// note's headings — the one-anchor-reader rule.
|
|
862
|
-
anchors: new Set(
|
|
863
|
-
(record.anchors ?? [])
|
|
869
|
+
anchors: new Set([
|
|
870
|
+
...(record.anchors ?? [])
|
|
864
871
|
.filter((anchor) => anchor.kind === "prose")
|
|
865
872
|
.map((anchor) => anchor.slug),
|
|
866
|
-
|
|
873
|
+
...(prepared?.anchors ?? []).map((anchor) => anchor.slug),
|
|
874
|
+
]),
|
|
867
875
|
// The note's event ids, which a wikilink may not name: a link to
|
|
868
876
|
// one is refused with its own message rather than as unknown.
|
|
869
877
|
eventAnchors: new Set(
|
|
@@ -871,8 +879,8 @@ export function buildContentLinkIndex(
|
|
|
871
879
|
.filter((anchor) => anchor.kind === "event")
|
|
872
880
|
.map((anchor) => anchor.slug),
|
|
873
881
|
),
|
|
874
|
-
anchorUuids: record.foundry?.note?.anchors,
|
|
875
|
-
docAnchorUuids: documentation?.foundry?.note?.anchors,
|
|
882
|
+
anchorUuids: prepared?.anchorUuids ?? record.foundry?.note?.anchors,
|
|
883
|
+
docAnchorUuids: prepared?.docAnchorUuids ?? documentation?.foundry?.note?.anchors,
|
|
876
884
|
});
|
|
877
885
|
}
|
|
878
886
|
// Packages this build links *into* but does not publish. Each publishes
|
package/engine/infobox.mjs
CHANGED
|
@@ -277,6 +277,9 @@ export const GEAR_UNITS = Object.freeze({
|
|
|
277
277
|
* unit?: string, group?: string, phrase?: (value: any) => string}>>}
|
|
278
278
|
*/
|
|
279
279
|
export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
280
|
+
seeAlso: Object.freeze({
|
|
281
|
+
withheld: "navigation machinery, rendered in its own reading section",
|
|
282
|
+
}),
|
|
280
283
|
...DURATION_LABELS,
|
|
281
284
|
...GEAR_UNITS,
|
|
282
285
|
|
package/engine/journals.mjs
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import log from "loglevel";
|
|
53
|
+
import { seeAlsoEntries, seeAlsoMarkdown } from "./see-also.mjs";
|
|
53
54
|
|
|
54
55
|
import path from "node:path";
|
|
55
56
|
|
|
@@ -72,6 +73,7 @@ import { reckoningContext } from "./reckoning-markers.mjs";
|
|
|
72
73
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
73
74
|
import { JOURNAL_TYPES } from "./ids.mjs";
|
|
74
75
|
import { journalHasContent } from "./note-state.mjs";
|
|
76
|
+
import { hasLexicalEntries } from "./lexical-sections.mjs";
|
|
75
77
|
import { draftNoticeFor } from "./draft-notice.mjs";
|
|
76
78
|
import { scanFigures } from "./content-figures.mjs";
|
|
77
79
|
import { separateFootnotes } from "./content-footnotes.mjs";
|
|
@@ -759,13 +761,15 @@ export class Journals extends BasePackCompiler {
|
|
|
759
761
|
* An entry holding only its infobox is empty by this test. The panel is a
|
|
760
762
|
* rendering of `data:` that the content index already carries, so a
|
|
761
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.
|
|
762
766
|
*
|
|
763
767
|
* @param {object} fm - The note's frontmatter.
|
|
764
768
|
* @param {string} body - The note body, frontmatter stripped.
|
|
765
769
|
* @returns {boolean} True to skip the note.
|
|
766
770
|
*/
|
|
767
771
|
skipNote(fm, body) {
|
|
768
|
-
return !journalHasContent(body);
|
|
772
|
+
return !journalHasContent(body) && !hasLexicalEntries(fm);
|
|
769
773
|
}
|
|
770
774
|
|
|
771
775
|
/**
|
|
@@ -825,7 +829,16 @@ export class Journals extends BasePackCompiler {
|
|
|
825
829
|
return buildJournalEntry({
|
|
826
830
|
id,
|
|
827
831
|
name,
|
|
828
|
-
markdown
|
|
832
|
+
markdown: [
|
|
833
|
+
markdown,
|
|
834
|
+
seeAlsoMarkdown(
|
|
835
|
+
seeAlsoEntries(fm, (ref) => resolveReference(this.linkIndex, ref)),
|
|
836
|
+
linkToUuid,
|
|
837
|
+
1,
|
|
838
|
+
),
|
|
839
|
+
]
|
|
840
|
+
.filter(Boolean)
|
|
841
|
+
.join("\n\n"),
|
|
829
842
|
resolveRole: (pathname) => roles.get(pathname),
|
|
830
843
|
infoboxes: boxes.map((box) => ({
|
|
831
844
|
id: box.id,
|
package/engine/language-data.mjs
CHANGED
|
@@ -160,6 +160,59 @@ export function checkLanguageData(note, { index, types } = {}) {
|
|
|
160
160
|
|
|
161
161
|
const findings = [];
|
|
162
162
|
|
|
163
|
+
// Only the shape of a supplied declaration: no word is compared with it.
|
|
164
|
+
const grammar = isMap(data.grammar) ? data.grammar : {};
|
|
165
|
+
listOf(grammar.plural).forEach((row, i) => {
|
|
166
|
+
if (row == null || (Array.isArray(row) && row.length === 0))
|
|
167
|
+
findings.push(
|
|
168
|
+
finding(
|
|
169
|
+
["data", "grammar", "plural", i],
|
|
170
|
+
"error",
|
|
171
|
+
`\`data.grammar.plural[${i}]\` must be a map with ending and becomes strings`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
const verbs = grammar.verbs;
|
|
176
|
+
if (Array.isArray(verbs) && verbs.length === 0)
|
|
177
|
+
findings.push(
|
|
178
|
+
finding(
|
|
179
|
+
["data", "grammar", "verbs"],
|
|
180
|
+
"error",
|
|
181
|
+
"`data.grammar.verbs` must state `invariant: true` or the six `persons` strings",
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
if (isMap(verbs)) {
|
|
185
|
+
const invariant = Object.hasOwn(verbs, "invariant");
|
|
186
|
+
const persons = Object.hasOwn(verbs, "persons");
|
|
187
|
+
if (invariant === persons)
|
|
188
|
+
findings.push(
|
|
189
|
+
finding(
|
|
190
|
+
["data", "grammar", "verbs"],
|
|
191
|
+
"error",
|
|
192
|
+
"`data.grammar.verbs` must state exactly one of `invariant: true` or `persons`",
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
else if (invariant && (verbs.invariant == null || verbs.invariant === false))
|
|
196
|
+
findings.push(
|
|
197
|
+
finding(
|
|
198
|
+
["data", "grammar", "verbs", "invariant"],
|
|
199
|
+
"error",
|
|
200
|
+
"`data.grammar.verbs.invariant` must be true",
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
else if (
|
|
204
|
+
persons &&
|
|
205
|
+
(verbs.persons == null || (Array.isArray(verbs.persons) && verbs.persons.length === 0))
|
|
206
|
+
)
|
|
207
|
+
findings.push(
|
|
208
|
+
finding(
|
|
209
|
+
["data", "grammar", "verbs", "persons"],
|
|
210
|
+
"error",
|
|
211
|
+
"`data.grammar.verbs.persons` must be a map of all six ending strings",
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
163
216
|
/**
|
|
164
217
|
* The note an Address names, or a finding where it names none.
|
|
165
218
|
*
|
|
@@ -43,6 +43,7 @@ export const LANGUAGE_DATA_KEYS = Object.freeze([
|
|
|
43
43
|
"older",
|
|
44
44
|
"negation",
|
|
45
45
|
"naming",
|
|
46
|
+
"grammar",
|
|
46
47
|
]);
|
|
47
48
|
|
|
48
49
|
/**
|
|
@@ -1594,6 +1595,81 @@ function listOfMaps(name, fields, describe) {
|
|
|
1594
1595
|
});
|
|
1595
1596
|
}
|
|
1596
1597
|
|
|
1598
|
+
/**
|
|
1599
|
+
* The six present persons of the one optional regular verb pattern, in
|
|
1600
|
+
* presentation order. Empty endings leave the citation form unchanged.
|
|
1601
|
+
*
|
|
1602
|
+
* @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
|
|
1603
|
+
*/
|
|
1604
|
+
export const VERB_PERSON_FIELDS = Object.freeze(
|
|
1605
|
+
[
|
|
1606
|
+
["I", "I"],
|
|
1607
|
+
["you", "you"],
|
|
1608
|
+
["heShe", "he/she"],
|
|
1609
|
+
["we", "we"],
|
|
1610
|
+
["youAll", "you all"],
|
|
1611
|
+
["they", "they"],
|
|
1612
|
+
].map(([name, label]) =>
|
|
1613
|
+
Object.freeze({
|
|
1614
|
+
name,
|
|
1615
|
+
label,
|
|
1616
|
+
kind: "string",
|
|
1617
|
+
required: true,
|
|
1618
|
+
allowEmpty: true,
|
|
1619
|
+
shape: "an ending string, empty for unchanged",
|
|
1620
|
+
describe: `The ending added to the citation form for ${label}.`,
|
|
1621
|
+
}),
|
|
1622
|
+
),
|
|
1623
|
+
);
|
|
1624
|
+
|
|
1625
|
+
/** The two alternatives of a supplied optional verb pattern. */
|
|
1626
|
+
const VERB_PATTERN_FIELDS = Object.freeze([
|
|
1627
|
+
Object.freeze({
|
|
1628
|
+
name: "invariant",
|
|
1629
|
+
kind: "boolean",
|
|
1630
|
+
shape: "true, as the alternative to persons",
|
|
1631
|
+
describe: "True where verbs keep the citation form for every person; excludes `persons`.",
|
|
1632
|
+
}),
|
|
1633
|
+
Object.freeze({
|
|
1634
|
+
name: "persons",
|
|
1635
|
+
kind: "map",
|
|
1636
|
+
shape: "a map — `{ I, you, heShe, we, youAll, they }` of ending strings",
|
|
1637
|
+
fields: VERB_PERSON_FIELDS,
|
|
1638
|
+
describe: "All six present endings, each added to the citation form; excludes `invariant`.",
|
|
1639
|
+
}),
|
|
1640
|
+
]);
|
|
1641
|
+
|
|
1642
|
+
/** The optional, display-only noun and verb patterns of one language. */
|
|
1643
|
+
const GRAMMAR_FIELDS = Object.freeze([
|
|
1644
|
+
innerList(
|
|
1645
|
+
"plural",
|
|
1646
|
+
Object.freeze(
|
|
1647
|
+
["ending", "becomes"].map((name) =>
|
|
1648
|
+
Object.freeze({
|
|
1649
|
+
name,
|
|
1650
|
+
kind: "string",
|
|
1651
|
+
required: true,
|
|
1652
|
+
allowEmpty: true,
|
|
1653
|
+
shape: "a literal ending string, which may be empty",
|
|
1654
|
+
describe:
|
|
1655
|
+
name === "ending" ?
|
|
1656
|
+
"The bare suffix replaced by this rule; empty matches every noun."
|
|
1657
|
+
: "The bare replacement; empty removes the matching suffix.",
|
|
1658
|
+
}),
|
|
1659
|
+
),
|
|
1660
|
+
),
|
|
1661
|
+
"Default noun plurals, replacing the first matching literal ending in authored order. " +
|
|
1662
|
+
"A word's explicit plural wins; no word is checked against this rule.",
|
|
1663
|
+
),
|
|
1664
|
+
Object.freeze({
|
|
1665
|
+
name: "verbs",
|
|
1666
|
+
kind: "map",
|
|
1667
|
+
shape: "a map — `{ invariant: true }` or `{ persons: { I, you, heShe, we, youAll, they } }`",
|
|
1668
|
+
fields: VERB_PATTERN_FIELDS,
|
|
1669
|
+
describe: "One optional regular present pattern, printed once at the dictionary head.",
|
|
1670
|
+
}),
|
|
1671
|
+
]);
|
|
1672
|
+
|
|
1597
1673
|
/**
|
|
1598
1674
|
* The `data` fields this module declares, in the order the vocabulary lists
|
|
1599
1675
|
* them. {@link module:engine/language-data.checkLanguageData} checks what the
|
|
@@ -1602,6 +1678,16 @@ function listOfMaps(name, fields, describe) {
|
|
|
1602
1678
|
* @type {readonly object[]}
|
|
1603
1679
|
*/
|
|
1604
1680
|
export const LANGUAGE_DATA_FIELDS = Object.freeze([
|
|
1681
|
+
Object.freeze({
|
|
1682
|
+
name: "grammar",
|
|
1683
|
+
kind: "map",
|
|
1684
|
+
shape: "a map — `{ plural?, verbs? }`",
|
|
1685
|
+
fields: GRAMMAR_FIELDS,
|
|
1686
|
+
describe:
|
|
1687
|
+
"Optional noun plurals and one verb pattern for dictionary presentation. Only the " +
|
|
1688
|
+
"shape of supplied keys is checked; no word must follow or state a rule. A language " +
|
|
1689
|
+
"skill note only.",
|
|
1690
|
+
}),
|
|
1605
1691
|
listOfMaps(
|
|
1606
1692
|
"tongues",
|
|
1607
1693
|
TONGUE_FIELDS,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* an ancestor's;
|
|
37
37
|
* - every source language resolves, and names a language skill;
|
|
38
38
|
* - a pronunciation is a respelling;
|
|
39
|
-
* - a `built` piece no word or
|
|
39
|
+
* - a `built` piece no word, affix or element of the language glosses is a warning.
|
|
40
40
|
*
|
|
41
41
|
* {@link checkLanguageFamily} owns `data.family`, the parent language a
|
|
42
42
|
* daughter reads words from: it is stated only on a language, names a
|
|
@@ -285,7 +285,7 @@ export function checkLanguageWords(note, { index } = {}) {
|
|
|
285
285
|
finding(
|
|
286
286
|
[...at("built"), j],
|
|
287
287
|
"warning",
|
|
288
|
-
`the piece \`${piece}\` of \`${word.form}\` matches no word or
|
|
288
|
+
`the piece \`${piece}\` of \`${word.form}\` matches no word, affix or element of this ` +
|
|
289
289
|
"language or its ancestors, so it prints with no gloss",
|
|
290
290
|
),
|
|
291
291
|
);
|