@heroiclands/package-build 22.4.3 → 22.4.5

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +28 -18
  2. package/CONTENT.md +77 -1
  3. package/README.md +9 -0
  4. package/bin/content-build.mjs +143 -0
  5. package/bin/package-build.mjs +18 -4
  6. package/content-config.mjs +7 -0
  7. package/docs/api.md +84 -0
  8. package/docs/commands.md +182 -2
  9. package/docs/configuration.md +19 -1
  10. package/docs/content-format.md +105 -6
  11. package/docs/diagnostics.md +1 -0
  12. package/docs/project-setup.md +7 -6
  13. package/engine/changelog-group.mjs +27 -6
  14. package/engine/changelog-lint.mjs +27 -7
  15. package/engine/frontmatter-lint.mjs +11 -1
  16. package/engine/homepage.mjs +9 -1
  17. package/engine/index.mjs +18 -0
  18. package/engine/map-build.mjs +240 -0
  19. package/engine/map-dot.mjs +685 -0
  20. package/engine/map-graphviz.mjs +138 -0
  21. package/engine/map-layout.mjs +712 -0
  22. package/engine/map-places.mjs +561 -0
  23. package/engine/metadata-index.mjs +7 -0
  24. package/engine/note-vocabulary.mjs +28 -0
  25. package/engine/place-relations.mjs +637 -0
  26. package/engine/related-pages.mjs +161 -0
  27. package/engine/site-build.mjs +107 -9
  28. package/engine/site-config.mjs +1 -1
  29. package/engine/site-index.mjs +5 -1
  30. package/engine/site-root.mjs +147 -6
  31. package/engine/web-wikilinks.mjs +13 -2
  32. package/package.json +3 -2
  33. package/types/engine/changelog-lint.d.mts +9 -7
  34. package/types/engine/homepage.d.mts +8 -1
  35. package/types/engine/index.d.mts +6 -0
  36. package/types/engine/map-build.d.mts +78 -0
  37. package/types/engine/map-dot.d.mts +130 -0
  38. package/types/engine/map-graphviz.d.mts +61 -0
  39. package/types/engine/map-layout.d.mts +359 -0
  40. package/types/engine/map-places.d.mts +259 -0
  41. package/types/engine/note-vocabulary.d.mts +12 -0
  42. package/types/engine/place-relations.d.mts +95 -0
  43. package/types/engine/related-pages.d.mts +70 -0
  44. package/types/engine/site-build.d.mts +36 -3
  45. package/types/engine/site-config.d.mts +1 -1
  46. package/types/engine/site-index.d.mts +5 -1
  47. package/types/engine/site-root.d.mts +59 -7
  48. package/types/engine/web-wikilinks.d.mts +11 -2
