@heroiclands/package-build 22.15.0 → 22.16.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 +103 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +303 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +294 -234
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +33 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +631 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4420 -0
- package/docs/reference/note-types.md +531 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-human.mjs +71 -0
- package/engine/calendar-notes.mjs +41 -3
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +745 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/itinerary-scenes.mjs +4 -2
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/map-raster.mjs +4 -2
- package/engine/markdown-expressions.mjs +117 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +283 -61
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +205 -93
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +2 -0
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +4 -2
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
|
@@ -97,10 +97,18 @@ import {
|
|
|
97
97
|
// question, asked the way `checkPlace` and `checkCalendarNote` are.
|
|
98
98
|
import { checkBeingAge } from "./being-age.mjs";
|
|
99
99
|
import { checkSocialTies } from "./social-ties.mjs";
|
|
100
|
+
import { checkDatedOffices } from "./office-holders.mjs";
|
|
101
|
+
import { checkCalendarChoice } from "./calendar-choice.mjs";
|
|
102
|
+
import { checkCultureChoice } from "./culture-choice.mjs";
|
|
100
103
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
101
104
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
102
105
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
103
106
|
import { checkHeld } from "./holdings.mjs";
|
|
107
|
+
import {
|
|
108
|
+
checkBeingMeasurement,
|
|
109
|
+
parseBeingHeight,
|
|
110
|
+
parseBeingWeight,
|
|
111
|
+
} from "./being-measurements.mjs";
|
|
104
112
|
import { checkCitedPopulations, checkPopulation } from "./populations.mjs";
|
|
105
113
|
// What trade a settlement supports — the scale and the check that holds a
|
|
106
114
|
// value to it, read rather than restated, so the reference below and the
|
|
@@ -435,6 +443,9 @@ function checkPlacePurpose(note) {
|
|
|
435
443
|
*/
|
|
436
444
|
export const DRAFT_TAG = "draft";
|
|
437
445
|
|
|
446
|
+
/** A note intended for the GM rather than the player-facing outputs. */
|
|
447
|
+
export const GM_TAG = "gm";
|
|
448
|
+
|
|
438
449
|
/**
|
|
439
450
|
* The tags that **classify** a note, grouped by what they classify.
|
|
440
451
|
*
|
|
@@ -571,6 +582,8 @@ export const DECLARED_TAGS = Object.freeze({
|
|
|
571
582
|
}),
|
|
572
583
|
/** A note's working state, which any note may carry. */
|
|
573
584
|
state: Object.freeze({ types: null, tags: Object.freeze([DRAFT_TAG]) }),
|
|
585
|
+
/** Who may read a completed note. */
|
|
586
|
+
audience: Object.freeze({ types: null, tags: Object.freeze([GM_TAG]) }),
|
|
574
587
|
});
|
|
575
588
|
|
|
576
589
|
/**
|
|
@@ -657,6 +670,11 @@ export function isDraftNote(fm) {
|
|
|
657
670
|
return hasTag(fm, DRAFT_TAG);
|
|
658
671
|
}
|
|
659
672
|
|
|
673
|
+
/** Whether a note is restricted to GM-facing output. */
|
|
674
|
+
export function isGmNote(fm) {
|
|
675
|
+
return hasTag(fm, GM_TAG);
|
|
676
|
+
}
|
|
677
|
+
|
|
660
678
|
/**
|
|
661
679
|
* Every note type this toolchain compiles, and the closed vocabulary it
|
|
662
680
|
* declares.
|
|
@@ -700,8 +718,15 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
700
718
|
ref: "lore",
|
|
701
719
|
accepts: ["lore"],
|
|
702
720
|
describe:
|
|
703
|
-
"Lore concerning this being
|
|
704
|
-
|
|
721
|
+
"Lore concerning this being, such as the standing it holds or the law it lives under.",
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
name: "culture",
|
|
725
|
+
...LINK,
|
|
726
|
+
ref: "lore",
|
|
727
|
+
accepts: ["lore"],
|
|
728
|
+
check: checkCultureChoice,
|
|
729
|
+
describe: "The being's primary culture, as a culture lore note.",
|
|
705
730
|
},
|
|
706
731
|
{
|
|
707
732
|
name: "homes",
|
|
@@ -743,6 +768,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
743
768
|
"When the being was born — a date, or `unknown` where the birth is " +
|
|
744
769
|
"unrecorded. Absent, the being was never born.",
|
|
745
770
|
},
|
|
771
|
+
{
|
|
772
|
+
name: "calendar",
|
|
773
|
+
...LINK,
|
|
774
|
+
ref: "lore",
|
|
775
|
+
accepts: ["lore"],
|
|
776
|
+
check: checkCalendarChoice,
|
|
777
|
+
describe: "Calendar used to display this being's dates.",
|
|
778
|
+
},
|
|
746
779
|
{
|
|
747
780
|
name: "died",
|
|
748
781
|
...TEXT,
|
|
@@ -770,8 +803,20 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
770
803
|
"Written by the compiler beside an `age` estimate — the `~` stripped, " +
|
|
771
804
|
"the magnitude alone. A note never authors this.",
|
|
772
805
|
},
|
|
773
|
-
{
|
|
774
|
-
|
|
806
|
+
{
|
|
807
|
+
name: "height",
|
|
808
|
+
shape: "metres or feet and inches",
|
|
809
|
+
kind: "string",
|
|
810
|
+
check: (note) => checkBeingMeasurement(note, "height", parseBeingHeight),
|
|
811
|
+
describe: "Height in metres or feet and inches.",
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: "weight",
|
|
815
|
+
shape: "kilograms or pounds",
|
|
816
|
+
kind: "string",
|
|
817
|
+
check: (note) => checkBeingMeasurement(note, "weight", parseBeingWeight),
|
|
818
|
+
describe: "Body weight in kilograms or pounds.",
|
|
819
|
+
},
|
|
775
820
|
{
|
|
776
821
|
name: "frame",
|
|
777
822
|
...TEXT,
|
|
@@ -849,7 +894,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
849
894
|
{
|
|
850
895
|
name: "governance.offices",
|
|
851
896
|
...ANY,
|
|
852
|
-
|
|
897
|
+
check: checkDatedOffices,
|
|
898
|
+
describe: "Named offices, each with a description and optional dated holders.",
|
|
853
899
|
},
|
|
854
900
|
{
|
|
855
901
|
name: "seat",
|
|
@@ -1241,20 +1287,21 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1241
1287
|
lore: Object.freeze({
|
|
1242
1288
|
stubbable: true,
|
|
1243
1289
|
subTypes: Object.freeze([
|
|
1244
|
-
"cosmology",
|
|
1245
|
-
"deity",
|
|
1246
|
-
"theology",
|
|
1247
|
-
"arcana",
|
|
1248
|
-
"spirit",
|
|
1249
|
-
"economy",
|
|
1250
|
-
"law",
|
|
1251
|
-
"calendar",
|
|
1252
|
-
"history",
|
|
1253
|
-
"material",
|
|
1254
|
-
"folk",
|
|
1255
|
-
"culture",
|
|
1256
|
-
"
|
|
1257
|
-
"
|
|
1290
|
+
"cosmology", // The structure and origin of reality.
|
|
1291
|
+
"deity", // An individual god and its attributed nature.
|
|
1292
|
+
"theology", // Beliefs about divinity, worship, and the afterlife.
|
|
1293
|
+
"arcana", // Beliefs and traditions concerning magic.
|
|
1294
|
+
"spirit", // Non-divine supernatural beings and their natures.
|
|
1295
|
+
"economy", // Trade, money, and the movement of wealth.
|
|
1296
|
+
"law", // Obligations, rights, courts, and tenure.
|
|
1297
|
+
"calendar", // The reckoning and marking of time.
|
|
1298
|
+
"history", // Events, eras, and chronicles of the past.
|
|
1299
|
+
"material", // Physical constituents and their qualities, including regional varieties.
|
|
1300
|
+
"folk", // A kindred or ancestry of related sapient beings.
|
|
1301
|
+
"culture", // A people sharing beliefs, mores, and values.
|
|
1302
|
+
"custom", // How a people practices a rite, observance, or usage.
|
|
1303
|
+
"bestiary", // A kind of creature that is not a people.
|
|
1304
|
+
"gathering", // A recurring public assembly, such as a fair or tournament.
|
|
1258
1305
|
]),
|
|
1259
1306
|
// A lore note is prose, and what it *is* about is its subType — with
|
|
1260
1307
|
// one exception. A calendar is a division of the year, and a division
|
|
@@ -1286,6 +1333,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1286
1333
|
]),
|
|
1287
1334
|
check: checkPlace,
|
|
1288
1335
|
data: Object.freeze([
|
|
1336
|
+
{
|
|
1337
|
+
name: "calendar",
|
|
1338
|
+
...LINK,
|
|
1339
|
+
ref: "lore",
|
|
1340
|
+
accepts: ["lore"],
|
|
1341
|
+
check: checkCalendarChoice,
|
|
1342
|
+
describe: "Calendar used to display this place's dates.",
|
|
1343
|
+
},
|
|
1289
1344
|
{
|
|
1290
1345
|
name: "demonym",
|
|
1291
1346
|
...TEXT,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
/** English cardinal words for finite whole numbers. */
|
|
4
|
+
const ONES = [
|
|
5
|
+
"zero",
|
|
6
|
+
"one",
|
|
7
|
+
"two",
|
|
8
|
+
"three",
|
|
9
|
+
"four",
|
|
10
|
+
"five",
|
|
11
|
+
"six",
|
|
12
|
+
"seven",
|
|
13
|
+
"eight",
|
|
14
|
+
"nine",
|
|
15
|
+
"ten",
|
|
16
|
+
"eleven",
|
|
17
|
+
"twelve",
|
|
18
|
+
"thirteen",
|
|
19
|
+
"fourteen",
|
|
20
|
+
"fifteen",
|
|
21
|
+
"sixteen",
|
|
22
|
+
"seventeen",
|
|
23
|
+
"eighteen",
|
|
24
|
+
"nineteen",
|
|
25
|
+
];
|
|
26
|
+
const TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
|
|
27
|
+
const SCALES = ["", "thousand", "million", "billion", "trillion", "quadrillion", "quintillion"];
|
|
28
|
+
|
|
29
|
+
function underThousand(value) {
|
|
30
|
+
const parts = [];
|
|
31
|
+
const hundreds = Math.floor(value / 100);
|
|
32
|
+
if (hundreds) parts.push(`${ONES[hundreds]} hundred`);
|
|
33
|
+
const rest = value % 100;
|
|
34
|
+
if (rest >= 20) {
|
|
35
|
+
const ten = TENS[Math.floor(rest / 10)];
|
|
36
|
+
parts.push(rest % 10 ? `${ten}-${ONES[rest % 10]}` : ten);
|
|
37
|
+
} else if (rest) parts.push(ONES[rest]);
|
|
38
|
+
return parts.join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Spell a SQL integer in running prose, using American cardinal style. */
|
|
42
|
+
export function numberWords(value) {
|
|
43
|
+
if (typeof value !== "bigint" && (typeof value !== "number" || !Number.isSafeInteger(value))) {
|
|
44
|
+
throw new TypeError("words needs a whole number; use digits for decimals");
|
|
45
|
+
}
|
|
46
|
+
let remaining = BigInt(value);
|
|
47
|
+
if (remaining === 0n) return "zero";
|
|
48
|
+
const negative = remaining < 0n;
|
|
49
|
+
if (negative) remaining = -remaining;
|
|
50
|
+
const parts = [];
|
|
51
|
+
let scale = 0;
|
|
52
|
+
while (remaining > 0n) {
|
|
53
|
+
if (scale >= SCALES.length) throw new RangeError("words cannot spell a number this large");
|
|
54
|
+
const group = Number(remaining % 1000n);
|
|
55
|
+
if (group)
|
|
56
|
+
parts.unshift(`${underThousand(group)}${SCALES[scale] ? ` ${SCALES[scale]}` : ""}`);
|
|
57
|
+
remaining /= 1000n;
|
|
58
|
+
scale++;
|
|
59
|
+
}
|
|
60
|
+
return `${negative ? "minus " : ""}${parts.join(" ")}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Render a SQL number with grouped thousands and its fractional part. */
|
|
64
|
+
export function numberDigits(value) {
|
|
65
|
+
if (typeof value === "bigint") return value.toLocaleString("en-US");
|
|
66
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
67
|
+
throw new TypeError("digits needs a finite number");
|
|
68
|
+
}
|
|
69
|
+
if (/[eE]/.test(String(value))) {
|
|
70
|
+
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 20 }).format(value);
|
|
71
|
+
}
|
|
72
|
+
const [whole, fraction] = String(value).split(".");
|
|
73
|
+
const grouped = BigInt(whole).toLocaleString("en-US");
|
|
74
|
+
return fraction === undefined ? grouped : `${grouped}.${fraction}`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { parseAddress, renderAddress } from "./address.mjs";
|
|
6
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
7
|
+
import { parseNoteDate } from "./note-dates.mjs";
|
|
8
|
+
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
9
|
+
|
|
10
|
+
const OFFICE_KEYS = new Set(["description", "holders"]);
|
|
11
|
+
const HOLDER_KEYS = new Set(["being", "start", "end", "contested"]);
|
|
12
|
+
const mapping = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate one affiliation's office roster against dated beings in the corpus.
|
|
16
|
+
* @param {object} note
|
|
17
|
+
* @param {{index?: object}} [options]
|
|
18
|
+
*/
|
|
19
|
+
export function checkDatedOffices(note, { index } = {}) {
|
|
20
|
+
const offices = note.fm?.data?.governance?.offices;
|
|
21
|
+
if (offices === undefined || offices === null) return [];
|
|
22
|
+
const findings = [];
|
|
23
|
+
const at = (path, message) => ({
|
|
24
|
+
file: note.file,
|
|
25
|
+
...positionOfFrontmatterPath(note.raw ?? "", path),
|
|
26
|
+
severity: "error",
|
|
27
|
+
message,
|
|
28
|
+
});
|
|
29
|
+
if (!mapping(offices))
|
|
30
|
+
return [
|
|
31
|
+
at(
|
|
32
|
+
["data", "governance", "offices"],
|
|
33
|
+
"data.governance.offices must be a map of named offices",
|
|
34
|
+
),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const dates = reckoningContext(index);
|
|
38
|
+
for (const [office, value] of Object.entries(offices)) {
|
|
39
|
+
const base = ["data", "governance", "offices", office];
|
|
40
|
+
if (typeof value === "string") continue;
|
|
41
|
+
if (!mapping(value)) {
|
|
42
|
+
findings.push(
|
|
43
|
+
at(base, `office ${office} must be a description or a {description, holders} map`),
|
|
44
|
+
);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(value))
|
|
48
|
+
if (!OFFICE_KEYS.has(key))
|
|
49
|
+
findings.push(
|
|
50
|
+
at(
|
|
51
|
+
[...base, key],
|
|
52
|
+
`office ${office} has unknown key ${key}; use description or holders`,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
if (typeof value.description !== "string")
|
|
56
|
+
findings.push(
|
|
57
|
+
at([...base, "description"], `office ${office} needs a string description`),
|
|
58
|
+
);
|
|
59
|
+
if (!Array.isArray(value.holders)) {
|
|
60
|
+
findings.push(at([...base, "holders"], `office ${office} needs a holders list`));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const terms = [];
|
|
64
|
+
value.holders.forEach((row, position) => {
|
|
65
|
+
const rowPath = [...base, "holders", position];
|
|
66
|
+
if (!mapping(row)) {
|
|
67
|
+
findings.push(at(rowPath, `office ${office} holder must be a map`));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const key of Object.keys(row))
|
|
71
|
+
if (!HOLDER_KEYS.has(key))
|
|
72
|
+
findings.push(
|
|
73
|
+
at([...rowPath, key], `office ${office} holder has unknown key ${key}`),
|
|
74
|
+
);
|
|
75
|
+
if (row.contested !== undefined && typeof row.contested !== "boolean")
|
|
76
|
+
findings.push(
|
|
77
|
+
at(
|
|
78
|
+
[...rowPath, "contested"],
|
|
79
|
+
`office ${office} contested must be true or false`,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (typeof row.being !== "string" || !row.being.trim()) {
|
|
84
|
+
findings.push(
|
|
85
|
+
at([...rowPath, "being"], `office ${office} holder needs a being Address`),
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
const tuple = parseAddress(row.being, {
|
|
89
|
+
package: index?.contentPackage,
|
|
90
|
+
system: "note",
|
|
91
|
+
type: "being",
|
|
92
|
+
types: index?.types,
|
|
93
|
+
packages: index?.packages,
|
|
94
|
+
});
|
|
95
|
+
if (tuple.reason || tuple.type !== "being") {
|
|
96
|
+
findings.push(
|
|
97
|
+
at(
|
|
98
|
+
[...rowPath, "being"],
|
|
99
|
+
`office ${office} holder ${row.being} must name a being Address`,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
} else if (index?.addressHit) {
|
|
103
|
+
const target = index.addressHit(renderAddress(tuple));
|
|
104
|
+
if (!target)
|
|
105
|
+
findings.push(
|
|
106
|
+
at(
|
|
107
|
+
[...rowPath, "being"],
|
|
108
|
+
`office ${office} holder ${row.being} does not resolve`,
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
else if ((target.fm ?? target).data?.died != null && row.end == null)
|
|
112
|
+
findings.push(
|
|
113
|
+
at(
|
|
114
|
+
[...rowPath, "being"],
|
|
115
|
+
`office ${office} claims a current holder ${row.being} whose died value is present`,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const bounds = {};
|
|
122
|
+
let invalidDate = false;
|
|
123
|
+
for (const key of ["start", "end"]) {
|
|
124
|
+
if (row[key] === undefined || row[key] === null) continue;
|
|
125
|
+
const parsed = parseNoteDate(row[key], {
|
|
126
|
+
...dates,
|
|
127
|
+
allowUnknown: false,
|
|
128
|
+
field: `data.governance.offices.${office}.holders.${key}`,
|
|
129
|
+
file: note.file,
|
|
130
|
+
raw: note.raw,
|
|
131
|
+
keyPath: [...rowPath, key],
|
|
132
|
+
});
|
|
133
|
+
findings.push(...parsed.findings);
|
|
134
|
+
if (
|
|
135
|
+
parsed.date &&
|
|
136
|
+
parsed.date.month !== null &&
|
|
137
|
+
parsed.date.canonicalDay === undefined
|
|
138
|
+
) {
|
|
139
|
+
findings.push(
|
|
140
|
+
at(
|
|
141
|
+
[...rowPath, key],
|
|
142
|
+
`office ${office} holder ${key} needs a marker or canonical day to order it`,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
invalidDate = true;
|
|
146
|
+
} else if (parsed.date && Number.isFinite(parsed.date.sort))
|
|
147
|
+
bounds[key] =
|
|
148
|
+
parsed.date.sort +
|
|
149
|
+
(key === "end" ?
|
|
150
|
+
((parsed.date.spanDays ?? 1) - 1) / (dates.daysPerYear ?? 365)
|
|
151
|
+
: 0);
|
|
152
|
+
else {
|
|
153
|
+
if (parsed.date)
|
|
154
|
+
findings.push(
|
|
155
|
+
at(
|
|
156
|
+
[...rowPath, key],
|
|
157
|
+
`office ${office} holder ${key} needs a resolved date`,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
invalidDate = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (bounds.start !== undefined && bounds.end !== undefined && bounds.end < bounds.start)
|
|
164
|
+
findings.push(
|
|
165
|
+
at([...rowPath, "end"], `office ${office} holder ends before its start`),
|
|
166
|
+
);
|
|
167
|
+
if (!invalidDate)
|
|
168
|
+
terms.push({
|
|
169
|
+
start: bounds.start ?? -Infinity,
|
|
170
|
+
end: bounds.end ?? Infinity,
|
|
171
|
+
contested: row.contested === true,
|
|
172
|
+
rowPath,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
for (let i = 0; i < terms.length; i++)
|
|
176
|
+
for (let j = i + 1; j < terms.length; j++)
|
|
177
|
+
if (
|
|
178
|
+
!terms[i].contested &&
|
|
179
|
+
!terms[j].contested &&
|
|
180
|
+
terms[i].start <= terms[j].end &&
|
|
181
|
+
terms[j].start <= terms[i].end
|
|
182
|
+
)
|
|
183
|
+
findings.push(
|
|
184
|
+
at(terms[j].rowPath, `office ${office} has overlapping holder terms`),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return findings;
|
|
188
|
+
}
|
package/engine/pack-config.mjs
CHANGED
|
@@ -123,7 +123,7 @@ export const CONFIG_BASENAME = "package-build.config";
|
|
|
123
123
|
* the order the names are *reported* in, which is the order a reader should
|
|
124
124
|
* reach for them: YAML first, `.mjs` last.
|
|
125
125
|
*
|
|
126
|
-
* The `
|
|
126
|
+
* The `package-build.config.*` stem this package read before the two toolchains
|
|
127
127
|
* merged is **not** resolved. 3.0.0 renames the file rather than accepting both:
|
|
128
128
|
* a deprecation window here would mean a repository could sit indefinitely on a
|
|
129
129
|
* name for a package that no longer exists, and the upgrade already requires
|
|
@@ -570,22 +570,9 @@ export function configFromData(data, configPath) {
|
|
|
570
570
|
);
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
|
-
const {
|
|
573
|
+
const { pkg } = readPackageJson(rootDir);
|
|
574
574
|
if (pkg.homepage !== undefined) input.homepage = pkg.homepage;
|
|
575
575
|
if (pkg.author !== undefined) input.author = pkg.author;
|
|
576
|
-
// `description` is npm metadata nothing displays for a private package —
|
|
577
|
-
// read by neither the manifest nor the site, so a declared one is a
|
|
578
|
-
// warning rather than a silent no-op that looks like it did something.
|
|
579
|
-
if (pkg.description !== undefined) {
|
|
580
|
-
emitDiagnostic({
|
|
581
|
-
file: manifestPath,
|
|
582
|
-
severity: "warning",
|
|
583
|
-
message:
|
|
584
|
-
"`description` is read by nothing; the Foundry pitch is " +
|
|
585
|
-
"`packageBuild.manifest.descriptionHtml` and the site's is " +
|
|
586
|
-
"`site.description`",
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
576
|
|
|
590
577
|
if (input.itemBuilders !== undefined) {
|
|
591
578
|
const declared = input.itemBuilders;
|
package/engine/pack-router.mjs
CHANGED
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
* another document type, or a companion pack, which no note may address —
|
|
45
45
|
* fails the build, naming the note and what it asked for. A silent fall-back
|
|
46
46
|
* to the default would publish an address into the wrong compendium.
|
|
47
|
-
* - A note's **derived** documents
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* - A note's **derived** documents use the default of their type. GM-only
|
|
48
|
+
* prose uses a private pack of that type. An item's `pack:` names where the
|
|
49
|
+
* item goes, not its prose JournalEntry.
|
|
50
50
|
* - A note feeding **more than one system** declares `pack:` inside the block
|
|
51
51
|
* that differs. `pack` needed no new mechanism for that: it is an
|
|
52
52
|
* ordinary shared property, so `<system>.pack` overrides the top-level one
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
import { NO_PACK, packForType } from "./ids.mjs";
|
|
84
84
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
85
85
|
import { blockDataProperty, systemBlock } from "./system-block.mjs";
|
|
86
|
+
import { isGmNote } from "./note-vocabulary.mjs";
|
|
86
87
|
|
|
87
88
|
/**
|
|
88
89
|
* A note that cannot be routed to a pack. Thrown rather than returned so no
|
|
@@ -236,7 +237,7 @@ export function createPackRouter(packs) {
|
|
|
236
237
|
* asking, only a pack of that system answers, or one that declares no
|
|
237
238
|
* system at all and so belongs to all of them. The type-wide default is
|
|
238
239
|
* otherwise free to be a neighbour's — `actors-sohl` is what
|
|
239
|
-
*
|
|
240
|
+
* the package's two-system layout marks `default: true` — and returning it
|
|
240
241
|
* to the HM3 pass would either route an HM3 document into the SoHL pack or,
|
|
241
242
|
* as it actually did, have the HM3 pass see a pack name that is not its own
|
|
242
243
|
* and skip every note in the tree. The second is worse: it is silent, and
|
|
@@ -364,7 +365,20 @@ export function createPackRouter(packs) {
|
|
|
364
365
|
return declared;
|
|
365
366
|
}
|
|
366
367
|
|
|
367
|
-
|
|
368
|
+
let fallback = defaultFor(docType, system);
|
|
369
|
+
if (isGmNote(fm) && docType !== ownDocType) {
|
|
370
|
+
const privatePacks = (byType.get(docType) ?? []).filter(
|
|
371
|
+
(name) => byName.get(name)?.private === true && !byName.get(name)?.system,
|
|
372
|
+
);
|
|
373
|
+
if (!fallback || !byName.get(fallback)?.private) {
|
|
374
|
+
fallback = privatePacks.length === 1 ? privatePacks[0] : undefined;
|
|
375
|
+
}
|
|
376
|
+
if (!fallback) {
|
|
377
|
+
throw new PackRoutingError(
|
|
378
|
+
`${noteLabel(fm)} needs one private ${docType} pack for its GM-only prose.`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
368
382
|
if (!fallback) {
|
|
369
383
|
const candidates =
|
|
370
384
|
system === undefined ?
|
|
@@ -466,6 +480,11 @@ export function createPackRouter(packs) {
|
|
|
466
480
|
return byName.get(name)?.system || undefined;
|
|
467
481
|
},
|
|
468
482
|
|
|
483
|
+
/** Whether Foundry restricts this pack to GMs. */
|
|
484
|
+
privateOf(name) {
|
|
485
|
+
return byName.get(name)?.private === true;
|
|
486
|
+
},
|
|
487
|
+
|
|
469
488
|
/**
|
|
470
489
|
* The pack of a type that receives notes declaring none.
|
|
471
490
|
*
|
package/engine/packages.mjs
CHANGED
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
* with, which is the whole point: the alternative is every package depending on
|
|
26
26
|
* one of the others just to borrow a banner.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* resolution is special-cased
|
|
28
|
+
* Only this toolchain's own documentation package may share the name. Other
|
|
29
|
+
* repositories cannot claim it, and asset resolution is special-cased because
|
|
30
|
+
* no installed Foundry directory sits behind it.
|
|
30
31
|
*
|
|
31
32
|
* This module is a **leaf with no local imports**, so the configuration
|
|
32
33
|
* validator can name it without closing a cycle around `content-config.mjs`.
|
package/engine/pathnames.mjs
CHANGED
|
@@ -36,22 +36,21 @@
|
|
|
36
36
|
* being built, and the whole of it is the suffix. That is the ordinary case and
|
|
37
37
|
* the one nearly every note writes.
|
|
38
38
|
*
|
|
39
|
-
* The four forms, for a `
|
|
40
|
-
* ships as the Foundry module `
|
|
39
|
+
* The four forms, for a `harnensemble` note writing `images/map.webp`
|
|
40
|
+
* (`harnensemble` ships as the Foundry module `harn-ensemble`):
|
|
41
41
|
*
|
|
42
|
-
* | Form | Address
|
|
43
|
-
* | --------- |
|
|
44
|
-
* | `foundry` | `modules/
|
|
45
|
-
* | `local` | `assets/images/map.webp`
|
|
46
|
-
* | `web` | `https://cdn.heroiclands.org/
|
|
47
|
-
* | `pdf` | `assets/images/map.webp`
|
|
42
|
+
* | Form | Address |
|
|
43
|
+
* | --------- | ------------------------------------------------------------- |
|
|
44
|
+
* | `foundry` | `modules/harn-ensemble/assets/images/map.webp` |
|
|
45
|
+
* | `local` | `assets/images/map.webp` |
|
|
46
|
+
* | `web` | `https://cdn.heroiclands.org/harnensemble/images/map.webp` |
|
|
47
|
+
* | `pdf` | `assets/images/map.webp` |
|
|
48
48
|
*
|
|
49
49
|
* **`<package>` and `<foundry-id>` are two different names.** The package is
|
|
50
|
-
* `
|
|
51
|
-
* and what a note writes. The Foundry id is `
|
|
52
|
-
* installs the module as
|
|
53
|
-
*
|
|
54
|
-
* than treated as one value.
|
|
50
|
+
* `harnensemble` — what the content is called, what the website serves it under,
|
|
51
|
+
* and what a note writes. The Foundry id is `harn-ensemble` — what Foundry
|
|
52
|
+
* installs the module as. The values coincide for `thalorna`, `sohl`, and
|
|
53
|
+
* `hm3`, but pathname resolution uses each in its own surface.
|
|
55
54
|
*
|
|
56
55
|
* `local` and `pdf` read the same and mean different places: `local` is the file
|
|
57
56
|
* in the owning repository's working tree, `pdf` is where the book stages a copy
|
|
@@ -338,16 +337,14 @@ function declaredSystemIds(config) {
|
|
|
338
337
|
export function packageAddresses(config) {
|
|
339
338
|
/** @type {Map<string, {root: string|null, id: string|null, own: boolean}>} */
|
|
340
339
|
const out = new Map();
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
});
|
|
350
|
-
}
|
|
340
|
+
out.set(config.contentPackage, {
|
|
341
|
+
// `assetRoot` is `<root>/<id>/assets`, and `null` for a
|
|
342
|
+
// `documentation` package — which is the same "Foundry serves no
|
|
343
|
+
// files for this" the `root` below says.
|
|
344
|
+
root: config.assetRoot ? config.packageKind : null,
|
|
345
|
+
id: config.foundryPackage ?? null,
|
|
346
|
+
own: true,
|
|
347
|
+
});
|
|
351
348
|
// The package being built wins every collision: it is the one whose files
|
|
352
349
|
// this repository actually holds, and another declaration of the same name
|
|
353
350
|
// describes that very package from outside.
|
|
@@ -420,7 +417,7 @@ export function resolvePathname(raw, config) {
|
|
|
420
417
|
// reading it as this package's own would put one package's name inside
|
|
421
418
|
// another's tree and report nothing.
|
|
422
419
|
const named = segments.length > 2 && segments[1] === ASSETS_SEGMENT ? segments[0] : "";
|
|
423
|
-
const owner = named ||
|
|
420
|
+
const owner = named || config.contentPackage;
|
|
424
421
|
const suffix = named ? segments.slice(2).join("/") : authored;
|
|
425
422
|
const entry = owner ? packages.get(owner) : undefined;
|
|
426
423
|
const host = String(config.site?.assets ?? "").replace(/\/+$/, "");
|