@heroiclands/package-build 23.13.0 → 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 (233) hide show
  1. package/CHANGELOG.md +36 -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 +244 -78
  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 +112 -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 +259 -314
  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-render.mjs +593 -0
  88. package/engine/site-root.mjs +22 -13
  89. package/engine/site-search.mjs +161 -0
  90. package/engine/site-serve.mjs +273 -0
  91. package/engine/site-theme-banner.mjs +154 -0
  92. package/engine/site-theme-urls.mjs +125 -0
  93. package/engine/site-theme.mjs +926 -0
  94. package/engine/text-maps.mjs +57 -10
  95. package/engine/typographer.mjs +248 -0
  96. package/engine/verse-metre.mjs +357 -0
  97. package/engine/wikilink-syntax.mjs +29 -47
  98. package/engine/wikilinks.mjs +758 -279
  99. package/engine/word-forms.mjs +39 -24
  100. package/engine/word-rows.mjs +12 -2
  101. package/engine/word-terms.mjs +29 -4
  102. package/manifest.mjs +3 -4
  103. package/package.json +10 -7
  104. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  105. package/site-theme/layouts/base.hbs +31 -0
  106. package/site-theme/layouts/blog-list.hbs +30 -0
  107. package/site-theme/layouts/blog-post.hbs +40 -0
  108. package/site-theme/layouts/home.hbs +35 -0
  109. package/site-theme/layouts/list.hbs +20 -0
  110. package/site-theme/layouts/not-found.hbs +31 -0
  111. package/site-theme/layouts/page.hbs +7 -0
  112. package/site-theme/partials/copy-buttons.hbs +37 -0
  113. package/site-theme/partials/draft-notice.hbs +5 -0
  114. package/site-theme/partials/exturl.hbs +2 -0
  115. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  116. package/site-theme/partials/header.hbs +44 -0
  117. package/site-theme/partials/hero-banner.hbs +11 -0
  118. package/site-theme/partials/infobox-entry.hbs +2 -0
  119. package/site-theme/partials/infobox-section.hbs +31 -0
  120. package/site-theme/partials/infobox-value.hbs +2 -0
  121. package/site-theme/partials/infobox.hbs +18 -0
  122. package/site-theme/partials/list-members.hbs +9 -0
  123. package/site-theme/partials/nav-links.hbs +5 -0
  124. package/site-theme/partials/page-body.hbs +48 -0
  125. package/site-theme/partials/page-list.hbs +14 -0
  126. package/site-theme/partials/related.hbs +24 -0
  127. package/site-theme/partials/search-meta.hbs +6 -0
  128. package/site-theme/partials/search-names.hbs +2 -0
  129. package/site-theme/partials/search.hbs +52 -0
  130. package/site-theme/partials/tags.hbs +4 -0
  131. package/site-theme/partials/toc.hbs +7 -0
  132. package/{hugo-theme → site-theme}/static/css/style.css +14 -0
  133. package/stage.mjs +1 -2
  134. package/types/config.d.mts +8 -4
  135. package/types/content-config.d.mts +15 -15
  136. package/types/engine/asset-shipping.d.mts +5 -2
  137. package/types/engine/base-compiler.d.mts +20 -2
  138. package/types/engine/content-address.d.mts +1 -1
  139. package/types/engine/content-alerts.d.mts +8 -2
  140. package/types/engine/content-blocks.d.mts +11 -4
  141. package/types/engine/content-expand.d.mts +288 -0
  142. package/types/engine/content-figures.d.mts +11 -3
  143. package/types/engine/content-glyphs.d.mts +85 -0
  144. package/types/engine/content-images.d.mts +6 -7
  145. package/types/engine/content-links.d.mts +1 -1
  146. package/types/engine/content-render.d.mts +199 -0
  147. package/types/engine/content-scripts.d.mts +62 -0
  148. package/types/engine/content-spans.d.mts +1 -1
  149. package/types/engine/content-styles.d.mts +33 -0
  150. package/types/engine/content-symbols.d.mts +35 -0
  151. package/types/engine/core-report.d.mts +45 -0
  152. package/types/engine/core-vocabulary.d.mts +53 -0
  153. package/types/engine/dictionary-html.d.mts +8 -0
  154. package/types/engine/field-reference.d.mts +1 -1
  155. package/types/engine/heading-attributes.d.mts +20 -0
  156. package/types/engine/helpers.d.mts +3 -10
  157. package/types/engine/homepage.d.mts +11 -24
  158. package/types/engine/html-minify.d.mts +9 -0
  159. package/types/engine/index.d.mts +2 -1
  160. package/types/engine/language-terms.d.mts +24 -2
  161. package/types/engine/logographic.d.mts +45 -0
  162. package/types/engine/map-lexicon.d.mts +48 -8
  163. package/types/engine/map-places.d.mts +8 -0
  164. package/types/engine/markdown-expressions.d.mts +0 -2
  165. package/types/engine/metre-declaration.d.mts +39 -0
  166. package/types/engine/pdf-render.d.mts +6 -2
  167. package/types/engine/post-fields.d.mts +44 -0
  168. package/types/engine/preview-index.d.mts +72 -0
  169. package/types/engine/preview-source.d.mts +51 -0
  170. package/types/engine/script-emphasis-terms.d.mts +21 -0
  171. package/types/engine/script-emphasis.d.mts +84 -0
  172. package/types/engine/script-font-delivery.d.mts +4 -4
  173. package/types/engine/site-build.d.mts +71 -113
  174. package/types/engine/site-config.d.mts +14 -134
  175. package/types/engine/site-files.d.mts +73 -0
  176. package/types/engine/site-index.d.mts +11 -3
  177. package/types/engine/site-preview.d.mts +15 -19
  178. package/types/engine/site-render.d.mts +171 -0
  179. package/types/engine/site-root.d.mts +12 -4
  180. package/types/engine/site-search.d.mts +54 -0
  181. package/types/engine/site-serve.d.mts +44 -0
  182. package/types/engine/site-theme-banner.d.mts +51 -0
  183. package/types/engine/site-theme-urls.d.mts +60 -0
  184. package/types/engine/site-theme.d.mts +127 -0
  185. package/types/engine/text-maps.d.mts +17 -3
  186. package/types/engine/typographer.d.mts +11 -0
  187. package/types/engine/verse-metre.d.mts +131 -0
  188. package/types/engine/wikilink-syntax.d.mts +30 -48
  189. package/types/engine/wikilinks.d.mts +330 -14
  190. package/types/engine/word-forms.d.mts +3 -2
  191. package/types/manifest.d.mts +3 -4
  192. package/types/stage.d.mts +1 -2
  193. package/types/vendor/markdownlint.d.mts +8 -0
  194. package/vendor/markdownlint.mjs +20388 -0
  195. package/engine/web-wikilinks.mjs +0 -594
  196. package/hugo-theme/layouts/404.html +0 -85
  197. package/hugo-theme/layouts/_default/baseof.html +0 -67
  198. package/hugo-theme/layouts/_default/list.html +0 -139
  199. package/hugo-theme/layouts/_default/single.html +0 -4
  200. package/hugo-theme/layouts/blog/list.html +0 -54
  201. package/hugo-theme/layouts/blog-post/single.html +0 -36
  202. package/hugo-theme/layouts/index.html +0 -75
  203. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  204. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  205. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  206. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  207. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  208. package/hugo-theme/layouts/partials/header.html +0 -35
  209. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  210. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  211. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  212. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  213. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  214. package/hugo-theme/layouts/partials/infobox.html +0 -57
  215. package/hugo-theme/layouts/partials/page-body.html +0 -152
  216. package/hugo-theme/layouts/partials/related.html +0 -138
  217. package/hugo-theme/layouts/partials/search.html +0 -84
  218. package/hugo-theme/layouts/partials/site-url.html +0 -32
  219. package/hugo-theme/layouts/partials/tags.html +0 -30
  220. package/hugo-theme/layouts/partials/toc.html +0 -28
  221. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  222. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  223. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  224. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  225. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  226. package/hugo-theme/theme.toml +0 -11
  227. package/types/engine/web-wikilinks.d.mts +0 -107
  228. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  229. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  230. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  231. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  232. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  233. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -272,8 +272,11 @@ export function assetsManifest(staged, stageDir) {
272
272
  *
273
273
  * The pictures are written beside whatever a `packageBuild.assets` row already
274
274
  * put under `icons/` and `images/`; nothing there is removed. A picture whose
275
- * deployed path is already a file in the stage is a finding, and no picture is
276
- * written. Clearing an earlier run's pictures is the stage reset's work.
275
+ * deployed path already holds a file with the bytes it would stage — after the
276
+ * transform, when there is one — is its own earlier output and is skipped, so
277
+ * staging twice into one directory is quiet. A file there with other bytes is a
278
+ * finding, and no picture is written. Clearing an earlier run's pictures is the
279
+ * stage reset's work.
277
280
  *
278
281
  * @param {object} opts - Options.
279
282
  * @param {string} opts.assetsBase - The package's asset directory, the roots'
@@ -309,13 +312,22 @@ export function stagePackageAssets({
309
312
  if (findings.length) return { staged: [], excluded: [], findings, manifest: null };
310
313
 
311
314
  const { staged, excluded } = selectStagedAssets(records, shippingLicenses);
312
- const planned = staged.map((record) => ({
313
- record,
314
- from: path.join(assetsBase, ...record.asset.path.split("/")),
315
- to: path.join(stageDir, .../** @type {string} */ (deployedAssetPath(record)).split("/")),
316
- }));
317
- for (const { record, from, to } of planned) {
315
+ const planned = staged.map((record) => {
316
+ const from = path.join(assetsBase, ...record.asset.path.split("/"));
317
+ return {
318
+ record,
319
+ from,
320
+ transformed: transform ? transform(from) : null,
321
+ to: path.join(
322
+ stageDir,
323
+ .../** @type {string} */ (deployedAssetPath(record)).split("/"),
324
+ ),
325
+ };
326
+ });
327
+ for (const { record, from, to, transformed } of planned) {
318
328
  if (!fs.existsSync(to)) continue;
329
+ const bytes = transformed != null ? transformed : fs.readFileSync(from);
330
+ if (Buffer.from(fs.readFileSync(to)).equals(Buffer.from(bytes))) continue;
319
331
  findings.push({
320
332
  file: to,
321
333
  severity: "error",
@@ -326,9 +338,9 @@ export function stagePackageAssets({
326
338
  });
327
339
  }
328
340
  if (findings.length) return { staged: [], excluded: [], findings, manifest: null };
329
- for (const { from, to } of planned) {
341
+ for (const { from, to, transformed } of planned) {
342
+ if (fs.existsSync(to)) continue;
330
343
  fs.mkdirSync(path.dirname(to), { recursive: true });
331
- const transformed = transform ? transform(from) : null;
332
344
  if (transformed != null) fs.writeFileSync(to, transformed);
333
345
  else fs.copyFileSync(from, to);
334
346
  }
@@ -68,10 +68,10 @@ import { scanSpans } from "./content-spans.mjs";
68
68
  import { scanBlocks } from "./content-blocks.mjs";
69
69
  import { scanRuns } from "./content-scripts.mjs";
70
70
  import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
71
- import { scanFigures } from "./content-figures.mjs";
72
71
  import { footnoteFindings } from "./content-footnotes.mjs";
73
72
  import { checkImages } from "./content-images.mjs";
74
- import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
73
+ import { embedRole } from "./content-embeds.mjs";
74
+ import { expandContent } from "./content-expand.mjs";
75
75
  import { eventNoteIndex } from "./event-fields.mjs";
76
76
  import { isGmNote } from "./note-vocabulary.mjs";
77
77
  import { cloneAddressState } from "./address-values.mjs";
@@ -84,8 +84,6 @@ import {
84
84
  makeFilename,
85
85
  resolveImg,
86
86
  resolveName,
87
- convertNoteWikilinks,
88
- expandNoteTables,
89
87
  statsForPack,
90
88
  } from "./helpers.mjs";
91
89
  // The record accessors only — see `engine/index-records.mjs` for why they live
@@ -131,25 +129,6 @@ import { annotateAssetReferences, assetReferenceLookup } from "./asset-reference
131
129
  * about. Counted as errors, never as skips.
132
130
  */
133
131
 
134
- /**
135
- * A `ref` call's Foundry rendering: a same-page wikilink rather than a
136
- * Markdown link, so the wikilink pass that runs after expressions render
137
- * addresses it exactly as an authored `[[#anchor|Text]]` is — by the
138
- * JournalEntryPage the figure's own fence became, whether that page holds
139
- * only the figure or carries it alongside other prose. A Markdown link's
140
- * `#anchor` fragment resolves nowhere in Foundry, which addresses a page by
141
- * UUID and an anchor within a page by that page's own anchor; this is why the
142
- * two surfaces do not share a renderer.
143
- *
144
- * @param {{anchor: string}} target - The figure's own anchor.
145
- * @param {string} label - The link's text, which may itself carry Markdown.
146
- * @returns {string} `[[#anchor|label]]`, with a literal `|` in the label
147
- * escaped so it cannot be read as the wikilink's own separator.
148
- */
149
- function foundryRefLink({ anchor }, label) {
150
- return `[[#${anchor}|${label.replace(/\|/g, "\\|")}]]`;
151
- }
152
-
153
132
  /**
154
133
  * The shared walk → filter → expand → convert → build → write → count loop.
155
134
  *
@@ -670,8 +649,9 @@ export class BasePackCompiler {
670
649
  /**
671
650
  * The body {@link BasePackCompiler#buildEntry} receives.
672
651
  *
673
- * Generated tables expand before wikilinks are converted, so a cell a
674
- * table emits is resolved along with the authored links.
652
+ * The body is expanded by {@link module:engine/content-expand.expandContent}
653
+ * for Foundry: generated tables expand before wikilinks are converted, so
654
+ * a cell a table emits is resolved along with the authored links.
675
655
  *
676
656
  * @param {object} fm - The note's frontmatter.
677
657
  * @param {string} body - The note body, as authored.
@@ -686,6 +666,14 @@ export class BasePackCompiler {
686
666
  // it made — `errors` or `findings`, never just the first — so nothing
687
667
  // here re-scans; it only stops taking `[0]`.
688
668
  const findings = [];
669
+ const bodyLine = this.currentNote?.bodyLine ?? 1;
670
+ const bodyColumn = this.currentNote?.bodyColumn ?? 1;
671
+ // A finding's body position, in the file. The parser trims the body,
672
+ // so only its first line is offset by the column the body starts at.
673
+ const inFile = ({ line, column }) => ({
674
+ line: bodyLine + line - 1,
675
+ column: line === 1 && column !== undefined ? bodyColumn + column - 1 : column,
676
+ });
689
677
  const syntax = this.corpus?.syntax;
690
678
  const scanned = syntax?.(body);
691
679
  for (const secretError of [
@@ -695,41 +683,27 @@ export class BasePackCompiler {
695
683
  ...scanRuns(body, this.corpus?.runs, syntax).errors,
696
684
  ...markupAnchorFindings(body, syntax),
697
685
  ]) {
698
- findings.push({
699
- message: secretError.message,
700
- line: (this.currentNote?.bodyLine ?? 1) + secretError.line - 1,
701
- column:
702
- secretError.line === 1 ?
703
- (this.currentNote?.bodyColumn ?? 1)
704
- : secretError.column,
705
- });
686
+ findings.push({ message: secretError.message, ...inFile(secretError) });
706
687
  }
707
688
  // A heading's attribute block, and a `.secret` written where no surface
708
689
  // can honour it. Read off the body as authored, before the tables
709
690
  // expand, because a heading is a heading whatever a pass does with the
710
691
  // prose under it — so a pass that converts nothing reports these too.
711
692
  for (const headingError of [
712
- ...scanHeadingAttributes(body, this.currentNote?.bodyLine ?? 1).errors,
713
- ...withheldSections(body, this.currentNote?.bodyLine ?? 1).errors,
693
+ ...scanHeadingAttributes(body, bodyLine).errors,
694
+ ...withheldSections(body, bodyLine).errors,
714
695
  ]) {
715
696
  findings.push({
716
697
  message: headingError.message,
717
698
  line: headingError.line,
718
699
  column:
719
- headingError.line === (this.currentNote?.bodyLine ?? 1) ?
720
- (this.currentNote?.bodyColumn ?? 1) + headingError.column - 1
700
+ headingError.line === bodyLine ?
701
+ bodyColumn + headingError.column - 1
721
702
  : headingError.column,
722
703
  });
723
704
  }
724
705
  for (const footnoteError of footnoteFindings(body)) {
725
- findings.push({
726
- message: footnoteError.message,
727
- line: (this.currentNote?.bodyLine ?? 1) + footnoteError.line - 1,
728
- column:
729
- footnoteError.line === 1 ?
730
- (this.currentNote?.bodyColumn ?? 1)
731
- : footnoteError.column,
732
- });
706
+ findings.push({ message: footnoteError.message, ...inFile(footnoteError) });
733
707
  }
734
708
  // An image sharing its paragraph with other text, a bad address or a
735
709
  // title with nowhere to draw it — the same findings `lint` reports,
@@ -737,10 +711,7 @@ export class BasePackCompiler {
737
711
  // already names rather than rendering the directive as though it were
738
712
  // absent. `checkImages` already resolves `bodyLine` and `bodyColumn`
739
713
  // internally, so its findings need no further adjustment.
740
- for (const imageError of checkImages(body, "", {
741
- bodyLine: this.currentNote?.bodyLine ?? 1,
742
- bodyColumn: this.currentNote?.bodyColumn ?? 1,
743
- })) {
714
+ for (const imageError of checkImages(body, "", { bodyLine, bodyColumn })) {
744
715
  findings.push({
745
716
  message: imageError.message,
746
717
  line: imageError.line,
@@ -752,69 +723,56 @@ export class BasePackCompiler {
752
723
  return body;
753
724
  }
754
725
  const name = resolveName(fm);
755
- const { absPath, bodyLine, bodyColumn } = this.currentNote ?? {};
756
- const { markdown: tabulated, lineMap } = expandNoteTables(body, {
757
- name,
758
- bodyLine,
759
- sqlTables: absPath ? this.sqlTables?.get(absPath) : undefined,
760
- pageLists: absPath ? this.sqlTables?.pageLists?.get(absPath) : undefined,
726
+ // The note's own position, unset where the caller supplied none, so a
727
+ // link finding drops the field rather than guessing it.
728
+ const { absPath, bodyLine: noteLine, bodyColumn: noteColumn } = this.currentNote ?? {};
729
+ // A `ref`'s cross-note resolution is the web's alone: a journal
730
+ // addresses a page by UUID, which this pass has none of for another
731
+ // note.
732
+ const expanded = expandContent(body, {
733
+ target: "foundry",
734
+ index: this.linkIndex,
735
+ note: { fm, file: absPath, source: name, bodyLine: noteLine, bodyColumn: noteColumn },
736
+ prepared: {
737
+ sqlTables: absPath ? this.sqlTables?.get(absPath) : undefined,
738
+ pageLists: absPath ? this.sqlTables?.pageLists?.get(absPath) : undefined,
739
+ inline: absPath ? this.sqlTables?.inline?.get(absPath) : undefined,
740
+ },
741
+ events: this.eventNotes(),
742
+ // A `ref` to a map reads the number the map's own page carries.
743
+ resolveRole: (address) => embedRole(this.linkIndex, address),
744
+ links: {
745
+ // A `[[#slug]]` self-link addresses the source note, which has
746
+ // no entry in the index — so where its own documents landed
747
+ // has to travel with it.
748
+ pack: this.router?.resolveOrNull(fm, packForType(fm.type).docType),
749
+ docPack: this.router?.resolveOrNull(fm, "JournalEntry"),
750
+ },
761
751
  });
762
- const figureScan = scanFigures(tabulated);
763
- for (const captionError of figureScan.errors) {
752
+ // A table the preparation could not answer fails the note on its own,
753
+ // at the table's line.
754
+ const { tables } = expanded.findings;
755
+ if (tables.length) {
756
+ const err = new Error(tables.map((e) => `content table — ${e.reason}`).join("; "));
757
+ if (noteLine !== undefined && tables[0].line !== undefined)
758
+ err.position = { line: noteLine + tables[0].line };
759
+ throw err;
760
+ }
761
+ const { lineMap } = expanded;
762
+ for (const captionError of expanded.findings.captions) {
763
+ const authored = lineMap[captionError.line - 1]?.line ?? captionError.line - 1;
764
764
  findings.push({
765
765
  message: captionError.message,
766
- line:
767
- (bodyLine ?? 1) +
768
- (lineMap[captionError.line - 1]?.line ?? captionError.line - 1),
769
- column: captionError.column,
766
+ ...inFile({ line: authored + 1, column: captionError.column }),
770
767
  });
771
768
  }
772
- // What the `ref` expression helper needs: this note's own figures, by
773
- // id, and how a reference renders — a same-page wikilink rather than
774
- // a Markdown link, so the wikilink pass below addresses it exactly as
775
- // an authored `[[#anchor|Text]]` is, by the page the figure's own
776
- // fence became. Cross-note resolution is omitted: a journal addresses
777
- // a page by UUID, which this pass has none of for another note.
778
- const figuresById = new Map(
779
- figureScan.figures
780
- .filter((figure) => figure.id)
781
- .map((figure) => [
782
- figure.id,
783
- { label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
784
- ]),
785
- );
786
- const expressions = renderMarkdownExpressions(tabulated, {
787
- fm,
788
- dates: reckoningContext(this.linkIndex),
789
- sqlResults: absPath ? this.sqlTables?.inline?.get(absPath) : undefined,
790
- file: absPath,
791
- bodyLine,
792
- figures: { get: (id) => figuresById.get(id), link: foundryRefLink },
793
- events: this.eventNotes(),
794
- });
795
- for (const finding of expressions.findings) {
769
+ for (const finding of expanded.findings.expressions) {
796
770
  findings.push({ message: finding.message, line: finding.line, column: finding.column });
797
771
  }
798
772
  this.reportConvertBodyFindings(findings);
799
- const { markdown, unresolved } = convertNoteWikilinks(expressions.markdown, {
800
- type: fm.type,
801
- id: fm.id,
802
- // Where this note is, so a link that resolves nowhere is reported
803
- // at a position an author can open rather than by note name.
804
- file: absPath,
805
- bodyLine,
806
- bodyColumn,
807
- lineMap,
808
- // A `[[#slug]]` self-link addresses the source note, which has no
809
- // entry in the index — so where its own documents landed has to
810
- // travel with it.
811
- pack: this.router?.resolveOrNull(fm, packForType(fm.type).docType),
812
- docPack: this.router?.resolveOrNull(fm, "JournalEntry"),
813
- index: this.linkIndex,
814
- name,
815
- });
816
- this.unresolvedLinks += unresolved.length;
817
- return markdown;
773
+ if (expanded.linkError) throw expanded.linkError;
774
+ this.unresolvedLinks += expanded.unresolved.length;
775
+ return expanded.markdown;
818
776
  }
819
777
 
820
778
  /**
@@ -553,7 +553,7 @@ function checkTooManyBullets(bullets) {
553
553
  severity: "error",
554
554
  message:
555
555
  `changelog-check/too-many-bullets ${bullets.length} top-level bullets is an ` +
556
- `itemized log, not a release note — one to six, so a reader sees the shape ` +
556
+ `itemized log, not a release note — at most ${MAX_TOP_BULLETS}, so a reader sees the shape ` +
557
557
  "of the release at a glance",
558
558
  },
559
559
  ];
@@ -140,7 +140,7 @@ export function addressSlug(fm) {
140
140
  * composing a URL prepends where the package is served (`/<package>/`); a
141
141
  * consumer composing a manifest entry measures against that same base. Nothing
142
142
  * else is prepended: `prefix` says where the content tree *mounts inside the
143
- * package* — the Hugo directory its pages are written under — and an address is
143
+ * package* — where its pages sit in the source tree — and an address is
144
144
  * `(type, shortcode)`, a package-wide identity that takes no mount, so `sohl`
145
145
  * publishes `/sohl/affliction-aconite/` from a file written under `kb/`. The
146
146
  * `type-` half is what keeps that flat namespace clear of the package's fixed
@@ -136,9 +136,13 @@ export function scanAlerts(source) {
136
136
  }
137
137
  /** Render alert wrappers while preserving ordinary Markdown and note footnotes.
138
138
  * @param {string} source - Authored Markdown.
139
- * @param {"web"|"foundry"} [target="web"] - Publishing surface.
139
+ * @param {"web"|"foundry"|"preview"} [target="web"] - Publishing surface: the
140
+ * preview draws the icon as an inline drawing, since a buffer has no icon font.
141
+ * @param {object} [options]
142
+ * @param {(markdown: string) => string} [options.renderInline] - How a title
143
+ * is rendered; omitted, a title is plain inline Markdown.
140
144
  */
141
- export function renderAlerts(source, target = "web") {
145
+ export function renderAlerts(source, target = "web", { renderInline } = {}) {
142
146
  const { blocks, errors } = scanAlerts(source),
143
147
  lines = String(source ?? "").split("\n"),
144
148
  output = [];
@@ -163,9 +167,9 @@ export function renderAlerts(source, target = "web") {
163
167
  output.push(
164
168
  "",
165
169
  `<aside ${attrs.join(" ")}>`,
166
- `<header class="alert-title">${definition.icon} ${titleParser.renderInline(block.title)}</header>`,
170
+ `<header class="alert-title">${target === "preview" ? definition.svg : definition.icon} ${(renderInline ?? titleParser.renderInline.bind(titleParser))(block.title)}</header>`,
167
171
  "",
168
- renderAlerts(block.body, target).markdown,
172
+ renderAlerts(block.body, target, { renderInline }).markdown,
169
173
  "",
170
174
  "</aside>",
171
175
  "",
@@ -38,7 +38,7 @@
38
38
  * **A block's body is left as Markdown, not pre-rendered.** The wrapper is
39
39
  * written with a blank line after the opening tag and before the closing one,
40
40
  * which is what lets the surrounding renderer — Foundry's single parse of the
41
- * whole page, or Hugo's of the whole site page — read the body as part of its
41
+ * whole page, or the site renderer's of the whole page — read the body as part of its
42
42
  * own document rather than as an isolated fragment. A fragment rendered on its
43
43
  * own has no access to the note's footnote definitions, which live at the top
44
44
  * level outside every block, so a reference inside one would fall through as
@@ -163,8 +163,8 @@ export function scanBlocks(source) {
163
163
  parsed.problems.push("block attributes need {…} braces");
164
164
  else parsed = parseExtensionAttributes(raw.trim().slice(1, -1));
165
165
  }
166
- for (const message of [...parsed.problems, ...refusedAttributes(parsed.values)])
167
- errors.push({ ...at, message });
166
+ const problems = [...parsed.problems, ...refusedAttributes(parsed.values)];
167
+ for (const message of problems) errors.push({ ...at, message });
168
168
  const attributes = { ...parsed.values };
169
169
  let title = BLOCK_NAMES[name] ?? "";
170
170
  if (name !== "div" && Object.hasOwn(attributes, "title")) {
@@ -178,7 +178,7 @@ export function scanBlocks(source) {
178
178
  id: parsed.id,
179
179
  classes: parsed.classes,
180
180
  attributes,
181
- rejected: parsed.problems.length > 0 || refusedAttributes(parsed.values).length > 0,
181
+ rejected: problems.length > 0,
182
182
  indent: /^ */.exec(line)[0],
183
183
  });
184
184
  }
@@ -237,7 +237,8 @@ function attributeText(block, target, classes) {
237
237
  * Render named blocks for one publishing surface.
238
238
  *
239
239
  * `foundry` emits a `<section>` whose heading is its first line; `web` emits a
240
- * `<details>` the reader opens. Neither carries an inline `style`.
240
+ * `<details>` the reader opens, and `preview` the same without the search
241
+ * marker. None carries an inline `style`.
241
242
  *
242
243
  * The body is left as Markdown between the wrapper's tags, each separated from
243
244
  * its tag by a blank line — see the module docs for why that is what lets the
@@ -247,10 +248,13 @@ function attributeText(block, target, classes) {
247
248
  * A finding does not stop the well-formed blocks around it from rendering.
248
249
  *
249
250
  * @param {string} source - Markdown containing named blocks.
250
- * @param {"foundry"|"web"} target - Publishing surface.
251
+ * @param {"foundry"|"web"|"preview"} target - Publishing surface.
252
+ * @param {object} [options]
253
+ * @param {(markdown: string) => string} [options.renderInline] - How a title
254
+ * is rendered; omitted, a title is plain inline Markdown.
251
255
  * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
252
256
  */
253
- export function renderBlocks(source, target) {
257
+ export function renderBlocks(source, target, { renderInline } = {}) {
254
258
  const { errors } = scanBlocks(source);
255
259
  source = renderVerse(source).markdown;
256
260
  const renderedBlocks = scanBlocks(source).blocks;
@@ -268,14 +272,14 @@ export function renderBlocks(source, target) {
268
272
  // at every depth, never a rendered fragment.
269
273
  const held =
270
274
  BLOCK_CONTAINERS.includes(block.name) ?
271
- renderBlocks(block.body, target).markdown
275
+ renderBlocks(block.body, target, { renderInline }).markdown
272
276
  : block.body;
273
277
  if (block.name === "div") {
274
278
  output.push("", `<div ${attributes}>`, "", held, "", "</div>", "");
275
279
  cursor = block.end + 1;
276
280
  continue;
277
281
  }
278
- const title = titleParser.renderInline(block.title);
282
+ const title = (renderInline ?? titleParser.renderInline.bind(titleParser))(block.title);
279
283
  if (target === "foundry") {
280
284
  output.push(
281
285
  "",
@@ -287,6 +291,16 @@ export function renderBlocks(source, target) {
287
291
  "</section>",
288
292
  "",
289
293
  );
294
+ } else if (target === "preview") {
295
+ output.push(
296
+ "",
297
+ `<details ${attributes}><summary>${title}</summary>`,
298
+ "",
299
+ held,
300
+ "",
301
+ "</details>",
302
+ "",
303
+ );
290
304
  } else {
291
305
  output.push(
292
306
  "",
@@ -314,7 +328,8 @@ export function renderBlocks(source, target) {
314
328
  *
315
329
  * **A spoiler, not access control.** The section is in the published page, so a
316
330
  * reader who opens the element or reads the source reads it. Foundry is the one
317
- * surface that withholds anything.
331
+ * surface that withholds anything. The disclosure is kept out of the site's
332
+ * search index, as a `secret` block is, so no result quotes it.
318
333
  *
319
334
  * @param {string} source - A page's Markdown, headings intact.
320
335
  * @returns {string} The same Markdown, each withheld section inside a
@@ -335,7 +350,7 @@ export function renderWithheldSections(source) {
335
350
  ];
336
351
  output.push(
337
352
  "",
338
- `<details class="${escapeAttribute(classes.join(" "))}">`,
353
+ `<details class="${escapeAttribute(classes.join(" "))}" data-pagefind-ignore>`,
339
354
  `<summary class="${escapeAttribute(WITHHELD_CLASS)}">${BLOCK_NAMES[WITHHELD_CLASS]}</summary>`,
340
355
  "",
341
356
  ...lines.slice(section.start, section.end),
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * **That is why the rewrite is a source-text pass rather than a markdown-it
37
37
  * inline rule.** Two of the four readers have no render pass to put a token in:
38
- * the website is handed *markdown* for Hugo to render, and the link checker
38
+ * the website's renderer is handed *markdown*, and the link checker
39
39
  * parses nothing at all. A third, the book, reads its staging list out of the
40
40
  * source text before it tokenises. A token would therefore serve one reader and
41
41
  * leave the other three needing the grammar a second time, which is the drift