@heroiclands/package-build 23.12.8 → 24.0.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 (258) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +3 -2
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +293 -24
  5. package/bin/content-commands.mjs +370 -213
  6. package/bin/package-build-preview.mjs +2 -2
  7. package/bin/package-build.mjs +3 -2
  8. package/ci/check-banners.mjs +7 -7
  9. package/config.mjs +21 -6
  10. package/content-config.mjs +199 -80
  11. package/docs/api.md +110 -89
  12. package/docs/authoring/links-and-markup.md +469 -97
  13. package/docs/commands.md +259 -78
  14. package/docs/configuration.md +211 -103
  15. package/docs/content-format.md +4 -4
  16. package/docs/diagnostics.md +44 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/guides/icons.md +1 -1
  19. package/docs/guides/module-project.md +5 -8
  20. package/docs/guides/release-and-deploy.md +2 -2
  21. package/docs/guides/site.md +112 -23
  22. package/docs/guides/system-project.md +1 -1
  23. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  24. package/docs/index.md +1 -0
  25. package/docs/project-setup.md +28 -30
  26. package/docs/reference/format-details.md +677 -319
  27. package/docs/reference/note-types.md +47 -10
  28. package/docs/reference/theme-pages.md +178 -108
  29. package/docs/source-preview.md +16 -2
  30. package/engine/anchors.mjs +2 -2
  31. package/engine/asset-shipping.mjs +22 -10
  32. package/engine/base-compiler.mjs +62 -104
  33. package/engine/changelog-lint.mjs +1 -1
  34. package/engine/code-fences.mjs +3 -3
  35. package/engine/content-address.mjs +1 -1
  36. package/engine/content-alerts.mjs +8 -4
  37. package/engine/content-blocks.mjs +29 -14
  38. package/engine/content-embeds.mjs +1 -1
  39. package/engine/content-expand.mjs +376 -0
  40. package/engine/content-figures.mjs +18 -11
  41. package/engine/content-format.yaml +8 -0
  42. package/engine/content-glyphs.mjs +213 -0
  43. package/engine/content-images.mjs +6 -7
  44. package/engine/content-index.mjs +1 -1
  45. package/engine/content-links.mjs +3 -3
  46. package/engine/content-lint.mjs +1 -1
  47. package/engine/content-render.mjs +453 -0
  48. package/engine/content-scripts.mjs +376 -43
  49. package/engine/content-spans.mjs +6 -3
  50. package/engine/content-styles.mjs +91 -0
  51. package/engine/content-symbols.mjs +186 -0
  52. package/engine/content-verse.mjs +738 -0
  53. package/engine/core-report.mjs +169 -0
  54. package/engine/core-vocabulary.mjs +208 -0
  55. package/engine/dictionary-html.mjs +119 -0
  56. package/engine/dictionary-rows.mjs +15 -4
  57. package/engine/field-reference.mjs +5 -5
  58. package/engine/first-use.mjs +17 -27
  59. package/engine/frontmatter-lint.mjs +1 -1
  60. package/engine/heading-attributes.mjs +89 -0
  61. package/engine/helpers.mjs +61 -101
  62. package/engine/homepage.mjs +17 -29
  63. package/engine/html-minify.mjs +70 -0
  64. package/engine/index.mjs +8 -5
  65. package/engine/key-case-collisions.mjs +136 -0
  66. package/engine/language-data.mjs +203 -6
  67. package/engine/language-rows.mjs +8 -2
  68. package/engine/language-terms.mjs +186 -16
  69. package/engine/language-words.mjs +12 -0
  70. package/engine/logographic.mjs +255 -0
  71. package/engine/map-lexicon.mjs +262 -47
  72. package/engine/map-places.mjs +5 -4
  73. package/engine/markdown-expressions.mjs +0 -2
  74. package/engine/markdownlint-run.mjs +9 -3
  75. package/engine/markup-validation.mjs +21 -0
  76. package/engine/metre-declaration.mjs +134 -0
  77. package/engine/names.mjs +36 -9
  78. package/engine/note-matter.mjs +66 -0
  79. package/engine/note-vocabulary.mjs +187 -3
  80. package/engine/pdf-build.mjs +69 -133
  81. package/engine/pdf-render.mjs +205 -58
  82. package/engine/post-fields.mjs +157 -0
  83. package/engine/preview-index.mjs +130 -0
  84. package/engine/preview-source.mjs +450 -0
  85. package/engine/project-init.mjs +5 -12
  86. package/engine/prose-config.mjs +3 -3
  87. package/engine/read-stdin.mjs +29 -0
  88. package/engine/run-rows.mjs +2 -2
  89. package/engine/run-words.mjs +6 -112
  90. package/engine/script-emphasis-terms.mjs +38 -0
  91. package/engine/script-emphasis.mjs +295 -0
  92. package/engine/script-font-delivery.mjs +4 -4
  93. package/engine/site-build.mjs +260 -315
  94. package/engine/site-config.mjs +26 -238
  95. package/engine/site-files.mjs +175 -0
  96. package/engine/site-index.mjs +9 -4
  97. package/engine/site-preview.mjs +52 -525
  98. package/engine/site-render.mjs +593 -0
  99. package/engine/site-root.mjs +22 -13
  100. package/engine/site-search.mjs +161 -0
  101. package/engine/site-serve.mjs +273 -0
  102. package/engine/site-theme-banner.mjs +154 -0
  103. package/engine/site-theme-urls.mjs +125 -0
  104. package/engine/site-theme.mjs +926 -0
  105. package/engine/sql-tables.mjs +20 -3
  106. package/engine/text-maps.mjs +57 -10
  107. package/engine/typographer.mjs +248 -0
  108. package/engine/verse-metre.mjs +357 -0
  109. package/engine/wikilink-syntax.mjs +29 -47
  110. package/engine/wikilinks.mjs +758 -279
  111. package/engine/word-forms.mjs +364 -0
  112. package/engine/word-rows.mjs +12 -2
  113. package/engine/word-terms.mjs +38 -4
  114. package/manifest.mjs +3 -4
  115. package/package.json +11 -8
  116. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  117. package/site-theme/layouts/base.hbs +31 -0
  118. package/site-theme/layouts/blog-list.hbs +30 -0
  119. package/site-theme/layouts/blog-post.hbs +40 -0
  120. package/site-theme/layouts/home.hbs +35 -0
  121. package/site-theme/layouts/list.hbs +20 -0
  122. package/site-theme/layouts/not-found.hbs +31 -0
  123. package/site-theme/layouts/page.hbs +7 -0
  124. package/site-theme/partials/copy-buttons.hbs +37 -0
  125. package/site-theme/partials/draft-notice.hbs +5 -0
  126. package/site-theme/partials/exturl.hbs +2 -0
  127. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  128. package/site-theme/partials/header.hbs +44 -0
  129. package/site-theme/partials/hero-banner.hbs +11 -0
  130. package/site-theme/partials/infobox-entry.hbs +2 -0
  131. package/site-theme/partials/infobox-section.hbs +31 -0
  132. package/site-theme/partials/infobox-value.hbs +2 -0
  133. package/site-theme/partials/infobox.hbs +18 -0
  134. package/site-theme/partials/list-members.hbs +9 -0
  135. package/site-theme/partials/nav-links.hbs +5 -0
  136. package/site-theme/partials/page-body.hbs +48 -0
  137. package/site-theme/partials/page-list.hbs +14 -0
  138. package/site-theme/partials/related.hbs +24 -0
  139. package/site-theme/partials/search-meta.hbs +6 -0
  140. package/site-theme/partials/search-names.hbs +2 -0
  141. package/site-theme/partials/search.hbs +52 -0
  142. package/site-theme/partials/tags.hbs +4 -0
  143. package/site-theme/partials/toc.hbs +7 -0
  144. package/{hugo-theme → site-theme}/static/css/style.css +106 -14
  145. package/stage.mjs +1 -2
  146. package/types/config.d.mts +8 -4
  147. package/types/content-config.d.mts +15 -15
  148. package/types/engine/asset-shipping.d.mts +5 -2
  149. package/types/engine/base-compiler.d.mts +20 -2
  150. package/types/engine/code-fences.d.mts +3 -3
  151. package/types/engine/content-address.d.mts +1 -1
  152. package/types/engine/content-alerts.d.mts +8 -2
  153. package/types/engine/content-blocks.d.mts +11 -4
  154. package/types/engine/content-expand.d.mts +288 -0
  155. package/types/engine/content-figures.d.mts +11 -3
  156. package/types/engine/content-glyphs.d.mts +85 -0
  157. package/types/engine/content-images.d.mts +6 -7
  158. package/types/engine/content-links.d.mts +1 -1
  159. package/types/engine/content-render.d.mts +199 -0
  160. package/types/engine/content-scripts.d.mts +70 -4
  161. package/types/engine/content-spans.d.mts +1 -1
  162. package/types/engine/content-styles.d.mts +33 -0
  163. package/types/engine/content-symbols.d.mts +35 -0
  164. package/types/engine/content-verse.d.mts +240 -0
  165. package/types/engine/core-report.d.mts +45 -0
  166. package/types/engine/core-vocabulary.d.mts +53 -0
  167. package/types/engine/dictionary-html.d.mts +8 -0
  168. package/types/engine/dictionary-rows.d.mts +10 -0
  169. package/types/engine/field-reference.d.mts +2 -2
  170. package/types/engine/heading-attributes.d.mts +20 -0
  171. package/types/engine/helpers.d.mts +3 -10
  172. package/types/engine/homepage.d.mts +11 -24
  173. package/types/engine/html-minify.d.mts +9 -0
  174. package/types/engine/index.d.mts +2 -1
  175. package/types/engine/key-case-collisions.d.mts +28 -0
  176. package/types/engine/language-data.d.mts +0 -2
  177. package/types/engine/language-terms.d.mts +24 -2
  178. package/types/engine/logographic.d.mts +45 -0
  179. package/types/engine/map-lexicon.d.mts +66 -13
  180. package/types/engine/map-places.d.mts +8 -0
  181. package/types/engine/markdown-expressions.d.mts +0 -2
  182. package/types/engine/metre-declaration.d.mts +39 -0
  183. package/types/engine/names.d.mts +34 -3
  184. package/types/engine/note-matter.d.mts +48 -0
  185. package/types/engine/pdf-render.d.mts +6 -2
  186. package/types/engine/post-fields.d.mts +44 -0
  187. package/types/engine/preview-index.d.mts +72 -0
  188. package/types/engine/preview-source.d.mts +51 -0
  189. package/types/engine/read-stdin.d.mts +11 -0
  190. package/types/engine/run-rows.d.mts +2 -2
  191. package/types/engine/run-words.d.mts +0 -15
  192. package/types/engine/script-emphasis-terms.d.mts +21 -0
  193. package/types/engine/script-emphasis.d.mts +84 -0
  194. package/types/engine/script-font-delivery.d.mts +4 -4
  195. package/types/engine/site-build.d.mts +71 -113
  196. package/types/engine/site-config.d.mts +14 -134
  197. package/types/engine/site-files.d.mts +73 -0
  198. package/types/engine/site-index.d.mts +11 -3
  199. package/types/engine/site-preview.d.mts +15 -19
  200. package/types/engine/site-render.d.mts +171 -0
  201. package/types/engine/site-root.d.mts +12 -4
  202. package/types/engine/site-search.d.mts +54 -0
  203. package/types/engine/site-serve.d.mts +44 -0
  204. package/types/engine/site-theme-banner.d.mts +51 -0
  205. package/types/engine/site-theme-urls.d.mts +60 -0
  206. package/types/engine/site-theme.d.mts +127 -0
  207. package/types/engine/sql-tables.d.mts +4 -1
  208. package/types/engine/text-maps.d.mts +17 -3
  209. package/types/engine/typographer.d.mts +11 -0
  210. package/types/engine/verse-metre.d.mts +131 -0
  211. package/types/engine/wikilink-syntax.d.mts +30 -48
  212. package/types/engine/wikilinks.d.mts +330 -14
  213. package/types/engine/word-forms.d.mts +83 -0
  214. package/types/manifest.d.mts +3 -4
  215. package/types/stage.d.mts +1 -2
  216. package/types/vendor/markdownlint.d.mts +8 -0
  217. package/vendor/markdownlint.mjs +20388 -0
  218. package/engine/content-poetry.mjs +0 -240
  219. package/engine/web-wikilinks.mjs +0 -594
  220. package/hugo-theme/layouts/404.html +0 -85
  221. package/hugo-theme/layouts/_default/baseof.html +0 -67
  222. package/hugo-theme/layouts/_default/list.html +0 -139
  223. package/hugo-theme/layouts/_default/single.html +0 -4
  224. package/hugo-theme/layouts/blog/list.html +0 -54
  225. package/hugo-theme/layouts/blog-post/single.html +0 -36
  226. package/hugo-theme/layouts/index.html +0 -75
  227. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  228. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  229. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  230. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  231. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  232. package/hugo-theme/layouts/partials/header.html +0 -35
  233. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  234. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  235. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  236. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  237. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  238. package/hugo-theme/layouts/partials/infobox.html +0 -57
  239. package/hugo-theme/layouts/partials/page-body.html +0 -152
  240. package/hugo-theme/layouts/partials/related.html +0 -138
  241. package/hugo-theme/layouts/partials/search.html +0 -84
  242. package/hugo-theme/layouts/partials/site-url.html +0 -32
  243. package/hugo-theme/layouts/partials/tags.html +0 -30
  244. package/hugo-theme/layouts/partials/toc.html +0 -28
  245. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  246. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  247. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  248. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  249. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  250. package/hugo-theme/theme.toml +0 -11
  251. package/types/engine/content-poetry.d.mts +0 -58
  252. package/types/engine/web-wikilinks.d.mts +0 -107
  253. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  254. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  255. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  256. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  257. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  258. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -51,37 +51,30 @@
