@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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.4.6
4
+
5
+ ### Patch Changes
6
+
7
+ `content-build map --chart <shortcode>` draws the chart from a place: the map from it at a larger horizon, north up, the hops chaining on as long as none turns back, each place on the ring of its summed days and each hop beyond the neighbours drawn fainter. `--horizon <days>` sets how far the rings reach, a year by default, and a place farther than that is left out.
8
+
9
+ ## 22.4.5
10
+
11
+ ### Patch Changes
12
+
13
+ A `place` note may state what it is next to and what it is reachable from, as `data.borders` (`{ to, bearing }`) and `data.routes` (`{ to, bearing, mode, days, terrain?, leagues? }`), with bearings from the eight compass points, modes `land | boat | ship`, days from the marker scale `1 2 3 5 10 20 30 45 60 90 180 360` and terrains from a closed registry. `content-build lint` checks that every `to` is a place, that every value is from its set, and that the other note states the pair back — a missing reciprocal is a warning naming both notes, a contradictory one an error. The content index carries both lists, so a dependency's places take part.
14
+
15
+ **Maps**
16
+
17
+ - A new `content-build map --tree` draws the containment tree from `parents`, clustered by continent, and reports a place with no parent, a parent no place declares, and a cycle.
18
+ - `content-build map --from <shortcode>` draws the map from that place: north up, each neighbour at its bearing and on a ring of its days, places two hops out drawn dimmer.
19
+ - `--from all` draws one map for every place with a border or a route, and `--travel` draws the whole route graph.
20
+ - The drawings land under `build/map/` as SVG beside their DOT source; GraphViz is needed for this command alone, and its absence is reported plainly.
21
+
22
+ **Site**
23
+
24
+ - The rendered site carries a search index, built beside the pages when the site's root files are written and served with them.
25
+ - `site.search: false` in `package-build.config.yaml` turns it off.
26
+
3
27
  ## 22.4.4
4
28
 
5
29
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -1168,8 +1168,35 @@ builder, so schema and compiler cannot disagree. The hand-written compilers —
1168
1168
  `being`, `macro`, `doc` and the three map types — declare theirs in
1169
1169
  `sohl/note-schemas.mjs`.
1170
1170
 
1171
+ **A place's `borders` and `routes` are checked from both ends.** A `place`
1172
+ note states what it is next to and what it is reachable from as two `data:`
1173
+ lists — `borders: [{ to, bearing }]` and
1174
+ `routes: [{ to, bearing, mode, days, terrain?, leagues? }]` — and every value
1175
+ is from a closed set: the eight bearings, `land | boat | ship`, a days scale of
1176
+ `1 2 3 5 10 20 30 45 60 90 180 360`, and a terrain registry in which each
1177
+ terrain names the modes that cross it. Every `to` must be the shortcode of a
1178
+ place, here or in a fetched index. The other end must state the pair back —
1179
+ at the opposite bearing, and for a route by the same mode in the same days.
1180
+ A neighbour stating nothing is a warning naming both notes; one stating a
1181
+ different bearing, mode or days is an error, as is a border to the note's own
1182
+ parent or child, or a pair listed twice by one mode. Each finding lands on the
1183
+ entry that states it. The keys, the sets and the days scale are specified under
1184
+ `type: place` in `docs/content-format.md`.
1185
+
1171
1186
  Nothing here writes. A check reports and an author fixes.
1172
1187
 
1188
+ **`content-build map` draws what those keys state.** `--tree` is the author's
1189
+ check of `parents` — the containment tree, clustered by continent, with a
1190
+ place no parent reaches, a parent no place declares and a cycle flagged red
1191
+ and reported as findings. `--from <shortcode>` is the reader's map: the place
1192
+ at the centre, north up, each neighbour at the angle of its bearing and on a
1193
+ log-spaced ring of its days, a second hop drawn dimmer where the two hops
1194
+ agree; `--from all` draws one for every place with a relation. `--travel` is
1195
+ the whole route graph. Every drawing reads the content index, so a
1196
+ dependency's places take part, and lands under `build/map/` as a `.dot`
1197
+ beside its `.svg`; GraphViz draws, and is needed by this command alone. The
1198
+ command and its rings are documented in `docs/commands.md`.
1199
+
1173
1200
  ### The `data:` container is closed; the top level is not
