@heroiclands/package-build 25.5.5 → 25.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/assets/styles/content-base.css +1 -1
- package/docs/api.md +24 -16
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +10 -9
- package/docs/content-format.md +40 -1
- package/docs/diagnostics.md +2 -2
- package/docs/encoding-core.md +43 -5
- package/docs/guides/reference-pages.md +4 -1
- package/docs/guides/site.md +1 -1
- package/docs/reference/format-details.md +239 -194
- package/docs/reference/note-types.md +187 -174
- package/docs/reference/theme-pages.md +1 -0
- package/engine/content-format.yaml +9 -7
- package/engine/content-html.mjs +3 -2
- package/engine/content-index.mjs +23 -0
- package/engine/content-links.mjs +8 -0
- package/engine/content-render.mjs +2 -2
- package/engine/content-tables.mjs +12 -5
- package/engine/culture-reach.mjs +3 -0
- package/engine/declared-sections.mjs +100 -0
- package/engine/derived-sections.mjs +36 -2
- package/engine/dictionary-rows.mjs +2 -1
- package/engine/encoding-template.mjs +28 -0
- package/engine/encoding-terms.mjs +20 -12
- package/engine/encoding.mjs +147 -17
- package/engine/generated-sections.mjs +2 -0
- package/engine/holdings.mjs +107 -3
- package/engine/index-records.mjs +3 -1
- package/engine/index.mjs +1 -1
- package/engine/language-data.mjs +0 -1
- package/engine/language-rows.mjs +1 -3
- package/engine/language-terms.mjs +7 -10
- package/engine/map-dot.mjs +22 -0
- package/engine/markdown-blocks.mjs +23 -8
- package/engine/metadata-index.mjs +1 -0
- package/engine/note-addresses.mjs +16 -0
- package/engine/note-vocabulary.mjs +19 -8
- package/engine/office-holders.mjs +76 -0
- package/engine/page-breaks.mjs +36 -8
- package/engine/pdf-render.mjs +4 -3
- package/engine/place-kinds.mjs +106 -0
- package/engine/settlement-tables.mjs +410 -675
- package/engine/settlement-terms.mjs +118 -0
- package/engine/site-build.mjs +2 -4
- package/engine/sql-tables.mjs +101 -7
- package/engine/standing-terms.mjs +9 -5
- package/engine/standings.mjs +83 -31
- package/engine/unique-items.mjs +85 -0
- package/package.json +9 -4
- package/site-theme/data/banner-kinds.yaml +2 -0
- package/sohl/actors.mjs +4 -3
- package/types/engine/content-html.d.mts +2 -1
- package/types/engine/content-scripts.d.mts +6 -0
- package/types/engine/culture-reach.d.mts +3 -0
- package/types/engine/declared-sections.d.mts +30 -0
- package/types/engine/derived-sections.d.mts +5 -1
- package/types/engine/encoding-lexicon.d.mts +6 -0
- package/types/engine/encoding-template.d.mts +6 -0
- package/types/engine/encoding.d.mts +12 -0
- package/types/engine/holdings.d.mts +34 -2
- package/types/engine/index-records.d.mts +2 -1
- package/types/engine/language-terms.d.mts +5 -1
- package/types/engine/markdown-blocks.d.mts +5 -1
- package/types/engine/office-holders.d.mts +33 -0
- package/types/engine/page-breaks.d.mts +8 -0
- package/types/engine/place-kinds.d.mts +18 -0
- package/types/engine/settlement-tables.d.mts +25 -158
- package/types/engine/settlement-terms.d.mts +53 -0
- package/types/engine/sql-tables.d.mts +35 -3
- package/types/engine/standing-terms.d.mts +3 -2
- package/types/engine/standings.d.mts +14 -2
- package/types/engine/unique-items.d.mts +12 -0
- package/vendor/markdownlint.mjs +855 -569
package/engine/encoding.mjs
CHANGED
|
@@ -470,7 +470,11 @@ function checkGroupCase(enc, groups, text, at, bound) {
|
|
|
470
470
|
* @param {string} text
|
|
471
471
|
* @param {{font?: string, verse?: boolean, start?: number, end?: number}} [options]
|
|
472
472
|
*/
|
|
473
|
-
function encodeRawPieces(
|
|
473
|
+
function encodeRawPieces(
|
|
474
|
+
enc,
|
|
475
|
+
text,
|
|
476
|
+
{ font, verse = false, start = 0, end = text.length, stops = [] } = {},
|
|
477
|
+
) {
|
|
474
478
|
const groups = tiers(enc, font);
|
|
475
479
|
if (typeof text !== "string") throw new EncodingError("Text must be a string");
|
|
476
480
|
if (
|
|
@@ -485,9 +489,13 @@ function encodeRawPieces(enc, text, { font, verse = false, start = 0, end = text
|
|
|
485
489
|
});
|
|
486
490
|
const bound = end;
|
|
487
491
|
const out = [];
|
|
488
|
-
let at = start
|
|
492
|
+
let at = start,
|
|
493
|
+
stop = 0;
|
|
489
494
|
while (at < bound) {
|
|
490
495
|
const first = firstAt(text, at);
|
|
496
|
+
while (stop < stops.length && stops[stop] <= at) stop++;
|
|
497
|
+
// A row never spans the start of a diaeresis vowel written by its plain vowel.
|
|
498
|
+
const limit = Math.min(bound, stops[stop] ?? bound);
|
|
491
499
|
if (first === OPEN) {
|
|
492
500
|
const close = span(text.slice(0, bound), at, OPEN, CLOSE);
|
|
493
501
|
out.push(
|
|
@@ -545,7 +553,7 @@ function encodeRawPieces(enc, text, { font, verse = false, start = 0, end = text
|
|
|
545
553
|
continue;
|
|
546
554
|
}
|
|
547
555
|
if (!enc.template) checkGroupCase(enc, groups, text, at, bound);
|
|
548
|
-
const row = enc.accepted.has(first) ? rowAt(enc, groups, text, at,
|
|
556
|
+
const row = enc.accepted.has(first) ? rowAt(enc, groups, text, at, limit) : null;
|
|
549
557
|
if (!row)
|
|
550
558
|
refused(
|
|
551
559
|
enc,
|
|
@@ -688,7 +696,13 @@ function sourceDetails(enc, text, options) {
|
|
|
688
696
|
pointSlice(normalized.slice(0, bound), at, enc.template.maxVowelGroup)
|
|
689
697
|
: "";
|
|
690
698
|
const lower = lookahead.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
691
|
-
const group = enc.template?.vowelGroups.find(
|
|
699
|
+
const group = enc.template?.vowelGroups.find(
|
|
700
|
+
(value) =>
|
|
701
|
+
lower.startsWith(value) &&
|
|
702
|
+
[...lookahead]
|
|
703
|
+
.slice(1, [...value].length)
|
|
704
|
+
.every((letter) => !/[\u0308\u0300]/u.test(letter.normalize("NFD"))),
|
|
705
|
+
);
|
|
692
706
|
if (group) {
|
|
693
707
|
let cursor = at;
|
|
694
708
|
for (let i = 0; i < [...group].length; i++) {
|
|
@@ -713,7 +727,16 @@ function sourceDetails(enc, text, options) {
|
|
|
713
727
|
});
|
|
714
728
|
throw error;
|
|
715
729
|
}
|
|
716
|
-
|
|
730
|
+
const letters = enc.template?.apart?.letters;
|
|
731
|
+
if (!letters) return { ...projection, folded: normalized, stops: [] };
|
|
732
|
+
const stops = [];
|
|
733
|
+
let folded = "";
|
|
734
|
+
for (let at = 0; at < normalized.length; at++) {
|
|
735
|
+
const plain = at >= begin && at < bound ? letters[normalized[at]] : undefined;
|
|
736
|
+
if (plain) stops.push(at);
|
|
737
|
+
folded += plain ?? normalized[at];
|
|
738
|
+
}
|
|
739
|
+
return { ...projection, folded, stops };
|
|
717
740
|
}
|
|
718
741
|
|
|
719
742
|
/**
|
|
@@ -736,16 +759,49 @@ export function validateSource(enc, text, options = {}) {
|
|
|
736
759
|
*/
|
|
737
760
|
export function encodePieces(enc, text, options = {}) {
|
|
738
761
|
if (!enc?.template) return encodeRawPieces(enc, text, options);
|
|
739
|
-
const { normalized, sourceAt, begin, bound } = sourceDetails(enc, text, options);
|
|
762
|
+
const { normalized, folded, stops, sourceAt, begin, bound } = sourceDetails(enc, text, options);
|
|
740
763
|
try {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
764
|
+
const pieces = encodeRawPieces(enc, folded, {
|
|
765
|
+
...options,
|
|
766
|
+
start: begin,
|
|
767
|
+
end: bound,
|
|
768
|
+
stops,
|
|
769
|
+
});
|
|
770
|
+
if (enc.roundTrip === "exact" && stops.length) {
|
|
771
|
+
const native = pieces.map((one) => one.to).join("");
|
|
772
|
+
const decoded = decodeTemplatePieces(enc, native, options);
|
|
773
|
+
let nativeAt = 0;
|
|
774
|
+
for (const one of pieces) {
|
|
775
|
+
const expected = normalized.slice(one.start, one.end);
|
|
776
|
+
const marked = stops.find(
|
|
777
|
+
(at) =>
|
|
778
|
+
at >= one.start &&
|
|
779
|
+
at < one.end &&
|
|
780
|
+
splitsDeclaredGroup(enc, normalized, at, begin, bound),
|
|
781
|
+
);
|
|
782
|
+
if (
|
|
783
|
+
marked !== undefined &&
|
|
784
|
+
!decoded.some(
|
|
785
|
+
(step) =>
|
|
786
|
+
step.start === nativeAt &&
|
|
787
|
+
step.end === nativeAt + one.to.length &&
|
|
788
|
+
step.from === expected,
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
textError(
|
|
792
|
+
folded,
|
|
793
|
+
marked,
|
|
794
|
+
"The encoding cannot distinguish this diaeresis split in an exact round trip",
|
|
795
|
+
);
|
|
796
|
+
nativeAt += one.to.length;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return pieces.map((one) => ({
|
|
800
|
+
...one,
|
|
801
|
+
from: text.slice(sourceAt[one.start], sourceAt[one.end]),
|
|
802
|
+
start: sourceAt[one.start],
|
|
803
|
+
end: sourceAt[one.end],
|
|
804
|
+
}));
|
|
749
805
|
} catch (error) {
|
|
750
806
|
if (error instanceof EncodingError && error.offset !== undefined)
|
|
751
807
|
throw new EncodingError(error.message, {
|
|
@@ -845,6 +901,12 @@ function decodeRawText(enc, text, { font } = {}) {
|
|
|
845
901
|
*/
|
|
846
902
|
export function decodeText(enc, text, options = {}) {
|
|
847
903
|
if (!enc?.template) return decodeRawText(enc, text, options);
|
|
904
|
+
return decodeTemplatePieces(enc, text, options)
|
|
905
|
+
.map((one) => one.from)
|
|
906
|
+
.join("");
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function decodeTemplatePieces(enc, text, options = {}) {
|
|
848
910
|
const readings = tiers(enc, options.font)[0].readings;
|
|
849
911
|
if (typeof text !== "string") throw new EncodingError("Text must be a string");
|
|
850
912
|
if (!templateReaders.has(enc)) {
|
|
@@ -905,7 +967,7 @@ export function decodeText(enc, text, options = {}) {
|
|
|
905
967
|
failedContext = Math.min(failedContext, at);
|
|
906
968
|
continue;
|
|
907
969
|
}
|
|
908
|
-
paths.set(at, { from: row.from, next });
|
|
970
|
+
paths.set(at, { from: row.from, row, next });
|
|
909
971
|
}
|
|
910
972
|
}
|
|
911
973
|
if (!paths.has(0)) {
|
|
@@ -950,8 +1012,76 @@ export function decodeText(enc, text, options = {}) {
|
|
|
950
1012
|
const out = [];
|
|
951
1013
|
for (let at = 0; at < text.length;) {
|
|
952
1014
|
const step = paths.get(at);
|
|
953
|
-
out.push(step.from);
|
|
1015
|
+
out.push({ from: step.from, row: step.row, start: at, end: step.next });
|
|
954
1016
|
at = step.next;
|
|
955
1017
|
}
|
|
956
|
-
|
|
1018
|
+
const apart = enc.template.apart;
|
|
1019
|
+
if (!apart) return out;
|
|
1020
|
+
return restoreDiaeresis(enc, out, options);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Restore only joins contradicted by the selected native row boundaries. */
|
|
1024
|
+
function restoreDiaeresis(enc, pieces, options) {
|
|
1025
|
+
const text = pieces.map((one) => one.from).join("");
|
|
1026
|
+
const letters = [];
|
|
1027
|
+
let offset = 0;
|
|
1028
|
+
for (const [part, piece] of pieces.entries()) {
|
|
1029
|
+
for (const char of piece.from) {
|
|
1030
|
+
letters.push({ char, part, offset, literal: !piece.row });
|
|
1031
|
+
offset += char.length;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const groups = tiers(enc, options.font);
|
|
1035
|
+
for (let i = 0; i < letters.length; i++) {
|
|
1036
|
+
if (letters[i].literal) continue;
|
|
1037
|
+
const group = enc.template.apart.groups.find((value) => {
|
|
1038
|
+
const span = [...value];
|
|
1039
|
+
if (
|
|
1040
|
+
!span.every((char, k) => {
|
|
1041
|
+
const letter = letters[i + k];
|
|
1042
|
+
return (
|
|
1043
|
+
letter &&
|
|
1044
|
+
!letter.literal &&
|
|
1045
|
+
letter.char.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase() ===
|
|
1046
|
+
char &&
|
|
1047
|
+
(k === 0 || letter.char.normalize("NFD").length === 1)
|
|
1048
|
+
);
|
|
1049
|
+
})
|
|
1050
|
+
)
|
|
1051
|
+
return false;
|
|
1052
|
+
const row = rowAt(enc, groups, text, letters[i].offset);
|
|
1053
|
+
return row && row.from.toLowerCase() === value;
|
|
1054
|
+
});
|
|
1055
|
+
if (!group) continue;
|
|
1056
|
+
const end = i + [...group].length;
|
|
1057
|
+
let boundary = i + 1;
|
|
1058
|
+
while (boundary < end && letters[boundary].part === letters[i].part) boundary++;
|
|
1059
|
+
if (boundary === end) {
|
|
1060
|
+
i = end - 1;
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const letter = letters[boundary];
|
|
1064
|
+
const marked = (letter.char + "\u0308").normalize("NFC");
|
|
1065
|
+
if (enc.template.apart.letters[marked]) {
|
|
1066
|
+
letter.char = marked;
|
|
1067
|
+
i = boundary - 1;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const output = pieces.map((piece) => ({ ...piece, from: "" }));
|
|
1071
|
+
for (const letter of letters) output[letter.part].from += letter.char;
|
|
1072
|
+
return output;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Whether a derived mark actually splits one of this note's declared groups. */
|
|
1076
|
+
function splitsDeclaredGroup(enc, text, at, begin, bound) {
|
|
1077
|
+
for (const group of enc.template.apart.groups) {
|
|
1078
|
+
const count = [...group].length;
|
|
1079
|
+
// The supported diaeresis letters are one NFC UTF-16 unit each.
|
|
1080
|
+
for (let start = Math.max(begin, at - count + 1); start < at; start++) {
|
|
1081
|
+
const span = pointSlice(text.slice(0, bound), start, count);
|
|
1082
|
+
if (start + span.length <= at) continue;
|
|
1083
|
+
if (span.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase() === group) return true;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
957
1087
|
}
|
|
@@ -57,9 +57,11 @@ export const CHRONOLOGY_SLUG = "chronology";
|
|
|
57
57
|
*/
|
|
58
58
|
export const GENERATED_SECTIONS = Object.freeze(
|
|
59
59
|
[
|
|
60
|
+
{ slug: "settlements", heading: "Settlements" },
|
|
60
61
|
{ slug: "within", heading: "Within" },
|
|
61
62
|
{ slug: "governedby", heading: "Governed by" },
|
|
62
63
|
{ slug: "governedplaces", heading: "Governed places" },
|
|
64
|
+
{ slug: "officeholders", heading: "Office holders" },
|
|
63
65
|
{ slug: CHRONOLOGY_SLUG, heading: "Chronology" },
|
|
64
66
|
{ slug: "events", heading: "Events" },
|
|
65
67
|
{ slug: "accounts", heading: "Accounts" },
|
package/engine/holdings.mjs
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Derive containment and government lists from place facts. Geography comes
|
|
16
16
|
* only from `data.parents`; government comes only from `data.government`.
|
|
17
|
-
* Neither graph expands through
|
|
17
|
+
* Neither graph expands through affiliation ancestors, and government expands
|
|
18
|
+
* through geographic ones only for a district stating none, which the content
|
|
19
|
+
* index resolves before these lists read it. Government identity preserves the Address package, type and shortcode.
|
|
18
20
|
* Its system normalizes to `note` because Item and journal documents share one page.
|
|
19
21
|
* The lists become a note's generated **Within**, **Governed by** and
|
|
20
22
|
* **Governed places** sections — see {@link module:engine/derived-sections}.
|
|
@@ -255,6 +257,106 @@ export function holdingsPages(nodes, { governmentNodes = [] } = {}) {
|
|
|
255
257
|
return lists;
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
/* --------------------------------------------------------------------- */
|
|
261
|
+
/* A district's government */
|
|
262
|
+
/* --------------------------------------------------------------------- */
|
|
263
|
+
|
|
264
|
+
/** Districts whose government the index supplied from a parent. */
|
|
265
|
+
const inheritedGovernment = new WeakSet();
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Whether a record's `data.government` came from a parent rather than its own
|
|
269
|
+
* note.
|
|
270
|
+
*
|
|
271
|
+
* @param {object} record - An index record.
|
|
272
|
+
* @returns {boolean} True for a district that inherited its government.
|
|
273
|
+
*/
|
|
274
|
+
export function governmentInherited(record) {
|
|
275
|
+
return inheritedGovernment.has(record);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Give each district that states no government its parent's.
|
|
280
|
+
*
|
|
281
|
+
* A `place/district` falls under the government of the place it stands
|
|
282
|
+
* within. One stating no `data.government` key takes the value of its nearest
|
|
283
|
+
* parent that states the key, walking `data.parents` breadth first, in the
|
|
284
|
+
* order written, through any parent that states none, and never visiting a
|
|
285
|
+
* place twice. An explicit value on the district, `null` (complete anarchy)
|
|
286
|
+
* included, is its own and is kept; a parent's explicit `null` is inherited as
|
|
287
|
+
* anarchy. Every reader of the index — the Governed by and Governed places
|
|
288
|
+
* sections, the "missing government" advisory, a consumer of the published
|
|
289
|
+
* index — then reads the effective government at `data.government`.
|
|
290
|
+
*
|
|
291
|
+
* @param {object[]} records - Content-index records, edited in place.
|
|
292
|
+
* @param {object} [opts]
|
|
293
|
+
* @param {(key: string) => object|undefined} [opts.foreignRecord] - A fetched
|
|
294
|
+
* index's record by canonical Address, for a parent in a dependency.
|
|
295
|
+
* @returns {void}
|
|
296
|
+
*/
|
|
297
|
+
export function inheritDistrictGovernments(records, { foreignRecord } = {}) {
|
|
298
|
+
const keyOf = (pkg, shortcode) =>
|
|
299
|
+
renderAddress({
|
|
300
|
+
package: String(pkg ?? "local"),
|
|
301
|
+
system: "note",
|
|
302
|
+
type: "place",
|
|
303
|
+
shortcode: String(shortcode).toLowerCase(),
|
|
304
|
+
});
|
|
305
|
+
const places = new Map();
|
|
306
|
+
for (const record of records) {
|
|
307
|
+
if (record?.type !== "place" || record.documents || !record.shortcode) continue;
|
|
308
|
+
const key = keyOf(record.package, record.shortcode);
|
|
309
|
+
if (!places.has(key)) places.set(key, record);
|
|
310
|
+
}
|
|
311
|
+
const parentKeys = (record) => {
|
|
312
|
+
const parents = record?.data?.parents ?? record?.parents;
|
|
313
|
+
const list =
|
|
314
|
+
Array.isArray(parents) ? parents
|
|
315
|
+
: parents == null ? []
|
|
316
|
+
: [parents];
|
|
317
|
+
return list
|
|
318
|
+
.map((value) => {
|
|
319
|
+
if (isAddressTuple(value)) return keyOf(value.package, value.shortcode);
|
|
320
|
+
const tuple =
|
|
321
|
+
readCanonicalKey(String(value ?? "")) ??
|
|
322
|
+
parseAddress(value, {
|
|
323
|
+
package: record.package ?? "local",
|
|
324
|
+
system: "note",
|
|
325
|
+
type: "place",
|
|
326
|
+
types: new Set(["place"]),
|
|
327
|
+
});
|
|
328
|
+
return isAddressTuple(tuple) ? keyOf(tuple.package, tuple.shortcode) : "";
|
|
329
|
+
})
|
|
330
|
+
.filter(Boolean);
|
|
331
|
+
};
|
|
332
|
+
const stated = (record) => {
|
|
333
|
+
const data = record?.data && typeof record.data === "object" ? record.data : record;
|
|
334
|
+
return data && Object.hasOwn(data, "government") && !inheritedGovernment.has(record) ?
|
|
335
|
+
{ value: data.government }
|
|
336
|
+
: undefined;
|
|
337
|
+
};
|
|
338
|
+
for (const district of places.values()) {
|
|
339
|
+
if (district.subType !== "district") continue;
|
|
340
|
+
const data = district.data && typeof district.data === "object" ? district.data : {};
|
|
341
|
+
if (Object.hasOwn(data, "government")) continue;
|
|
342
|
+
const seen = new Set([keyOf(district.package, district.shortcode)]);
|
|
343
|
+
const queue = parentKeys(district);
|
|
344
|
+
let found;
|
|
345
|
+
while (queue.length && !found) {
|
|
346
|
+
const key = queue.shift();
|
|
347
|
+
if (seen.has(key)) continue;
|
|
348
|
+
seen.add(key);
|
|
349
|
+
const parent = places.get(key) ?? foreignRecord?.(key);
|
|
350
|
+
if (!parent) continue;
|
|
351
|
+
found = stated(parent);
|
|
352
|
+
if (!found) queue.push(...parentKeys(parent));
|
|
353
|
+
}
|
|
354
|
+
if (!found) continue;
|
|
355
|
+
district.data = { ...data, government: found.value };
|
|
356
|
+
inheritedGovernment.add(district);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
258
360
|
/* --------------------------------------------------------------------- */
|
|
259
361
|
/* The lint */
|
|
260
362
|
/* --------------------------------------------------------------------- */
|
|
@@ -264,8 +366,10 @@ export function holdingsPages(nodes, { governmentNodes = [] } = {}) {
|
|
|
264
366
|
* Missing, null or zero population establishes no inhabited population.
|
|
265
367
|
* Property presence distinguishes omitted government from explicit null,
|
|
266
368
|
* which means complete anarchy. Non-null values are validated by the declared
|
|
267
|
-
* affiliation reference field.
|
|
268
|
-
* supplies
|
|
369
|
+
* affiliation reference field. Tenure never supplies this fact, and
|
|
370
|
+
* containment supplies it only to a district, which the content index gives
|
|
371
|
+
* its nearest parent's government (see {@link inheritDistrictGovernments}), so
|
|
372
|
+
* a district under a governed place is never advised.
|
|
269
373
|
*
|
|
270
374
|
* @param {object} note - The note with parsed frontmatter and raw source.
|
|
271
375
|
* @returns {object[]} Located warning findings.
|
package/engine/index-records.mjs
CHANGED
|
@@ -42,7 +42,8 @@ import { isAddressTuple } from "./address.mjs";
|
|
|
42
42
|
* since a note declaring its own is a hard error — and it matches what
|
|
43
43
|
* the content-table expander puts on the same field, so a query reads the same
|
|
44
44
|
* value from either. `file` namespaces the note's place in the tree, again
|
|
45
|
-
* matching the expander's `file.*`.
|
|
45
|
+
* matching the expander's `file.*`. `officeHolders` lists, on a body, the
|
|
46
|
+
* living beings whose own standing names each of its offices.
|
|
46
47
|
*
|
|
47
48
|
* Both are checked rather than assumed: `folder` is real frontmatter on most
|
|
48
49
|
* notes, so the neighbouring names are close enough to a real key that a silent
|
|
@@ -68,6 +69,7 @@ export const DERIVED_KEYS = Object.freeze([
|
|
|
68
69
|
"citations",
|
|
69
70
|
"names",
|
|
70
71
|
"runs",
|
|
72
|
+
"officeHolders",
|
|
71
73
|
]);
|
|
72
74
|
|
|
73
75
|
/**
|
package/engine/index.mjs
CHANGED
|
@@ -103,7 +103,7 @@ export * as populations from "./populations.mjs";
|
|
|
103
103
|
/** What trade a settlement supports: the six-step market scale, and the check that holds a value to it. */
|
|
104
104
|
export * as marketClass from "./market-class.mjs";
|
|
105
105
|
|
|
106
|
-
/** A region's settlements table:
|
|
106
|
+
/** A region's settlements table: its rows, the columns, and the check that holds a table to them. */
|
|
107
107
|
export * as settlementTables from "./settlement-tables.mjs";
|
|
108
108
|
|
|
109
109
|
/** The places a map is drawn from, read from the content index, and what `parents` says about them. */
|
package/engine/language-data.mjs
CHANGED
|
@@ -1071,7 +1071,6 @@ export function checkLanguageData(note, { index, types } = {}) {
|
|
|
1071
1071
|
for (const kind of SCOPE_KINDS) {
|
|
1072
1072
|
listOf(scope[kind]).forEach((value, i) => {
|
|
1073
1073
|
const path = ["data", "scope", kind, i];
|
|
1074
|
-
if (kind === "paths") return;
|
|
1075
1074
|
if (kind === "types") {
|
|
1076
1075
|
if (types && typeof value === "string" && !types.includes(value))
|
|
1077
1076
|
findings.push(finding(path, "error", `\`${value}\` is not a note type`));
|
package/engine/language-rows.mjs
CHANGED
|
@@ -413,14 +413,12 @@ export function inScope(record, address, scope) {
|
|
|
413
413
|
const wanted = values(kind);
|
|
414
414
|
return strings(list).some((one) => wanted.has(one));
|
|
415
415
|
};
|
|
416
|
-
const file = typeof record.file?.path === "string" ? record.file.path : "";
|
|
417
416
|
return (
|
|
418
417
|
values("addresses").has(address) ||
|
|
419
418
|
any("folders", data.packFolder) ||
|
|
420
419
|
any("cultures", data.culture) ||
|
|
421
420
|
any("lore", data.lore) ||
|
|
422
|
-
any("parents", data.parents)
|
|
423
|
-
strings(scope.paths).some((prefix) => file.startsWith(prefix))
|
|
421
|
+
any("parents", data.parents)
|
|
424
422
|
);
|
|
425
423
|
}
|
|
426
424
|
|
|
@@ -152,8 +152,12 @@ export const JOIN_MEETINGS = Object.freeze(["C+C", "V+C", "V+V"]);
|
|
|
152
152
|
*/
|
|
153
153
|
export const JOIN_RULES = Object.freeze(["merge", "insert", "drop", "double"]);
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
155
|
+
/**
|
|
156
|
+
* What a written mark says of its letter: `stress` falls on it, `length` holds
|
|
157
|
+
* it longer, `apart` says it apart from its neighbour, `quality` changes
|
|
158
|
+
* which vowel is said, and `tone` marks a rising or falling pitch.
|
|
159
|
+
*/
|
|
160
|
+
export const MARK_KINDS = Object.freeze(["stress", "length", "apart", "quality", "tone"]);
|
|
157
161
|
|
|
158
162
|
/** How many of a mark a word may carry: `one` at most, or `any` number. */
|
|
159
163
|
export const MARK_LIMITS = Object.freeze(["one", "any"]);
|
|
@@ -320,13 +324,6 @@ export const SCOPE_FIELDS = Object.freeze([
|
|
|
320
324
|
entries: Object.freeze({ kind: "address", shape: "an Address" }),
|
|
321
325
|
describe: "These notes themselves.",
|
|
322
326
|
}),
|
|
323
|
-
Object.freeze({
|
|
324
|
-
name: "paths",
|
|
325
|
-
kind: "list",
|
|
326
|
-
shape: "a list of content paths",
|
|
327
|
-
entries: Object.freeze({ kind: "string", shape: "a path such as `Regions/Aelwyth/`" }),
|
|
328
|
-
describe: "Every note whose file sits below one of these paths of the content tree.",
|
|
329
|
-
}),
|
|
330
327
|
Object.freeze({
|
|
331
328
|
name: "types",
|
|
332
329
|
kind: "list",
|
|
@@ -1926,7 +1923,7 @@ export const LANGUAGE_DATA_FIELDS = Object.freeze([
|
|
|
1926
1923
|
Object.freeze({
|
|
1927
1924
|
name: "scope",
|
|
1928
1925
|
kind: "map",
|
|
1929
|
-
shape: "a map — `{ folders?, cultures?, lore?, parents?, addresses?,
|
|
1926
|
+
shape: "a map — `{ folders?, cultures?, lore?, parents?, addresses?, types?, skip? }`",
|
|
1930
1927
|
fields: SCOPE_FIELDS,
|
|
1931
1928
|
describe: "The notes the register covers. A language skill note only.",
|
|
1932
1929
|
}),
|
package/engine/map-dot.mjs
CHANGED
|
@@ -131,6 +131,26 @@ export const STYLE = Object.freeze({
|
|
|
131
131
|
penwidth: 2.0,
|
|
132
132
|
extraStyle: "rounded",
|
|
133
133
|
},
|
|
134
|
+
district: {
|
|
135
|
+
shape: "box",
|
|
136
|
+
fill: "#C5E1A5",
|
|
137
|
+
border: DEFAULT_BORDER,
|
|
138
|
+
fontsize: 9,
|
|
139
|
+
width: 0.8,
|
|
140
|
+
height: 0.4,
|
|
141
|
+
penwidth: 1.0,
|
|
142
|
+
extraStyle: "rounded,dashed",
|
|
143
|
+
},
|
|
144
|
+
route: {
|
|
145
|
+
shape: "parallelogram",
|
|
146
|
+
fill: "#FFE082",
|
|
147
|
+
border: DEFAULT_BORDER,
|
|
148
|
+
fontsize: 9,
|
|
149
|
+
width: 0.9,
|
|
150
|
+
height: 0.4,
|
|
151
|
+
penwidth: 1.0,
|
|
152
|
+
extraStyle: "",
|
|
153
|
+
},
|
|
134
154
|
settlement: {
|
|
135
155
|
shape: "ellipse",
|
|
136
156
|
fill: "#A5D6A7",
|
|
@@ -187,10 +207,12 @@ export const STYLE = Object.freeze({
|
|
|
187
207
|
const LEGEND = Object.freeze([
|
|
188
208
|
["world", "the root note"],
|
|
189
209
|
["region", "continent, region, sub-region"],
|
|
210
|
+
["district", "quarter, ward, shire"],
|
|
190
211
|
["settlement", "city, town, village"],
|
|
191
212
|
["site", "ruin, landmark, dungeon, wonder"],
|
|
192
213
|
["feature", "river, mountain range, sea"],
|
|
193
214
|
["structure", "a single building"],
|
|
215
|
+
["route", "road, pass, sea lane"],
|
|
194
216
|
["polity", "affiliation, dashed edge to its region"],
|
|
195
217
|
]);
|
|
196
218
|
|
|
@@ -25,35 +25,50 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import MarkdownIt from "markdown-it";
|
|
28
|
+
import footnotePlugin from "markdown-it-footnote";
|
|
28
29
|
import { verseDivPlugin } from "./verse-regions.mjs";
|
|
29
30
|
|
|
30
31
|
/** The renderer's block grammar, with inline parsing and its passes switched off. */
|
|
31
32
|
const parser = new MarkdownIt({ html: true }).use(verseDivPlugin);
|
|
32
|
-
|
|
33
|
+
const footnoteParser = new MarkdownIt({ html: true }).use(footnotePlugin).use(verseDivPlugin);
|
|
34
|
+
for (const projection of [parser, footnoteParser])
|
|
35
|
+
projection.core.ruler.disable([
|
|
36
|
+
"inline",
|
|
37
|
+
"linkify",
|
|
38
|
+
"replacements",
|
|
39
|
+
"smartquotes",
|
|
40
|
+
"text_join",
|
|
41
|
+
]);
|
|
42
|
+
// Keep definition containers and their original maps in place for scanners.
|
|
43
|
+
footnoteParser.core.ruler.disable("footnote_tail");
|
|
33
44
|
|
|
34
45
|
/** How many recent sources keep their tokens. */
|
|
35
46
|
const RECENT = 16;
|
|
36
47
|
|
|
37
48
|
/** @type {Map<string, import("markdown-it").Token[]>} */
|
|
38
49
|
const recent = new Map();
|
|
50
|
+
const recentFootnotes = new Map();
|
|
39
51
|
|
|
40
52
|
/**
|
|
41
53
|
* The block tokens of a Markdown source. The array is shared between callers
|
|
42
54
|
* and must not be modified; an `inline` token carries no `children`.
|
|
43
55
|
*
|
|
44
56
|
* @param {unknown} source - Markdown.
|
|
57
|
+
* @param {object} [options] - Optional block projection.
|
|
58
|
+
* @param {boolean} [options.footnotes=false] - Include footnote definition containers.
|
|
45
59
|
* @returns {readonly import("markdown-it").Token[]} The block tokens, in order.
|
|
46
60
|
*/
|
|
47
|
-
export function blockTokens(source) {
|
|
61
|
+
export function blockTokens(source, { footnotes = false } = {}) {
|
|
48
62
|
const text = String(source ?? "");
|
|
49
|
-
const
|
|
63
|
+
const cache = footnotes ? recentFootnotes : recent;
|
|
64
|
+
const hit = cache.get(text);
|
|
50
65
|
if (hit) {
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
cache.delete(text);
|
|
67
|
+
cache.set(text, hit);
|
|
53
68
|
return hit;
|
|
54
69
|
}
|
|
55
|
-
const tokens = parser.parse(text, { verseSource: text });
|
|
56
|
-
|
|
57
|
-
if (
|
|
70
|
+
const tokens = (footnotes ? footnoteParser : parser).parse(text, { verseSource: text });
|
|
71
|
+
cache.set(text, tokens);
|
|
72
|
+
if (cache.size > RECENT) cache.delete(cache.keys().next().value);
|
|
58
73
|
return tokens;
|
|
59
74
|
}
|
|
@@ -507,6 +507,7 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
507
507
|
}
|
|
508
508
|
: {}),
|
|
509
509
|
population: record.data?.population ?? undefined,
|
|
510
|
+
market: record.data?.market ?? undefined,
|
|
510
511
|
...(Object.hasOwn(record.data ?? {}, "government") ?
|
|
511
512
|
{ government: record.data.government }
|
|
512
513
|
: {}),
|
|
@@ -43,6 +43,22 @@ export {
|
|
|
43
43
|
|
|
44
44
|
/** Structured positions whose Address is nested inside a declared container. */
|
|
45
45
|
export const STRUCTURED_ADDRESSES = Object.freeze([
|
|
46
|
+
{
|
|
47
|
+
noteType: "place",
|
|
48
|
+
path: ["data", "settlements", "rows", "*", "place"],
|
|
49
|
+
shape: "value",
|
|
50
|
+
type: "place",
|
|
51
|
+
accepts: ["place"],
|
|
52
|
+
lenient: true,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
noteType: "place",
|
|
56
|
+
path: ["data", "settlements", "rows", "*", "heldBy"],
|
|
57
|
+
shape: "value",
|
|
58
|
+
type: "affiliation",
|
|
59
|
+
accepts: ["affiliation", "being"],
|
|
60
|
+
lenient: true,
|
|
61
|
+
},
|
|
46
62
|
{ path: ["data", "seeAlso", "*", "address"], shape: "value", lenient: true },
|
|
47
63
|
{
|
|
48
64
|
path: ["packFolder"],
|