@heroiclands/package-build 22.4.6 → 22.5.0

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 (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CONTENT.md +192 -1
  3. package/bin/content-build.mjs +26 -4
  4. package/content-config.mjs +8 -3
  5. package/docs/api.md +99 -26
  6. package/docs/configuration.md +20 -1
  7. package/docs/content-format.md +238 -0
  8. package/docs/diagnostics.md +44 -0
  9. package/engine/content-index.mjs +63 -21
  10. package/engine/content-links.mjs +60 -6
  11. package/engine/content-tables.mjs +29 -2
  12. package/engine/frontmatter-lint.mjs +2 -0
  13. package/engine/holdings.mjs +416 -0
  14. package/engine/index-records.mjs +25 -0
  15. package/engine/index.mjs +16 -0
  16. package/engine/map-graphviz.mjs +12 -3
  17. package/engine/map-places.mjs +33 -3
  18. package/engine/market-class.mjs +178 -0
  19. package/engine/metadata-index.mjs +7 -4
  20. package/engine/note-state.mjs +213 -0
  21. package/engine/note-vocabulary.mjs +59 -2
  22. package/engine/pdf-build.mjs +4 -2
  23. package/engine/populations.mjs +557 -0
  24. package/engine/site-build.mjs +96 -10
  25. package/engine/site-maps.mjs +165 -0
  26. package/engine/sql-tables.mjs +157 -4
  27. package/engine/stub-lint.mjs +237 -0
  28. package/engine/wikilink-syntax.mjs +27 -1
  29. package/package.json +1 -1
  30. package/types/engine/content-index.d.mts +19 -5
  31. package/types/engine/content-links.d.mts +3 -3
  32. package/types/engine/content-tables.d.mts +2 -1
  33. package/types/engine/holdings.d.mts +157 -0
  34. package/types/engine/index-records.d.mts +22 -0
  35. package/types/engine/index.d.mts +6 -0
  36. package/types/engine/map-graphviz.d.mts +8 -1
  37. package/types/engine/map-places.d.mts +26 -0
  38. package/types/engine/market-class.d.mts +62 -0
  39. package/types/engine/note-state.d.mts +110 -0
  40. package/types/engine/note-vocabulary.d.mts +25 -0
  41. package/types/engine/populations.d.mts +121 -0
  42. package/types/engine/site-build.d.mts +37 -6
  43. package/types/engine/site-maps.d.mts +81 -0
  44. package/types/engine/stub-lint.d.mts +51 -0
  45. package/types/engine/wikilink-syntax.d.mts +19 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Three states of a note** — a note with no body is now a **stub**: it carries
8
+
9
+ - A page's settlements, deities or characters can be listed with the unwritten
10
+ ones beside the written, and counted by how finished they are — select
11
+ `FROM entries` rather than `FROM notes`, which leaves stubs out as it always
12
+ has.
13
+ - A folder note and the homepage are complete with no body, and keep their pages.
14
+ - An unwritten note still compiles the compendium document its facts describe.
15
+ - `content-build lint` asks a stub for a description, refuses one tagged
16
+ `draft`, and reports a body that says only "TBD".
17
+ - Every build now prints how many of a package's notes are full, draft and stub.
18
+
19
+ ### Patch Changes
20
+
21
+ **Population figures are checked against each other.** A region whose polities
22
+ or inner regions count more people than it states, a settlement larger than the
23
+ land around it, and a reference page carrying a figure the place's own note
24
+ contradicts are each reported while the note is open.
25
+
26
+ ## 22.4.7
27
+
28
+ ### Patch Changes
29
+
30
+ Every place page that states a border or a route, or is named in one, carries the map from that place: `from-<shortcode>.svg` beside the page and `map:` naming it in the front matter, with every place name on it a link to that place's page. GraphViz draws it; when GraphViz is not installed the site build says so once and writes every page without a map, and `site.maps: false` draws none.
31
+
32
+ A place's page carries what lies within it and who holds it, and an affiliation's page carries what it holds, read from the `parents` and `domains` the notes already state — a dependency's places and affiliations included. `content-build lint` warns of unheld land: a settlement, site or structure that no affiliation's `domains` names.
33
+
34
+ A `place` note can state a settlement's market class: `data.market`, a whole number from 1 to 6 beside `population`. `1` is a hamlet, with no market beyond what neighbours trade among themselves; `2` a village and its weekly market; `3` a town with several trades working full time; `4` a market town, its chartered fair and its moneylender; `5` a city, where anything ordinary can be had in quantity; `6` a great city that banks, imports as a matter of course, and is the market from which the other markets buy. The key is optional, and a value off the scale is reported at the line that states it rather than rounded onto a class nobody wrote.
35
+
3
36
  ## 22.4.6
4
37
 
5
38
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -1093,6 +1093,60 @@ it was addressed by the package rather than by a slug. It carries an address
1093
1093
  like every other note now; the guard is unchanged, because what it reads
1094
1094
  was never the key count.)