1174
1201
 
1175
1202
  A note's frontmatter has three regions, and only one of them is open:
@@ -1409,7 +1436,9 @@ mount's `_index.md` as the `home` kind at `baseURL`, which is
1409
1436
  since Cloudflare Pages would apply it. Its `_headers` suppress indexing on
1410
1437
  every host-assigned address and nothing else: no `Cache-Control` is pinned on
1411
1438
  the prefix root, because a lifetime on the homepage would hold a stale copy at
1412
- the most-linked address after a deploy.
1439
+ the most-linked address after a deploy. The same command then indexes the
1440
+ rendered pages for search into `<package>/pagefind/`, served with the rest of
1441
+ the site; `site.search: false` skips it.
1413
1442
 
1414
1443
  ### The homepage's own links
1415
1444
 
package/README.md CHANGED
@@ -335,6 +335,7 @@ npx package-build lang coverage [--unused]
335
335
  npx package-build lang hardcoded
336
336
  npx package-build bundle check
337
337
  npx package-build release [--no-pdf]
338
+ npx package-build site-root [--out <dir>]
338
339
  npx package-build deploy <stage>
339
340
  npx package-build container <stage> <start|stop|restart|recreate|rm|status|logs|pull>
340
341
  npx package-build e2e <seed|run|open|fast|sweep>
@@ -345,6 +346,14 @@ npx package-build e2e <seed|run|open|fast|sweep>
345
346
  publishes a content tree — the book that tree compiles to, as
346
347
  `<artifact>-<version>.pdf`.
347
348
 
349
+ `package-build site-root` finishes a site build. Hugo renders into
350
+ `build/site/<contentPackage>/`; this writes the deployment's `_headers` beside
351
+ that, and indexes the rendered pages for search with Pagefind into
352
+ `build/site/<contentPackage>/pagefind/`, deployed with the pages and served at
353
+ `/<contentPackage>/pagefind/`. The dependency is this package's, declared once,
354
+ so every site — and every local build — gets an index without the repository
355
+ adding one; `site.search: false` in `package-build.config.yaml` turns it off.
356
+
348
357
  The book is **built by default and never fatal**. A package publishing only a
349
358
  homepage, one with no `pdf:` block, and one with no content tree each build
350
359
  none and say so; `--no-pdf` is for a release that has a tree and does not want
@@ -47,6 +47,7 @@
47
47
  * npx content-build markdown [paths..] [--fix]
48
48
  * npx content-build manifest [root] [--out <dir>]
49
49
  * npx content-build site [--out <dir>]
50
+ * npx content-build map [--tree] [--from <shortcode>] [--chart <shortcode>] [--travel] [--out <dir>]
50
51
  * npx content-build reachability <dir> [file] [--index <shortcode>]
51
52
  * npx content-build addresses diff --from <zip|dir> [--strict]
52
53
  *
@@ -144,6 +145,10 @@ import {
144
145
  addressFindingMessage,
145
146
  } from "../engine/address-diff.mjs";
146
147
  import { emittedArtFor, itemPackJsonDirs } from "../engine/generate.mjs";
148
+ import { buildMaps, FROM_ALL, MAP_DIR } from "../engine/map-build.mjs";
149
+ import { CHART_HORIZON_DAYS } from "../engine/map-layout.mjs";
150
+ import { GRAPHVIZ_ENGINES } from "../engine/map-graphviz.mjs";
151
+ import { loadMapWorld } from "../engine/map-places.mjs";
147
152
 
