@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
|
@@ -424,16 +424,15 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
424
424
|
// the record naming a path at all.
|
|
425
425
|
asset: record.asset ?? undefined,
|
|
426
426
|
// What a place is next to, reachable from and within, and
|
|
427
|
-
//
|
|
427
|
+
// which affiliation governs it, carried so a consumer's lint
|
|
428
428
|
// checks a border across the package boundary from both ends,
|
|
429
429
|
// a consumer's map draws the dependency's places beside its
|
|
430
430
|
// own, and a consumer's pages list what a dependency's
|
|
431
|
-
// affiliation
|
|
431
|
+
// affiliation governs and what lies within its regions. A
|
|
432
432
|
// population lets the same checks include dependency figures.
|
|
433
433
|
borders: record.data?.borders ?? undefined,
|
|
434
434
|
routes: record.data?.routes ?? undefined,
|
|
435
435
|
parents: record.data?.parents ?? undefined,
|
|
436
|
-
domains: record.data?.domains ?? undefined,
|
|
437
436
|
population: record.data?.population ?? undefined,
|
|
438
437
|
...(Object.hasOwn(record.data ?? {}, "government") ?
|
|
439
438
|
{ government: record.data.government }
|
|
@@ -209,8 +209,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
209
209
|
* @property {(note: object, opts: {index?: object}) => object[]} [check] - A
|
|
210
210
|
* check of the whole note, run by the frontmatter lint beside the field
|
|
211
211
|
* checks with the same index. For a rule that is about the note rather than
|
|
212
|
-
* one of its fields
|
|
213
|
-
* `domains`, and is located at the note's `type:` line.
|
|
212
|
+
* one of its fields, located at the note's `type:` line.
|
|
214
213
|
*/
|
|
215
214
|
|
|
216
215
|
/* --------------------------------------------------------------------- */
|
|
@@ -1090,13 +1089,6 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1090
1089
|
accepts: ["place"],
|
|
1091
1090
|
describe: "Where the affiliation's authority sits.",
|
|
1092
1091
|
},
|
|
1093
|
-
{
|
|
1094
|
-
name: "domains",
|
|
1095
|
-
...LINKS,
|
|
1096
|
-
ref: "place",
|
|
1097
|
-
accepts: ["place"],
|
|
1098
|
-
describe: "Places over which it holds sway.",
|
|
1099
|
-
},
|
|
1100
1092
|
{ name: "population", ...NUM, describe: "How many people it counts." },
|
|
1101
1093
|
{
|
|
1102
1094
|
name: "economy",
|
|
@@ -1567,12 +1559,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1567
1559
|
{
|
|
1568
1560
|
name: "government",
|
|
1569
1561
|
...LINK,
|
|
1570
|
-
shape: "an
|
|
1562
|
+
shape: "an Address or null",
|
|
1571
1563
|
ref: "affiliation",
|
|
1572
1564
|
accepts: ["affiliation"],
|
|
1573
1565
|
nullText: "Complete anarchy",
|
|
1574
1566
|
describe:
|
|
1575
|
-
"The governing affiliation;
|
|
1567
|
+
"The governing affiliation Address; the default target type is affiliation. " +
|
|
1568
|
+
"Explicit null means complete anarchy. " +
|
|
1576
1569
|
"A positive population with no government key produces an advisory.",
|
|
1577
1570
|
},
|
|
1578
1571
|
{
|
|
@@ -1868,3 +1861,15 @@ export function subTypes(type, vocabulary = NOTE_VOCABULARY) {
|
|
|
1868
1861
|
if (!entry || !Object.hasOwn(entry, "subTypes")) return undefined;
|
|
1869
1862
|
return entry.subTypes;
|
|
1870
1863
|
}
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* Accepted migration input with no canonical field or emitted value.
|
|
1867
|
+
*
|
|
1868
|
+
* Legacy affiliation domains are ignored. They never supply a government.
|
|
1869
|
+
* @param {string} type - The note type.
|
|
1870
|
+
* @param {string} key - An authored data key.
|
|
1871
|
+
* @returns {boolean} Whether the obsolete key is accepted during migration.
|
|
1872
|
+
*/
|
|
1873
|
+
export function isLegacyDataField(type, key) {
|
|
1874
|
+
return currentType(type) === "affiliation" && key === "domains";
|
|
1875
|
+
}
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -745,7 +745,7 @@ export async function buildPdf({
|
|
|
745
745
|
* @returns {string} Typst markup.
|
|
746
746
|
*/
|
|
747
747
|
const figureCounts = { code: 0, table: 0, figure: 0, map: 0, poem: 0, prose: 0 };
|
|
748
|
-
// The role lookup a
|
|
748
|
+
// The role lookup a captioned item's `map` counter reaches through —
|
|
749
749
|
// see `engine/content-figures.mjs`'s `resolveRole`. Addressed form, for a
|
|
750
750
|
// page's own scan below, which runs before its embeds are rewritten into
|
|
751
751
|
// ordinary images; `pathRoles` is the same lookup keyed by the pathname
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -85,10 +85,13 @@ export const BOOK_IMAGE_WIDTHS = Object.freeze({
|
|
|
85
85
|
xlarge: "8cm",
|
|
86
86
|
"full-width": '"full-width"',
|
|
87
87
|
});
|
|
88
|
+
import { markupAnchorFindings } from "./anchors.mjs";
|
|
88
89
|
import { slugify } from "./content-slug.mjs";
|
|
90
|
+
import { alertMarkdownPlugin, scanAlerts, ALERT_TYPES } from "./content-alerts.mjs";
|
|
91
|
+
import { spanMarkdownPlugin, scanSpans } from "./content-spans.mjs";
|
|
89
92
|
import { scanFigures } from "./content-figures.mjs";
|
|
90
93
|
import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
|
|
91
|
-
import { inlineVerse, poetryStanzas } from "./content-poetry.mjs";
|
|
94
|
+
import { inlineVerse, poetryStanzas, scanPoetry } from "./content-poetry.mjs";
|
|
92
95
|
import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
|
|
93
96
|
import { HTML_TAG, htmlMessage } from "./content-html.mjs";
|
|
94
97
|
import { EXPRESSION } from "./markdown-expressions.mjs";
|
|
@@ -108,9 +111,7 @@ import {
|
|
|
108
111
|
* a class and let CSS decide.
|
|
109
112
|
*/
|
|
110
113
|
const BLOCK_PRINT = Object.freeze({
|
|
111
|
-
info: Object.freeze({ color: "#2f6f9f", background: "#eef6fb", symbol: "i" }),
|
|
112
114
|
secret: Object.freeze({ color: "#5c4b8a", background: "#f2eefb", symbol: "!" }),
|
|
113
|
-
warn: Object.freeze({ color: "#9a6700", background: "#fff5db", symbol: "!" }),
|
|
114
115
|
});
|
|
115
116
|
|
|
116
117
|
import {
|
|
@@ -197,6 +198,24 @@ export function labelFor(anchor) {
|
|
|
197
198
|
export function createParser(registry) {
|
|
198
199
|
const md = new MarkdownIt({ html: false, linkify: false, typographer: false });
|
|
199
200
|
md.use(footnotePlugin);
|
|
201
|
+
md.use(alertMarkdownPlugin);
|
|
202
|
+
md.use(spanMarkdownPlugin);
|
|
203
|
+
// Verse is held inside a literal fence. Parse its inline content before
|
|
204
|
+
// footnote_tail collects definitions, using the surrounding note's env.
|
|
205
|
+
md.core.ruler.after("inline", "poetry_inline", (state) => {
|
|
206
|
+
for (const token of state.tokens) {
|
|
207
|
+
if (token.type !== "fence" || !/^poetry(?:\s|$)/.test(token.info?.trim() || ""))
|
|
208
|
+
continue;
|
|
209
|
+
token.meta ??= {};
|
|
210
|
+
token.meta.poetryStanzas = poetryStanzas(token.content).map((stanza) =>
|
|
211
|
+
stanza.map(({ text, level }) => {
|
|
212
|
+
const children = [];
|
|
213
|
+
state.md.inline.parse(inlineVerse(text), state.md, state.env, children);
|
|
214
|
+
return { children, level };
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
200
219
|
md.use(deflistPlugin);
|
|
201
220
|
md.use(iconPlugin(registry));
|
|
202
221
|
// The same plugin the HTML surfaces use, so one directive is read once and
|
|
@@ -298,7 +317,7 @@ function offsetPosition(source, offset, opts = {}) {
|
|
|
298
317
|
* those passes did not run — `opts.prepared` — because each of them leaves the
|
|
299
318
|
* markup as written when it fails and has already said so in its own words.
|
|
300
319
|
*
|
|
301
|
-
* A
|
|
320
|
+
* A fenced div and a leading caption are the structures this pass handles, and their
|
|
302
321
|
* own scanners decide what is well formed; their findings are reported here so
|
|
303
322
|
* that a front-matter file, a prose file and a note body all get them.
|
|
304
323
|
*
|
|
@@ -325,7 +344,7 @@ function reportUnrenderable(source, definitions, opts, original) {
|
|
|
325
344
|
};
|
|
326
345
|
|
|
327
346
|
const blocks = scanBlocks(source);
|
|
328
|
-
for (const error of blocks.errors)
|
|
347
|
+
for (const error of [...blocks.errors, ...scanAlerts(source).errors])
|
|
329
348
|
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
330
349
|
// A container's body, read for faults of its own — an empty box, an
|
|
331
350
|
// attribute that does not parse — which the scan above passes over, because
|
|
@@ -349,6 +368,8 @@ function reportUnrenderable(source, definitions, opts, original) {
|
|
|
349
368
|
);
|
|
350
369
|
}
|
|
351
370
|
}
|
|
371
|
+
for (const error of [...scanSpans(source).errors, ...markupAnchorFindings(source)])
|
|
372
|
+
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
352
373
|
for (const error of scanFigures(source).errors)
|
|
353
374
|
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
354
375
|
// An image sharing its paragraph with other text, or an address or title
|
|
@@ -527,14 +548,11 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
527
548
|
}
|
|
528
549
|
}
|
|
529
550
|
if (!opts.insideAdmonition) {
|
|
530
|
-
const
|
|
551
|
+
const captionTargets = scanFigures(source).figures;
|
|
531
552
|
const blocks = scanBlocks(source).blocks.filter(
|
|
532
553
|
(block) =>
|
|
533
|
-
!
|
|
534
|
-
(figure) =>
|
|
535
|
-
block.name === "poetry" &&
|
|
536
|
-
block.start >= figure.bodyStart &&
|
|
537
|
-
block.end < figure.bodyEnd,
|
|
554
|
+
!captionTargets.some(
|
|
555
|
+
(figure) => block.start >= figure.bodyStart && block.end < figure.bodyEnd,
|
|
538
556
|
),
|
|
539
557
|
);
|
|
540
558
|
if (blocks.length) {
|
|
@@ -548,21 +566,13 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
548
566
|
insideAdmonition: true,
|
|
549
567
|
}),
|
|
550
568
|
);
|
|
551
|
-
if (block.name === "
|
|
552
|
-
const content =
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
insideAdmonition: true,
|
|
559
|
-
});
|
|
560
|
-
return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
|
|
561
|
-
})
|
|
562
|
-
.join(" \\\n"),
|
|
563
|
-
)
|
|
564
|
-
.join("\n\n");
|
|
565
|
-
output.push(`\n#block(width: 100%)[${content}]\n`);
|
|
569
|
+
if (block.name === "div") {
|
|
570
|
+
const content = markdownToTypst(block.body, {
|
|
571
|
+
...sharedOptions,
|
|
572
|
+
insideAdmonition: false,
|
|
573
|
+
});
|
|
574
|
+
const anchor = block.id ? ` <${sectionLabel(anchorPrefix, block.id)}>` : "";
|
|
575
|
+
output.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
|
|
566
576
|
cursor = block.end + 1;
|
|
567
577
|
continue;
|
|
568
578
|
}
|
|
@@ -629,7 +639,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
629
639
|
{ ...caption, imagesRemaining: { n: countImages(block) } }
|
|
630
640
|
: caption;
|
|
631
641
|
const segments = [
|
|
632
|
-
caption.kind
|
|
642
|
+
caption.kind !== "table" && caption.kind !== "figure" && caption.kind !== "map" ?
|
|
633
643
|
markdownToTypst(block, { ...sharedOptions, captions: undefined })
|
|
634
644
|
: renderMarkdownSegment(block, md, { ...ctx, caption: captioned }, definitions),
|
|
635
645
|
];
|
|
@@ -732,7 +742,23 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
732
742
|
}
|
|
733
743
|
case "fence":
|
|
734
744
|
case "code_block": {
|
|
735
|
-
|
|
745
|
+
if (token.type === "fence" && /^poetry(?:\s|$)/.test(token.info?.trim() || "")) {
|
|
746
|
+
const content = token.meta.poetryStanzas
|
|
747
|
+
.map((stanza) =>
|
|
748
|
+
stanza
|
|
749
|
+
.map(({ children, level }) => {
|
|
750
|
+
const verse = renderInline({ children }, ctx);
|
|
751
|
+
return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
|
|
752
|
+
})
|
|
753
|
+
.join(" \\\n"),
|
|
754
|
+
)
|
|
755
|
+
.join("\n\n");
|
|
756
|
+
const poem = scanPoetry(
|
|
757
|
+
`${token.markup}${token.info}\n${token.content}${token.markup}`,
|
|
758
|
+
).blocks[0];
|
|
759
|
+
const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
|
|
760
|
+
out.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
|
|
761
|
+
} else out.push(rawBlock(token.content, token.info?.trim() || ""));
|
|
736
762
|
return 1;
|
|
737
763
|
}
|
|
738
764
|
case "hr":
|
|
@@ -741,7 +767,20 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
741
767
|
case "blockquote_open": {
|
|
742
768
|
const end = matching(tokens, i, "blockquote_open", "blockquote_close");
|
|
743
769
|
const inner = renderTokens(tokens.slice(i + 1, end), { ...ctx });
|
|
744
|
-
|
|
770
|
+
if (token.meta?.alert) {
|
|
771
|
+
const alert = token.meta.alert;
|
|
772
|
+
const colors = {
|
|
773
|
+
note: "#0969da",
|
|
774
|
+
tip: "#1a7f37",
|
|
775
|
+
important: "#8250df",
|
|
776
|
+
warning: "#9a6700",
|
|
777
|
+
};
|
|
778
|
+
const spec = ALERT_TYPES[alert.type];
|
|
779
|
+
const icon = `#image(bytes("${escapeTypstString(spec.svg.replace("<svg ", '<svg xmlns="http://www.w3.org/2000/svg" ').replaceAll("currentColor", colors[alert.type]))}"), format: "svg", width: 0.9em, height: 0.9em)`;
|
|
780
|
+
const anchor = alert.id ? ` <${sectionLabel(ctx.anchorPrefix, alert.id)}>` : "";
|
|
781
|
+
const box = `\n#block(width: 100%, stroke: (left: 2pt + rgb("${colors[alert.type]}")), inset: 8pt, above: 0.7em, below: 0.7em)[#text(fill: rgb("${colors[alert.type]}"), weight: "bold")[${icon} ${inlineMarkup(alert.title ?? alert.attributes?.title ?? spec.title, ctx)}]\n\n${inner}]${anchor}\n\n`;
|
|
782
|
+
out.push(alert.classes?.includes("border") ? figureBorder(box) : box);
|
|
783
|
+
} else out.push(`\n#quote(block: true)[${inner}]\n\n`);
|
|
745
784
|
return end - i + 1;
|
|
746
785
|
}
|
|
747
786
|
case "bullet_list_open":
|
|
@@ -964,13 +1003,15 @@ function inlineMarkup(text, ctx) {
|
|
|
964
1003
|
*/
|
|
965
1004
|
function captionMarkup(caption, ctx) {
|
|
966
1005
|
const label = escapeTypst(caption.label);
|
|
967
|
-
return caption.hasCaption ?
|
|
1006
|
+
return caption.hasCaption ?
|
|
1007
|
+
`${label ? `${label}: ` : ""}${inlineMarkup(caption.caption, ctx)}`
|
|
1008
|
+
: label;
|
|
968
1009
|
}
|
|
969
1010
|
|
|
970
1011
|
/**
|
|
971
1012
|
* How many markdown images a figure's contents carry.
|
|
972
1013
|
*
|
|
973
|
-
* A grouped figure's
|
|
1014
|
+
* A grouped figure's leading caption describes the whole plate, not any one
|
|
974
1015
|
* picture in it, so {@link renderImage} needs to know which image is the
|
|
975
1016
|
* last — the one that carries the group's single label and anchor — and this
|
|
976
1017
|
* is the count it counts down from.
|
|
@@ -1157,6 +1198,24 @@ function renderInline(token, ctx) {
|
|
|
1157
1198
|
case "code_inline":
|
|
1158
1199
|
out.push(inlineRaw(child.content));
|
|
1159
1200
|
break;
|
|
1201
|
+
case "span_open": {
|
|
1202
|
+
out.push("#box[");
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
case "span_close": {
|
|
1206
|
+
let depth = 1;
|
|
1207
|
+
let opening;
|
|
1208
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
1209
|
+
if (children[j].type === "span_close") depth += 1;
|
|
1210
|
+
if (children[j].type === "span_open" && --depth === 0) {
|
|
1211
|
+
opening = children[j];
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const id = opening?.attrGet?.("id");
|
|
1216
|
+
out.push(`]${id ? ` <${sectionLabel(ctx.anchorPrefix, id)}>` : ""}`);
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1160
1219
|
case "strong_open":
|
|
1161
1220
|
out.push("#strong[");
|
|
1162
1221
|
break;
|
package/engine/populations.mjs
CHANGED
|
@@ -14,17 +14,14 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Whether a world's population figures agree from the region down.
|
|
16
16
|
*
|
|
17
|
-
* A place states `data.population` and an affiliation states
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* keys — a place's `data.parents` for geography, an affiliation's
|
|
26
|
-
* `data.domains` for tenure. Where that module asks whether anybody holds a
|
|
27
|
-
* place, this one asks whether the numbers add up.
|
|
17
|
+
* A place states `data.population` and an affiliation states its membership
|
|
18
|
+
* population. Three claims can be checked: the regions inside a place fit
|
|
19
|
+
* inside it, a settlement fits inside every place that contains it, and a page
|
|
20
|
+
* citing a figure states the figure that note states.
|
|
21
|
+
*
|
|
22
|
+
* Geography comes from a place's `data.parents`. Government is independent:
|
|
23
|
+
* an affiliation's membership population does not state the population of the
|
|
24
|
+
* territory it governs, so these checks do not compare the two.
|
|
28
25
|
*
|
|
29
26
|
* **Every finding is a warning.** A figure out of step with its neighbours is
|
|
30
27
|
* a thing an author wants to see while the note is open, and the same gap can
|
|
@@ -39,10 +36,9 @@
|
|
|
39
36
|
* **A stated figure is compared; an absent one is not.** A place or an
|
|
40
37
|
* affiliation with no `population` contributes nothing to a sum and is never
|
|
41
38
|
* the subject of a finding, so a half-written corpus is quiet rather than
|
|
42
|
-
* noisy. A dependency's places and affiliations take part
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* nowhere.
|
|
39
|
+
* noisy. A dependency's places and affiliations take part when their fetched
|
|
40
|
+
* entry supplies a population figure; an older index without one contributes
|
|
41
|
+
* no population.
|
|
46
42
|
*
|
|
47
43
|
* **There is deliberately no urban-share rule.** A share computed from the
|
|
48
44
|
* named settlements against a region's total measures nothing: the settlement
|
|
@@ -81,14 +77,6 @@ export const POPULATION_TOLERANCE = 1.05;
|
|
|
81
77
|
* @type {ReadonlyArray<{name: string, describe: string}>}
|
|
82
78
|
*/
|
|
83
79
|
export const POPULATION_RULES = Object.freeze([
|
|
84
|
-
Object.freeze({
|
|
85
|
-
name: "over-held land",
|
|
86
|
-
describe:
|
|
87
|
-
"The polities whose `domains` name a place count more people than the place " +
|
|
88
|
-
"states. A polity subordinate to another holding the same place is already " +
|
|
89
|
-
"counted in that polity's figure, and a settlement is exempt, its holder's " +
|
|
90
|
-
"figure counting a hinterland the settlement does not.",
|
|
91
|
-
}),
|
|
92
80
|
Object.freeze({
|
|
93
81
|
name: "over-full region",
|
|
94
82
|
describe:
|
|
@@ -133,19 +121,16 @@ const CORPUS = new WeakMap();
|
|
|
133
121
|
* @property {number} [population] The stated figure, absent where unstated.
|
|
134
122
|
* @property {string[]} parents A place's enclosing places, an affiliation's
|
|
135
123
|
* superiors.
|
|
136
|
-
* @property {string[]} domains What an affiliation holds.
|
|
137
124
|
*/
|
|
138
125
|
|
|
139
126
|
/**
|
|
140
|
-
* The whole tree, indexed
|
|
127
|
+
* The whole tree, indexed by place, affiliation and geographical parent.
|
|
141
128
|
*
|
|
142
129
|
* @typedef {object} Corpus
|
|
143
130
|
* @property {Map<string, PopulationNode>} places By shortcode.
|
|
144
131
|
* @property {Map<string, PopulationNode>} affiliations By shortcode.
|
|
145
132
|
* @property {Map<string, string[]>} childrenOf Place shortcode → the places
|
|
146
133
|
* whose `parents` name it.
|
|
147
|
-
* @property {Map<string, string[]>} holdersOf Place shortcode → the polities
|
|
148
|
-
* whose `domains` name it.
|
|
149
134
|
* @property {Map<string, PopulationNode>} byAddress `type/shortcode` → the
|
|
150
135
|
* node, for the citation rule.
|
|
151
136
|
* Package-blind by design: a
|
|
@@ -210,7 +195,6 @@ function localNode(note) {
|
|
|
210
195
|
title: String(fm.name?.full ?? shortcode),
|
|
211
196
|
population: figureOf(data.population),
|
|
212
197
|
parents: shortcodesOf(data.parents),
|
|
213
|
-
domains: type === "affiliation" ? shortcodesOf(data.domains) : [],
|
|
214
198
|
};
|
|
215
199
|
}
|
|
216
200
|
|
|
@@ -236,14 +220,13 @@ export function foreignNode(canonical, entry) {
|
|
|
236
220
|
title: String(entry.name ?? shortcode),
|
|
237
221
|
population: figureOf(entry.population),
|
|
238
222
|
parents: shortcodesOf(entry.parents),
|
|
239
|
-
domains: type === "affiliation" ? shortcodesOf(entry.domains) : [],
|
|
240
223
|
};
|
|
241
224
|
}
|
|
242
225
|
|
|
243
226
|
/**
|
|
244
227
|
* The corpus a link index describes.
|
|
245
228
|
*
|
|
246
|
-
* Population containment
|
|
229
|
+
* Population containment uses the same package-blind Shortcode
|
|
247
230
|
* identity as holdings and maps. Each type has one node per shortcode, with
|
|
248
231
|
* local notes taking precedence over fetched entries. A fetched place can
|
|
249
232
|
* attach to local geography, while its authored `parents` remains an Address.
|
|
@@ -289,18 +272,7 @@ function corpusOf(index) {
|
|
|
289
272
|
}
|
|
290
273
|
}
|
|
291
274
|
|
|
292
|
-
|
|
293
|
-
const holdersOf = new Map();
|
|
294
|
-
for (const node of affiliations.values()) {
|
|
295
|
-
if (node.subType !== "polity") continue;
|
|
296
|
-
for (const domain of node.domains) {
|
|
297
|
-
const list = holdersOf.get(domain);
|
|
298
|
-
if (list) list.push(node.shortcode);
|
|
299
|
-
else holdersOf.set(domain, [node.shortcode]);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
found = { places, affiliations, childrenOf, holdersOf, byAddress };
|
|
275
|
+
found = { places, affiliations, childrenOf, byAddress };
|
|
304
276
|
CORPUS.set(index, found);
|
|
305
277
|
return found;
|
|
306
278
|
}
|
|
@@ -348,27 +320,6 @@ function ancestors(corpus, shortcode) {
|
|
|
348
320
|
return out;
|
|
349
321
|
}
|
|
350
322
|
|
|
351
|
-
/**
|
|
352
|
-
* The polities holding a place, each counted once.
|
|
353
|
-
*
|
|
354
|
-
* A polity subordinate to another polity that holds the same place is already
|
|
355
|
-
* inside that polity's figure, so only the topmost of a chain counts.
|
|
356
|
-
*
|
|
357
|
-
* @param {Corpus} corpus - The corpus.
|
|
358
|
-
* @param {string} shortcode - The place.
|
|
359
|
-
* @returns {PopulationNode[]} The holders.
|
|
360
|
-
*/
|
|
361
|
-
function topHolders(corpus, shortcode) {
|
|
362
|
-
const holders = corpus.holdersOf.get(shortcode) ?? [];
|
|
363
|
-
const held = new Set(holders);
|
|
364
|
-
return holders
|
|
365
|
-
.map((name) => corpus.affiliations.get(name))
|
|
366
|
-
.filter(
|
|
367
|
-
/** @returns {node is PopulationNode} */
|
|
368
|
-
(node) => Boolean(node) && !(node?.parents ?? []).some((p) => held.has(p)),
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
323
|
/**
|
|
373
324
|
* The regions immediately inside a place.
|
|
374
325
|
*
|
|
@@ -416,7 +367,7 @@ function atFigure(note, rule, message) {
|
|
|
416
367
|
* runs it with the index every reference check resolves through. Without an
|
|
417
368
|
* index the questions cannot be asked and nothing is reported.
|
|
418
369
|
*
|
|
419
|
-
*
|
|
370
|
+
* Two geographical rules, each reported on the note being linted:
|
|
420
371
|
* `over-full region` on the place whose figure is exceeded, and
|
|
421
372
|
* `oversized settlement` on the settlement that exceeds one.
|
|
422
373
|
*
|
|
@@ -441,23 +392,6 @@ export function checkPopulation(note, { index } = {}) {
|
|
|
441
392
|
const allowed = stated * POPULATION_TOLERANCE;
|
|
442
393
|
const findings = [];
|
|
443
394
|
|
|
444
|
-
// A settlement is exempt: the polity holding a city-state counts the
|
|
445
|
-
// hinterland the city does not, so its figure is properly the larger.
|
|
446
|
-
if (subType !== "settlement") {
|
|
447
|
-
const holders = topHolders(corpus, shortcode);
|
|
448
|
-
const sum = holders.reduce((total, node) => total + (node.population ?? 0), 0);
|
|
449
|
-
if (sum > allowed) {
|
|
450
|
-
findings.push(
|
|
451
|
-
atFigure(
|
|
452
|
-
note,
|
|
453
|
-
"over-held land",
|
|
454
|
-
`the polities holding ${title} count ${counted(sum)} people against its ` +
|
|
455
|
-
`stated ${counted(stated)}`,
|
|
456
|
-
),
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
395
|
const regions = nearestRegions(corpus, shortcode);
|
|
462
396
|
const inside = regions.reduce((total, node) => total + (node.population ?? 0), 0);
|
|
463
397
|
if (inside > allowed) {
|
|
@@ -520,8 +454,8 @@ const CITED =
|
|
|
520
454
|
* restated figures drift. Each is located at the figure it wrote, counting
|
|
521
455
|
* repeats so two rows carrying one number land on their own lines.
|
|
522
456
|
*
|
|
523
|
-
* A citation naming a note
|
|
524
|
-
*
|
|
457
|
+
* A citation naming a note with no published population figure is skipped:
|
|
458
|
+
* there is nothing to disagree with.
|
|
525
459
|
*
|
|
526
460
|
* Declared as the `doc` vocabulary's type-level check.
|
|
527
461
|
*
|
package/engine/site-build.mjs
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
* @module
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
+
import { renderAlerts, scanAlerts } from "./content-alerts.mjs";
|
|
54
55
|
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
55
56
|
import { decodeNoteAddresses, noteAddressContext, encodeAddresses } from "./note-addresses.mjs";
|
|
56
57
|
import fs from "node:fs";
|
|
@@ -62,11 +63,12 @@ import { addressSlug } from "./content-address.mjs";
|
|
|
62
63
|
import { protectCode } from "./code-fences.mjs";
|
|
63
64
|
import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
64
65
|
import { expandContentTables } from "./content-tables.mjs";
|
|
66
|
+
import { renderSpans, scanSpans } from "./content-spans.mjs";
|
|
65
67
|
import { renderBlocks, renderWithheldSections, scanBlocks } from "./content-blocks.mjs";
|
|
66
68
|
import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
|
|
67
69
|
import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
|
|
68
70
|
import { footnoteFindings } from "./content-footnotes.mjs";
|
|
69
|
-
import { collectAnchors } from "./anchors.mjs";
|
|
71
|
+
import { collectAnchors, markupAnchorFindings } from "./anchors.mjs";
|
|
70
72
|
import { checkImages, renderImageFigures } from "./content-images.mjs";
|
|
71
73
|
import { assetPathnameKey, pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
72
74
|
import {
|
|
@@ -563,7 +565,7 @@ export function tableUniverse(pages) {
|
|
|
563
565
|
* about every other page, known only once the whole tree has resolved — see
|
|
564
566
|
* {@link module:engine/related-pages} — so an authored value is dropped the
|
|
565
567
|
* way `aliases` is, and {@link renderPages} writes the derived block once it
|
|
566
|
-
* holds the graph.
|
|
568
|
+
* holds the graph. Derived geographical and government lists are dropped for the
|
|
567
569
|
* same reason — see {@link module:engine/holdings}. **So is `map`**: the
|
|
568
570
|
* file a place page names is the one the build drew beside it — see
|
|
569
571
|
* {@link module:engine/site-maps} — and a page with no drawing names none.
|
|
@@ -601,6 +603,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
|
601
603
|
kbfolder: page.folder,
|
|
602
604
|
};
|
|
603
605
|
if (decorate) decorate(data, page);
|
|
606
|
+
if (data.type === "affiliation" && data.data) delete data.data.domains;
|
|
604
607
|
delete data.aliases;
|
|
605
608
|
delete data.related;
|
|
606
609
|
for (const key of HOLDINGS_KEYS) delete data[key];
|
|
@@ -754,7 +757,7 @@ export function renderSitePage(
|
|
|
754
757
|
return src;
|
|
755
758
|
};
|
|
756
759
|
const artSrc = (value, type, accepts) => artPathname(artIndex, value, type, accepts).pathname;
|
|
757
|
-
// The role lookup a
|
|
760
|
+
// The role lookup a captioned item's `map` counter reaches through —
|
|
758
761
|
// see `engine/content-figures.mjs`'s `resolveRole`. Addressed form, for
|
|
759
762
|
// the page's own scan below, which runs before an embed is rewritten into
|
|
760
763
|
// an ordinary image; `roleByWebSrc` is the same lookup keyed by the web
|
|
@@ -837,10 +840,14 @@ export function renderSitePage(
|
|
|
837
840
|
});
|
|
838
841
|
expressionErrors.push(...expressions.findings);
|
|
839
842
|
const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
843
|
+
const figured = renderFigureBlocks(
|
|
844
|
+
protectCode(expressions.markdown, resolve),
|
|
845
|
+
undefined,
|
|
846
|
+
undefined,
|
|
847
|
+
{
|
|
848
|
+
resolveRole: (address) => roleByWebSrc.get(address),
|
|
849
|
+
},
|
|
850
|
+
);
|
|
844
851
|
for (const error of figureScan.errors)
|
|
845
852
|
captionErrors.push({
|
|
846
853
|
file: page.file,
|
|
@@ -848,7 +855,12 @@ export function renderSitePage(
|
|
|
848
855
|
column: error.column,
|
|
849
856
|
message: error.message,
|
|
850
857
|
});
|
|
851
|
-
for (const error of
|
|
858
|
+
for (const error of [
|
|
859
|
+
...scanBlocks(page.body).errors,
|
|
860
|
+
...scanAlerts(page.body).errors,
|
|
861
|
+
...scanSpans(page.body).errors,
|
|
862
|
+
...markupAnchorFindings(page.body),
|
|
863
|
+
])
|
|
852
864
|
secretErrors.push({
|
|
853
865
|
file: page.file,
|
|
854
866
|
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
@@ -877,7 +889,10 @@ export function renderSitePage(
|
|
|
877
889
|
// The disclosure is written last, over the Markdown the page ships: the
|
|
878
890
|
// passes before this one carry line positions into their findings, and a
|
|
879
891
|
// line inserted ahead of them would move every one of them.
|
|
880
|
-
body: renderWithheldSections(
|
|
892
|
+
body: renderWithheldSections(
|
|
893
|
+
renderSpans(renderAlerts(renderBlocks(figured.markdown, "web").markdown).markdown)
|
|
894
|
+
.markdown,
|
|
895
|
+
),
|
|
881
896
|
data,
|
|
882
897
|
resolved,
|
|
883
898
|
tableErrors,
|
|
@@ -1053,13 +1068,26 @@ export function renderPages(pages, options) {
|
|
|
1053
1068
|
}
|
|
1054
1069
|
|
|
1055
1070
|
const related = relatedPages(edges, entries);
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1071
|
+
// Geography follows parents; governing-body lists follow explicit government
|
|
1072
|
+
// references across this package and every fetched index.
|
|
1073
|
+
const holdings = holdingsPages(
|
|
1074
|
+
[
|
|
1075
|
+
...pages.map((page) =>
|
|
1076
|
+
holdingsNode(page.fm, { title: pageTitle(page), url: page.url, package: page.pkg }),
|
|
1077
|
+
),
|
|
1078
|
+
...foreignHoldingsNodes(foreign?.index),
|
|
1079
|
+
],
|
|
1080
|
+
{
|
|
1081
|
+
governmentNodes: records
|
|
1082
|
+
.filter((record) => !isGmNote(record))
|
|
1083
|
+
.map((record) =>
|
|
1084
|
+
holdingsNode(record, {
|
|
1085
|
+
title: record.name?.full ?? record.shortcode,
|
|
1086
|
+
package: config?.contentPackage ?? pages[0]?.pkg,
|
|
1087
|
+
}),
|
|
1088
|
+
),
|
|
1089
|
+
},
|
|
1090
|
+
);
|
|
1063
1091
|
|
|
1064
1092
|
let withMap = 0;
|
|
1065
1093
|
const outputs = capture === true ? new Map() : null;
|
package/engine/site-preview.mjs
CHANGED
|
@@ -284,12 +284,28 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
284
284
|
});
|
|
285
285
|
const related = relatedPages(edges, entries).get(page.url);
|
|
286
286
|
if (related) result.data.related = related;
|
|
287
|
-
const holdings = holdingsPages(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
const holdings = holdingsPages(
|
|
288
|
+
[
|
|
289
|
+
...allPages.map((item) =>
|
|
290
|
+
holdingsNode(item.fm, {
|
|
291
|
+
title: item.name,
|
|
292
|
+
url: item.url,
|
|
293
|
+
package: item.pkg ?? config.contentPackage,
|
|
294
|
+
}),
|
|
295
|
+
),
|
|
296
|
+
...foreignHoldingsNodes(snapshot.gates.foreign.index),
|
|
297
|
+
],
|
|
298
|
+
{
|
|
299
|
+
governmentNodes: records
|
|
300
|
+
.filter((record) => !isGmNote(record))
|
|
301
|
+
.map((record) =>
|
|
302
|
+
holdingsNode(record, {
|
|
303
|
+
title: record.name?.full ?? record.shortcode,
|
|
304
|
+
package: config.contentPackage,
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
},
|
|
308
|
+
);
|
|
293
309
|
Object.assign(result.data, holdings.get(page.url));
|
|
294
310
|
return {
|
|
295
311
|
ok: true,
|