@heroiclands/package-build 23.13.0 → 24.0.1

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 (236) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -1
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +150 -1
  5. package/bin/content-commands.mjs +356 -210
  6. package/bin/package-build.mjs +3 -2
  7. package/ci/check-banners.mjs +7 -7
  8. package/config.mjs +21 -6
  9. package/content-config.mjs +199 -80
  10. package/docs/api.md +93 -72
  11. package/docs/authoring/links-and-markup.md +316 -78
  12. package/docs/commands.md +248 -80
  13. package/docs/configuration.md +210 -102
  14. package/docs/content-format.md +2 -2
  15. package/docs/diagnostics.md +2 -2
  16. package/docs/getting-started.md +1 -1
  17. package/docs/guides/icons.md +1 -1
  18. package/docs/guides/module-project.md +5 -8
  19. package/docs/guides/release-and-deploy.md +2 -2
  20. package/docs/guides/site.md +155 -23
  21. package/docs/guides/system-project.md +1 -1
  22. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  23. package/docs/index.md +1 -0
  24. package/docs/project-setup.md +28 -30
  25. package/docs/reference/format-details.md +589 -308
  26. package/docs/reference/note-types.md +40 -10
  27. package/docs/reference/theme-pages.md +127 -108
  28. package/docs/source-preview.md +15 -1
  29. package/engine/asset-shipping.mjs +22 -10
  30. package/engine/base-compiler.mjs +62 -104
  31. package/engine/changelog-lint.mjs +1 -1
  32. package/engine/content-address.mjs +1 -1
  33. package/engine/content-alerts.mjs +8 -4
  34. package/engine/content-blocks.mjs +26 -11
  35. package/engine/content-embeds.mjs +1 -1
  36. package/engine/content-expand.mjs +376 -0
  37. package/engine/content-figures.mjs +13 -6
  38. package/engine/content-format.yaml +8 -0
  39. package/engine/content-glyphs.mjs +213 -0
  40. package/engine/content-images.mjs +6 -7
  41. package/engine/content-links.mjs +3 -3
  42. package/engine/content-lint.mjs +1 -1
  43. package/engine/content-render.mjs +453 -0
  44. package/engine/content-scripts.mjs +326 -27
  45. package/engine/content-spans.mjs +4 -1
  46. package/engine/content-styles.mjs +91 -0
  47. package/engine/content-symbols.mjs +186 -0
  48. package/engine/content-verse.mjs +6 -18
  49. package/engine/core-report.mjs +169 -0
  50. package/engine/core-vocabulary.mjs +208 -0
  51. package/engine/dictionary-html.mjs +119 -0
  52. package/engine/field-reference.mjs +1 -1
  53. package/engine/frontmatter-lint.mjs +1 -1
  54. package/engine/heading-attributes.mjs +89 -0
  55. package/engine/helpers.mjs +61 -101
  56. package/engine/homepage.mjs +17 -29
  57. package/engine/html-minify.mjs +70 -0
  58. package/engine/index.mjs +8 -5
  59. package/engine/language-rows.mjs +8 -2
  60. package/engine/language-terms.mjs +115 -14
  61. package/engine/language-words.mjs +12 -0
  62. package/engine/logographic.mjs +255 -0
  63. package/engine/map-lexicon.mjs +221 -25
  64. package/engine/map-places.mjs +5 -4
  65. package/engine/markdown-expressions.mjs +0 -2
  66. package/engine/markdownlint-run.mjs +9 -3
  67. package/engine/markup-validation.mjs +21 -0
  68. package/engine/metre-declaration.mjs +134 -0
  69. package/engine/note-matter.mjs +2 -2
  70. package/engine/note-vocabulary.mjs +187 -3
  71. package/engine/pdf-build.mjs +68 -133
  72. package/engine/pdf-render.mjs +102 -44
  73. package/engine/post-fields.mjs +157 -0
  74. package/engine/preview-index.mjs +130 -0
  75. package/engine/preview-source.mjs +450 -0
  76. package/engine/project-init.mjs +5 -12
  77. package/engine/prose-config.mjs +3 -3
  78. package/engine/run-words.mjs +3 -2
  79. package/engine/script-emphasis-terms.mjs +38 -0
  80. package/engine/script-emphasis.mjs +295 -0
  81. package/engine/script-font-delivery.mjs +4 -4
  82. package/engine/site-build.mjs +265 -313
  83. package/engine/site-config.mjs +26 -238
  84. package/engine/site-files.mjs +175 -0
  85. package/engine/site-index.mjs +9 -4
  86. package/engine/site-preview.mjs +50 -523
  87. package/engine/site-reach.mjs +213 -0
  88. package/engine/site-render.mjs +668 -0
  89. package/engine/site-root.mjs +90 -39
  90. package/engine/site-search.mjs +161 -0
  91. package/engine/site-serve.mjs +274 -0
  92. package/engine/site-theme-banner.mjs +154 -0
  93. package/engine/site-theme-urls.mjs +125 -0
  94. package/engine/site-theme.mjs +939 -0
  95. package/engine/sql-tables.mjs +9 -6
  96. package/engine/text-maps.mjs +57 -10
  97. package/engine/typographer.mjs +248 -0
  98. package/engine/verse-metre.mjs +357 -0
  99. package/engine/wikilink-syntax.mjs +29 -47
  100. package/engine/wikilinks.mjs +758 -279
  101. package/engine/word-forms.mjs +39 -24
  102. package/engine/word-rows.mjs +12 -2
  103. package/engine/word-terms.mjs +29 -4
  104. package/manifest.mjs +3 -4
  105. package/package.json +10 -7
  106. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  107. package/site-theme/layouts/base.hbs +31 -0
  108. package/site-theme/layouts/blog-list.hbs +30 -0
  109. package/site-theme/layouts/blog-post.hbs +40 -0
  110. package/site-theme/layouts/home.hbs +35 -0
  111. package/site-theme/layouts/list.hbs +20 -0
  112. package/site-theme/layouts/not-found.hbs +31 -0
  113. package/site-theme/layouts/page.hbs +7 -0
  114. package/site-theme/partials/copy-buttons.hbs +37 -0
  115. package/site-theme/partials/draft-notice.hbs +5 -0
  116. package/site-theme/partials/exturl.hbs +2 -0
  117. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  118. package/site-theme/partials/header.hbs +44 -0
  119. package/site-theme/partials/hero-banner.hbs +11 -0
  120. package/site-theme/partials/infobox-entry.hbs +2 -0
  121. package/site-theme/partials/infobox-section.hbs +31 -0
  122. package/site-theme/partials/infobox-value.hbs +2 -0
  123. package/site-theme/partials/infobox.hbs +18 -0
  124. package/site-theme/partials/list-members.hbs +9 -0
  125. package/site-theme/partials/nav-links.hbs +5 -0
  126. package/site-theme/partials/page-body.hbs +48 -0
  127. package/site-theme/partials/page-list.hbs +14 -0
  128. package/site-theme/partials/related.hbs +24 -0
  129. package/site-theme/partials/search-meta.hbs +6 -0
  130. package/site-theme/partials/search-names.hbs +2 -0
  131. package/site-theme/partials/search.hbs +52 -0
  132. package/site-theme/partials/tags.hbs +4 -0
  133. package/site-theme/partials/toc.hbs +7 -0
  134. package/{hugo-theme → site-theme}/static/css/style.css +14 -0
  135. package/stage.mjs +1 -2
  136. package/types/config.d.mts +8 -4
  137. package/types/content-config.d.mts +15 -15
  138. package/types/engine/asset-shipping.d.mts +5 -2
  139. package/types/engine/base-compiler.d.mts +20 -2
  140. package/types/engine/content-address.d.mts +1 -1
  141. package/types/engine/content-alerts.d.mts +8 -2
  142. package/types/engine/content-blocks.d.mts +11 -4
  143. package/types/engine/content-expand.d.mts +288 -0
  144. package/types/engine/content-figures.d.mts +11 -3
  145. package/types/engine/content-glyphs.d.mts +85 -0
  146. package/types/engine/content-images.d.mts +6 -7
  147. package/types/engine/content-links.d.mts +1 -1
  148. package/types/engine/content-render.d.mts +199 -0
  149. package/types/engine/content-scripts.d.mts +62 -0
  150. package/types/engine/content-spans.d.mts +1 -1
  151. package/types/engine/content-styles.d.mts +33 -0
  152. package/types/engine/content-symbols.d.mts +35 -0
  153. package/types/engine/core-report.d.mts +45 -0
  154. package/types/engine/core-vocabulary.d.mts +53 -0
  155. package/types/engine/dictionary-html.d.mts +8 -0
  156. package/types/engine/field-reference.d.mts +1 -1
  157. package/types/engine/heading-attributes.d.mts +20 -0
  158. package/types/engine/helpers.d.mts +3 -10
  159. package/types/engine/homepage.d.mts +11 -24
  160. package/types/engine/html-minify.d.mts +9 -0
  161. package/types/engine/index.d.mts +2 -1
  162. package/types/engine/language-terms.d.mts +24 -2
  163. package/types/engine/logographic.d.mts +45 -0
  164. package/types/engine/map-lexicon.d.mts +48 -8
  165. package/types/engine/map-places.d.mts +8 -0
  166. package/types/engine/markdown-expressions.d.mts +0 -2
  167. package/types/engine/metre-declaration.d.mts +39 -0
  168. package/types/engine/pdf-render.d.mts +6 -2
  169. package/types/engine/post-fields.d.mts +44 -0
  170. package/types/engine/preview-index.d.mts +72 -0
  171. package/types/engine/preview-source.d.mts +51 -0
  172. package/types/engine/script-emphasis-terms.d.mts +21 -0
  173. package/types/engine/script-emphasis.d.mts +84 -0
  174. package/types/engine/script-font-delivery.d.mts +4 -4
  175. package/types/engine/site-build.d.mts +71 -113
  176. package/types/engine/site-config.d.mts +14 -134
  177. package/types/engine/site-files.d.mts +73 -0
  178. package/types/engine/site-index.d.mts +11 -3
  179. package/types/engine/site-preview.d.mts +15 -19
  180. package/types/engine/site-reach.d.mts +76 -0
  181. package/types/engine/site-render.d.mts +173 -0
  182. package/types/engine/site-root.d.mts +14 -4
  183. package/types/engine/site-search.d.mts +54 -0
  184. package/types/engine/site-serve.d.mts +44 -0
  185. package/types/engine/site-theme-banner.d.mts +51 -0
  186. package/types/engine/site-theme-urls.d.mts +60 -0
  187. package/types/engine/site-theme.d.mts +127 -0
  188. package/types/engine/text-maps.d.mts +17 -3
  189. package/types/engine/typographer.d.mts +11 -0
  190. package/types/engine/verse-metre.d.mts +131 -0
  191. package/types/engine/wikilink-syntax.d.mts +30 -48
  192. package/types/engine/wikilinks.d.mts +330 -14
  193. package/types/engine/word-forms.d.mts +3 -2
  194. package/types/manifest.d.mts +3 -4
  195. package/types/stage.d.mts +1 -2
  196. package/types/vendor/markdownlint.d.mts +8 -0
  197. package/vendor/markdownlint.mjs +20388 -0
  198. package/engine/web-wikilinks.mjs +0 -594
  199. package/hugo-theme/layouts/404.html +0 -85
  200. package/hugo-theme/layouts/_default/baseof.html +0 -67
  201. package/hugo-theme/layouts/_default/list.html +0 -139
  202. package/hugo-theme/layouts/_default/single.html +0 -4
  203. package/hugo-theme/layouts/blog/list.html +0 -54
  204. package/hugo-theme/layouts/blog-post/single.html +0 -36
  205. package/hugo-theme/layouts/index.html +0 -75
  206. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  207. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  208. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  209. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  210. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  211. package/hugo-theme/layouts/partials/header.html +0 -35
  212. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  213. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  214. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  215. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  216. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  217. package/hugo-theme/layouts/partials/infobox.html +0 -57
  218. package/hugo-theme/layouts/partials/page-body.html +0 -152
  219. package/hugo-theme/layouts/partials/related.html +0 -138
  220. package/hugo-theme/layouts/partials/search.html +0 -84
  221. package/hugo-theme/layouts/partials/site-url.html +0 -32
  222. package/hugo-theme/layouts/partials/tags.html +0 -30
  223. package/hugo-theme/layouts/partials/toc.html +0 -28
  224. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  225. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  226. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  227. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  228. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  229. package/hugo-theme/theme.toml +0 -11
  230. package/types/engine/web-wikilinks.d.mts +0 -107
  231. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  232. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  233. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  234. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  235. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  236. /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 { readMatter, writeMatter } from "./note-matter.mjs";
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,15 @@ 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";
102
+ import { GENERATED_MARK } from "./site-reach.mjs";
116
103
  import { drawSiteMaps, inlineDrawingFigure } from "./site-maps.mjs";