148
153
  /**
149
154
  * The packs `unpack` extracts.
@@ -250,6 +255,7 @@ const argv = yargs(hideBin(process.argv))
250
255
  .command(contentIndexCommand())
251
256
  .command(siteCommand())
252
257
  .command(pdfCommand())
258
+ .command(mapCommand())
253
259
  .command(reachabilityCommand())
254
260
  .command(addressesCommand())
255
261
  .version(ownVersion())
@@ -1663,6 +1669,163 @@ function siteCommand() {
1663
1669
  };
1664
1670
  }
1665
1671
 
1672
+ /**
1673
+ * `content-build map` — draw what the place notes state.
1674
+ *
1675
+ * Reads the content index — this package's and every declared dependency's —
1676
+ * so a consumer's map draws its dependencies' places beside its own, and
1677
+ * writes under `build/map/`, always the `.dot` beside the `.svg`. Four
1678
+ * drawings: the containment tree from `parents` (`--tree`, the author's
1679
+ * check, whose anomalies are reported as findings), the map from a place
1680
+ * from its `borders` and `routes` (`--from`), the chart from a place — the
1681
+ * same drawing at a larger horizon (`--chart`, `--horizon`) — and the whole
1682
+ * route graph (`--travel`). GraphViz draws; when it is absent the command
1683
+ * says what to install and stops.
1684
+ *
1685
+ * @returns {object} The yargs command module.
1686
+ */
1687
+ // eslint-disable-next-line
1688
+ function mapCommand() {
1689
+ return {
1690
+ command: "map",
1691
+ describe:
1692
+ "Draw the containment tree, the map or the chart from a place, or the route graph",
1693
+ builder: (yargs) => {
1694
+ yargs
1695
+ .option("tree", {
1696
+ describe:
1697
+ "Draw the containment tree from `parents`: `tree.svg`, and one " +
1698
+ "`tree-<shortcode>.svg` per continent. Reports the anomalies.",
1699
+ type: "boolean",
1700
+ default: false,
1701
+ })
1702
+ .option("root", {
1703
+ describe:
1704
+ "With --tree, draw the subtree beneath this place as " +
1705
+ "`tree-<shortcode>.svg` and report nothing.",
1706
+ type: "string",
1707
+ })
1708
+ .option("from", {
1709
+ describe:
1710
+ "Draw the map from this place as `from-<shortcode>.svg`; repeatable, " +
1711
+ `or \`${FROM_ALL}\` for every place with a border or a route.`,
1712
+ type: "string",
1713
+ array: true,
1714
+ })
1715
+ .option("chart", {
1716
+ describe:
1717
+ "Draw the chart from this place as `chart-<shortcode>.svg` — the map " +
1718
+ "from it at a larger horizon, the hops chaining on; repeatable, " +
1719
+ `or \`${FROM_ALL}\` for every place with a border or a route.`,
1720
+ type: "string",
1721
+ array: true,
1722
+ })
1723
+ .option("horizon", {
1724
+ describe:
1725
+ "The charts' horizon in days, a marker of the scale: rings are " +
1726
+ "drawn to it, and a place farther than it is left out.",
1727
+ type: "number",
1728
+ default: CHART_HORIZON_DAYS,
1729
+ })
1730
+ .option("travel", {
1731
+ describe: "Draw the whole route graph as `travel.svg`.",
1732
+ type: "boolean",
1733
+ default: false,
1734
+ })
1735
+ .option("engine", {
1736
+ describe: "The tree's layout engine. `twopi` is the radial one.",
1737
+ choices: [...GRAPHVIZ_ENGINES],
1738
+ default: "dot",
1739
+ })
1740
+ .option("rankdir", {
1741
+ describe: "The tree's rank direction; `dot` engine only.",
1742
+ choices: ["TB", "LR", "BT", "RL"],
1743
+ default: "TB",
1744
+ })
1745
+ .option("nodesep", {
1746
+ describe: "The tree's GraphViz `nodesep`, in inches.",
1747
+ type: "number",
1748
+ })
1749
+ .option("ranksep", {
1750
+ describe: "The tree's GraphViz `ranksep`, in inches.",
1751
+ type: "number",
1752
+ })
1753
+ .option("scale", {
1754
+ describe: "Multiply every node's size, font and border weight.",
1755
+ type: "number",
1756
+ default: 1,
1757
+ })
1758
+ // Declared positively so yargs derives `--no-polities` from it.
1759
+ .option("polities", {
1760
+ describe:
1761
+ "Draw polities beside their regions in the tree. `--no-polities` " +
1762
+ "drops them.",
1763
+ type: "boolean",
1764
+ default: true,
1765
+ })
1766
+ .option("base", {
1767
+ describe:
1768
+ "The site base this package's pages are served under, ending in " +
1769
+ "a slash; given, every name is a link to its page.",
1770
+ type: "string",
1771
+ })
1772
+ .option("out", {
1773
+ describe: `Directory to write into. Defaults to \`${MAP_DIR}\`.`,
1774
+ type: "string",
1775
+ });
1776
+ },
1777
+ handler: (argv) => {
1778
+ try {
1779
+ const config = loadPackConfig();
1780
+ const outDir = argv.out ?? path.join(config.rootDir, ...MAP_DIR.split("/"));
1781
+ const problems = [];
1782
+ const world = loadMapWorld({ config, base: argv.base, problems });
1783
+ for (const problem of problems) emitDiagnostic(problem);
1784
+ if (problems.length) process.exitCode = 1;
1785
+ for (const s of world.stale) {
1786
+ emitDiagnostic({
1787
+ file: cachedIndexPath(config, s.package),
1788
+ severity: "error",
1789
+ message: `unusable content index: ${s.reason}`,
1790
+ });
1791
+ }
1792
+ if (world.stale.length) {
1793
+ log.error("Re-run `content-build deps fetch`.");
1794
+ process.exitCode = 1;
1795
+ return;
1796
+ }
1797
+
1798
+ const result = buildMaps({
1799
+ world,
1800
+ outDir,
1801
+ tree: argv.tree,
1802
+ root: argv.root,
1803
+ from: argv.from ?? [],
1804
+ chart: argv.chart ?? [],
1805
+ horizon: argv.horizon,
1806
+ travel: argv.travel,
1807
+ engine: argv.engine,
1808
+ rankdir: argv.rankdir,
1809
+ nodesep: argv.nodesep,
1810
+ ranksep: argv.ranksep,
1811
+ scale: argv.scale,
1812
+ polities: argv.polities !== false,
1813
+ });
1814
+ const errors = reportFindings(result.findings, {});
1815
+ const svgs = result.written.filter((f) => f.endsWith(".svg"));
1816
+ log.info(
1817
+ `${world.places.size} place(s) → ${svgs.length} drawing(s) under ` +
1818
+ `${path.relative(process.cwd(), outDir) || "."}`,
1819
+ );
1820
+ if (errors) process.exitCode = 1;
1821
+ } catch (err) {
1822
+ reportFailure(err);
1823
+ process.exitCode = 1;
1824
+ }
1825
+ },
1826
+ };
1827
+ }
1828
+
1666
1829
  /**
1667
1830
  * `content-build reachability <dir> [file]` — check that a corpus reads through.
1668
1831
  *
@@ -456,19 +456,21 @@ function manifestCommand() {
456
456
  }
457
457
 
458
458
  /**
459
- * `package-build site-root` — the deployment's root files.
459
+ * `package-build site-root` — the deployment's root files, and the search
460
+ * index.
460
461
  *
461
462
  * Hugo owns everything under the `/<package>/` prefix; this owns what sits
462
463
  * beside it, which is what Cloudflare Pages reads from the uploaded directory
463
464
  * and nowhere else: `_headers`, and no `_redirects`, since the prefix root is
464
- * the homepage.
465
+ * the homepage. It then indexes the rendered pages for search into
466
+ * `<package>/pagefind/`, unless `site.search` is off.
465
467
  *
466
468
  * @returns {object} The yargs command.
467
469
  */
