@heroiclands/package-build 22.24.0 → 22.26.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 +14 -0
- package/README.md +2 -0
- package/assets/styles/content-base.css +45 -0
- package/ci/check-banners.mjs +183 -0
- package/ci/ci-steps.mjs +21 -11
- package/docs/api.md +14 -11
- package/docs/authoring/frontmatter.md +21 -0
- package/docs/authoring/links-and-markup.md +69 -2
- package/docs/content-format.md +11 -0
- package/docs/reference/format-details.md +50 -35
- package/docs/reference/note-types.md +27 -26
- package/docs/reference/theme-pages.md +4 -2
- package/engine/calendar-notes.mjs +1 -1
- package/engine/content-blocks.mjs +83 -14
- package/engine/content-figures.mjs +18 -3
- package/engine/content-format.yaml +1 -0
- package/engine/content-poetry.mjs +61 -0
- package/engine/holdings.mjs +38 -66
- package/engine/index.mjs +1 -1
- package/engine/infobox.mjs +10 -0
- package/engine/metadata-index.mjs +3 -0
- package/engine/note-vocabulary.mjs +18 -5
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-render.mjs +32 -2
- package/engine/rank-ladder.mjs +32 -0
- package/hugo-theme/data/banners.yaml +1 -1
- package/hugo-theme/static/css/style.css +29 -0
- package/package.json +4 -2
- package/types/engine/calendar-notes.d.mts +1 -1
- package/types/engine/content-figures.d.mts +1 -0
- package/types/engine/content-poetry.d.mts +19 -0
- package/types/engine/holdings.d.mts +20 -17
- package/types/engine/infobox.d.mts +1 -0
- package/types/engine/note-vocabulary.d.mts +4 -0
- package/types/engine/rank-ladder.d.mts +8 -0
package/engine/holdings.mjs
CHANGED
|
@@ -35,16 +35,15 @@
|
|
|
35
35
|
* package publishes is listed within a local region, and a house another
|
|
36
36
|
* package publishes is named on the local manor it holds.
|
|
37
37
|
*
|
|
38
|
-
* **
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* through its polity's `domains` and a feature by nobody, so both are exempt.
|
|
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.
|
|
42
41
|
*
|
|
43
42
|
* @module
|
|
44
43
|
*/
|
|
45
44
|
|
|
46
45
|
import { isAddressTuple } from "./address.mjs";
|
|
47
|
-
import {
|
|
46
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
48
47
|
import { parseAddress } from "./address.mjs";
|
|
49
48
|
import { readCanonicalKey } from "./content-address.mjs";
|
|
50
49
|
|
|
@@ -66,9 +65,9 @@ const PLACE_TYPES = Object.freeze(new Set(["place"]));
|
|
|
66
65
|
export const HOLDINGS_KEYS = Object.freeze(["contains", "held_by", "holdings"]);
|
|
67
66
|
|
|
68
67
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* Legacy subtypes of the retired tenure advisory. Government advisories
|
|
69
|
+
* apply to every place subtype when its authored population is positive.
|
|
70
|
+
* @deprecated This list no longer gates any advisory.
|
|
72
71
|
*
|
|
73
72
|
* @type {readonly string[]}
|
|
74
73
|
*/
|
|
@@ -373,71 +372,44 @@ export function holdingsPages(nodes) {
|
|
|
373
372
|
/* --------------------------------------------------------------------- */
|
|
374
373
|
|
|
375
374
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* The shortcodes every affiliation's `domains` names, across the link index.
|
|
387
|
-
*
|
|
388
|
-
* @param {object} index - The link index.
|
|
389
|
-
* @returns {Set<string>} The held shortcodes, lower case.
|
|
375
|
+
* Advise when a place with an authored positive population omits government.
|
|
376
|
+
* Missing, null or zero population establishes no inhabited population.
|
|
377
|
+
* Property presence distinguishes omitted government from explicit null,
|
|
378
|
+
* which means complete anarchy. Non-null values are validated by the declared
|
|
379
|
+
* affiliation reference field. Neither geographic containment nor tenure
|
|
380
|
+
* supplies this fact, and the rule is independent of place subtype.
|
|
381
|
+
*
|
|
382
|
+
* @param {object} note - The note with parsed frontmatter and raw source.
|
|
383
|
+
* @returns {object[]} Located warning findings.
|
|
390
384
|
*/
|
|
391
|
-
function
|
|
392
|
-
let held = HELD.get(index);
|
|
393
|
-
if (held) return held;
|
|
394
|
-
held = new Set();
|
|
395
|
-
for (const note of index.notes ?? []) {
|
|
396
|
-
if (String(note.type ?? note.fm?.type ?? "") !== "affiliation") continue;
|
|
397
|
-
const data = note.fm?.data;
|
|
398
|
-
const domains = data && typeof data === "object" ? data.domains : undefined;
|
|
399
|
-
for (const shortcode of shortcodesOf(domains)) held.add(shortcode);
|
|
400
|
-
}
|
|
401
|
-
for (const entry of index.foreign?.index?.values?.() ?? []) {
|
|
402
|
-
if (String(entry?.type ?? "") !== "affiliation") continue;
|
|
403
|
-
for (const shortcode of shortcodesOf(entry.domains)) held.add(shortcode);
|
|
404
|
-
}
|
|
405
|
-
HELD.set(index, held);
|
|
406
|
-
return held;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Check a place note's tenure: a settlement, a site or a structure that no
|
|
411
|
-
* affiliation's `domains` names is unheld land, reported as a warning at the
|
|
412
|
-
* note's `type:` line.
|
|
413
|
-
*
|
|
414
|
-
* Declared on the `place` vocabulary as its type-level check, so the lint
|
|
415
|
-
* runs it beside the field checks with the same index. Without an index the
|
|
416
|
-
* question cannot be asked and nothing is reported.
|
|
417
|
-
*
|
|
418
|
-
* @param {object} note - The note, as the link index hands it over.
|
|
419
|
-
* @param {object} [opts]
|
|
420
|
-
* @param {object} [opts.index] - The link index, holding every affiliation.
|
|
421
|
-
* @returns {object[]} Findings.
|
|
422
|
-
*/
|
|
423
|
-
export function checkHeld(note, { index } = {}) {
|
|
424
|
-
if (!index) return [];
|
|
385
|
+
export function checkGovernment(note) {
|
|
425
386
|
const fm = note.fm ?? {};
|
|
426
|
-
if (
|
|
427
|
-
const
|
|
428
|
-
if (!
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
431
|
-
|
|
387
|
+
if (fm.type !== "place") return [];
|
|
388
|
+
const data = fm.data;
|
|
389
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
390
|
+
const population = data.population;
|
|
391
|
+
if (typeof population !== "number" && typeof population !== "string") return [];
|
|
392
|
+
const count = Number(population);
|
|
393
|
+
if (!Number.isFinite(count) || count <= 0 || Object.hasOwn(data, "government")) return [];
|
|
432
394
|
return [
|
|
433
395
|
{
|
|
434
396
|
file: note.file,
|
|
435
|
-
...
|
|
397
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "population"], { key: true }),
|
|
436
398
|
severity: "warning",
|
|
437
399
|
message:
|
|
438
|
-
`
|
|
439
|
-
|
|
440
|
-
`order or polity that does`,
|
|
400
|
+
`missing government: place "${fm.shortcode ?? ""}" has positive population ` +
|
|
401
|
+
"but no `data.government`; name an affiliation or write null for complete anarchy",
|
|
441
402
|
},
|
|
442
403
|
];
|
|
443
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The historical API name for the government advisory. Tenure is no longer checked.
|
|
408
|
+
* @deprecated Use checkGovernment.
|
|
409
|
+
* @param {object} note - The place note.
|
|
410
|
+
* @param {object} [_opts] - Historical options, no longer needed.
|
|
411
|
+
* @returns {object[]} Government advisory findings.
|
|
412
|
+
*/
|
|
413
|
+
export function checkHeld(note, _opts = {}) {
|
|
414
|
+
return checkGovernment(note);
|
|
415
|
+
}
|
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, who holds it, and what an affiliation holds — the page lists, and the
|
|
102
|
+
/** What lies within a place, who holds it, and what an affiliation holds — the 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
|
@@ -427,6 +427,7 @@ export function isUnsetSentinel(value) {
|
|
|
427
427
|
/**
|
|
428
428
|
* Whether a value is worth a row.
|
|
429
429
|
*
|
|
430
|
+
* A field declaring `nullText` handles explicit null before this general test.
|
|
430
431
|
* Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
|
|
431
432
|
* writes "nobody filled this in" — a note that declares every key of its type
|
|
432
433
|
* and leaves most of them empty is the ordinary shape, not the exception — and
|
|
@@ -943,6 +944,15 @@ function noteBox(
|
|
|
943
944
|
if (structured.length) shown.add(field.name);
|
|
944
945
|
continue;
|
|
945
946
|
}
|
|
947
|
+
if (raw === null && field.nullText) {
|
|
948
|
+
rows.push({
|
|
949
|
+
label: overlay.label ?? humanizeFieldName(field.name),
|
|
950
|
+
kind: "text",
|
|
951
|
+
value: field.nullText,
|
|
952
|
+
});
|
|
953
|
+
shown.add(field.name);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
946
956
|
if (!hasValue(raw)) continue;
|
|
947
957
|
|
|
948
958
|
if (overlay.group) {
|
|
@@ -435,6 +435,9 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
435
435
|
parents: record.data?.parents ?? undefined,
|
|
436
436
|
domains: record.data?.domains ?? undefined,
|
|
437
437
|
population: record.data?.population ?? undefined,
|
|
438
|
+
...(Object.hasOwn(record.data ?? {}, "government") ?
|
|
439
|
+
{ government: record.data.government }
|
|
440
|
+
: {}),
|
|
438
441
|
package: pkg,
|
|
439
442
|
});
|
|
440
443
|
const entry = index.get(key);
|
|
@@ -105,14 +105,14 @@ import { checkSocialTies } from "./social-ties.mjs";
|
|
|
105
105
|
import { checkStandings } from "./standings.mjs";
|
|
106
106
|
import { STANDING_BODY_TYPES } from "./standing-terms.mjs";
|
|
107
107
|
import { checkDatedOffices } from "./office-holders.mjs";
|
|
108
|
-
import { checkRankLadder } from "./rank-ladder.mjs";
|
|
108
|
+
import { checkAffiliationRankFloor, checkRankLadder } from "./rank-ladder.mjs";
|
|
109
109
|
import { checkCalendarChoice } from "./calendar-choice.mjs";
|
|
110
110
|
import { checkLoreEvents } from "./lore-events.mjs";
|
|
111
111
|
import { checkCultureChoice } from "./culture-choice.mjs";
|
|
112
112
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
113
113
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
114
114
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
115
|
-
import {
|
|
115
|
+
import { checkGovernment } from "./holdings.mjs";
|
|
116
116
|
import {
|
|
117
117
|
checkBeingMeasurement,
|
|
118
118
|
parseBeingHeight,
|
|
@@ -153,6 +153,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
153
153
|
* so each takes a row labelled and anchored by the post's own name.
|
|
154
154
|
* @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
|
|
155
155
|
* @property {readonly string[]} [accepts] - Allowed Address types, separate from `ref`.
|
|
156
|
+
* @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
|
|
156
157
|
* @property {string} [shape] - Human-readable shape, for a finding and for
|
|
157
158
|
* documentation.
|
|
158
159
|
* @property {string} [entryShape] - For a `scalar-or-map` field, what one
|
|
@@ -512,8 +513,8 @@ const CHARGES = Object.freeze([
|
|
|
512
513
|
/**
|
|
513
514
|
* Everything `place` asks of a whole note.
|
|
514
515
|
*
|
|
515
|
-
* Two questions about one subject, and they are independent: who
|
|
516
|
-
*
|
|
516
|
+
* Two questions about one subject, and they are independent: who governs this
|
|
517
|
+
* inhabited place, and — where the note is a body rather than somewhere within one — what
|
|
517
518
|
* the body states about itself. A type declares one whole-note check, so the
|
|
518
519
|
* two are composed here rather than either being folded into the other.
|
|
519
520
|
*
|
|
@@ -522,7 +523,7 @@ const CHARGES = Object.freeze([
|
|
|
522
523
|
* @returns {object[]} Findings from both.
|
|
523
524
|
*/
|
|
524
525
|
function checkPlace(note, opts) {
|
|
525
|
-
return [...
|
|
526
|
+
return [...checkGovernment(note), ...checkWorldFacts(note, opts)];
|
|
526
527
|
}
|
|
527
528
|
|
|
528
529
|
/** Validate a being's authored date using the shared note-date grammar. */
|
|
@@ -1014,6 +1015,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1014
1015
|
|
|
1015
1016
|
affiliation: Object.freeze({
|
|
1016
1017
|
stubbable: true,
|
|
1018
|
+
check: checkAffiliationRankFloor,
|
|
1017
1019
|
subTypes: Object.freeze([
|
|
1018
1020
|
"guild",
|
|
1019
1021
|
"order",
|
|
@@ -1562,6 +1564,17 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1562
1564
|
check: checkPopulation,
|
|
1563
1565
|
describe: "Approximate population, to two significant digits.",
|
|
1564
1566
|
},
|
|
1567
|
+
{
|
|
1568
|
+
name: "government",
|
|
1569
|
+
...LINK,
|
|
1570
|
+
shape: "an affiliation Address or null",
|
|
1571
|
+
ref: "affiliation",
|
|
1572
|
+
accepts: ["affiliation"],
|
|
1573
|
+
nullText: "Complete anarchy",
|
|
1574
|
+
describe:
|
|
1575
|
+
"The governing affiliation; explicit null means complete anarchy. " +
|
|
1576
|
+
"A positive population with no government key produces an advisory.",
|
|
1577
|
+
},
|
|
1565
1578
|
{
|
|
1566
1579
|
name: "market",
|
|
1567
1580
|
...NUM,
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -744,7 +744,7 @@ export async function buildPdf({
|
|
|
744
744
|
* @param {number} columns - The entry's page columns.
|
|
745
745
|
* @returns {string} Typst markup.
|
|
746
746
|
*/
|
|
747
|
-
const figureCounts = { code: 0, table: 0, figure: 0, map: 0, prose: 0 };
|
|
747
|
+
const figureCounts = { code: 0, table: 0, figure: 0, map: 0, poem: 0, prose: 0 };
|
|
748
748
|
// The role lookup a `:::figure` fence'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
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -88,6 +88,7 @@ export const BOOK_IMAGE_WIDTHS = Object.freeze({
|
|
|
88
88
|
import { slugify } from "./content-slug.mjs";
|
|
89
89
|
import { scanFigures } from "./content-figures.mjs";
|
|
90
90
|
import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
|
|
91
|
+
import { inlineVerse, poetryStanzas } from "./content-poetry.mjs";
|
|
91
92
|
import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
|
|
92
93
|
import { HTML_TAG, htmlMessage } from "./content-html.mjs";
|
|
93
94
|
import { EXPRESSION } from "./markdown-expressions.mjs";
|
|
@@ -526,7 +527,16 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
526
527
|
}
|
|
527
528
|
}
|
|
528
529
|
if (!opts.insideAdmonition) {
|
|
529
|
-
const
|
|
530
|
+
const figuresHoldingPoetry = scanFigures(source).figures;
|
|
531
|
+
const blocks = scanBlocks(source).blocks.filter(
|
|
532
|
+
(block) =>
|
|
533
|
+
!figuresHoldingPoetry.some(
|
|
534
|
+
(figure) =>
|
|
535
|
+
block.name === "poetry" &&
|
|
536
|
+
block.start >= figure.bodyStart &&
|
|
537
|
+
block.end < figure.bodyEnd,
|
|
538
|
+
),
|
|
539
|
+
);
|
|
530
540
|
if (blocks.length) {
|
|
531
541
|
const lines = source.split("\n");
|
|
532
542
|
const output = [];
|
|
@@ -538,6 +548,24 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
538
548
|
insideAdmonition: true,
|
|
539
549
|
}),
|
|
540
550
|
);
|
|
551
|
+
if (block.name === "poetry") {
|
|
552
|
+
const content = poetryStanzas(block.body)
|
|
553
|
+
.map((stanza) =>
|
|
554
|
+
stanza
|
|
555
|
+
.map(({ text, level }) => {
|
|
556
|
+
const verse = markdownToTypst(inlineVerse(text), {
|
|
557
|
+
...sharedOptions,
|
|
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`);
|
|
566
|
+
cursor = block.end + 1;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
541
569
|
const { color, background, symbol } = BLOCK_PRINT[block.name];
|
|
542
570
|
// The title is inline markdown, rendered the way every other
|
|
543
571
|
// phrase in the book is. Set as it was written it would be read
|
|
@@ -601,7 +629,9 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
601
629
|
{ ...caption, imagesRemaining: { n: countImages(block) } }
|
|
602
630
|
: caption;
|
|
603
631
|
const segments = [
|
|
604
|
-
|
|
632
|
+
caption.kind === "poem" ?
|
|
633
|
+
markdownToTypst(block, { ...sharedOptions, captions: undefined })
|
|
634
|
+
: renderMarkdownSegment(block, md, { ...ctx, caption: captioned }, definitions),
|
|
605
635
|
];
|
|
606
636
|
if (caption.kind !== "table" && caption.kind !== "figure" && caption.kind !== "map") {
|
|
607
637
|
segments.push(
|
package/engine/rank-ladder.mjs
CHANGED
|
@@ -50,6 +50,38 @@ const RUNG_KEYS = Object.freeze(["level", "title", "description", "lore"]);
|
|
|
50
50
|
/** The keys a rung must carry a value for. */
|
|
51
51
|
const REQUIRED_TEXT = Object.freeze(["title", "description"]);
|
|
52
52
|
|
|
53
|
+
/** Every affiliation has an ordinary standing that a being can name as rank 1. */
|
|
54
|
+
export function checkAffiliationRankFloor(note) {
|
|
55
|
+
if (note.fm?.type !== "affiliation") return [];
|
|
56
|
+
const governance = note.fm?.data?.governance;
|
|
57
|
+
const ranks = governance?.ranks;
|
|
58
|
+
// A non-list already has a shape finding. Do not repeat it here.
|
|
59
|
+
if (ranks !== undefined && ranks !== null && !Array.isArray(ranks)) return [];
|
|
60
|
+
if (
|
|
61
|
+
Array.isArray(ranks) &&
|
|
62
|
+
ranks.some((rung) => integer(rung?.level) && Number(rung.level) === 1)
|
|
63
|
+
)
|
|
64
|
+
return [];
|
|
65
|
+
|
|
66
|
+
const path = Array.isArray(ranks) ? ["data", "governance", "ranks"] : ["data", "governance"];
|
|
67
|
+
const atRankOrGovernance = positionOfFrontmatterPath(note.raw ?? "", path, { key: true });
|
|
68
|
+
const position =
|
|
69
|
+
atRankOrGovernance.line !== undefined ?
|
|
70
|
+
atRankOrGovernance
|
|
71
|
+
: positionOfFrontmatterPath(note.raw ?? "", ["data"], { key: true });
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
file: note.file,
|
|
75
|
+
...position,
|
|
76
|
+
severity: "error",
|
|
77
|
+
message:
|
|
78
|
+
!Array.isArray(ranks) || ranks.length === 0 ?
|
|
79
|
+
"affiliation needs data.governance.ranks with a level 1 standing"
|
|
80
|
+
: "affiliation data.governance.ranks needs a level 1 standing",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
53
85
|
/**
|
|
54
86
|
* Whether a value is a YAML mapping rather than a list or a scalar.
|
|
55
87
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# used as given and never looked for here.
|
|
20
20
|
#
|
|
21
21
|
# Keeping it in step is not left to hope: `npm run lint:banners`
|
|
22
|
-
# (`
|
|
22
|
+
# (`ci/check-banners.mjs`, part of `npm run lint`) fetches every name below
|
|
23
23
|
# from a real asset host and fails on any non-200. Add a name here only after
|
|
24
24
|
# the file is published, or the check will say so.
|
|
25
25
|
#
|
|
@@ -1373,6 +1373,35 @@ a.tag:hover {
|
|
|
1373
1373
|
background: var(--bg-card);
|
|
1374
1374
|
}
|
|
1375
1375
|
|
|
1376
|
+
/* Verse lines are hard breaks in the markup; paragraphs mark stanzas. */
|
|
1377
|
+
.single-content .poetry {
|
|
1378
|
+
margin: 1.5rem 0;
|
|
1379
|
+
line-height: 1.7;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.single-content .poetry p {
|
|
1383
|
+
margin: 0;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
.single-content .poetry p + p {
|
|
1387
|
+
margin-top: 1em;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
.single-content .poetry .poetry-line {
|
|
1391
|
+
display: inline-block;
|
|
1392
|
+
margin-inline-start: var(--poetry-indent, 0em);
|
|
1393
|
+
white-space: pre-wrap;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
.single-content .poetry .poetry-line.i1 { --poetry-indent: 1.25em; }
|
|
1397
|
+
.single-content .poetry .poetry-line.i2 { --poetry-indent: 2.5em; }
|
|
1398
|
+
.single-content .poetry .poetry-line.i3 { --poetry-indent: 3.75em; }
|
|
1399
|
+
.single-content .poetry .poetry-line.i4 { --poetry-indent: 5em; }
|
|
1400
|
+
.single-content .poetry .poetry-line.i5 { --poetry-indent: 6.25em; }
|
|
1401
|
+
.single-content .poetry .poetry-line.i6 { --poetry-indent: 7.5em; }
|
|
1402
|
+
.single-content .poetry .poetry-line.i7 { --poetry-indent: 8.75em; }
|
|
1403
|
+
.single-content .poetry .poetry-line.i8 { --poetry-indent: 10em; }
|
|
1404
|
+
|
|
1376
1405
|
/* Ornamental horizontal rule: thin line flanking a centered gold
|
|
1377
1406
|
diamond. Uses gradients on ::before/::after to keep the markup
|
|
1378
1407
|
clean (authors just write `---` in markdown). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.26.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -187,10 +187,12 @@
|
|
|
187
187
|
"build:asset-index": "node ci/emit-asset-index.mjs",
|
|
188
188
|
"format": "prettier --write .",
|
|
189
189
|
"format:check": "prettier --check .",
|
|
190
|
-
"lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:content-format",
|
|
190
|
+
"lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:image-roles lint:banners lint:content-format",
|
|
191
191
|
"lint:markdown": "node bin/package-build.mjs markdown",
|
|
192
192
|
"lint:yaml": "node bin/package-build.mjs yaml",
|
|
193
193
|
"lint:labels": "node bin/package-build.mjs labels check",
|
|
194
|
+
"lint:image-roles": "vitest run tests/theme-image-roles.test.ts",
|
|
195
|
+
"lint:banners": "node ci/check-banners.mjs",
|
|
194
196
|
"lint:markdown:fix": "node bin/package-build.mjs markdown --fix",
|
|
195
197
|
"lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields && node ci/content-format-reference.mjs --check",
|
|
196
198
|
"lint:content-format:schema": "node bin/package-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json --schema hm3=tests/fixtures/content-format/schema-hm3.json",
|
|
@@ -29,7 +29,7 @@ export function checkCalendarDateFormats(note: any): {
|
|
|
29
29
|
/**
|
|
30
30
|
* Check a `place` note's world facts.
|
|
31
31
|
*
|
|
32
|
-
* Declared on `place` beside its
|
|
32
|
+
* Declared on `place` beside its government advisory, so the lint runs both with the
|
|
33
33
|
* same index. Four rules, and each is a fact about the setting rather than
|
|
34
34
|
* about the file:
|
|
35
35
|
*
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The authored verse lines, excluding stanza separators. */
|
|
2
|
+
export function verseLines(body: any): string[];
|
|
3
|
+
/**
|
|
4
|
+
* The least-indented verse is level zero. Every two additional leading spaces
|
|
5
|
+
* add one level; a leftover space is discarded and the level stops at eight.
|
|
6
|
+
* Blank lines divide stanzas rather than contributing to the baseline.
|
|
7
|
+
*/
|
|
8
|
+
export function poetryStanzas(body: any): {
|
|
9
|
+
text: string;
|
|
10
|
+
level: number;
|
|
11
|
+
}[][];
|
|
12
|
+
/** A verse is inline Markdown even when it starts with block syntax. */
|
|
13
|
+
export function inlineVerse(text: any): string;
|
|
14
|
+
/**
|
|
15
|
+
* Keep stanzas as Markdown paragraphs and make every verse line a hard break.
|
|
16
|
+
* The surrounding renderer still sees the entire note, so links and footnote
|
|
17
|
+
* definitions remain in scope. The span carries the indent for each line.
|
|
18
|
+
*/
|
|
19
|
+
export function poetryMarkdown(body: any): string;
|
|
@@ -53,22 +53,25 @@ export function foreignHoldingsNodes(foreignIndex: Map<string, object> | undefin
|
|
|
53
53
|
*/
|
|
54
54
|
export function holdingsPages(nodes: Iterable<HoldingsNode | null>): Map<string, Holdings>;
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Advise when a place with an authored positive population omits government.
|
|
57
|
+
* Missing, null or zero population establishes no inhabited population.
|
|
58
|
+
* Property presence distinguishes omitted government from explicit null,
|
|
59
|
+
* which means complete anarchy. Non-null values are validated by the declared
|
|
60
|
+
* affiliation reference field. Neither geographic containment nor tenure
|
|
61
|
+
* supplies this fact, and the rule is independent of place subtype.
|
|
59
62
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*
|
|
66
|
-
* @
|
|
67
|
-
* @
|
|
63
|
+
* @param {object} note - The note with parsed frontmatter and raw source.
|
|
64
|
+
* @returns {object[]} Located warning findings.
|
|
65
|
+
*/
|
|
66
|
+
export function checkGovernment(note: object): object[];
|
|
67
|
+
/**
|
|
68
|
+
* The historical API name for the government advisory. Tenure is no longer checked.
|
|
69
|
+
* @deprecated Use checkGovernment.
|
|
70
|
+
* @param {object} note - The place note.
|
|
71
|
+
* @param {object} [_opts] - Historical options, no longer needed.
|
|
72
|
+
* @returns {object[]} Government advisory findings.
|
|
68
73
|
*/
|
|
69
|
-
export function checkHeld(note: object,
|
|
70
|
-
index?: object | undefined;
|
|
71
|
-
}): object[];
|
|
74
|
+
export function checkHeld(note: object, _opts?: object): object[];
|
|
72
75
|
/**
|
|
73
76
|
* The keys this module writes, and which a note cannot author: `contains`
|
|
74
77
|
* and `held_by` on a place, `holdings` on an affiliation.
|
|
@@ -77,9 +80,9 @@ export function checkHeld(note: object, { index }?: {
|
|
|
77
80
|
*/
|
|
78
81
|
export const HOLDINGS_KEYS: readonly string[];
|
|
79
82
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
+
* Legacy subtypes of the retired tenure advisory. Government advisories
|
|
84
|
+
* apply to every place subtype when its authored population is positive.
|
|
85
|
+
* @deprecated This list no longer gates any advisory.
|
|
83
86
|
*
|
|
84
87
|
* @type {readonly string[]}
|
|
85
88
|
*/
|
|
@@ -27,6 +27,7 @@ export function isUnsetSentinel(value: unknown): boolean;
|
|
|
27
27
|
/**
|
|
28
28
|
* Whether a value is worth a row.
|
|
29
29
|
*
|
|
30
|
+
* A field declaring `nullText` handles explicit null before this general test.
|
|
30
31
|
* Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
|
|
31
32
|
* writes "nobody filled this in" — a note that declares every key of its type
|
|
32
33
|
* and leaves most of them empty is the ordinary shape, not the exception — and
|
|
@@ -380,6 +380,10 @@ export type DataFieldSpec = {
|
|
|
380
380
|
* - Allowed Address types, separate from `ref`.
|
|
381
381
|
*/
|
|
382
382
|
accepts?: readonly string[] | undefined;
|
|
383
|
+
/**
|
|
384
|
+
* - Infobox text for an explicitly authored null with a declared meaning.
|
|
385
|
+
*/
|
|
386
|
+
nullText?: string | undefined;
|
|
383
387
|
/**
|
|
384
388
|
* - Human-readable shape, for a finding and for
|
|
385
389
|
* documentation.
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** Every affiliation has an ordinary standing that a being can name as rank 1. */
|
|
2
|
+
export function checkAffiliationRankFloor(note: any): {
|
|
3
|
+
severity: string;
|
|
4
|
+
message: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
column?: number;
|
|
7
|
+
file: any;
|
|
8
|
+
}[];
|
|
1
9
|
/**
|
|
2
10
|
* Check one affiliation's `data.governance.ranks` against the rung contract.
|
|
3
11
|
*
|