@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/docs/commands.md CHANGED
@@ -261,7 +261,8 @@ package-build: `packageKind: documentation` ships no Foundry package, so there i
261
261
 
262
262
  **NAME**
263
263
 
264
- `package-build site-root` — write the deployment's `_headers`.
264
+ `package-build site-root` — write the deployment's `_headers`, and index the
265
+ site for search.
265
266
 
266
267
  **SYNOPSIS**
267
268
 
@@ -294,6 +295,29 @@ it.
294
295
  The rules are scoped to those hostnames, so a site deployed under a domain of
295
296
  its own stays indexable.
296
297
 
298
+ With `_headers` written, the rendered site is indexed for search. Pagefind
299
+ reads every page under `<out>/<contentPackage>/` and writes the index into
300
+ `<out>/<contentPackage>/pagefind/`, so it is deployed with the pages and served
301
+ at `/<contentPackage>/pagefind/`; the theme loads `pagefind.js` from there,
302
+ relative to the site's base URL, and the browser bundle derives the prefix from
303
+ its own address, so nothing about the prefix is configured on either side. What
304
+ is indexed is the body of every page and the homepage; the theme marks its own
305
+ chrome — header, footer, breadcrumbs, infobox, the related-pages card —
306
+ `data-pagefind-ignore`, and carries `data-pagefind-filter` attributes for a
307
+ page's `type` and `package`, so a search can be narrowed to one kind of page.
308
+ This command's part is only to run the indexer over whatever the theme
309
+ rendered.
310
+
311
+ The indexer is the `pagefind` package this toolchain depends on, which installs
312
+ the binary for the platform as an optional dependency; nothing goes on a
313
+ `PATH`. Every failure of the step is an error located at the rendered site —
314
+ `build/site/<contentPackage>: error: search index: …` — and a binary that is
315
+ not installed names the platform package `npm ci` installs.
316
+
317
+ [`site.search: false`](configuration.md#site) skips the step, and removes a
318
+ `pagefind/` an earlier build left beside the pages, so a site that wants no
319
+ search deploys none.
320
+
297
321
  **OPTIONS**
298
322
 
299
323
  `--out <dir>` — the directory that is deployed. Defaults to `build/site`.
@@ -302,13 +326,20 @@ its own stays indexable.
302
326
 
303
327
  1 when `<out>/<contentPackage>/` holds no rendered site, which means the site
304
328
  build has not run and writing root files would publish a deployment with nothing
305
- under the prefix. Otherwise 0.
329
+ under the prefix; 1 when the index cannot be built. Otherwise 0.
306
330
 
307
331
  **EXAMPLES**
308
332
 
309
333
  ```
310
334
  $ package-build site-root
311
335
  ✅ Wrote build/site/_headers.
336
+ ✅ Indexed 412 pages for search into build/site/thalorna/pagefind/.
337
+ ```
338
+
339
+ ```
340
+ $ package-build site-root
341
+ ✅ Wrote build/site/_headers.
342
+ Search is off (`site.search: false`), so no index was written.
312
343
  ```
313
344
 
314
345
  **SEE ALSO**
@@ -1829,6 +1860,155 @@ demo-book: error: the document tree named by `pdf.document` cannot be read
1829
1860
  `content-build site`, `content-build package <action> [pack] [entry]`,
1830
1861
  [Diagnostics](diagnostics.md), [Configuration](configuration.md).
1831
1862
 
1863
+ ### `content-build map`
1864
+
1865
+ **NAME**
1866
+
1867
+ Draw the containment tree, the map from a place, or the whole route graph.
1868
+
1869
+ **SYNOPSIS**
1870
+
1871
+ ```
1872
+ content-build map --tree [--root <shortcode>] [--engine dot|twopi|neato] [--rankdir TB|LR|BT|RL]
1873
+ [--nodesep <inches>] [--ranksep <inches>] [--no-polities]
1874
+ content-build map --from <shortcode>.. | --from all
1875
+ content-build map --travel
1876
+ content-build map [...] [--scale <factor>] [--base <url>] [--out <dir>]
1877
+ ```
1878
+
1879
+ **DESCRIPTION**
1880
+
1881
+ Draws what the place notes state. Every drawing is read from the content
1882
+ index — this package's records and every declared dependency's fetched
1883
+ index — so a consumer's map draws its dependencies' places beside its own
1884
+ and no note is opened except to locate a finding. Everything lands under
1885
+ `build/map/`, always the `.dot` beside the `.svg`, so a drawing can be
1886
+ re-rendered, diffed or read without the index. GraphViz draws: `dot` (or
1887
+ `twopi`, `neato`) for the tree, `neato` for the other two. It is a
1888
+ build-time tool of this command alone; when the engine a run needs is not
1889
+ installed the command says so, names what to install, and stops. A build
1890
+ that asks for maps as one step of a larger job — a site build — writes
1891
+ the `.dot` files, emits one warning and skips the renderings rather than
1892
+ failing.
1893
+
1894
+ The three drawings, any of which may be asked for in one run:
1895
+
1896
+ **`--tree`** — the author's check. The containment tree from `data.parents`,
1897
+ edges running child → parent, clustered by continent — a `region` parented
1898
+ directly on the `world` note — with the places reaching no continent
1899
+ floating outside every cluster. Node shape, colour and size are by
1900
+ `subType` (a gold double circle for the world, a blue box for a region, a
1901
+ small green ellipse for a settlement, a purple diamond for a site, a teal
1902
+ hexagon for a feature, an orange house for a structure), and a legend
1903
+ drawn from the same style table sits on every drawing. A `polity`
1904
+ affiliation is drawn as a grey dashed cylinder beside every region note in
1905
+ its own directory. Anomalies are highlighted and reported as findings: a
1906
+ place with **no parent** and a **placeholder for a parent shortcode no
1907
+ place declares** are filled red (errors); a place on a **cycle** is
1908
+ outlined red (an error); a place with **more than one parent**, a region
1909
+ with **no `packFolder`** and a polity whose directory holds **no region
1910
+ note** are warnings. The world note has no parent by nature and is not a
1911
+ finding. Writes `tree.svg` for the whole world and `tree-<shortcode>.svg`
1912
+ for each continent; with `--root`, the subtree beneath that one place as
1913
+ `tree-<shortcode>.svg` — every place whose `parents` chain reaches it by
1914
+ any path — and no findings.
1915
+
1916
+ **`--from <shortcode>`** — the map from a place, `from-<shortcode>.svg`.
1917
+ Pre-modern maps were itineraries: partial, centred on their maker, exact
1918
+ about the next stage and vague about the tenth. A place's `borders` and
1919
+ `routes` are exactly that, so the reader's map is one map per place, from
1920
+ that place. The place sits at the centre, **north up, east right**. Every
1921
+ neighbour it states — or that states it, since a border or a route is one
1922
+ fact stated from each end — sits at the angle of its bearing and on the
1923
+ ring of its days, with an edge labelled by the bearing and days; a border
1924
+ is a dashed edge, and where several modes reach one place the label lists
1925
+ each. Beyond each neighbour, every place a second hop reaches is drawn
1926
+ dimmer at the composed bearing where the two hops agree — the same bearing
1927
+ or adjacent ones, added as vectors weighted by their days — on the ring of
1928
+ their total; where the hops disagree the place is omitted rather than
1929
+ guessed at. Nodes sharing a ring and a bearing are fanned apart. `all`
1930
+ draws the map from every place that states or is named in a border or a
1931
+ route. Positions are computed by the command and pinned, so `neato` only
1932
+ draws; nothing on the page is a solver's decision.
1933
+
1934
+ _The rings._ One faint dashed circle per marker of the days scale up to
1935
+ the horizon of 90 days — `1 2 3 5 10 20 30 45 60 90` — each labelled at
1936
+ its top, spaced logarithmically so that equal ratios of days are equal
1937
+ steps of radius: a day to two days is the same step as five to ten, and a
1938
+ day's ride and a month's voyage both fit on one page. A neighbour with a
1939
+ route sits on the ring of its shortest route's days. A neighbour reached by
1940
+ a border alone sits on the innermost ring, because a frontier states no
1941
+ distance. One more circle, the rim, lies a log step beyond the horizon:
1942
+ a place farther than 90 days, and a second hop whose days are unknown
1943
+ because either hop was a border, are a name at the rim carrying its days
1944
+ or `?` — beyond what the notes state, the map states nothing more.
1945
+
1946
+ _Two scales._ The generated map from a place is the default regional map:
1947
+ every place gets one at build time, from its `borders` and `routes`. An
1948
+ authored `regionalmap` note is an artefact of the setting — a road book, a
1949
+ sailor's chart, a tax survey, partial and from somewhere, whose prose says
1950
+ whose it is — and never the survey. The two are two sources of the same
1951
+ thing on a place's page.
1952
+
1953
+ **`--travel`** — the whole route graph, `travel.svg`, for the author: one
1954
+ undirected edge per pair of places, a border dashed, each route asking
1955
+ `neato` for a length on the same log scale as the rings so the god's-eye
1956
+ view and the itinerary agree about what a day and a month look like; the
1957
+ engine solves the positions.
1958
+
1959
+ Every node's label is the place's `name.full` and its tooltip the canonical
1960
+ address. With `--base`, every name is also a link — GraphViz writes an
1961
+ `<a href>` around it — to the page at `<base><slug>/` for this package's
1962
+ places and to the URL the fetched index resolved for a dependency's, so a
1963
+ page can inline the SVG. Without it the names are plain text.
1964
+
1965
+ **OPTIONS**
1966
+
1967
+ | Option | Type | Default | Description |
1968
+ | ------------ | ------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
1969
+ | `--tree` | boolean | `false` | Draw the containment tree: `tree.svg`, one `tree-<shortcode>.svg` per continent, and the findings. |
1970
+ | `--root` | string | — | With `--tree`, draw the subtree beneath this place as `tree-<shortcode>.svg` and report nothing. |
1971
+ | `--from` | string | — | Draw the map from this place as `from-<shortcode>.svg`; repeatable, or `all` for every place with a border or a route. |
1972
+ | `--travel` | boolean | `false` | Draw the whole route graph as `travel.svg`. |
1973
+ | `--engine` | choice | `dot` | The tree's layout engine — `dot`, `twopi` (radial, rooted on the world or the `--root`) or `neato`. The other two drawings are always `neato`. |
1974
+ | `--rankdir` | choice | `TB` | The tree's rank direction — `TB`, `LR`, `BT` or `RL`; `dot` engine only. |
1975
+ | `--nodesep` | number | by `--scale` | The tree's GraphViz `nodesep`, in inches. |
1976
+ | `--ranksep` | number | by engine | The tree's GraphViz `ranksep`, in inches. |
1977
+ | `--scale` | number | `1` | Multiply every node's width, height, font size and border weight, the legend's included. |
1978
+ | `--polities` | boolean | `true` | Draw polities beside their regions in the tree. `--no-polities` drops them. |
1979
+ | `--base` | string | — | The site base this package's pages are served under, ending in a slash; given, every name is a link to its page. |
1980
+ | `--out` | string | `build/map` | Directory to write into, relative to the working directory. |
1981
+
1982
+ **EXIT STATUS**
1983
+
1984
+ 1 when no drawing is asked for, when a `--from` or `--root` names no
1985
+ place, when the GraphViz engine a drawing needs is not installed, when a
1986
+ dependency's index cannot be read, when a note the index cannot record is
1987
+ met, on any error finding from `--tree` (a place with no parent, a parent
1988
+ that does not resolve, a cycle), or on any other thrown error. Otherwise 0
1989
+ — the warnings are reported and never fail the command.
1990
+
1991
+ **EXAMPLES**
1992
+
1993
+ ```
1994
+ $ content-build map --tree
1995
+ assets/content/Regions/Sea.md:16:5: warning: place "innersea" has more than one parent (northc, southc), which is legal for a sea between continents or a region split between two, and worth a look otherwise
1996
+ assets/content/Regions/Marches/Lost_Fort.md:9:14: error: place "lostfort" names parent "marchs", which does not resolve to any place in this package or a fetched index
1997
+ […] 394 place(s) → 7 drawing(s) under build/map
1998
+
1999
+ $ content-build map --from dunharargn --base /thalorna/
2000
+ […] 394 place(s) → 1 drawing(s) under build/map
2001
+
2002
+ $ content-build map --travel
2003
+ […] GraphViz's `neato` is not installed, and `content-build map` draws with it; install GraphViz (`brew install graphviz` on macOS, `apt-get install graphviz` on Debian or Ubuntu) and run the command again
2004
+ ```
2005
+
2006
+ **SEE ALSO**
2007
+
2008
+ `content-build lint [root]` (which checks `borders` and `routes` from both
2009
+ ends), `content-build deps fetch`, `content-build content-index [root]`,
2010
+ [Content format](content-format.md), [Diagnostics](diagnostics.md).
2011
+
1832
2012
  ### `content-build reachability <dir> [file]`
1833
2013
 
1834
2014
  **NAME**
@@ -769,6 +769,7 @@ Any other key under `docs.itemFields` is refused:
769
769
  | `site.passOptions` | object | `{}` |
770
770
  | `site.notfound` | object | `null` |
771
771
  | `site.hugo` | object | `{}` |
772
+ | `site.search` | boolean | `true` |
772
773
 
773
774
  How much of a package reaches the web at all is **not** here — it is
774
775
  [`publish.site`](#publish). `site` is framing: which named pass bundle
@@ -818,7 +819,7 @@ published. A `site.out` is refused by name:
818
819
 
819
820
  > ``package-build config: `site` must be a mapping.``
820
821
 
821
- > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, pass, passOptions, notfound, hugo).``
822
+ > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, pass, passOptions, notfound, hugo, search).``
822
823
 