468
470
  function siteRootCommand() {
469
471
  return {
470
472
  command: "site-root",
471
- describe: "Write the deployment's _headers",
473
+ describe: "Write the deployment's _headers and index the site for search",
472
474
  builder: (y) =>
473
475
  y.option("out", {
474
476
  type: "string",
@@ -479,10 +481,22 @@ function siteRootCommand() {
479
481
  const shared = loadPackConfig();
480
482
  const out = path.resolve(config.rootDir, argv.out ?? DEPLOY_ROOT);
481
483
 
482
- const { files } = writeSiteRoot({ pkg: shared.contentPackage, out });
484
+ const { files, search } = await writeSiteRoot({
485
+ pkg: shared.contentPackage,
486
+ out,
487
+ search: shared.site.search,
488
+ });
483
489
  for (const file of files) {
484
490
  console.log(`✅ Wrote ${path.relative(config.rootDir, file)}.`);
485
491
  }
492
+ if (search) {
493
+ console.log(
494
+ `✅ Indexed ${search.pages} pages for search into ` +
495
+ `${path.relative(config.rootDir, search.dir)}/.`,
496
+ );
497
+ } else {
498
+ console.log("Search is off (`site.search: false`), so no index was written.");
499
+ }
486
500
  }),
487
501
  };
488
502
  }
@@ -771,6 +771,7 @@ const SITE_KEYS = [
771
771
  "passOptions",
772
772
  "notfound",
773
773
  "hugo",
774
+ "search",
774
775
  ];
775
776
  const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
776
777
  const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
@@ -1710,6 +1711,10 @@ const SITE_INDEX_MESSAGE =
1710
1711
  * shared with the link manifest so the two cannot disagree about where a
1711
1712
  * page is.
1712
1713
  *
1714
+ * `search` is the one switch here: whether `package-build site-root` writes a
1715
+ * search index beside the rendered pages. On by default — every site carries
1716
+ * one unless it says otherwise.
1717
+ *
1713
1718
  * **What the site publishes is the homepage and the content tree, and nothing
1714
1719
  * beside them.** A page of documentation is a note — `type: doc`, addressed
1715
1720
  * by its shortcode, compiling into no document where it says `pack: none` —
@@ -1733,6 +1738,7 @@ function normalizeSite(value) {
1733
1738
  passOptions: Object.freeze({}),
1734
1739
  notfound: null,
1735
1740
  hugo: Object.freeze({}),
1741
+ search: true,
1736
1742
  });
