@heroiclands/package-build 23.12.8 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +3 -2
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +293 -24
  5. package/bin/content-commands.mjs +370 -213
  6. package/bin/package-build-preview.mjs +2 -2
  7. package/bin/package-build.mjs +3 -2
  8. package/ci/check-banners.mjs +7 -7
  9. package/config.mjs +21 -6
  10. package/content-config.mjs +199 -80
  11. package/docs/api.md +110 -89
  12. package/docs/authoring/links-and-markup.md +469 -97
  13. package/docs/commands.md +259 -78
  14. package/docs/configuration.md +211 -103
  15. package/docs/content-format.md +4 -4
  16. package/docs/diagnostics.md +44 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/guides/icons.md +1 -1
  19. package/docs/guides/module-project.md +5 -8
  20. package/docs/guides/release-and-deploy.md +2 -2
  21. package/docs/guides/site.md +112 -23
  22. package/docs/guides/system-project.md +1 -1
  23. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  24. package/docs/index.md +1 -0
  25. package/docs/project-setup.md +28 -30
  26. package/docs/reference/format-details.md +677 -319
  27. package/docs/reference/note-types.md +47 -10
  28. package/docs/reference/theme-pages.md +178 -108
  29. package/docs/source-preview.md +16 -2
  30. package/engine/anchors.mjs +2 -2
  31. package/engine/asset-shipping.mjs +22 -10
  32. package/engine/base-compiler.mjs +62 -104
  33. package/engine/changelog-lint.mjs +1 -1
  34. package/engine/code-fences.mjs +3 -3
  35. package/engine/content-address.mjs +1 -1
  36. package/engine/content-alerts.mjs +8 -4
  37. package/engine/content-blocks.mjs +29 -14
  38. package/engine/content-embeds.mjs +1 -1
  39. package/engine/content-expand.mjs +376 -0
  40. package/engine/content-figures.mjs +18 -11
  41. package/engine/content-format.yaml +8 -0
  42. package/engine/content-glyphs.mjs +213 -0
  43. package/engine/content-images.mjs +6 -7
  44. package/engine/content-index.mjs +1 -1
  45. package/engine/content-links.mjs +3 -3
  46. package/engine/content-lint.mjs +1 -1
  47. package/engine/content-render.mjs +453 -0
  48. package/engine/content-scripts.mjs +376 -43
  49. package/engine/content-spans.mjs +6 -3
  50. package/engine/content-styles.mjs +91 -0
  51. package/engine/content-symbols.mjs +186 -0
  52. package/engine/content-verse.mjs +738 -0
  53. package/engine/core-report.mjs +169 -0
  54. package/engine/core-vocabulary.mjs +208 -0
  55. package/engine/dictionary-html.mjs +119 -0
  56. package/engine/dictionary-rows.mjs +15 -4
  57. package/engine/field-reference.mjs +5 -5
  58. package/engine/first-use.mjs +17 -27
  59. package/engine/frontmatter-lint.mjs +1 -1
  60. package/engine/heading-attributes.mjs +89 -0
  61. package/engine/helpers.mjs +61 -101
  62. package/engine/homepage.mjs +17 -29
  63. package/engine/html-minify.mjs +70 -0
  64. package/engine/index.mjs +8 -5
  65. package/engine/key-case-collisions.mjs +136 -0
  66. package/engine/language-data.mjs +203 -6
  67. package/engine/language-rows.mjs +8 -2
  68. package/engine/language-terms.mjs +186 -16
  69. package/engine/language-words.mjs +12 -0
  70. package/engine/logographic.mjs +255 -0
  71. package/engine/map-lexicon.mjs +262 -47
  72. package/engine/map-places.mjs +5 -4
  73. package/engine/markdown-expressions.mjs +0 -2
  74. package/engine/markdownlint-run.mjs +9 -3
  75. package/engine/markup-validation.mjs +21 -0
  76. package/engine/metre-declaration.mjs +134 -0
  77. package/engine/names.mjs +36 -9
  78. package/engine/note-matter.mjs +66 -0
  79. package/engine/note-vocabulary.mjs +187 -3
  80. package/engine/pdf-build.mjs +69 -133
  81. package/engine/pdf-render.mjs +205 -58
  82. package/engine/post-fields.mjs +157 -0
  83. package/engine/preview-index.mjs +130 -0
  84. package/engine/preview-source.mjs +450 -0
  85. package/engine/project-init.mjs +5 -12
  86. package/engine/prose-config.mjs +3 -3
  87. package/engine/read-stdin.mjs +29 -0
  88. package/engine/run-rows.mjs +2 -2
  89. package/engine/run-words.mjs +6 -112
  90. package/engine/script-emphasis-terms.mjs +38 -0
  91. package/engine/script-emphasis.mjs +295 -0
  92. package/engine/script-font-delivery.mjs +4 -4
  93. package/engine/site-build.mjs +260 -315
  94. package/engine/site-config.mjs +26 -238
  95. package/engine/site-files.mjs +175 -0
  96. package/engine/site-index.mjs +9 -4
  97. package/engine/site-preview.mjs +52 -525
  98. package/engine/site-render.mjs +593 -0
  99. package/engine/site-root.mjs +22 -13
  100. package/engine/site-search.mjs +161 -0
  101. package/engine/site-serve.mjs +273 -0
  102. package/engine/site-theme-banner.mjs +154 -0
  103. package/engine/site-theme-urls.mjs +125 -0
  104. package/engine/site-theme.mjs +926 -0
  105. package/engine/sql-tables.mjs +20 -3
  106. package/engine/text-maps.mjs +57 -10
  107. package/engine/typographer.mjs +248 -0
  108. package/engine/verse-metre.mjs +357 -0
  109. package/engine/wikilink-syntax.mjs +29 -47
  110. package/engine/wikilinks.mjs +758 -279
  111. package/engine/word-forms.mjs +364 -0
  112. package/engine/word-rows.mjs +12 -2
  113. package/engine/word-terms.mjs +38 -4
  114. package/manifest.mjs +3 -4
  115. package/package.json +11 -8
  116. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  117. package/site-theme/layouts/base.hbs +31 -0
  118. package/site-theme/layouts/blog-list.hbs +30 -0
  119. package/site-theme/layouts/blog-post.hbs +40 -0
  120. package/site-theme/layouts/home.hbs +35 -0
  121. package/site-theme/layouts/list.hbs +20 -0
  122. package/site-theme/layouts/not-found.hbs +31 -0
  123. package/site-theme/layouts/page.hbs +7 -0
  124. package/site-theme/partials/copy-buttons.hbs +37 -0
  125. package/site-theme/partials/draft-notice.hbs +5 -0
  126. package/site-theme/partials/exturl.hbs +2 -0
  127. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  128. package/site-theme/partials/header.hbs +44 -0
  129. package/site-theme/partials/hero-banner.hbs +11 -0
  130. package/site-theme/partials/infobox-entry.hbs +2 -0
  131. package/site-theme/partials/infobox-section.hbs +31 -0
  132. package/site-theme/partials/infobox-value.hbs +2 -0
  133. package/site-theme/partials/infobox.hbs +18 -0
  134. package/site-theme/partials/list-members.hbs +9 -0
  135. package/site-theme/partials/nav-links.hbs +5 -0
  136. package/site-theme/partials/page-body.hbs +48 -0
  137. package/site-theme/partials/page-list.hbs +14 -0
  138. package/site-theme/partials/related.hbs +24 -0
  139. package/site-theme/partials/search-meta.hbs +6 -0
  140. package/site-theme/partials/search-names.hbs +2 -0
  141. package/site-theme/partials/search.hbs +52 -0
  142. package/site-theme/partials/tags.hbs +4 -0
  143. package/site-theme/partials/toc.hbs +7 -0
  144. package/{hugo-theme → site-theme}/static/css/style.css +106 -14
  145. package/stage.mjs +1 -2
  146. package/types/config.d.mts +8 -4
  147. package/types/content-config.d.mts +15 -15
  148. package/types/engine/asset-shipping.d.mts +5 -2
  149. package/types/engine/base-compiler.d.mts +20 -2
  150. package/types/engine/code-fences.d.mts +3 -3
  151. package/types/engine/content-address.d.mts +1 -1
  152. package/types/engine/content-alerts.d.mts +8 -2
  153. package/types/engine/content-blocks.d.mts +11 -4
  154. package/types/engine/content-expand.d.mts +288 -0
  155. package/types/engine/content-figures.d.mts +11 -3
  156. package/types/engine/content-glyphs.d.mts +85 -0
  157. package/types/engine/content-images.d.mts +6 -7
  158. package/types/engine/content-links.d.mts +1 -1
  159. package/types/engine/content-render.d.mts +199 -0
  160. package/types/engine/content-scripts.d.mts +70 -4
  161. package/types/engine/content-spans.d.mts +1 -1
  162. package/types/engine/content-styles.d.mts +33 -0
  163. package/types/engine/content-symbols.d.mts +35 -0
  164. package/types/engine/content-verse.d.mts +240 -0
  165. package/types/engine/core-report.d.mts +45 -0
  166. package/types/engine/core-vocabulary.d.mts +53 -0
  167. package/types/engine/dictionary-html.d.mts +8 -0
  168. package/types/engine/dictionary-rows.d.mts +10 -0
  169. package/types/engine/field-reference.d.mts +2 -2
  170. package/types/engine/heading-attributes.d.mts +20 -0
  171. package/types/engine/helpers.d.mts +3 -10
  172. package/types/engine/homepage.d.mts +11 -24
  173. package/types/engine/html-minify.d.mts +9 -0
  174. package/types/engine/index.d.mts +2 -1
  175. package/types/engine/key-case-collisions.d.mts +28 -0
  176. package/types/engine/language-data.d.mts +0 -2
  177. package/types/engine/language-terms.d.mts +24 -2
  178. package/types/engine/logographic.d.mts +45 -0
  179. package/types/engine/map-lexicon.d.mts +66 -13
  180. package/types/engine/map-places.d.mts +8 -0
  181. package/types/engine/markdown-expressions.d.mts +0 -2
  182. package/types/engine/metre-declaration.d.mts +39 -0
  183. package/types/engine/names.d.mts +34 -3
  184. package/types/engine/note-matter.d.mts +48 -0
  185. package/types/engine/pdf-render.d.mts +6 -2
  186. package/types/engine/post-fields.d.mts +44 -0
  187. package/types/engine/preview-index.d.mts +72 -0
  188. package/types/engine/preview-source.d.mts +51 -0
  189. package/types/engine/read-stdin.d.mts +11 -0
  190. package/types/engine/run-rows.d.mts +2 -2
  191. package/types/engine/run-words.d.mts +0 -15
  192. package/types/engine/script-emphasis-terms.d.mts +21 -0
  193. package/types/engine/script-emphasis.d.mts +84 -0
  194. package/types/engine/script-font-delivery.d.mts +4 -4
  195. package/types/engine/site-build.d.mts +71 -113
  196. package/types/engine/site-config.d.mts +14 -134
  197. package/types/engine/site-files.d.mts +73 -0
  198. package/types/engine/site-index.d.mts +11 -3
  199. package/types/engine/site-preview.d.mts +15 -19
  200. package/types/engine/site-render.d.mts +171 -0
  201. package/types/engine/site-root.d.mts +12 -4
  202. package/types/engine/site-search.d.mts +54 -0
  203. package/types/engine/site-serve.d.mts +44 -0
  204. package/types/engine/site-theme-banner.d.mts +51 -0
  205. package/types/engine/site-theme-urls.d.mts +60 -0
  206. package/types/engine/site-theme.d.mts +127 -0
  207. package/types/engine/sql-tables.d.mts +4 -1
  208. package/types/engine/text-maps.d.mts +17 -3
  209. package/types/engine/typographer.d.mts +11 -0
  210. package/types/engine/verse-metre.d.mts +131 -0
  211. package/types/engine/wikilink-syntax.d.mts +30 -48
  212. package/types/engine/wikilinks.d.mts +330 -14
  213. package/types/engine/word-forms.d.mts +83 -0
  214. package/types/manifest.d.mts +3 -4
  215. package/types/stage.d.mts +1 -2
  216. package/types/vendor/markdownlint.d.mts +8 -0
  217. package/vendor/markdownlint.mjs +20388 -0
  218. package/engine/content-poetry.mjs +0 -240
  219. package/engine/web-wikilinks.mjs +0 -594
  220. package/hugo-theme/layouts/404.html +0 -85
  221. package/hugo-theme/layouts/_default/baseof.html +0 -67
  222. package/hugo-theme/layouts/_default/list.html +0 -139
  223. package/hugo-theme/layouts/_default/single.html +0 -4
  224. package/hugo-theme/layouts/blog/list.html +0 -54
  225. package/hugo-theme/layouts/blog-post/single.html +0 -36
  226. package/hugo-theme/layouts/index.html +0 -75
  227. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  228. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  229. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  230. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  231. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  232. package/hugo-theme/layouts/partials/header.html +0 -35
  233. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  234. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  235. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  236. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  237. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  238. package/hugo-theme/layouts/partials/infobox.html +0 -57
  239. package/hugo-theme/layouts/partials/page-body.html +0 -152
  240. package/hugo-theme/layouts/partials/related.html +0 -138
  241. package/hugo-theme/layouts/partials/search.html +0 -84
  242. package/hugo-theme/layouts/partials/site-url.html +0 -32
  243. package/hugo-theme/layouts/partials/tags.html +0 -30
  244. package/hugo-theme/layouts/partials/toc.html +0 -28
  245. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  246. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  247. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  248. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  249. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  250. package/hugo-theme/theme.toml +0 -11
  251. package/types/engine/content-poetry.d.mts +0 -58
  252. package/types/engine/web-wikilinks.d.mts +0 -107
  253. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  254. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  255. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  256. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  257. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  258. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -216,6 +216,95 @@ export function headingAttributesPlugin(md) {
216
216
  });