51
51
  * @module
52
52
  */
53
53
 
54
- import { renderAlerts, scanAlerts } from "./content-alerts.mjs";
54
+ import { scanAlerts } from "./content-alerts.mjs";
55
55
  import { positionOfYamlPath } from "./diagnostics.mjs";
56
- import { decodeNoteAddresses, noteAddressContext, encodeAddresses } from "./note-addresses.mjs";
56
+ import { decodeNoteAddresses, noteAddressContext } from "./note-addresses.mjs";
57
57
  import fs from "node:fs";
58
58
  import path from "node:path";
59
59
  import { createRequire } from "node:module";
60
- import matter from "gray-matter";
60
+ import { readMatter } from "./note-matter.mjs";
61
61
 
62
62
  import { addressSlug } from "./content-address.mjs";
63
- import { protectCode } from "./code-fences.mjs";
64
- import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
63
+ import { expandContent } from "./content-expand.mjs";
65
64
  import { eventNoteIndex } from "./event-fields.mjs";
66
65
  import { expandContentTables } from "./content-tables.mjs";
67
- import { renderSpans, scanSpans } from "./content-spans.mjs";
68
- import { renderRuns, runContext, scanRuns } from "./content-scripts.mjs";
69
- import { renderBlocks, renderWithheldSections, scanBlocks } from "./content-blocks.mjs";
66
+ import { scanSpans } from "./content-spans.mjs";
67
+ import { runContext, scanRuns } from "./content-scripts.mjs";
68
+ import { scanBlocks } from "./content-blocks.mjs";
70
69
  import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