823
824
  `site.assets` is the host every package's imagery is served from, and it is
824
825
  the one address in this file that is not this repository's own. A note names
@@ -932,6 +933,23 @@ the block cannot grow into a second configuration file:
932
933
 
933
934
  > ``package-build config: `site.hugo.baseURL` is derived from package.json `homepage` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
934
935
 
936
+ `site.search` is whether the rendered site carries a search index. On by
937
+ default: `package-build site-root` runs Pagefind over the rendered pages after
938
+ it writes `_headers`, and the index lands beside them as
939
+ `build/site/<contentPackage>/pagefind/`, served at `/<contentPackage>/pagefind/`
940
+ with the rest of the site. The theme loads it relative to the site's base URL
941
+ and renders the search box; what is indexed is the body of every page, with the
942
+ theme's own chrome marked out. `false` skips the step and removes an index an
943
+ earlier build left, so a site that wants no search deploys none. See
944
+ [`package-build site-root`](commands.md#package-build-site-root).
945
+
946
+ ```yaml
947
+ site:
948
+ search: false
949
+ ```
950
+
951
+ > ``package-build config: `site.search` must be a boolean.``
952
+
935
953
  ### The generated Hugo configuration
936
954
 
937
955
  `content-build site` writes `build/hugo/hugo.toml` on every run. Every value
@@ -3081,12 +3081,111 @@ three maps, and none of those maps needs to know it is a keep.
3081
3081
  - structure: A single building or habitation — halls, keeps, temples, inns, towers.
3082
3082
  - feature: A place significant by its terrain — forests, rivers, falls, passes, fords.
3083
3083
 
3084
- | `data` property | Values | Description |
3085
- | --------------- | ------------ | ---------------------------------------------------------------------------- |
3086
- | `demonym` | `string` | What a person from this place is called — a Vylarian |
3087
- | `lore` | `WikiLink[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
3088
- | `parents` | `WikiLink[]` | Enclosing places within which this place is located |
3089
- | `population` | `number` | Approximate population (precision 2 significant digits) |
3084
+ | `data` property | Values | Description |
3085
+ | --------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
3086
+ | `demonym` | `string` | What a person from this place is called — a Vylarian |
3087
+ | `lore` | `WikiLink[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
3088
+ | `parents` | `WikiLink[]` | Enclosing places within which this place is located |
3089
+ | `population` | `number` | Approximate population (precision 2 significant digits) |
3090
+ | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
3091
+ | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
3092
+
3093
+ #### Where a place is, in relation to other places
3094
+
3095
+ `parents` states what a place is **within**. Two more properties state what
3096
+ it is **next to** and what it is **reachable from**, as data rather than as
3097
+ prose, so that two notes describing one frontier cannot quietly disagree about
3098
+ which side of it each is on:
3099
+
3100
+ ```yaml
3101
+ data:
3102
+ borders: # places sharing a frontier
3103
+ - { to: dunharargn, bearing: NE } # both required
3104
+ - { to: bethuargn, bearing: W }
3105
+ routes: # journeys from this place's centre
3106
+ - { to: steinmark, bearing: SW, mode: land, days: 3, terrain: [road, mountains] }
3107
+ - { to: denizara, bearing: W, mode: ship, days: 5 }
3108
+ - { to: kethramir, bearing: E, mode: land, days: 30, terrain: [dunes], leagues: 90 }
3109
+ ```
3110
+
3111
+ - `to` — required; the `shortcode` of a `place` note in this package or in a
3112
+ declared dependency's index. A shortcode, not an address.
3113
+ - `bearing` — required; where the neighbour or the destination lies from here.
3114
+ - `mode` — required on a route; how the journey is travelled.
3115
+ - `days` — required on a route; a marker meaning "about this, under normal
3116
+ conditions", from the scale below. Any other value is an error.
3117
+ - `terrain` — optional on a route; the terrains crossed, in travel order, from
3118
+ the registry below. Each terrain names the modes that cross it, so open sea
3119
+ on a land route — or a road on a ship route — is an error.
3120
+ - `leagues` — optional on a route; a distance, only where the note states one.
3121
+
3122
+ **Regions border; settlements are days apart.** A frontier is a fact about two
3123
+ regions, and a journey is a fact about two places a traveller sets out from and
3124
+ arrives at. A pair appears in `routes` once per `mode` — a port and its
3125
+ neighbour may be three days by land and one by ship — and in `borders` once.
3126
+ **A place never borders its own parent or child**: containment is `parents`,
3127
+ and restating it as adjacency makes the two contradict each other.
3128
+
3129
+ **Both ends state the relation, and `content-build lint` checks that they
3130
+ agree.** A border at `NE` from one side is a border at `SW` from the other; a
3131
+ route stated by land in three days from one side is stated by land in three
3132
+ days from the other, at the opposite bearing. A neighbour that states nothing
3133
+ back is a warning naming both notes — the pair is half-written, not wrong. A
3134
+ neighbour that states a different bearing, a different `days` or a different
3135
+ `mode` is an error, because one of the two notes is wrong and a reader cannot
3136
+ tell which. Every `to` must resolve to a place, every value must come from its
3137
+ closed set, and each finding is located at the entry that states it.
3138
+
3139
+ | `bearing` value | where the other place lies |
3140
+ | --------------- | -------------------------- |
3141
+ | `N` | north |
3142
+ | `NE` | north-east |
3143
+ | `E` | east |
3144
+ | `SE` | south-east |
3145
+ | `S` | south |
3146
+ | `SW` | south-west |
3147
+ | `W` | west |
3148
+ | `NW` | north-west |
3149
+
3150
+ | `mode` value | travelled |
3151
+ | ------------ | ---------------------------- |
3152
+ | `land` | on foot, mounted, or by cart |
3153
+ | `boat` | on a river or a lake |
3154
+ | `ship` | on open sea |
3155
+
3156
+ | `days` value | meaning |
3157
+ | ------------ | -------------------------------------------- |
3158
+ | `1` | a day |
3159
+ | `2` | two days |
3160
+ | `3` | three days |
3161
+ | `5` | about five days |
3162
+ | `10` | a ten-day week |
3163
+ | `20` | two ten-day weeks |
3164
+ | `30` | three ten-day weeks |
3165
+ | `45` | a month and a half |
3166
+ | `60` | two months |
3167
+ | `90` | a season |
3168
+ | `180` | many months |
3169
+ | `360` | who knows — a journey nobody reckons in days |
3170
+
3171
+ | `terrain` value | crossed by |
3172
+ | --------------- | ------------------ |
3173
+ | `road` | land |
3174
+ | `track` | land |
3175
+ | `plain` | land |
3176
+ | `steppe` | land |
3177
+ | `hills` | land |
3178
+ | `mountains` | land |
3179
+ | `forest` | land |
3180
+ | `jungle` | land |
3181
+ | `marsh` | land |
3182
+ | `dunes` | land |
3183
+ | `desert` | land |
3184
+ | `ice` | land |
3185
+ | `coast` | land, boat or ship |
3186
+ | `open-sea` | ship |
3187
+ | `river` | boat |
3188
+ | `lake` | boat |
3090
3189
 
3091
3190
  **A place declares only what is true of ground.** Four properties were removed
3092
3191
  because they were true of something else, and each removal has a home to go to.
@@ -242,6 +242,7 @@ over one would teach people to stop reading the report.
242
242
  | `content-build markdown [paths..]` | it reports any markdownlint finding |
243
243
  | `content-build site` | any gate finding (no homepage, a wikilink in frontmatter, an address that cannot be derived, an unusable or unaddressable foreign manifest, a conflicting address) or any table or wikilink error while writing pages — the run stops at the first gate that fires |
244
244
  | `content-build pdf` | it could not build the book for a reason other than a deliberate no-op — never on a reported finding by itself |
245
+ | `content-build map` | `--tree` reports an `error`-severity finding (a place with no parent, a parent that does not resolve, a cycle), a named place is not one, the GraphViz engine a drawing needs is not installed, or the corpus cannot be indexed — never on a warning |
245
246
  | `content-build docs item-fields --check` | the generated page differs from what is committed |
246
247
  | `content-build package compile` | pack JSON generation reported any `error`-severity finding (checked, and the compile refused, before any pack is written), or a compiled pack fails its Scene/Level integrity check |
247
248
  | `content-build deps fetch` | the fetch itself throws — it emits no diagnostics |
@@ -263,12 +263,13 @@ package carries this group:
263
263
  `build:site-content` writes the whole Hugo source tree under `build/hugo/` —
264
264
  the generated `hugo.toml` and the content mount — with `content-build site`;
265
265
  `build:site-html` runs Hugo over it, rendering into `build/site/<contentPackage>/`;
266
- `build:site-root` writes the deployment's `_headers` beside
267
- that; and `serve:site` does the first and then `hugo server` for a live
268
- preview. The repository carries no Hugo configuration of its own: `hugo.toml`
269
- is generated on every run from `package.json`, `package-build.config.yaml`,
270
- the installed `@heroiclands/hugo-theme` and the navigation `deps fetch`
271
- cached, and the only file to add is `@heroiclands/hugo-theme` under
266
+ `build:site-root` writes the deployment's `_headers` beside that and indexes
267
+ the rendered pages for search into `build/site/<contentPackage>/pagefind/`;
268
+ and `serve:site` does the first and then `hugo server` for a live preview.
269
+ The repository carries no Hugo configuration of its own: `hugo.toml` is
270
+ generated on every run from `package.json`, `package-build.config.yaml`, the
271
+ installed `@heroiclands/hugo-theme` and the navigation `deps fetch` cached,
272
+ and the only file to add is `@heroiclands/hugo-theme` under
272
273
  `devDependencies`. Hugo itself is a separate install — the extended edition,
273
274
  on the developer's `PATH` and the runner's.
274
275
 
@@ -50,13 +50,22 @@ const CHANGES_HEADING_RE = /^### (?:Major|Minor|Patch) Changes$/gm;
50
50
  *
51
51
  * @param {string} blockText - One block, as {@link topLevelBlocks} collects
52
52
  * it — its first line is the label line for a labelled block.
53
- * @returns {{labelLine: string, rest: string}} `rest` has its leading blank
54
- * line dropped; a block with nothing but the label line yields `""`.
53
+ * @returns {{labelLine: string, rest: string, hadBlankLine: boolean}} `rest`
54
+ * has its leading blank line dropped; a block with nothing but the label
55
+ * line yields `""`. `hadBlankLine` says whether the label line was
56
+ * followed by a blank line in `blockText` — false for a bold sentence a
57
+ * hard-wrapped paragraph continues directly beneath, with no blank line
58
+ * of its own.
55
59
  */
56
60
  function splitLabelLine(blockText) {
57
61
  const nl = blockText.indexOf("\n");
58
- if (nl === -1) return { labelLine: blockText, rest: "" };
59
- return { labelLine: blockText.slice(0, nl), rest: blockText.slice(nl + 1).replace(/^\n+/, "") };
62
+ if (nl === -1) return { labelLine: blockText, rest: "", hadBlankLine: false };
63
+ const after = blockText.slice(nl + 1);
64
+ return {
65
+ labelLine: blockText.slice(0, nl),
66
+ rest: after.replace(/^\n+/, ""),
67
+ hadBlankLine: after.startsWith("\n"),
68
+ };
60
69
  }
61
70
 
62
71
  /**
@@ -115,6 +124,14 @@ function joinItems(items) {
115
124
  * One label's blocks (or every unlabelled one), folded into the single
116
125
  * block `group` writes for it.
117
126
  *
127
+ * A single, unmerged block — the common case, one changeset writing one
128
+ * label once — keeps its own label-to-body separator rather than forcing a
129
+ * blank line: a bold sentence a hard-wrapped paragraph continues directly
130
+ * beneath must round-trip unchanged, not gain a blank line it never had.
131
+ * Merging two or more blocks, or a label whose lone block carries several
132
+ * items (bullets), still separates the label from its body with a blank
133
+ * line — the convention every existing multi-item block already uses.
134
+ *
118
135
  * @param {Array<{label: string|null, text: string}>} blocks - Every block
119
136
  * sharing one label, in file order. All carry the same `label`.
120
137
  * @returns {string}
@@ -123,9 +140,13 @@ function mergeGroup(blocks) {
123
140
  if (blocks[0].label === null) {
124
141
  return joinItems(dedupeItems(blocks.flatMap((block) => itemsOf(block.text))));
125
142
  }
126
- const { labelLine } = splitLabelLine(blocks[0].text);
143
+ const { labelLine, hadBlankLine } = splitLabelLine(blocks[0].text);
127
144
  const items = dedupeItems(blocks.flatMap((block) => itemsOf(splitLabelLine(block.text).rest)));
128
- return items.length ? `${labelLine}\n\n${joinItems(items)}` : labelLine;
145
+ if (items.length === 0) return labelLine;
146
+ if (blocks.length === 1 && items.length === 1) {
147
+ return `${labelLine}${hadBlankLine ? "\n\n" : "\n"}${items[0]}`;
148
+ }
149
+ return `${labelLine}\n\n${joinItems(items)}`;
129
150
  }
130
151
 
131
152
  /**
@@ -234,13 +234,15 @@ export function topLevelBullets(text, codeLines) {
234
234
  * {@link topLevelBullets}, where a bullet marker *is* the top-level
235
235
  * construct — a `-` bullet at column 0 belongs to whatever block is
236
236
  * already open, since a label's bullets sit unindented directly under it.
237
- * Any other column-0 line (plain prose with no label, a bullet with no
238
- * block open yet) starts the one "lead" block (`label: null`) a changeset
239
- * with no category writes — the case `changelog group` sorts first and
240
- * `check` never flags. Nested detail — a wrapped line, a bullet's own
241
- * continuation — stays indented and belongs to whatever it follows.
242
- * `changelog group` folds same-label blocks together; `check` warns when a
243
- * block's label is not in the declared vocabulary.
237
+ * A plain column-0 line that is neither a label nor a bullet continues
238
+ * whatever block is open, because Prettier's `proseWrap` puts every line of
239
+ * a hard-wrapped paragraph at column 0 — it starts a new "lead" block
240
+ * (`label: null`) only at a paragraph boundary, the start of the section or
241
+ * a line right after a blank one, which is the case `changelog group`
242
+ * sorts first and `check` never flags. A blank line never closes a block by
243
+ * itself; it only ends the run of continuation lines a following column-0
244
+ * line can attach to. `changelog group` folds same-label blocks together;
245
+ * `check` warns when a block's label is not in the declared vocabulary.
244
246
  *
245
247
  * @param {string} text - Section text.
246
248
  * @param {Set<number>} codeLines - Lines inside a code region, from
@@ -266,6 +268,9 @@ export function topLevelBlocks(text, codeLines, scaffoldLines = new Set()) {
266
268
  });
267
269
  current = null;
268
270
  };
271
+ // A paragraph boundary — the start of the section, or right after a
272
+ // blank line — is the only place an unlabelled lead block may start.
273
+ let afterBlank = true;
269
274
 
270
275
  for (let i = 0; i < lines.length; i++) {
271
276
  const lineNo = i + 1;
@@ -273,29 +278,44 @@ export function topLevelBlocks(text, codeLines, scaffoldLines = new Set()) {
273
278
 
274
279
  if (codeLines.has(lineNo)) {
275
280
  if (current) current.parts.push(raw);
281
+ afterBlank = raw.trim() === "";
276
282
  continue;
277
283
  }
278
284
  if (scaffoldLines.has(lineNo)) {
279
285
  close();
286
+ afterBlank = true;
280
287
  continue;
281
288
  }
282
289
  const atColumnZero = raw.trim() !== "" && !/^[ \t]/.test(raw);
283
290
  if (!atColumnZero) {
284
291
  if (current) current.parts.push(raw);
292
+ afterBlank = raw.trim() === "";
285
293
  continue;
286
294
  }
287
295
  const label = LABEL_LINE_RE.exec(raw)?.[1] ?? null;
288
296
  if (label !== null) {
289
297
  close();
290
298
  current = { startLine: lineNo, label, parts: [raw] };
299
+ afterBlank = false;
291
300
  continue;
292
301
  }
293
302
  if (TOP_BULLET_RE.test(raw) && current) {
294
303
  current.parts.push(raw);
304
+ afterBlank = false;
305
+ continue;
306
+ }
307
+ // Plain prose at column 0: it continues whatever block is already
308
+ // open unless it opens a new paragraph — Prettier's `proseWrap`
309
+ // puts every line of a hard-wrapped paragraph at column 0, so a
310
+ // continuation line must never be read as a new lead block.
311
+ if (current && !afterBlank) {
312
+ current.parts.push(raw);
313
+ afterBlank = false;
295
314
  continue;
296
315
  }
297
316
  close();
298
317
  current = { startLine: lineNo, label: null, parts: [raw] };
318
+ afterBlank = false;
299
319
  }
300
320
  close();
301
321
  return blocks;
@@ -1403,17 +1403,27 @@ export function lintNote(
1403
1403
  // to recognise it would be worse than not checking.
1404
1404
  const entry = vocabulary?.[current];
1405
1405
  if (entry) {
1406
+ const fields = dataFields(current, vocabulary) ?? [];
1406
1407
  findings.push(
1407
1408
  ...checkDataContainer(note, {
1408
1409
  type,
1409
1410
  // Read through `dataFields`, so the keys **every** type accepts
1410
1411
  // are part of what this type declares rather than a second list
1411
1412
  // the container check would have to be told about.
1412
- fields: dataFields(current, vocabulary) ?? [],
1413
+ fields,
1413
1414
  packs,
1414
1415
  }),
1415
1416
  );
1416
1417
  findings.push(...checkSubType(note, { type, entry }));
1418
+ // A field that declares a check of its own, beyond its shape. Handed
1419
+ // the index the reference check resolves through, so a value naming
1420
+ // another note reads that note — local or in a fetched index — the
1421
+ // way a wikilink would. The linter names no such field itself; the
1422
+ // vocabulary does, beside the field.
1423
+ for (const field of fields) {
1424
+ if (typeof field.check !== "function") continue;
1425
+ findings.push(...field.check(note, { index }));
1426
+ }
1417
1427
  }
1418
1428
 
1419
1429
  const fields = authoredFields(schema);
@@ -378,15 +378,23 @@ export function homepageTitle(fm, config) {
378
378
  * redirect stub at each one. The field is retired and refused before a
379
379
  * build reaches here, which makes this a guard rather than a working path.
380
380
  *
381
+ * `related` is derived like every other page's — what links to the homepage
382
+ * is a fact about the rest of the tree — so an authored value is dropped and
383
+ * the caller's block, where it has one, is written in its place.
384
+ *
381
385
  * @param {object} fm - The note's frontmatter.
382
386
  * @param {object} options - Options.
383
387
  * @param {string} options.contentPackage - The package this build publishes.
384
388
  * @param {string} options.title - The resolved title.
389
+ * @param {object} [options.related] - The homepage's backlinks and mentions,
390
+ * where the site build found either.
385
391
  * @returns {object} The frontmatter to write.
386
392
  */
387
- export function homepageFrontmatter(fm, { contentPackage, title }) {
393
+ export function homepageFrontmatter(fm, { contentPackage, title, related }) {
388
394
  const data = { ...fm, package: contentPackage, title };
389
395
  delete data.aliases;
396
+ delete data.related;
397
+ if (related) data.related = related;
390
398
  return data;
391
399
  }
392
400
 
package/engine/index.mjs CHANGED
@@ -84,6 +84,24 @@ export * as noteSchemas from "./note-schemas.mjs";
84
84
  /** The closed frontmatter regions: the `data:` container and each type's `subType`. */
85
85
  export * as noteVocabulary from "./note-vocabulary.mjs";
86
86
 
87
+ /** What a place is next to and reachable from: the closed sets, and the checks. */
88
+ export * as placeRelations from "./place-relations.mjs";
89
+
90
+ /** The places a map is drawn from, read from the content index, and what `parents` says about them. */
91
+ export * as mapPlaces from "./map-places.mjs";
92
+
93
+ /** The geometry of the map from a place: bearings to angles, days to rings, hops composed. */
94
+ export * as mapLayout from "./map-layout.mjs";
95
+
96
+ /** The three drawings of `content-build map`, as DOT. */
97
+ export * as mapDot from "./map-dot.mjs";
98
+
99
+ /** GraphViz, as the map command finds and runs it. */
100
+ export * as mapGraphviz from "./map-graphviz.mjs";
101
+
102
+ /** `content-build map`: the drawings, written under `build/map/`. */
103
+ export * as mapBuild from "./map-build.mjs";
104
+
87
105
  /** The closed registry of system ids, and the `none` that stands for no system. */
88
106
  export * as systems from "./systems.mjs";
89
107