1095
1095
 
1096
+ ### An empty body is a stub, on purpose
1097
+
1098
+ A note whose body is empty is a **stub**: it carries its frontmatter into the
1099
+ content index and is queryable there, it keeps its `id` and the Foundry document
1100
+ its type compiles, and it publishes no page, no address and no link target. A
1101
+ note with a body and the `draft` tag is a **draft**, and one with a body and no
1102
+ tag is **full**. `docs/content-format.md` states the rule; this is what the lint
1103
+ does with it.
1104
+
1105
+ Four findings and two reports:
1106
+
1107
+ - **A stub carries a `description`.** With nothing in the body it is the only
1108
+ sentence a reader gets, and it is what every table listing the note prints. An
1109
+ **error**, at the note's `type:` line.
1110
+ - **A stub is not tagged `draft`.** A thing not started is not a thing in
1111
+ progress. An **error**: remove the tag, or write a body.
1112
+ - **A body that reduces to nothing is an abandoned draft, not a stub.** A body
1113
+ that is only headings, rules, comments and placeholder phrases — `TBD`, `To be
1114
+ written`, `Coming soon`, `Placeholder`, `N/A` — publishes a page that tells a
1115
+ reader nothing. An **error** naming the phrase and where it sits: empty the
1116
+ body to make it a stub, or write it.
1117
+ - **A short unmarked body may be a draft that forgot its marker.** Under
1118
+ twenty-five words with no `draft` tag is a **warning, never an error**. A note
1119
+ reading `See [[affiliation-meivor|Mëivōr]]` says everything it has to say, and
1120
+ a corpus holds many of them.
1121
+
1122
+ And two things the rules cannot decide, so they are reported as prose rather
1123
+ than flagged:
1124
+
1125
+ - **The oldest drafts are listed** by last modification. A long-standing draft
1126
+ is unfinished, which the tag already says; a warning nobody can clear is how a
1127
+ report teaches its reader to stop looking at it.
1128
+ - **The counts are reported**, per package and per type:
1129
+
1130
+ ```text
1131
+ thalorna: 2572 notes — 2089 full, 464 draft, 19 stub
1132
+ ```
1133
+
1134
+ `content-build content-index` prints the same line, so the ratio of written to
1135
+ unwritten is visible on every build rather than discovered in a year.
1136
+
1137
+ #### Adopting it
1138
+
1139
+ A package with no empty-bodied note is unaffected. A package that has them:
1140
+
1141
+ 1. Take the release and run `content-build lint`. Act on the stub findings — a
1142
+ `description` on every stub, and no stub tagged `draft`.
1143
+ 2. Change the `sql` fences that **should** see stubs from `FROM notes` to
1144
+ `FROM entries`. The ones that would gain rows are warned about at the fence,
1145
+ so the list comes to you.
1146
+ 3. Read the site's page count. A stub publishes no page, so a tree with stubs
1147
+ publishes fewer pages than it did — which is the point, and worth confirming
1148
+ against any `min-pages` gate the deploy declares.
1149
+
1096
1150
  ### Exactly one homepage
1097
1151
 
1098
1152
  A content tree declares **exactly one** `type: homepage` note. Zero is an
@@ -1183,6 +1237,45 @@ parent or child, or a pair listed twice by one mode. Each finding lands on the
1183
1237
  entry that states it. The keys, the sets and the days scale are specified under
1184
1238
  `type: place` in `docs/content-format.md`.
1185
1239
 