217
217
  }
218
218
 
219
+ /**
220
+ * The id a heading's text gives it on the web: GitHub's form, lower case, each
221
+ * space or hyphen a hyphen, every character that is not a letter, a digit or
222
+ * an underscore dropped.
223
+ *
224
+ * @param {string} text - The heading's text, without its markup.
225
+ * @returns {string} The id, or `heading` when nothing is left of the text.
226
+ */
227
+ export function headingId(text) {
228
+ let id = "";
229
+ for (const char of String(text ?? "").trim()) {
230
+ if (char === " " || char === "-") id += "-";
231
+ else if (char === "_" || /[\p{L}\p{Nd}]/u.test(char)) id += char.toLowerCase();
232
+ }
233
+ return id || "heading";
234
+ }
235
+
236
+ /** The tokens whose content is a heading's text. */
237
+ const TEXT_TOKENS = new Set(["text", "code_inline", "typographic"]);
238
+
239
+ /**
240
+ * The text a heading's id is made from.
241
+ *
242
+ * Its words, code and punctuation; a line break as a newline; no markup. An
243
+ * element inside the heading — emphasis, a link, an image — gives only its
244
+ * first piece of text, which ends at the first punctuation the page sets
245
+ * typographically, so `[Nkaru'thar](…)` gives `Nkaru`.
246
+ *
247
+ * @param {object[]} children - The heading's inline tokens.
248
+ * @returns {string} The text.
249
+ */
250
+ function headingText(children) {
251
+ const list = children ?? [];
252
+ /** The first piece of text inside the element opening at `index`. */
253
+ const first = (index) => {
254
+ const token = list[index];
255
+ if (!token || token.nesting === -1) return "";
256
+ if (TEXT_TOKENS.has(token.type)) return token.content;
257
+ if (token.type === "image") return headingText(token.children?.slice(0, 1));
258
+ if (token.nesting === 1) return first(index + 1);
259
+ return "";
260
+ };
261
+ let text = "";
262
+ for (let i = 0; i < list.length; i++) {
263
+ const token = list[i];
264
+ if (TEXT_TOKENS.has(token.type)) text += token.content;
265
+ else if (token.type === "softbreak" || token.type === "hardbreak") text += "\n";
266
+ else if (token.type === "image") text += headingText(token.children?.slice(0, 1));
267
+ else if (token.nesting === 1) {
268
+ text += first(i + 1);
269
+ for (let depth = 1; depth > 0 && i + 1 < list.length;) {
270
+ i++;
271
+ depth += list[i].nesting;
272
+ }
273
+ }
274
+ }
275
+ return text;
276
+ }
277
+
278
+ /**
279
+ * Give every heading an id, so a page's contents list and a reader's link can
280
+ * name any section.
281
+ *
282
+ * An authored `{#id}` is kept, and every other heading takes {@link headingId}
283
+ * of its text. A repeated id takes `-1`, `-2` and so on, counted over every id
284
+ * on the page that came before it.
285
+ *
286
+ * @param {import("markdown-it")} md - Markdown parser to extend.
287
+ */
288
+ export function headingIdsPlugin(md) {
289
+ md.core.ruler.push("heading_ids", (state) => {
290
+ const taken = new Set();
291
+ const tokens = state.tokens;
292
+ for (let i = 0; i < tokens.length; i++) {
293
+ if (tokens[i].type !== "heading_open") continue;
294
+ const authored = tokens[i].attrGet("id");
295
+ if (authored) {
296
+ taken.add(authored);
297
+ continue;
298
+ }
299
+ const base = headingId(headingText(tokens[i + 1]?.children));
300
+ let id = base;
301
+ for (let n = 1; taken.has(id); n++) id = `${base}-${n}`;
302
+ taken.add(id);
303
+ tokens[i].attrSet("id", id);
304
+ }
305
+ });
306
+ }
307
+
219
308
  /**
220
309
  * Parse one line as a heading, and say whether it starts a journal page.
221
310
  *
@@ -25,8 +25,7 @@
25
25
  * journals.mjs, actors.mjs).
26
26
  */
