@heroiclands/package-build 25.5.5 → 25.6.1
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 +25 -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/encoding-core.md +43 -5
- package/docs/guides/reference-pages.md +4 -1
- package/docs/guides/site.md +1 -1
- package/docs/reference/format-details.md +239 -194
- package/docs/reference/note-types.md +187 -174
- 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/encoding-template.mjs +28 -0
- package/engine/encoding-terms.mjs +20 -12
- package/engine/encoding.mjs +147 -17
- 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/content-scripts.d.mts +6 -0
- 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/encoding-lexicon.d.mts +6 -0
- package/types/engine/encoding-template.d.mts +6 -0
- package/types/engine/encoding.d.mts +12 -0
- 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
|
@@ -218,6 +218,7 @@ something to show:
|
|
|
218
218
|
| Within | `within` | a place | the places whose `data.parents` names it, a paragraph to each kind of place |
|
|
219
219
|
| Governed by | `governedby` | a place | the affiliation its `data.government` names, with that affiliation's kind |
|
|
220
220
|
| Governed places | `governedplaces` | an affiliation | the places whose `data.government` names it, a paragraph to each kind of place |
|
|
221
|
+
| Office holders | `officeholders` | an affiliation | each office a living being holds, one line each, with its holders |
|
|
221
222
|
| Chronology | `chronology` | a place | the [event views](format-details.md#event-views) |
|
|
222
223
|
| Events | `events` | a being, an affiliation or a people | the event views |
|
|
223
224
|
| Accounts | `accounts` | an affiliation, a place or a people | the event views |
|
|
@@ -96,6 +96,7 @@ types:
|
|
|
96
96
|
- durability
|
|
97
97
|
- made
|
|
98
98
|
- lost
|
|
99
|
+
- unique
|
|
99
100
|
subTypes: []
|
|
100
101
|
armorlocation:
|
|
101
102
|
data:
|
|
@@ -114,6 +115,7 @@ types:
|
|
|
114
115
|
- durability
|
|
115
116
|
- made
|
|
116
117
|
- lost
|
|
118
|
+
- unique
|
|
117
119
|
- quantity
|
|
118
120
|
- potency
|
|
119
121
|
- strength
|
|
@@ -130,6 +132,7 @@ types:
|
|
|
130
132
|
- durability
|
|
131
133
|
- made
|
|
132
134
|
- lost
|
|
135
|
+
- unique
|
|
133
136
|
- capacity
|
|
134
137
|
subTypes: []
|
|
135
138
|
miscgear:
|
|
@@ -141,6 +144,7 @@ types:
|
|
|
141
144
|
- durability
|
|
142
145
|
- made
|
|
143
146
|
- lost
|
|
147
|
+
- unique
|
|
144
148
|
- quantity
|
|
145
149
|
subTypes: []
|
|
146
150
|
mystery:
|
|
@@ -189,6 +193,7 @@ types:
|
|
|
189
193
|
- durability
|
|
190
194
|
- made
|
|
191
195
|
- lost
|
|
196
|
+
- unique
|
|
192
197
|
- quantity
|
|
193
198
|
subTypes:
|
|
194
199
|
- none
|
|
@@ -268,6 +273,7 @@ types:
|
|
|
268
273
|
- durability
|
|
269
274
|
- made
|
|
270
275
|
- lost
|
|
276
|
+
- unique
|
|
271
277
|
subTypes: []
|
|
272
278
|
lore:
|
|
273
279
|
data:
|
|
@@ -323,6 +329,7 @@ types:
|
|
|
323
329
|
- culture
|
|
324
330
|
- population
|
|
325
331
|
- government
|
|
332
|
+
- settlements
|
|
326
333
|
- market
|
|
327
334
|
- borders
|
|
328
335
|
- routes
|
|
@@ -343,10 +350,12 @@ types:
|
|
|
343
350
|
subTypes:
|
|
344
351
|
- world
|
|
345
352
|
- region
|
|
353
|
+
- district
|
|
346
354
|
- settlement
|
|
347
355
|
- site
|
|
348
356
|
- structure
|
|
349
357
|
- feature
|
|
358
|
+
- route
|
|
350
359
|
- celestial
|
|
351
360
|
scenario:
|
|
352
361
|
data:
|
|
@@ -355,7 +364,6 @@ types:
|
|
|
355
364
|
- cast
|
|
356
365
|
- factions
|
|
357
366
|
- follows
|
|
358
|
-
- status
|
|
359
367
|
- party.size
|
|
360
368
|
- party.archetypes
|
|
361
369
|
subTypes:
|
|
@@ -740,12 +748,6 @@ vocabularies:
|
|
|
740
748
|
- "4"
|
|
741
749
|
- "5"
|
|
742
750
|
- "6"
|
|
743
|
-
holdingModel:
|
|
744
|
-
- held
|
|
745
|
-
- endowed
|
|
746
|
-
- registered
|
|
747
|
-
- kin
|
|
748
|
-
- polis
|
|
749
751
|
bearing:
|
|
750
752
|
- N
|
|
751
753
|
- NE
|
package/engine/content-html.mjs
CHANGED
|
@@ -64,7 +64,8 @@ import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
|
|
|
64
64
|
* markdown's own angle brackets out of it: an autolink is `<https://…>`, and
|
|
65
65
|
* `https` is followed by `:` rather than whitespace or `>`, so the pattern
|
|
66
66
|
* stops. A comparison written in prose — `a < b` — has no name after the
|
|
67
|
-
* bracket at all.
|
|
67
|
+
* bracket at all. A doubled `<<` opens a quotation, so its second bracket
|
|
68
|
+
* never starts an HTML tag.
|
|
68
69
|
*
|
|
69
70
|
* Attributes are consumed as "anything but a bracket", deliberately loosely: a
|
|
70
71
|
* finding names the tag, and a pattern that tried to parse attribute syntax
|
|
@@ -73,7 +74,7 @@ import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
|
|
|
73
74
|
*
|
|
74
75
|
* @type {RegExp}
|
|
75
76
|
*/
|
|
76
|
-
export const HTML_TAG =
|
|
77
|
+
export const HTML_TAG = /(?<!<)<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^<>]*)?\/?>/g;
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
80
|
* What a note carrying raw HTML is told.
|
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);
|
|
@@ -21,6 +21,12 @@ const scalar = (value) =>
|
|
|
21
21
|
!/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
|
|
22
22
|
const base = (value) => value.normalize("NFD").replace(/\p{M}/gu, "");
|
|
23
23
|
const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
24
|
+
/** Each diaeresis vowel and the plain vowel whose mapping writes it. */
|
|
25
|
+
const DIAERESIS_VOWELS = Object.freeze(
|
|
26
|
+
Object.fromEntries(
|
|
27
|
+
[..."aeiouAEIOU"].map((plain) => [(plain + "\u0308").normalize("NFC"), plain]),
|
|
28
|
+
),
|
|
29
|
+
);
|
|
24
30
|
const codeName = /^[A-Za-z0-9_]+$/;
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -339,12 +345,27 @@ export function templateProgram(data, options, fault) {
|
|
|
339
345
|
]),
|
|
340
346
|
];
|
|
341
347
|
if (!Array.isArray(accepts)) fault("accepts", "accepts must be a list of characters or ranges");
|
|
348
|
+
// A diaeresis vowel no source names is written with its plain vowel's rows,
|
|
349
|
+
// and decoding restores it by rule.
|
|
350
|
+
const named = [...mappings.map(([from]) => from), ...contextual.map((r) => r.from)];
|
|
351
|
+
const derived =
|
|
352
|
+
rules.diaeresis?.allowed === true ?
|
|
353
|
+
Object.fromEntries(
|
|
354
|
+
Object.entries(DIAERESIS_VOWELS).filter(
|
|
355
|
+
([letter, plain]) =>
|
|
356
|
+
named.includes(plain) &&
|
|
357
|
+
!named.some((from) => from.includes(letter)) &&
|
|
358
|
+
!separators.some((row) => row.from.includes(letter)),
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
: undefined;
|
|
342
362
|
return {
|
|
343
363
|
definition: {
|
|
344
364
|
name: "encoding-note",
|
|
345
365
|
fonts: [data.font],
|
|
346
366
|
accepts: [
|
|
347
367
|
...accepts,
|
|
368
|
+
...Object.keys(derived ?? {}),
|
|
348
369
|
...STANDARD_ROWS.flatMap((r) => [...(Array.isArray(r) ? r[0] : r.from)]),
|
|
349
370
|
],
|
|
350
371
|
separator: " ",
|
|
@@ -366,6 +387,13 @@ export function templateProgram(data, options, fault) {
|
|
|
366
387
|
maxVowelGroup: Math.max(0, ...vowelGroups.map((group) => [...group].length)),
|
|
367
388
|
stress,
|
|
368
389
|
diaeresis: rules.diaeresis?.allowed === false,
|
|
390
|
+
apart:
|
|
391
|
+
derived ?
|
|
392
|
+
Object.freeze({
|
|
393
|
+
letters: Object.freeze(derived),
|
|
394
|
+
groups: Object.freeze(vowelGroups),
|
|
395
|
+
})
|
|
396
|
+
: undefined,
|
|
369
397
|
grave: rules["grave-accent"]?.allowed === false,
|
|
370
398
|
}),
|
|
371
399
|
},
|
|
@@ -89,18 +89,26 @@ export const ENCODING_RULE_FIELDS = Object.freeze([
|
|
|
89
89
|
describe: "The complete authored group inventory.",
|
|
90
90
|
},
|
|
91
91
|
]),
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
block("diaeresis", "Whether diaeresis vowels are allowed, and how they are written.", [
|
|
93
|
+
{
|
|
94
|
+
name: "allowed",
|
|
95
|
+
kind: "boolean",
|
|
96
|
+
shape: "true or false",
|
|
97
|
+
required: true,
|
|
98
|
+
describe:
|
|
99
|
+
"False forbids the diaeresis; true derives plain-vowel mappings and restores splits where a declared group's native mapping distinguishes joined and separate vowels. Exact conversion refuses an unrecoverable derived group split; canonical conversion keeps its conventional reading.",
|
|
100
|
+
},
|
|
101
|
+
]),
|
|
102
|
+
block("grave-accent", "Whether grave-accent mappings are allowed.", [
|
|
103
|
+
{
|
|
104
|
+
name: "allowed",
|
|
105
|
+
kind: "boolean",
|
|
106
|
+
shape: "true or false",
|
|
107
|
+
required: true,
|
|
108
|
+
describe:
|
|
109
|
+
"False forbids the accent; true permits explicitly mapped and accepted characters.",
|
|
110
|
+
},
|
|
111
|
+
]),
|
|
104
112
|
{
|
|
105
113
|
name: "contextual-mappings",
|
|
106
114
|
kind: "list",
|