1240
+ **A settlement is held by someone.** An affiliation's `domains` names the
1241
+ places it holds, and a `place` of subType `settlement`, `site` or `structure`
1242
+ that no affiliation's `domains` names — in this tree or in a fetched index —
1243
+ is a warning, `unheld land`, at the note's `type:` line, so a gap in tenure
1244
+ shows. A region is held through its polity's `domains` and a feature by
1245
+ nobody, so neither is checked.
1246
+
1247
+ **Population figures agree from the region down.** A place states
1248
+ `data.population` and an affiliation states how many people it counts, and four
1249
+ warnings read those figures against each other. The polities whose `domains`
1250
+ name a place must not count more people than the place states — `over-held
1251
+ land` — with a polity subordinate to another holding the same place counted
1252
+ once and a settlement exempt, its holder's figure counting a hinterland the
1253
+ settlement does not. The regions whose `parents` name a place must not count
1254
+ more than it states either — `over-full region` — a region naming both its
1255
+ parent and its grandparent counting under the nearer. A settlement must not
1256
+ hold more than any place containing it states, which is `oversized settlement`.
1257
+ And a `doc` note carrying a figure beside a wikilink to the place or
1258
+ affiliation it belongs to must state that note's figure, which is `disputed
1259
+ figure`, located at the number it wrote. Figures are authored to two
1260
+ significant digits, so a parent rounded up to five per cent below what it
1261
+ contains is not a finding, and a place or affiliation stating no figure is
1262
+ neither compared nor counted. A fetched index carries no figure, so a
1263
+ dependency's people are counted nowhere. There is deliberately **no urban-share
1264
+ rule**: a share computed from the named settlements against a region's total
1265
+ measures how much of the region has been written, not how much of it is urban.
1266
+
1267
+ **A settlement's market class is one of six.** `data.market` states what trade
1268
+ a settlement supports — `1` a hamlet, with no market beyond what neighbours
1269
+ trade among themselves; `2` a village and its weekly market; `3` a town with
1270
+ several trades working full time; `4` a market town, its chartered fair and its
1271
+ moneylender; `5` a city, where anything ordinary can be had in quantity; `6` a
1272
+ great city that banks, imports as a matter of course, and is the market from
1273
+ which the other markets buy. The numbers order the steps and do nothing else, so a value off the
1274
+ scale is an error at the value that states it rather than a number rounded onto
1275
+ a class the author did not write. The key is optional and the lint checks no
1276
+ `subType` condition, as it checks none for `population`. The scale is stated
1277
+ under `type: place` in `docs/content-format.md`.
1278
+
1186
1279
  Nothing here writes. A check reports and an author fixes.
1187
1280
 
1188
1281
  **`content-build map` draws what those keys state.** `--tree` is the author's
@@ -2035,7 +2128,15 @@ the build fails naming both.
2035
2128
  ### Every note's address, and every anchor it defines
2036
2129
 
2037
2130
  A record states the address a wikilink writes to reach the note, and every
2038
- `{#slug}` anchor its body declares:
2131
+ `{#slug}` anchor its body declares — unless the note is a **stub**, which
2132
+ carries both as `null`: it publishes no page, so it holds no address and offers
2133
+ no anchor to reach. Everything else it declares is kept, its `id` and `foundry`
2134
+ block included, because a document is derived from `data:` rather than from
2135
+ prose.
2136
+
2137
+ The SQL views read that absence as the state: `entries` is every row plus a
2138
+ derived `state` column, and `notes` is `entries` without the stubs. Nothing
2139
+ about the state is stored, and no note may author it.
2039
2140
 
2040
2141
  ```json
2041
2142
  {
@@ -2409,6 +2510,96 @@ The theme's `related.html` partial reads both lists and renders them as one
2409
2510
  `related` is derived, never authored: what links to a page is a fact about
2410
2511
  every other page in the tree. A note that writes one has it replaced.
2411
2512
 
2513
+ ### A place page says what lies within it and who holds it; an affiliation page says what it holds
2514
+
2515
+ Two keys the notes already carry answer three questions a reader asks of a
2516
+ page. A place's `data.parents` is geography — what it sits within — and an
2517
+ affiliation's `data.domains` is tenure — what it holds. The build reads both
2518
+ across the tree and every fetched index, inverts them, and writes each page
2519
+ the lists that concern it, shaped like `related`:
2520
+
2521
+ ```yaml
2522
+ # on a place
2523
+ contains: # every place whose `parents` names this one
2524
+ - title: Khaset-Mehtet
2525
+ url: /thalorna/place-khasetmehtet/
2526
+ type: place
2527
+ subType: settlement
2528
+ held_by: # every affiliation whose `domains` names this one
2529
+ - title: The Nome of Ankhsetun
2530
+ url: /thalorna/affiliation-nomenkhstn/
2531
+ type: affiliation
2532
+ subType: polity
2533
+
2534
+ # on an affiliation
2535
+ holdings: # every place its `domains` names
2536
+ - title: Ankhsetun
2537
+ url: /thalorna/place-ankhsetunnome/
2538
+ type: place
2539
+ subType: region
2540
+ ```
2541
+
2542
+ - **`contains` and `holdings` are sorted by `subType`, then `title`**;
2543
+ `held_by` by `title`. An entry carries `subType` where the note declares
2544
+ one.
2545
+ - **A key is present only when it has an entry.** A place nobody holds and
2546
+ nothing sits within carries neither key; an affiliation whose `domains` is
2547
+ empty carries no `holdings`. The theme's silent-disappear convention, kept
2548
+ at the source.
2549
+ - **`domains` names what an affiliation holds directly, and is never
2550
+ expanded.** A polity whose `domains` names a region holds the region; the
2551
+ settlements within it are reached through the region's `contains`, and are
2552
+ not repeated in the polity's `holdings`. Tenure runs through
2553
+ `affiliation.parents` — a house of its earl, an earl of the crown — and
2554
+ geography through `place.parents`. A subinfeudated manor sits in one region
2555
+ by the second and under a lord of another polity by the first, and both
2556
+ pages say so.
2557
+ - **A dependency's places and affiliations take part.** A fetched index entry
2558
+ carries the `parents` and `domains` its record stated, so a place another
2559
+ package publishes is listed within a local region, and a house another
2560
+ package publishes is named on the local manor it holds. A local note
2561
+ declaring a shortcode shadows a fetched entry declaring the same one.
2562
+ - **`url` composes `<base><slug>/`**, as every `related` entry does.
2563
+
2564
+ All three are derived, never authored: a note that writes one has it
2565
+ replaced. The theme renders each list as a table on the page.
2566
+
2567
+ ### A place page carries the map from that place
2568
+
2569
+ A place that states a border or a route, or is named in one, has a map from
2570
+ it — the drawing `content-build map --from` makes, with the place at the
2571
+ centre and each neighbour at its bearing. The site build draws that map for
2572
+ every such place and writes it with the page: the page is a leaf bundle,
2573
+ `place-<shortcode>/index.md`, the drawing sits beside it as
2574
+ `from-<shortcode>.svg`, and the front matter names it:
2575
+
2576
+ ```yaml
2577
+ map: from-kthrmr.svg
2578
+ ```
2579
+
2580
+ The theme's "From here" panel reads the key, inlines the file as a page
2581
+ resource, and so every place name on the map is a link to that place's page.
2582
+ What the build guarantees:
2583
+
2584
+ - **The address does not move.** A bundled page states the same `url` a flat
2585
+ one does, `/<slug>/`, so `place-kthrmr/index.md` publishes exactly where
2586
+ `place-kthrmr.md` would.
2587
+ - **Every name links through the site base.** The map is drawn with the base
2588
+ the pages are served under, so each `href` composes `<base><slug>/` the way
2589
+ every other href the build renders does.
2590
+ - **It is written for inlining.** No XML prologue, no DOCTYPE, no comment, no
2591
+ reference to anything outside the file; the root carries a `viewBox` and no
2592
+ fixed width or height, so the theme sizes it to the column.
2593
+ - **A place with no relation carries nothing**: no file beside it, no `map`
2594
+ key, and the page stays a flat file. So does every page that is not a place.
2595
+ - **A site never fails for want of a map.** GraphViz draws; when it is not
2596
+ installed the build says so once, as a warning naming what to install, and
2597
+ every page is written as it would be without maps. `site.maps: false` in
2598
+ `package-build.config.yaml` draws nothing and asks nothing of GraphViz.
2599
+
2600
+ `map` is derived, never authored: a note that writes one has it replaced when
2601
+ the build draws a map, and dropped when there is none to draw.
2602
+
2412
2603
  ### Why the output location is fixed
2413
2604
 
2414
2605
  The content mount is a build artifact and is **deleted on every run**, so that a
@@ -83,6 +83,7 @@ import {
83
83
  import { fetchNavigation, generateHugoConfig, writeHugoConfig } from "../engine/site-config.mjs";
84
84
  import { renderItemFieldReference, renderItemFieldsPage } from "../engine/field-reference.mjs";
85
85
  import { lintContentTree } from "../engine/content-lint.mjs";
86
+ import { lintNoteStates } from "../engine/stub-lint.mjs";
86
87
  import { lintContentCharset } from "../engine/content-charset.mjs";
87
88
  import { lintContentHtml } from "../engine/content-html.mjs";
88
89
  import { lintContentIcons } from "../engine/content-icons.mjs";
@@ -1004,6 +1005,17 @@ function lintCommand() {
1004
1005
  config,
1005
1006
  });
1006
1007
 
1008
+ // Whether every empty body is a stub on purpose, and how
1009
+ // finished the tree is. The counts and the oldest drafts are
1010
+ // prose rather than findings: a note nobody has finished is not
1011
+ // wrong, and 464 warnings a reader cannot clear is how a report
1012
+ // teaches its reader to stop looking at it.
1013
+ const states = lintNoteStates(root, {
1014
+ records,
1015
+ contentPackage: config.contentPackage,
1016
+ });
1017
+ for (const line of states.summary) log.info(line);
1018
+
1007
1019
  const findings = [
1008
1020
  ...addresses.findings,
1009
1021
  ...frontmatter.findings,
@@ -1012,6 +1024,7 @@ function lintCommand() {
1012
1024
  ...icons.findings,
1013
1025
  ...html.findings,
1014
1026
  ...images.findings,
1027
+ ...states.findings,
1015
1028
  ];
1016
1029
  // Only an **error** fails the run. Every finding was an error
1017
1030
  // by then, so this changes nothing on its own —
@@ -1404,14 +1417,15 @@ function contentIndexCommand() {
1404
1417
  handler: (argv) => {
1405
1418
  try {
1406
1419
  const config = loadPackConfig();
1407
- const { file, notes, bytes } = emitContentIndex({
1420
+ const { file, notes, bytes, full, draft, stub } = emitContentIndex({
1408
1421
  config,
1409
1422
  ...(argv.root ? { contentBase: argv.root } : {}),
1410
1423
  ...(argv.out ? { outDir: argv.out } : {}),
1411
1424
  });
1412
1425
  log.info(
1413
1426
  `${config.contentPackage} → ${path.relative(process.cwd(), file)} ` +
1414
- `(${notes} notes, ${Math.round(bytes / 1024)} KiB)`,
1427
+ `(${notes} notes — ${full} full, ${draft} draft, ` +
1428
+ `${stub} stub — ${Math.round(bytes / 1024)} KiB)`,
1415
1429
  );
1416
1430
  } catch (err) {
1417
1431
  reportFailure(err);
@@ -1515,7 +1529,8 @@ function pdfCommand() {
1515
1529
  * wikilink resolution, code-fence protection and the foreign-manifest merge —
1516
1530
  * happens here, from configuration. So does the Hugo configuration: the whole
1517
1531
  * source tree Hugo reads lands under `build/hugo/`, and the consumer's script
1518
- * runs Hugo over it.
1532
+ * runs Hugo over it. With `site.maps` on and GraphViz installed, every place
1533
+ * page with a border or a route carries the map from that place.
1519
1534
  *
1520
1535
  * **The Hugo configuration is generated before anything is written.** Its
1521
1536
  * sources — `package.json`'s `homepage`, the cached navigation, the installed
@@ -1653,10 +1668,17 @@ function siteCommand() {
1653
1668
  return;
1654
1669
  }
1655
1670
 
1671
+ // What drawing the maps found: warnings, every one. A site
1672
+ // never fails for want of a map, so none of these touches the
1673
+ // exit code — GraphViz absent is one line naming what to
1674
+ // install, and every page is written as it would be without.
1675
+ for (const f of result.mapFindings) emitDiagnostic(f);
1676
+
1656
1677
  const s = result.stats;
1657
1678
  log.info(
1658
1679
  `wrote ${s.homepages ?? 0} homepage(s) + ` +
1659
- `${s.content ?? 0} content page(s) to ` +
1680
+ `${s.content ?? 0} content page(s), ` +
1681
+ `${s.maps ?? 0} with a map, to ` +
1660
1682
  `${path.relative(process.cwd(), s.out)}`,
1661
1683
  );
1662
1684
  const { file } = writeHugoConfig(config, hugo);
@@ -772,6 +772,7 @@ const SITE_KEYS = [
772
772
  "notfound",
773
773
  "hugo",
774
774
  "search",
775
+ "maps",
775
776
  ];
776
777
  const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
777
778
  const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
@@ -1711,9 +1712,11 @@ const SITE_INDEX_MESSAGE =
1711
1712
  * shared with the link manifest so the two cannot disagree about where a
1712
1713
  * page is.
1713
1714
  *
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.
1715
+ * Two switches sit here, both on by default. `search` is whether
1716
+ * `package-build site-root` writes a search index beside the rendered pages.
1717
+ * `maps` is whether `content-build site` draws the map from each place that
1718
+ * states a border or a route and writes it into that place's page — a site
1719
+ * carries both unless it says otherwise.
1717
1720
  *
1718
1721
  * **What the site publishes is the homepage and the content tree, and nothing
1719
1722
  * beside them.** A page of documentation is a note — `type: doc`, addressed
@@ -1739,6 +1742,7 @@ function normalizeSite(value) {
1739
1742
  notfound: null,
1740
1743
  hugo: Object.freeze({}),
1741
1744
  search: true,
1745
+ maps: true,
1742
1746
  });
1743
1747
  if (value === undefined) return empty;
1744
1748
  if (!isPlainObject(value)) fail("site", "must be a mapping");
@@ -1795,6 +1799,7 @@ function normalizeSite(value) {
1795
1799
  notfound: normalizeSiteNotfound(input.notfound),
1796
1800
  hugo: normalizeSiteHugo(input.hugo),
1797
1801
  search: optionalBoolean(input.search, "site.search", true),
1802
+ maps: optionalBoolean(input.maps, "site.maps", true),
1798
1803
  });
1799
1804
  }
1800
1805
 
package/docs/api.md CHANGED
@@ -412,6 +412,29 @@ 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.noteState`
416
+
417
+ The three states of a note — **stub**, **draft**, **full** — and the one observable fact they rest on. A note whose body is empty is a stub: it carries its facts into the index and is queryable there, and it has no page, no address and no link target. Nothing is stored: the ladder is derived once in the SQL `entries` view and no note can assert its own state.
418
+
419
+ | Export | Signature | Returns | Use it when |
420
+ | --------------------- | ------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
421
+ | `NOTE_STATES` | `const NOTE_STATES` | `ReadonlyArray<string>` | naming the three states in ladder order |
422
+ | `isEmptyBody` | `isEmptyBody(body)` | `boolean` | asking the exact question: is everything after the closing frontmatter fence whitespace? |
423
+ | `isStubbableType` | `isStubbableType(type, vocabulary)` | `boolean` | asking whether an empty body suppresses this type's page; `folder` and `homepage` are structural and exempt |
424
+ | `isStubNote` | `isStubNote(frontmatter, body, vocabulary)` | `boolean` | deciding whether one note is a stub, which is what the index and the site build both ask |
425
+ | `PLACEHOLDER_PHRASES` | `const PLACEHOLDER_PHRASES` | `ReadonlyArray<string>` | reading the phrases that stand in for prose nobody has written |
426
+ | `placeholderBody` | `placeholderBody(body)` | `Array<{phrase: string, line: number}>` | finding a body that reduces to nothing but placeholders, which is an abandoned draft rather than a stub |
427
+ | `bodyWordCount` | `bodyWordCount(body)` | `number` | counting the prose a reader meets, with a wikilink counting as the one word it renders |
428
+
429
+ ### `engine.stubLint`
430
+
431
+ An empty body must be deliberate. Four rules hold the state honest — a stub states what it is, a stub is not tagged `draft`, a body that renders to nothing is an abandoned draft, and a short unmarked body is asked whether it forgot its marker — and two reports say what the rules cannot decide.
432
+
433
+ | Export | Signature | Returns | Use it when |
434
+ | ------------------ | ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
435
+ | `SHORT_BODY_WORDS` | `const SHORT_BODY_WORDS` | `number` | reading the word count below which an unmarked body is asked whether it is a draft |
436
+ | `lintNoteStates` | `lintNoteStates(contentBase, { records, contentPackage })` | `{findings, counts, byType, oldestDrafts, summary}` | checking a tree's states and counting the ladder, from the corpus the command already derived |
437
+
415
438
  ### `engine.placeRelations`
416
439
 
417
440
  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.
@@ -429,6 +452,42 @@ What a place is next to (`data.borders`) and reachable from (`data.routes`): the
429
452
  | `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
453
  | `checkRoutes` | `checkRoutes(note, { index })` | `object[]` | linting a place's `routes` — as `checkBorders`, plus `mode`, `days` and `terrain`, once per pair per mode |
431
454
 
455
+ ### `engine.holdings`
456
+
457
+ What lies within a place, who holds it, and what an affiliation holds — read off `data.parents` and `data.domains` across this package and every fetched index, inverted into the `contains`, `held_by` and `holdings` lists the site build writes — and the tenure check `content-build lint` runs on a place.
458
+
459
+ | Export | Signature | Returns | Use it when |
460
+ | ---------------------- | ------------------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
461
+ | `HOLDINGS_KEYS` | `const HOLDINGS_KEYS` | `readonly string[]` | the three derived keys — `contains`, `held_by`, `holdings` — which a note cannot author |
462
+ | `HELD_SUBTYPES` | `const HELD_SUBTYPES` | `readonly string[]` | the place subTypes tenure is checked on — `settlement`, `site`, `structure` |
463
+ | `holdingsNode` | `holdingsNode(fm, { title, url })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
464
+ | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
465
+ | `holdingsPages` | `holdingsPages(nodes)` | `Map<string, Holdings>` | inverting `parents` and `domains` into each page's sorted lists, keyed by URL, for every page with at least one entry |
466
+ | `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` or `domains` value names — bare, addressed or wikilinked — empties dropped and repeats collapsed |
467
+ | `checkHeld` | `checkHeld(note, { index })` | `object[]` | linting a place's tenure — a settlement, site or structure no affiliation's `domains` names is a warning at its `type:` line |
468
+
469
+ ### `engine.populations`
470
+
471
+ Whether a world's population figures agree from the region down — the polities and regions inside a place against what it states, a settlement against every place containing it, and a `doc` note's cited figures against the notes that own them. `content-build lint` runs all four as warnings.
472
+
473
+ | Export | Signature | Returns | Use it when |
474
+ | ----------------------- | ---------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
475
+ | `POPULATION_TOLERANCE` | `const POPULATION_TOLERANCE` | `number` | how far a figure written to two significant digits may round below what it contains before that is a finding |
476
+ | `POPULATION_RULES` | `const POPULATION_RULES` | `readonly object[]` | the rules in order, each its `name` — the prefix its findings carry — and what it `describe`s |
477
+ | `checkPopulation` | `checkPopulation(note, { index })` | `object[]` | linting a place's `population` — over-held land, an over-full region, and an oversized settlement |
478
+ | `checkCitedPopulations` | `checkCitedPopulations(note, { index })` | `object[]` | linting a `doc` note's cited figures, each located at the `~figure` that states it |
479
+
480
+ ### `engine.marketClass`
481
+
482
+ What trade a settlement supports (`data.market`): the six-step scale, stated once for the lint, the author-facing reference and the specification, and the check that holds a note's value to it.
483
+
484
+ | Export | Signature | Returns | Use it when |
485
+ | ---------------- | ---------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
486
+ | `MARKET_CLASSES` | `const MARKET_CLASSES` | `readonly object[]` | the scale in order, each step its `value`, the `name` a settlement of it goes by, and its `trade` |
487
+ | `isMarketClass` | `isMarketClass(value)` | `boolean` | asking whether an authored value names a class, a quoted number included |
488
+ | `marketClass` | `marketClass(value)` | `object\|undefined` | the class a value names, for rendering what a number means |
489
+ | `checkMarket` | `checkMarket(note)` | `object[]` | linting a place's `market` — a value off the scale, located at the value that states it |
490
+
432
491
  ### `engine.mapPlaces`
433
492
 
434
493
  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.
@@ -437,6 +496,7 @@ The places a map is drawn from — this package's index records and every fetche
437
496
  | --------------------- | ---------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
438
497
  | `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
498
  | `loadMapWorld` | `loadMapWorld({ config, contentBase, base, problems })` | `MapWorld` | loading the world a map is drawn from — this package's index and every declared dependency's |
499
+ | `mapWorld` | `mapWorld({ records, foreignIndex, contentBase, base, config })` | `MapWorld` | the same world out of records and indexes a caller already holds, as the site build does |
440
500
  | `analyzeContainment` | `analyzeContainment(places, polities)` | `Containment` | finding the world, the continents, each place's continent and the anomalies of `parents` |
441
501
  | `subtreeOf` | `subtreeOf(analysis, root)` | `Set<string>` | every place beneath one, by following `parents` downward |
442
502
  | `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 |
@@ -485,6 +545,7 @@ GraphViz, as the map command reaches it: a build-time tool of that command alone
485
545
  | Export | Signature | Returns | Use it when |
486
546
  | ------------------------ | ------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
487
547
  | `GRAPHVIZ_ENGINES` | `const GRAPHVIZ_ENGINES` | `readonly string[]` | the engines the map command may ask for — `dot`, `twopi`, `neato` |
548
+ | `GRAPHVIZ_INSTALL` | `const GRAPHVIZ_INSTALL` | `string` | what installs GraphViz, as the clause every "not installed" message ends with |
488
549
  | `findGraphviz` | `findGraphviz(engine, { env, fallbacks })` | `string\|undefined` | finding an engine's binary on `PATH` or in the directories a package manager installs into |
489
550
  | `graphvizMissingMessage` | `graphvizMissingMessage(engine)` | `string` | saying an engine is not installed, and what installs it |
490
551
  | `renderDot` | `renderDot(dotPath, outPath, { binary, format, args })` | `{warnings}` | rendering a `.dot` file, the engine's warnings returned and a non-zero exit thrown |
@@ -500,6 +561,16 @@ GraphViz, as the map command reaches it: a build-time tool of that command alone
500
561
  | `relatedPlaces` | `relatedPlaces(places)` | `string[]` | every place that states, or is named in, a border or a route |
501
562
  | `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
563
 
564
+ ### `engine.siteMaps`
565
+
566
+ The map from each place, drawn for the site: `content-build site` draws the map from every place of this package that states, or is named in, a border or a route — with the site's base, so every name links through it — and the page writer lays each drawing beside its page as `from-<shortcode>.svg` and names it in the front matter as `map`. GraphViz absent is one warning and no map; `site.maps: false` draws none.
567
+
568
+ | Export | Signature | Returns | Use it when |
569
+ | -------------- | --------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
570
+ | `SITE_MAP_DIR` | `const SITE_MAP_DIR` | `string` | where the site build draws below a package's root, `build/map/site` |
571
+ | `inlineSvg` | `inlineSvg(svg)` | `string` | rewriting a GraphViz SVG for inlining — no prologue, no comment, no `xlink:`, the root sized by `viewBox` alone |
572
+ | `drawSiteMaps` | `drawSiteMaps({ records, foreignIndex, config, base, locate })` | `{maps, findings}` | drawing the map from every related place of this package, keyed by the URL of the page that carries it |
573
+
503
574
  ### `engine.systems`
504
575
 
505
576
  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.
@@ -615,19 +686,19 @@ Emitting this package's cross-package link manifest. `engine/content-address.mjs
615
686
 
616
687
  Emitting this package's content index. Every content build already walks the whole note tree and parses every note's frontmatter — the pack compilers, the site build, and the content-table expander each do it — and every one of them throws the result away when it finishes. So nothing outside a build can ask a question about the content: "which beings carry no `kbcat`?", "what does this table actually select?", "did that type rename leave anything behind?" have no answer short of writing a throwaway script that re-walks the tree. Eight dead Bestiary tables shipped for weeks behind exactly that gap.
617
688
 
618
- | Export | Signature | Returns | Use it when |
619
- | ----------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
620
- | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
621
- | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
622
- | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
623
- | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
624
- | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
625
- | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
626
- | `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | {{file: string, notes: number, assets: number, records: number, bytes: number}} Where it was written, how many notes and how many assets it holds, how many records that is in all, and its size. | Emit this package's content index. |
627
- | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
628
- | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's), and each asset root yields one per file |
689
+ | Export | Signature | Returns | Use it when |
690
+ | ----------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
691
+ | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
692
+ | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
693
+ | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
694
+ | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
695
+ | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
696
+ | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
697
+ | `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | {{file: string, notes: number, assets: number, records: number, bytes: number, full: number, draft: number, stub: number}} Where it was written, how many notes and how many assets it holds, how many records that is in all, its size, and the ladder those notes sit on. | Emit this package's content index. |
698
+ | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
699
+ | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's), and each asset root yields one per file |
629
700
 
630
- `engine.contentIndex` also re-exports seven names from neighbouring leaf modules, at this same import path:
701
+ `engine.contentIndex` also re-exports eight names from neighbouring leaf modules, at this same import path:
631
702
 
632
703
  | Export | Signature | Returns | Use it when |
633
704
  | --------------------- | -------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -636,6 +707,7 @@ Emitting this package's content index. Every content build already walks the who
636
707
  | `noteFile` | `noteFile(contentBase, record)` | `string` — the note's absolute path | composing an index record's `file.path` (recorded relative, for a byte-stable artifact) back into an openable absolute path |
637
708
  | `authoredFrontmatter` | `authoredFrontmatter(record)` | `Record<string, any>` — the frontmatter without `DERIVED_KEYS` | reading back exactly what a note authored from its index record, so a pass can lint or compile from the index without reasoning about derived fields as if the author wrote them |
638
709
  | `isNoteRecord` | `isNoteRecord(record)` | `boolean` | telling a note's own record apart from its documentation journal's and from an asset's, when enumerating the corpus |
710
+ | `isStub` | `isStub(record)` | `boolean` | telling a stub — a note with an empty body — apart from a note that publishes a page; asked of the absent address, which is the whole signal |
639
711
  | `isAssetRecord` | `isAssetRecord(record)` | `boolean` | telling a record that addresses a file apart from one that addresses a note; the `asset` block is what marks it |
640
712
  | `recordPath` | `recordPath(record)` | `string` — the path the record names inside its package | ordering or reporting across both record shapes: a note names the `.md` it was parsed from, an asset the file it is |
641
713
 
@@ -694,20 +766,20 @@ The toolchain's own content index — the files it ships, addressed. Every other
694
766
 
695
767
  Publishing a content tree as a website. Compiling a content tree into compendium packs is `content-build package compile`. Publishing the _same tree_ as a website was a script each consumer wrote for itself — 473 code lines in `sohl` and 462 in `sohl-thalorna`, 87 of them identical — and the copies drifted in ways neither repository could see. `sohl-thalorna` reimplemented four things this package already exported, not because it needed different behaviour but because its script predates the extraction. That is the failure a command removes: a consumer cannot accidentally reimplement one.
696
768
 
697
- | Export | Signature | Returns | Use it when |
698
- | --------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
699
- | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
700
- | `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
701
- | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage as the package root's `_index.md`. |
702
- | `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
703
- | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
704
- | `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
705
- | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
706
- | `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
707
- | `pageDestination` | `function pageDestination(page)` | — | Where a page is written, relative to the output root. |
708
- | `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
709
- | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
710
- | `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
769
+ | Export | Signature | Returns | Use it when |
770
+ | --------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
771
+ | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
772
+ | `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
773
+ | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage as the package root's `_index.md`. |
774
+ | `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
775
+ | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
776
+ | `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
777
+ | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
778
+ | `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
779
+ | `pageDestination` | `function pageDestination(page, { bundle })` | — | Where a page is written, relative to the output root: flat, or as a leaf bundle when it carries a file beside itself. |
780
+ | `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
781
+ | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
782
+ | `buildSite` | `buildSite({ config, sqlTables, locate })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], mapFindings: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
711
783
 