27
27
 
28
- import { renderAlerts } from "./content-alerts.mjs";
29
- import { renderRuns } from "./content-scripts.mjs";
28
+ import { createRenderer } from "./content-render.mjs";
30
29
  import { buildReferenceTargets } from "./reference-targets.mjs";
31
30
  import { standingsDigest } from "./standings.mjs";
32
31
 
@@ -39,12 +38,7 @@ import crypto from "crypto";
39
38
  import path from "path";
40
39
  import yaml from "yaml";
41
40
  import unidecode from "unidecode";
42
- import markdownit from "markdown-it";
43
- import footnotePlugin from "markdown-it-footnote";
44
- import deflistPlugin from "markdown-it-deflist";
45
- import { dictionaryListPlugin } from "./definition-list.mjs";
46
- import { iconPlugin } from "./content-icons.mjs";
47
- import { imagePlugin, imagesIn } from "./content-images.mjs";
41
+ import { imagesIn } from "./content-images.mjs";
48
42
  import { embedRole, resolveEmbeds } from "./content-embeds.mjs";
49
43
  import { collectAssetRecords } from "./asset-index.mjs";
50
44
  import { assetImageInfoByPathname } from "./art-fields.mjs";
@@ -67,19 +61,15 @@ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs
67
61
  // a record needs none of that.
