@heroiclands/package-build 25.5.4 → 25.6.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 +40 -0
- package/README.md +7 -0
- package/assets/styles/content-base.css +1 -1
- package/bin/content-commands.mjs +32 -8
- package/docs/api.md +81 -32
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +59 -10
- package/docs/commands.md +60 -17
- package/docs/configuration.md +3 -2
- package/docs/content-format.md +50 -1
- package/docs/diagnostics.md +17 -0
- package/docs/guides/module-project.md +11 -3
- package/docs/guides/reference-pages.md +4 -1
- package/docs/guides/site.md +13 -3
- package/docs/guides/writing-in-invented-scripts.md +5 -2
- package/docs/reference/format-details.md +233 -138
- package/docs/reference/note-types.md +165 -152
- package/docs/reference/theme-pages.md +1 -0
- package/e2e.mjs +1 -1
- package/engine/base-compiler.mjs +2 -0
- package/engine/compile-corpus.mjs +6 -2
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format.yaml +9 -7
- package/engine/content-html.mjs +3 -2
- package/engine/content-index.mjs +23 -0
- package/engine/content-links.mjs +8 -0
- package/engine/content-render.mjs +14 -2
- package/engine/content-scripts.mjs +44 -6
- package/engine/content-session.mjs +11 -7
- package/engine/content-spans.mjs +3 -4
- package/engine/content-tables.mjs +12 -5
- package/engine/content-verse.mjs +52 -10
- package/engine/core-report.mjs +27 -18
- package/engine/culture-reach.mjs +3 -0
- package/engine/declared-sections.mjs +100 -0
- package/engine/derived-sections.mjs +36 -2
- package/engine/dictionary-rows.mjs +2 -1
- package/engine/generated-sections.mjs +2 -0
- package/engine/holdings.mjs +107 -3
- package/engine/index-records.mjs +3 -1
- package/engine/index.mjs +1 -1
- package/engine/language-data.mjs +0 -1
- package/engine/language-names.mjs +53 -23
- package/engine/language-rows.mjs +1 -3
- package/engine/language-terms.mjs +7 -10
- package/engine/map-dot.mjs +22 -0
- package/engine/markdown-blocks.mjs +23 -8
- package/engine/markdownlint-run.mjs +1 -1
- package/engine/markup-validation.mjs +2 -0
- package/engine/metadata-index.mjs +1 -0
- package/engine/note-addresses.mjs +16 -0
- package/engine/note-vocabulary.mjs +29 -13
- package/engine/npc-generate.mjs +52 -7
- package/engine/office-holders.mjs +76 -0
- package/engine/page-breaks.mjs +143 -0
- package/engine/pdf-build.mjs +12 -4
- package/engine/pdf-render.mjs +12 -2
- package/engine/place-kinds.mjs +106 -0
- package/engine/preview-index.mjs +4 -2
- package/engine/settlement-tables.mjs +410 -675
- package/engine/settlement-terms.mjs +118 -0
- package/engine/site-build.mjs +14 -7
- package/engine/site-preview.mjs +3 -0
- package/engine/site-render.mjs +18 -4
- package/engine/site-root.mjs +10 -1
- package/engine/site-serve-http.mjs +151 -0
- package/engine/site-serve.mjs +196 -126
- package/engine/sql-tables.mjs +101 -7
- package/engine/standing-terms.mjs +9 -5
- package/engine/standings.mjs +83 -31
- package/engine/unique-items.mjs +85 -0
- package/engine/verse-metre.mjs +50 -5
- package/engine/verse-regions.mjs +16 -0
- package/package.json +9 -4
- package/site-theme/data/banner-kinds.yaml +2 -0
- package/sohl/actors.mjs +4 -3
- package/types/e2e.d.mts +1 -1
- package/types/engine/content-html.d.mts +2 -1
- package/types/engine/content-scripts.d.mts +9 -1
- package/types/engine/content-verse.d.mts +11 -2
- package/types/engine/core-report.d.mts +6 -2
- package/types/engine/culture-reach.d.mts +3 -0
- package/types/engine/declared-sections.d.mts +30 -0
- package/types/engine/derived-sections.d.mts +5 -1
- package/types/engine/holdings.d.mts +34 -2
- package/types/engine/index-records.d.mts +2 -1
- package/types/engine/language-names.d.mts +18 -6
- package/types/engine/language-terms.d.mts +5 -1
- package/types/engine/markdown-blocks.d.mts +5 -1
- package/types/engine/npc-generate.d.mts +3 -0
- package/types/engine/office-holders.d.mts +33 -0
- package/types/engine/page-breaks.d.mts +38 -0
- package/types/engine/pdf-render.d.mts +2 -0
- package/types/engine/place-kinds.d.mts +18 -0
- package/types/engine/settlement-tables.d.mts +25 -158
- package/types/engine/settlement-terms.d.mts +53 -0
- package/types/engine/site-build.d.mts +3 -1
- package/types/engine/site-render.d.mts +6 -2
- package/types/engine/site-root.d.mts +3 -1
- package/types/engine/site-serve-http.d.mts +1 -0
- package/types/engine/site-serve.d.mts +12 -6
- package/types/engine/sql-tables.d.mts +35 -3
- package/types/engine/standing-terms.d.mts +3 -2
- package/types/engine/standings.d.mts +14 -2
- package/types/engine/unique-items.d.mts +12 -0
- package/types/engine/verse-metre.d.mts +2 -1
- package/types/engine/verse-regions.d.mts +8 -0
- package/vendor/markdownlint.mjs +855 -569
package/engine/core-report.mjs
CHANGED
|
@@ -79,7 +79,7 @@ function idsOf(word) {
|
|
|
79
79
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
80
80
|
* @param {{language?: string}} [options] - A language to report alone, by its
|
|
81
81
|
* name, shortcode or reference slug whether or not it states `data.core`.
|
|
82
|
-
* @returns {{concepts: number, languages: Array<{name: string, language: string, core: boolean, total: number, covered: number, missing: Array<{group: string, count: number, total: number, concepts: Array<{id: string, gloss: string}>}>}>}|{problem: string, about: string}}
|
|
82
|
+
* @returns {{concepts: number, notCovered: string[], languages: Array<{name: string, language: string, core: boolean, total: number, covered: number, missing: Array<{group: string, count: number, total: number, concepts: Array<{id: string, gloss: string}>}>}>}|{problem: string, about: string}}
|
|
83
83
|
* The report, or the problem when there is no concept list or the language
|
|
84
84
|
* asked for is none.
|
|
85
85
|
*/
|
|
@@ -113,6 +113,13 @@ export function coreReport(records, { language } = {}) {
|
|
|
113
113
|
const groups = [...new Set(concepts.map((concept) => concept.group))];
|
|
114
114
|
return {
|
|
115
115
|
concepts: concepts.length,
|
|
116
|
+
notCovered:
|
|
117
|
+
language === undefined ?
|
|
118
|
+
languages
|
|
119
|
+
.filter((record) => record.data?.core !== true)
|
|
120
|
+
.map(nameOf)
|
|
121
|
+
.sort((a, b) => a.localeCompare(b))
|
|
122
|
+
: [],
|
|
116
123
|
languages: chosen.map((record) => {
|
|
117
124
|
const have = new Set();
|
|
118
125
|
const forms = new Set();
|
|
@@ -154,24 +161,26 @@ export function coreReport(records, { language } = {}) {
|
|
|
154
161
|
* A core report as text: one block per language, its missing concepts under
|
|
155
162
|
* their groups with counts, each concept's id and gloss tab-separated.
|
|
156
163
|
*
|
|
157
|
-
*
|
|
164
|
+
* The default report ends with the names of languages it does not measure.
|
|
165
|
+
*
|
|
166
|
+
* @param {{concepts: number, notCovered: string[], languages: Array<object>}} report - A report from {@link coreReport}.
|
|
158
167
|
* @returns {string} The text, ending in a newline.
|
|
159
168
|
*/
|
|
160
169
|
export function coreReportText(report) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
for (const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
170
|
+
const blocks = report.languages.map((one) => {
|
|
171
|
+
if (!one.missing.length)
|
|
172
|
+
return `${one.name} (${one.language}): all ${one.total} core concepts have a word\n`;
|
|
173
|
+
const lines = [
|
|
174
|
+
`${one.name} (${one.language}): ${one.total - one.covered} of ${one.total} core concepts have no word`,
|
|
175
|
+
];
|
|
176
|
+
for (const group of one.missing) {
|
|
177
|
+
lines.push(` ${group.group}: ${group.count} of ${group.total}`);
|
|
178
|
+
for (const concept of group.concepts) lines.push(` ${concept.id}\t${concept.gloss}`);
|
|
179
|
+
}
|
|
180
|
+
return `${lines.join("\n")}\n`;
|
|
181
|
+
});
|
|
182
|
+
if (!blocks.length) blocks.push("No language states `data.core: true`.\n");
|
|
183
|
+
if (report.notCovered.length)
|
|
184
|
+
blocks.push(`Not covered (no data.core): ${report.notCovered.join(", ")}\n`);
|
|
185
|
+
return blocks.join("\n");
|
|
177
186
|
}
|
package/engine/culture-reach.mjs
CHANGED
|
@@ -345,6 +345,9 @@ export function noteTerms(index, note, entry) {
|
|
|
345
345
|
* in a tree that describes peoples. Its ranks and offices then have no
|
|
346
346
|
* culture associations for term queries.
|
|
347
347
|
*
|
|
348
|
+
* The note's `data.culture` is the culture the content index resolved for it:
|
|
349
|
+
* its own, else the one its `data.parents` chain agrees on.
|
|
350
|
+
*
|
|
348
351
|
* @param {object} note - An `affiliation` note.
|
|
349
352
|
* @param {{index?: object}} [context]
|
|
350
353
|
* @returns {object[]} One warning, at the note's `type`, or none.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
/** Place declared content in its authored section, or create that section. */
|
|
14
|
+
import { scanHeadingAttributes } from "./heading-attributes.mjs";
|
|
15
|
+
import { scanBlocks } from "./content-blocks.mjs";
|
|
16
|
+
|
|
17
|
+
/** Authored headings, excluding literal examples and named containers. */
|
|
18
|
+
export function declaredHeadings(markdown, syntax) {
|
|
19
|
+
const blocks = (syntax ? syntax(markdown).blocks : scanBlocks(markdown)).blocks;
|
|
20
|
+
return scanHeadingAttributes(markdown).headings.filter(
|
|
21
|
+
(heading) =>
|
|
22
|
+
!blocks.some(
|
|
23
|
+
(block) => heading.line - 1 >= block.start && heading.line - 1 <= block.end,
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Insert indexed content at its heading while preserving subsequent source lines.
|
|
30
|
+
* @param {string} markdown - Expanded body.
|
|
31
|
+
* @param {Array<{slug:string,heading:string,markdown:string}>} [sections] - Declared content.
|
|
32
|
+
* @param {{authored?:string,lineMap?:object[],generatedFrom?:number,syntax?:Function}} [context] - Captured source mappings.
|
|
33
|
+
* @returns {{markdown:string,lineMap:object[],generatedFrom?:number}} Expanded body and locations.
|
|
34
|
+
*/
|
|
35
|
+
export function insertDeclaredSections(
|
|
36
|
+
markdown,
|
|
37
|
+
sections = [],
|
|
38
|
+
{ authored = markdown, lineMap, generatedFrom, syntax } = {},
|
|
39
|
+
) {
|
|
40
|
+
const lines = String(markdown ?? "").split("\n");
|
|
41
|
+
const locations = lineMap ? [...lineMap] : lines.map((_, line) => ({ line, generated: false }));
|
|
42
|
+
if (!sections.length)
|
|
43
|
+
return {
|
|
44
|
+
markdown: lines.join("\n"),
|
|
45
|
+
lineMap: locations,
|
|
46
|
+
...(generatedFrom === undefined ? {} : { generatedFrom }),
|
|
47
|
+
};
|
|
48
|
+
const headings = declaredHeadings(authored, syntax)
|
|
49
|
+
.map((heading) => ({
|
|
50
|
+
...heading,
|
|
51
|
+
sourceLine: heading.line - 1,
|
|
52
|
+
line: locations.findIndex(
|
|
53
|
+
(location, at) =>
|
|
54
|
+
location.line === heading.line - 1 &&
|
|
55
|
+
!location.generated &&
|
|
56
|
+
(generatedFrom === undefined || at < generatedFrom),
|
|
57
|
+
),
|
|
58
|
+
}))
|
|
59
|
+
.filter((h) => h.line >= 0);
|
|
60
|
+
const edits = [];
|
|
61
|
+
for (const section of sections) {
|
|
62
|
+
const own = headings.find((h) => h.id === section.slug || h.text === section.heading);
|
|
63
|
+
const next = own && headings.find((h) => h.line > own.line && h.level <= own.level);
|
|
64
|
+
const at = next?.line ?? generatedFrom ?? lines.length;
|
|
65
|
+
if (own && !own.id) {
|
|
66
|
+
const line = lines[own.line].replace(/\s*#*\s*$/, "");
|
|
67
|
+
lines[own.line] =
|
|
68
|
+
/\{[^{}]*\}$/.test(line) ?
|
|
69
|
+
line.replace(/\}$/, ` #${section.slug}}`)
|
|
70
|
+
: line + ` {#${section.slug}}`;
|
|
71
|
+
}
|
|
72
|
+
edits.push({
|
|
73
|
+
at,
|
|
74
|
+
source: own?.sourceLine ?? Math.max(0, String(authored).split("\n").length - 1),
|
|
75
|
+
lines: [
|
|
76
|
+
"",
|
|
77
|
+
...(own ?
|
|
78
|
+
section.markdown
|
|
79
|
+
: `# ${section.heading} {#${section.slug}}\n\n${section.markdown}`
|
|
80
|
+
).split("\n"),
|
|
81
|
+
"",
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (const edit of edits.sort((a, b) => b.at - a.at)) {
|
|
86
|
+
lines.splice(edit.at, 0, ...edit.lines);
|
|
87
|
+
locations.splice(
|
|
88
|
+
edit.at,
|
|
89
|
+
0,
|
|
90
|
+
...edit.lines.map(() => ({ line: edit.source, generated: true })),
|
|
91
|
+
);
|
|
92
|
+
if (generatedFrom !== undefined && edit.at <= generatedFrom)
|
|
93
|
+
generatedFrom += edit.lines.length;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
markdown: lines.join("\n"),
|
|
97
|
+
lineMap: locations,
|
|
98
|
+
...(generatedFrom === undefined ? {} : { generatedFrom }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
* | Within | `within` | a place | the places whose `parents` names it, grouped by kind |
|
|
22
22
|
* | Governed by | `governedby` | a place | the affiliation its `data.government` names |
|
|
23
23
|
* | Governed places | `governedplaces` | an affiliation | the places naming it as government, grouped by kind |
|
|
24
|
+
* | Office holders | `officeholders` | an affiliation | each declared office's living holders, from the beings' own `office` |
|
|
24
25
|
* | In song and story | `insongandstory` | any note | the works naming it, or one of its events, in `subjects` |
|
|
25
26
|
* | From here | `fromhere` | a place on a border or a route | the map from it, as an image |
|
|
26
27
|
*
|
|
27
|
-
* The lists are the derivations {@link module:engine/holdings}
|
|
28
|
+
* The lists are the derivations {@link module:engine/holdings},
|
|
29
|
+
* {@link module:engine/office-holders} and
|
|
28
30
|
* {@link module:engine/literature-works} make, over this package's records and
|
|
29
31
|
* every fetched index. An entry naming a note that publishes a page is a
|
|
30
32
|
* wikilink with an empty label, so each surface resolves it its own way and
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
*/
|
|
40
42
|
|
|
41
43
|
import fs from "node:fs";
|
|
44
|
+
import { assertSettlementRecords, settlementMarkdown } from "./settlement-tables.mjs";
|
|
42
45
|
import path from "node:path";
|
|
43
46
|
|
|
44
47
|
import {
|
|
@@ -52,6 +55,8 @@ import { isNoteRecord } from "./index-records.mjs";
|
|
|
52
55
|
import { foreignWorksNodes, worksNode, worksPages } from "./literature-works.mjs";
|
|
53
56
|
import { buildMaps, relatedPlaces } from "./map-build.mjs";
|
|
54
57
|
import { mapWorld } from "./map-places.mjs";
|
|
58
|
+
import { officeBodyKey, officeHolders } from "./office-holders.mjs";
|
|
59
|
+
import { renderAddress } from "./address.mjs";
|
|
55
60
|
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
56
61
|
|
|
57
62
|
/**
|
|
@@ -65,10 +70,12 @@ import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
|
65
70
|
export const PLACE_KIND_LABELS = Object.freeze({
|
|
66
71
|
world: "Worlds",
|
|
67
72
|
region: "Regions",
|
|
73
|
+
district: "Districts",
|
|
68
74
|
settlement: "Settlements",
|
|
69
75
|
site: "Sites",
|
|
70
76
|
structure: "Structures",
|
|
71
77
|
feature: "Features",
|
|
78
|
+
route: "Routes",
|
|
72
79
|
celestial: "Celestial places",
|
|
73
80
|
});
|
|
74
81
|
|
|
@@ -166,6 +173,22 @@ function section(slug, content) {
|
|
|
166
173
|
return `${sectionHeading(slug)}\n\n${content}\n`;
|
|
167
174
|
}
|
|
168
175
|
|
|
176
|
+
/**
|
|
177
|
+
* A body's offices, one line each, with the beings holding each.
|
|
178
|
+
*
|
|
179
|
+
* @param {Map<string, object[]>} held - Office → its holders' records.
|
|
180
|
+
* @returns {string} Markdown.
|
|
181
|
+
*/
|
|
182
|
+
function officeLines(held) {
|
|
183
|
+
const holder = (record) =>
|
|
184
|
+
record.address ?
|
|
185
|
+
`[[${renderAddress({ ...record.address.canonical, system: "note" })}|]]`
|
|
186
|
+
: plain(titleOf(record));
|
|
187
|
+
return [...held]
|
|
188
|
+
.map(([office, holders]) => `- **${plain(office)}:** ${holders.map(holder).join(", ")}`)
|
|
189
|
+
.join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
169
192
|
/**
|
|
170
193
|
* The note's name, as the derivations title an entry.
|
|
171
194
|
*
|
|
@@ -257,12 +280,18 @@ export function drawMapsFrom(records, { outDir, foreignIndex = new Map(), config
|
|
|
257
280
|
* prepared results are keyed.
|
|
258
281
|
* @param {Map<string, object>} [opts.foreignIndex] - The fetched indexes.
|
|
259
282
|
* @param {object} [opts.config] - The resolved configuration.
|
|
283
|
+
* @param {(file:string)=>string|undefined} [opts.rawOf] - Captured source locators.
|
|
284
|
+
* @param {object[]} [opts.all=records] - Complete records for settlement validation.
|
|
260
285
|
* @param {string} [opts.only] - One note file to give sections to; absent,
|
|
261
286
|
* every note.
|
|
262
287
|
* @returns {Map<string, Map<string, string>>} Note file to its sections, each
|
|
263
288
|
* keyed by its slug, for every note given at least one.
|
|
264
289
|
*/
|
|
265
|
-
export function derivedSections(
|
|
290
|
+
export function derivedSections(
|
|
291
|
+
records,
|
|
292
|
+
{ fileOf, foreignIndex = new Map(), config, only, rawOf, all = records },
|
|
293
|
+
) {
|
|
294
|
+
assertSettlementRecords(all, { fileOf, foreignIndex, rawOf });
|
|
266
295
|
const notes = records.filter((record) => isNoteRecord(record) && record.shortcode);
|
|
267
296
|
const pages = notes.filter((record) => record.address);
|
|
268
297
|
const nodeOf = (record) => ({
|
|
@@ -286,6 +315,7 @@ export function derivedSections(records, { fileOf, foreignIndex = new Map(), con
|
|
|
286
315
|
{ types: new Set(Object.keys(NOTE_VOCABULARY)) },
|
|
287
316
|
);
|
|
288
317
|
const mapped = new Set(placesWithMaps(notes, { foreignIndex, config }));
|
|
318
|
+
const offices = officeHolders(notes);
|
|
289
319
|
|
|
290
320
|
const out = new Map();
|
|
291
321
|
for (const record of pages) {
|
|
@@ -297,6 +327,8 @@ export function derivedSections(records, { fileOf, foreignIndex = new Map(), con
|
|
|
297
327
|
if (content && !authoredSection(record, slug))
|
|
298
328
|
sections.set(slug, section(slug, content));
|
|
299
329
|
};
|
|
330
|
+
const settlements = settlementMarkdown(record);
|
|
331
|
+
if (settlements) sections.set("settlements", section("settlements", settlements));
|
|
300
332
|
if (lists.contains?.length) add("within", groupedPlaces(lists.contains));
|
|
301
333
|
if (lists.governed_by?.length)
|
|
302
334
|
add(
|
|
@@ -305,6 +337,8 @@ export function derivedSections(records, { fileOf, foreignIndex = new Map(), con
|
|
|
305
337
|
);
|
|
306
338
|
if (lists.governed_places?.length)
|
|
307
339
|
add("governedplaces", groupedPlaces(lists.governed_places));
|
|
340
|
+
const held = record.type === "affiliation" && offices.get(officeBodyKey(record));
|
|
341
|
+
if (held) add("officeholders", officeLines(held));
|
|
308
342
|
const written = works.get(key)?.works ?? [];
|
|
309
343
|
if (written.length)
|
|
310
344
|
add(
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
51
|
import { pluralOf, pluralByRule } from "./word-inflections.mjs";
|
|
52
|
+
import { standingOffices } from "./standings.mjs";
|
|
52
53
|
export { pluralOf, pluralByRule } from "./word-inflections.mjs";
|
|
53
54
|
import { createHash } from "node:crypto";
|
|
54
55
|
import { parseAddress, renderAddress } from "./address.mjs";
|
|
@@ -393,7 +394,7 @@ export function dictionaryAndTermRows(records, all = records) {
|
|
|
393
394
|
: [];
|
|
394
395
|
add("office", title, isMap(office) ? office.description : office, [
|
|
395
396
|
...holders,
|
|
396
|
-
...membersOf(body, (standing) => standing.
|
|
397
|
+
...membersOf(body, (standing) => standingOffices(standing).includes(title)),
|
|
397
398
|
]);
|
|
398
399
|
}
|
|
399
400
|
governance.set(body, rows);
|
|
@@ -57,9 +57,11 @@ export const CHRONOLOGY_SLUG = "chronology";
|
|
|
57
57
|
*/
|
|
58
58
|
export const GENERATED_SECTIONS = Object.freeze(
|
|
59
59
|
[
|
|
60
|
+
{ slug: "settlements", heading: "Settlements" },
|
|
60
61
|
{ slug: "within", heading: "Within" },
|
|
61
62
|
{ slug: "governedby", heading: "Governed by" },
|
|
62
63
|
{ slug: "governedplaces", heading: "Governed places" },
|
|
64
|
+
{ slug: "officeholders", heading: "Office holders" },
|
|
63
65
|
{ slug: CHRONOLOGY_SLUG, heading: "Chronology" },
|
|
64
66
|
{ slug: "events", heading: "Events" },
|
|
65
67
|
{ slug: "accounts", heading: "Accounts" },
|
package/engine/holdings.mjs
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Derive containment and government lists from place facts. Geography comes
|
|
16
16
|
* only from `data.parents`; government comes only from `data.government`.
|
|
17
|
-
* Neither graph expands through
|
|
17
|
+
* Neither graph expands through affiliation ancestors, and government expands
|
|
18
|
+
* through geographic ones only for a district stating none, which the content
|
|
19
|
+
* index resolves before these lists read it. Government identity preserves the Address package, type and shortcode.
|
|
18
20
|
* Its system normalizes to `note` because Item and journal documents share one page.
|
|
19
21
|
* The lists become a note's generated **Within**, **Governed by** and
|
|
20
22
|
* **Governed places** sections — see {@link module:engine/derived-sections}.
|
|
@@ -255,6 +257,106 @@ export function holdingsPages(nodes, { governmentNodes = [] } = {}) {
|
|
|
255
257
|
return lists;
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
/* --------------------------------------------------------------------- */
|
|
261
|
+
/* A district's government */
|
|
262
|
+
/* --------------------------------------------------------------------- */
|
|
263
|
+
|
|
264
|
+
/** Districts whose government the index supplied from a parent. */
|
|
265
|
+
const inheritedGovernment = new WeakSet();
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Whether a record's `data.government` came from a parent rather than its own
|
|
269
|
+
* note.
|
|
270
|
+
*
|
|
271
|
+
* @param {object} record - An index record.
|
|
272
|
+
* @returns {boolean} True for a district that inherited its government.
|
|
273
|
+
*/
|
|
274
|
+
export function governmentInherited(record) {
|
|
275
|
+
return inheritedGovernment.has(record);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Give each district that states no government its parent's.
|
|
280
|
+
*
|
|
281
|
+
* A `place/district` falls under the government of the place it stands
|
|
282
|
+
* within. One stating no `data.government` key takes the value of its nearest
|
|
283
|
+
* parent that states the key, walking `data.parents` breadth first, in the
|
|
284
|
+
* order written, through any parent that states none, and never visiting a
|
|
285
|
+
* place twice. An explicit value on the district, `null` (complete anarchy)
|
|
286
|
+
* included, is its own and is kept; a parent's explicit `null` is inherited as
|
|
287
|
+
* anarchy. Every reader of the index — the Governed by and Governed places
|
|
288
|
+
* sections, the "missing government" advisory, a consumer of the published
|
|
289
|
+
* index — then reads the effective government at `data.government`.
|
|
290
|
+
*
|
|
291
|
+
* @param {object[]} records - Content-index records, edited in place.
|
|
292
|
+
* @param {object} [opts]
|
|
293
|
+
* @param {(key: string) => object|undefined} [opts.foreignRecord] - A fetched
|
|
294
|
+
* index's record by canonical Address, for a parent in a dependency.
|
|
295
|
+
* @returns {void}
|
|
296
|
+
*/
|
|
297
|
+
export function inheritDistrictGovernments(records, { foreignRecord } = {}) {
|
|
298
|
+
const keyOf = (pkg, shortcode) =>
|
|
299
|
+
renderAddress({
|
|
300
|
+
package: String(pkg ?? "local"),
|
|
301
|
+
system: "note",
|
|
302
|
+
type: "place",
|
|
303
|
+
shortcode: String(shortcode).toLowerCase(),
|
|
304
|
+
});
|
|
305
|
+
const places = new Map();
|
|
306
|
+
for (const record of records) {
|
|
307
|
+
if (record?.type !== "place" || record.documents || !record.shortcode) continue;
|
|
308
|
+
const key = keyOf(record.package, record.shortcode);
|
|
309
|
+
if (!places.has(key)) places.set(key, record);
|
|
310
|
+
}
|
|
311
|
+
const parentKeys = (record) => {
|
|
312
|
+
const parents = record?.data?.parents ?? record?.parents;
|
|
313
|
+
const list =
|
|
314
|
+
Array.isArray(parents) ? parents
|
|
315
|
+
: parents == null ? []
|
|
316
|
+
: [parents];
|
|
317
|
+
return list
|
|
318
|
+
.map((value) => {
|
|
319
|
+
if (isAddressTuple(value)) return keyOf(value.package, value.shortcode);
|
|
320
|
+
const tuple =
|
|
321
|
+
readCanonicalKey(String(value ?? "")) ??
|
|
322
|
+
parseAddress(value, {
|
|
323
|
+
package: record.package ?? "local",
|
|
324
|
+
system: "note",
|
|
325
|
+
type: "place",
|
|
326
|
+
types: new Set(["place"]),
|
|
327
|
+
});
|
|
328
|
+
return isAddressTuple(tuple) ? keyOf(tuple.package, tuple.shortcode) : "";
|
|
329
|
+
})
|
|
330
|
+
.filter(Boolean);
|
|
331
|
+
};
|
|
332
|
+
const stated = (record) => {
|
|
333
|
+
const data = record?.data && typeof record.data === "object" ? record.data : record;
|
|
334
|
+
return data && Object.hasOwn(data, "government") && !inheritedGovernment.has(record) ?
|
|
335
|
+
{ value: data.government }
|
|
336
|
+
: undefined;
|
|
337
|
+
};
|
|
338
|
+
for (const district of places.values()) {
|
|
339
|
+
if (district.subType !== "district") continue;
|
|
340
|
+
const data = district.data && typeof district.data === "object" ? district.data : {};
|
|
341
|
+
if (Object.hasOwn(data, "government")) continue;
|
|
342
|
+
const seen = new Set([keyOf(district.package, district.shortcode)]);
|
|
343
|
+
const queue = parentKeys(district);
|
|
344
|
+
let found;
|
|
345
|
+
while (queue.length && !found) {
|
|
346
|
+
const key = queue.shift();
|
|
347
|
+
if (seen.has(key)) continue;
|
|
348
|
+
seen.add(key);
|
|
349
|
+
const parent = places.get(key) ?? foreignRecord?.(key);
|
|
350
|
+
if (!parent) continue;
|
|
351
|
+
found = stated(parent);
|
|
352
|
+
if (!found) queue.push(...parentKeys(parent));
|
|
353
|
+
}
|
|
354
|
+
if (!found) continue;
|
|
355
|
+
district.data = { ...data, government: found.value };
|
|
356
|
+
inheritedGovernment.add(district);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
258
360
|
/* --------------------------------------------------------------------- */
|
|
259
361
|
/* The lint */
|
|
260
362
|
/* --------------------------------------------------------------------- */
|
|
@@ -264,8 +366,10 @@ export function holdingsPages(nodes, { governmentNodes = [] } = {}) {
|
|
|
264
366
|
* Missing, null or zero population establishes no inhabited population.
|
|
265
367
|
* Property presence distinguishes omitted government from explicit null,
|
|
266
368
|
* which means complete anarchy. Non-null values are validated by the declared
|
|
267
|
-
* affiliation reference field.
|
|
268
|
-
* supplies
|
|
369
|
+
* affiliation reference field. Tenure never supplies this fact, and
|
|
370
|
+
* containment supplies it only to a district, which the content index gives
|
|
371
|
+
* its nearest parent's government (see {@link inheritDistrictGovernments}), so
|
|
372
|
+
* a district under a governed place is never advised.
|
|
269
373
|
*
|
|
270
374
|
* @param {object} note - The note with parsed frontmatter and raw source.
|
|
271
375
|
* @returns {object[]} Located warning findings.
|
package/engine/index-records.mjs
CHANGED
|
@@ -42,7 +42,8 @@ import { isAddressTuple } from "./address.mjs";
|
|
|
42
42
|
* since a note declaring its own is a hard error — and it matches what
|
|
43
43
|
* the content-table expander puts on the same field, so a query reads the same
|
|
44
44
|
* value from either. `file` namespaces the note's place in the tree, again
|
|
45
|
-
* matching the expander's `file.*`.
|
|
45
|
+
* matching the expander's `file.*`. `officeHolders` lists, on a body, the
|
|
46
|
+
* living beings whose own standing names each of its offices.
|
|
46
47
|
*
|
|
47
48
|
* Both are checked rather than assumed: `folder` is real frontmatter on most
|
|
48
49
|
* notes, so the neighbouring names are close enough to a real key that a silent
|
|
@@ -68,6 +69,7 @@ export const DERIVED_KEYS = Object.freeze([
|
|
|
68
69
|
"citations",
|
|
69
70
|
"names",
|
|
70
71
|
"runs",
|
|
72
|
+
"officeHolders",
|
|
71
73
|
]);
|
|
72
74
|
|
|
73
75
|
/**
|
package/engine/index.mjs
CHANGED
|
@@ -103,7 +103,7 @@ export * as populations from "./populations.mjs";
|
|
|
103
103
|
/** What trade a settlement supports: the six-step market scale, and the check that holds a value to it. */
|
|
104
104
|
export * as marketClass from "./market-class.mjs";
|
|
105
105
|
|
|
106
|
-
/** A region's settlements table:
|
|
106
|
+
/** A region's settlements table: its rows, the columns, and the check that holds a table to them. */
|
|
107
107
|
export * as settlementTables from "./settlement-tables.mjs";
|
|
108
108
|
|
|
109
109
|
/** The places a map is drawn from, read from the content index, and what `parents` says about them. */
|
package/engine/language-data.mjs
CHANGED
|
@@ -1071,7 +1071,6 @@ export function checkLanguageData(note, { index, types } = {}) {
|
|
|
1071
1071
|
for (const kind of SCOPE_KINDS) {
|
|
1072
1072
|
listOf(scope[kind]).forEach((value, i) => {
|
|
1073
1073
|
const path = ["data", "scope", kind, i];
|
|
1074
|
-
if (kind === "paths") return;
|
|
1075
1074
|
if (kind === "types") {
|
|
1076
1075
|
if (types && typeof value === "string" && !types.includes(value))
|
|
1077
1076
|
findings.push(finding(path, "error", `\`${value}\` is not a note type`));
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
* tongue whose forms differ by gender states a map of gender words to a
|
|
30
30
|
* pattern or a list of them. A gender with no pattern of its own takes every
|
|
31
31
|
* pattern stated.
|
|
32
|
-
* - `given` —
|
|
33
|
-
*
|
|
34
|
-
* person's gender's list, or
|
|
35
|
-
* has none
|
|
36
|
-
* person's gender, as a patronymic draws a father's name.
|
|
32
|
+
* - `given` — one list of names for everyone, or a map of gender words to
|
|
33
|
+
* lists. The map requires `female` and `male`. `{given}` draws from the shared
|
|
34
|
+
* list, the person's gender's list, or every given list together where that
|
|
35
|
+
* gender has none. `{given.<gender>}` draws from the shared list or that
|
|
36
|
+
* gender's list whatever the person's gender, as a patronymic draws a father's name.
|
|
37
37
|
* - `en` — English names, in groups of lists: `en.epithet` is a list a pattern
|
|
38
38
|
* draws from as `{en.epithet}`, as `"{given} the {en.epithet}"` does. English
|
|
39
39
|
* names are never encoded in the tongue's script or checked against its
|
|
@@ -73,7 +73,7 @@ const LIST_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
|
73
73
|
const PLACEHOLDER = /^([A-Za-z][A-Za-z0-9_]*)(?:\.([A-Za-z][A-Za-z0-9_]*))?$/;
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* The genders
|
|
76
|
+
* The genders a mapped `data.names.given` states a list for.
|
|
77
77
|
*
|
|
78
78
|
* A function rather than a `const`: `note-vocabulary.mjs` reads it while the
|
|
79
79
|
* module graph is still evaluating.
|
|
@@ -317,8 +317,12 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
317
317
|
|
|
318
318
|
const given = names[GIVEN_LIST];
|
|
319
319
|
if (given === undefined || given === null) shape([], "`data.names` must state `given`", true);
|
|
320
|
+
else if (Array.isArray(given)) checkList([GIVEN_LIST]);
|
|
320
321
|
else if (!isPlainMap(given))
|
|
321
|
-
shape(
|
|
322
|
+
shape(
|
|
323
|
+
[GIVEN_LIST],
|
|
324
|
+
`\`${label([GIVEN_LIST])}\` should be a list of names or a map of genders to lists`,
|
|
325
|
+
);
|
|
322
326
|
else {
|
|
323
327
|
const genders = genderWords(lookVocabulary);
|
|
324
328
|
for (const gender of requiredGivenGenders()) {
|
|
@@ -428,7 +432,18 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
428
432
|
}
|
|
429
433
|
named.add(part.list);
|
|
430
434
|
if (part.gender !== undefined) {
|
|
431
|
-
if (
|
|
435
|
+
if (Array.isArray(given)) {
|
|
436
|
+
if (!genders.includes(part.gender)) {
|
|
437
|
+
const guess = nearest(part.gender, genders);
|
|
438
|
+
problem(
|
|
439
|
+
path,
|
|
440
|
+
`${JSON.stringify(part.gender)} is not a gender; ` +
|
|
441
|
+
`\`{${placeholderText(part)}}\` in \`${label(path)}\` takes ` +
|
|
442
|
+
genders.join(", ") +
|
|
443
|
+
(guess ? `. Did you mean "${guess}"?` : ""),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
} else if (givenMap[part.gender] === undefined || givenMap[part.gender] === null)
|
|
432
447
|
problem(
|
|
433
448
|
path,
|
|
434
449
|
`\`{${placeholderText(part)}}\` in \`${label(path)}\` names no list: ` +
|
|
@@ -476,7 +491,7 @@ export function namesProblems(names, lookVocabulary) {
|
|
|
476
491
|
* keys cannot: a pattern that does not parse, a placeholder naming no list, a
|
|
477
492
|
* list no pattern names, a list name a placeholder cannot spell, an empty list
|
|
478
493
|
* of patterns or of names, an empty name, a pattern or given list keyed by no
|
|
479
|
-
* gender, and a missing `female` or `male` list. Each is located at the pattern, list, entry or key at fault.
|
|
494
|
+
* gender, and a missing `female` or `male` list in a gender map. Each is located at the pattern, list, entry or key at fault.
|
|
480
495
|
*
|
|
481
496
|
* @param {object} note - The note: `file`, `raw` and parsed `fm`.
|
|
482
497
|
* @param {Readonly<Record<string, readonly {value: string}[]>>} lookVocabulary -
|
|
@@ -534,8 +549,8 @@ export function namesFindings(note, lookVocabulary) {
|
|
|
534
549
|
}
|
|
535
550
|
|
|
536
551
|
/**
|
|
537
|
-
* The given names a gender draws from: its own list, or
|
|
538
|
-
* together where it has none.
|
|
552
|
+
* The given names a gender draws from: the shared list, its own list, or
|
|
553
|
+
* every given list together where it has none.
|
|
539
554
|
*
|
|
540
555
|
* @param {object} names - Checked `data.names`.
|
|
541
556
|
* @param {string} gender - A gender word.
|
|
@@ -543,6 +558,7 @@ export function namesFindings(note, lookVocabulary) {
|
|
|
543
558
|
*/
|
|
544
559
|
export function givenNames(names, gender) {
|
|
545
560
|
const given = names[GIVEN_LIST];
|
|
561
|
+
if (Array.isArray(given)) return given;
|
|
546
562
|
if (Array.isArray(given[gender])) return given[gender];
|
|
547
563
|
return [...new Set(Object.values(given).filter(Array.isArray).flat())];
|
|
548
564
|
}
|
|
@@ -551,29 +567,42 @@ export function givenNames(names, gender) {
|
|
|
551
567
|
function listFor(names, part, gender) {
|
|
552
568
|
if (part.sub !== undefined) return names[part.list][part.sub];
|
|
553
569
|
if (part.list !== GIVEN_LIST) return names[part.list];
|
|
554
|
-
if (part.gender !== undefined) return names
|
|
570
|
+
if (part.gender !== undefined) return givenNames(names, part.gender);
|
|
555
571
|
return givenNames(names, gender);
|
|
556
572
|
}
|
|
557
573
|
|
|
558
574
|
/**
|
|
559
|
-
* A name
|
|
560
|
-
*
|
|
575
|
+
* A name's selected literal and placeholder parts. One of the gender's
|
|
576
|
+
* patterns is selected first, then each placeholder is an even draw from its
|
|
577
|
+
* source list.
|
|
561
578
|
*
|
|
562
579
|
* @param {object} names - Checked `data.names`.
|
|
563
580
|
* @param {string} gender - A gender word.
|
|
564
581
|
* @param {() => number} next - A stream of numbers in `[0, 1)`.
|
|
565
|
-
* @returns {string} The
|
|
582
|
+
* @returns {(PatternPart & {text: string})[]} The selected parts in pattern order.
|
|
566
583
|
*/
|
|
567
|
-
export function
|
|
584
|
+
export function drawNameParts(names, gender, next) {
|
|
568
585
|
const patterns = patternsFor(names, gender);
|
|
569
586
|
const pattern =
|
|
570
587
|
patterns.length > 1 ? patterns[Math.floor(next() * patterns.length)] : patterns[0];
|
|
571
|
-
return parsePattern(pattern)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
588
|
+
return parsePattern(pattern).parts.map((part) => {
|
|
589
|
+
if ("literal" in part) return { ...part, text: part.literal };
|
|
590
|
+
const list = listFor(names, part, gender);
|
|
591
|
+
return { ...part, text: list[Math.floor(next() * list.length)] };
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* A name drawn from a language's lists.
|
|
597
|
+
*
|
|
598
|
+
* @param {object} names - Checked `data.names`.
|
|
599
|
+
* @param {string} gender - A gender word.
|
|
600
|
+
* @param {() => number} next - A stream of numbers in `[0, 1)`.
|
|
601
|
+
* @returns {string} The name.
|
|
602
|
+
*/
|
|
603
|
+
export function drawName(names, gender, next) {
|
|
604
|
+
return drawNameParts(names, gender, next)
|
|
605
|
+
.map((part) => part.text)
|
|
577
606
|
.join("");
|
|
578
607
|
}
|
|
579
608
|
|
|
@@ -640,7 +669,8 @@ export function nameMisfit(full, names, gender) {
|
|
|
640
669
|
const text = nfc(full);
|
|
641
670
|
const given = names[GIVEN_LIST];
|
|
642
671
|
const own = givenNames(names, gender).map(nfc);
|
|
643
|
-
const every =
|
|
672
|
+
const every = (
|
|
673
|
+
Array.isArray(given) ? given : Object.values(given).filter(Array.isArray).flat()).map(nfc);
|
|
644
674
|
const lookup = (forGiven) => (part) =>
|
|
645
675
|
part.list === GIVEN_LIST && part.gender === undefined ?
|
|
646
676
|
forGiven
|
package/engine/language-rows.mjs
CHANGED
|
@@ -413,14 +413,12 @@ export function inScope(record, address, scope) {
|
|
|
413
413
|
const wanted = values(kind);
|
|
414
414
|
return strings(list).some((one) => wanted.has(one));
|
|
415
415
|
};
|
|
416
|
-
const file = typeof record.file?.path === "string" ? record.file.path : "";
|
|
417
416
|
return (
|
|
418
417
|
values("addresses").has(address) ||
|
|
419
418
|
any("folders", data.packFolder) ||
|
|
420
419
|
any("cultures", data.culture) ||
|
|
421
420
|
any("lore", data.lore) ||
|
|
422
|
-
any("parents", data.parents)
|
|
423
|
-
strings(scope.paths).some((prefix) => file.startsWith(prefix))
|
|
421
|
+
any("parents", data.parents)
|
|
424
422
|
);
|
|
425
423
|
}
|
|
426
424
|
|