117
- import { generatedDrawing, markGenerated, splitGenerated } from "./generated-sections.mjs";
104
+ import { generatedDrawing } from "./generated-sections.mjs";
118
105
  import { IMAGE_PATTERN } from "./content-images.mjs";
119
106
 
120
107
  const require = createRequire(import.meta.url);
@@ -196,9 +183,10 @@ function readNote(file, ctx) {
196
183
  *
197
184
  * @param {string} contentBase - Absolute path to the content tree.
198
185
  * @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.
186
+ * mount, scheme, only }`. `contentPackage` is the package a note that
187
+ * declares none belongs to; `base` is where the package is served and `mount`
188
+ * is where its content tree sits inside it. `only`, an absolute path, reads
189
+ * that one note and no other.
202
190
  * @returns {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}
203
191
  */
204
192
  export function collectContentPages(contentBase, ctx) {
@@ -227,6 +215,7 @@ export function collectContentPages(contentBase, ctx) {
227
215
  const dates = reckoningContext({ notes: records });
228
216
 
229
217
  for (const file of siteCorpusFiles(contentBase, ctx)) {
218
+ if (ctx.only && file !== ctx.only) continue;
230
219
  const note = readNote(file, ctx);
231
220
  if (!note) continue;
232
221
  applyComputedBeingAge(note.fm, present, dates);
@@ -292,16 +281,43 @@ export function collectContentPages(contentBase, ctx) {
292
281
  // `file.folder`, and the page's `kbfolder`.
293
282
  folder: path.basename(path.dirname(file)),
294
283
  // 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}/`,
