@heroiclands/package-build 23.0.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 (153) hide show
  1. package/CHANGELOG.md +58 -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 +124 -141
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/frontmatter.md +2 -2
  9. package/docs/authoring/links-and-markup.md +145 -29
  10. package/docs/commands.md +8 -10
  11. package/docs/configuration.md +13 -11
  12. package/docs/content-format.md +2 -2
  13. package/docs/guides/book.md +59 -4
  14. package/docs/guides/icons.md +4 -5
  15. package/docs/guides/packs.md +6 -0
  16. package/docs/guides/reference-pages.md +21 -1
  17. package/docs/guides/release-and-deploy.md +17 -1
  18. package/docs/guides/site.md +17 -18
  19. package/docs/reference/format-details.md +877 -276
  20. package/docs/reference/note-types.md +278 -91
  21. package/docs/reference/theme-pages.md +58 -97
  22. package/engine/address-values.mjs +124 -4
  23. package/engine/address.mjs +44 -3
  24. package/engine/anchors.mjs +103 -3
  25. package/engine/art-slots.mjs +2 -9
  26. package/engine/base-compiler.mjs +25 -6
  27. package/engine/calendar-artifacts.mjs +4 -1
  28. package/engine/calendar-notes.mjs +166 -102
  29. package/engine/content-format-check.mjs +2 -3
  30. package/engine/content-format.mjs +44 -1
  31. package/engine/content-format.yaml +79 -16
  32. package/engine/content-images.mjs +10 -1
  33. package/engine/content-index.mjs +37 -9
  34. package/engine/content-links.mjs +24 -5
  35. package/engine/content-tables.mjs +24 -3
  36. package/engine/data-keys.mjs +347 -0
  37. package/engine/derived-sections.mjs +322 -0
  38. package/engine/diagnostics.mjs +28 -0
  39. package/engine/event-fields.mjs +187 -0
  40. package/engine/event-rows.mjs +126 -0
  41. package/engine/event-views.mjs +305 -0
  42. package/engine/exported-scene.mjs +29 -53
  43. package/engine/frontmatter-lint.mjs +103 -77
  44. package/engine/frontmatter.mjs +7 -2
  45. package/engine/generate.mjs +53 -0
  46. package/engine/generated-sections.mjs +223 -0
  47. package/engine/heading-attributes.mjs +4 -2
  48. package/engine/helpers.mjs +12 -1
  49. package/engine/holdings.mjs +12 -33
  50. package/engine/ids.mjs +17 -11
  51. package/engine/index.mjs +4 -4
  52. package/engine/infobox-render.mjs +21 -6
  53. package/engine/infobox.mjs +217 -99
  54. package/engine/journals.mjs +104 -47
  55. package/engine/literature-notes.mjs +4 -1
  56. package/engine/literature-works.mjs +21 -13
  57. package/engine/map-graphviz.mjs +4 -18
  58. package/engine/markdown-expressions.mjs +116 -11
  59. package/engine/metadata-index.mjs +26 -0
  60. package/engine/near-miss.mjs +90 -0
  61. package/engine/note-addresses.mjs +75 -19
  62. package/engine/note-dates.mjs +5 -3
  63. package/engine/note-event-terms.mjs +364 -0
  64. package/engine/note-events.mjs +823 -0
  65. package/engine/note-vocabulary.mjs +184 -88
  66. package/engine/office-holders.mjs +20 -54
  67. package/engine/packagebuild-index.mjs +16 -1
  68. package/engine/pdf-build.mjs +97 -60
  69. package/engine/pdf-index.mjs +263 -0
  70. package/engine/pdf-render.mjs +47 -5
  71. package/engine/place-relation-terms.mjs +174 -0
  72. package/engine/place-relations.mjs +72 -160
  73. package/engine/rank-ladder.mjs +14 -41
  74. package/engine/reckoning-markers.mjs +2 -0
  75. package/engine/region-events.mjs +6 -12
  76. package/engine/scenes.mjs +73 -468
  77. package/engine/site-build.mjs +75 -101
  78. package/engine/site-index.mjs +3 -1
  79. package/engine/site-maps.mjs +43 -29
  80. package/engine/site-preview.mjs +40 -31
  81. package/engine/social-ties.mjs +2 -1
  82. package/engine/sql-tables.mjs +212 -9
  83. package/engine/standing-terms.mjs +145 -6
  84. package/engine/standings.mjs +37 -49
  85. package/engine/web-wikilinks.mjs +21 -1
  86. package/engine/wikilink-syntax.mjs +34 -4
  87. package/engine/wikilinks.mjs +27 -2
  88. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  89. package/hugo-theme/layouts/partials/page-body.html +0 -3
  90. package/hugo-theme/static/css/style.css +25 -55
  91. package/package.json +1 -2
  92. package/packagebuild-metadata.jsonl +13 -13
  93. package/sohl/note-schemas.mjs +8 -73
  94. package/types/engine/address-values.d.mts +50 -3
  95. package/types/engine/address.d.mts +21 -0
  96. package/types/engine/anchors.d.mts +49 -2
  97. package/types/engine/art-slots.d.mts +1 -1
  98. package/types/engine/base-compiler.d.mts +13 -4
  99. package/types/engine/calendar-notes.d.mts +7 -0
  100. package/types/engine/content-format.d.mts +52 -0
  101. package/types/engine/content-images.d.mts +5 -1
  102. package/types/engine/content-tables.d.mts +9 -1
  103. package/types/engine/data-keys.d.mts +145 -0
  104. package/types/engine/derived-sections.d.mts +73 -0
  105. package/types/engine/diagnostics.d.mts +15 -0
  106. package/types/engine/event-fields.d.mts +99 -0
  107. package/types/engine/event-rows.d.mts +12 -0
  108. package/types/engine/event-views.d.mts +67 -0
  109. package/types/engine/exported-scene.d.mts +5 -2
  110. package/types/engine/frontmatter-lint.d.mts +3 -3
  111. package/types/engine/generated-sections.d.mts +143 -0
  112. package/types/engine/holdings.d.mts +6 -20
  113. package/types/engine/ids.d.mts +15 -12
  114. package/types/engine/index.d.mts +1 -1
  115. package/types/engine/infobox.d.mts +13 -3
  116. package/types/engine/journals.d.mts +24 -11
  117. package/types/engine/literature-works.d.mts +1 -2
  118. package/types/engine/map-graphviz.d.mts +2 -10
  119. package/types/engine/markdown-expressions.d.mts +12 -3
  120. package/types/engine/near-miss.d.mts +42 -0
  121. package/types/engine/note-addresses.d.mts +7 -1
  122. package/types/engine/note-event-terms.d.mts +68 -0
  123. package/types/engine/note-events.d.mts +71 -0
  124. package/types/engine/note-vocabulary.d.mts +37 -8
  125. package/types/engine/office-holders.d.mts +3 -0
  126. package/types/engine/pdf-index.d.mts +35 -0
  127. package/types/engine/pdf-render.d.mts +8 -2
  128. package/types/engine/place-relation-terms.d.mts +83 -0
  129. package/types/engine/place-relations.d.mts +11 -66
  130. package/types/engine/rank-ladder.d.mts +2 -0
  131. package/types/engine/region-events.d.mts +6 -12
  132. package/types/engine/scenes.d.mts +6 -32
  133. package/types/engine/site-build.d.mts +8 -22
  134. package/types/engine/site-maps.d.mts +15 -7
  135. package/types/engine/sql-tables.d.mts +77 -2
  136. package/types/engine/standing-terms.d.mts +37 -4
  137. package/types/engine/standings.d.mts +14 -9
  138. package/types/engine/wikilink-syntax.d.mts +20 -0
  139. package/types/engine/wikilinks.d.mts +3 -2
  140. package/docs/guides/government-migration.md +0 -52
  141. package/engine/itinerary-scenes.mjs +0 -166
  142. package/engine/lore-events.mjs +0 -197
  143. package/engine/map-notes.mjs +0 -1309
  144. package/engine/map-raster.mjs +0 -51
  145. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  146. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  147. package/hugo-theme/layouts/partials/holdings.html +0 -61
  148. package/hugo-theme/layouts/partials/literature-works.html +0 -29
  149. package/hugo-theme/layouts/partials/place-map.html +0 -9
  150. package/types/engine/itinerary-scenes.d.mts +0 -29
  151. package/types/engine/lore-events.d.mts +0 -14
  152. package/types/engine/map-notes.d.mts +0 -297
  153. 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 {
@@ -95,7 +96,7 @@ import { resolvedDateFields } from "./note-dates.mjs";
95
96
  import { isNoteRecord, noteFile } from "./index-records.mjs";
96
97
  // The one statement of what an empty body means, shared with the index.
97
98
  import { isStubNote } from "./note-state.mjs";
98
- import { NOTE_VOCABULARY, isGmNote } from "./note-vocabulary.mjs";
99
+ import { isGmNote } from "./note-vocabulary.mjs";
99
100
  import { ART_SLOTS, artPathname, assetAddressIndex, pathnameRoles } from "./art-fields.mjs";
100
101
  import { embedRole } from "./content-embeds.mjs";
101
102
  import {
@@ -108,9 +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 { WORKS_KEY, foreignWorksNodes, worksNode, worksPages } from "./literature-works.mjs";
113
- 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";
114
115
 
115
116
  const require = createRequire(import.meta.url);
116
117
 
@@ -566,10 +567,7 @@ export function tableUniverse(pages) {
566
567
  * about every other page, known only once the whole tree has resolved — see
567
568
  * {@link module:engine/related-pages} — so an authored value is dropped the
568
569
  * way `aliases` is, and {@link renderPages} writes the derived block once it
569
- * holds the graph. Derived geographical and government lists are dropped for the
570
- * same reason — see {@link module:engine/holdings}. **So is `map`**: the
571
- * file a place page names is the one the build drew beside it — see
572
- * {@link module:engine/site-maps} — and a page with no drawing names none.
570
+ * holds the graph.
573
571
  *
574
572
  * @param {object} page - The page.
575
573
  * @param {object} options
@@ -604,12 +602,8 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
604
602
  kbfolder: page.folder,
605
603
  };
606
604
  if (decorate) decorate(data, page);
607
- if (data.type === "affiliation" && data.data) delete data.data.domains;
608
605
  delete data.aliases;
609
606
  delete data.related;
610
- for (const key of HOLDINGS_KEYS) delete data[key];
611
- delete data[WORKS_KEY];
612
- delete data.map;
613
607
  if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
614
608
  return data;
615
609
  }
@@ -682,20 +676,11 @@ function isPlainObject(value) {
682
676
  * `doc` note's `subType` may be spelled the same as another note's `type`, and
683
677
  * `doc-gear.md` and `weapongear-gear.md` are distinct.
684
678
  *
685
- * **A page that carries a file beside itself is a leaf bundle**, `<slug>/index.md`,
686
- * because that is the one shape under which Hugo hands a page its own
687
- * resources — the map from a place, inlined by the theme through
688
- * `.Resources.Get`. The directory is the page, not a section: it holds no
689
- * `_index.md`, and the page's stated `url` keeps its address exactly where the
690
- * flat file's was.
691
- *
692
679
  * @param {{slug: string}} page - The page.
693
- * @param {object} [opts]
694
- * @param {boolean} [opts.bundle=false] - Whether the page carries a resource.
695
680
  * @returns {string} The file, relative to the mount.
696
681
  */
697
- export function pageDestination(page, { bundle = false } = {}) {
698
- return bundle ? `${page.slug}/index.md` : `${page.slug}.md`;
682
+ export function pageDestination(page) {
683
+ return `${page.slug}.md`;
699
684
  }
700
685
 
701
686
  /** Transform one content page without writing the site mount. */
@@ -712,6 +697,8 @@ export function renderSitePage(
712
697
  config,
713
698
  artIndex,
714
699
  figuresByAddress = new Map(),
700
+ events,
701
+ drawings = new Map(),
715
702
  },
716
703
  ) {
717
704
  const tableErrors = [];
@@ -788,10 +775,21 @@ export function renderSitePage(
788
775
  const resolve = (text) => {
789
776
  let transformed = pass.beforeLinks ? pass.beforeLinks(text, page) : text;
790
777
  transformed = resolveWebWikilinks(transformed, ctx);
791
- return renderImageFigures(transformed, webSrcWithRole, lookupAsset);
778
+ return renderImageFigures(inlineDrawings(transformed), webSrcWithRole, lookupAsset);
792
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
+ });
793
791
 
794
- const { markdown, errors, lineMap } = expandContentTables(page.body, {
792
+ const { markdown, errors, lineMap, generatedFrom } = expandContentTables(page.body, {
795
793
  docs: universe.get(page.pkg) ?? [],
796
794
  linkable,
797
795
  source: src,
@@ -818,12 +816,14 @@ export function renderSitePage(
818
816
  // This page's own anchors, so a `[[#slug]]` self-link is checked against
819
817
  // what the page actually declares rather than trusted unconditionally.
820
818
  ctx.anchors = new Set(collectAnchors(markdown).map((anchor) => anchor.slug));
821
- const expressions = renderMarkdownExpressions(markdown, {
819
+ ctx.eventAnchors = new Set(eventAnchors(page.fm).map((anchor) => anchor.slug));
820
+ const expressions = renderMarkdownExpressions(markGenerated(markdown, generatedFrom), {
822
821
  fm: page.fm,
823
822
  dates: index.dateContext,
824
823
  file: page.file,
825
824
  bodyLine: page.bodyLine,
826
825
  sqlResults: sqlTables?.inline?.get(page.file),
826
+ events,
827
827
  figures: {
828
828
  get: (id) => figuresById.get(id),
829
829
  // A cross-note `ref` resolves the written address exactly as a
@@ -842,14 +842,17 @@ export function renderSitePage(
842
842
  });
843
843
  expressionErrors.push(...expressions.findings);
844
844
  const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
845
- const figured = renderFigureBlocks(
846
- protectCode(expressions.markdown, resolve),
847
- undefined,
848
- undefined,
849
- {
850
- resolveRole: (address) => roleByWebSrc.get(address),
851
- },
852
- );
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
+ });
853
856
  for (const error of figureScan.errors)
854
857
  captionErrors.push({
855
858
  file: page.file,
@@ -896,7 +899,7 @@ export function renderSitePage(
896
899
  .markdown,
897
900
  ),
898
901
  data,
899
- resolved,
902
+ resolved: resolved.slice(0, authoredLinks),
900
903
  tableErrors,
901
904
  expressionErrors,
902
905
  secretErrors,
@@ -931,25 +934,23 @@ export function renderSitePage(
931
934
  * point the resolver answers, and inverted once between them. Nothing is
932
935
  * resolved twice.
933
936
  *
934
- * **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
935
938
  * this render — see {@link module:engine/site-maps} — and handed in keyed by
936
- * page URL; the page that owns one is written as a leaf bundle, the file is
937
- * copied beside it, and the front matter names it as `map`. Every other page
938
- * is written flat, and carries no such key.
939
+ * the file name the page's **From here** section names.
939
940
  *
940
941
  * @param {object[]} pages - Every page.
941
942
  * @param {object} options - Everything the render needs. `homepages` is the
942
943
  * homepage as the index knows it — `{ fm, body, url, title }` — which takes
943
944
  * no part in the render and every part in the graph: a content page links
944
945
  * it by wikilink, and its own markdown links name content pages. `maps` is
945
- * each drawing by the URL of the page that carries it.
946
+ * each drawing by the file name a **From here** section names.
946
947
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
947
948
  * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
948
949
  * related: Map<string, import("./related-pages.mjs").Related>,
949
950
  * maps: number}} `related`
950
951
  * is keyed by page URL, and holds the homepage's block beside every content
951
952
  * page's, so the caller can write it on the page this render does not.
952
- * `maps` counts the pages written with one.
953
+ * `maps` counts the pages carrying a map.
953
954
  */
954
955
  export function renderPages(pages, options) {
955
956
  const {
@@ -1009,6 +1010,14 @@ export function renderPages(pages, options) {
1009
1010
  figuresByAddress.set(key, byId);
1010
1011
  }
1011
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
+
1012
1021
  const tableErrors = [];
1013
1022
  const expressionErrors = [];
1014
1023
  const secretErrors = [];
@@ -1055,6 +1064,8 @@ export function renderPages(pages, options) {
1055
1064
  config,
1056
1065
  artIndex,
1057
1066
  figuresByAddress,
1067
+ events,
1068
+ drawings: maps,
1058
1069
  });
1059
1070
  tableErrors.push(...result.tableErrors);
1060
1071
  expressionErrors.push(...result.expressionErrors);
@@ -1070,55 +1081,16 @@ export function renderPages(pages, options) {
1070
1081
  }
1071
1082
 
1072
1083
  const related = relatedPages(edges, entries);
1073
- // Geography follows parents; governing-body lists follow explicit government
1074
- // references across this package and every fetched index.
1075
- const holdings = holdingsPages(
1076
- [
1077
- ...pages.map((page) =>
1078
- holdingsNode(page.fm, { title: pageTitle(page), url: page.url, package: page.pkg }),
1079
- ),
1080
- ...foreignHoldingsNodes(foreign?.index),
1081
- ],
1082
- {
1083
- governmentNodes: records
1084
- .filter((record) => !isGmNote(record))
1085
- .map((record) =>
1086
- holdingsNode(record, {
1087
- title: record.name?.full ?? record.shortcode,
1088
- package: config?.contentPackage ?? pages[0]?.pkg,
1089
- }),
1090
- ),
1091
- },
1092
- );
1093
-
1094
- // Each work of literature lists on the pages of the subjects it names,
1095
- // including works a fetched index carries.
1096
- const works = worksPages(
1097
- [
1098
- ...pages.map((page) =>
1099
- worksNode(page.fm, { title: pageTitle(page), url: page.url, package: page.pkg }),
1100
- ),
1101
- ...foreignWorksNodes(foreign?.index),
1102
- ],
1103
- { types: new Set(Object.keys(NOTE_VOCABULARY)) },
1104
- );
1105
1084
 
1106
1085
  let withMap = 0;
1107
1086
  const outputs = capture === true ? new Map() : null;
1108
1087
  for (const { page, body, data } of rendered) {
1109
1088
  const block = related.get(page.url);
1110
1089
  if (block) data.related = block;
1111
- Object.assign(data, holdings.get(page.url));
1112
- Object.assign(data, works.get(page.url));
1113
- const map = maps.get(page.url);
1114
- if (map) {
1115
- data.map = map.name;
1116
- withMap += 1;
1117
- }
1090
+ if (/\{#fromhere\}/.test(sqlTables?.get(page.file)?.generated ?? "")) withMap += 1;
1118
1091
  if (write) {
1119
- const dest = path.join(outRoot, pageDestination(page, { bundle: Boolean(map) }));
1092
+ const dest = path.join(outRoot, pageDestination(page));
1120
1093
  fs.mkdirSync(path.dirname(dest), { recursive: true });
1121
- if (map) fs.copyFileSync(map.file, path.join(path.dirname(dest), map.name));
1122
1094
  fs.writeFileSync(dest, matter.stringify(body, encodeAddresses(data)));
1123
1095
  }
1124
1096
  if (outputs) outputs.set(page.file, { markdown: body, frontmatter: data });
@@ -1181,7 +1153,11 @@ export function resolveSitePass(name, options) {
1181
1153
  export function sitePageDecorator(config, index) {
1182
1154
  const router = routerFor(config);
1183
1155
  return (data, page) => {
1184
- 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
+ );
1185
1161
  if (Object.keys(resolvedDates).length) data.resolvedDates = resolvedDates;
1186
1162
  data.infoboxes = noteInfoboxes(page.fm, {
1187
1163
  resolve: (ref, hint) => resolveInfoboxRef(index, ref, hint),
@@ -1396,20 +1372,18 @@ export function buildSite({ config, sqlTables } = {}) {
1396
1372
  config: resolved,
1397
1373
  });
1398
1374
 
1399
- // The map from each related place, drawn now that every page is known to
1400
- // be addressable and before any is written, so the page writer lays each
1401
- // drawing beside its page. Drawn with `base`, which is what every name
1402
- // in a drawing links through — the same `<base><slug>/` every other href
1403
- // composes. Off, nothing is drawn and nothing is asked of GraphViz.
1404
- const drawn =
1405
- site.maps ?
1406
- drawSiteMaps({
1407
- records: ctx.records,
1408
- foreignIndex: gates.foreign.index,
1409
- config: resolved,
1410
- base,
1411
- })
1412
- : { 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
+ });
1413
1387
 
1414
1388
  const rendered = renderPages(pages, {
1415
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(", ")}`,