@heroiclands/package-build 22.26.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 +11 -0
- package/README.md +3 -1
- package/assets/styles/content-base.css +46 -3
- package/docs/api.md +33 -13
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +96 -149
- package/docs/content-format.md +5 -3
- package/docs/guides/government-migration.md +52 -0
- package/docs/guides/site.md +5 -5
- package/docs/reference/format-details.md +110 -219
- package/docs/reference/note-types.md +27 -29
- package/docs/reference/theme-pages.md +31 -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/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 +0 -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 +165 -273
- package/engine/index.mjs +1 -1
- package/engine/infobox.mjs +1 -1
- package/engine/journals.mjs +8 -6
- package/engine/markdown-expressions.mjs +9 -3
- package/engine/metadata-index.mjs +2 -3
- package/engine/note-vocabulary.mjs +16 -11
- 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/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 +59 -105
- package/types/engine/markdown-expressions.d.mts +1 -1
- package/types/engine/note-vocabulary.d.mts +10 -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,194 +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
|
-
* **Government is stated separately from tenure.** A place with positive
|
|
39
|
-
* `data.population` and no authored `data.government` receives an advisory.
|
|
40
|
-
* Explicit null means complete anarchy. Holdings never supply a government.
|
|
41
|
-
*
|
|
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.
|
|
42
21
|
* @module
|
|
43
22
|
*/
|
|
44
|
-
|
|
45
|
-
import { isAddressTuple } from "./address.mjs";
|
|
23
|
+
import { isAddressTuple, ownDocumentSystem, parseAddress, renderAddress } from "./address.mjs";
|
|
46
24
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
47
|
-
import { parseAddress } from "./address.mjs";
|
|
48
25
|
import { readCanonicalKey } from "./content-address.mjs";
|
|
49
26
|
|
|
50
|
-
/**
|
|
51
|
-
* The one type a `parents` or `domains` entry may name — the default
|
|
52
|
-
* {@link module:engine/address.parseAddress} fills in when the segment is
|
|
53
|
-
* omitted, and the whole of what this position accepts.
|
|
54
|
-
*
|
|
55
|
-
* @type {ReadonlySet<string>}
|
|
56
|
-
*/
|
|
57
|
-
const PLACE_TYPES = Object.freeze(new Set(["place"]));
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The keys this module writes, and which a note cannot author: `contains`
|
|
61
|
-
* and `held_by` on a place, `holdings` on an affiliation.
|
|
62
|
-
*
|
|
27
|
+
/** Derived keys, including retired keys removed from generated pages.
|
|
63
28
|
* @type {readonly string[]}
|
|
64
29
|
*/
|
|
65
|
-
export const HOLDINGS_KEYS = Object.freeze([
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
72
38
|
* @type {readonly string[]}
|
|
73
39
|
*/
|
|
74
40
|
export const HELD_SUBTYPES = Object.freeze(["settlement", "site", "structure"]);
|
|
75
41
|
|
|
42
|
+
/** @typedef {{title:string,url?:string,type:string,subType?:string}} HoldingsEntry */
|
|
76
43
|
/**
|
|
77
|
-
* One entry of a holdings list — a page a reader is pointed at.
|
|
78
|
-
*
|
|
79
|
-
* @typedef {object} HoldingsEntry
|
|
80
|
-
* @property {string} title The page's published title.
|
|
81
|
-
* @property {string} [url] `<base><slug>/` — the page's address as every
|
|
82
|
-
* href this build renders composes it. **Absent
|
|
83
|
-
* for a stub**, which has no page: an entry with no
|
|
84
|
-
* `url` renders as plain text.
|
|
85
|
-
* @property {string} type The note's `type` — `place` or `affiliation`.
|
|
86
|
-
* @property {string} [subType] The note's `subType`, where it declares one.
|
|
87
|
-
*/
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* One place or affiliation, as the derivation reads it — a local page or a
|
|
91
|
-
* fetched index entry through one shape.
|
|
92
|
-
*
|
|
93
44
|
* @typedef {object} HoldingsNode
|
|
94
|
-
* @property {string} shortcode
|
|
45
|
+
* @property {string} shortcode
|
|
95
46
|
* @property {"place"|"affiliation"} type
|
|
96
47
|
* @property {string} [subType]
|
|
97
|
-
* @property {string} title
|
|
98
|
-
* @property {string} [url]
|
|
99
|
-
*
|
|
100
|
-
* @property {string[]
|
|
101
|
-
* @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.
|
|
102
56
|
*/
|
|
57
|
+
/** @typedef {{contains?:HoldingsEntry[],governed_by?:HoldingsEntry[],governed_places?:HoldingsEntry[]}} Holdings */
|
|
103
58
|
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* @typedef {object} Holdings
|
|
109
|
-
* @property {HoldingsEntry[]} [contains] Places whose `parents` name this one.
|
|
110
|
-
* @property {HoldingsEntry[]} [held_by] Affiliations whose `domains` name this one.
|
|
111
|
-
* @property {HoldingsEntry[]} [holdings] Places this affiliation's `domains` name.
|
|
112
|
-
*/
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* The shortcode a `parents` or `domains` entry names, lower case.
|
|
116
|
-
*
|
|
117
|
-
* **A `parents` or `domains` entry is an Address** — the specification types
|
|
118
|
-
* both `Address[]`, targeting `place` — read at whatever length says what it
|
|
119
|
-
* means, `place` the default and the whole of the accepted set. Frontmatter
|
|
120
|
-
* never holds a Wikilink, so no bracket, `|` or `#` is stripped here; a
|
|
121
|
-
* qualified form is read structurally by {@link readCanonicalKey} for its
|
|
122
|
-
* literal four segments, since this position asks neither its package nor its
|
|
123
|
-
* system, only the shortcode {@link module:engine/address} would resolve it
|
|
124
|
-
* to anyway.
|
|
125
|
-
*
|
|
126
|
-
* **The holdings graph has package-blind Shortcode identity.** `places` and
|
|
127
|
-
* `affiliations` merge local and fetched notes into one shortcode space; the
|
|
128
|
-
* first declaration wins. A fetched place attaches to a local parent with
|
|
129
|
-
* the same shortcode, and a fetched affiliation can name both local and
|
|
130
|
-
* fetched domains. The authored entries remain Addresses; this graph uses
|
|
131
|
-
* their shortcode projection for containment and tenure. See
|
|
132
|
-
* `tests/site-holdings.test.ts`'s `empire.domains: ["mill", "abroad"]`, where
|
|
133
|
-
* `mill` is local and `abroad` is the dependency's own, both bare, both
|
|
134
|
-
* correct only because neither package is asked.
|
|
135
|
-
*
|
|
136
|
-
* @param {unknown} value - One entry.
|
|
137
|
-
* @returns {string} Its shortcode, or `""` for an entry that names nothing.
|
|
138
|
-
*/
|
|
139
|
-
function namedShortcode(value) {
|
|
140
|
-
if (isAddressTuple(value)) return value.shortcode.toLowerCase();
|
|
141
|
-
const text = String(value ?? "").trim();
|
|
142
|
-
if (!text) return "";
|
|
143
|
-
const qualified = readCanonicalKey(text);
|
|
144
|
-
if (qualified) return qualified.shortcode.toLowerCase();
|
|
145
|
-
const read = parseAddress(text, { type: "place", types: PLACE_TYPES });
|
|
146
|
-
return read.reason ? "" : read.shortcode.toLowerCase();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* A `parents` or `domains` value as the shortcodes it names, in order,
|
|
151
|
-
* empties dropped and repeats collapsed.
|
|
152
|
-
*
|
|
153
|
-
* @param {unknown} value - The authored value — a list, or a lone entry.
|
|
154
|
-
* @returns {string[]} The shortcodes.
|
|
59
|
+
/** Historical shortcode projection for callers that explicitly need one.
|
|
60
|
+
* @param {unknown} value
|
|
61
|
+
* @returns {string[]}
|
|
155
62
|
*/
|
|
156
63
|
export function shortcodesOf(value) {
|
|
157
64
|
const list =
|
|
158
65
|
Array.isArray(value) ? value
|
|
159
66
|
: value ? [value]
|
|
160
67
|
: [];
|
|
161
|
-
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
|
+
];
|
|
162
81
|
}
|
|
163
82
|
|
|
164
|
-
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* @
|
|
169
|
-
* @param {HoldingsEntry} b
|
|
170
|
-
* @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}
|
|
171
88
|
*/
|
|
172
|
-
function
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
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 },
|
|
176
103
|
);
|
|
104
|
+
if (tuple.reason || tuple.type !== type) return "";
|
|
105
|
+
return renderAddress({ ...tuple, system: "note" }).toLowerCase();
|
|
177
106
|
}
|
|
178
107
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
});
|
|
188
117
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
* emitted all the same. A stub is a place somebody has not written yet: it
|
|
195
|
-
* carries its facts, and Weyshott belongs in Aelwyth's `contains` whether or
|
|
196
|
-
* not anyone has written Weyshott's page. What it cannot have is a link, so
|
|
197
|
-
* the renderer prints the title as plain text — the same rule a table cell
|
|
198
|
-
* follows when its `_ref` is null, in a different renderer.
|
|
199
|
-
*
|
|
200
|
-
* @param {HoldingsNode} node - The node.
|
|
201
|
-
* @returns {HoldingsEntry} Its entry.
|
|
202
|
-
*/
|
|
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} */
|
|
203
123
|
function entryOf(node) {
|
|
204
124
|
return {
|
|
205
125
|
title: node.title,
|
|
@@ -208,35 +128,20 @@ function entryOf(node) {
|
|
|
208
128
|
...(node.subType === undefined ? {} : { subType: node.subType }),
|
|
209
129
|
};
|
|
210
130
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
* does not exist, so this guards {@link holdingsPages}'s `on(url)` calls — and
|
|
218
|
-
* nothing else. Guarding the membership too is how a stub vanishes from every
|
|
219
|
-
* containment and tenure list in the corpus, silently, which is the one failure
|
|
220
|
-
* mode a diff cannot show.
|
|
221
|
-
*
|
|
222
|
-
* @param {HoldingsNode} node - The node.
|
|
223
|
-
* @returns {boolean} Whether it has a page to write a list on.
|
|
224
|
-
*/
|
|
225
|
-
function listed(node) {
|
|
226
|
-
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
|
+
);
|
|
227
137
|
}
|
|
228
138
|
|
|
229
|
-
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* @
|
|
233
|
-
* @param {object} page - The page it publishes as.
|
|
234
|
-
* @param {string} page.title - Its published title.
|
|
235
|
-
* @param {string} page.url - Its URL.
|
|
236
|
-
* @returns {HoldingsNode|null} The node, or `null` for a note that is neither
|
|
237
|
-
* 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}
|
|
238
143
|
*/
|
|
239
|
-
export function holdingsNode(fm, { title, url }) {
|
|
144
|
+
export function holdingsNode(fm, { title, url, package: pkg, system, governmentSystem }) {
|
|
240
145
|
const type = String(fm?.type ?? "");
|
|
241
146
|
if (type !== "place" && type !== "affiliation") return null;
|
|
242
147
|
const shortcode = String(fm?.shortcode ?? "").toLowerCase();
|
|
@@ -248,123 +153,110 @@ export function holdingsNode(fm, { title, url }) {
|
|
|
248
153
|
subType: fm.subType == null ? undefined : String(fm.subType),
|
|
249
154
|
title,
|
|
250
155
|
url,
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
: {}),
|
|
253
168
|
};
|
|
254
169
|
}
|
|
255
170
|
|
|
256
|
-
/**
|
|
257
|
-
* Read a fetched index's places and affiliations as nodes.
|
|
258
|
-
*
|
|
259
|
-
* An entry carries what the producer's record stated — its name, `subType`,
|
|
260
|
-
* `parents` and `domains` — and its page URL where the package publishes one.
|
|
261
|
-
*
|
|
262
|
-
* @param {Map<string, object>|undefined} foreignIndex - As
|
|
263
|
-
* {@link module:engine/metadata-index.loadForeignIndexes} returns it.
|
|
264
|
-
* @returns {HoldingsNode[]} The nodes, in index order.
|
|
265
|
-
*/
|
|
171
|
+
/** @param {Map<string,object>|undefined} foreignIndex @returns {HoldingsNode[]} */
|
|
266
172
|
export function foreignHoldingsNodes(foreignIndex) {
|
|
267
173
|
const out = [];
|
|
268
|
-
|
|
269
|
-
|
|
174
|
+
for (const [canonical, entry] of foreignIndex ?? []) {
|
|
175
|
+
const tuple = readCanonicalKey(canonical);
|
|
270
176
|
const type = String(entry?.type ?? "");
|
|
271
|
-
if (type !== "place" && type !== "affiliation") continue;
|
|
272
|
-
const shortcode = readCanonicalKey(canonical)?.shortcode?.toLowerCase() ?? "";
|
|
273
|
-
if (!shortcode) continue;
|
|
177
|
+
if (!tuple || (type !== "place" && type !== "affiliation")) continue;
|
|
274
178
|
out.push({
|
|
275
|
-
|
|
179
|
+
canonical,
|
|
180
|
+
package: tuple.package,
|
|
181
|
+
system: tuple.system,
|
|
182
|
+
shortcode: tuple.shortcode.toLowerCase(),
|
|
276
183
|
type,
|
|
277
184
|
subType: entry.subType == null ? undefined : String(entry.subType),
|
|
278
|
-
title: String(entry.name ?? shortcode),
|
|
185
|
+
title: String(entry.name ?? tuple.shortcode),
|
|
279
186
|
url: entry.url,
|
|
280
|
-
parents:
|
|
281
|
-
|
|
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
|
+
: {}),
|
|
282
196
|
});
|
|
283
197
|
}
|
|
284
198
|
return out;
|
|
285
199
|
}
|
|
286
200
|
|
|
287
|
-
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* dropped: a `parents` naming nowhere is the map's finding, a `domains`
|
|
296
|
-
* naming nowhere the reference check's.
|
|
297
|
-
*
|
|
298
|
-
* The result holds only pages with at least one entry on at least one list,
|
|
299
|
-
* and a page's block carries only the keys it has entries for — the theme's
|
|
300
|
-
* silent-disappear convention, kept at the source.
|
|
301
|
-
*
|
|
302
|
-
* @param {Iterable<HoldingsNode|null>} nodes - Every place and affiliation,
|
|
303
|
-
* local first. `parents` and `domains` are read as written, so a node
|
|
304
|
-
* built by hand may carry them as the note spells them.
|
|
305
|
-
* @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>}
|
|
306
209
|
*/
|
|
307
|
-
export function holdingsPages(nodes) {
|
|
308
|
-
/** @type {Map<string, HoldingsNode>} */
|
|
210
|
+
export function holdingsPages(nodes, { governmentNodes = [] } = {}) {
|
|
309
211
|
const places = new Map();
|
|
310
|
-
/** @type {Map<string, HoldingsNode>} */
|
|
311
212
|
const affiliations = new Map();
|
|
213
|
+
const geographicPlaces = new Map();
|
|
312
214
|
for (const node of nodes) {
|
|
313
|
-
if (!node) continue;
|
|
215
|
+
if (!node || !node.shortcode) continue;
|
|
314
216
|
const by = node.type === "place" ? places : affiliations;
|
|
315
|
-
const
|
|
316
|
-
if (!
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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);
|
|
327
231
|
}
|
|
328
|
-
|
|
329
|
-
/** @type {Map<string, {contains: HoldingsEntry[], held_by: HoldingsEntry[], holdings: HoldingsEntry[]}>} */
|
|
330
232
|
const lists = new Map();
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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);
|
|
336
239
|
}
|
|
337
|
-
|
|
240
|
+
(block[key] ??= []).push(entryOf(entry));
|
|
338
241
|
};
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!parent || !listed(parent) || parent === child) continue;
|
|
345
|
-
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);
|
|
346
247
|
}
|
|
347
248
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (listed(holder)) on(holder.url).holdings.push(entryOf(held));
|
|
354
|
-
if (listed(held)) on(held.url).held_by.push(entryOf(holder));
|
|
355
|
-
}
|
|
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);
|
|
356
254
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
/** @type {Holdings} */
|
|
361
|
-
const block = {};
|
|
362
|
-
if (contains.length) block.contains = contains.sort(bySubTypeThenTitle);
|
|
363
|
-
if (held_by.length) block.held_by = held_by.sort(byTitle);
|
|
364
|
-
if (holdings.length) block.holdings = holdings.sort(bySubTypeThenTitle);
|
|
365
|
-
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);
|
|
366
258
|
}
|
|
367
|
-
return
|
|
259
|
+
return lists;
|
|
368
260
|
}
|
|
369
261
|
|
|
370
262
|
/* --------------------------------------------------------------------- */
|
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
|
|
package/engine/infobox.mjs
CHANGED
|
@@ -723,7 +723,7 @@ function structuredRows(field, raw, resolve, label, contentPackage) {
|
|
|
723
723
|
// Address: an authored plain list of addresses normalises to a list of
|
|
724
724
|
// Address tuples, which `isMapping` also reports true for, and treating
|
|
725
725
|
// one as a relationship keyed off a `to` it never carries is what emptied
|
|
726
|
-
// `lore`, `parents
|
|
726
|
+
// `lore`, `parents` and `economy` silently.
|
|
727
727
|
const relations =
|
|
728
728
|
Array.isArray(raw) ? raw.filter((entry) => isMapping(entry) && !isAddressTuple(entry)) : [];
|
|
729
729
|
if (relations.length) {
|
package/engine/journals.mjs
CHANGED
|
@@ -196,7 +196,7 @@ export function splitPages(body, leadName = "Introduction", resolveRole) {
|
|
|
196
196
|
if (figure) {
|
|
197
197
|
closeCurrent();
|
|
198
198
|
current = {
|
|
199
|
-
name: figure.label,
|
|
199
|
+
name: figure.label || figure.caption,
|
|
200
200
|
anchorSlug: figure.id || null,
|
|
201
201
|
level: 1,
|
|
202
202
|
classes: [],
|
|
@@ -404,7 +404,7 @@ function figureIsWholePage(figure, page) {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
/**
|
|
407
|
-
* The fields particular to the Foundry page a
|
|
407
|
+
* The fields particular to the Foundry page a captioned item begins.
|
|
408
408
|
*
|
|
409
409
|
* A fence holding exactly one picture, captioned with no inline markup, and
|
|
410
410
|
* standing alone on its page becomes a page of type `image`: `src` the
|
|
@@ -442,14 +442,16 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
|
|
|
442
442
|
// image's own render takes when no configuration resolves it.
|
|
443
443
|
}
|
|
444
444
|
return {
|
|
445
|
-
name: figure.label,
|
|
445
|
+
name: figure.label || figure.caption,
|
|
446
446
|
type: "image",
|
|
447
447
|
src,
|
|
448
448
|
image: figure.hasCaption ? { caption: figure.caption } : {},
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
451
|
const label =
|
|
452
|
-
figure.hasCaption ?
|
|
452
|
+
figure.hasCaption ?
|
|
453
|
+
`${figure.label ? `${figure.label}: ` : ""}${md.renderInline(figure.caption)}`
|
|
454
|
+
: figure.label;
|
|
453
455
|
const content = renderFoundryMarkdown(
|
|
454
456
|
page.markdown,
|
|
455
457
|
undefined,
|
|
@@ -458,7 +460,7 @@ function buildFigurePageFields(figure, page, pageId, footnoteNumbers, resolveRol
|
|
|
458
460
|
resolveRole,
|
|
459
461
|
);
|
|
460
462
|
return {
|
|
461
|
-
name: figure.label,
|
|
463
|
+
name: figure.label || figure.caption,
|
|
462
464
|
type: "text",
|
|
463
465
|
text: {
|
|
464
466
|
format: 1,
|
|
@@ -763,7 +765,7 @@ export class Journals extends BasePackCompiler {
|
|
|
763
765
|
|
|
764
766
|
// The markdown this pass reads has already had its embeds rewritten
|
|
765
767
|
// into ordinary images, each `src` the pathname `pathnameRoles` keys
|
|
766
|
-
// its roles by — so a
|
|
768
|
+
// its roles by — so a captioned map names itself `Map 1` here
|
|
767
769
|
// exactly as the website and the book name the same picture.
|
|
768
770
|
const roles = pathnameRoles(this.linkIndex);
|
|
769
771
|
|
|
@@ -154,7 +154,7 @@ const HELPERS = Object.freeze([
|
|
|
154
154
|
name: "ref",
|
|
155
155
|
params: 'address [form="number"|"full"|"title"]',
|
|
156
156
|
summary:
|
|
157
|
-
'A link to a
|
|
157
|
+
'A link to a captioned item, by its anchor — `"#thorn"` on this note, ' +
|
|
158
158
|
'`"note-address#thorn"` on another. `form` is `number` (the default, and ' +
|
|
159
159
|
'admitted explicitly), rendering "Figure 13"; `full`, the number and the ' +
|
|
160
160
|
"caption; or `title`, the caption alone. Always renders as a link to the " +
|
|
@@ -173,6 +173,10 @@ const HELPERS = Object.freeze([
|
|
|
173
173
|
const { parsed, figure, crossNote, noteFound, url } = refFigure(address, figures);
|
|
174
174
|
if (!figure)
|
|
175
175
|
throw new RangeError(refNotFoundFault(address, parsed, crossNote, noteFound));
|
|
176
|
+
if (form === "number" && !figure.label)
|
|
177
|
+
throw new RangeError(
|
|
178
|
+
'a reference to an unnumbered caption requires form="title" or form="full"',
|
|
179
|
+
);
|
|
176
180
|
if (form !== "number" && !figure.hasCaption)
|
|
177
181
|
throw new RangeError(refNoCaptionFault(form, parsed));
|
|
178
182
|
return renderRef(figure, parsed, form, { url, link: figures?.link });
|
|
@@ -271,7 +275,7 @@ function flattenCaptionLinks(caption) {
|
|
|
271
275
|
function refLabel(figure, form) {
|
|
272
276
|
if (form === "number") return figure.label;
|
|
273
277
|
const title = flattenCaptionLinks(figure.caption);
|
|
274
|
-
return form === "title" ? title : `${figure.label}: ${title}`;
|
|
278
|
+
return form === "title" || !figure.label ? title : `${figure.label}: ${title}`;
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
/**
|
|
@@ -394,6 +398,8 @@ function refCallFault(node, figures) {
|
|
|
394
398
|
if (typeof address !== "string" || !address.trim()) return null;
|
|
395
399
|
const { parsed, figure, crossNote, noteFound } = refFigure(address, figures);
|
|
396
400
|
if (!figure) return refNotFoundFault(address, parsed, crossNote, noteFound);
|
|
401
|
+
if (form === "number" && !figure.label)
|
|
402
|
+
return 'a reference to an unnumbered caption requires form="title" or form="full"';
|
|
397
403
|
if (form !== "number" && !figure.hasCaption) return refNoCaptionFault(form, parsed);
|
|
398
404
|
return null;
|
|
399
405
|
}
|
|
@@ -448,7 +454,7 @@ function positionAt(body, offset, bodyLine) {
|
|
|
448
454
|
* {url: string|null, figures: Map<string, object>}|undefined,
|
|
449
455
|
* link?: Function}, file?: string, bodyLine?: number}} [options] -
|
|
450
456
|
* `figures` is the `ref` helper's own view of the corpus: `get` reads this
|
|
451
|
-
* note's
|
|
457
|
+
* note's captioned items by id, and `note` resolves another note's the
|
|
452
458
|
* way a wikilink would — omitted, a cross-note address is refused rather
|
|
453
459
|
* than looked up. `link` overrides how a resolved reference renders; the
|
|
454
460
|
* default is a Markdown link to the target's own anchor.
|