71
- import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
70
+ import { scanFigures } from "./content-figures.mjs";
72
71
  import { footnoteFindings } from "./content-footnotes.mjs";
73
- import { collectAnchors, eventAnchors, markupAnchorFindings } from "./anchors.mjs";
72
+ import { markupAnchorFindings } from "./anchors.mjs";
74
73
  import { checkImages, renderImageFigures } from "./content-images.mjs";
75
74
  import { isAddressTuple } from "./address.mjs";
76
75
  import { assetPathnameKey, pathnameProblem, resolvePathname } from "./pathnames.mjs";
77
- import {
78
- buildSiteIndex,
79
- figureIndexKeys,
80
- resolveCrossNoteFigures,
81
- resolveInfoboxRef,
82
- wikiContext,
83
- } from "./site-index.mjs";
84
- import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
76
+ import { buildSiteIndex, figureIndexKeys, resolveInfoboxRef } from "./site-index.mjs";
77
+ import { frontmatterWikilinks } from "./wikilinks.mjs";
85
78
  import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
86
79
  import { noteInfoboxes } from "./infobox-registry.mjs";
87
80
  import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
@@ -100,21 +93,14 @@ import { isNoteRecord, noteFile } from "./index-records.mjs";
100
93
  // The one statement of what an empty body means, shared with the index.
101
94
  import { isStubNote } from "./note-state.mjs";
102
95
  import { isGmNote } from "./note-vocabulary.mjs";
103
- import { ART_SLOTS, artPathname, assetAddressIndex, pathnameRoles } from "./art-fields.mjs";
96
+ import { ART_SLOTS, artPathname, assetAddressIndex } from "./art-fields.mjs";
104
97
  import { embedRole } from "./content-embeds.mjs";
105
- import {
106
- HOMEPAGE_DESTINATION,
107
- checkHomepageCount,
108
- homepageFrontmatter,
109
- homepageTitle,
110
- isHomepage,
111
- } from "./homepage.mjs";
112
- import { publishesContentPages } from "../content-config.mjs";
113
- import { HUGO_CONTENT } from "./site-config.mjs";
98
+ import { checkHomepageCount, homepageFrontmatter, homepageTitle, isHomepage } from "./homepage.mjs";
99
+ import { publishesContentPages, siteBase } from "../content-config.mjs";
114
100
  import { seeAlsoPages } from "./see-also.mjs";
115
101
  import { homepageLinkTargets, relatedPages } from "./related-pages.mjs";
116
102
  import { drawSiteMaps, inlineDrawingFigure } from "./site-maps.mjs";
117
- import { generatedDrawing, markGenerated, splitGenerated } from "./generated-sections.mjs";
103
+ import { generatedDrawing } from "./generated-sections.mjs";
118
104
  import { IMAGE_PATTERN } from "./content-images.mjs";
119
105
 
120
106
  const require = createRequire(import.meta.url);
@@ -166,7 +152,7 @@ function readNote(file, ctx) {
166
152
  let source;
167
153
  try {
168
154
  source = fs.readFileSync(file, "utf8");
169
- parsed = matter(source);
155
+ parsed = readMatter(source);
170
156
  } catch {
171
157
  return null;
172
158
  }
@@ -196,9 +182,10 @@ function readNote(file, ctx) {
196
182
  *
197
183
  * @param {string} contentBase - Absolute path to the content tree.
198
184
  * @param {object} ctx - `{ packages, contentPackage, skipDirectories, base,
199
- * mount, scheme }`. `contentPackage` is the package a note that declares none
200
- * belongs to; `base` is where the package is served and `mount` is where its
201
- * content tree sits inside it.
185
+ * mount, scheme, only }`. `contentPackage` is the package a note that
186
+ * declares none belongs to; `base` is where the package is served and `mount`
187
+ * is where its content tree sits inside it. `only`, an absolute path, reads
188
+ * that one note and no other.
202
189
  * @returns {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}
203
190
  */
204
191
  export function collectContentPages(contentBase, ctx) {
@@ -227,6 +214,7 @@ export function collectContentPages(contentBase, ctx) {
227
214
  const dates = reckoningContext({ notes: records });
228
215
 
229
216
  for (const file of siteCorpusFiles(contentBase, ctx)) {
217
+ if (ctx.only && file !== ctx.only) continue;
230
218
  const note = readNote(file, ctx);
231
219
  if (!note) continue;
232
220
  applyComputedBeingAge(note.fm, present, dates);
@@ -292,16 +280,43 @@ export function collectContentPages(contentBase, ctx) {
292
280
  // `file.folder`, and the page's `kbfolder`.
293
281
  folder: path.basename(path.dirname(file)),
294
282
  // Every page is addressed by `(type, shortcode)` at the package
295
- // root, which takes no content mount. The file is written
296
- // flat under the mount — see {@link pageDestination} — and the
297
- // front matter carries this `url` so Hugo publishes it at its
298
- // address rather than at its path.
299
- url: `${ctx.base}${slug}/`,
283
+ // root, which takes no content mount.
284
+ //
285
+ // A note `site.sections` names is the page of its section, and is
286
+ // published at the section rather than at its own address.
287
+ url:
288
+ ctx.sectionPages?.has(slug) ?
289
+ `${ctx.base}${ctx.sectionPages.get(slug)}/`
290
+ : `${ctx.base}${slug}/`,
300
291
  });
301
292
  }
302
293
  return { pages, addressFindings, fmLinkFindings };
303
294
  }
304
295
 
296
+ /**
297
+ * Each section `site.sections` names whose page no note in the tree is.
298
+ *
299
+ * The finding names the configuration key rather than a note, since it is the
300
+ * key that has to change; the command locates it in the configuration file.
301
+ *
302
+ * @param {Readonly<Record<string, string>>} sections - `site.sections`.
303
+ * @param {readonly {slug: string}[]} pages - The content pages.
304
+ * @returns {Array<{param: string[], severity: "error", message: string}>} The findings.
305
+ */
306
+ function unmatchedSectionPages(sections, pages) {
307
+ const addresses = new Set(pages.map((page) => page.slug));
308
+ return Object.entries(sections)
309
+ .filter(([, address]) => !addresses.has(address))
310
+ .map(([name, address]) => ({
311
+ param: ["sections", name],
312
+ severity: "error",
313
+ message:
314
+ `\`site.sections.${name}\` is \`${address}\`, and no note in the content ` +
315
+ `tree has that address, so the section has no page to publish at ` +
316
+ `\`/${name}/\``,
317
+ }));
318
+ }
319
+
305
320
  /**
306
321
  * The package's homepage notes — the authored page at `/<contentPackage>/`.
307
322
  *
@@ -347,34 +362,10 @@ export function collectHomepages(contentBase, ctx) {
347
362
  }
348
363
 
349
364
  /**
350
- * Writes each homepage as the package root's `_index.md`.
351
- *
352
- * Its own writer, deliberately small. A homepage is authored markdown published
353
- * verbatim — no table expansion and no link resolution — so routing it through
354
- * {@link renderPages} would give it a pipeline it does not use.
355
- *
356
- * **Verbatim is the answer, not a gap.** A homepage's links could not be
357
- * *resolved* here, so they are **checked** instead:
358
- * {@link auditHomepageLinks} reads the body's markdown links, and reports a
359
- * wikilink on the page rather than resolving one.
365
+ * Each homepage's frontmatter as its page publishes it, and its body.
360
366
  *
361
- * **Its destination is fixed**: {@link HOMEPAGE_DESTINATION}, at the package's
362
- * site root, which Hugo renders as the `home` kind at `/<package>/`. The page
363
- * states no `url` — the home kind has none to state — and the mount below it
364
- * holds pages and nothing else, so this is the only `_index.md` in the tree.
365
- *
366
- * **Verbatim, but not unconnected.** The homepage is a page like any other on
367
- * both sides of the link graph: a content page reaches it through
368
- * `[[homepage-root|Text]]`, and its own markdown links name content pages. So
369
- * it carries the same `related` block every content page does — handed in by
370
- * the caller, since the graph is read off the content render and a tree with
371
- * only a homepage has no such graph.
372
- *
373
- * @param {string} outRoot - The package's site root — the content mount's
374
- * root, `build/hugo/content`, one level above the mount itself.
375
367
  * @param {readonly object[]} pages - From {@link collectHomepages}.
376
- * @param {object} config - The resolved configuration, for the package name and
377
- * the default title.
368
+ * @param {object} config - The resolved configuration.
378
369
  * @param {object} [options] - Options.
379
370
  * @param {import("./related-pages.mjs").Related} [options.related] - The
380
371
  * homepage's backlinks and mentions. Absent where nothing connects to it,
@@ -383,15 +374,16 @@ export function collectHomepages(contentBase, ctx) {
383
374
  * address resolves against the images this package ships.
384
375
  * @param {object} [options.foreign] - The dependencies' vendored indexes, so a
385
376
  * `data.banner` may name an image a dependency ships.
386
- * @returns {number} How many pages were written.
377
+ * @returns {Array<{file: string, body: string, data: object}>} One entry per
378
+ * homepage, in walk order.
387
379
  */
