@heroiclands/package-build 22.25.0 → 22.27.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 +17 -0
- package/README.md +4 -0
- package/assets/styles/content-base.css +46 -3
- package/docs/api.md +35 -12
- package/docs/authoring/frontmatter.md +21 -0
- package/docs/authoring/links-and-markup.md +96 -149
- package/docs/content-format.md +6 -2
- package/docs/guides/government-migration.md +52 -0
- package/docs/guides/site.md +5 -5
- package/docs/reference/format-details.md +152 -246
- package/docs/reference/note-types.md +27 -28
- package/docs/reference/theme-pages.md +33 -29
- package/engine/anchors.mjs +85 -13
- package/engine/art-fields.mjs +1 -1
- package/engine/base-compiler.mjs +9 -1
- package/engine/bundles.mjs +1 -1
- package/engine/calendar-notes.mjs +1 -1
- package/engine/code-fences.mjs +6 -2
- package/engine/content-alerts.mjs +195 -0
- package/engine/content-blocks.mjs +119 -241
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-figures.mjs +166 -230
- package/engine/content-format-check.mjs +3 -2
- package/engine/content-format.yaml +1 -5
- package/engine/content-images.mjs +3 -3
- package/engine/content-poetry.mjs +141 -0
- package/engine/content-spans.mjs +177 -0
- package/engine/foundry-entries.mjs +2 -0
- package/engine/frontmatter-lint.mjs +2 -1
- package/engine/heading-attributes.mjs +1 -1
- package/engine/helpers.mjs +8 -3
- package/engine/holdings.mjs +197 -333
- package/engine/index.mjs +1 -1
- package/engine/infobox.mjs +11 -1
- package/engine/journals.mjs +8 -6
- package/engine/markdown-expressions.mjs +9 -3
- package/engine/metadata-index.mjs +5 -3
- package/engine/note-vocabulary.mjs +30 -13
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-render.mjs +90 -31
- package/engine/populations.mjs +17 -83
- package/engine/site-build.mjs +44 -16
- package/engine/site-preview.mjs +22 -6
- package/engine/wikilinks.mjs +18 -4
- package/hugo-theme/layouts/partials/holdings/entry.html +2 -2
- package/hugo-theme/layouts/partials/holdings.html +14 -14
- package/hugo-theme/static/css/style.css +40 -2
- package/package.json +1 -1
- package/sohl/item-fields.mjs +0 -16
- package/types/engine/anchors.d.mts +13 -4
- package/types/engine/art-fields.d.mts +1 -1
- package/types/engine/calendar-notes.d.mts +1 -1
- package/types/engine/code-fences.d.mts +3 -1
- package/types/engine/content-alerts.d.mts +62 -0
- package/types/engine/content-blocks.d.mts +3 -10
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-figures.d.mts +13 -20
- package/types/engine/content-images.d.mts +1 -1
- package/types/engine/content-poetry.d.mts +32 -0
- package/types/engine/content-spans.d.mts +18 -0
- package/types/engine/holdings.d.mts +75 -118
- package/types/engine/infobox.d.mts +1 -0
- package/types/engine/markdown-expressions.d.mts +1 -1
- package/types/engine/note-vocabulary.d.mts +14 -2
- package/types/engine/populations.d.mts +4 -13
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/wikilinks.d.mts +5 -1
package/engine/holdings.mjs
CHANGED
|
@@ -12,195 +12,114 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `contains` and `held_by` on a place, `holdings` on an affiliation, each
|
|
22
|
-
* shaped like an entry of `related` and absent where empty.
|
|
23
|
-
*
|
|
24
|
-
* **`domains` names what an affiliation holds directly**, and is never
|
|
25
|
-
* expanded. A polity whose `domains` names a region holds the region; the
|
|
26
|
-
* settlements within it are reachable from the region's `contains`, and
|
|
27
|
-
* repeating them in `holdings` would make every list say the same thing at
|
|
28
|
-
* every level. Tenure below a lord runs through `affiliation.parents` — a
|
|
29
|
-
* house of its earl, an earl of the crown — and geography through
|
|
30
|
-
* `place.parents`, so a subinfeudated manor sits in one region by the first
|
|
31
|
-
* and under a lord of another polity by the second, and both pages say so.
|
|
32
|
-
*
|
|
33
|
-
* **A dependency's places and affiliations take part.** A fetched index entry
|
|
34
|
-
* carries the `parents` and `domains` its record stated, so a place another
|
|
35
|
-
* package publishes is listed within a local region, and a house another
|
|
36
|
-
* package publishes is named on the local manor it holds.
|
|
37
|
-
*
|
|
38
|
-
* **A place's tenure is checked.** A settlement, a site or a structure that no
|
|
39
|
-
* affiliation's `domains` names is land nobody holds — a gap in tenure the
|
|
40
|
-
* lint reports as a warning at the note's `type:` line. A region is held
|
|
41
|
-
* through its polity's `domains` and a feature by nobody, so both are exempt.
|
|
42
|
-
*
|
|
15
|
+
* Derive containment and government lists from place facts. Geography comes
|
|
16
|
+
* only from `data.parents`; government comes only from `data.government`.
|
|
17
|
+
* Affiliation domains never supply government, and neither graph expands
|
|
18
|
+
* through geographic or affiliation ancestors. Government identity preserves the Address package, type and shortcode.
|
|
19
|
+
* Its system normalizes to `note` because Item and journal documents share one page.
|
|
20
|
+
* Historical holdings API names remain for callers migrating their imports.
|
|
43
21
|
* @module
|
|
44
22
|
*/
|
|
45
|
-
|
|
46
|
-
import {
|
|
47
|
-
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
48
|
-
import { parseAddress } from "./address.mjs";
|
|
23
|
+
import { isAddressTuple, ownDocumentSystem, parseAddress, renderAddress } from "./address.mjs";
|
|
24
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
49
25
|
import { readCanonicalKey } from "./content-address.mjs";
|
|
50
26
|
|
|
51
|
-
/**
|
|
52
|
-
* The one type a `parents` or `domains` entry may name — the default
|
|
53
|
-
* {@link module:engine/address.parseAddress} fills in when the segment is
|
|
54
|
-
* omitted, and the whole of what this position accepts.
|
|
55
|
-
*
|
|
56
|
-
* @type {ReadonlySet<string>}
|
|
57
|
-
*/
|
|
58
|
-
const PLACE_TYPES = Object.freeze(new Set(["place"]));
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The keys this module writes, and which a note cannot author: `contains`
|
|
62
|
-
* and `held_by` on a place, `holdings` on an affiliation.
|
|
63
|
-
*
|
|
27
|
+
/** Derived keys, including retired keys removed from generated pages.
|
|
64
28
|
* @type {readonly string[]}
|
|
65
29
|
*/
|
|
66
|
-
export const HOLDINGS_KEYS = Object.freeze([
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
30
|
+
export const HOLDINGS_KEYS = Object.freeze([
|
|
31
|
+
"contains",
|
|
32
|
+
"governed_by",
|
|
33
|
+
"governed_places",
|
|
34
|
+
"held_by",
|
|
35
|
+
"holdings",
|
|
36
|
+
]);
|
|
37
|
+
/** @deprecated Government advisories apply to every inhabited place subtype.
|
|
73
38
|
* @type {readonly string[]}
|
|
74
39
|
*/
|
|
75
40
|
export const HELD_SUBTYPES = Object.freeze(["settlement", "site", "structure"]);
|
|
76
41
|
|
|
42
|
+
/** @typedef {{title:string,url?:string,type:string,subType?:string}} HoldingsEntry */
|
|
77
43
|
/**
|
|
78
|
-
* One entry of a holdings list — a page a reader is pointed at.
|
|
79
|
-
*
|
|
80
|
-
* @typedef {object} HoldingsEntry
|
|
81
|
-
* @property {string} title The page's published title.
|
|
82
|
-
* @property {string} [url] `<base><slug>/` — the page's address as every
|
|
83
|
-
* href this build renders composes it. **Absent
|
|
84
|
-
* for a stub**, which has no page: an entry with no
|
|
85
|
-
* `url` renders as plain text.
|
|
86
|
-
* @property {string} type The note's `type` — `place` or `affiliation`.
|
|
87
|
-
* @property {string} [subType] The note's `subType`, where it declares one.
|
|
88
|
-
*/
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* One place or affiliation, as the derivation reads it — a local page or a
|
|
92
|
-
* fetched index entry through one shape.
|
|
93
|
-
*
|
|
94
44
|
* @typedef {object} HoldingsNode
|
|
95
|
-
* @property {string} shortcode
|
|
45
|
+
* @property {string} shortcode
|
|
96
46
|
* @property {"place"|"affiliation"} type
|
|
97
47
|
* @property {string} [subType]
|
|
98
|
-
* @property {string} title
|
|
99
|
-
* @property {string} [url]
|
|
100
|
-
*
|
|
101
|
-
* @property {string[]
|
|
102
|
-
* @property {string[]
|
|
48
|
+
* @property {string} title
|
|
49
|
+
* @property {string} [url]
|
|
50
|
+
* @property {string} [package] Owning content package.
|
|
51
|
+
* @property {string} [system] Own document system.
|
|
52
|
+
* @property {string} [canonical] Full own Address.
|
|
53
|
+
* @property {string} [governmentSystem] Affiliation reference default system.
|
|
54
|
+
* @property {unknown[]} parents
|
|
55
|
+
* @property {unknown} [government] Null is explicit anarchy; absence is omission.
|
|
103
56
|
*/
|
|
57
|
+
/** @typedef {{contains?:HoldingsEntry[],governed_by?:HoldingsEntry[],governed_places?:HoldingsEntry[]}} Holdings */
|
|
104
58
|
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* @typedef {object} Holdings
|
|
110
|
-
* @property {HoldingsEntry[]} [contains] Places whose `parents` name this one.
|
|
111
|
-
* @property {HoldingsEntry[]} [held_by] Affiliations whose `domains` name this one.
|
|
112
|
-
* @property {HoldingsEntry[]} [holdings] Places this affiliation's `domains` name.
|
|
113
|
-
*/
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The shortcode a `parents` or `domains` entry names, lower case.
|
|
117
|
-
*
|
|
118
|
-
* **A `parents` or `domains` entry is an Address** — the specification types
|
|
119
|
-
* both `Address[]`, targeting `place` — read at whatever length says what it
|
|
120
|
-
* means, `place` the default and the whole of the accepted set. Frontmatter
|
|
121
|
-
* never holds a Wikilink, so no bracket, `|` or `#` is stripped here; a
|
|
122
|
-
* qualified form is read structurally by {@link readCanonicalKey} for its
|
|
123
|
-
* literal four segments, since this position asks neither its package nor its
|
|
124
|
-
* system, only the shortcode {@link module:engine/address} would resolve it
|
|
125
|
-
* to anyway.
|
|
126
|
-
*
|
|
127
|
-
* **The holdings graph has package-blind Shortcode identity.** `places` and
|
|
128
|
-
* `affiliations` merge local and fetched notes into one shortcode space; the
|
|
129
|
-
* first declaration wins. A fetched place attaches to a local parent with
|
|
130
|
-
* the same shortcode, and a fetched affiliation can name both local and
|
|
131
|
-
* fetched domains. The authored entries remain Addresses; this graph uses
|
|
132
|
-
* their shortcode projection for containment and tenure. See
|
|
133
|
-
* `tests/site-holdings.test.ts`'s `empire.domains: ["mill", "abroad"]`, where
|
|
134
|
-
* `mill` is local and `abroad` is the dependency's own, both bare, both
|
|
135
|
-
* correct only because neither package is asked.
|
|
136
|
-
*
|
|
137
|
-
* @param {unknown} value - One entry.
|
|
138
|
-
* @returns {string} Its shortcode, or `""` for an entry that names nothing.
|
|
139
|
-
*/
|
|
140
|
-
function namedShortcode(value) {
|
|
141
|
-
if (isAddressTuple(value)) return value.shortcode.toLowerCase();
|
|
142
|
-
const text = String(value ?? "").trim();
|
|
143
|
-
if (!text) return "";
|
|
144
|
-
const qualified = readCanonicalKey(text);
|
|
145
|
-
if (qualified) return qualified.shortcode.toLowerCase();
|
|
146
|
-
const read = parseAddress(text, { type: "place", types: PLACE_TYPES });
|
|
147
|
-
return read.reason ? "" : read.shortcode.toLowerCase();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* A `parents` or `domains` value as the shortcodes it names, in order,
|
|
152
|
-
* empties dropped and repeats collapsed.
|
|
153
|
-
*
|
|
154
|
-
* @param {unknown} value - The authored value — a list, or a lone entry.
|
|
155
|
-
* @returns {string[]} The shortcodes.
|
|
59
|
+
/** Historical shortcode projection for callers that explicitly need one.
|
|
60
|
+
* @param {unknown} value
|
|
61
|
+
* @returns {string[]}
|
|
156
62
|
*/
|
|
157
63
|
export function shortcodesOf(value) {
|
|
158
64
|
const list =
|
|
159
65
|
Array.isArray(value) ? value
|
|
160
66
|
: value ? [value]
|
|
161
67
|
: [];
|
|
162
|
-
return [
|
|
68
|
+
return [
|
|
69
|
+
...new Set(
|
|
70
|
+
list
|
|
71
|
+
.map((one) => {
|
|
72
|
+
if (isAddressTuple(one)) return one.shortcode.toLowerCase();
|
|
73
|
+
const canonical = readCanonicalKey(String(one ?? "").trim());
|
|
74
|
+
if (canonical) return canonical.shortcode.toLowerCase();
|
|
75
|
+
const tuple = parseAddress(one, { type: "place", types: new Set(["place"]) });
|
|
76
|
+
return tuple.reason ? "" : tuple.shortcode.toLowerCase();
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean),
|
|
79
|
+
),
|
|
80
|
+
];
|
|
163
81
|
}
|
|
164
82
|
|
|
165
|
-
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* @
|
|
170
|
-
* @param {HoldingsEntry} b
|
|
171
|
-
* @returns {number}
|
|
83
|
+
/** Resolve in the citing node's package, never another package's namespace.
|
|
84
|
+
* @param {unknown} value
|
|
85
|
+
* @param {HoldingsNode} node
|
|
86
|
+
* @param {"place"|"affiliation"} type
|
|
87
|
+
* @returns {string}
|
|
172
88
|
*/
|
|
173
|
-
function
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
89
|
+
function referenceKey(value, node, type) {
|
|
90
|
+
if (value == null) return "";
|
|
91
|
+
const tuple = parseAddress(
|
|
92
|
+
value,
|
|
93
|
+
{
|
|
94
|
+
package: node.package ?? "local",
|
|
95
|
+
system:
|
|
96
|
+
type === "place" ? "note" : (
|
|
97
|
+
(node.governmentSystem ?? ownDocumentSystem("affiliation"))
|
|
98
|
+
),
|
|
99
|
+
type,
|
|
100
|
+
types: new Set([type]),
|
|
101
|
+
},
|
|
102
|
+
{ declared: true, legacyShortcodeCase: true },
|
|
177
103
|
);
|
|
104
|
+
if (tuple.reason || tuple.type !== type) return "";
|
|
105
|
+
return renderAddress({ ...tuple, system: "note" }).toLowerCase();
|
|
178
106
|
}
|
|
179
107
|
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
108
|
+
/** @param {HoldingsNode} node @returns {string} */
|
|
109
|
+
function ownKey(node) {
|
|
110
|
+
const tuple = node.canonical ? readCanonicalKey(node.canonical) : node;
|
|
111
|
+
return renderAddress({
|
|
112
|
+
package: tuple.package ?? "local",
|
|
113
|
+
system: "note",
|
|
114
|
+
type: node.type,
|
|
115
|
+
shortcode: node.shortcode.toLowerCase(),
|
|
116
|
+
});
|
|
189
117
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
* emitted all the same. A stub is a place somebody has not written yet: it
|
|
196
|
-
* carries its facts, and Weyshott belongs in Aelwyth's `contains` whether or
|
|
197
|
-
* not anyone has written Weyshott's page. What it cannot have is a link, so
|
|
198
|
-
* the renderer prints the title as plain text — the same rule a table cell
|
|
199
|
-
* follows when its `_ref` is null, in a different renderer.
|
|
200
|
-
*
|
|
201
|
-
* @param {HoldingsNode} node - The node.
|
|
202
|
-
* @returns {HoldingsEntry} Its entry.
|
|
203
|
-
*/
|
|
118
|
+
/** @param {HoldingsNode} node @returns {boolean} */
|
|
119
|
+
function listed(node) {
|
|
120
|
+
return node.url !== undefined && node.url !== null && node.url !== "";
|
|
121
|
+
}
|
|
122
|
+
/** @param {HoldingsNode} node @returns {HoldingsEntry} */
|
|
204
123
|
function entryOf(node) {
|
|
205
124
|
return {
|
|
206
125
|
title: node.title,
|
|
@@ -209,35 +128,20 @@ function entryOf(node) {
|
|
|
209
128
|
...(node.subType === undefined ? {} : { subType: node.subType }),
|
|
210
129
|
};
|
|
211
130
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
* does not exist, so this guards {@link holdingsPages}'s `on(url)` calls — and
|
|
219
|
-
* nothing else. Guarding the membership too is how a stub vanishes from every
|
|
220
|
-
* containment and tenure list in the corpus, silently, which is the one failure
|
|
221
|
-
* mode a diff cannot show.
|
|
222
|
-
*
|
|
223
|
-
* @param {HoldingsNode} node - The node.
|
|
224
|
-
* @returns {boolean} Whether it has a page to write a list on.
|
|
225
|
-
*/
|
|
226
|
-
function listed(node) {
|
|
227
|
-
return node.url !== undefined && node.url !== null && node.url !== "";
|
|
131
|
+
/** @param {HoldingsEntry} a @param {HoldingsEntry} b @returns {number} */
|
|
132
|
+
function bySubTypeThenTitle(a, b) {
|
|
133
|
+
return (
|
|
134
|
+
(a.subType ?? "").localeCompare(b.subType ?? "", "en") ||
|
|
135
|
+
a.title.localeCompare(b.title, "en")
|
|
136
|
+
);
|
|
228
137
|
}
|
|
229
138
|
|
|
230
|
-
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* @
|
|
234
|
-
* @param {object} page - The page it publishes as.
|
|
235
|
-
* @param {string} page.title - Its published title.
|
|
236
|
-
* @param {string} page.url - Its URL.
|
|
237
|
-
* @returns {HoldingsNode|null} The node, or `null` for a note that is neither
|
|
238
|
-
* a place nor an affiliation, or that declares no shortcode.
|
|
139
|
+
/** Read local facts without converting government omission into anarchy.
|
|
140
|
+
* @param {object} fm
|
|
141
|
+
* @param {{title:string,url?:string,package?:string,system?:string,governmentSystem?:string}} page
|
|
142
|
+
* @returns {HoldingsNode|null}
|
|
239
143
|
*/
|
|
240
|
-
export function holdingsNode(fm, { title, url }) {
|
|
144
|
+
export function holdingsNode(fm, { title, url, package: pkg, system, governmentSystem }) {
|
|
241
145
|
const type = String(fm?.type ?? "");
|
|
242
146
|
if (type !== "place" && type !== "affiliation") return null;
|
|
243
147
|
const shortcode = String(fm?.shortcode ?? "").toLowerCase();
|
|
@@ -249,123 +153,110 @@ export function holdingsNode(fm, { title, url }) {
|
|
|
249
153
|
subType: fm.subType == null ? undefined : String(fm.subType),
|
|
250
154
|
title,
|
|
251
155
|
url,
|
|
252
|
-
|
|
253
|
-
|
|
156
|
+
...(pkg ? { package: pkg } : {}),
|
|
157
|
+
...(system ? { system } : {}),
|
|
158
|
+
...(governmentSystem ? { governmentSystem } : {}),
|
|
159
|
+
parents:
|
|
160
|
+
type === "place" ?
|
|
161
|
+
Array.isArray(data.parents) ? data.parents
|
|
162
|
+
: data.parents ? [data.parents]
|
|
163
|
+
: []
|
|
164
|
+
: [],
|
|
165
|
+
...(type === "place" && Object.hasOwn(data, "government") ?
|
|
166
|
+
{ government: data.government }
|
|
167
|
+
: {}),
|
|
254
168
|
};
|
|
255
169
|
}
|
|
256
170
|
|
|
257
|
-
/**
|
|
258
|
-
* Read a fetched index's places and affiliations as nodes.
|
|
259
|
-
*
|
|
260
|
-
* An entry carries what the producer's record stated — its name, `subType`,
|
|
261
|
-
* `parents` and `domains` — and its page URL where the package publishes one.
|
|
262
|
-
*
|
|
263
|
-
* @param {Map<string, object>|undefined} foreignIndex - As
|
|
264
|
-
* {@link module:engine/metadata-index.loadForeignIndexes} returns it.
|
|
265
|
-
* @returns {HoldingsNode[]} The nodes, in index order.
|
|
266
|
-
*/
|
|
171
|
+
/** @param {Map<string,object>|undefined} foreignIndex @returns {HoldingsNode[]} */
|
|
267
172
|
export function foreignHoldingsNodes(foreignIndex) {
|
|
268
173
|
const out = [];
|
|
269
|
-
|
|
270
|
-
|
|
174
|
+
for (const [canonical, entry] of foreignIndex ?? []) {
|
|
175
|
+
const tuple = readCanonicalKey(canonical);
|
|
271
176
|
const type = String(entry?.type ?? "");
|
|
272
|
-
if (type !== "place" && type !== "affiliation") continue;
|
|
273
|
-
const shortcode = readCanonicalKey(canonical)?.shortcode?.toLowerCase() ?? "";
|
|
274
|
-
if (!shortcode) continue;
|
|
177
|
+
if (!tuple || (type !== "place" && type !== "affiliation")) continue;
|
|
275
178
|
out.push({
|
|
276
|
-
|
|
179
|
+
canonical,
|
|
180
|
+
package: tuple.package,
|
|
181
|
+
system: tuple.system,
|
|
182
|
+
shortcode: tuple.shortcode.toLowerCase(),
|
|
277
183
|
type,
|
|
278
184
|
subType: entry.subType == null ? undefined : String(entry.subType),
|
|
279
|
-
title: String(entry.name ?? shortcode),
|
|
185
|
+
title: String(entry.name ?? tuple.shortcode),
|
|
280
186
|
url: entry.url,
|
|
281
|
-
parents:
|
|
282
|
-
|
|
187
|
+
parents:
|
|
188
|
+
type === "place" ?
|
|
189
|
+
Array.isArray(entry.parents) ? entry.parents
|
|
190
|
+
: entry.parents ? [entry.parents]
|
|
191
|
+
: []
|
|
192
|
+
: [],
|
|
193
|
+
...(type === "place" && Object.hasOwn(entry, "government") ?
|
|
194
|
+
{ government: entry.government }
|
|
195
|
+
: {}),
|
|
283
196
|
});
|
|
284
197
|
}
|
|
285
198
|
return out;
|
|
286
199
|
}
|
|
287
200
|
|
|
288
|
-
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* dropped: a `parents` naming nowhere is the map's finding, a `domains`
|
|
297
|
-
* naming nowhere the reference check's.
|
|
298
|
-
*
|
|
299
|
-
* The result holds only pages with at least one entry on at least one list,
|
|
300
|
-
* and a page's block carries only the keys it has entries for — the theme's
|
|
301
|
-
* silent-disappear convention, kept at the source.
|
|
302
|
-
*
|
|
303
|
-
* @param {Iterable<HoldingsNode|null>} nodes - Every place and affiliation,
|
|
304
|
-
* local first. `parents` and `domains` are read as written, so a node
|
|
305
|
-
* built by hand may carry them as the note spells them.
|
|
306
|
-
* @returns {Map<string, Holdings>} URL → the page's lists, sorted.
|
|
201
|
+
/** Invert direct place facts. A stub remains a plain text entry; a stub's own
|
|
202
|
+
* absent page receives no lists. First declaration of a page Address wins;
|
|
203
|
+
* different document systems for one page normalize to its `note` identity.
|
|
204
|
+
* @param {Iterable<HoldingsNode|null>} nodes
|
|
205
|
+
* @param {object} [options]
|
|
206
|
+
* @param {Iterable<HoldingsNode|null>} [options.governmentNodes] Additional public
|
|
207
|
+
* records without pages, participating only in government relationships.
|
|
208
|
+
* @returns {Map<string,Holdings>}
|
|
307
209
|
*/
|
|
308
|
-
export function holdingsPages(nodes) {
|
|
309
|
-
/** @type {Map<string, HoldingsNode>} */
|
|
210
|
+
export function holdingsPages(nodes, { governmentNodes = [] } = {}) {
|
|
310
211
|
const places = new Map();
|
|
311
|
-
/** @type {Map<string, HoldingsNode>} */
|
|
312
212
|
const affiliations = new Map();
|
|
213
|
+
const geographicPlaces = new Map();
|
|
313
214
|
for (const node of nodes) {
|
|
314
|
-
if (!node) continue;
|
|
215
|
+
if (!node || !node.shortcode) continue;
|
|
315
216
|
const by = node.type === "place" ? places : affiliations;
|
|
316
|
-
const
|
|
317
|
-
if (!
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
217
|
+
const key = ownKey(node);
|
|
218
|
+
if (!by.has(key)) by.set(key, node);
|
|
219
|
+
if (node.type === "place" && !geographicPlaces.has(node.shortcode.toLowerCase())) {
|
|
220
|
+
// Shortcode identity: geography keeps its historical first-declaration rule.
|
|
221
|
+
geographicPlaces.set(node.shortcode.toLowerCase(), node);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Local records with no published page still state government. They must
|
|
225
|
+
// not add geographic edges: containment keeps its existing page graph.
|
|
226
|
+
for (const node of governmentNodes) {
|
|
227
|
+
if (!node || !node.shortcode) continue;
|
|
228
|
+
const by = node.type === "place" ? places : affiliations;
|
|
229
|
+
const key = ownKey(node);
|
|
230
|
+
if (!by.has(key)) by.set(key, node);
|
|
328
231
|
}
|
|
329
|
-
|
|
330
|
-
/** @type {Map<string, {contains: HoldingsEntry[], held_by: HoldingsEntry[], holdings: HoldingsEntry[]}>} */
|
|
331
232
|
const lists = new Map();
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
233
|
+
const add = (node, key, entry) => {
|
|
234
|
+
if (!listed(node)) return;
|
|
235
|
+
let block = lists.get(node.url);
|
|
236
|
+
if (!block) {
|
|
237
|
+
block = {};
|
|
238
|
+
lists.set(node.url, block);
|
|
337
239
|
}
|
|
338
|
-
|
|
240
|
+
(block[key] ??= []).push(entryOf(entry));
|
|
339
241
|
};
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (!parent || !listed(parent) || parent === child) continue;
|
|
346
|
-
on(parent.url).contains.push(entryOf(child));
|
|
242
|
+
for (const child of geographicPlaces.values()) {
|
|
243
|
+
const parentKeys = shortcodesOf(child.parents);
|
|
244
|
+
for (const key of parentKeys) {
|
|
245
|
+
const parent = geographicPlaces.get(key);
|
|
246
|
+
if (parent && parent !== child) add(parent, "contains", child);
|
|
347
247
|
}
|
|
348
248
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (listed(holder)) on(holder.url).holdings.push(entryOf(held));
|
|
355
|
-
if (listed(held)) on(held.url).held_by.push(entryOf(holder));
|
|
356
|
-
}
|
|
249
|
+
for (const child of places.values()) {
|
|
250
|
+
const government = affiliations.get(referenceKey(child.government, child, "affiliation"));
|
|
251
|
+
if (!government) continue;
|
|
252
|
+
add(child, "governed_by", government);
|
|
253
|
+
add(government, "governed_places", child);
|
|
357
254
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
/** @type {Holdings} */
|
|
362
|
-
const block = {};
|
|
363
|
-
if (contains.length) block.contains = contains.sort(bySubTypeThenTitle);
|
|
364
|
-
if (held_by.length) block.held_by = held_by.sort(byTitle);
|
|
365
|
-
if (holdings.length) block.holdings = holdings.sort(bySubTypeThenTitle);
|
|
366
|
-
if (Object.keys(block).length) out.set(url, block);
|
|
255
|
+
for (const block of lists.values()) {
|
|
256
|
+
block.contains?.sort(bySubTypeThenTitle);
|
|
257
|
+
block.governed_places?.sort(bySubTypeThenTitle);
|
|
367
258
|
}
|
|
368
|
-
return
|
|
259
|
+
return lists;
|
|
369
260
|
}
|
|
370
261
|
|
|
371
262
|
/* --------------------------------------------------------------------- */
|
|
@@ -373,71 +264,44 @@ export function holdingsPages(nodes) {
|
|
|
373
264
|
/* --------------------------------------------------------------------- */
|
|
374
265
|
|
|
375
266
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* The shortcodes every affiliation's `domains` names, across the link index.
|
|
387
|
-
*
|
|
388
|
-
* @param {object} index - The link index.
|
|
389
|
-
* @returns {Set<string>} The held shortcodes, lower case.
|
|
267
|
+
* Advise when a place with an authored positive population omits government.
|
|
268
|
+
* Missing, null or zero population establishes no inhabited population.
|
|
269
|
+
* Property presence distinguishes omitted government from explicit null,
|
|
270
|
+
* which means complete anarchy. Non-null values are validated by the declared
|
|
271
|
+
* affiliation reference field. Neither geographic containment nor tenure
|
|
272
|
+
* supplies this fact, and the rule is independent of place subtype.
|
|
273
|
+
*
|
|
274
|
+
* @param {object} note - The note with parsed frontmatter and raw source.
|
|
275
|
+
* @returns {object[]} Located warning findings.
|
|
390
276
|
*/
|
|
391
|
-
function
|
|
392
|
-
let held = HELD.get(index);
|
|
393
|
-
if (held) return held;
|
|
394
|
-
held = new Set();
|
|
395
|
-
for (const note of index.notes ?? []) {
|
|
396
|
-
if (String(note.type ?? note.fm?.type ?? "") !== "affiliation") continue;
|
|
397
|
-
const data = note.fm?.data;
|
|
398
|
-
const domains = data && typeof data === "object" ? data.domains : undefined;
|
|
399
|
-
for (const shortcode of shortcodesOf(domains)) held.add(shortcode);
|
|
400
|
-
}
|
|
401
|
-
for (const entry of index.foreign?.index?.values?.() ?? []) {
|
|
402
|
-
if (String(entry?.type ?? "") !== "affiliation") continue;
|
|
403
|
-
for (const shortcode of shortcodesOf(entry.domains)) held.add(shortcode);
|
|
404
|
-
}
|
|
405
|
-
HELD.set(index, held);
|
|
406
|
-
return held;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Check a place note's tenure: a settlement, a site or a structure that no
|
|
411
|
-
* affiliation's `domains` names is unheld land, reported as a warning at the
|
|
412
|
-
* note's `type:` line.
|
|
413
|
-
*
|
|
414
|
-
* Declared on the `place` vocabulary as its type-level check, so the lint
|
|
415
|
-
* runs it beside the field checks with the same index. Without an index the
|
|
416
|
-
* question cannot be asked and nothing is reported.
|
|
417
|
-
*
|
|
418
|
-
* @param {object} note - The note, as the link index hands it over.
|
|
419
|
-
* @param {object} [opts]
|
|
420
|
-
* @param {object} [opts.index] - The link index, holding every affiliation.
|
|
421
|
-
* @returns {object[]} Findings.
|
|
422
|
-
*/
|
|
423
|
-
export function checkHeld(note, { index } = {}) {
|
|
424
|
-
if (!index) return [];
|
|
277
|
+
export function checkGovernment(note) {
|
|
425
278
|
const fm = note.fm ?? {};
|
|
426
|
-
if (
|
|
427
|
-
const
|
|
428
|
-
if (!
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
431
|
-
|
|
279
|
+
if (fm.type !== "place") return [];
|
|
280
|
+
const data = fm.data;
|
|
281
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
282
|
+
const population = data.population;
|
|
283
|
+
if (typeof population !== "number" && typeof population !== "string") return [];
|
|
284
|
+
const count = Number(population);
|
|
285
|
+
if (!Number.isFinite(count) || count <= 0 || Object.hasOwn(data, "government")) return [];
|
|
432
286
|
return [
|
|
433
287
|
{
|
|
434
288
|
file: note.file,
|
|
435
|
-
...
|
|
289
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "population"], { key: true }),
|
|
436
290
|
severity: "warning",
|
|
437
291
|
message:
|
|
438
|
-
`
|
|
439
|
-
|
|
440
|
-
`order or polity that does`,
|
|
292
|
+
`missing government: place "${fm.shortcode ?? ""}" has positive population ` +
|
|
293
|
+
"but no `data.government`; name an affiliation or write null for complete anarchy",
|
|
441
294
|
},
|
|
442
295
|
];
|
|
443
296
|
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The historical API name for the government advisory. Tenure is no longer checked.
|
|
300
|
+
* @deprecated Use checkGovernment.
|
|
301
|
+
* @param {object} note - The place note.
|
|
302
|
+
* @param {object} [_opts] - Historical options, no longer needed.
|
|
303
|
+
* @returns {object[]} Government advisory findings.
|
|
304
|
+
*/
|
|
305
|
+
export function checkHeld(note, _opts = {}) {
|
|
306
|
+
return checkGovernment(note);
|
|
307
|
+
}
|
package/engine/index.mjs
CHANGED
|
@@ -99,7 +99,7 @@ export * as stubLint from "./stub-lint.mjs";
|
|
|
99
99
|
/** What a place is next to and reachable from: the closed sets, and the checks. */
|
|
100
100
|
export * as placeRelations from "./place-relations.mjs";
|
|
101
101
|
|
|
102
|
-
/** What lies within a place
|
|
102
|
+
/** What lies within a place and who governs it — derived page lists and the government advisory. */
|
|
103
103
|
export * as holdings from "./holdings.mjs";
|
|
104
104
|
export * as populations from "./populations.mjs";
|
|
105
105
|
|