@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
|
@@ -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,10 +74,11 @@ 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";
|
|
80
|
-
import { positionInBody } from "./diagnostics.mjs";
|
|
81
|
+
import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
|
|
81
82
|
// The pure `sohl:` frontmatter readers live in a leaf module so the item-type
|
|
82
83
|
// registry can import them without reaching back through this one.
|
|
83
84
|
// Re-exported here so every existing importer keeps its single import path.
|
|
@@ -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)
|
|
@@ -231,7 +233,7 @@ export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, res
|
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
/**
|
|
234
|
-
* Parses a markdown file with YAML frontmatter.
|
|
236
|
+
* Parses a markdown file with LF or CRLF YAML frontmatter delimiters.
|
|
235
237
|
*
|
|
236
238
|
* Returns `{ frontmatter, body, description, bodyLine, bodyColumn, raw }` where
|
|
237
239
|
* `body` is the trimmed raw markdown after the frontmatter block, and
|
|
@@ -243,20 +245,27 @@ export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, res
|
|
|
243
245
|
* `{ frontmatter: null, body: "", description: "", raw }` and no
|
|
244
246
|
* position: there is no body to have one.
|
|
245
247
|
* @param {string} filePath - Markdown file to read.
|
|
246
|
-
* @param {
|
|
248
|
+
* @param {object} [opts]
|
|
249
|
+
* @param {object} [opts.addressContext] - Address defaults for decoded fields.
|
|
250
|
+
* @param {string} [opts.raw] - Exact captured source; omitted, reads the file.
|
|
251
|
+
* @param {boolean} [opts.renderDescription=true] - Render the body's HTML description.
|
|
252
|
+
* False leaves `description` empty for readers that only use authored text.
|
|
247
253
|
* @returns {{frontmatter: object|null, body: string, description: string, raw: string,
|
|
248
254
|
* bodyLine?: number, bodyColumn?: number, parseError?: {message: string,
|
|
249
255
|
* line?: number, column?: number}}} Parsed note and body position.
|
|
250
256
|
*/
|
|
251
|
-
export function parseMarkdownFile(
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
export function parseMarkdownFile(
|
|
258
|
+
filePath,
|
|
259
|
+
{ addressContext, raw: capturedRaw, renderDescription = true } = {},
|
|
260
|
+
) {
|
|
261
|
+
const content = capturedRaw ?? fs.readFileSync(filePath, "utf8");
|
|
262
|
+
const fmMatch = splitFrontmatter(content);
|
|
254
263
|
if (!fmMatch) {
|
|
255
264
|
return { frontmatter: null, body: "", description: "", raw: content };
|
|
256
265
|
}
|
|
257
266
|
let frontmatter;
|
|
258
267
|
try {
|
|
259
|
-
frontmatter = yaml.parse(fmMatch
|
|
268
|
+
frontmatter = yaml.parse(fmMatch.yaml) || {};
|
|
260
269
|
} catch (err) {
|
|
261
270
|
const firstLine = String(err.message).split("\n", 1)[0];
|
|
262
271
|
const message = firstLine.replace(/ at line \d+, column \d+.*$/, "");
|
|
@@ -277,7 +286,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
277
286
|
try {
|
|
278
287
|
decodeNoteAddresses(frontmatter, addressContext);
|
|
279
288
|
} catch (error) {
|
|
280
|
-
const position = positionOfYamlPath(fmMatch
|
|
289
|
+
const position = positionOfYamlPath(fmMatch.yaml, error.keyPath ?? [], {
|
|
281
290
|
key: error.addressKey,
|
|
282
291
|
});
|
|
283
292
|
if (position.line) position.line++;
|
|
@@ -286,9 +295,9 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
286
295
|
throw error;
|
|
287
296
|
}
|
|
288
297
|
}
|
|
289
|
-
const raw = fmMatch
|
|
298
|
+
const raw = fmMatch.body;
|
|
290
299
|
const body = raw.trim();
|
|
291
|
-
const description = body ? renderFoundryMarkdown(body) : "";
|
|
300
|
+
const description = body && renderDescription ? renderFoundryMarkdown(body) : "";
|
|
292
301
|
// Where the trimmed body starts in the *file*, so an offset within it can
|
|
293
302
|
// be reported as a file position. The frontmatter's lines and the
|
|
294
303
|
// blank lines `trim()` removes both sit in between, and the trim can take
|
|
@@ -374,13 +383,19 @@ export function assertSuppliedCorpus(records, who) {
|
|
|
374
383
|
* Required: the scope is the caller's to state, so two passes cannot
|
|
375
384
|
* disagree about which files are the corpus.
|
|
376
385
|
* @param {object} [opts.addressContext] - Address resolution context.
|
|
386
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Selected sources in walk order.
|
|
387
|
+
* Supplied, the walk parses these strings instead of reading the filesystem.
|
|
388
|
+
* @param {boolean} [opts.renderDescription=true] - Render each body's description.
|
|
377
389
|
* @yields {{frontmatter: object|null, body: string, description: string,
|
|
378
390
|
* file: string, absPath: string, bodyLine?: number, bodyColumn?: number}}
|
|
379
391
|
* One entry per `.md` file found.
|
|
380
392
|
* @throws {Error} When `skipDirectories` is not stated — see
|
|
381
393
|
* {@link assertStatedScope}.
|
|
382
394
|
*/
|
|
383
|
-
export function* walkMarkdownTree(
|
|
395
|
+
export function* walkMarkdownTree(
|
|
396
|
+
rootDir,
|
|
397
|
+
{ skipDirectories, addressContext, sources, renderDescription = true } = {},
|
|
398
|
+
) {
|
|
384
399
|
// Stated by the caller, never resolved here. A default here
|
|
385
400
|
// — `loadPackConfig().skipDirectories` — read whichever configuration
|
|
386
401
|
// resolved from the working directory rather than the one the caller was
|
|
@@ -393,6 +408,20 @@ export function* walkMarkdownTree(rootDir, { skipDirectories, addressContext } =
|
|
|
393
408
|
// the ambient config rather than the passed one, which a fixture can pass
|
|
394
409
|
// on indefinitely.
|
|
395
410
|
assertStatedScope(skipDirectories, "walkMarkdownTree");
|
|
411
|
+
if (sources) {
|
|
412
|
+
for (const [absPath, source] of sources) {
|
|
413
|
+
yield {
|
|
414
|
+
...parseMarkdownFile(absPath, {
|
|
415
|
+
addressContext,
|
|
416
|
+
raw: source.raw,
|
|
417
|
+
renderDescription,
|
|
418
|
+
}),
|
|
419
|
+
file: path.basename(absPath),
|
|
420
|
+
absPath,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
396
425
|
if (!fs.existsSync(rootDir)) return;
|
|
397
426
|
const stack = [rootDir];
|
|
398
427
|
while (stack.length > 0) {
|
|
@@ -411,7 +440,7 @@ export function* walkMarkdownTree(rootDir, { skipDirectories, addressContext } =
|
|
|
411
440
|
stack.push(absPath);
|
|
412
441
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
413
442
|
yield {
|
|
414
|
-
...parseMarkdownFile(absPath, { addressContext }),
|
|
443
|
+
...parseMarkdownFile(absPath, { addressContext, renderDescription }),
|
|
415
444
|
file: entry.name,
|
|
416
445
|
absPath,
|
|
417
446
|
};
|
|
@@ -781,12 +810,16 @@ import { collectAnchors } from "./anchors.mjs";
|
|
|
781
810
|
* omitted.
|
|
782
811
|
* @param {readonly object[]} [opts.records] - The corpus, derived once per
|
|
783
812
|
* compile and handed in. Required: see {@link assertSuppliedCorpus}.
|
|
813
|
+
* @param {Map<string, object[]>} [opts.sqlTables] - Prepared directives,
|
|
814
|
+
* including the anchors and Foundry page maps of their expanded bodies.
|
|
815
|
+
* @param {ReturnType<import("./metadata-index.mjs").loadForeignIndexes>} [opts.foreign] -
|
|
816
|
+
* Captured dependency address and reference views for this context.
|
|
784
817
|
* @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
|
|
785
818
|
*/
|
|
786
819
|
export function buildContentLinkIndex(
|
|
787
820
|
contentBase,
|
|
788
821
|
router = packRouter(),
|
|
789
|
-
{ config, records } = {},
|
|
822
|
+
{ config, records, sqlTables, foreign: capturedForeign } = {},
|
|
790
823
|
) {
|
|
791
824
|
const docs = [];
|
|
792
825
|
/** The files this package ships, by canonical address. */
|
|
@@ -812,6 +845,7 @@ export function buildContentLinkIndex(
|
|
|
812
845
|
// authored fields, and neither may be handed the keys the index derived.
|
|
813
846
|
const fm = authoredFrontmatter(record);
|
|
814
847
|
const absPath = noteFile(contentBase, record);
|
|
848
|
+
const prepared = sqlTables?.get(absPath);
|
|
815
849
|
// The id a note's document is filed under: its authored pin, or the
|
|
816
850
|
// one derived from its canonical address. Derived by the index
|
|
817
851
|
// against the configuration this build resolved — never here through
|
|
@@ -861,11 +895,12 @@ export function buildContentLinkIndex(
|
|
|
861
895
|
// discarded here — the walk yields the body and nothing read it.
|
|
862
896
|
// Read from the record rather than from a second reading of the
|
|
863
897
|
// note's headings — the one-anchor-reader rule.
|
|
864
|
-
anchors: new Set(
|
|
865
|
-
(record.anchors ?? [])
|
|
898
|
+
anchors: new Set([
|
|
899
|
+
...(record.anchors ?? [])
|
|
866
900
|
.filter((anchor) => anchor.kind === "prose")
|
|
867
901
|
.map((anchor) => anchor.slug),
|
|
868
|
-
|
|
902
|
+
...(prepared?.anchors ?? []).map((anchor) => anchor.slug),
|
|
903
|
+
]),
|
|
869
904
|
// The note's event ids, which a wikilink may not name: a link to
|
|
870
905
|
// one is refused with its own message rather than as unknown.
|
|
871
906
|
eventAnchors: new Set(
|
|
@@ -873,8 +908,8 @@ export function buildContentLinkIndex(
|
|
|
873
908
|
.filter((anchor) => anchor.kind === "event")
|
|
874
909
|
.map((anchor) => anchor.slug),
|
|
875
910
|
),
|
|
876
|
-
anchorUuids: record.foundry?.note?.anchors,
|
|
877
|
-
docAnchorUuids: documentation?.foundry?.note?.anchors,
|
|
911
|
+
anchorUuids: prepared?.anchorUuids ?? record.foundry?.note?.anchors,
|
|
912
|
+
docAnchorUuids: prepared?.docAnchorUuids ?? documentation?.foundry?.note?.anchors,
|
|
878
913
|
});
|
|
879
914
|
}
|
|
880
915
|
// Packages this build links *into* but does not publish. Each publishes
|
|
@@ -886,7 +921,7 @@ export function buildContentLinkIndex(
|
|
|
886
921
|
index: foreign,
|
|
887
922
|
references: foreignReferences,
|
|
888
923
|
stale,
|
|
889
|
-
} = loadForeignIndexes(resolved, [resolved.contentPackage], PACKAGE_BASE);
|
|
924
|
+
} = capturedForeign ?? loadForeignIndexes(resolved, [resolved.contentPackage], PACKAGE_BASE);
|
|
890
925
|
if (stale.length) {
|
|
891
926
|
for (const st of stale) {
|
|
892
927
|
log.error(`Unusable content index for "${st.package}": ${st.reason}`);
|
package/engine/index-records.mjs
CHANGED
package/engine/index.mjs
CHANGED
|
@@ -187,6 +187,9 @@ export * as foundryEntries from "./foundry-entries.mjs";
|
|
|
187
187
|
/** Publishing the note tree as a queryable JSON Lines index. */
|
|
188
188
|
export * as contentIndex from "./content-index.mjs";
|
|
189
189
|
|
|
190
|
+
/** One invocation's captured content and context-specific prepared views. */
|
|
191
|
+
export * as contentSession from "./content-session.mjs";
|
|
192
|
+
|
|
190
193
|
/** The asset types — `icon`, `image`, `audio` — and the roots they are walked from. */
|
|
191
194
|
export * as assetTypes from "./asset-types.mjs";
|
|
192
195
|
|
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";
|
|
@@ -102,9 +103,9 @@ const WITHHELD_OWNERSHIP = 0;
|
|
|
102
103
|
* @param {string} markdown - A note's body, as {@link splitPages} receives it.
|
|
103
104
|
* @returns {Set<number>} 0-based line indices inside a named block.
|
|
104
105
|
*/
|
|
105
|
-
function namedBlockLines(markdown) {
|
|
106
|
+
function namedBlockLines(markdown, syntax) {
|
|
106
107
|
const lines = new Set();
|
|
107
|
-
for (const { start, end } of scanBlocks(markdown).blocks) {
|
|
108
|
+
for (const { start, end } of (syntax ? syntax(markdown).blocks : scanBlocks(markdown)).blocks) {
|
|
108
109
|
for (let i = start; i <= end; i++) lines.add(i);
|
|
109
110
|
}
|
|
110
111
|
return lines;
|
|
@@ -141,17 +142,18 @@ function namedBlockLines(markdown) {
|
|
|
141
142
|
* picture's address to the role its asset declares, so a figure page's
|
|
142
143
|
* name reads `Map 1` rather than `Figure 1` where it is due — see
|
|
143
144
|
* {@link module:engine/content-figures.scanFigures}.
|
|
145
|
+
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session scanner projections.
|
|
144
146
|
* @returns {Array<{name: string, anchorSlug: string|null, level: number,
|
|
145
147
|
* classes: string[], figure: object|null, continues?: string,
|
|
146
148
|
* markdown: string}>} Pages in document order.
|
|
147
149
|
*/
|
|
148
|
-
export function splitPages(body, leadName = "Introduction", resolveRole) {
|
|
150
|
+
export function splitPages(body, leadName = "Introduction", resolveRole, syntax) {
|
|
149
151
|
const { markdown, definitions } = separateFootnotes(body);
|
|
150
152
|
const lines = markdown.split("\n");
|
|
151
153
|
const figureStarts = new Map(
|
|
152
154
|
scanFigures(markdown, { resolveRole }).figures.map((figure) => [figure.line - 1, figure]),
|
|
153
155
|
);
|
|
154
|
-
const blockLines = namedBlockLines(markdown);
|
|
156
|
+
const blockLines = namedBlockLines(markdown, syntax);
|
|
155
157
|
const openings = pageOpenings(markdown, blockLines);
|
|
156
158
|
const pages = [];
|
|
157
159
|
const beforeFirstH1 = [];
|
|
@@ -635,6 +637,7 @@ export function buildPages(rawPages, entryId, noteName, figures, resolveRole) {
|
|
|
635
637
|
* a picture's address to the role its asset declares, so a figure page's
|
|
636
638
|
* name reads `Map 1` rather than `Figure 1` where it is due — see
|
|
637
639
|
* {@link module:engine/content-figures.scanFigures}.
|
|
640
|
+
* @param {import("./content-session.mjs").ContentSyntax} [params.syntax] - Session scanner projections.
|
|
638
641
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
639
642
|
*/
|
|
640
643
|
export function buildJournalEntry({
|
|
@@ -647,8 +650,9 @@ export function buildJournalEntry({
|
|
|
647
650
|
infoboxes = [],
|
|
648
651
|
notice = "",
|
|
649
652
|
resolveRole,
|
|
653
|
+
syntax,
|
|
650
654
|
}) {
|
|
651
|
-
const rawPages = splitPages(markdown, leadName, resolveRole);
|
|
655
|
+
const rawPages = splitPages(markdown, leadName, resolveRole, syntax);
|
|
652
656
|
const pages = buildPages(
|
|
653
657
|
rawPages,
|
|
654
658
|
id,
|
|
@@ -760,13 +764,15 @@ export class Journals extends BasePackCompiler {
|
|
|
760
764
|
* An entry holding only its infobox is empty by this test. The panel is a
|
|
761
765
|
* rendering of `data:` that the content index already carries, so a
|
|
762
766
|
* compendium entry of nothing else tells a reader what the index told them.
|
|
767
|
+
* A frontmatter dictionary or glossary supplies pages of its own, so it
|
|
768
|
+
* qualifies before the shared body expansion inserts it.
|
|
763
769
|
*
|
|
764
770
|
* @param {object} fm - The note's frontmatter.
|
|
765
771
|
* @param {string} body - The note body, frontmatter stripped.
|
|
766
772
|
* @returns {boolean} True to skip the note.
|
|
767
773
|
*/
|
|
768
774
|
skipNote(fm, body) {
|
|
769
|
-
return !journalHasContent(body);
|
|
775
|
+
return !journalHasContent(body) && !hasLexicalEntries(fm);
|
|
770
776
|
}
|
|
771
777
|
|
|
772
778
|
/**
|
|
@@ -824,6 +830,7 @@ export class Journals extends BasePackCompiler {
|
|
|
824
830
|
const roles = pathnameRoles(this.linkIndex);
|
|
825
831
|
|
|
826
832
|
return buildJournalEntry({
|
|
833
|
+
syntax: this.corpus?.syntax,
|
|
827
834
|
id,
|
|
828
835
|
name,
|
|
829
836
|
markdown: [
|
package/engine/language-data.mjs
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* syllables, its consonants per vowel or both, each range's `min` no
|
|
45
45
|
* higher than its `max`; an edge stands once for each class of name; a kind
|
|
46
46
|
* of name has one set of draws; and a held-from row takes `elements` only
|
|
47
|
-
* where it takes an `element`, and is clipped or given a
|
|
47
|
+
* where it takes an `element`, and is clipped or given a joining ending only
|
|
48
48
|
* where it takes a name.
|
|
49
49
|
*
|
|
50
50
|
* @module
|
|
@@ -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
|
*
|
|
@@ -574,7 +627,7 @@ export function checkLanguageData(note, { index, types } = {}) {
|
|
|
574
627
|
),
|
|
575
628
|
);
|
|
576
629
|
if (!HELD_NAMES.includes(row.takes))
|
|
577
|
-
for (const key of ["clip", "
|
|
630
|
+
for (const key of ["clip", "joining"])
|
|
578
631
|
if (row[key] != null)
|
|
579
632
|
findings.push(
|
|
580
633
|
finding(
|
package/engine/language-rows.mjs
CHANGED
|
@@ -122,7 +122,7 @@ export const HELD_FROM_COLUMNS = Object.freeze([
|
|
|
122
122
|
"source",
|
|
123
123
|
"takes",
|
|
124
124
|
"clip",
|
|
125
|
-
"
|
|
125
|
+
"joining",
|
|
126
126
|
"elements",
|
|
127
127
|
]);
|
|
128
128
|
/** Every column of the `near_names` relation, in order. */
|
|
@@ -232,7 +232,7 @@ export const LANGUAGE_COLUMN_TYPES = Object.freeze({
|
|
|
232
232
|
},
|
|
233
233
|
name_edges: { position: "BIGINT", sounds: LIST, vowel: "BOOLEAN" },
|
|
234
234
|
name_draws: { position: "BIGINT", elements: LIST },
|
|
235
|
-
held_from: { position: "BIGINT",
|
|
235
|
+
held_from: { position: "BIGINT", joining: LIST, elements: LIST },
|
|
236
236
|
near_names: { position: "BIGINT", min_syllables: "BIGINT" },
|
|
237
237
|
stress: {},
|
|
238
238
|
finals: { position: "BIGINT" },
|
|
@@ -653,8 +653,8 @@ export function heldFromRows(records) {
|
|
|
653
653
|
return listRows(
|
|
654
654
|
records,
|
|
655
655
|
(data) => namingOf(data).heldFrom,
|
|
656
|
-
["source", "takes", "clip", "
|
|
657
|
-
{ lists: ["
|
|
656
|
+
["source", "takes", "clip", "joining", "elements"],
|
|
657
|
+
{ lists: ["joining", "elements"] },
|
|
658
658
|
);
|
|
659
659
|
}
|
|
660
660
|
|
|
@@ -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
|
/**
|
|
@@ -1141,10 +1142,10 @@ export const ELEMENT_FIELDS = Object.freeze([
|
|
|
1141
1142
|
"ancestor.",
|
|
1142
1143
|
}),
|
|
1143
1144
|
Object.freeze({
|
|
1144
|
-
name: "
|
|
1145
|
+
name: "joining",
|
|
1145
1146
|
kind: "string",
|
|
1146
1147
|
shape: "the element as written, such as `-borgar`",
|
|
1147
|
-
describe: "The element
|
|
1148
|
+
describe: "The element's joining form, as written when it qualifies another word.",
|
|
1148
1149
|
}),
|
|
1149
1150
|
]);
|
|
1150
1151
|
|
|
@@ -1471,11 +1472,11 @@ export const HELD_FROM_FIELDS = Object.freeze([
|
|
|
1471
1472
|
describe: "How the name is clipped: `first`, to its first element; whole where absent.",
|
|
1472
1473
|
}),
|
|
1473
1474
|
Object.freeze({
|
|
1474
|
-
name: "
|
|
1475
|
+
name: "joining",
|
|
1475
1476
|
kind: "list",
|
|
1476
1477
|
shape: "a list of endings",
|
|
1477
1478
|
entries: STRINGS,
|
|
1478
|
-
describe: "The
|
|
1479
|
+
describe: "The joining endings, one of which stands at the seam after the name.",
|
|
1479
1480
|
}),
|
|
1480
1481
|
Object.freeze({
|
|
1481
1482
|
name: "elements",
|
|
@@ -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
|
);
|