68
62
  import { authoredFrontmatter, isAssetRecord, isNoteRecord, noteFile } from "./index-records.mjs";
69
63
  import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
70
- // One vocabulary of link findings, and one message per class, so the three
71
- // resolvers cannot word the same defect differently.
64
+ // One vocabulary of link findings, and one message per class, so no two
65
+ // builds word the same defect differently.
72
66
  import { linkFindingMessage } from "./wikilink-syntax.mjs";
73
67
  // The declared tag vocabulary, which is where `draft` is stated. Read
74
68
  // from there rather than respelt, so the tag and its one reader cannot drift.
75
69
  import { isGmNote, isDraftNote } from "./note-vocabulary.mjs";
76
70
  import { DERIVED_PACKED_TYPES, NEVER_PACKED_TYPES } from "./note-claims.mjs";
77
71
  import { expandContentTables } from "./content-tables.mjs";
78
- import { spanMarkdownPlugin } from "./content-spans.mjs";
79
- import { dictionaryEntryPlugin } from "./dictionary-layout.mjs";
80
- import { renderBlocks } from "./content-blocks.mjs";
81
- import { headingAttributesPlugin } from "./heading-attributes.mjs";
82
- import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
72
+ import { scanFigures } from "./content-figures.mjs";
83
73
  import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