284
+ // root, which takes no content mount.
285
+ //
286
+ // A note `site.sections` names is the page of its section, and is
287
+ // published at the section rather than at its own address.
288
+ url:
289
+ ctx.sectionPages?.has(slug) ?
290
+ `${ctx.base}${ctx.sectionPages.get(slug)}/`
291
+ : `${ctx.base}${slug}/`,
300
292
  });
301
293
  }
302
294
  return { pages, addressFindings, fmLinkFindings };
303
295
  }
304
296
 
297
+ /**
298
+ * Each section `site.sections` names whose page no note in the tree is.
299
+ *
300
+ * The finding names the configuration key rather than a note, since it is the
301
+ * key that has to change; the command locates it in the configuration file.
302
+ *
303
+ * @param {Readonly<Record<string, string>>} sections - `site.sections`.
304
+ * @param {readonly {slug: string}[]} pages - The content pages.
305
+ * @returns {Array<{param: string[], severity: "error", message: string}>} The findings.
306
+ */
307
+ function unmatchedSectionPages(sections, pages) {
308
+ const addresses = new Set(pages.map((page) => page.slug));
309
+ return Object.entries(sections)
310
+ .filter(([, address]) => !addresses.has(address))
311
+ .map(([name, address]) => ({
312
+ param: ["sections", name],
313
+ severity: "error",
314
+ message:
315
+ `\`site.sections.${name}\` is \`${address}\`, and no note in the content ` +
316
+ `tree has that address, so the section has no page to publish at ` +
317
+ `\`/${name}/\``,
318
+ }));
319
+ }
320
+
305
321
  /**
306
322
  * The package's homepage notes — the authored page at `/<contentPackage>/`.
307
323
  *
@@ -347,34 +363,10 @@ export function collectHomepages(contentBase, ctx) {
347
363
  }
348
364
 
349
365
  /**
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.
366
+ * Each homepage's frontmatter as its page publishes it, and its body.
360
367
  *
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
368
  * @param {readonly object[]} pages - From {@link collectHomepages}.
376
- * @param {object} config - The resolved configuration, for the package name and
377
- * the default title.
369
+ * @param {object} config - The resolved configuration.
378
370
  * @param {object} [options] - Options.
379
371
  * @param {import("./related-pages.mjs").Related} [options.related] - The
380
372
  * homepage's backlinks and mentions. Absent where nothing connects to it,
@@ -383,15 +375,16 @@ export function collectHomepages(contentBase, ctx) {
383
375
  * address resolves against the images this package ships.
384
376
  * @param {object} [options.foreign] - The dependencies' vendored indexes, so a
385
377
  * `data.banner` may name an image a dependency ships.
386
- * @returns {number} How many pages were written.
378
+ * @returns {Array<{file: string, body: string, data: object}>} One entry per
379
+ * homepage, in walk order.
387
380
  */
