@heroiclands/package-build 22.4.6 → 22.4.7
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 +10 -0
- package/CONTENT.md +109 -0
- package/bin/content-build.mjs +10 -2
- package/content-config.mjs +8 -3
- package/docs/api.md +50 -14
- package/docs/configuration.md +20 -1
- package/docs/content-format.md +71 -0
- package/engine/frontmatter-lint.mjs +2 -0
- package/engine/holdings.mjs +391 -0
- package/engine/index.mjs +9 -0
- package/engine/map-graphviz.mjs +12 -3
- package/engine/map-places.mjs +33 -3
- package/engine/market-class.mjs +178 -0
- package/engine/metadata-index.mjs +7 -4
- package/engine/note-vocabulary.mjs +19 -0
- package/engine/site-build.mjs +88 -10
- package/engine/site-maps.mjs +165 -0
- package/package.json +1 -1
- package/types/engine/holdings.d.mts +146 -0
- package/types/engine/index.d.mts +3 -0
- package/types/engine/map-graphviz.d.mts +8 -1
- package/types/engine/map-places.d.mts +26 -0
- package/types/engine/market-class.d.mts +62 -0
- package/types/engine/note-vocabulary.d.mts +10 -0
- package/types/engine/site-build.d.mts +37 -6
- package/types/engine/site-maps.d.mts +81 -0
|
@@ -85,6 +85,11 @@ import { currentType } from "./ids.mjs";
|
|
|
85
85
|
// What a place is next to and reachable from — the two relation lists and
|
|
86
86
|
// the checks that hold them to their closed sets and to each other.
|
|
87
87
|
import { checkBorders, checkRoutes } from "./place-relations.mjs";
|
|
88
|
+
import { checkHeld } from "./holdings.mjs";
|
|
89
|
+
// What trade a settlement supports — the scale and the check that holds a
|
|
90
|
+
// value to it, read rather than restated, so the reference below and the
|
|
91
|
+
// finding an author meets state one list.
|
|
92
|
+
import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
|
|
88
93
|
|
|
89
94
|
/**
|
|
90
95
|
* One `data:` key a note type may carry.
|
|
@@ -141,6 +146,11 @@ import { checkBorders, checkRoutes } from "./place-relations.mjs";
|
|
|
141
146
|
* @property {readonly DataFieldSpec[]} data - The `data:` keys, closed.
|
|
142
147
|
* @property {readonly string[]|null} [subTypes] - The top-level `subType`
|
|
143
148
|
* values, as above.
|
|
149
|
+
* @property {(note: object, opts: {index?: object}) => object[]} [check] - A
|
|
150
|
+
* check of the whole note, run by the frontmatter lint beside the field
|
|
151
|
+
* checks with the same index. For a rule that is about the note rather than
|
|
152
|
+
* one of its fields — a place's tenure is a fact about every affiliation's
|
|
153
|
+
* `domains`, and is located at the note's `type:` line.
|
|
144
154
|
*/
|
|
145
155
|
|
|
146
156
|
/* --------------------------------------------------------------------- */
|
|
@@ -984,6 +994,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
984
994
|
|
|
985
995
|
place: Object.freeze({
|
|
986
996
|
subTypes: Object.freeze(["world", "region", "settlement", "site", "structure", "feature"]),
|
|
997
|
+
check: checkHeld,
|
|
987
998
|
data: Object.freeze([
|
|
988
999
|
{
|
|
989
1000
|
name: "demonym",
|
|
@@ -1007,6 +1018,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1007
1018
|
...NUM,
|
|
1008
1019
|
describe: "Approximate population, to two significant digits.",
|
|
1009
1020
|
},
|
|
1021
|
+
{
|
|
1022
|
+
name: "market",
|
|
1023
|
+
...NUM,
|
|
1024
|
+
check: checkMarket,
|
|
1025
|
+
describe:
|
|
1026
|
+
"What trade the settlement supports, on a scale of six: " +
|
|
1027
|
+
`${MARKET_CLASSES.map((c) => `${c.value} ${c.name}`).join(", ")}.`,
|
|
1028
|
+
},
|
|
1010
1029
|
{
|
|
1011
1030
|
name: "borders",
|
|
1012
1031
|
...LIST,
|
package/engine/site-build.mjs
CHANGED
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
* `/<package>/<type>-<shortcode>/`. Nothing is generated between them — no
|
|
37
37
|
* section directory, no listing, no `_index.md` but the root's. An index of
|
|
38
38
|
* what the package publishes is a `doc` note carrying a content table, and it
|
|
39
|
-
* is authored where every other page is.
|
|
39
|
+
* is authored where every other page is. The one file a page carries beside
|
|
40
|
+
* itself is the map from a place — see `engine/site-maps.mjs` — and a page
|
|
41
|
+
* that carries one is a leaf bundle at the same address.
|
|
40
42
|
*
|
|
41
43
|
* **Every gate reports; none exits.** The integrity checks a site build needs —
|
|
42
44
|
* a wikilink authored in frontmatter, a name that yields no slug, an unusable
|
|
@@ -82,6 +84,8 @@ import {
|
|
|
82
84
|
import { publishesContentPages } from "../content-config.mjs";
|
|
83
85
|
import { HUGO_CONTENT } from "./site-config.mjs";
|
|
84
86
|
import { homepageLinkTargets, relatedPages } from "./related-pages.mjs";
|
|
87
|
+
import { HOLDINGS_KEYS, foreignHoldingsNodes, holdingsNode, holdingsPages } from "./holdings.mjs";
|
|
88
|
+
import { drawSiteMaps } from "./site-maps.mjs";
|
|
85
89
|
|
|
86
90
|
const require = createRequire(import.meta.url);
|
|
87
91
|
|
|
@@ -489,7 +493,10 @@ export function tableUniverse(pages) {
|
|
|
489
493
|
* about every other page, known only once the whole tree has resolved — see
|
|
490
494
|
* {@link module:engine/related-pages} — so an authored value is dropped the
|
|
491
495
|
* way `aliases` is, and {@link renderPages} writes the derived block once it
|
|
492
|
-
* holds the graph.
|
|
496
|
+
* holds the graph. `contains`, `held_by` and `holdings` are dropped for the
|
|
497
|
+
* same reason — see {@link module:engine/holdings}. **So is `map`**: the
|
|
498
|
+
* file a place page names is the one the build drew beside it — see
|
|
499
|
+
* {@link module:engine/site-maps} — and a page with no drawing names none.
|
|
493
500
|
*
|
|
494
501
|
* @param {object} page - The page.
|
|
495
502
|
* @param {object} options
|
|
@@ -525,6 +532,8 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
|
525
532
|
if (decorate) decorate(data, page);
|
|
526
533
|
delete data.aliases;
|
|
527
534
|
delete data.related;
|
|
535
|
+
for (const key of HOLDINGS_KEYS) delete data[key];
|
|
536
|
+
delete data.map;
|
|
528
537
|
if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
|
|
529
538
|
return data;
|
|
530
539
|
}
|
|
@@ -598,11 +607,20 @@ function isPlainObject(value) {
|
|
|
598
607
|
* `doc` note's `subType` may be spelled the same as another note's `type`, and
|
|
599
608
|
* `doc-gear.md` and `weapongear-gear.md` are distinct.
|
|
600
609
|
*
|
|
610
|
+
* **A page that carries a file beside itself is a leaf bundle**, `<slug>/index.md`,
|
|
611
|
+
* because that is the one shape under which Hugo hands a page its own
|
|
612
|
+
* resources — the map from a place, inlined by the theme through
|
|
613
|
+
* `.Resources.Get`. The directory is the page, not a section: it holds no
|
|
614
|
+
* `_index.md`, and the page's stated `url` keeps its address exactly where the
|
|
615
|
+
* flat file's was.
|
|
616
|
+
*
|
|
601
617
|
* @param {object} page - The page.
|
|
618
|
+
* @param {object} [opts]
|
|
619
|
+
* @param {boolean} [opts.bundle=false] - Whether the page carries a resource.
|
|
602
620
|
* @returns {string} The file, relative to the mount.
|
|
603
621
|
*/
|
|
604
|
-
export function pageDestination(page) {
|
|
605
|
-
return `${page.slug}.md`;
|
|
622
|
+
export function pageDestination(page, { bundle = false } = {}) {
|
|
623
|
+
return bundle ? `${page.slug}/index.md` : `${page.slug}.md`;
|
|
606
624
|
}
|
|
607
625
|
|
|
608
626
|
/**
|
|
@@ -627,16 +645,25 @@ export function pageDestination(page) {
|
|
|
627
645
|
* point the resolver answers, and inverted once between them. Nothing is
|
|
628
646
|
* resolved twice.
|
|
629
647
|
*
|
|
648
|
+
* **A map is written with its page.** The drawing from a place is made before
|
|
649
|
+
* this render — see {@link module:engine/site-maps} — and handed in keyed by
|
|
650
|
+
* page URL; the page that owns one is written as a leaf bundle, the file is
|
|
651
|
+
* copied beside it, and the front matter names it as `map`. Every other page
|
|
652
|
+
* is written flat, and carries no such key.
|
|
653
|
+
*
|
|
630
654
|
* @param {object[]} pages - Every page.
|
|
631
655
|
* @param {object} options - Everything the render needs. `homepages` is the
|
|
632
656
|
* homepage as the index knows it — `{ fm, body, url, title }` — which takes
|
|
633
657
|
* no part in the render and every part in the graph: a content page links
|
|
634
|
-
* it by wikilink, and its own markdown links name content pages.
|
|
658
|
+
* it by wikilink, and its own markdown links name content pages. `maps` is
|
|
659
|
+
* each drawing by the URL of the page that carries it.
|
|
635
660
|
* @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
|
|
636
661
|
* wikiErrors: object[], imageErrors: object[],
|
|
637
|
-
* related: Map<string, import("./related-pages.mjs").Related
|
|
662
|
+
* related: Map<string, import("./related-pages.mjs").Related>,
|
|
663
|
+
* maps: number}} `related`
|
|
638
664
|
* is keyed by page URL, and holds the homepage's block beside every content
|
|
639
665
|
* page's, so the caller can write it on the page this render does not.
|
|
666
|
+
* `maps` counts the pages written with one.
|
|
640
667
|
*/
|
|
641
668
|
export function renderPages(pages, options) {
|
|
642
669
|
const {
|
|
@@ -651,6 +678,7 @@ export function renderPages(pages, options) {
|
|
|
651
678
|
config,
|
|
652
679
|
records = [],
|
|
653
680
|
homepages = [],
|
|
681
|
+
maps = new Map(),
|
|
654
682
|
} = options;
|
|
655
683
|
|
|
656
684
|
// The address space the art slots and the body's embeds resolve against:
|
|
@@ -783,17 +811,40 @@ export function renderPages(pages, options) {
|
|
|
783
811
|
}
|
|
784
812
|
|
|
785
813
|
const related = relatedPages(edges, entries);
|
|
814
|
+
// What lies within a place, who holds it, and what an affiliation holds
|
|
815
|
+
// — read off `parents` and `domains` across this package and every
|
|
816
|
+
// fetched index, local notes first so they shadow a dependency's.
|
|
817
|
+
const holdings = holdingsPages([
|
|
818
|
+
...pages.map((page) => holdingsNode(page.fm, { title: pageTitle(page), url: page.url })),
|
|
819
|
+
...foreignHoldingsNodes(foreign?.index),
|
|
820
|
+
]);
|
|
786
821
|
|
|
822
|
+
let withMap = 0;
|
|
787
823
|
for (const { page, body, data } of rendered) {
|
|
788
824
|
const block = related.get(page.url);
|
|
789
825
|
if (block) data.related = block;
|
|
790
|
-
|
|
826
|
+
Object.assign(data, holdings.get(page.url));
|
|
827
|
+
const map = maps.get(page.url);
|
|
828
|
+
const dest = path.join(outRoot, pageDestination(page, { bundle: Boolean(map) }));
|
|
791
829
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
830
|
+
if (map) {
|
|
831
|
+
fs.copyFileSync(map.file, path.join(path.dirname(dest), map.name));
|
|
832
|
+
data.map = map.name;
|
|
833
|
+
withMap += 1;
|
|
834
|
+
}
|
|
792
835
|
fs.writeFileSync(dest, matter.stringify(body, data));
|
|
793
836
|
byKind[page.kind] = (byKind[page.kind] ?? 0) + 1;
|
|
794
837
|
}
|
|
795
838
|
|
|
796
|
-
return {
|
|
839
|
+
return {
|
|
840
|
+
written: pages.length,
|
|
841
|
+
byKind,
|
|
842
|
+
tableErrors,
|
|
843
|
+
wikiErrors,
|
|
844
|
+
imageErrors,
|
|
845
|
+
related,
|
|
846
|
+
maps: withMap,
|
|
847
|
+
};
|
|
797
848
|
}
|
|
798
849
|
|
|
799
850
|
/**
|
|
@@ -846,10 +897,15 @@ export function resolveSitePass(name, options) {
|
|
|
846
897
|
* {@link module:engine/sql-tables.prepareSqlTables}. A page authoring an
|
|
847
898
|
* `sql` directive with none prepared is a table error: nothing here runs a
|
|
848
899
|
* query.
|
|
900
|
+
* @param {(engine: string) => string|undefined} [options.locate] - How a
|
|
901
|
+
* GraphViz engine's binary is found, for the maps; the real lookup by
|
|
902
|
+
* default.
|
|
849
903
|
* @returns {{gates: object, stats: object|null, tableErrors: object[],
|
|
850
|
-
* wikiErrors: object[], imageErrors: object[],
|
|
904
|
+
* wikiErrors: object[], imageErrors: object[], mapFindings: object[],
|
|
905
|
+
* manifests: object|null}} `mapFindings` is what drawing the maps found —
|
|
906
|
+
* warnings, never a reason to fail the build.
|
|
851
907
|
*/
|
|
852
|
-
export function buildSite({ config, sqlTables } = {}) {
|
|
908
|
+
export function buildSite({ config, sqlTables, locate } = {}) {
|
|
853
909
|
const resolved = config ?? loadPackConfig();
|
|
854
910
|
const site = resolved.site;
|
|
855
911
|
const scheme = resolved.publish.address;
|
|
@@ -945,6 +1001,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
945
1001
|
tableErrors: [],
|
|
946
1002
|
wikiErrors: [],
|
|
947
1003
|
imageErrors: [],
|
|
1004
|
+
mapFindings: [],
|
|
948
1005
|
stats: null,
|
|
949
1006
|
};
|
|
950
1007
|
}
|
|
@@ -963,6 +1020,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
963
1020
|
tableErrors: [],
|
|
964
1021
|
wikiErrors: [],
|
|
965
1022
|
imageErrors: [],
|
|
1023
|
+
mapFindings: [],
|
|
966
1024
|
stats: {
|
|
967
1025
|
homepages: writeHomepages(homeRoot, homepages, resolved),
|
|
968
1026
|
out: homeRoot,
|
|
@@ -1004,6 +1062,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1004
1062
|
tableErrors: [],
|
|
1005
1063
|
wikiErrors: [],
|
|
1006
1064
|
imageErrors: [],
|
|
1065
|
+
mapFindings: [],
|
|
1007
1066
|
};
|
|
1008
1067
|
}
|
|
1009
1068
|
|
|
@@ -1012,6 +1071,22 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1012
1071
|
repoRoot: resolved.rootDir,
|
|
1013
1072
|
});
|
|
1014
1073
|
|
|
1074
|
+
// The map from each related place, drawn now that every page is known to
|
|
1075
|
+
// be addressable and before any is written, so the page writer lays each
|
|
1076
|
+
// drawing beside its page. Drawn with `base`, which is what every name
|
|
1077
|
+
// in a drawing links through — the same `<base><slug>/` every other href
|
|
1078
|
+
// composes. Off, nothing is drawn and nothing is asked of GraphViz.
|
|
1079
|
+
const drawn =
|
|
1080
|
+
site.maps ?
|
|
1081
|
+
drawSiteMaps({
|
|
1082
|
+
records: ctx.records,
|
|
1083
|
+
foreignIndex: gates.foreign.index,
|
|
1084
|
+
config: resolved,
|
|
1085
|
+
base,
|
|
1086
|
+
...(locate ? { locate } : {}),
|
|
1087
|
+
})
|
|
1088
|
+
: { maps: new Map(), findings: [] };
|
|
1089
|
+
|
|
1015
1090
|
const rendered = renderPages(pages, {
|
|
1016
1091
|
outRoot: out,
|
|
1017
1092
|
sqlTables,
|
|
@@ -1021,6 +1096,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1021
1096
|
foreign: gates.foreign,
|
|
1022
1097
|
universe: tableUniverse(pages),
|
|
1023
1098
|
homepages: homepageEntries,
|
|
1099
|
+
maps: drawn.maps,
|
|
1024
1100
|
pass,
|
|
1025
1101
|
// What counts as a being is the toolchain's to say, not a consumer's.
|
|
1026
1102
|
// Asking in a consumer's script is how one came to still be checking
|
|
@@ -1053,9 +1129,11 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1053
1129
|
tableErrors: rendered.tableErrors,
|
|
1054
1130
|
wikiErrors: rendered.wikiErrors,
|
|
1055
1131
|
imageErrors: rendered.imageErrors,
|
|
1132
|
+
mapFindings: drawn.findings,
|
|
1056
1133
|
stats: {
|
|
1057
1134
|
...rendered.byKind,
|
|
1058
1135
|
homepages: homepagesWritten,
|
|
1136
|
+
maps: rendered.maps,
|
|
1059
1137
|
out,
|
|
1060
1138
|
},
|
|
1061
1139
|
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The maps a site build writes into its place pages.
|
|
16
|
+
*
|
|
17
|
+
* A place that states, or is named in, a border or a route has a map from it
|
|
18
|
+
* — the drawing `content-build map --from` makes — and the site build draws
|
|
19
|
+
* that map for every such place of this package and hands each one to the
|
|
20
|
+
* page writer, which lays it beside the page as `from-<shortcode>.svg` and
|
|
21
|
+
* names it in the front matter as `map`. The theme inlines it, so its place
|
|
22
|
+
* names are links to their pages: every drawing is made with the site's base,
|
|
23
|
+
* and every `href` in it composes `<base><slug>/` the way every other href
|
|
24
|
+
* the build renders does.
|
|
25
|
+
*
|
|
26
|
+
* GraphViz draws, and a site never fails for want of a map: when the engine is
|
|
27
|
+
* absent the build says so once, as a warning, and writes every page as it
|
|
28
|
+
* would without maps. `site.maps: false` says the site carries none and asks
|
|
29
|
+
* nothing of GraphViz.
|
|
30
|
+
*
|
|
31
|
+
* The drawings land under `build/map/site/` — beside the author's own
|
|
32
|
+
* drawings under `build/map/`, and apart from them, because the site's
|
|
33
|
+
* carry links and the author's may not — and are copied from there into the
|
|
34
|
+
* content mount. A build never mutates its inputs.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import fs from "node:fs";
|
|
40
|
+
import path from "node:path";
|
|
41
|
+
|
|
42
|
+
import { buildMaps, relatedPlaces } from "./map-build.mjs";
|
|
43
|
+
import { GRAPHVIZ_INSTALL, findGraphviz } from "./map-graphviz.mjs";
|
|
44
|
+
import { mapWorld } from "./map-places.mjs";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Where the site build draws, below the package root.
|
|
48
|
+
*
|
|
49
|
+
* @type {string}
|
|
50
|
+
*/
|
|
51
|
+
export const SITE_MAP_DIR = "build/map/site";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The engine the map from a place is drawn with.
|
|
55
|
+
*
|
|
56
|
+
* @type {string}
|
|
57
|
+
*/
|
|
58
|
+
const ENGINE = "neato";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A drawing as the page writer receives it.
|
|
62
|
+
*
|
|
63
|
+
* @typedef {object} SiteMap
|
|
64
|
+
* @property {string} name - The file's name in the page's bundle, and the
|
|
65
|
+
* value of the page's `map` key: `from-<shortcode>.svg`.
|
|
66
|
+
* @property {string} file - Where the drawing is, absolute.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A GraphViz SVG rewritten for inlining in an HTML page.
|
|
71
|
+
*
|
|
72
|
+
* GraphViz writes a standalone document: an XML declaration, a DOCTYPE naming
|
|
73
|
+
* the SVG 1.1 DTD by URL, generator comments, a root sized in points beside
|
|
74
|
+
* its `viewBox`, and links through the `xlink` namespace. Inline in HTML the
|
|
75
|
+
* prologue is invalid, the DTD reference is the one external reference the
|
|
76
|
+
* drawing would carry, the fixed size fights the column it sits in, and
|
|
77
|
+
* `xlink:` is the pre-SVG-2 spelling. So the prologue and the comments go,
|
|
78
|
+
* the root keeps its `viewBox` and loses `width` and `height`, and
|
|
79
|
+
* `xlink:href` and `xlink:title` become `href` and `title`, with the
|
|
80
|
+
* namespace declaration they needed.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} svg - The SVG as GraphViz wrote it.
|
|
83
|
+
* @returns {string} The SVG to inline, starting at `<svg`.
|
|
84
|
+
*/
|
|
85
|
+
export function inlineSvg(svg) {
|
|
86
|
+
const start = svg.indexOf("<svg");
|
|
87
|
+
let out = start < 0 ? svg : svg.slice(start);
|
|
88
|
+
out = out.replace(/<!--[\s\S]*?-->\n?/g, "");
|
|
89
|
+
out = out.replace(/^<svg[^>]*>/, (root) =>
|
|
90
|
+
root
|
|
91
|
+
.replace(/\s+(width|height)="[^"]*"/g, "")
|
|
92
|
+
.replace(/\s+xmlns:xlink="[^"]*"/, "")
|
|
93
|
+
.replace(/^<svg\s+/, "<svg "),
|
|
94
|
+
);
|
|
95
|
+
return out.replace(/\bxlink:(href|title)=/g, "$1=");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Draw the map from every related place of this package.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} opts
|
|
102
|
+
* @param {Array<Record<string, any>>} opts.records - The package's index
|
|
103
|
+
* records, as the site build read them.
|
|
104
|
+
* @param {Map<string, object>} opts.foreignIndex - The fetched indexes the
|
|
105
|
+
* site build resolved links against.
|
|
106
|
+
* @param {object} opts.config - The resolved configuration.
|
|
107
|
+
* @param {string} opts.base - The site base every page is served under,
|
|
108
|
+
* ending in a slash: what every name in a drawing links through.
|
|
109
|
+
* @param {(engine: string) => string|undefined} [opts.locate] - How the
|
|
110
|
+
* engine's binary is found; the real lookup by default.
|
|
111
|
+
* @returns {{maps: Map<string, SiteMap>, findings: Array<{file: string,
|
|
112
|
+
* line?: number, column?: number, severity: "error"|"warning",
|
|
113
|
+
* message: string}>}} The drawings, keyed by the URL of the page each
|
|
114
|
+
* belongs to, and what the drawing found — one warning naming what to
|
|
115
|
+
* install when GraphViz is absent, and a warning for each relation that
|
|
116
|
+
* names no place.
|
|
117
|
+
*/
|
|
118
|
+
export function drawSiteMaps({ records, foreignIndex, config, base, locate = findGraphviz }) {
|
|
119
|
+
/** @type {Map<string, SiteMap>} */
|
|
120
|
+
const maps = new Map();
|
|
121
|
+
const outDir = path.join(config.rootDir, ...SITE_MAP_DIR.split("/"));
|
|
122
|
+
// Every run draws afresh: a place that lost its relations must not keep
|
|
123
|
+
// the map an earlier run drew.
|
|
124
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
125
|
+
|
|
126
|
+
if (!locate(ENGINE)) {
|
|
127
|
+
return {
|
|
128
|
+
maps,
|
|
129
|
+
findings: [
|
|
130
|
+
{
|
|
131
|
+
file: config.rootDir,
|
|
132
|
+
severity: "warning",
|
|
133
|
+
message:
|
|
134
|
+
`GraphViz's \`${ENGINE}\` is not installed, so no place page carries ` +
|
|
135
|
+
`the map from that place; ${GRAPHVIZ_INSTALL} to draw them, or set ` +
|
|
136
|
+
"`site.maps: false` to say the site carries none",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const world = mapWorld({
|
|
143
|
+
records,
|
|
144
|
+
foreignIndex,
|
|
145
|
+
contentBase: config.paths.content,
|
|
146
|
+
base,
|
|
147
|
+
config,
|
|
148
|
+
});
|
|
149
|
+
// This package's related places, which are the ones with a page here to
|
|
150
|
+
// carry a map. A dependency's place is drawn on those maps and gets none
|
|
151
|
+
// of its own from this build.
|
|
152
|
+
const centres = relatedPlaces(world.places).filter((s) => world.places.get(s)?.local);
|
|
153
|
+
if (centres.length === 0) return { maps, findings: [] };
|
|
154
|
+
|
|
155
|
+
const { findings } = buildMaps({ world, outDir, from: centres, locate });
|
|
156
|
+
for (const centre of centres) {
|
|
157
|
+
const place = world.places.get(centre);
|
|
158
|
+
if (!place?.url) continue;
|
|
159
|
+
const name = `from-${centre}.svg`;
|
|
160
|
+
const file = path.join(outDir, name);
|
|
161
|
+
fs.writeFileSync(file, inlineSvg(fs.readFileSync(file, "utf8")));
|
|
162
|
+
maps.set(place.url, { name, file });
|
|
163
|
+
}
|
|
164
|
+
return { maps, findings };
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.4.
|
|
3
|
+
"version": "22.4.7",
|
|
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",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a local note into a node, or `null` for a note that takes no part.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} fm - The note's frontmatter.
|
|
5
|
+
* @param {object} page - The page it publishes as.
|
|
6
|
+
* @param {string} page.title - Its published title.
|
|
7
|
+
* @param {string} page.url - Its URL.
|
|
8
|
+
* @returns {HoldingsNode|null} The node, or `null` for a note that is neither
|
|
9
|
+
* a place nor an affiliation, or that declares no shortcode.
|
|
10
|
+
*/
|
|
11
|
+
export function holdingsNode(fm: object, { title, url }: {
|
|
12
|
+
title: string;
|
|
13
|
+
url: string;
|
|
14
|
+
}): HoldingsNode | null;
|
|
15
|
+
/**
|
|
16
|
+
* Read a fetched index's places and affiliations as nodes.
|
|
17
|
+
*
|
|
18
|
+
* An entry carries what the producer's record stated — its name, `subType`,
|
|
19
|
+
* `parents` and `domains` — and its page URL where the package publishes one.
|
|
20
|
+
*
|
|
21
|
+
* @param {Map<string, object>|undefined} foreignIndex - As
|
|
22
|
+
* {@link module:engine/metadata-index.loadForeignIndexes} returns it.
|
|
23
|
+
* @returns {HoldingsNode[]} The nodes, in index order.
|
|
24
|
+
*/
|
|
25
|
+
export function foreignHoldingsNodes(foreignIndex: Map<string, object> | undefined): HoldingsNode[];
|
|
26
|
+
/**
|
|
27
|
+
* Invert `parents` and `domains` into each page's lists.
|
|
28
|
+
*
|
|
29
|
+
* A shortcode is declared once per type: the first node declaring it wins,
|
|
30
|
+
* so local nodes handed in ahead of fetched ones shadow a dependency's, the
|
|
31
|
+
* way the link resolver answers. A node with no URL is nobody's entry and
|
|
32
|
+
* carries no lists of its own, since there is no page to write them on. A
|
|
33
|
+
* name no node declares is dropped: a `parents` naming nowhere is the map's
|
|
34
|
+
* finding, a `domains` naming nowhere the reference check's.
|
|
35
|
+
*
|
|
36
|
+
* The result holds only pages with at least one entry on at least one list,
|
|
37
|
+
* and a page's block carries only the keys it has entries for — the theme's
|
|
38
|
+
* silent-disappear convention, kept at the source.
|
|
39
|
+
*
|
|
40
|
+
* @param {Iterable<HoldingsNode|null>} nodes - Every place and affiliation,
|
|
41
|
+
* local first. `parents` and `domains` are read as written, so a node
|
|
42
|
+
* built by hand may carry them as the note spells them.
|
|
43
|
+
* @returns {Map<string, Holdings>} URL → the page's lists, sorted.
|
|
44
|
+
*/
|
|
45
|
+
export function holdingsPages(nodes: Iterable<HoldingsNode | null>): Map<string, Holdings>;
|
|
46
|
+
/**
|
|
47
|
+
* Check a place note's tenure: a settlement, a site or a structure that no
|
|
48
|
+
* affiliation's `domains` names is unheld land, reported as a warning at the
|
|
49
|
+
* note's `type:` line.
|
|
50
|
+
*
|
|
51
|
+
* Declared on the `place` vocabulary as its type-level check, so the lint
|
|
52
|
+
* runs it beside the field checks with the same index. Without an index the
|
|
53
|
+
* question cannot be asked and nothing is reported.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} note - The note, as the link index hands it over.
|
|
56
|
+
* @param {object} [opts]
|
|
57
|
+
* @param {object} [opts.index] - The link index, holding every affiliation.
|
|
58
|
+
* @returns {object[]} Findings.
|
|
59
|
+
*/
|
|
60
|
+
export function checkHeld(note: object, { index }?: {
|
|
61
|
+
index?: object | undefined;
|
|
62
|
+
}): object[];
|
|
63
|
+
/**
|
|
64
|
+
* The keys this module writes, and which a note cannot author: `contains`
|
|
65
|
+
* and `held_by` on a place, `holdings` on an affiliation.
|
|
66
|
+
*
|
|
67
|
+
* @type {readonly string[]}
|
|
68
|
+
*/
|
|
69
|
+
export const HOLDINGS_KEYS: readonly string[];
|
|
70
|
+
/**
|
|
71
|
+
* The place subTypes tenure is checked on. A region is held through its
|
|
72
|
+
* polity's `domains`, a world by nobody, a feature by nobody — a river has no
|
|
73
|
+
* lord — so the rest are the kinds of place a body holds directly.
|
|
74
|
+
*
|
|
75
|
+
* @type {readonly string[]}
|
|
76
|
+
*/
|
|
77
|
+
export const HELD_SUBTYPES: readonly string[];
|
|
78
|
+
/**
|
|
79
|
+
* One entry of a holdings list — a page a reader is pointed at.
|
|
80
|
+
*/
|
|
81
|
+
export type HoldingsEntry = {
|
|
82
|
+
/**
|
|
83
|
+
* The page's published title.
|
|
84
|
+
*/
|
|
85
|
+
title: string;
|
|
86
|
+
/**
|
|
87
|
+
* `<base><slug>/` — the page's address as every
|
|
88
|
+
* href this build renders composes it.
|
|
89
|
+
*/
|
|
90
|
+
url: string;
|
|
91
|
+
/**
|
|
92
|
+
* The note's `type` — `place` or `affiliation`.
|
|
93
|
+
*/
|
|
94
|
+
type: string;
|
|
95
|
+
/**
|
|
96
|
+
* The note's `subType`, where it declares one.
|
|
97
|
+
*/
|
|
98
|
+
subType?: string | undefined;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* One place or affiliation, as the derivation reads it — a local page or a
|
|
102
|
+
* fetched index entry through one shape.
|
|
103
|
+
*/
|
|
104
|
+
export type HoldingsNode = {
|
|
105
|
+
/**
|
|
106
|
+
* Lower case.
|
|
107
|
+
*/
|
|
108
|
+
shortcode: string;
|
|
109
|
+
type: "place" | "affiliation";
|
|
110
|
+
subType?: string | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* The page's published title.
|
|
113
|
+
*/
|
|
114
|
+
title: string;
|
|
115
|
+
/**
|
|
116
|
+
* The page's URL; absent where the package
|
|
117
|
+
* publishes no page for it.
|
|
118
|
+
*/
|
|
119
|
+
url?: string | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* What a place sits within, as written.
|
|
122
|
+
*/
|
|
123
|
+
parents: string[];
|
|
124
|
+
/**
|
|
125
|
+
* What an affiliation holds, as written.
|
|
126
|
+
*/
|
|
127
|
+
domains: string[];
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The lists one page carries. Every key is optional, and a key present holds
|
|
131
|
+
* at least one entry.
|
|
132
|
+
*/
|
|
133
|
+
export type Holdings = {
|
|
134
|
+
/**
|
|
135
|
+
* Places whose `parents` name this one.
|
|
136
|
+
*/
|
|
137
|
+
contains?: HoldingsEntry[] | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Affiliations whose `domains` name this one.
|
|
140
|
+
*/
|
|
141
|
+
held_by?: HoldingsEntry[] | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Places this affiliation's `domains` name.
|
|
144
|
+
*/
|
|
145
|
+
holdings?: HoldingsEntry[] | undefined;
|
|
146
|
+
};
|
package/types/engine/index.d.mts
CHANGED
|
@@ -16,11 +16,14 @@ export * as homepage from "./homepage.mjs";
|
|
|
16
16
|
export * as noteSchemas from "./note-schemas.mjs";
|
|
17
17
|
export * as noteVocabulary from "./note-vocabulary.mjs";
|
|
18
18
|
export * as placeRelations from "./place-relations.mjs";
|
|
19
|
+
export * as holdings from "./holdings.mjs";
|
|
20
|
+
export * as marketClass from "./market-class.mjs";
|
|
19
21
|
export * as mapPlaces from "./map-places.mjs";
|
|
20
22
|
export * as mapLayout from "./map-layout.mjs";
|
|
21
23
|
export * as mapDot from "./map-dot.mjs";
|
|
22
24
|
export * as mapGraphviz from "./map-graphviz.mjs";
|
|
23
25
|
export * as mapBuild from "./map-build.mjs";
|
|
26
|
+
export * as siteMaps from "./site-maps.mjs";
|
|
24
27
|
export * as systems from "./systems.mjs";
|
|
25
28
|
export * as contentSlug from "./content-slug.mjs";
|
|
26
29
|
export * as contentAddress from "./content-address.mjs";
|
|
@@ -21,7 +21,7 @@ export function findGraphviz(engine: string, { env, fallbacks }?: {
|
|
|
21
21
|
}): string | undefined;
|
|
22
22
|
/**
|
|
23
23
|
* What to say when an engine is not installed: the name, and what installs
|
|
24
|
-
* it
|
|
24
|
+
* it.
|
|
25
25
|
*
|
|
26
26
|
* @param {string} engine - The engine that was wanted.
|
|
27
27
|
* @returns {string} The message.
|
|
@@ -59,3 +59,10 @@ export function renderDot(dotPath: string, outPath: string, { binary, format, ar
|
|
|
59
59
|
* @type {readonly string[]}
|
|
60
60
|
*/
|
|
61
61
|
export const GRAPHVIZ_ENGINES: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* What installs GraphViz on the two platforms a contributor is likely to be
|
|
64
|
+
* on, as the clause every "not installed" message ends with.
|
|
65
|
+
*
|
|
66
|
+
* @type {string}
|
|
67
|
+
*/
|
|
68
|
+
export const GRAPHVIZ_INSTALL: string;
|
|
@@ -34,6 +34,32 @@ export function loadMapWorld({ config, contentBase, base, problems }: {
|
|
|
34
34
|
base?: string | undefined;
|
|
35
35
|
problems?: object[] | undefined;
|
|
36
36
|
}): MapWorld;
|
|
37
|
+
/**
|
|
38
|
+
* The world a map is drawn from, out of records and indexes a caller already
|
|
39
|
+
* holds: this package's places from its index records, and every
|
|
40
|
+
* dependency's from the fetched indexes. The site build reads its corpus and
|
|
41
|
+
* its foreign indexes once for every surface it publishes, and hands them
|
|
42
|
+
* here rather than walking the tree a second time.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} opts
|
|
45
|
+
* @param {Array<Record<string, any>>} opts.records - Index records, as
|
|
46
|
+
* {@link module:engine/content-index.indexRecordsFor} returns them.
|
|
47
|
+
* @param {Map<string, object>} opts.foreignIndex - The fetched indexes, as
|
|
48
|
+
* {@link module:engine/metadata-index.loadForeignIndexes} returns `index`.
|
|
49
|
+
* @param {string} opts.contentBase - The content root the records were read from.
|
|
50
|
+
* @param {string} [opts.base] - The site base this package's pages are served
|
|
51
|
+
* under. Absent, no local place carries a URL.
|
|
52
|
+
* @param {object} opts.config - The resolved configuration.
|
|
53
|
+
* @returns {MapWorld} The world, with nothing stale: the caller has already
|
|
54
|
+
* judged its indexes usable.
|
|
55
|
+
*/
|
|
56
|
+
export function mapWorld({ records, foreignIndex, contentBase, base, config }: {
|
|
57
|
+
records: Array<Record<string, any>>;
|
|
58
|
+
foreignIndex: Map<string, object>;
|
|
59
|
+
contentBase: string;
|
|
60
|
+
base?: string | undefined;
|
|
61
|
+
config: object;
|
|
62
|
+
}): MapWorld;
|
|
37
63
|
/**
|
|
38
64
|
* Analyse the containment tree.
|
|
39
65
|
*
|