712
784
  ### `engine.contentLint`
713
785
 
@@ -941,6 +1013,7 @@ What a `[[…]]` **is**, before anything decides where it points. One authored l
941
1013
  | `unlabelledLinkMessage` | `function unlabelledLinkMessage(target)` | {string} The message, unpunctuated at the end as a finding is. | What an author writing an unlabelled link is told, in one place. |
942
1014
  | `LINK_FINDING_REASONS` | `const LINK_FINDING_REASONS` | — | Every way a link can fail, named once for all three resolvers. |
943
1015
  | `unresolvedAddressMessage` | `function unresolvedAddressMessage(target)` | {string} The message, unpunctuated at the end as a finding is. | What an author writing an address that resolves to nothing is told. |
1016
+ | `stubAddressMessage` | `function stubAddressMessage(target, file)` | {string} The message, unpunctuated at the end as a finding is. | What an author linking to a stub is told, naming the file to open. |
944
1017
  | `ambiguousAddressMessage` | `function ambiguousAddressMessage(target, packages` | {string} The message. | What an author writing a short address more than one package publishes is told. |
945
1018
  | `linkFindingMessage` | `linkFindingMessage({ reason, target, packages, anchor })` | {string} The message. | The message for one link finding, whichever resolver found it. |
946
1019
  | `isSamePage` | `isSamePage({ target, anchor })` | {boolean} True when the link is same-page. | Whether a parsed link addresses a section of the page it is written on. |