388
- export function writeHomepages(outRoot, pages, config, { related, records, foreign } = {}) {
381
+ export function homepageData(pages, config, { related, records, foreign } = {}) {
389
382
  const artIndex = assetAddressIndex(records ?? [], { config, foreign });
390
383
  // A pathname no asset host serves resolves to `null`, which the theme reads
391
384
  // as an unset banner rather than as a URL to nowhere.
392
385
  const webSrc = (src) => resolvePathname(src, config)?.web ?? null;
393
386
  const artSrc = (value, type, accepts) => artPathname(artIndex, value, type, accepts).pathname;
394
- for (const page of pages) {
387
+ return pages.map((page) => {
395
388
  const data = homepageFrontmatter(page.fm, {
396
389
  contentPackage: config.contentPackage,
397
390
  title: homepageTitle(page.fm, config),
@@ -399,11 +392,8 @@ export function writeHomepages(outRoot, pages, config, { related, records, forei
399
392
  });
400
393
  if (isPlainObject(data.data)) data.data = { ...data.data };
401
394
  resolveArtFields(data, webSrc, artSrc);
402
- const dest = path.join(outRoot, HOMEPAGE_DESTINATION);
403
- fs.mkdirSync(path.dirname(dest), { recursive: true });
404
- fs.writeFileSync(dest, writeMatter(page.body, encodeAddresses(data)));
405
- }
406
- return pages.length;
395
+ return { file: page.file, body: page.body, data };
396
+ });
407
397
  }
408
398
 
409
399
  /**
@@ -442,13 +432,14 @@ export function siteGates(pages, findings, { config, records, sqlTables }) {
442
432
  homepages: [],
443
433
  frontmatterLinks: findings.fmLinkFindings ?? [],
444
434
  addressErrors: findings.addressFindings ?? [],
435
+ sections: findings.sectionFindings ?? [],
445
436
  staleManifests: [],
446
437
  unaddressable: [],
447
438
  index: null,
448
439
  foreign: null,
449
440
  manifests: null,
450
441
  };
451
- if (out.frontmatterLinks.length || out.addressErrors.length) return out;
442
+ if (out.frontmatterLinks.length || out.addressErrors.length || out.sections.length) return out;
452
443
 
453
444
  const content = pages.filter((p) => p.kind === "content");
454
445
  // Which packages are *local* is what decides which manifests are foreign,
@@ -503,6 +494,7 @@ export function emptyGates() {
503
494
  homepages: [],
504
495
  frontmatterLinks: [],
505
496
  addressErrors: [],
497
+ sections: [],
506
498
  staleManifests: [],
507
499
  unaddressable: [],
508
500
  index: null,
@@ -514,7 +506,7 @@ export function emptyGates() {
514
506
  /**
515
507
  * Whether any gate produced a finding.
516
508
  * @param {{homepages: unknown[], frontmatterLinks: unknown[],
517
- * addressErrors: unknown[], staleManifests: unknown[],
509
+ * addressErrors: unknown[], sections?: unknown[], staleManifests: unknown[],
518
510
  * unaddressable: unknown[]}} gates - Results of the site checks.
519
511
  * @returns {boolean} Whether at least one check found an error.
520
512
  */
@@ -523,11 +515,38 @@ export function gatesFailed(gates) {
523
515
  gates.homepages.length ||
524
516
  gates.frontmatterLinks.length ||
525
517
  gates.addressErrors.length ||
518
+ gates.sections?.length ||
526
519
  gates.staleManifests.length ||
527
520
  gates.unaddressable.length,
528
521
  );
529
522
  }
530
523
 
524
+ /**
525
+ * Whether a site build found anything that stops its pages being written: a
526
+ * gate that fired, or a note whose table, expression, block, caption, heading,
527
+ * footnote, link, image or embed is in error. What drawing the maps found does
528
+ * not stop it.
529
+ *
530
+ * @param {object} result - From {@link buildSite}.
531
+ * @returns {boolean} Whether the build failed.
532
+ */
533
+ export function siteBuildFailed(result) {
534
+ return (
535
+ gatesFailed(result.gates) ||
536
+ [
537
+ result.tableErrors,
538
+ result.expressionErrors,
539
+ result.secretErrors,
540
+ result.captionErrors,
541
+ result.headingErrors,
542
+ result.footnoteErrors,
543
+ result.wikiErrors,
544
+ result.imageErrors,
545
+ result.embedErrors,
546
+ ].some((list) => list?.length)
547
+ );
548
+ }
549
+
531
550
  /**
532
551
  * The universe a generated table searches, grouped by package.
533
552
  *
@@ -559,39 +578,19 @@ export function tableUniverse(pages) {
559
578
  /**
560
579
  * The frontmatter a page publishes with.
561
580
  *
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>/`.
581
+ * An authored `aliases` is a list of names a reader might call the note. It is
582
+ * dropped: this build emits no redirects.
582
583
  *
583
584
  * 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
585
+ * declares one, so the note's frontmatter alone would publish a page that does
586
+ * not say which package it belongs to. Writing the derived value keeps a page
588
587
  * self-describing and makes sweeping the field out of a content tree
589
588
  * output-preserving for a site as it already is for the packs.
590
589
  *
591
590
  * **`related` is derived, never authored.** What links to a page is a fact
592
591
  * about every other page, known only once the whole tree has resolved — see
593
592
  * {@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
593
+ * way `aliases` is, and {@link renderPages} sets the derived block once it
595
594
  * holds the graph.
596
595
  *
597
596
  * @param {object} page - The page.
@@ -605,7 +604,7 @@ export function tableUniverse(pages) {
605
604
  * pathname into the address the website serves. Every artwork field goes
606
605
  * through it, so a page's `img:` and its body images name the same file the
607
606
  * same way.
608
- * @returns {object} The frontmatter to write.
607
+ * @returns {object} The frontmatter.
609
608
  */
610
609
  export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
611
610
  const { fm, slug } = page;
@@ -615,12 +614,7 @@ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
615
614
  // Spread after the note's own frontmatter. Guarded because
616
615
  // `package: undefined` is not a value YAML can carry.
617
616
  ...(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.
617
+ // The address, stated site-root relative; `slug` is its last segment.
624
618
  slug,
625
619
  url: `/${slug}/`,
626
620
  title: pageTitle(page),
@@ -692,37 +686,14 @@ function isPlainObject(value) {
692
686
  return typeof value === "object" && value !== null && !Array.isArray(value);
693
687
  }
694
688
 
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. */
689
+ /** Transform one content page. */
717
690
  export function renderSitePage(
718
691
  page,
719
692
  {
720
693
  index,
721
694
  foreign,
722
- universe,
723
695
  pass = {},
724
696
  decorate,
725
- linkable = (d) => Boolean(d.fm.shortcode),
726
697
  sqlTables,
727
698
  config,
728
699
  artIndex,
@@ -732,26 +703,13 @@ export function renderSitePage(
732
703
  runs = runContext(config),
733
704
  },
734
705
  ) {
735
- const tableErrors = [];
736
- const expressionErrors = [];
737
706
  const secretErrors = [];
738
707
  const captionErrors = [];
739
708
  const headingErrors = [];
740
709
  const footnoteErrors = [];
741
- const wikiErrors = [];
742
710
  const imageErrors = [];
743
711
  const embedErrors = [];
744
- const resolved = [];
745
712
  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
713
 
756
714
  const seenImages = new Map();
757
715
  const webSrc = (src) => {
@@ -777,24 +735,10 @@ export function renderSitePage(
777
735
  return src;
778
736
  };
779
737
  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.
738
+ // The role lookup a captioned item's `map` counter reaches through — see
739
+ // `engine/content-figures.mjs`'s `resolveRole`. It numbers the page's
740
+ // figures once, in expansion, and the render below reads that numbering.
789
741
  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
742
  // The picture's role and pixel size, by the address it resolved to — read
799
743
  // before `webSrc` translates it to the page's own host. A body image is
800
744
  // free to write the bare, own-package form, so the address is normalized
@@ -803,11 +747,6 @@ export function renderSitePage(
803
747
  const key = assetPathnameKey(src, config);
804
748
  return key ? artIndex?.byPath?.get(key) : undefined;
805
749
  };
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
750
  // A generated **From here** section names the drawing the build made, which
812
751
  // the asset host does not serve: it is set inline, so its place names stay
813
752
  // links. A drawing this build did not make is left for the image pass,
@@ -820,74 +759,40 @@ export function renderSitePage(
820
759
  return inlineDrawingFigure(fs.readFileSync(drawing.file, "utf8"), alt);
821
760
  });
822
761
 
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),
762
+ // The frontmatter's art resolves before the body's pictures, so a picture
763
+ // named in both is counted in that order.
764
+ const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
765
+ const expanded = expandContent(page.body, {
766
+ target: "web",
767
+ index,
768
+ note: { fm: page.fm, file: page.file, source: src, bodyLine: page.bodyLine },
769
+ prepared: {
770
+ sqlTables: sqlTables?.get(page.file),
771
+ pageLists: sqlTables?.pageLists?.get(page.file),
772
+ inline: sqlTables?.inline?.get(page.file),
773
+ },
857
774
  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
- }),
775
+ resolveRole,
776
+ figuresByAddress,
777
+ links: {
778
+ foreign: foreign.index,
779
+ assets: artIndex,
780
+ beforeLinks: pass.beforeLinks ? (text) => pass.beforeLinks(text, page) : undefined,
781
+ afterLinks: (text) => renderImageFigures(inlineDrawings(text), webSrc, lookupAsset),
872
782
  },
873
783
  });
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;
784
+ const { lineMap } = expanded;
785
+ const tableErrors = expanded.findings.tables;
786
+ const expressionErrors = expanded.findings.expressions;
787
+ const wikiErrors = expanded.findings.links;
788
+ // The author's text and the sections the build generated after it are
789
+ // told apart by a comment, so the site writer can leave the generated
790
+ // links out of what makes a page reachable.
882
791
  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)
792
+ expanded.generated ?
793
+ `${expanded.authored}\n\n${GENERATED_MARK}\n\n${expanded.generated}`
794
+ : expanded.markdown;
795
+ for (const error of expanded.findings.captions)
891
796
  captionErrors.push({
892
797
  file: page.file,
893
798
  line: (page.bodyLine ?? 1) + (lineMap[error.line - 1]?.line ?? error.line - 1),
@@ -926,15 +831,15 @@ export function renderSitePage(
926
831
  embedErrors.push(error);
927
832
  return {
928
833
  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
- ),
834
+ // The expanded body and its numbered figures, for the renderer that
835
+ // writes the whole page.
836
+ markdown: resolvedBody,
837
+ figures: expanded.figures.figures,
936
838
  data,
937
- resolved: resolved.slice(0, authoredLinks),
839
+ // The links a generated section makes join no page's Related card:
840
+ // that card is what the authors link, and the sections already list
841
+ // what they derive.
842
+ resolved: expanded.resolved.slice(0, expanded.authoredLinks),
938
843
  tableErrors,
939
844
  expressionErrors,
940
845
  secretErrors,
@@ -948,24 +853,20 @@ export function renderSitePage(
948
853
  }
949
854
 
950
855
  /**
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:
856
+ * Renders every page.
955
857
  *
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.
858
+ * Each page's body is expanded by
859
+ * {@link module:engine/content-expand.expandContent}, the step the pack
860
+ * compilers take too: tables first and outside code-fence protection, then
861
+ * figure numbers and expressions, then, inside protection, the consumer's
862
+ * `beforeLinks` pass and wikilink resolution. A `{@link}` tag may sit in prose
863
+ * a wikilink also touches, so the repository's own rewrite runs before the
864
+ * shared one rather than replacing it.
964
865
  *
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
866
+ * **Every page is rendered before any is finished.** What links *to* a page is
867
+ * known only once every other page has resolved, and it is set in the page's
868
+ * own front matter, so the render is one pass over the tree and the
869
+ * finishing is a second — with the link graph read off the first, at the one
969
870
  * point the resolver answers, and inverted once between them. Nothing is
970
871
  * resolved twice.
971
872
  *
@@ -982,14 +883,15 @@ export function renderSitePage(
982
883
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
983
884
  * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
984
885
  * related: Map<string, import("./related-pages.mjs").Related>,
985
- * maps: number}} `related`
886
+ * maps: number, pages: object[], runs: object}} `related`
986
887
  * 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.
888
+ * page's, so the caller can set it on the page this render does not.
889
+ * `maps` counts the pages carrying a map. `pages` holds
890
+ * `{page, markdown, figures, data}` for each page and `runs` the package's
891
+ * marked-text context.
989
892
  */
990
893
  export function renderPages(pages, options) {
991
894
  const {
992
- outRoot,
993
895
  index,
994
896
  foreign,
995
897
  universe,
@@ -1001,8 +903,6 @@ export function renderPages(pages, options) {
1001
903
  records = [],
1002
904
  homepages = [],
1003
905
  maps = new Map(),
1004
- write = true,
1005
- capture = false,
1006
906
  } = options;
1007
907
 
1008
908
  // The address space the art slots and the body's embeds resolve against:
@@ -1033,7 +933,7 @@ export function renderPages(pages, options) {
1033
933
  self: { fm: searchableFrontmatter(page.fm, page.pkg), path: page.relPath },
1034
934
  });
1035
935
  const byId = new Map(
1036
- scanFigures(markdown)
936
+ scanFigures(markdown, { resolveRole: (address) => embedRole(artIndex, address) })
1037
937
  .figures.filter((figure) => figure.id)
1038
938
  .map((figure) => [
1039
939
  figure.id,
@@ -1075,6 +975,7 @@ export function renderPages(pages, options) {
1075
975
  title: pageTitle(page),
1076
976
  url: page.url,
1077
977
  type: String(page.fm.type),
978
+ ...(page.fm.subType ? { subType: String(page.fm.subType) } : {}),
1078
979
  ...(typeof page.fm.name?.sort === "string" && page.fm.name.sort ?
1079
980
  { sort: page.fm.name.sort }
1080
981
  : {}),
@@ -1087,11 +988,12 @@ export function renderPages(pages, options) {
1087
988
  }
1088
989
  }
1089
990
  // What each page renders as, held until the graph is whole.
1090
- /** @type {Array<{page: object, body: string, data: object}>} */
991
+ /** @type {Array<{page: object, markdown: string, figures: object[], data: object}>} */
1091
992
  const rendered = [];
993
+ const runs = runContext(config, records);
1092
994
 
1093
- for (const page of pages) {
1094
- const result = renderSitePage(page, {
995
+ const renderOne = (page) =>
996
+ renderSitePage(page, {
1095
997
  index,
1096
998
  foreign,
1097
999
  universe,
@@ -1104,8 +1006,10 @@ export function renderPages(pages, options) {
1104
1006
  figuresByAddress,
1105
1007
  events,
1106
1008
  drawings: maps,
1107
- runs: runContext(config, records),
1009
+ runs,
1108
1010
  });
1011
+ for (const page of pages) {
1012
+ const result = renderOne(page);
1109
1013
  tableErrors.push(...result.tableErrors);
1110
1014
  expressionErrors.push(...result.expressionErrors);
1111
1015
  secretErrors.push(...result.secretErrors);
@@ -1115,7 +1019,12 @@ export function renderPages(pages, options) {
1115
1019
  wikiErrors.push(...result.wikiErrors);
1116
1020
  imageErrors.push(...result.imageErrors);
1117
1021
  embedErrors.push(...result.embedErrors);
1118
- rendered.push({ page, body: result.body, data: result.data });
1022
+ rendered.push({
1023
+ page,
1024
+ markdown: result.markdown,
1025
+ figures: result.figures,
1026
+ data: result.data,
1027
+ });
1119
1028
  for (const hit of result.resolved) if (hit.url) edges.push([page.url, hit.url]);
1120
1029
  }
1121
1030
 
@@ -1125,17 +1034,10 @@ export function renderPages(pages, options) {
1125
1034
  const related = relatedPages(edges, entries, deliberate);
1126
1035
 
1127
1036
  let withMap = 0;
1128
- const outputs = capture === true ? new Map() : null;
1129
- for (const { page, body, data } of rendered) {
1037
+ for (const { page, data } of rendered) {
1130
1038
  const block = related.get(page.url);
1131
1039
  if (block) data.related = block;
1132
1040
  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, writeMatter(body, encodeAddresses(data)));
1137
- }
1138
- if (outputs) outputs.set(page.file, { markdown: body, frontmatter: data });
1139
1041
  byKind[page.kind] = (byKind[page.kind] ?? 0) + 1;
1140
1042
  }
1141
1043
 
@@ -1153,7 +1055,21 @@ export function renderPages(pages, options) {
1153
1055
  embedErrors,
1154
1056
  related,
1155
1057
  maps: withMap,
1156
- ...(capture ? { ...(outputs ? { outputs } : {}), edges, entries } : {}),
1058
+ pages: rendered.map(({ page, markdown, figures, data }) => ({
1059
+ page,
1060
+ markdown,
1061
+ figures,
1062
+ data,
1063
+ })),
1064
+ runs,
1065
+ // One page rendered again with everything the whole render resolved
1066
+ // against, its place in the link graph included.
1067
+ renderAgain: (page) => {
1068
+ const result = renderOne(page);
1069
+ const block = related.get(page.url);
1070
+ if (block) result.data.related = block;
1071
+ return result;
1072
+ },
1157
1073
  };
1158
1074
  }
1159
1075
 
@@ -1211,7 +1127,7 @@ export function sitePageDecorator(config, index) {
1211
1127
  }
1212
1128
 
1213
1129
  /**
1214
- * Builds a Hugo content tree from a content tree, and reports what it found.
1130
+ * Collects, checks and renders a content tree's pages, and reports what it found.
1215
1131
  *
1216
1132
  * Returns rather than exits, in every case. A caller — the command, or a test —
1217
1133
  * decides what a finding means; the gates below are grouped so it can report
@@ -1229,8 +1145,13 @@ export function sitePageDecorator(config, index) {
1229
1145
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
1230
1146
  * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
1231
1147
  * mapFindings: object[],
1232
- * manifests: object|null}} `mapFindings` is what drawing the maps found —
1233
- * warnings, never a reason to fail the build.
1148
+ * manifests: object|null, site?: object}} `mapFindings` is what drawing the
1149
+ * maps found — warnings, never a reason to fail the build. `site` holds what a page renderer needs: `base`, `pages` (each content
1150
+ * page's `{page, markdown, figures, data}`), `homepages` (each
1151
+ * `{file, body, data}`), `runs`, `index`, `foreign` and `records`; and
1152
+ * `renderNote(file)`, which reads one content note again and renders it
1153
+ * against the rest of the build, or returns `null` when its address, name
1154
+ * or type changed and the whole site must be built again.
1234
1155
  */
1235
1156
  export function buildSite({ config, sqlTables } = {}) {
1236
1157
  const resolved = config ?? loadPackConfig();
@@ -1244,29 +1165,9 @@ export function buildSite({ config, sqlTables } = {}) {
1244
1165
  // that publishes it, and `prefix` is the content tree's mount within the
1245
1166
  // package — the same `prefix` the link manifest records against, so a page
1246
1167
  // and its manifest entry cannot disagree.
1247
- const base = site.base || `/${resolved.contentPackage}/`;
1168
+ const base = siteBase(resolved);
1248
1169
  const mount = `${base}${scheme.prefix}`;
1249
1170
 
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
1171
  const packages = new Set(site.packages.length ? site.packages : [resolved.contentPackage]);
1271
1172
 
1272
1173
  const ctx = {
@@ -1290,6 +1191,11 @@ export function buildSite({ config, sqlTables } = {}) {
1290
1191
  base,
1291
1192
  mount,
1292
1193
  scheme,
1194
+ // The notes `site.sections` names, by address, each with the section
1195
+ // it is the page of.
1196
+ sectionPages: new Map(
1197
+ Object.entries(site.sections ?? {}).map(([name, address]) => [address, name]),
1198
+ ),
1293
1199
  };
1294
1200
 
1295
1201
  const collected = collectHomepages(resolved.paths.content, ctx);
@@ -1333,10 +1239,6 @@ export function buildSite({ config, sqlTables } = {}) {
1333
1239
  };
1334
1240
  }
1335
1241
 
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
1242
  // Homepage-only stops here, and stopping is the point: nothing below reads
1341
1243
  // the content tree for pages, so `sohl-kethira-basic` and `harn-adventures`
1342
1244
  // cannot publish one whatever else their `site:` block declares.
@@ -1354,15 +1256,22 @@ export function buildSite({ config, sqlTables } = {}) {
1354
1256
  imageErrors: [],
1355
1257
  embedErrors: [],
1356
1258
  mapFindings: [],
1357
- stats: {
1358
- homepages: writeHomepages(homeRoot, homepages, resolved, { records: ctx.records }),
1359
- out: homeRoot,
1259
+ stats: { homepages: homepages.length },
1260
+ site: {
1261
+ base,
1262
+ pages: [],
1263
+ homepages: homepageData(homepages, resolved, { records: ctx.records }),
1264
+ runs: runContext(resolved, ctx.records),
1265
+ index: null,
1266
+ foreign: null,
1267
+ records: ctx.records,
1268
+ renderNote: () => null,
1360
1269
  },
1361
1270
  };
1362
1271
  }
1363
-
1364
1272
  const content = collectContentPages(resolved.paths.content, ctx);
1365
1273
  const { pages } = content;
1274
+ content.sectionFindings = unmatchedSectionPages(site.sections ?? {}, pages);
1366
1275
 
1367
1276
  // The homepage is **indexed but not rendered**. `[[homepage-root|Text]]`
1368
1277
  // is an ordinary wikilink and has to resolve to the page the build
@@ -1429,7 +1338,6 @@ export function buildSite({ config, sqlTables } = {}) {
1429
1338
  });
1430
1339
 
1431
1340
  const rendered = renderPages(pages, {
1432
- outRoot: out,
1433
1341
  sqlTables,
1434
1342
  config: resolved,
1435
1343
  records: ctx.records,
@@ -1443,14 +1351,14 @@ export function buildSite({ config, sqlTables } = {}) {
1443
1351
  decorate: sitePageDecorator(resolved, gates.index),
1444
1352
  });
1445
1353
 
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, {
1354
+ // The package's front page is not part of the content tree it
1355
+ // introduces. It publishes at `base`, which is where the graph keys its
1356
+ // block.
1357
+ const homepageOptions = {
1450
1358
  related: rendered.related.get(base),
1451
1359
  records: ctx.records,
1452
1360
  foreign: gates.foreign,
1453
- });
1361
+ };
1454
1362
 
1455
1363
  return {
1456
1364
  gates,
@@ -1466,9 +1374,53 @@ export function buildSite({ config, sqlTables } = {}) {
1466
1374
  mapFindings: drawn.findings,
1467
1375
  stats: {
1468
1376
  ...rendered.byKind,
1469
- homepages: homepagesWritten,
1377
+ homepages: homepages.length,
1470
1378
  maps: rendered.maps,
1471
- out,
1379
+ },
1380
+ site: {
1381
+ base,
1382
+ pages: rendered.pages,
1383
+ homepages: homepageData(homepages, resolved, homepageOptions),
1384
+ runs: rendered.runs,
1385
+ index: gates.index,
1386
+ foreign: gates.foreign,
1387
+ records: ctx.records,
1388
+ renderNote: (file) => {
1389
+ const before = pages.find((page) => page.file === file);
1390
+ const after = collectContentPages(resolved.paths.content, {
1391
+ ...ctx,
1392
+ only: file,
1393
+ }).pages[0];
1394
+ // A note with a new address, name or type, or a new
1395
+ // note, changes what other pages resolve to, which
1396
+ // only a whole build answers.
1397
+ if (
1398
+ !before ||
1399
+ !after ||
1400
+ after.url !== before.url ||
1401
+ after.name !== before.name ||
1402
+ String(after.fm.type) !== String(before.fm.type)
1403
+ )
1404
+ return null;
1405
+ const result = rendered.renderAgain(after);
1406
+ return {
1407
+ page: after,
1408
+ markdown: result.markdown,
1409
+ figures: result.figures,
1410
+ data: result.data,
1411
+ findings: [
1412
+ ...result.tableErrors,
1413
+ ...result.expressionErrors,
1414
+ ...result.secretErrors,
1415
+ ...result.captionErrors,
1416
+ ...result.headingErrors,
1417
+ ...result.footnoteErrors,
1418
+ ...result.wikiErrors,
1419
+ ...result.imageErrors,
1420
+ ...result.embedErrors,
1421
+ ],
1422
+ };
1423
+ },
1472
1424
  },
1473
1425
  };
1474
1426
  }