package/CHANGELOG.md CHANGED
@@ -1,45 +1,55 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.4.5
4
+
5
+ ### Patch Changes
6
+
7
+ 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.
8
+
9
+ **Maps**
10
+
11
+ - 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.
12
+ - `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.
13
+ - `--from all` draws one map for every place with a border or a route, and `--travel` draws the whole route graph.
14
+ - 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.
15
+
16
+ **Site**
17
+
18
+ - The rendered site carries a search index, built beside the pages when the site's root files are written and served with them.
19
+ - `site.search: false` in `package-build.config.yaml` turns it off.
20
+
21
+ ## 22.4.4
22
+
23
+ ### Patch Changes
24
+
25
+ Every page a site build publishes carries a `related` block in its front matter — the pages that link to it and the pages it links to, each named once with its title, address and type — which the theme renders as the Related card at the foot of the page. A page nothing connects to carries no block, and the homepage is counted on both sides like any other page.
26
+
27
+ `changelog group` keeps a hard-wrapped paragraph as one block instead of fragmenting it one block per line, and no longer inserts a blank line under a bold sentence a paragraph continues directly beneath.
28
+
3
29
  ## 22.4.3
4
30
 
5
31
  ### Patch Changes
6
32
 
33
+ **A new `changelog group` command folds a release's changeset blocks together by their bold label.**
7
34
  `@heroiclands/package-build/changelog` writes each changeset's summary into `CHANGELOG.md` as its
8
-
9
35
  own block, so three pull requests each touching compendium content leave three separate
10
-
11
36
  `**Compendiums**` blocks instead of one. `changelog group` rewrites the newest release section in
12
-
13
37
  place: every block sharing one label is merged into a single block, bullets kept in their original
14
-
15
38
  order with an exact duplicate kept once, and the merged blocks are ordered by a new
16
-
17
39
  `changelog.labels` configuration key — the unlabelled lead paragraph first, then every declared
18
-
19
40
  label in order, then any undeclared label last with a warning naming it. `changelog check` warns on
20
-
21
41
  a label absent from a declared `changelog.labels` too, so a drifted spelling is caught before a
22
-
23
42
  release ships it.
24
43
 
44
+ **A shipped changelog generator writes a release note without a commit hash.**
25
45
  `@heroiclands/package-build/changelog` is a Changesets changelog generator
26
-
27
46
  whose `getReleaseLine` renders a changeset's summary as a block, verbatim —
28
-
29
47
  no bullet, no indentation, separated from its neighbours by one blank line —
30
-
31
48
  and whose `getDependencyReleaseLine` writes nothing. Point
32
-
33
49
  `.changeset/config.json` at it
34
-
35
50
  (`"changelog": "@heroiclands/package-build/changelog"`) to stop every release
36
-
37
51
  note from carrying a commit-hash prefix.
38
52
 
39
- **A new `changelog group` command folds a release's changeset blocks together by their bold label.**
40
-
41
- **A shipped changelog generator writes a release note without a commit hash.**
42
-
43
53
  ## 22.4.2
44
54
 
45
55
  ### 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
 
@@ -2333,6 +2362,53 @@ notes it lists, and link it from the homepage. Nothing is generated between
2333
2362
  the homepage and the pages, so delete the key.
2334
2363
  ````
2335
2364
 
2365
+ ### Every page carries what links to it, and what it links to
2366
+
2367
+ The build resolves every wikilink on every page through the address index, so
2368
+ at the moment it writes a page it holds the whole link graph. It writes the
2369
+ part that concerns each page into that page's front matter:
2370
+
2371
+ ```yaml
2372
+ related:
2373
+ backlinks: # pages that link to this one
2374
+ - title: Afzandah Parnâzar
2375
+ url: /thalorna/being-afzndhprnzr/
2376
+ type: being
2377
+ mentions: # pages this one links to
2378
+ - title: Kethramír
2379
+ url: /thalorna/place-kthrmr/
2380
+ type: place
2381
+ ```
2382
+
2383
+ The theme's `related.html` partial reads both lists and renders them as one
2384
+ "Related" card, grouped by `type`. What the build guarantees about them:
2385
+
2386
+ - **A page appears once per list**, however many times it is linked, and
2387
+ whichever address the link was written to: `docbeing-grod` and `being-grod`
2388
+ are one page, and a page that cites it under both lists it once. A link from
2389
+ a page to itself is not a connection and is dropped.
2390
+ - **Each list is sorted by `type`, then `title`**, so the card's grouping is
2391
+ the same on every build whatever order the notes were walked in.
2392
+ - **Both lists are present whenever either has an entry**, so a theme reads one
2393
+ shape; a page with no links either way carries **no `related` key at all**,
2394
+ which is the partial's silent-disappear convention kept at the source.
2395
+ - **`url` composes `<base><slug>/`**, like every other href this build renders
2396
+ — see [A page's URL is its address](#a-pages-url-is-its-address). The page
2397
+ states its own address as `/<slug>/`; everything pointing _at_ it, this
2398
+ included, carries the base.
2399
+ - **The homepage is a page like any other on both sides.** A content page
2400
+ reaches it through `[[homepage-root|Text]]` and is listed in its backlinks;
2401
+ its own body links are ordinary markdown, resolved against the package root
2402
+ the way a browser resolves them, so `being-grod/`, `/thalorna/being-grod/`
2403
+ and `./being-grod/#top` each count as a mention of that page. A link that
2404
+ names a host leaves the site and counts as nothing.
2405
+ - **Only a page of this site is an endpoint.** A link into another package
2406
+ resolves to an href the build renders but no page it writes, so it is listed
2407
+ on neither side; a backlink can be counted only on a page this build emits.
2408
+
2409
+ `related` is derived, never authored: what links to a page is a fact about
2410
+ every other page in the tree. A note that writes one has it replaced.
2411
+
2336
2412
  ### Why the output location is fixed