84
74
  // The pure `sohl:` frontmatter readers live in a leaf module so the item-type
85
75
  // registry can import them without reaching back through this one.
@@ -100,72 +90,68 @@ export {
100
90
  let localAssetImageInfoCache;
101
91
 
102
92
  /**
103
- * The markdown renderer every surface shares.
93
+ * The Foundry renderer: the shared configuration of
94
+ * {@link module:engine/content-render.createRenderer} for the `foundry`
95
+ * target.
104
96
  *
105
97
  * `html: true` is long-standing and load-bearing — notes carry raw blocks — and
106
98
  * it is also why {@link module:engine/content-icons} exists rather than an
107
99
  * instruction to write `<i class="fa-solid …">` by hand: that would render on
108
100
  * the two HTML surfaces and be silently dropped by the third.
101
+ *
102
+ * The icon registry and the image resolvers are read per render, not at import:
103
+ * this constant is built before any configuration is read, and a package's own
104
+ * icons live in the configuration. A tree with none — or a caller with no
105
+ * configuration to find — falls back to the shipped table and to the authored
106
+ * pathname.
109
107
  */
110
- export const md = markdownit({ html: true })
111
- .use(footnotePlugin)
112
- .use(deflistPlugin)
113
- .use(dictionaryListPlugin)
114
- .use(spanMarkdownPlugin, { spoilers: true })
115
- .use(dictionaryEntryPlugin)
116
- // A heading's attribute block, written onto the heading element rather than
117
- // typeset into it — the reading the website's renderer does for itself.
118
- .use(headingAttributesPlugin)
119
- .use(
120
- // Resolved per render, not at import: this constant is built before any
121
- // configuration is read, and a package's own icons live in the
122
- // configuration. A tree with none — or a caller with no configuration to
123
- // find — falls back to the shipped table.
124
- iconPlugin(() => {
125
- try {
126
- return loadPackConfig().icons;
127
- } catch {
128
- return undefined;
129
- }
130
- }),
131
- )
108
+ const renderer = createRenderer("foundry", {
109
+ icons: () => {
110
+ try {
111
+ return loadPackConfig().icons;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ },
132
116
  // The width and position an image states, honoured as a figure. The
133
117
  // vocabularies are closed and need no configuration; the address does, and
134
118
  // is resolved per render for the reason the icon registry is — Foundry
135
119
  // serves a file from inside the install, so a body image's address is
136
120
  // translated by the same rule `img:` follows.
137
- .use(
138
- imagePlugin(
139
- (src) => {
140
- // **Reported upstream, never here.** A renderer has no channel to
141
- // report through, and this one runs inside the very passes whose
142
- // job is to collect findings — a throw would take the whole lint
143
- // down and lose every other finding in the tree. So nothing reaches
144
- // this point unreported: {@link convertNoteWikilinks} refuses a body
145
- // image with no address inside the install before a compiler renders
146
- // one, and the address passes report the same pathname with a line
147
- // and a column. What is left here is a fallback for a caller with no
148
- // configuration to resolve against, where the authored pathname is
149
- // the most honest thing to emit.
150
- try {
151
- return resolveImg(src, loadPackConfig()) ?? src;
152
- } catch {
153
- return src;
154
- }
155
- },
156
- (src) => {
157
- // A body image is free to write the bare, own-package form,
158
- // so the address is normalized to the pathname the cache
159
- // below keys by before it is looked up.
160
- try {
161
- const key = assetPathnameKey(src, loadPackConfig());
162
- return key ? localAssetImageInfo().get(key) : undefined;
163
- } catch {
164
- return undefined;
165
- }
166
- },
167
- ),
168
- );
121
+ images: {
122
+ resolveSrc: (src) => {
123
+ // **Reported upstream, never here.** A renderer has no channel to
124
+ // report through, and this one runs inside the very passes whose
125
+ // job is to collect findings — a throw would take the whole lint
126
+ // down and lose every other finding in the tree. So nothing reaches
127
+ // this point unreported: {@link convertNoteWikilinks} refuses a body
128
+ // image with no address inside the install before a compiler renders
129
+ // one, and the address passes report the same pathname with a line
130
+ // and a column. What is left here is a fallback for a caller with no
131
+ // configuration to resolve against, where the authored pathname is
132
+ // the most honest thing to emit.
133
+ try {
134
+ return resolveImg(src, loadPackConfig()) ?? src;
135
+ } catch {
136
+ return src;
137
+ }
138
+ },
139
+ lookupAsset: (src) => {
140
+ // A body image is free to write the bare, own-package form,
141
+ // so the address is normalized to the pathname the cache
142
+ // below keys by before it is looked up.
143
+ try {
144
+ const key = assetPathnameKey(src, loadPackConfig());
145
+ return key ? localAssetImageInfo().get(key) : undefined;
146
+ } catch {
147
+ return undefined;
148
+ }
149
+ },
150
+ },
151
+ });
152
+
153
+ /** The Foundry renderer's parser, for callers that parse or render a fragment. */
154
+ export const md = renderer.md;
169
155
 
170
156
  /**
171
157
  * The pictures this package's own tree ships, by the pathname their address
@@ -194,25 +180,6 @@ function localAssetImageInfo() {
194
180
  return localAssetImageInfoCache;
195
181
  }
196
182
 
197
- md.renderer.rules.footnote_block_open = () =>
198
- '<section class="footnotes"><h2>Footnotes</h2><ol class="footnotes-list">\n';
199
- md.renderer.rules.footnote_caption = (tokens, idx, _options, env) => {
200
- const meta = tokens[idx].meta;
201
- const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
202
- return `[${number}${meta.subId > 0 ? `:${meta.subId}` : ""}]`;
203
- };
204
- md.renderer.rules.footnote_anchor_name = (tokens, idx, _options, env) => {
205
- const meta = tokens[idx].meta;
206
- const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
207
- return `${env.docId ? `-${env.docId}-` : ""}${number}`;
208
- };
209
- md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
210
- const meta = tokens[idx].meta;
211
- const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
212
- const id = renderer.rules.footnote_anchor_name(tokens, idx, options, env, renderer);
213
- return `<li id="fn${id}" class="footnote-item" value="${number}">`;
214
- };
215
-
216
183
  /**
217
184
  * Render a note body with Foundry's named-block markup.
218
185
  *
@@ -232,12 +199,7 @@ md.renderer.rules.footnote_open = (tokens, idx, options, env, renderer) => {
232
199
  * @returns {string} The rendered HTML.
233
200
  */
234
201
  export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, resolveRole, runs) {
235
- const marked = runs ? renderRuns(body, runs, undefined, { target: "foundry" }).markdown : body;
236
- const figured = renderFigureBlocks(marked, (block) => md.render(block), figures, {
237
- resolveRole,
238
- });
239
- const blocks = renderBlocks(figured.markdown, "foundry");
240
- return md.render(renderAlerts(blocks.markdown, "foundry").markdown, { footnoteNumbers, docId });
202
+ return renderer.render(body, { runs, figures, resolveRole, footnoteNumbers, docId });
241
203
  }
242
204
 
243
205
  /**
@@ -983,7 +945,7 @@ export function buildContentLinkIndex(
983
945
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line.
984
946
  * @param {number} [ctx.bodyColumn] - 1-based file column of the same character.
985
947
  * @param {Array<{line: number, generated: boolean}>} [ctx.lineMap] - Which
986
- * authored line each body line came from, from {@link expandNoteTables}.
948
+ * authored line each body line came from, from {@link module:engine/content-tables.expandContentTables}.
987
949
  * @returns {{markdown: string, unresolved: Array<object>}}
988
950
  * @throws {Error} On any link that does not resolve — an unlabelled one, a
989
951
  * target that is not an address, or an address nothing publishes — and on a
@@ -1086,11 +1048,9 @@ export function convertNoteWikilinks(
1086
1048
  }
1087
1049
 
1088
1050
  for (const u of result.unresolved) {
1089
- // Every class fails, and every class is worded by the shared table.
1090
- // The three resolvers read one authored link, so an author who
1091
- // ran the pack build first and the link checker second must not be told
1092
- // two different things about the same mistake — and a class the pack
1093
- // build alone knew how to describe is how they came apart before.
1051
+ // Every class fails, and every class is worded by the shared table,
1052
+ // so an author who ran the pack build first and the link checker second
1053
+ // is told one thing about one mistake.
1094
1054
  //
1095
1055
  // The note's name is appended rather than woven in: the message is the
1096
1056
  // defect, the name is the context this build can add.
@@ -43,8 +43,8 @@
43
43
  * module, which is most of the packages that need a homepage and nothing else.
44
44
  *
45
45
  * **Published at the package root, addressed like every other note.** A
46
- * homepage is written as the mount's `_index.md`, so Hugo's `home` kind renders
47
- * it at `/<package>/` — the address a reader expects a package's front page at,
46
+ * homepage is rendered at `/<package>/` — the address a reader expects a
47
+ * package's front page at,
48
48
  * and the one `package.json`'s `homepage` states. It still declares a
49
49
  * `shortcode` — conventionally {@link HOMEPAGE_SHORTCODE} — because that is what
50
50
  * a link is written with: `[[homepage-root|Text]]` is an ordinary wikilink, and
@@ -96,19 +96,6 @@ export const HOMEPAGE_FIELDS = Object.freeze([]);
96
96
  */
97
97
  export const HOMEPAGE_SHORTCODE = "root";
98
98
 
99
- /**
100
- * The file a homepage is written to, relative to the package's site root.
101
- *
102
- * The mount's own `_index.md`: Hugo renders it as the `home` kind, at
103
- * `baseURL` — which is `/<package>/`, the package's own address. One fixed
104
- * destination rather than one derived from the note's address, because the
105
- * homepage's address *is* the package root; the note's `shortcode` names the
106
- * page in links and decides no file.
107
- *
108
- * @type {string}
109
- */
110
- export const HOMEPAGE_DESTINATION = "_index.md";
111
-
112
99
  /**
113
100
  * Whether a note's frontmatter declares the homepage type.
114
101
  *
@@ -337,8 +324,10 @@ function positionOfType(file) {
337
324
  * authored `name.full` wins, because a front page may greet a reader
338
325
  * differently from a package browser.
339
326
  *
340
- * Falls back to `contentPackage` last, so a package that has no manifest of its
341
- * own still yields a titled page rather than a blank heading.
327
+ * A package with no manifest title — a `documentation` package has no manifest
328
+ * at all — takes `site.title`, the name its site is known by. It falls back to
329
+ * `contentPackage` last, so a package that states neither still yields a
330
+ * titled page rather than a blank heading.
342
331
  *
343
332
  * @param {object|null|undefined} fm - The note's frontmatter.
344
333
  * @param {object} config - The resolved configuration.
@@ -351,7 +340,9 @@ export function homepageTitle(fm, config) {
351
340
  config?.packageBuild?.manifest
352
341
  );
353
342
  const title = manifest?.title;
354
- return typeof title === "string" && title.trim() ? title : config.contentPackage;
343
+ if (typeof title === "string" && title.trim()) return title;
344
+ const site = config?.site?.title;
345
+ return typeof site === "string" && site.trim() ? site : config.contentPackage;
355
346
  }
356
347
 
357
348
  /**
@@ -359,20 +350,17 @@ export function homepageTitle(fm, config) {
359
350
  *
360
351
  * The note's own, plus the two derived values every emitted page carries: the
361
352
  * resolved `title`, and the package the build derived — no note declares one
362
- * and the theme's breadcrumb partial reads `.Params.package`.
353
+ * and the theme's breadcrumbs read.
363
354
  *
364
- * **No `url` and no `slug`.** Hugo publishes the `home` kind at `baseURL`,
365
- * whose path is already where the package is served, so the page has no
366
- * address to state; a content page states one because its file sits under the
367
- * mount and its address does not.
355
+ * **No `url` and no `slug`.** The homepage publishes at the package root, so
356
+ * the page has no address to state.
368
357
  *
369
- * An authored `aliases` is dropped for the same reason it is on every other
370
- * page: Hugo reads it as URL redirects, so passing it through would publish a
371
- * redirect stub at each one.
358
+ * An authored `aliases` is dropped, as it is on every other page: this build
359
+ * emits no redirects.
372
360
  *
373
361
  * `related` is derived like every other page's — what links to the homepage
374
362
  * is a fact about the rest of the tree — so an authored value is dropped and
375
- * the caller's block, where it has one, is written in its place.
363
+ * the caller's block, where it has one, is set in its place.
376
364
  *
377
365
  * @param {object} fm - The note's frontmatter.
378
366
  * @param {object} options - Options.
@@ -380,7 +368,7 @@ export function homepageTitle(fm, config) {
380
368
  * @param {string} options.title - The resolved title.
381
369
  * @param {object} [options.related] - The homepage's backlinks and mentions,
382
370
  * where the site build found either.
383
- * @returns {object} The frontmatter to write.
371
+ * @returns {object} The frontmatter.
384
372
  */
385
373
  export function homepageFrontmatter(fm, { contentPackage, title, related }) {
386
374
  const data = { ...fm, package: contentPackage, title };
@@ -395,7 +383,7 @@ export function homepageFrontmatter(fm, { contentPackage, title, related }) {
395
383
  *
396
384
  * Reference-style links are deliberately not matched: a homepage is published
397
385
  * verbatim, so a `[x][y]` whose definition sits in the body reaches the reader
398
- * as Hugo renders it, and one whose definition is missing is a defect the
386
+ * as authored, and one whose definition is missing is a defect the
399
387
  * rendered page shows for itself.
400
388
  *
401
389
  * @type {RegExp}
@@ -0,0 +1,70 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **Minified site pages.** {@link minifyHtml} drops what a browser does not
16
+ * read: comments, and every run of white space but one character.
17
+ *
18
+ * What a reader sees is unchanged. A run of white space between two words or
19
+ * two tags still separates them, as one space; it is only made shorter. The
20
+ * places where white space is the content are kept exactly as written: a
21
+ * `<pre>`, a `<textarea>`, a `<script>` or `<style>`, and a verse's lines,
22
+ * which set their spacing as authored. A tag is kept as written too, its
23
+ * attribute values included.
24
+ *
25
+ * @module
26
+ */
27
+
28
+ /** The elements whose content is written out as it stands. */
29
+ const VERBATIM = /^<(pre|textarea|script|style)\b/i;
30
+
31
+ /** A verse's spans, whose white space is set as authored. */
32
+ const VERSE_SPAN = /\bclass\s*=\s*"[^"]*\bverse-(?:line|column|part)\b/i;
33
+
34
+ /**
35
+ * Minify a page.
36
+ *
37
+ * @param {string} html - A complete document.
38
+ * @returns {string} The same document without comments and with each run of
39
+ * white space outside the verbatim elements cut to one character: a newline
40
+ * where the run held one, a space otherwise.
41
+ */
42
+ export function minifyHtml(html) {
43
+ const source = String(html ?? "");
44
+ const pieces =
45
+ /<!--[\s\S]*?-->|<(pre|textarea|script|style)\b[^>]*>[\s\S]*?<\/\1\s*>|<[^>]*>|[^<]+|</gi;
46
+ const verse = [];
47
+ let out = "";
48
+ for (const [piece] of source.matchAll(pieces)) {
49
+ if (piece.startsWith("<!--")) {
50
+ // A conditional comment is read by the browser it names.
51
+ if (piece.startsWith("<!--[if")) out += piece;
52
+ continue;
53
+ }
54
+ if (piece.startsWith("<")) {
55
+ if (VERBATIM.test(piece)) {
56
+ out += piece;
57
+ continue;
58
+ }
59
+ if (/^<span\b/i.test(piece)) verse.push(verse.at(-1) || VERSE_SPAN.test(piece));
60
+ else if (/^<\/span\s*>/i.test(piece)) verse.pop();
61
+ out += piece;
62
+ continue;
63
+ }
64
+ out +=
65
+ verse.at(-1) ? piece : (
66
+ piece.replace(/\s+/g, (run) => (run.includes("\n") ? "\n" : " "))
67
+ );
68
+ }
69
+ return out.replace(/^\s+/, "");
70
+ }
package/engine/index.mjs CHANGED
@@ -216,6 +216,12 @@ export * as siteBuild from "./site-build.mjs";
216
216
  /** In-memory page rendering for an editor preview. */
217
217
  export * as sitePreview from "./site-preview.mjs";
218
218
 
219
+ /** What an editor with a content index gives the preview, so it renders as the site does. */
220
+ export * as previewIndex from "./preview-index.mjs";
221
+
222
+ /** The one markdown-it configuration every target renders through. */
223
+ export * as contentRender from "./content-render.mjs";
224
+
219
225
  /** Address rules every content tree is linted against: shape, uniqueness, alias. */
220
226
  export * as contentLint from "./content-lint.mjs";
221
227
 
@@ -244,9 +250,6 @@ export * as pathnames from "./pathnames.mjs";
244
250
  /** Resolving every link in a tree, and the ones that land nowhere. */
245
251
  export * as contentLinks from "./content-links.mjs";
246
252
 
247
- /** Wikilinks resolved to a **web URL** — the site half of the pair below. */
248
- export * as webWikilinks from "./web-wikilinks.mjs";
249
-
250
253
  /** Prepared SQL content tables, expanded into Markdown at compile time. */
251
254
  export * as contentTables from "./content-tables.mjs";
252
255
 
@@ -262,10 +265,10 @@ export * as documentSubtypes from "./document-subtypes.mjs";
262
265
  /** Which types carry documentation of their own, and where it is addressed. */
263
266
  export * as itemDocs from "./item-docs.mjs";
264
267
 
265
- /** Wikilinks resolved to a **Foundry UUID**: qualifiers, the index, rewriting. */
268
+ /** The link resolver: the pack index, the views, and each target's link form. */
266
269
  export * as wikilinks from "./wikilinks.mjs";
267
270
 
268
- /** What a `[[…]]` is, before either resolver decides where it points. */
271
+ /** What a `[[…]]` is, before the resolver decides where it points. */
269
272
  export * as wikilinkSyntax from "./wikilink-syntax.mjs";
270
273
 
271
274
  /** The address index a site build resolves its wikilinks against. */