@heroiclands/package-build 22.4.4 → 22.4.6
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 +24 -0
- package/CONTENT.md +30 -1
- package/README.md +9 -0
- package/bin/content-build.mjs +163 -0
- package/bin/package-build.mjs +18 -4
- package/content-config.mjs +7 -0
- package/docs/api.md +88 -0
- package/docs/commands.md +222 -2
- package/docs/configuration.md +19 -1
- package/docs/content-format.md +105 -6
- package/docs/diagnostics.md +1 -0
- package/docs/project-setup.md +7 -6
- package/engine/frontmatter-lint.mjs +11 -1
- package/engine/index.mjs +18 -0
- package/engine/map-build.mjs +270 -0
- package/engine/map-dot.mjs +745 -0
- package/engine/map-graphviz.mjs +138 -0
- package/engine/map-layout.mjs +894 -0
- package/engine/map-places.mjs +561 -0
- package/engine/metadata-index.mjs +7 -0
- package/engine/note-vocabulary.mjs +28 -0
- package/engine/place-relations.mjs +637 -0
- package/engine/site-config.mjs +1 -1
- package/engine/site-root.mjs +147 -6
- package/package.json +3 -2
- package/types/engine/index.d.mts +6 -0
- package/types/engine/map-build.d.mts +84 -0
- package/types/engine/map-dot.d.mts +143 -0
- package/types/engine/map-graphviz.d.mts +61 -0
- package/types/engine/map-layout.d.mts +427 -0
- package/types/engine/map-places.d.mts +259 -0
- package/types/engine/note-vocabulary.d.mts +12 -0
- package/types/engine/place-relations.d.mts +95 -0
- package/types/engine/site-config.d.mts +1 -1
- package/types/engine/site-root.d.mts +59 -7
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
* `content-build map`: the drawings, written under `build/map/`.
|
|
16
|
+
*
|
|
17
|
+
* Four modes, each a function of the world the content index describes:
|
|
18
|
+
*
|
|
19
|
+
* - **`tree`** — the containment tree from `parents`, the author's check.
|
|
20
|
+
* `tree.svg` for the whole world and `tree-<continent>.svg` per continent,
|
|
21
|
+
* or `tree-<root>.svg` for the subtree beneath one place. The anomalies
|
|
22
|
+
* are returned as findings.
|
|
23
|
+
* - **`from`** — the map from a place, `from-<shortcode>.svg`, for each
|
|
24
|
+
* place named, or for every place that takes part in a border or a route
|
|
25
|
+
* with `all`.
|
|
26
|
+
* - **`chart`** — the chart from a place, `chart-<shortcode>.svg`: the same
|
|
27
|
+
* drawing at a larger horizon, the hops chaining on as long as none turns
|
|
28
|
+
* back. `all` as for `from`.
|
|
29
|
+
* - **`travel`** — the whole route graph, `travel.svg`.
|
|
30
|
+
*
|
|
31
|
+
* Every rendering is written beside the `.dot` it was drawn from, and the
|
|
32
|
+
* `.dot` is written first: with GraphViz absent the command either stops with
|
|
33
|
+
* an error naming what to install, or — where the caller asked it to — writes
|
|
34
|
+
* the `.dot` files, returns one warning, and skips the rendering, which is
|
|
35
|
+
* what a site build asking for maps does rather than fail.
|
|
36
|
+
*
|
|
37
|
+
* A build never mutates its inputs: nothing here reads a note except to
|
|
38
|
+
* locate a finding, and everything lands under the output directory.
|
|
39
|
+
*
|
|
40
|
+
* @module
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import fs from "node:fs";
|
|
44
|
+
import path from "node:path";
|
|
45
|
+
|
|
46
|
+
import { fromDot, travelDot, treeDot } from "./map-dot.mjs";
|
|
47
|
+
import { findGraphviz, graphvizMissingMessage, renderDot } from "./map-graphviz.mjs";
|
|
48
|
+
import { CHART_HORIZON_DAYS, layoutChart, layoutFrom, travelGraph } from "./map-layout.mjs";
|
|
49
|
+
import { TRAVEL_DAYS } from "./place-relations.mjs";
|
|
50
|
+
import { analyzeContainment, containmentFindings } from "./map-places.mjs";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The directory the maps land in, below a package's root.
|
|
54
|
+
*
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
export const MAP_DIR = "build/map";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The value of `--from` or `--chart` that means every place with a relation.
|
|
61
|
+
*
|
|
62
|
+
* @type {string}
|
|
63
|
+
*/
|
|
64
|
+
export const FROM_ALL = "all";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Every place that states, or is named in, a border or a route — the places
|
|
68
|
+
* `--from all` and `--chart all` draw.
|
|
69
|
+
*
|
|
70
|
+
* @param {Map<string, import("./map-places.mjs").MapPlace>} places - Every place.
|
|
71
|
+
* @returns {string[]} Shortcodes, sorted.
|
|
72
|
+
*/
|
|
73
|
+
export function relatedPlaces(places) {
|
|
74
|
+
const related = new Set();
|
|
75
|
+
for (const place of places.values()) {
|
|
76
|
+
for (const entry of [...place.borders, ...place.routes]) {
|
|
77
|
+
if (!places.has(entry.to)) continue;
|
|
78
|
+
related.add(place.shortcode);
|
|
79
|
+
related.add(entry.to);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...related].sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the maps.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} opts
|
|
89
|
+
* @param {import("./map-places.mjs").MapWorld} opts.world - The places, as
|
|
90
|
+
* {@link module:engine/map-places.loadMapWorld} returns them.
|
|
91
|
+
* @param {string} opts.outDir - Where to write; created if absent.
|
|
92
|
+
* @param {boolean} [opts.tree] - Draw the containment tree.
|
|
93
|
+
* @param {string} [opts.root] - With `tree`, the subtree beneath this place.
|
|
94
|
+
* @param {string[]} [opts.from] - Draw the map from each of these places, or
|
|
95
|
+
* from every related place for {@link FROM_ALL}.
|
|
96
|
+
* @param {string[]} [opts.chart] - Draw the chart from each of these places,
|
|
97
|
+
* or from every related place for {@link FROM_ALL}.
|
|
98
|
+
* @param {number} [opts.horizon] - The charts' horizon in days, a marker of
|
|
99
|
+
* the scale; the end of the scale by default.
|
|
100
|
+
* @param {boolean} [opts.travel] - Draw the whole route graph.
|
|
101
|
+
* @param {string} [opts.engine="dot"] - The tree's engine: `dot`, `twopi` or
|
|
102
|
+
* `neato`. The map and the chart from a place are always `neato` with
|
|
103
|
+
* every node pinned, and the route graph always `neato`.
|
|
104
|
+
* @param {string} [opts.rankdir="TB"] - The tree's rank direction, `dot` only.
|
|
105
|
+
* @param {number} [opts.nodesep] - The tree's `nodesep`, inches.
|
|
106
|
+
* @param {number} [opts.ranksep] - The tree's `ranksep`, inches.
|
|
107
|
+
* @param {number} [opts.scale=1] - Multiplies every node's size and font.
|
|
108
|
+
* @param {boolean} [opts.polities=true] - Whether the tree draws polities.
|
|
109
|
+
* @param {(engine: string) => string|undefined} [opts.locate] - How a GraphViz
|
|
110
|
+
* engine's binary is found; the real lookup by default.
|
|
111
|
+
* @param {boolean} [opts.requireGraphviz=true] - Whether a missing engine is
|
|
112
|
+
* an error. False, the `.dot` files are written, the renderings skipped,
|
|
113
|
+
* and one warning returned.
|
|
114
|
+
* @returns {{written: string[], findings: Array<{file: string, line?: number,
|
|
115
|
+
* column?: number, severity: "error"|"warning", message: string}>,
|
|
116
|
+
* skipped: boolean}} What was written, what was found, and whether the
|
|
117
|
+
* rendering was skipped for want of GraphViz.
|
|
118
|
+
* @throws {Error} When no mode is asked for, a named place is not one, the
|
|
119
|
+
* horizon is not a marker of the scale, or GraphViz is required and absent.
|
|
120
|
+
*/
|
|
121
|
+
export function buildMaps({
|
|
122
|
+
world,
|
|
123
|
+
outDir,
|
|
124
|
+
tree = false,
|
|
125
|
+
root,
|
|
126
|
+
from = [],
|
|
127
|
+
chart = [],
|
|
128
|
+
horizon = CHART_HORIZON_DAYS,
|
|
129
|
+
travel = false,
|
|
130
|
+
engine = "dot",
|
|
131
|
+
rankdir = "TB",
|
|
132
|
+
nodesep,
|
|
133
|
+
ranksep,
|
|
134
|
+
scale = 1,
|
|
135
|
+
polities = true,
|
|
136
|
+
locate = findGraphviz,
|
|
137
|
+
requireGraphviz = true,
|
|
138
|
+
}) {
|
|
139
|
+
if (!tree && from.length === 0 && chart.length === 0 && !travel) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"name a drawing: --tree, --from <shortcode>, --chart <shortcode> or --travel",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!TRAVEL_DAYS.includes(horizon)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`--horizon must be a marker of the days scale — one of ${TRAVEL_DAYS.join(", ")} — and ${horizon} is not`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const { places } = world;
|
|
150
|
+
|
|
151
|
+
// Every binary this run needs, found once and before anything is drawn,
|
|
152
|
+
// so a missing one is reported before a directory is touched.
|
|
153
|
+
const engines = new Set();
|
|
154
|
+
if (tree) engines.add(engine);
|
|
155
|
+
if (from.length || chart.length || travel) engines.add("neato");
|
|
156
|
+
/** @type {Map<string, string>} */
|
|
157
|
+
const binaries = new Map();
|
|
158
|
+
const findings = [];
|
|
159
|
+
let skipped = false;
|
|
160
|
+
for (const name of engines) {
|
|
161
|
+
const binary = locate(name);
|
|
162
|
+
if (binary) binaries.set(name, binary);
|
|
163
|
+
else if (requireGraphviz) throw new Error(graphvizMissingMessage(name));
|
|
164
|
+
else if (!skipped) {
|
|
165
|
+
skipped = true;
|
|
166
|
+
findings.push({
|
|
167
|
+
file: outDir,
|
|
168
|
+
severity: "warning",
|
|
169
|
+
message: `${graphvizMissingMessage(name)}; the .dot files are written and the renderings skipped`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
175
|
+
const written = [];
|
|
176
|
+
const emit = (name, dot, engineName, args = []) => {
|
|
177
|
+
const dotPath = path.join(outDir, `${name}.dot`);
|
|
178
|
+
fs.writeFileSync(dotPath, dot);
|
|
179
|
+
written.push(dotPath);
|
|
180
|
+
const binary = binaries.get(engineName);
|
|
181
|
+
if (!binary) return;
|
|
182
|
+
const svgPath = path.join(outDir, `${name}.svg`);
|
|
183
|
+
const { warnings } = renderDot(dotPath, svgPath, { binary, args });
|
|
184
|
+
written.push(svgPath);
|
|
185
|
+
if (warnings) {
|
|
186
|
+
findings.push({ file: dotPath, severity: "warning", message: warnings });
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (tree) {
|
|
191
|
+
const analysis = analyzeContainment(places, world.polities);
|
|
192
|
+
const common = {
|
|
193
|
+
places,
|
|
194
|
+
polities: world.polities,
|
|
195
|
+
analysis,
|
|
196
|
+
engine,
|
|
197
|
+
rankdir,
|
|
198
|
+
nodesep,
|
|
199
|
+
ranksep,
|
|
200
|
+
scale,
|
|
201
|
+
drawPolities: polities,
|
|
202
|
+
};
|
|
203
|
+
if (root !== undefined) {
|
|
204
|
+
emit(`tree-${root}`, treeDot({ ...common, root }), engine);
|
|
205
|
+
} else {
|
|
206
|
+
emit("tree", treeDot(common), engine);
|
|
207
|
+
for (const continent of analysis.continents) {
|
|
208
|
+
emit(`tree-${continent}`, treeDot({ ...common, root: continent }), engine);
|
|
209
|
+
}
|
|
210
|
+
findings.push(
|
|
211
|
+
...containmentFindings(analysis, places, {
|
|
212
|
+
contentBase: world.contentBase,
|
|
213
|
+
config: world.config,
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const centres = from.includes(FROM_ALL) ? relatedPlaces(places) : from;
|
|
220
|
+
for (const centre of centres) {
|
|
221
|
+
const layout = layoutFrom(places, centre);
|
|
222
|
+
for (const { from: place, to } of layout.unresolved) {
|
|
223
|
+
findings.push({
|
|
224
|
+
file: unresolvedFile(world, place),
|
|
225
|
+
severity: "warning",
|
|
226
|
+
message: `place "${place}" names "${to}" in a border or a route, and no place declares that shortcode; the map centred on "${centre}" leaves it out`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
emit(`from-${centre}`, fromDot(layout, places, { scale }), "neato", ["-n2"]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const viewpoints = chart.includes(FROM_ALL) ? relatedPlaces(places) : chart;
|
|
233
|
+
for (const centre of viewpoints) {
|
|
234
|
+
const layout = layoutChart(places, centre, { horizon });
|
|
235
|
+
for (const { from: place, to } of layout.unresolved) {
|
|
236
|
+
findings.push({
|
|
237
|
+
file: unresolvedFile(world, place),
|
|
238
|
+
severity: "warning",
|
|
239
|
+
message: `place "${place}" names "${to}" in a border or a route, and no place declares that shortcode; the chart centred on "${centre}" leaves it out`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
emit(`chart-${centre}`, fromDot(layout, places, { scale }), "neato", ["-n2"]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (travel) {
|
|
246
|
+
const graph = travelGraph(places);
|
|
247
|
+
for (const { from: place, to } of graph.unresolved) {
|
|
248
|
+
findings.push({
|
|
249
|
+
file: unresolvedFile(world, place),
|
|
250
|
+
severity: "warning",
|
|
251
|
+
message: `place "${place}" names "${to}" in a border or a route, and no place declares that shortcode; the route graph leaves it out`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
emit("travel", travelDot(graph, places, { scale }), "neato");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { written, findings, skipped };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The file a finding about a place's relation is reported against.
|
|
262
|
+
*
|
|
263
|
+
* @param {import("./map-places.mjs").MapWorld} world - The world.
|
|
264
|
+
* @param {string} shortcode - The place.
|
|
265
|
+
* @returns {string} The note, or the content root for a place with no file.
|
|
266
|
+
*/
|
|
267
|
+
function unresolvedFile(world, shortcode) {
|
|
268
|
+
const place = world.places.get(shortcode);
|
|
269
|
+
return place?.file ? path.join(world.contentBase, ...place.file.split("/")) : world.contentBase;
|
|
270
|
+
}
|