@heroiclands/package-build 22.28.0 → 23.1.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 (156) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -1
  3. package/bin/content-commands.mjs +1 -1
  4. package/ci/content-format-reference.mjs +46 -0
  5. package/docs/api.md +125 -142
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/first-note.md +1 -1
  9. package/docs/authoring/frontmatter.md +2 -2
  10. package/docs/authoring/links-and-markup.md +145 -29
  11. package/docs/commands.md +8 -10
  12. package/docs/configuration.md +13 -11
  13. package/docs/content-format.md +3 -3
  14. package/docs/guides/book.md +59 -4
  15. package/docs/guides/icons.md +4 -5
  16. package/docs/guides/packs.md +6 -0
  17. package/docs/guides/reference-pages.md +21 -1
  18. package/docs/guides/release-and-deploy.md +17 -1
  19. package/docs/guides/site.md +17 -18
  20. package/docs/reference/format-details.md +904 -275
  21. package/docs/reference/note-types.md +282 -91
  22. package/docs/reference/theme-pages.md +58 -66
  23. package/engine/address-values.mjs +124 -4
  24. package/engine/address.mjs +44 -3
  25. package/engine/anchors.mjs +103 -3
  26. package/engine/art-slots.mjs +2 -9
  27. package/engine/base-compiler.mjs +25 -6
  28. package/engine/calendar-artifacts.mjs +4 -1
  29. package/engine/calendar-notes.mjs +166 -102
  30. package/engine/content-format-check.mjs +2 -3
  31. package/engine/content-format.mjs +44 -1
  32. package/engine/content-format.yaml +84 -16
  33. package/engine/content-images.mjs +10 -1
  34. package/engine/content-index.mjs +37 -9
  35. package/engine/content-links.mjs +24 -5
  36. package/engine/content-tables.mjs +24 -3
  37. package/engine/data-keys.mjs +347 -0
  38. package/engine/derived-sections.mjs +322 -0
  39. package/engine/diagnostics.mjs +28 -0
  40. package/engine/event-fields.mjs +187 -0
  41. package/engine/event-rows.mjs +126 -0
  42. package/engine/event-views.mjs +305 -0
  43. package/engine/exported-scene.mjs +29 -53
  44. package/engine/frontmatter-lint.mjs +103 -77
  45. package/engine/frontmatter.mjs +7 -2
  46. package/engine/generate.mjs +53 -0
  47. package/engine/generated-sections.mjs +223 -0
  48. package/engine/heading-attributes.mjs +4 -2
  49. package/engine/helpers.mjs +12 -1
  50. package/engine/holdings.mjs +12 -33
  51. package/engine/ids.mjs +17 -11
  52. package/engine/index.mjs +4 -4
  53. package/engine/infobox-render.mjs +21 -6
  54. package/engine/infobox.mjs +217 -99
  55. package/engine/journals.mjs +104 -47
  56. package/engine/literature-notes.mjs +136 -0
  57. package/engine/literature-works.mjs +161 -0
  58. package/engine/map-graphviz.mjs +4 -18
  59. package/engine/markdown-expressions.mjs +116 -11
  60. package/engine/metadata-index.mjs +34 -0
  61. package/engine/near-miss.mjs +90 -0
  62. package/engine/note-addresses.mjs +75 -19
  63. package/engine/note-dates.mjs +5 -3
  64. package/engine/note-event-terms.mjs +364 -0
  65. package/engine/note-events.mjs +823 -0
  66. package/engine/note-vocabulary.mjs +195 -92
  67. package/engine/office-holders.mjs +20 -54
  68. package/engine/packagebuild-index.mjs +16 -1
  69. package/engine/pdf-build.mjs +97 -60
  70. package/engine/pdf-index.mjs +263 -0
  71. package/engine/pdf-render.mjs +47 -5
  72. package/engine/place-relation-terms.mjs +174 -0
  73. package/engine/place-relations.mjs +72 -160
  74. package/engine/rank-ladder.mjs +14 -41
  75. package/engine/reckoning-markers.mjs +2 -0
  76. package/engine/region-events.mjs +6 -12
  77. package/engine/scenes.mjs +73 -468
  78. package/engine/site-build.mjs +74 -85
  79. package/engine/site-index.mjs +3 -1
  80. package/engine/site-maps.mjs +43 -29
  81. package/engine/site-preview.mjs +40 -31
  82. package/engine/social-ties.mjs +2 -1
  83. package/engine/sql-tables.mjs +212 -9
  84. package/engine/standing-terms.mjs +145 -6
  85. package/engine/standings.mjs +37 -49
  86. package/engine/stub-lint.mjs +6 -0
  87. package/engine/web-wikilinks.mjs +21 -1
  88. package/engine/wikilink-syntax.mjs +34 -4
  89. package/engine/wikilinks.mjs +27 -2
  90. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  91. package/hugo-theme/layouts/partials/page-body.html +0 -2
  92. package/hugo-theme/static/css/style.css +25 -55
  93. package/package.json +1 -2
  94. package/packagebuild-metadata.jsonl +13 -13
  95. package/sohl/default-item-art.mjs +6 -6
  96. package/sohl/note-schemas.mjs +8 -73
  97. package/types/engine/address-values.d.mts +50 -3
  98. package/types/engine/address.d.mts +21 -0
  99. package/types/engine/anchors.d.mts +49 -2
  100. package/types/engine/art-slots.d.mts +1 -1
  101. package/types/engine/base-compiler.d.mts +13 -4
  102. package/types/engine/calendar-notes.d.mts +7 -0
  103. package/types/engine/content-format.d.mts +52 -0
  104. package/types/engine/content-images.d.mts +5 -1
  105. package/types/engine/content-tables.d.mts +9 -1
  106. package/types/engine/data-keys.d.mts +145 -0
  107. package/types/engine/derived-sections.d.mts +73 -0
  108. package/types/engine/diagnostics.d.mts +15 -0
  109. package/types/engine/event-fields.d.mts +99 -0
  110. package/types/engine/event-rows.d.mts +12 -0
  111. package/types/engine/event-views.d.mts +67 -0
  112. package/types/engine/exported-scene.d.mts +5 -2
  113. package/types/engine/frontmatter-lint.d.mts +3 -3
  114. package/types/engine/generated-sections.d.mts +143 -0
  115. package/types/engine/holdings.d.mts +6 -20
  116. package/types/engine/ids.d.mts +15 -12
  117. package/types/engine/index.d.mts +1 -1
  118. package/types/engine/infobox.d.mts +13 -3
  119. package/types/engine/journals.d.mts +24 -11
  120. package/types/engine/literature-notes.d.mts +29 -0
  121. package/types/engine/literature-works.d.mts +67 -0
  122. package/types/engine/map-graphviz.d.mts +2 -10
  123. package/types/engine/markdown-expressions.d.mts +12 -3
  124. package/types/engine/near-miss.d.mts +42 -0
  125. package/types/engine/note-addresses.d.mts +7 -1
  126. package/types/engine/note-event-terms.d.mts +68 -0
  127. package/types/engine/note-events.d.mts +71 -0
  128. package/types/engine/note-vocabulary.d.mts +37 -8
  129. package/types/engine/office-holders.d.mts +3 -0
  130. package/types/engine/pdf-index.d.mts +35 -0
  131. package/types/engine/pdf-render.d.mts +8 -2
  132. package/types/engine/place-relation-terms.d.mts +83 -0
  133. package/types/engine/place-relations.d.mts +11 -66
  134. package/types/engine/rank-ladder.d.mts +2 -0
  135. package/types/engine/region-events.d.mts +6 -12
  136. package/types/engine/scenes.d.mts +6 -32
  137. package/types/engine/site-build.d.mts +8 -22
  138. package/types/engine/site-maps.d.mts +15 -7
  139. package/types/engine/sql-tables.d.mts +77 -2
  140. package/types/engine/standing-terms.d.mts +37 -4
  141. package/types/engine/standings.d.mts +14 -9
  142. package/types/engine/wikilink-syntax.d.mts +20 -0
  143. package/types/engine/wikilinks.d.mts +3 -2
  144. package/docs/guides/government-migration.md +0 -52
  145. package/engine/itinerary-scenes.mjs +0 -166
  146. package/engine/lore-events.mjs +0 -197
  147. package/engine/map-notes.mjs +0 -1309
  148. package/engine/map-raster.mjs +0 -51
  149. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  150. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  151. package/hugo-theme/layouts/partials/holdings.html +0 -61
  152. package/hugo-theme/layouts/partials/place-map.html +0 -9
  153. package/types/engine/itinerary-scenes.d.mts +0 -29
  154. package/types/engine/lore-events.d.mts +0 -14
  155. package/types/engine/map-notes.d.mts +0 -297
  156. package/types/engine/map-raster.d.mts +0 -8
