@heroiclands/package-build 25.5.5 → 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 +18 -0
- package/assets/styles/content-base.css +1 -1
- package/docs/api.md +24 -16
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +10 -9
- package/docs/content-format.md +40 -1
- package/docs/diagnostics.md +2 -2
- package/docs/guides/reference-pages.md +4 -1
- package/docs/guides/site.md +1 -1
- package/docs/reference/format-details.md +174 -129
- package/docs/reference/note-types.md +134 -121
- package/docs/reference/theme-pages.md +1 -0
- 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 +2 -2
- package/engine/content-tables.mjs +12 -5
- 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-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/metadata-index.mjs +1 -0
- package/engine/note-addresses.mjs +16 -0
- package/engine/note-vocabulary.mjs +19 -8
- package/engine/office-holders.mjs +76 -0
- package/engine/page-breaks.mjs +36 -8
- package/engine/pdf-render.mjs +4 -3
- package/engine/place-kinds.mjs +106 -0
- package/engine/settlement-tables.mjs +410 -675
- package/engine/settlement-terms.mjs +118 -0
- package/engine/site-build.mjs +2 -4
- 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/package.json +9 -4
- package/site-theme/data/banner-kinds.yaml +2 -0
- package/sohl/actors.mjs +4 -3
- package/types/engine/content-html.d.mts +2 -1
- 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-terms.d.mts +5 -1
- package/types/engine/markdown-blocks.d.mts +5 -1
- package/types/engine/office-holders.d.mts +33 -0
- package/types/engine/page-breaks.d.mts +8 -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/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/vendor/markdownlint.mjs +855 -569
package/engine/content-index.mjs
CHANGED
|
@@ -141,6 +141,8 @@ import { EXPRESSION, renderMarkdownExpressions } from "./markdown-expressions.mj
|
|
|
141
141
|
import { scanFigures } from "./content-figures.mjs";
|
|
142
142
|
import { eventNoteIndex } from "./event-fields.mjs";
|
|
143
143
|
import { derivedSections } from "./derived-sections.mjs";
|
|
144
|
+
import { officeBodyKey, officeHolders } from "./office-holders.mjs";
|
|
145
|
+
import { inheritDistrictGovernments } from "./holdings.mjs";
|
|
144
146
|
// One reader for a note's anchors, shared with the link checker and with the
|
|
145
147
|
// builds that emit a link. Re-exported because this is where callers
|
|
146
148
|
// have always addressed it.
|
|
@@ -1311,6 +1313,8 @@ export function collectContentIndex(
|
|
|
1311
1313
|
parsed,
|
|
1312
1314
|
});
|
|
1313
1315
|
resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems, parsed });
|
|
1316
|
+
// A district stating no government falls under its parent's.
|
|
1317
|
+
inheritDistrictGovernments(records, { foreignRecord });
|
|
1314
1318
|
const cultureNotes = records
|
|
1315
1319
|
.filter((record) => isNoteRecord(record) && record.type === "lore")
|
|
1316
1320
|
.map((record) => ({
|
|
@@ -1333,6 +1337,24 @@ export function collectContentIndex(
|
|
|
1333
1337
|
for (const [note, inherited] of inheritance.values)
|
|
1334
1338
|
if (Object.keys(inherited).length) note.fm.data = { ...note.fm.data, ...inherited };
|
|
1335
1339
|
|
|
1340
|
+
// An office's holders are the living beings naming it, read once every
|
|
1341
|
+
// being is recorded. A public body lists only public beings, as its page
|
|
1342
|
+
// does.
|
|
1343
|
+
phase("index office holders");
|
|
1344
|
+
const publicHolders = officeHolders(records.filter((record) => !isGmNote(record)));
|
|
1345
|
+
const allHolders = officeHolders(records);
|
|
1346
|
+
for (const record of records) {
|
|
1347
|
+
if (!isNoteRecord(record) || record.type !== "affiliation") continue;
|
|
1348
|
+
const held = (isGmNote(record) ? allHolders : publicHolders).get(officeBodyKey(record));
|
|
1349
|
+
if (!held) continue;
|
|
1350
|
+
record.officeHolders = [...held].map(([office, holders]) => ({
|
|
1351
|
+
office,
|
|
1352
|
+
holders: holders.map((being) =>
|
|
1353
|
+
readCanonicalKey(canonicalKey(being.package, "note", "being", being.shortcode)),
|
|
1354
|
+
),
|
|
1355
|
+
}));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1336
1358
|
// Automatic lexical sections require only the recorded frontmatter. Their
|
|
1337
1359
|
// published anchors and journal targets therefore use the same pure body
|
|
1338
1360
|
// projection as the surfaces, without making the index emitter async.
|
|
@@ -1924,6 +1946,7 @@ export function emitContentIndex({ contentBase, outDir, config, publication } =
|
|
|
1924
1946
|
record.data?.events?.length,
|
|
1925
1947
|
) ||
|
|
1926
1948
|
derivedSections(notes, {
|
|
1949
|
+
rawOf: (file) => parsed.get(file)?.raw,
|
|
1927
1950
|
fileOf: (record) => noteFile(tree, record),
|
|
1928
1951
|
config: resolved,
|
|
1929
1952
|
foreignIndex: loadForeignIndexes(resolved, [contentPackage]).index,
|
package/engine/content-links.mjs
CHANGED
|
@@ -56,6 +56,7 @@ import { readWikilink } from "./wikilink-syntax.mjs";
|
|
|
56
56
|
import { encodeAddresses } from "./address-values.mjs";
|
|
57
57
|
import fs from "node:fs";
|
|
58
58
|
import { cultureRecordProblems, cultureInheritedFields } from "./culture-parents.mjs";
|
|
59
|
+
import { governmentInherited } from "./holdings.mjs";
|
|
59
60
|
import path from "node:path";
|
|
60
61
|
|
|
61
62
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
@@ -248,6 +249,13 @@ export function buildLinkIndex(
|
|
|
248
249
|
if (typeof record.languageCode === "string") note.languageCode = record.languageCode;
|
|
249
250
|
const hierarchyProblems = cultureRecordProblems(record);
|
|
250
251
|
note.inheritedCultureFields = cultureInheritedFields(record);
|
|
252
|
+
// A district's government taken from its parent is the parent's
|
|
253
|
+
// authored value, checked where it is written, not here.
|
|
254
|
+
if (governmentInherited(record))
|
|
255
|
+
note.inheritedCultureFields = new Set([
|
|
256
|
+
...(note.inheritedCultureFields ?? []),
|
|
257
|
+
"government",
|
|
258
|
+
]);
|
|
251
259
|
if (hierarchyProblems)
|
|
252
260
|
note.reportedCultureParents = new Set(
|
|
253
261
|
hierarchyProblems
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
*
|
|
41
41
|
* A note reaches {@link Renderer#render} with its index-dependent constructs
|
|
42
42
|
* already resolved by {@link module:engine/content-expand.expandContent}.
|
|
43
|
+
* Valid authored page breaks are omitted before these stages.
|
|
44
|
+
*
|
|
43
45
|
* {@link Renderer#prepare} then writes the constructs markdown-it has no
|
|
44
46
|
* syntax for as HTML between blank lines, in this order:
|
|
45
47
|
*
|
|
46
|
-
* Valid authored page breaks are omitted before these stages.
|
|
47
|
-
*
|
|
48
48
|
* 1. **Marked text** — a run's language, face and mapping, from the
|
|
49
49
|
* configuration's script notes.
|
|
50
50
|
* 2. **Captioned items** — each figure's wrapper and label. Its number, kind
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { FENCE_LINE } from "./code-fences.mjs";
|
|
15
15
|
import { PAGE_LIST_LANGUAGE } from "./page-lists.mjs";
|
|
16
16
|
import { appendGeneratedSections } from "./generated-sections.mjs";
|
|
17
|
+
import { insertDeclaredSections } from "./declared-sections.mjs";
|
|
17
18
|
import { insertLexicalSections } from "./lexical-sections.mjs";
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -129,8 +130,8 @@ export function expandContentTables(markdown, { source = "", sqlTables, pageList
|
|
|
129
130
|
source,
|
|
130
131
|
directive: block.join("\n"),
|
|
131
132
|
reason: failure,
|
|
132
|
-
line: i,
|
|
133
|
-
column: indent.length + 1,
|
|
133
|
+
line: prepared?.line ?? i,
|
|
134
|
+
column: prepared?.column ?? indent.length + 1,
|
|
134
135
|
});
|
|
135
136
|
block.forEach((value, offset) => emit(value, i + offset));
|
|
136
137
|
i = close;
|
|
@@ -149,11 +150,17 @@ export function expandContentTables(markdown, { source = "", sqlTables, pageList
|
|
|
149
150
|
if (close + 1 < lines.length && lines[close + 1].trim()) emit("", i, true);
|
|
150
151
|
i = close;
|
|
151
152
|
}
|
|
153
|
+
const declared = insertDeclaredSections(out.join("\n"), sqlTables?.declared, {
|
|
154
|
+
authored,
|
|
155
|
+
lineMap,
|
|
156
|
+
generatedFrom,
|
|
157
|
+
syntax: sqlTables?.syntax,
|
|
158
|
+
});
|
|
152
159
|
return {
|
|
153
|
-
...insertLexicalSections(
|
|
160
|
+
...insertLexicalSections(declared.markdown, sqlTables?.lexical, {
|
|
154
161
|
authored,
|
|
155
|
-
lineMap,
|
|
156
|
-
generatedFrom,
|
|
162
|
+
lineMap: declared.lineMap,
|
|
163
|
+
generatedFrom: declared.generatedFrom,
|
|
157
164
|
syntax: sqlTables?.syntax,
|
|
158
165
|
}),
|
|
159
166
|
errors,
|
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`));
|
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
|
|
|
@@ -152,8 +152,12 @@ export const JOIN_MEETINGS = Object.freeze(["C+C", "V+C", "V+V"]);
|
|
|
152
152
|
*/
|
|
153
153
|
export const JOIN_RULES = Object.freeze(["merge", "insert", "drop", "double"]);
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
155
|
+
/**
|
|
156
|
+
* What a written mark says of its letter: `stress` falls on it, `length` holds
|
|
157
|
+
* it longer, `apart` says it apart from its neighbour, and `quality` changes
|
|
158
|
+
* which vowel is said.
|
|
159
|
+
*/
|
|
160
|
+
export const MARK_KINDS = Object.freeze(["stress", "length", "apart", "quality"]);
|
|
157
161
|
|
|
158
162
|
/** How many of a mark a word may carry: `one` at most, or `any` number. */
|
|
159
163
|
export const MARK_LIMITS = Object.freeze(["one", "any"]);
|
|
@@ -320,13 +324,6 @@ export const SCOPE_FIELDS = Object.freeze([
|
|
|
320
324
|
entries: Object.freeze({ kind: "address", shape: "an Address" }),
|
|
321
325
|
describe: "These notes themselves.",
|
|
322
326
|
}),
|
|
323
|
-
Object.freeze({
|
|
324
|
-
name: "paths",
|
|
325
|
-
kind: "list",
|
|
326
|
-
shape: "a list of content paths",
|
|
327
|
-
entries: Object.freeze({ kind: "string", shape: "a path such as `Regions/Aelwyth/`" }),
|
|
328
|
-
describe: "Every note whose file sits below one of these paths of the content tree.",
|
|
329
|
-
}),
|
|
330
327
|
Object.freeze({
|
|
331
328
|
name: "types",
|
|
332
329
|
kind: "list",
|
|
@@ -1926,7 +1923,7 @@ export const LANGUAGE_DATA_FIELDS = Object.freeze([
|
|
|
1926
1923
|
Object.freeze({
|
|
1927
1924
|
name: "scope",
|
|
1928
1925
|
kind: "map",
|
|
1929
|
-
shape: "a map — `{ folders?, cultures?, lore?, parents?, addresses?,
|
|
1926
|
+
shape: "a map — `{ folders?, cultures?, lore?, parents?, addresses?, types?, skip? }`",
|
|
1930
1927
|
fields: SCOPE_FIELDS,
|
|
1931
1928
|
describe: "The notes the register covers. A language skill note only.",
|
|
1932
1929
|
}),
|
package/engine/map-dot.mjs
CHANGED
|
@@ -131,6 +131,26 @@ export const STYLE = Object.freeze({
|
|
|
131
131
|
penwidth: 2.0,
|
|
132
132
|
extraStyle: "rounded",
|
|
133
133
|
},
|
|
134
|
+
district: {
|
|
135
|
+
shape: "box",
|
|
136
|
+
fill: "#C5E1A5",
|
|
137
|
+
border: DEFAULT_BORDER,
|
|
138
|
+
fontsize: 9,
|
|
139
|
+
width: 0.8,
|
|
140
|
+
height: 0.4,
|
|
141
|
+
penwidth: 1.0,
|
|
142
|
+
extraStyle: "rounded,dashed",
|
|
143
|
+
},
|
|
144
|
+
route: {
|
|
145
|
+
shape: "parallelogram",
|
|
146
|
+
fill: "#FFE082",
|
|
147
|
+
border: DEFAULT_BORDER,
|
|
148
|
+
fontsize: 9,
|
|
149
|
+
width: 0.9,
|
|
150
|
+
height: 0.4,
|
|
151
|
+
penwidth: 1.0,
|
|
152
|
+
extraStyle: "",
|
|
153
|
+
},
|
|
134
154
|
settlement: {
|
|
135
155
|
shape: "ellipse",
|
|
136
156
|
fill: "#A5D6A7",
|
|
@@ -187,10 +207,12 @@ export const STYLE = Object.freeze({
|
|
|
187
207
|
const LEGEND = Object.freeze([
|
|
188
208
|
["world", "the root note"],
|
|
189
209
|
["region", "continent, region, sub-region"],
|
|
210
|
+
["district", "quarter, ward, shire"],
|
|
190
211
|
["settlement", "city, town, village"],
|
|
191
212
|
["site", "ruin, landmark, dungeon, wonder"],
|
|
192
213
|
["feature", "river, mountain range, sea"],
|
|
193
214
|
["structure", "a single building"],
|
|
215
|
+
["route", "road, pass, sea lane"],
|
|
194
216
|
["polity", "affiliation, dashed edge to its region"],
|
|
195
217
|
]);
|
|
196
218
|
|