2337
2413
 
2338
2414
  The content mount is a build artifact and is **deleted on every run**, so that a
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>] [--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,9 @@ 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 { GRAPHVIZ_ENGINES } from "../engine/map-graphviz.mjs";
150
+ import { loadMapWorld } from "../engine/map-places.mjs";
147
151
 
148
152
  /**
149
153
  * The packs `unpack` extracts.
@@ -250,6 +254,7 @@ const argv = yargs(hideBin(process.argv))
250
254
  .command(contentIndexCommand())
251
255
  .command(siteCommand())
252
256
  .command(pdfCommand())
257
+ .command(mapCommand())
253
258
  .command(reachabilityCommand())
254
259
  .command(addressesCommand())
255
260
  .version(ownVersion())
@@ -1663,6 +1668,144 @@ function siteCommand() {
1663
1668
  };
1664
1669
  }
1665
1670
 
1671
+ /**
1672
+ * `content-build map` — draw what the place notes state.
1673
+ *
1674
+ * Reads the content index — this package's and every declared dependency's —
1675
+ * so a consumer's map draws its dependencies' places beside its own, and
1676
+ * writes under `build/map/`, always the `.dot` beside the `.svg`. Three
1677
+ * drawings: the containment tree from `parents` (`--tree`, the author's
1678
+ * check, whose anomalies are reported as findings), the map from a place
1679
+ * from its `borders` and `routes` (`--from`), and the whole route graph
1680
+ * (`--travel`). GraphViz draws; when it is absent the command says what to
1681
+ * install and stops.
1682
+ *
1683
+ * @returns {object} The yargs command module.
1684
+ */
1685
+ // eslint-disable-next-line
1686
+ function mapCommand() {
1687
+ return {
1688
+ command: "map",
1689
+ describe: "Draw the containment tree, the map from a place, or the route graph",
1690
+ builder: (yargs) => {
1691
+ yargs
1692
+ .option("tree", {
1693
+ describe:
1694
+ "Draw the containment tree from `parents`: `tree.svg`, and one " +
1695
+ "`tree-<shortcode>.svg` per continent. Reports the anomalies.",
1696
+ type: "boolean",
1697
+ default: false,
1698
+ })
1699
+ .option("root", {
1700
+ describe:
1701
+ "With --tree, draw the subtree beneath this place as " +
1702
+ "`tree-<shortcode>.svg` and report nothing.",
1703
+ type: "string",
1704
+ })
1705
+ .option("from", {
1706
+ describe:
1707
+ "Draw the map from this place as `from-<shortcode>.svg`; repeatable, " +
1708
+ `or \`${FROM_ALL}\` for every place with a border or a route.`,
1709
+ type: "string",
1710
+ array: true,
1711
+ })
1712
+ .option("travel", {
1713
+ describe: "Draw the whole route graph as `travel.svg`.",
1714
+ type: "boolean",
1715
+ default: false,
1716
+ })
1717
+ .option("engine", {
1718
+ describe: "The tree's layout engine. `twopi` is the radial one.",
1719
+ choices: [...GRAPHVIZ_ENGINES],
1720
+ default: "dot",
1721
+ })
1722
+ .option("rankdir", {
1723
+ describe: "The tree's rank direction; `dot` engine only.",
1724
+ choices: ["TB", "LR", "BT", "RL"],
1725
+ default: "TB",
1726
+ })
1727
+ .option("nodesep", {
1728
+ describe: "The tree's GraphViz `nodesep`, in inches.",
1729
+ type: "number",
1730
+ })
1731
+ .option("ranksep", {
1732
+ describe: "The tree's GraphViz `ranksep`, in inches.",
1733
+ type: "number",
1734
+ })
1735
+ .option("scale", {
1736
+ describe: "Multiply every node's size, font and border weight.",
1737
+ type: "number",
1738
+ default: 1,
1739
+ })
1740
+ // Declared positively so yargs derives `--no-polities` from it.
1741
+ .option("polities", {
1742
+ describe:
1743
+ "Draw polities beside their regions in the tree. `--no-polities` " +
1744
+ "drops them.",
1745
+ type: "boolean",
1746
+ default: true,
1747
+ })
1748
+ .option("base", {
1749
+ describe:
1750
+ "The site base this package's pages are served under, ending in " +
1751
+ "a slash; given, every name is a link to its page.",
1752
+ type: "string",
1753
+ })
1754
+ .option("out", {
1755
+ describe: `Directory to write into. Defaults to \`${MAP_DIR}\`.`,
1756
+ type: "string",
1757
+ });
1758
+ },
1759
+ handler: (argv) => {
1760
+ try {
1761
+ const config = loadPackConfig();
1762
+ const outDir = argv.out ?? path.join(config.rootDir, ...MAP_DIR.split("/"));
1763
+ const problems = [];
1764
+ const world = loadMapWorld({ config, base: argv.base, problems });
1765
+ for (const problem of problems) emitDiagnostic(problem);
1766
+ if (problems.length) process.exitCode = 1;
1767
+ for (const s of world.stale) {
1768
+ emitDiagnostic({
1769
+ file: cachedIndexPath(config, s.package),
1770
+ severity: "error",
1771
+ message: `unusable content index: ${s.reason}`,
1772
+ });
1773
+ }
1774
+ if (world.stale.length) {
1775
+ log.error("Re-run `content-build deps fetch`.");
1776
+ process.exitCode = 1;
1777
+ return;
1778
+ }
1779
+
1780
+ const result = buildMaps({
1781
+ world,
1782
+ outDir,
1783
+ tree: argv.tree,
1784
+ root: argv.root,
1785
+ from: argv.from ?? [],
1786
+ travel: argv.travel,
1787
+ engine: argv.engine,
1788
+ rankdir: argv.rankdir,
1789
+ nodesep: argv.nodesep,
1790
+ ranksep: argv.ranksep,
1791
+ scale: argv.scale,
1792
+ polities: argv.polities !== false,
1793
+ });
1794
+ const errors = reportFindings(result.findings, {});
1795
+ const svgs = result.written.filter((f) => f.endsWith(".svg"));
1796
+ log.info(
1797
+ `${world.places.size} place(s) → ${svgs.length} drawing(s) under ` +
1798
+ `${path.relative(process.cwd(), outDir) || "."}`,
1799
+ );
1800
+ if (errors) process.exitCode = 1;
1801
+ } catch (err) {
1802
+ reportFailure(err);
1803
+ process.exitCode = 1;
1804
+ }
1805
+ },
1806
+ };
1807
+ }
1808
+
1666
1809
  /**
1667
1810
  * `content-build reachability <dir> [file]` — check that a corpus reads through.
1668
1811
  *
@@ -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,90 @@ 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, 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, two hops composed into one bearing only where they agree. 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
+ | `RING_DAYS` | `const RING_DAYS` | `readonly number[]` | the days markers that are rings — every marker up to the horizon |
452
+ | `bearingAngle` | `bearingAngle(bearing)` | `number\|undefined` | the angle of a bearing in degrees counter-clockwise from east — `N` is 90, `E` 0 |
453
+ | `bearingVector` | `bearingVector(bearing)` | `{x, y}\|undefined` | the unit vector of a bearing |
454
+ | `bearingSteps` | `bearingSteps(a, b)` | `number\|undefined` | how many steps around the compass separate two bearings, the shorter way |
455
+ | `rimRadius` | `rimRadius()` | `number` | the radius of the rim, one log step beyond the horizon |
456
+ | `ringRadius` | `ringRadius(days)` | `number` | the radius of the ring a days value sits on — the rim for none, or for a value beyond the horizon |
457
+ | `rings` | `rings()` | `object[]` | the rings inside out, each marker with its radius |
458
+ | `daysMarker` | `daysMarker(total)` | `number\|undefined` | the smallest marker of the scale that covers a total of days |
459
+ | `edgeLength` | `edgeLength(days)` | `number` | the length a route asks of `neato` in the route graph, inches, on the rings' log scale |
460
+ | `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` |
461
+ | `relationsOf` | `relationsOf(places, shortcode)` | `object` | every border and route touching one place, read from both ends, plus the targets that resolved to nothing |
462
+ | `layoutFrom` | `layoutFrom(places, centre)` | `object` | laying out the map from a place: every node pinned in points, every edge, the rings and the rim |
463
+ | `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 |
464
+
465
+ ### `engine.mapDot`
466
+
467
+ The three 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.
468
+
469
+ | Export | Signature | Returns | Use it when |
470
+ | ----------- | -------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
471
+ | `STYLE` | `const STYLE` | — | the shape, colour and size of each `subType`, and of a polity |
472
+ | `dotString` | `dotString(value)` | `string` | quoting text for DOT |
473
+ | `treeDot` | `treeDot({ places, polities, analysis, root, … })` | `string` | the containment tree — the whole world clustered by continent, or the subtree beneath `root` |
474
+ | `fromDot` | `fromDot(layout, places, { scale, title })` | `string` | the map from a place, every node pinned for `neato -n2`, the rings drawn faintly with their markers |
475
+ | `travelDot` | `travelDot(graph, places, { scale, title })` | `string` | the whole route graph for `neato`, each edge asking for a length from its days |
476
+
477
+ ### `engine.mapGraphviz`
478
+
479
+ 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.
480
+
481
+ | Export | Signature | Returns | Use it when |
482
+ | ------------------------ | ------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
483
+ | `GRAPHVIZ_ENGINES` | `const GRAPHVIZ_ENGINES` | `readonly string[]` | the engines the map command may ask for — `dot`, `twopi`, `neato` |
484
+ | `findGraphviz` | `findGraphviz(engine, { env, fallbacks })` | `string\|undefined` | finding an engine's binary on `PATH` or in the directories a package manager installs into |
485
+ | `graphvizMissingMessage` | `graphvizMissingMessage(engine)` | `string` | saying an engine is not installed, and what installs it |
486
+ | `renderDot` | `renderDot(dotPath, outPath, { binary, format, args })` | `{warnings}` | rendering a `.dot` file, the engine's warnings returned and a non-zero exit thrown |
487
+
488
+ ### `engine.mapBuild`
489
+
490
+ `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.
491
+
492
+ | Export | Signature | Returns | Use it when |
493
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------------------- |
494
+ | `MAP_DIR` | `const MAP_DIR` | `string` | where the maps land below a package's root, `build/map` |
495
+ | `FROM_ALL` | `const FROM_ALL` | `string` | the `--from` value that means every place with a relation, `all` |
496
+ | `relatedPlaces` | `relatedPlaces(places)` | `string[]` | every place that states, or is named in, a border or a route |
497
+ | `buildMaps` | `buildMaps({ world, outDir, tree, root, from, travel, engine, rankdir, nodesep, ranksep, scale, polities, locate, requireGraphviz })` | `{written, findings, skipped}` | drawing any of the three from a loaded world |
498
+
415
499
  ### `engine.systems`
416
500
 
417
501
  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.