@@ -36,9 +36,9 @@
36
36
  * `/<package>/<type>-<shortcode>/`. Nothing is generated between them — no
37
37
  * section directory, no listing, no `_index.md` but the root's. An index of
38
38
  * what the package publishes is a `doc` note carrying a content table, and it
39
- * is authored where every other page is. The one file a page carries beside
40
- * itself is the map from a place — see `engine/site-maps.mjs` — and a page
41
- * that carries one is a leaf bundle at the same address.
39
+ * is authored where every other page is. Every page is one file; the map from a
40
+ * place is set inline in its **From here** section — see
41
+ * `engine/site-maps.mjs`.
42
42
  *
43
43
  * **Every gate reports; none exits.** The integrity checks a site build needs —
44
44
  * a wikilink authored in frontmatter, a name that yields no slug, an unusable
@@ -62,13 +62,14 @@ import matter from "gray-matter";
62
62
  import { addressSlug } from "./content-address.mjs";
63
63
  import { protectCode } from "./code-fences.mjs";
64
64
  import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
65
+ import { eventNoteIndex } from "./event-fields.mjs";
65
66
  import { expandContentTables } from "./content-tables.mjs";
66
67
  import { renderSpans, scanSpans } from "./content-spans.mjs";
67
68
  import { renderBlocks, renderWithheldSections, scanBlocks } from "./content-blocks.mjs";