388
- export function writeHomepages(outRoot, pages, config, { related, records, foreign } = {}) {
380
+ export function homepageData(pages, config, { related, records, foreign } = {}) {
389
381
  const artIndex = assetAddressIndex(records ?? [], { config, foreign });
390
382
  // A pathname no asset host serves resolves to `null`, which the theme reads
391
383
  // as an unset banner rather than as a URL to nowhere.
392
384
  const webSrc = (src) => resolvePathname(src, config)?.web ?? null;
393
385
  const artSrc = (value, type, accepts) => artPathname(artIndex, value, type, accepts).pathname;
394
- for (const page of pages) {
386
+ return pages.map((page) => {
395
387
  const data = homepageFrontmatter(page.fm, {
396
388
  contentPackage: config.contentPackage,
397
389
  title: homepageTitle(page.fm, config),
@@ -399,11 +391,8 @@ export function writeHomepages(outRoot, pages, config, { related, records, forei
399
391
  });
400
392
  if (isPlainObject(data.data)) data.data = { ...data.data };
401
393
  resolveArtFields(data, webSrc, artSrc);
402
- const dest = path.join(outRoot, HOMEPAGE_DESTINATION);
403
- fs.mkdirSync(path.dirname(dest), { recursive: true });
404
- fs.writeFileSync(dest, matter.stringify(page.body, encodeAddresses(data)));
405
- }
406
- return pages.length;
394
+ return { file: page.file, body: page.body, data };
395
+ });
407
396
  }
408
397
 
409
398
  /**
@@ -442,13 +431,14 @@ export function siteGates(pages, findings, { config, records, sqlTables }) {
442
431
  homepages: [],
443
432
  frontmatterLinks: findings.fmLinkFindings ?? [],
444
433
  addressErrors: findings.addressFindings ?? [],
434
+ sections: findings.sectionFindings ?? [],
445
435
  staleManifests: [],
446
436
  unaddressable: [],
447
437
  index: null,
448
438
  foreign: null,
449
439
  manifests: null,
450
440
  };
451
- if (out.frontmatterLinks.length || out.addressErrors.length) return out;
441
+ if (out.frontmatterLinks.length || out.addressErrors.length || out.sections.length) return out;
452
442
 
453
443
  const content = pages.filter((p) => p.kind === "content");
454
444
  // Which packages are *local* is what decides which manifests are foreign,
@@ -503,6 +493,7 @@ export function emptyGates() {
503
493
  homepages: [],
504
494
  frontmatterLinks: [],
505
495
  addressErrors: [],
496
+ sections: [],
506
497
  staleManifests: [],
507
498
  unaddressable: [],
508
499
  index: null,
@@ -514,7 +505,7 @@ export function emptyGates() {
514
505
  /**
515
506
  * Whether any gate produced a finding.
516
507
  * @param {{homepages: unknown[], frontmatterLinks: unknown[],
517
- * addressErrors: unknown[], staleManifests: unknown[],
508
+ * addressErrors: unknown[], sections?: unknown[], staleManifests: unknown[],
518
509
  * unaddressable: unknown[]}} gates - Results of the site checks.
519
510
  * @returns {boolean} Whether at least one check found an error.
520
511
  */
@@ -523,11 +514,38 @@ export function gatesFailed(gates) {
523
514
  gates.homepages.length ||
524
515
  gates.frontmatterLinks.length ||
525
516
  gates.addressErrors.length ||
517
+ gates.sections?.length ||
526
518
  gates.staleManifests.length ||
527
519
  gates.unaddressable.length,
528
520
  );
529
521
  }
530
522
 
523
+ /**
524
+ * Whether a site build found anything that stops its pages being written: a
525
+ * gate that fired, or a note whose table, expression, block, caption, heading,
526
+ * footnote, link, image or embed is in error. What drawing the maps found does
527
+ * not stop it.
528
+ *
529
+ * @param {object} result - From {@link buildSite}.
530
+ * @returns {boolean} Whether the build failed.
531
+ */
532
+ export function siteBuildFailed(result) {
533
+ return (
534
+ gatesFailed(result.gates) ||
535
+ [
536
+ result.tableErrors,
537
+ result.expressionErrors,
538
+ result.secretErrors,
539
+ result.captionErrors,
540
+ result.headingErrors,
541
+ result.footnoteErrors,
542
+ result.wikiErrors,
543
+ result.imageErrors,
544
+ result.embedErrors,
545
+ ].some((list) => list?.length)
546
+ );
547
+ }
548
+
531
549
  /**
532
550
  * The universe a generated table searches, grouped by package.
533
551
  *
@@ -559,39 +577,19 @@ export function tableUniverse(pages) {
559
577
  /**
560
578
  * The frontmatter a page publishes with.
561
579
  *
562
- * An authored `aliases` is a list of names a reader might call the note. Hugo reads
563
- * `aliases` as **URL redirects**, so passing them through would publish a
564
- * redirect stub at each name. They are dropped, and this build emits no
565
- * redirects of its own.
566
- *
567
- * A content page states its own **`url`**, which is its address rather than its
568
- * path. It is written flat under the content mount, so Hugo would
569
- * otherwise publish it at `<mount><type>-<shortcode>/` rather than at the
570
- * package-wide address the link manifest records — the same address, one
571
- * segment too deep. So the address is stated and the mount does not reach it.
572
- *
573
- * **It is stated relative to the site root, and so carries no package base**.
574
- * Hugo resolves a `url` against `baseURL`, whose path is already where
575
- * the package is served — a consumer's Hugo site *is* its package — so writing
576
- * `page.url`, which carries the base for every href this build renders, wrote
577
- * that base a second time and published every content page a segment too deep
578
- * (`/sohl/sohl/doc-rulesintro/`). The two are separate quantities: the page
579
- * states `/<slug>/`, and everything that points *at* the page — the address
580
- * index a wikilink resolves through, and the link manifest — composes
581
- * `<base><slug>/`.
580
+ * An authored `aliases` is a list of names a reader might call the note. It is
581
+ * dropped: this build emits no redirects.
582
582
  *
583
583
  * A content page carries the package the build **derived**. No note
584
- * declares one, so the note's frontmatter alone
585
- * would publish a page that does not say which package it belongs to. The
586
- * emitted page is what a theme reads: `breadcrumbs.html` reads
587
- * `.Params.package`. Writing the derived value keeps a page
584
+ * declares one, so the note's frontmatter alone would publish a page that does
585
+ * not say which package it belongs to. Writing the derived value keeps a page
588
586
  * self-describing and makes sweeping the field out of a content tree
589
587
  * output-preserving for a site as it already is for the packs.
590
588
  *
591
589
  * **`related` is derived, never authored.** What links to a page is a fact
592
590
  * about every other page, known only once the whole tree has resolved — see
593
591
  * {@link module:engine/related-pages} — so an authored value is dropped the
594
- * way `aliases` is, and {@link renderPages} writes the derived block once it
592
+ * way `aliases` is, and {@link renderPages} sets the derived block once it
595
593
  * holds the graph.
596
594
  *
597
595
  * @param {object} page - The page.
@@ -605,7 +603,7 @@ export function tableUniverse(pages) {
605
603
  * pathname into the address the website serves. Every artwork field goes
606
604
  * through it, so a page's `img:` and its body images name the same file the
607
605
  * same way.
608
- * @returns {object} The frontmatter to write.
606
+ * @returns {object} The frontmatter.
609
607
  */
610
608
  export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
611
609
  const { fm, slug } = page;
@@ -615,12 +613,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
615
613
  // Spread after the note's own frontmatter. Guarded because
616
614
  // `package: undefined` is not a value YAML can carry.
617
615
  ...(page.pkg ? { package: page.pkg } : {}),
618
- // The address, stated — site-root relative, because Hugo prefixes
619
- // the site's own base to it. `slug` is written beside it
620
- // because it is the last segment of that address and Hugo's own key
621
- // for one; it decides nothing while `url` is present, but a page
622
- // that carried only `url` would report a slug Hugo had inferred
623
- // from the filename.
616
+ // The address, stated site-root relative; `slug` is its last segment.
624
617
  slug,
625
618
  url: `/${slug}/`,
626
619
  title: pageTitle(page),
@@ -692,37 +685,14 @@ function isPlainObject(value) {
692
685
  return typeof value === "object" && value !== null && !Array.isArray(value);
693
686
  }
694
687
 
695
- /**
696
- * Where a page is written, relative to the output root.
697
- *
698
- * **Flat, under the mount, named by its address**. A content page's URL
699
- * is its address — `/<package>/<type>-<shortcode>/` — and the file is named
700
- * the same way, so the two agree. No page creates a directory: a section is
701
- * a Hugo listing over a content directory, the `section` kind is disabled on
702
- * every site, and a directory chosen only to satisfy a rendering engine's idea
703
- * of what a section is has no business in the note format.
704
- *
705
- * The name is the *whole* address, so two types cannot fight over one file: a
706
- * `doc` note's `subType` may be spelled the same as another note's `type`, and
707
- * `doc-gear.md` and `weapongear-gear.md` are distinct.
708
- *
709
- * @param {{slug: string}} page - The page.
710
- * @returns {string} The file, relative to the mount.
711
- */
712
- export function pageDestination(page) {
713
- return `${page.slug}.md`;
714
- }
715
-
716
- /** Transform one content page without writing the site mount. */
688
+ /** Transform one content page. */
717
689
  export function renderSitePage(
718
690
  page,
719
691
  {
720
692
  index,
721
693
  foreign,
722
- universe,
723
694
  pass = {},
724
695
  decorate,
725
- linkable = (d) => Boolean(d.fm.shortcode),
726
696
  sqlTables,
727
697
  config,
728
698
  artIndex,
@@ -732,26 +702,13 @@ export function renderSitePage(
732
702
  runs = runContext(config),
733
703
  },
734
704
  ) {
735
- const tableErrors = [];
736
- const expressionErrors = [];
737
705
  const secretErrors = [];
738
706
  const captionErrors = [];
739
707
  const headingErrors = [];
740
708
  const footnoteErrors = [];
741
- const wikiErrors = [];
742
709
  const imageErrors = [];
743
710
  const embedErrors = [];
744
- const resolved = [];
745
711
  const src = page.relPath ?? page.base;
746
- const ctx = wikiContext(index, {
747
- src,
748
- file: page.file,
749
- type: page.fm.type ?? null,
750
- errors: wikiErrors,
751
- foreignIndex: foreign.index,
752
- assets: artIndex,
753
- resolved,
754
- });
755
712
 
756
713
  const seenImages = new Map();
757
714
  const webSrc = (src) => {
@@ -777,24 +734,10 @@ export function renderSitePage(
777
734
  return src;
778
735
  };
779
736
  const artSrc = (value, type, accepts) => artPathname(artIndex, value, type, accepts).pathname;
780
- // The role lookup a captioned item's `map` counter reaches through —
781
- // see `engine/content-figures.mjs`'s `resolveRole`. Addressed form, for
782
- // the page's own scan below, which runs before an embed is rewritten into
783
- // an ordinary image; `roleByWebSrc` is the same lookup keyed by the web
784
- // address each picture that reaches `renderImageFigures` resolves to,
785
- // recorded as it is resolved rather than built across the whole corpus —
786
- // `webSrc` itself reports an image's own problems as a side effect, which
787
- // a speculative call over every asset would misfire for one this page
788
- // never names.
737
+ // The role lookup a captioned item's `map` counter reaches through — see
738
+ // `engine/content-figures.mjs`'s `resolveRole`. It numbers the page's
739
+ // figures once, in expansion, and the render below reads that numbering.
789
740
  const resolveRole = (address) => embedRole(artIndex, address);
790
- const pathRoles = pathnameRoles(artIndex);
791
- const roleByWebSrc = new Map();
792
- const webSrcWithRole = (src) => {
793
- const result = webSrc(src);
794
- const role = pathRoles.get(src);
795
- if (role) roleByWebSrc.set(result, role);
796
- return result;
797
- };
798
741
  // The picture's role and pixel size, by the address it resolved to — read
799
742
  // before `webSrc` translates it to the page's own host. A body image is
800
743
  // free to write the bare, own-package form, so the address is normalized
@@ -803,11 +746,6 @@ export function renderSitePage(
803
746
  const key = assetPathnameKey(src, config);
804
747
  return key ? artIndex?.byPath?.get(key) : undefined;
805
748
  };
806
- const resolve = (text) => {
807
- let transformed = pass.beforeLinks ? pass.beforeLinks(text, page) : text;
808
- transformed = resolveWebWikilinks(transformed, ctx);
809
- return renderImageFigures(inlineDrawings(transformed), webSrcWithRole, lookupAsset);
810
- };
811
749
  // A generated **From here** section names the drawing the build made, which
812
750
  // the asset host does not serve: it is set inline, so its place names stay
813
751
  // links. A drawing this build did not make is left for the image pass,
@@ -820,74 +758,34 @@ export function renderSitePage(
820
758
  return inlineDrawingFigure(fs.readFileSync(drawing.file, "utf8"), alt);
821
759
  });
822
760
 
823
- const { markdown, errors, lineMap, generatedFrom } = expandContentTables(page.body, {
824
- docs: universe.get(page.pkg) ?? [],
825
- linkable,
826
- source: src,
827
- sqlTables: sqlTables?.get(page.file),
828
- pageLists: sqlTables?.pageLists?.get(page.file),
829
- self: { fm: searchableFrontmatter(page.fm, page.pkg), path: page.relPath },
830
- });
831
- tableErrors.push(...errors);
832
- const figureScan = scanFigures(markdown, { resolveRole });
833
- ctx.captionLabels = new Map(
834
- figureScan.figures.filter((figure) => figure.id).map((figure) => [figure.id, figure.label]),
835
- );
836
- // What the `ref` expression helper needs beyond the label: the caption
837
- // text and whether one was authored, by the same id `ctx.captionLabels`
838
- // keys on.
839
- const figuresById = new Map(
840
- figureScan.figures
841
- .filter((figure) => figure.id)
842
- .map((figure) => [
843
- figure.id,
844
- { label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
845
- ]),
846
- );
847
- // This page's own anchors, so a `[[#slug]]` self-link is checked against
848
- // what the page actually declares rather than trusted unconditionally.
849
- ctx.anchors = new Set(collectAnchors(markdown).map((anchor) => anchor.slug));
850
- ctx.eventAnchors = new Set(eventAnchors(page.fm).map((anchor) => anchor.slug));
851
- const expressions = renderMarkdownExpressions(markGenerated(markdown, generatedFrom), {
852
- fm: page.fm,
853
- dates: index.dateContext,
854
- file: page.file,
855
- bodyLine: page.bodyLine,
856
- sqlResults: sqlTables?.inline?.get(page.file),
761
+ // The frontmatter's art resolves before the body's pictures, so a picture
762
+ // named in both is counted in that order.
763
+ const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
764
+ const expanded = expandContent(page.body, {
765
+ target: "web",
766
+ index,
767
+ note: { fm: page.fm, file: page.file, source: src, bodyLine: page.bodyLine },
768
+ prepared: {
769
+ sqlTables: sqlTables?.get(page.file),
770
+ pageLists: sqlTables?.pageLists?.get(page.file),
771
+ inline: sqlTables?.inline?.get(page.file),
772
+ },
857
773
  events,
858
- figures: {
859
- get: (id) => figuresById.get(id),
860
- // A cross-note `ref` resolves the written address exactly as a
861
- // wikilink does, against every page's figures scanned before any
862
- // page's expressions render — see `renderPages`.
863
- note: (target) =>
864
- resolveCrossNoteFigures(target, {
865
- contentTypes: index.contentTypes,
866
- packages: index.packages,
867
- noIndexPackages: index.noIndexPackages,
868
- contentPackage: index.contentPackage,
869
- siteIndexMap: index.index,
870
- figuresByAddress,
871
- }),
774
+ resolveRole,
775
+ figuresByAddress,
776
+ links: {
777
+ foreign: foreign.index,
778
+ assets: artIndex,
779
+ beforeLinks: pass.beforeLinks ? (text) => pass.beforeLinks(text, page) : undefined,
780
+ afterLinks: (text) => renderImageFigures(inlineDrawings(text), webSrc, lookupAsset),
872
781
  },
873
782
  });
874
- expressionErrors.push(...expressions.findings);
875
- const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
876
- // The author's text and the generated sections are resolved apart, so the
877
- // links a generated section makes join no page's Related card: that card is
878
- // what the authors link, and the sections already list what they derive.
879
- const { authored, generated } = splitGenerated(expressions.markdown);
880
- const authoredBody = protectCode(authored, resolve);
881
- const authoredLinks = resolved.length;
882
- const resolvedBody =
883
- generated ? `${authoredBody}\n${protectCode(generated, resolve)}` : authoredBody;
884
- const figured = renderFigureBlocks(
885
- renderRuns(resolvedBody, runs, undefined, { target: "site" }).markdown,
886
- undefined,
887
- undefined,
888
- { resolveRole: (address) => roleByWebSrc.get(address) },
889
- );
890
- for (const error of figureScan.errors)
783
+ const { lineMap } = expanded;
784
+ const tableErrors = expanded.findings.tables;
785
+ const expressionErrors = expanded.findings.expressions;
786
+ const wikiErrors = expanded.findings.links;
787
+ const resolvedBody = expanded.markdown;
788
+ for (const error of expanded.findings.captions)
891
789
  captionErrors.push({
892
790
  file: page.file,
893
791
  line: (page.bodyLine ?? 1) + (lineMap[error.line - 1]?.line ?? error.line - 1),
@@ -926,15 +824,15 @@ export function renderSitePage(
926
824
  embedErrors.push(error);
927
825
  return {
928
826
  page,
929
- // The disclosure is written last, over the Markdown the page ships: the
930
- // passes before this one carry line positions into their findings, and a
931
- // line inserted ahead of them would move every one of them.
932
- body: renderWithheldSections(
933
- renderSpans(renderAlerts(renderBlocks(figured.markdown, "web").markdown).markdown)
934
- .markdown,
935
- ),
827
+ // The expanded body and its numbered figures, for the renderer that
828
+ // writes the whole page.
829
+ markdown: resolvedBody,
830
+ figures: expanded.figures.figures,
936
831
  data,
937
- resolved: resolved.slice(0, authoredLinks),
832
+ // The links a generated section makes join no page's Related card:
833
+ // that card is what the authors link, and the sections already list
834
+ // what they derive.
835
+ resolved: expanded.resolved.slice(0, expanded.authoredLinks),
938
836
  tableErrors,
939
837
  expressionErrors,
940
838
  secretErrors,
@@ -948,24 +846,20 @@ export function renderSitePage(
948
846
  }
949
847
 
950
848
  /**
951
- * Renders and writes every page.
952
- *
953
- * The order inside a page is load-bearing and is the same order the pack
954
- * compilers use:
849
+ * Renders every page.
955
850
  *
956
- * 1. **Tables expand first**, and outside code-fence protection. A table is
957
- * authored as a fenced `sql` block, which `protectCode` would otherwise
958
- * stash away before the expander saw it. Expanding first leaves an ordinary
959
- * markdown table to walk, with every other fence still protected.
960
- * 2. **Then, inside protection**: the consumer's `beforeLinks` pass, then
961
- * wikilink resolution. A `{@link}` tag may sit in prose a wikilink also
962
- * touches, so the repository's own rewrite runs before the shared one
963
- * rather than replacing it.
851
+ * Each page's body is expanded by
852
+ * {@link module:engine/content-expand.expandContent}, the step the pack
853
+ * compilers take too: tables first and outside code-fence protection, then
854
+ * figure numbers and expressions, then, inside protection, the consumer's
855
+ * `beforeLinks` pass and wikilink resolution. A `{@link}` tag may sit in prose
856
+ * a wikilink also touches, so the repository's own rewrite runs before the
857
+ * shared one rather than replacing it.
964
858
  *
965
- * **Every page is rendered before any is written.** What links *to* a page is
966
- * known only once every other page has resolved, and it is written into the
967
- * page's own front matter, so the render is one pass over the tree and the
968
- * write is a second — with the link graph read off the first, at the one
859
+ * **Every page is rendered before any is finished.** What links *to* a page is
860
+ * known only once every other page has resolved, and it is set in the page's
861
+ * own front matter, so the render is one pass over the tree and the
862
+ * finishing is a second — with the link graph read off the first, at the one
969
863
  * point the resolver answers, and inverted once between them. Nothing is
970
864
  * resolved twice.
971
865
  *
@@ -982,14 +876,15 @@ export function renderSitePage(
982
876
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
983
877
  * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
984
878
  * related: Map<string, import("./related-pages.mjs").Related>,
985
- * maps: number}} `related`
879
+ * maps: number, pages: object[], runs: object}} `related`
986
880
  * is keyed by page URL, and holds the homepage's block beside every content
987
- * page's, so the caller can write it on the page this render does not.
988
- * `maps` counts the pages carrying a map.
881
+ * page's, so the caller can set it on the page this render does not.
882
+ * `maps` counts the pages carrying a map. `pages` holds
883
+ * `{page, markdown, figures, data}` for each page and `runs` the package's
884
+ * marked-text context.
989
885
  */
990
886
  export function renderPages(pages, options) {
991
887
  const {
992
- outRoot,
993
888
  index,
994
889
  foreign,
995
890
  universe,
@@ -1001,8 +896,6 @@ export function renderPages(pages, options) {
1001
896
  records = [],
1002
897
  homepages = [],
1003
898
  maps = new Map(),
1004
- write = true,
1005
- capture = false,
1006
899
  } = options;
1007
900
 
1008
901
  // The address space the art slots and the body's embeds resolve against:
@@ -1033,7 +926,7 @@ export function renderPages(pages, options) {
1033
926
  self: { fm: searchableFrontmatter(page.fm, page.pkg), path: page.relPath },
1034
927
  });
1035
928
  const byId = new Map(
1036
- scanFigures(markdown)
929
+ scanFigures(markdown, { resolveRole: (address) => embedRole(artIndex, address) })
1037
930
  .figures.filter((figure) => figure.id)
1038
931
  .map((figure) => [
1039
932
  figure.id,
@@ -1075,6 +968,7 @@ export function renderPages(pages, options) {
1075
968
  title: pageTitle(page),
1076
969
  url: page.url,
1077
970
  type: String(page.fm.type),
971
+ ...(page.fm.subType ? { subType: String(page.fm.subType) } : {}),
1078
972
  ...(typeof page.fm.name?.sort === "string" && page.fm.name.sort ?
1079
973
  { sort: page.fm.name.sort }
1080
974
  : {}),
@@ -1087,11 +981,12 @@ export function renderPages(pages, options) {
1087
981
  }
1088
982
  }
1089
983
  // What each page renders as, held until the graph is whole.
1090
- /** @type {Array<{page: object, body: string, data: object}>} */
984
+ /** @type {Array<{page: object, markdown: string, figures: object[], data: object}>} */
1091
985
  const rendered = [];
986
+ const runs = runContext(config, records);
1092
987
 
1093
- for (const page of pages) {
1094
- const result = renderSitePage(page, {
988
+ const renderOne = (page) =>
989
+ renderSitePage(page, {
1095
990
  index,
1096
991
  foreign,
1097
992
  universe,
@@ -1104,8 +999,10 @@ export function renderPages(pages, options) {
1104
999
  figuresByAddress,
1105
1000
  events,
1106
1001
  drawings: maps,
1107
- runs: runContext(config, records),
1002
+ runs,
1108
1003
  });
1004
+ for (const page of pages) {
1005
+ const result = renderOne(page);
1109
1006
  tableErrors.push(...result.tableErrors);
1110
1007
  expressionErrors.push(...result.expressionErrors);
1111
1008
  secretErrors.push(...result.secretErrors);
@@ -1115,7 +1012,12 @@ export function renderPages(pages, options) {
1115
1012
  wikiErrors.push(...result.wikiErrors);
1116
1013
  imageErrors.push(...result.imageErrors);
1117
1014
  embedErrors.push(...result.embedErrors);
1118
- rendered.push({ page, body: result.body, data: result.data });
1015
+ rendered.push({
1016
+ page,
1017
+ markdown: result.markdown,
1018
+ figures: result.figures,
1019
+ data: result.data,
1020
+ });
1119
1021
  for (const hit of result.resolved) if (hit.url) edges.push([page.url, hit.url]);
1120
1022
  }
1121
1023
 
@@ -1125,17 +1027,10 @@ export function renderPages(pages, options) {
1125
1027
  const related = relatedPages(edges, entries, deliberate);
1126
1028
 
1127
1029
  let withMap = 0;
1128
- const outputs = capture === true ? new Map() : null;
1129
- for (const { page, body, data } of rendered) {
1030
+ for (const { page, data } of rendered) {
1130
1031
  const block = related.get(page.url);
1131
1032
  if (block) data.related = block;
1132
1033
  if (/\{#fromhere\}/.test(sqlTables?.get(page.file)?.generated ?? "")) withMap += 1;
1133
- if (write) {
1134
- const dest = path.join(outRoot, pageDestination(page));
1135
- fs.mkdirSync(path.dirname(dest), { recursive: true });
1136
- fs.writeFileSync(dest, matter.stringify(body, encodeAddresses(data)));
1137
- }
1138
- if (outputs) outputs.set(page.file, { markdown: body, frontmatter: data });
1139
1034
  byKind[page.kind] = (byKind[page.kind] ?? 0) + 1;
1140
1035
  }
1141
1036
 
@@ -1153,7 +1048,21 @@ export function renderPages(pages, options) {
1153
1048
  embedErrors,
1154
1049
  related,
1155
1050
  maps: withMap,
1156
- ...(capture ? { ...(outputs ? { outputs } : {}), edges, entries } : {}),
1051
+ pages: rendered.map(({ page, markdown, figures, data }) => ({
1052
+ page,
1053
+ markdown,
1054
+ figures,
1055
+ data,
1056
+ })),
1057
+ runs,
1058
+ // One page rendered again with everything the whole render resolved
1059
+ // against, its place in the link graph included.
1060
+ renderAgain: (page) => {
1061
+ const result = renderOne(page);
1062
+ const block = related.get(page.url);
1063
+ if (block) result.data.related = block;
1064
+ return result;
1065
+ },
1157
1066
  };
1158
1067
  }
1159
1068
 
@@ -1211,7 +1120,7 @@ export function sitePageDecorator(config, index) {
1211
1120
  }
1212
1121
 
1213
1122
  /**
1214
- * Builds a Hugo content tree from a content tree, and reports what it found.
1123
+ * Collects, checks and renders a content tree's pages, and reports what it found.
1215
1124
  *
1216
1125
  * Returns rather than exits, in every case. A caller — the command, or a test —
1217
1126
  * decides what a finding means; the gates below are grouped so it can report
@@ -1229,8 +1138,13 @@ export function sitePageDecorator(config, index) {
1229
1138
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
1230
1139
  * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
1231
1140
  * mapFindings: object[],
1232
- * manifests: object|null}} `mapFindings` is what drawing the maps found —
1233
- * warnings, never a reason to fail the build.
1141
+ * manifests: object|null, site?: object}} `mapFindings` is what drawing the
1142
+ * maps found — warnings, never a reason to fail the build. `site` holds what a page renderer needs: `base`, `pages` (each content
1143
+ * page's `{page, markdown, figures, data}`), `homepages` (each
1144
+ * `{file, body, data}`), `runs`, `index`, `foreign` and `records`; and
1145
+ * `renderNote(file)`, which reads one content note again and renders it
1146
+ * against the rest of the build, or returns `null` when its address, name
1147
+ * or type changed and the whole site must be built again.
1234
1148
  */
1235
1149
  export function buildSite({ config, sqlTables } = {}) {
1236
1150
  const resolved = config ?? loadPackConfig();
@@ -1244,29 +1158,9 @@ export function buildSite({ config, sqlTables } = {}) {
1244
1158
  // that publishes it, and `prefix` is the content tree's mount within the
1245
1159
  // package — the same `prefix` the link manifest records against, so a page
1246
1160
  // and its manifest entry cannot disagree.
1247
- const base = site.base || `/${resolved.contentPackage}/`;
1161
+ const base = siteBase(resolved);
1248
1162
  const mount = `${base}${scheme.prefix}`;
1249
1163
 
1250
- // The Hugo content tree mirrors that mount: the homepage is written at
1251
- // `<out>/_index.md` and every page under `<out>/<prefix>/`.
1252
- // The root is fixed — `build/hugo/content`, beside the generated
1253
- // `hugo.toml` — and resolved against the repository root for the same
1254
- // reason every configured path is, so the build reads and writes the same
1255
- // places whatever directory it was launched from. It is wiped on every
1256
- // run, which is safe precisely because it is not configurable: nothing an
1257
- // author writes can point it at the working tree.
1258
- const outBase = path.join(resolved.rootDir, HUGO_CONTENT);
1259
- const out =
1260
- publishesContent ?
1261
- path.join(outBase, scheme.prefix.replace(/\/$/, ""))
1262
- // A tree containing only the homepage has no content mount, so its root is
1263
- // the output root.
1264
- : outBase;
1265
- // The homepage publishes at `/<contentPackage>/`, so its file goes at the
1266
- // package's own root — one level above the content mount, and the same
1267
- // directory when the tree contains only a homepage.
1268
- const homeRoot = publishesContent ? outBase : out;
1269
-
1270
1164
  const packages = new Set(site.packages.length ? site.packages : [resolved.contentPackage]);
1271
1165
 
1272
1166
  const ctx = {
@@ -1290,6 +1184,11 @@ export function buildSite({ config, sqlTables } = {}) {
1290
1184
  base,
1291
1185
  mount,
1292
1186
  scheme,
1187
+ // The notes `site.sections` names, by address, each with the section
1188
+ // it is the page of.
1189
+ sectionPages: new Map(
1190
+ Object.entries(site.sections ?? {}).map(([name, address]) => [address, name]),
1191
+ ),
1293
1192
  };
1294
1193
 
1295
1194
  const collected = collectHomepages(resolved.paths.content, ctx);
@@ -1333,10 +1232,6 @@ export function buildSite({ config, sqlTables } = {}) {
1333
1232
  };
1334
1233
  }
1335
1234
 
1336
- // The whole tree is a build artifact, regenerated every run: a page whose
1337
- // note was deleted or moved would otherwise linger and keep publishing.
1338
- fs.rmSync(outBase, { recursive: true, force: true });
1339
-
1340
1235
  // Homepage-only stops here, and stopping is the point: nothing below reads
1341
1236
  // the content tree for pages, so `sohl-kethira-basic` and `harn-adventures`
1342
1237
  // cannot publish one whatever else their `site:` block declares.
@@ -1354,15 +1249,22 @@ export function buildSite({ config, sqlTables } = {}) {
1354
1249
  imageErrors: [],
1355
1250
  embedErrors: [],
1356
1251
  mapFindings: [],
1357
- stats: {
1358
- homepages: writeHomepages(homeRoot, homepages, resolved, { records: ctx.records }),
1359
- out: homeRoot,
1252
+ stats: { homepages: homepages.length },
1253
+ site: {
1254
+ base,
1255
+ pages: [],
1256
+ homepages: homepageData(homepages, resolved, { records: ctx.records }),
1257
+ runs: runContext(resolved, ctx.records),
1258
+ index: null,
1259
+ foreign: null,
1260
+ records: ctx.records,
1261
+ renderNote: () => null,
1360
1262
  },
1361
1263
  };
1362
1264
  }
1363
-
1364
1265
  const content = collectContentPages(resolved.paths.content, ctx);
1365
1266
  const { pages } = content;
1267
+ content.sectionFindings = unmatchedSectionPages(site.sections ?? {}, pages);
1366
1268
 
1367
1269
  // The homepage is **indexed but not rendered**. `[[homepage-root|Text]]`
1368
1270
  // is an ordinary wikilink and has to resolve to the page the build
@@ -1429,7 +1331,6 @@ export function buildSite({ config, sqlTables } = {}) {
1429
1331
  });
1430
1332
 
1431
1333
  const rendered = renderPages(pages, {
1432
- outRoot: out,
1433
1334
  sqlTables,
1434
1335
  config: resolved,
1435
1336
  records: ctx.records,
@@ -1443,14 +1344,14 @@ export function buildSite({ config, sqlTables } = {}) {
1443
1344
  decorate: sitePageDecorator(resolved, gates.index),
1444
1345
  });
1445
1346
 
1446
- // Last, and outside the mount: the package's front page is not part of the
1447
- // content tree it introduces. It publishes at `base`, which is where the
1448
- // graph keys its block.
1449
- const homepagesWritten = writeHomepages(homeRoot, homepages, resolved, {
1347
+ // The package's front page is not part of the content tree it
1348
+ // introduces. It publishes at `base`, which is where the graph keys its
1349
+ // block.
1350
+ const homepageOptions = {
1450
1351
  related: rendered.related.get(base),
1451
1352
  records: ctx.records,
1452
1353
  foreign: gates.foreign,
1453
- });
1354
+ };
1454
1355
 
1455
1356
  return {
1456
1357
  gates,
@@ -1466,9 +1367,53 @@ export function buildSite({ config, sqlTables } = {}) {
1466
1367
  mapFindings: drawn.findings,
1467
1368
  stats: {
1468
1369
  ...rendered.byKind,
1469
- homepages: homepagesWritten,
1370
+ homepages: homepages.length,
1470
1371
  maps: rendered.maps,
1471
- out,
1372
+ },
1373
+ site: {
1374
+ base,
1375
+ pages: rendered.pages,
1376
+ homepages: homepageData(homepages, resolved, homepageOptions),
1377
+ runs: rendered.runs,
1378
+ index: gates.index,
1379
+ foreign: gates.foreign,
1380
+ records: ctx.records,
1381
+ renderNote: (file) => {
1382
+ const before = pages.find((page) => page.file === file);
1383
+ const after = collectContentPages(resolved.paths.content, {
1384
+ ...ctx,
1385
+ only: file,
1386
+ }).pages[0];
1387
+ // A note with a new address, name or type, or a new
1388
+ // note, changes what other pages resolve to, which
1389
+ // only a whole build answers.
1390
+ if (
1391
+ !before ||
1392
+ !after ||
1393
+ after.url !== before.url ||
1394
+ after.name !== before.name ||
1395
+ String(after.fm.type) !== String(before.fm.type)
1396
+ )
1397
+ return null;
1398
+ const result = rendered.renderAgain(after);
1399
+ return {
1400
+ page: after,
1401
+ markdown: result.markdown,
1402
+ figures: result.figures,
1403
+ data: result.data,
1404
+ findings: [
1405
+ ...result.tableErrors,
1406
+ ...result.expressionErrors,
1407
+ ...result.secretErrors,
1408
+ ...result.captionErrors,
1409
+ ...result.headingErrors,
1410
+ ...result.footnoteErrors,
1411
+ ...result.wikiErrors,
1412
+ ...result.imageErrors,
1413
+ ...result.embedErrors,
1414
+ ],
1415
+ };
1416
+ },
1472
1417
  },
1473
1418
  };
1474
1419
  }