1737
1743
  if (value === undefined) return empty;
1738
1744
  if (!isPlainObject(value)) fail("site", "must be a mapping");
@@ -1788,6 +1794,7 @@ function normalizeSite(value) {
1788
1794
  : Object.freeze({ ...input.passOptions }),
1789
1795
  notfound: normalizeSiteNotfound(input.notfound),
1790
1796
  hugo: normalizeSiteHugo(input.hugo),
1797
+ search: optionalBoolean(input.search, "site.search", true),
1791
1798
  });
1792
1799
  }
1793
1800
 
package/docs/api.md CHANGED
@@ -412,6 +412,94 @@ Drawing a declared infobox: HTML for a Foundry Journal Page, Typst for the book.
412
412
  | `linkToUuid` | `linkToUuid(value)` | `string` | drawing a link value as a Foundry document reference |
413
413
  | `sectionHasContent` | `sectionHasContent(section)` | `boolean` | deciding whether a section holds anything worth drawing |
414
414
 
415
+ ### `engine.placeRelations`
416
+
417
+ What a place is next to (`data.borders`) and reachable from (`data.routes`): the closed sets both draw from, stated once for the lint, the specification and a consumer drawing a map, and the checks that hold a note to them and to the note at the other end.
418
+
419
+ | Export | Signature | Returns | Use it when |
420
+ | ----------------- | ------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
421
+ | `BEARINGS` | `const BEARINGS` | `readonly string[]` | the eight compass bearings, clockwise from `N` |
422
+ | `ROUTE_MODES` | `const ROUTE_MODES` | `readonly string[]` | the modes a route is travelled by — `land`, `boat`, `ship` |
423
+ | `TRAVEL_DAYS` | `const TRAVEL_DAYS` | `readonly number[]` | the days markers a route may state, each meaning "about this, under normal conditions" |
424
+ | `TERRAIN_MODES` | `const TERRAIN_MODES` | — | the terrain registry, each terrain naming the modes that cross it |
425
+ | `TERRAINS` | `const TERRAINS` | `readonly string[]` | the terrain names, in registry order |
426
+ | `BORDER_KEYS` | `const BORDER_KEYS` | `readonly string[]` | the keys a `borders` entry carries |
427
+ | `ROUTE_KEYS` | `const ROUTE_KEYS` | `readonly string[]` | the keys a `routes` entry carries |
428
+ | `oppositeBearing` | `oppositeBearing(bearing)` | `string\|undefined` | the bearing the other end of a relation states, or `undefined` for a value that is not a bearing |
429
+ | `checkBorders` | `checkBorders(note, { index })` | `object[]` | linting a place's `borders` — targets, values, once per pair, no parent or child, and reciprocity with the other end |
430
+ | `checkRoutes` | `checkRoutes(note, { index })` | `object[]` | linting a place's `routes` — as `checkBorders`, plus `mode`, `days` and `terrain`, once per pair per mode |
431
+
432
+ ### `engine.mapPlaces`
433
+
434
+ The places a map is drawn from — this package's index records and every fetched dependency's entries, read through one shape — and what their `parents` say: the world, the continents, each place's continent, and everything the containment tree can get wrong, reported as findings located at the entry that states it.
435
+
436
+ | Export | Signature | Returns | Use it when |
437
+ | --------------------- | ---------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
438
+ | `placesFromRecords` | `placesFromRecords(records, { contentBase, base })` | `MapWorld` | reading this package's places and polities out of its index records, with a page URL each where a site base is given |
439
+ | `loadMapWorld` | `loadMapWorld({ config, contentBase, base, problems })` | `MapWorld` | loading the world a map is drawn from — this package's index and every declared dependency's |
440
+ | `analyzeContainment` | `analyzeContainment(places, polities)` | `Containment` | finding the world, the continents, each place's continent and the anomalies of `parents` |
441
+ | `subtreeOf` | `subtreeOf(analysis, root)` | `Set<string>` | every place beneath one, by following `parents` downward |
442
+ | `containmentFindings` | `containmentFindings(analysis, places, { contentBase, config })` | `object[]` | the anomalies as `file:line:column: severity: message` findings — no parent, an unresolved parent and a cycle are errors |
443
+
444
+ ### `engine.mapLayout`
445
+
446
+ The geometry of the map from a place and of the chart from one, as arithmetic over the place records: north up, east right, the angle from the bearing and the radius from the days marker on log-spaced rings, hops composed into one bearing as long as none turns back. Nothing here touches a file or GraphViz.
447
+
448
+ | Export | Signature | Returns | Use it when |
449
+ | -------------------- | ------------------------------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
450
+ | `HORIZON_DAYS` | `const HORIZON_DAYS` | `number` | the horizon of the map from a place, 90 days; beyond it a place is a name at the rim |
451
+ | `CHART_HORIZON_DAYS` | `const CHART_HORIZON_DAYS` | `number` | the horizon of the chart from a place when none is asked for, 360 days — the end of the scale |
452
+ | `RING_DAYS` | `const RING_DAYS` | `readonly number[]` | the days markers that are rings on the map from a place — every marker up to its horizon |
453
+ | `bearingAngle` | `bearingAngle(bearing)` | `number\|undefined` | the angle of a bearing in degrees counter-clockwise from east — `N` is 90, `E` 0 |
454
+ | `bearingVector` | `bearingVector(bearing)` | `{x, y}\|undefined` | the unit vector of a bearing |
455
+ | `bearingSteps` | `bearingSteps(a, b)` | `number\|undefined` | how many steps around the compass separate two bearings, the shorter way |
456
+ | `rimRadius` | `rimRadius(horizon?)` | `number` | the radius of the rim, one log step beyond the horizon — the map from a place's by default |
457
+ | `ringRadius` | `ringRadius(days, horizon?)` | `number` | the radius of the ring a days value sits on — the rim for none, or for a value beyond the horizon |
458
+ | `rings` | `rings(horizon?)` | `object[]` | the rings inside out, each marker up to the horizon with its radius |
459
+ | `daysMarker` | `daysMarker(total)` | `number\|undefined` | the smallest marker of the scale that covers a total of days |
460
+ | `edgeLength` | `edgeLength(days)` | `number` | the length a route asks of `neato` in the route graph, inches, on the rings' log scale |
461
+ | `composeChain` | `composeChain(hops)` | `object\|undefined` | a chain of hops composed into one bearing and one total, each hop within an octant of the running direction, else `undefined` |
462
+ | `composeHops` | `composeHops(first, second)` | `object\|undefined` | two hops composed into one bearing and one total where they agree — the same bearing or adjacent — else `undefined` |
463
+ | `relationsOf` | `relationsOf(places, shortcode)` | `object` | every border and route touching one place, read from both ends, plus the targets that resolved to nothing |
464
+ | `layoutFrom` | `layoutFrom(places, centre)` | `object` | laying out the map from a place: every node pinned in points, every edge, the rings and the rim |
465
+ | `layoutChart` | `layoutChart(places, centre, { horizon })` | `object` | laying out the chart from a place: the map from it at a larger horizon, the hops chaining on as long as none turns back |
466
+ | `travelGraph` | `travelGraph(places)` | `object` | the whole route graph — one edge per pair, however many ends and modes state it, each with a length from its days |
467
+
468
+ ### `engine.mapDot`
469
+
470
+ The drawings of `content-build map` as DOT text, sharing one style table: shape, colour and size by `subType`, an anomaly overriding colour alone, every label a `name.full`, every tooltip an address, and a `URL` where the place carries one so the SVG's names are links.
471
+
472
+ | Export | Signature | Returns | Use it when |
473
+ | ------------ | -------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
474
+ | `STYLE` | `const STYLE` | — | the shape, colour and size of each `subType`, and of a polity |
475
+ | `dotString` | `dotString(value)` | `string` | quoting text for DOT |
476
+ | `treeDot` | `treeDot({ places, polities, analysis, root, … })` | `string` | the containment tree — the whole world clustered by continent, or the subtree beneath `root` |
477
+ | `hopOpacity` | `hopOpacity(hop)` | `number` | how faint a place is drawn by its hop count — full to the second hop, a step down each hop beyond, floored |
478
+ | `fromDot` | `fromDot(layout, places, { scale, title })` | `string` | the map or the chart from a place, every node pinned for `neato -n2`, the rings drawn faintly with their markers |
479
+ | `travelDot` | `travelDot(graph, places, { scale, title })` | `string` | the whole route graph for `neato`, each edge asking for a length from its days |
480
+
481
+ ### `engine.mapGraphviz`
482
+
483
+ GraphViz, as the map command reaches it: a build-time tool of that command alone, found once, named plainly when absent, and run over a `.dot` file the command keeps beside the rendering.
484
+
485
+ | Export | Signature | Returns | Use it when |
486
+ | ------------------------ | ------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
487
+ | `GRAPHVIZ_ENGINES` | `const GRAPHVIZ_ENGINES` | `readonly string[]` | the engines the map command may ask for — `dot`, `twopi`, `neato` |
488
+ | `findGraphviz` | `findGraphviz(engine, { env, fallbacks })` | `string\|undefined` | finding an engine's binary on `PATH` or in the directories a package manager installs into |
489
+ | `graphvizMissingMessage` | `graphvizMissingMessage(engine)` | `string` | saying an engine is not installed, and what installs it |
490
+ | `renderDot` | `renderDot(dotPath, outPath, { binary, format, args })` | `{warnings}` | rendering a `.dot` file, the engine's warnings returned and a non-zero exit thrown |
491
+
492
+ ### `engine.mapBuild`
493
+
494
+ `content-build map` as a function: the drawings written under an output directory, always the `.dot` beside the `.svg`, the tree's anomalies returned as findings, and a missing GraphViz either an error or — for a build that would rather skip — one warning.
495
+
496
+ | Export | Signature | Returns | Use it when |
497
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------- |
498
+ | `MAP_DIR` | `const MAP_DIR` | `string` | where the maps land below a package's root, `build/map` |
499
+ | `FROM_ALL` | `const FROM_ALL` | `string` | the `--from` or `--chart` value that means every place with a relation, `all` |
500
+ | `relatedPlaces` | `relatedPlaces(places)` | `string[]` | every place that states, or is named in, a border or a route |
501
+ | `buildMaps` | `buildMaps({ world, outDir, tree, root, from, chart, horizon, travel, engine, rankdir, nodesep, ranksep, scale, polities, locate, requireGraphviz })` | `{written, findings, skipped}` | drawing any of the four from a loaded world |
502
+
415
503
  ### `engine.systems`
416
504
 
417
505
  The closed registry of system ids, and the `none` that stands for no system at all. An unknown system value is an error; adding a system is a data change to this registry rather than a hardcoded set scattered through the pipeline.