68
69
  import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
69
70
  import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
70
71
  import { footnoteFindings } from "./content-footnotes.mjs";
71
- import { collectAnchors, markupAnchorFindings } from "./anchors.mjs";
72
+ import { collectAnchors, eventAnchors, markupAnchorFindings } from "./anchors.mjs";
72
73
  import { checkImages, renderImageFigures } from "./content-images.mjs";
73
74
  import { assetPathnameKey, pathnameProblem, resolvePathname } from "./pathnames.mjs";
74
75
  import {
@@ -108,8 +109,9 @@ import {
108
109
  import { publishesContentPages } from "../content-config.mjs";
109
110
  import { HUGO_CONTENT } from "./site-config.mjs";
110
111
  import { homepageLinkTargets, relatedPages } from "./related-pages.mjs";
111
- import { HOLDINGS_KEYS, foreignHoldingsNodes, holdingsNode, holdingsPages } from "./holdings.mjs";
112
- import { drawSiteMaps } from "./site-maps.mjs";
112
+ import { drawSiteMaps, inlineDrawingFigure } from "./site-maps.mjs";
113
+ import { generatedDrawing, markGenerated, splitGenerated } from "./generated-sections.mjs";
114
+ import { IMAGE_PATTERN } from "./content-images.mjs";
113
115
 
114
116
  const require = createRequire(import.meta.url);
115
117
 
@@ -565,10 +567,7 @@ export function tableUniverse(pages) {
565
567
  * about every other page, known only once the whole tree has resolved — see
566
568
  * {@link module:engine/related-pages} — so an authored value is dropped the
567
569
  * way `aliases` is, and {@link renderPages} writes the derived block once it
568
- * holds the graph. Derived geographical and government lists are dropped for the
569
- * same reason — see {@link module:engine/holdings}. **So is `map`**: the
570
- * file a place page names is the one the build drew beside it — see
571
- * {@link module:engine/site-maps} — and a page with no drawing names none.
570
+ * holds the graph.
572
571
  *
573
572
  * @param {object} page - The page.
574
573
  * @param {object} options
@@ -603,11 +602,8 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
603
602
  kbfolder: page.folder,
604
603
  };
605
604
  if (decorate) decorate(data, page);
606
- if (data.type === "affiliation" && data.data) delete data.data.domains;
607
605
  delete data.aliases;
608
606
  delete data.related;
609
- for (const key of HOLDINGS_KEYS) delete data[key];
610
- delete data.map;
611
607
  if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
612
608
  return data;
613
609
  }
@@ -680,20 +676,11 @@ function isPlainObject(value) {
680
676
  * `doc` note's `subType` may be spelled the same as another note's `type`, and
681
677
  * `doc-gear.md` and `weapongear-gear.md` are distinct.
682
678
  *
683
- * **A page that carries a file beside itself is a leaf bundle**, `<slug>/index.md`,
684
- * because that is the one shape under which Hugo hands a page its own
685
- * resources — the map from a place, inlined by the theme through
686
- * `.Resources.Get`. The directory is the page, not a section: it holds no
687
- * `_index.md`, and the page's stated `url` keeps its address exactly where the
688
- * flat file's was.
689
- *
690
679
  * @param {{slug: string}} page - The page.
691
- * @param {object} [opts]
692
- * @param {boolean} [opts.bundle=false] - Whether the page carries a resource.
693
680
  * @returns {string} The file, relative to the mount.
694
681
  */
695
- export function pageDestination(page, { bundle = false } = {}) {
696
- return bundle ? `${page.slug}/index.md` : `${page.slug}.md`;
682
+ export function pageDestination(page) {
683
+ return `${page.slug}.md`;
697
684
  }
698
685
 
699
686
  /** Transform one content page without writing the site mount. */
@@ -710,6 +697,8 @@ export function renderSitePage(
710
697
  config,
711
698
  artIndex,
712
699
  figuresByAddress = new Map(),
700
+ events,
701
+ drawings = new Map(),
713
702
  },
714
703
  ) {
715
704
  const tableErrors = [];
@@ -786,10 +775,21 @@ export function renderSitePage(
786
775
  const resolve = (text) => {
787
776
  let transformed = pass.beforeLinks ? pass.beforeLinks(text, page) : text;
788
777
  transformed = resolveWebWikilinks(transformed, ctx);
789
- return renderImageFigures(transformed, webSrcWithRole, lookupAsset);
778
+ return renderImageFigures(inlineDrawings(transformed), webSrcWithRole, lookupAsset);
790
779
  };
780
+ // A generated **From here** section names the drawing the build made, which
781
+ // the asset host does not serve: it is set inline, so its place names stay
782
+ // links. A drawing this build did not make is left for the image pass,
783
+ // which reports it.
784
+ const inlineDrawings = (text) =>
785
+ text.replace(new RegExp(IMAGE_PATTERN.source, "g"), (whole, alt, src) => {
786
+ const name = generatedDrawing(src);
787
+ const drawing = name ? drawings.get(name) : undefined;
788
+ if (!drawing) return whole;
789
+ return inlineDrawingFigure(fs.readFileSync(drawing.file, "utf8"), alt);
790
+ });
791
791
 
792
- const { markdown, errors, lineMap } = expandContentTables(page.body, {
792
+ const { markdown, errors, lineMap, generatedFrom } = expandContentTables(page.body, {
793
793
  docs: universe.get(page.pkg) ?? [],
794
794
  linkable,
795
795
  source: src,
@@ -816,12 +816,14 @@ export function renderSitePage(
816
816
  // This page's own anchors, so a `[[#slug]]` self-link is checked against
817
817
  // what the page actually declares rather than trusted unconditionally.
818
818
  ctx.anchors = new Set(collectAnchors(markdown).map((anchor) => anchor.slug));
819
- const expressions = renderMarkdownExpressions(markdown, {
819
+ ctx.eventAnchors = new Set(eventAnchors(page.fm).map((anchor) => anchor.slug));
820
+ const expressions = renderMarkdownExpressions(markGenerated(markdown, generatedFrom), {
820
821
  fm: page.fm,
821
822
  dates: index.dateContext,
822
823
  file: page.file,
823
824
  bodyLine: page.bodyLine,
824
825
  sqlResults: sqlTables?.inline?.get(page.file),
826
+ events,
825
827
  figures: {
826
828
  get: (id) => figuresById.get(id),
827
829
  // A cross-note `ref` resolves the written address exactly as a
@@ -840,14 +842,17 @@ export function renderSitePage(
840
842
  });
841
843
  expressionErrors.push(...expressions.findings);
842
844
  const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
843
- const figured = renderFigureBlocks(
844
- protectCode(expressions.markdown, resolve),
845
- undefined,
846
- undefined,
847
- {
848
- resolveRole: (address) => roleByWebSrc.get(address),
849
- },
850
- );
845
+ // The author's text and the generated sections are resolved apart, so the
846
+ // links a generated section makes join no page's Related card: that card is
847
+ // what the authors link, and the sections already list what they derive.
848
+ const { authored, generated } = splitGenerated(expressions.markdown);
849
+ const authoredBody = protectCode(authored, resolve);
850
+ const authoredLinks = resolved.length;
851
+ const resolvedBody =
852
+ generated ? `${authoredBody}\n${protectCode(generated, resolve)}` : authoredBody;
853
+ const figured = renderFigureBlocks(resolvedBody, undefined, undefined, {
854
+ resolveRole: (address) => roleByWebSrc.get(address),
855
+ });
851
856
  for (const error of figureScan.errors)
852
857
  captionErrors.push({
853
858
  file: page.file,
@@ -894,7 +899,7 @@ export function renderSitePage(
894
899
  .markdown,
895
900
  ),
896
901
  data,
897
- resolved,
902
+ resolved: resolved.slice(0, authoredLinks),
898
903
  tableErrors,
899
904
  expressionErrors,
900
905
  secretErrors,
@@ -929,25 +934,23 @@ export function renderSitePage(
929
934
  * point the resolver answers, and inverted once between them. Nothing is
930
935
  * resolved twice.
931
936
  *
932
- * **A map is written with its page.** The drawing from a place is made before
937
+ * **A map is set inline in its page.** The drawing from a place is made before
933
938
  * this render — see {@link module:engine/site-maps} — and handed in keyed by
934
- * page URL; the page that owns one is written as a leaf bundle, the file is
935
- * copied beside it, and the front matter names it as `map`. Every other page
936
- * is written flat, and carries no such key.
939
+ * the file name the page's **From here** section names.
937
940
  *
938
941
  * @param {object[]} pages - Every page.
939
942
  * @param {object} options - Everything the render needs. `homepages` is the
940
943
  * homepage as the index knows it — `{ fm, body, url, title }` — which takes
941
944
  * no part in the render and every part in the graph: a content page links
942
945
  * it by wikilink, and its own markdown links name content pages. `maps` is
943
- * each drawing by the URL of the page that carries it.
946
+ * each drawing by the file name a **From here** section names.
944
947
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
945
948
  * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
946
949
  * related: Map<string, import("./related-pages.mjs").Related>,
947
950
  * maps: number}} `related`
948
951
  * is keyed by page URL, and holds the homepage's block beside every content
949
952
  * page's, so the caller can write it on the page this render does not.
950
- * `maps` counts the pages written with one.
953
+ * `maps` counts the pages carrying a map.
951
954
  */
952
955
  export function renderPages(pages, options) {
953
956
  const {
@@ -1007,6 +1010,14 @@ export function renderPages(pages, options) {
1007
1010
  figuresByAddress.set(key, byId);
1008
1011
  }
1009
1012
 
1013
+ // Every note's events by Address, for an inline reference to an event's
1014
+ // field — the same lookup the Foundry compile and the book build.
1015
+ const events = eventNoteIndex(records, {
1016
+ contentPackage: index?.contentPackage,
1017
+ foreignIndex: foreign?.index,
1018
+ dates: index?.dateContext,
1019
+ });
1020
+
1010
1021
  const tableErrors = [];
1011
1022
  const expressionErrors = [];
1012
1023
  const secretErrors = [];
@@ -1053,6 +1064,8 @@ export function renderPages(pages, options) {
1053
1064
  config,
1054
1065
  artIndex,
1055
1066
  figuresByAddress,
1067
+ events,
1068
+ drawings: maps,
1056
1069
  });
1057
1070
  tableErrors.push(...result.tableErrors);
1058
1071
  expressionErrors.push(...result.expressionErrors);
@@ -1068,42 +1081,16 @@ export function renderPages(pages, options) {
1068
1081
  }
1069
1082
 
1070
1083
  const related = relatedPages(edges, entries);
1071
- // Geography follows parents; governing-body lists follow explicit government
1072
- // references across this package and every fetched index.
1073
- const holdings = holdingsPages(
1074
- [
1075
- ...pages.map((page) =>
1076
- holdingsNode(page.fm, { title: pageTitle(page), url: page.url, package: page.pkg }),
1077
- ),
1078
- ...foreignHoldingsNodes(foreign?.index),
1079
- ],
1080
- {
1081
- governmentNodes: records
1082
- .filter((record) => !isGmNote(record))
1083
- .map((record) =>
1084
- holdingsNode(record, {
1085
- title: record.name?.full ?? record.shortcode,
1086
- package: config?.contentPackage ?? pages[0]?.pkg,
1087
- }),
1088
- ),
1089
- },
1090
- );
1091
1084
 
1092
1085
  let withMap = 0;
1093
1086
  const outputs = capture === true ? new Map() : null;
1094
1087
  for (const { page, body, data } of rendered) {
1095
1088
  const block = related.get(page.url);
1096
1089
  if (block) data.related = block;
1097
- Object.assign(data, holdings.get(page.url));
1098
- const map = maps.get(page.url);
1099
- if (map) {
1100
- data.map = map.name;
1101
- withMap += 1;
1102
- }
1090
+ if (/\{#fromhere\}/.test(sqlTables?.get(page.file)?.generated ?? "")) withMap += 1;
1103
1091
  if (write) {
1104
- const dest = path.join(outRoot, pageDestination(page, { bundle: Boolean(map) }));
1092
+ const dest = path.join(outRoot, pageDestination(page));
1105
1093
  fs.mkdirSync(path.dirname(dest), { recursive: true });
1106
- if (map) fs.copyFileSync(map.file, path.join(path.dirname(dest), map.name));
1107
1094
  fs.writeFileSync(dest, matter.stringify(body, encodeAddresses(data)));
1108
1095
  }
1109
1096
  if (outputs) outputs.set(page.file, { markdown: body, frontmatter: data });
@@ -1166,7 +1153,11 @@ export function resolveSitePass(name, options) {
1166
1153
  export function sitePageDecorator(config, index) {
1167
1154
  const router = routerFor(config);
1168
1155
  return (data, page) => {
1169
- const resolvedDates = resolvedDateFields(page.fm, index.dateContext);
1156
+ // A being's dates reach the page; a note's events reach no page.
1157
+ const { events: _events, ...resolvedDates } = resolvedDateFields(
1158
+ page.fm,
1159
+ index.dateContext,
1160
+ );
1170
1161
  if (Object.keys(resolvedDates).length) data.resolvedDates = resolvedDates;
1171
1162
  data.infoboxes = noteInfoboxes(page.fm, {
1172
1163
  resolve: (ref, hint) => resolveInfoboxRef(index, ref, hint),
@@ -1381,20 +1372,18 @@ export function buildSite({ config, sqlTables } = {}) {
1381
1372
  config: resolved,
1382
1373
  });
1383
1374
 
1384
- // The map from each related place, drawn now that every page is known to
1385
- // be addressable and before any is written, so the page writer lays each
1386
- // drawing beside its page. Drawn with `base`, which is what every name
1387
- // in a drawing links through — the same `<base><slug>/` every other href
1388
- // composes. Off, nothing is drawn and nothing is asked of GraphViz.
1389
- const drawn =
1390
- site.maps ?
1391
- drawSiteMaps({
1392
- records: ctx.records,
1393
- foreignIndex: gates.foreign.index,
1394
- config: resolved,
1395
- base,
1396
- })
1397
- : { maps: new Map(), findings: [] };
1375
+ // The map from each place given a From here section, drawn now that every
1376
+ // page is known to be addressable and before any is written, so the page
1377
+ // writer sets each drawing inline in its section. Drawn with `base`, which
1378
+ // is what every name in a drawing links through — the same `<base><slug>/`
1379
+ // every other href composes. With `site.maps: false` no place is given the
1380
+ // section, nothing is drawn and nothing is asked of GraphViz.
1381
+ const drawn = drawSiteMaps({
1382
+ records: ctx.records,
1383
+ foreignIndex: gates.foreign.index,
1384
+ config: resolved,
1385
+ base,
1386
+ });
1398
1387
 
1399
1388
  const rendered = renderPages(pages, {
1400
1389
  outRoot: out,
@@ -53,7 +53,7 @@ import { buildReferenceTargets } from "./reference-targets.mjs";
53
53
  import { standingsDigest } from "./standings.mjs";
54
54
  // The leaf reader, shared with the content index and the link checker, so a
55
55
  // page's anchors are read once rather than re-derived a third way here.
56
- import { collectAnchors } from "./anchors.mjs";
56
+ import { collectAnchors, eventAnchors } from "./anchors.mjs";
57
57
 
58
58
  import { resolveShortcodeReference } from "./shortcode-references.mjs";
59
59
 
@@ -247,6 +247,8 @@ export function buildSiteIndex(
247
247
  // URL unverified. A Set even when empty: a page that declares no
248
248
  // anchor still has a definite answer for one an author names.
249
249
  anchors: new Set(collectAnchors(e.body ?? "").map((anchor) => anchor.slug)),
250
+ // The note's event ids, which a wikilink may not name.
251
+ eventAnchors: new Set(eventAnchors(e.fm).map((anchor) => anchor.slug)),
250
252
  };
251
253
 
252
254
  const shortcode = e.fm.shortcode;
@@ -12,24 +12,22 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The maps a site build writes into its place pages.
15
+ * The maps a site build draws for its place pages.
16
16
  *
17
- * A place that states, or is named in, a border or a route has a map from it
18
- * — the drawing `package-build map --from` makes — and the site build draws
19
- * that map for every such place of this package and hands each one to the
20
- * page writer, which lays it beside the page as `from-<shortcode>.svg` and
21
- * names it in the front matter as `map`. The theme inlines it, so its place
22
- * names are links to their pages: every drawing is made with the site's base,
23
- * and every `href` in it composes `<base><slug>/` the way every other href
24
- * the build renders does.
17
+ * A place that states, or is named in, a border or a route is given a
18
+ * **From here** section — see {@link module:engine/derived-sections} — naming
19
+ * the drawing `package-build map --from` makes. The site build draws that map
20
+ * for every such place and sets it inline in the section, so its place names
21
+ * are links to their pages: every drawing is made with the site's base, and
22
+ * every `href` in it composes `<base><slug>/` the way every other href the
23
+ * build renders does.
25
24
  *
26
- * The npm Graphviz runtime draws each map. `site.maps: false` says the site
27
- * carries none and skips map rendering.
25
+ * The npm Graphviz runtime draws each map. `site.maps: false` gives no place
26
+ * the section, and nothing is drawn.
28
27
  *
29
28
  * The drawings land under `build/map/site/` — beside the author's own
30
29
  * drawings under `build/map/`, and apart from them, because the site's
31
- * carry links and the author's may not — and are copied from there into the
32
- * content mount. A build never mutates its inputs.
30
+ * carry links and the author's may not. A build never mutates its inputs.
33
31
  *
34
32
  * @module
35
33
  */
@@ -37,7 +35,9 @@
37
35
  import fs from "node:fs";
38
36
  import path from "node:path";
39
37
 
40
- import { buildMaps, relatedPlaces } from "./map-build.mjs";
38
+ import { placesWithMaps } from "./derived-sections.mjs";
39
+ import { fromHereFile } from "./generated-sections.mjs";
40
+ import { buildMaps } from "./map-build.mjs";
41
41
  import { mapWorld } from "./map-places.mjs";
42
42
 
43
43
  /**
@@ -51,8 +51,7 @@ export const SITE_MAP_DIR = "build/map/site";
51
51
  * A drawing as the page writer receives it.
52
52
  *
53
53
  * @typedef {object} SiteMap
54
- * @property {string} name - The file's name in the page's bundle, and the
55
- * value of the page's `map` key: `from-<shortcode>.svg`.
54
+ * @property {string} name - The file's name: `from-<shortcode>.svg`.
56
55
  * @property {string} file - Where the drawing is, absolute.
57
56
  */
58
57
 
@@ -86,7 +85,7 @@ export function inlineSvg(svg) {
86
85
  }
87
86
 
88
87
  /**
89
- * Draw the map from every related place of this package.
88
+ * Draw the map from every place of this package that is given one.
90
89
  *
91
90
  * @param {object} opts
92
91
  * @param {Array<Record<string, any>>} opts.records - The package's index
@@ -98,8 +97,8 @@ export function inlineSvg(svg) {
98
97
  * ending in a slash: what every name in a drawing links through.
99
98
  * @returns {{maps: Map<string, SiteMap>, findings: Array<{file: string,
100
99
  * line?: number, column?: number, severity: "error"|"warning",
101
- * message: string}>}} The drawings, keyed by the URL of the page each
102
- * belongs to, and warnings for relations that name no place.
100
+ * message: string}>}} The drawings, keyed by the file name a **From here**
101
+ * section names, and warnings for relations that name no place.
103
102
  */
104
103
  export function drawSiteMaps({ records, foreignIndex, config, base }) {
105
104
  /** @type {Map<string, SiteMap>} */
@@ -109,6 +108,8 @@ export function drawSiteMaps({ records, foreignIndex, config, base }) {
109
108
  // the map an earlier run drew.
110
109
  fs.rmSync(outDir, { recursive: true, force: true });
111
110
 
111
+ const centres = placesWithMaps(records, { foreignIndex, config });
112
+ if (centres.length === 0) return { maps, findings: [] };
112
113
  const world = mapWorld({
113
114
  records,
114
115
  foreignIndex,
@@ -116,20 +117,33 @@ export function drawSiteMaps({ records, foreignIndex, config, base }) {
116
117
  base,
117
118
  config,
118
119
  });
119
- // This package's related places, which are the ones with a page here to
120
- // carry a map. A dependency's place is drawn on those maps and gets none
121
- // of its own from this build.
122
- const centres = relatedPlaces(world.places).filter((s) => world.places.get(s)?.local);
123
- if (centres.length === 0) return { maps, findings: [] };
124
-
125
120
  const { findings } = buildMaps({ world, outDir, from: centres });
126
121
  for (const centre of centres) {
127
- const place = world.places.get(centre);
128
- if (!place?.url) continue;
129
- const name = `from-${centre}.svg`;
122
+ const name = fromHereFile(centre);
130
123
  const file = path.join(outDir, name);
131
124
  fs.writeFileSync(file, inlineSvg(fs.readFileSync(file, "utf8")));
132
- maps.set(place.url, { name, file });
125
+ maps.set(name, { name, file });
133
126
  }
134
127
  return { maps, findings };
135
128
  }
129
+
130
+ /**
131
+ * The figure the website sets a drawing in: the SVG inline, so its place names
132
+ * stay links, with no blank line inside it — a blank line would end the HTML
133
+ * block and hand the rest of the drawing to the Markdown renderer.
134
+ *
135
+ * @param {string} svg - The drawing, as {@link inlineSvg} wrote it.
136
+ * @param {string} alt - What the drawing shows, for a screen reader.
137
+ * @returns {string} One HTML block.
138
+ */
139
+ export function inlineDrawingFigure(svg, alt) {
140
+ const label = String(alt ?? "").replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
141
+ const body = String(svg)
142
+ .split("\n")
143
+ .filter((line) => line.trim() !== "")
144
+ .join("\n");
145
+ return (
146
+ `<figure class="note-image note-image-full-width" role="img" ` +
147
+ `aria-label="${label}">\n${body}\n</figure>`
148
+ );
149
+ }
@@ -17,17 +17,25 @@ import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
17
17
  import { reckoningContext } from "./reckoning-markers.mjs";
18
18
  import { cachedMetadataIndexes, noContentIndexPackages } from "./metadata-index.mjs";
19
19
  import { buildSiteIndex } from "./site-index.mjs";
20
- import { openNotesDatabase, prepareSqlTables, findSqlBlocks } from "./sql-tables.mjs";
20
+ import {
21
+ attachGeneratedSections,
22
+ findSqlBlocks,
23
+ generatedSectionsFor,
24
+ joinGeneratedSections,
25
+ openNotesDatabase,
26
+ prepareSqlTables,
27
+ } from "./sql-tables.mjs";
21
28
  import { findPageListBlocks } from "./page-lists.mjs";
22
29
  import { isGmNote } from "./note-vocabulary.mjs";
23
30
  import { relatedPages } from "./related-pages.mjs";
24
- import { holdingsNode, holdingsPages, foreignHoldingsNodes } from "./holdings.mjs";
25
31
  import { assetAddressIndex } from "./art-fields.mjs";
32
+ import { eventNoteIndex } from "./event-fields.mjs";
26
33
  import { frontmatterWikilinks } from "./web-wikilinks.mjs";
27
34
  import { positionOfLiteral } from "./diagnostics.mjs";
28
35
  import { addressSlug } from "./content-address.mjs";
29
36
  import { homepageTitle } from "./homepage.mjs";
30
37
  import { loadPackConfig } from "./pack-config.mjs";
38
+ import { drawSiteMaps } from "./site-maps.mjs";
31
39
  import {
32
40
  collectContentPages,
33
41
  collectHomepages,
@@ -97,7 +105,19 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
97
105
  )
98
106
  .map((page) => ({ source: page.file, markdown: page.body }));
99
107
  const publicRecords = records.filter((record) => !isGmNote(record));
108
+ const generated = await generatedSectionsFor(db, publicRecords, contentBase, {
109
+ foreignIndex: gates.foreign.index,
110
+ config,
111
+ });
112
+ joinGeneratedSections(generated, sources);
100
113
  const sqlTables = await prepareSqlTables(db, sources, { records: publicRecords });
114
+ attachGeneratedSections(sqlTables, generated);
115
+ const drawings = drawSiteMaps({
116
+ records,
117
+ foreignIndex: gates.foreign.index,
118
+ config,
119
+ base,
120
+ }).maps;
101
121
  const rendered = renderPages(pages, {
102
122
  index: gates.index,
103
123
  foreign: gates.foreign,
@@ -108,10 +128,11 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
108
128
  config,
109
129
  records,
110
130
  homepages: homeEntries,
131
+ maps: drawings,
111
132
  write: false,
112
133
  capture: "graph",
113
134
  });
114
- return { records, pages, homeEntries, gates, db, rendered };
135
+ return { records, pages, homeEntries, gates, db, rendered, drawings };
115
136
  } catch (error) {
116
137
  await db.close();
117
138
  throw error;
@@ -219,11 +240,16 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
219
240
  },
220
241
  );
221
242
  }
222
- const sqlTables = await prepareSqlTables(
223
- db,
224
- [{ source: absolute, markdown: body }],
225
- { records: records.filter((record) => !isGmNote(record)) },
226
- );
243
+ const publicRecords = records.filter((record) => !isGmNote(record));
244
+ const sources = [{ source: absolute, markdown: body }];
245
+ const generated = await generatedSectionsFor(db, publicRecords, contentBase, {
246
+ only: absolute,
247
+ foreignIndex: snapshot.gates.foreign.index,
248
+ config,
249
+ });
250
+ joinGeneratedSections(generated, sources);
251
+ const sqlTables = await prepareSqlTables(db, sources, { records: publicRecords });
252
+ attachGeneratedSections(sqlTables, generated);
227
253
  const allPages = snapshot.pages.map((item) =>
228
254
  item.file === absolute ? page : item,
229
255
  );
@@ -240,6 +266,12 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
240
266
  foreign: snapshot.gates.foreign,
241
267
  types: index.contentTypes,
242
268
  }),
269
+ events: eventNoteIndex(records, {
270
+ contentPackage: index.contentPackage,
271
+ foreignIndex: snapshot.gates.foreign?.index,
272
+ dates: index.dateContext,
273
+ }),
274
+ drawings: snapshot.drawings,
243
275
  });
244
276
  for (const error of result.tableErrors)
245
277
  findings.push({
@@ -284,29 +316,6 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
284
316
  });
285
317
  const related = relatedPages(edges, entries).get(page.url);
286
318
  if (related) result.data.related = related;
287
- const holdings = holdingsPages(
288
- [
289
- ...allPages.map((item) =>
290
- holdingsNode(item.fm, {
291
- title: item.name,
292
- url: item.url,
293
- package: item.pkg ?? config.contentPackage,
294
- }),
295
- ),
296
- ...foreignHoldingsNodes(snapshot.gates.foreign.index),
297
- ],
298
- {
299
- governmentNodes: records
300
- .filter((record) => !isGmNote(record))
301
- .map((record) =>
302
- holdingsNode(record, {
303
- title: record.name?.full ?? record.shortcode,
304
- package: config.contentPackage,
305
- }),
306
- ),
307
- },
308
- );
309
- Object.assign(result.data, holdings.get(page.url));
310
319
  return {
311
320
  ok: true,
312
321
  markdown: result.body,
@@ -127,7 +127,8 @@ export function checkSocialTies(note, { index } = {}) {
127
127
  });
128
128
  }
129
129
  }
130
- if (!TERMS.has(term))
130
+ // A term that is not text is the inner-key check's finding.
131
+ if ((typeof term === "string" || term === null || term === undefined) && !TERMS.has(term))
131
132
  findings.push({
132
133
  ...at(sourceKey),
133
134
  message: `data.socialTies value ${JSON.stringify(term)} must be one of ${SOCIAL_TIES.map(({ term: value }) => value).join(", ")}`,