@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
@@ -344,7 +344,7 @@ export function escapeHtml(text) {
344
344
  *
345
345
  * **The role class and the `width`/`height` attributes are what a stylesheet
346
346
  * elsewhere keys a role's size on.** This module states none — the measure a
347
- * `portrait` fills on a page is a Hugo theme's or a Foundry system's own
347
+ * `portrait` fills on a page is a site theme's or a Foundry system's own
348
348
  * decision — and emits only what the resolved asset record states: a role
349
349
  * class when the picture declares one, and the attributes whenever the
350
350
  * picture resolved to an asset record at all, blank for a vector. The
@@ -684,12 +684,11 @@ export function lintContentImages(contentBase, { skipDirectories = [], config, s
684
684
  /**
685
685
  * Rewrite every block image in a body into the figure the website publishes.
686
686
  *
687
- * **Hugo is handed markdown, not a rendered page.** The site emitter writes a
688
- * note's body through verbatim, so a `{…}` directive left in it reaches the
689
- * page as its own literal braces: Goldmark's block-attribute parser is off, and
690
- * turning it on would accept `style` and `id` alongside the closed vocabularies,
691
- * which is the injection surface this rule exists to avoid. So the directive is
692
- * resolved here, into markup Goldmark passes through.
687
+ * **The directive is resolved before the markdown is parsed.** A `{…}`
688
+ * directive left in the body would reach the page as its own literal braces: a
689
+ * block-attribute parser would accept `style` and `id` alongside the closed
690
+ * vocabularies, which is the injection surface this rule exists to avoid. So
691
+ * the directive is resolved here, into markup the renderer passes through.
693
692
  *
694
693
  * An image whose directive does not parse is left exactly as written, so the
695
694
  * page shows the braces and the lint says why — the same visible degradation an
@@ -89,7 +89,7 @@ import {
89
89
  readCanonicalKey,
90
90
  } from "./content-address.mjs";
91
91
  import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
92
- import { frontmatterWikilinks, slugify } from "./web-wikilinks.mjs";
92
+ import { frontmatterWikilinks, slugify } from "./wikilinks.mjs";
93
93
  import { homepageAddresses, isHomepage } from "./homepage.mjs";
94
94
  import { isEventAnchor, parseWikilink, WIKILINK } from "./wikilink-syntax.mjs";
95
95
  import { readQualifier } from "./wikilinks.mjs";
@@ -677,7 +677,7 @@ export function buildLinkIndex(
677
677
  linksOf,
678
678
  embedsOf,
679
679
  /**
680
- * Resolve a link target the way both builds do, or `undefined`. Every
680
+ * Resolve a link target the way every build does, or `undefined`. Every
681
681
  * link is an address, so this is {@link resolveAddress} under the name
682
682
  * the walkers use.
683
683
  */
@@ -955,7 +955,7 @@ export function auditHomepageLinks(index) {
955
955
  * `"unresolved"`, never `"ambiguous"`: every target here expands to one
956
956
  * canonical address before anything is looked up, so there is no candidate
957
957
  * set for two packages to claim — and every reason is an **error**: the
958
- * three resolvers agree on severity for every class.
958
+ * checker and the resolver agree on severity for every class.
959
959
  */
960
960
  export function auditLinks(index) {
961
961
  const { notes, anchors, linksOf, embedsOf, resolve, manifestHit, isAddress } = index;
@@ -43,7 +43,7 @@
43
43
  *
44
44
  * **There is deliberately no third rule** requiring every note to repeat its
45
45
  * own `type-shortcode` address in a top-level `aliases:` list. It would serve
46
- * no build reader: both resolvers parse the hyphen
46
+ * no build reader: the resolver and the checker parse the hyphen
47
47
  * qualifier themselves.
48
48
  *
49
49
  * **What is deliberately absent.** Corpus reachability — "every Rules document
@@ -0,0 +1,453 @@
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
+ * **The renderer.** {@link createRenderer} builds the one markdown-it
16
+ * configuration every target reads, so a rule such as "an alert is a labelled
17
+ * callout" is written once and holds everywhere.
18
+ *
19
+ * ## One configuration, four targets
20
+ *
21
+ * | | `foundry` | `web` | `preview` | `book` |
22
+ * | --- | --- | --- | --- | --- |
23
+ * | the shared plugins | all | all | all | all that read tokens |
24
+ * | an inline `[text]{.secret}` | a focusable spoiler | a toggle button | a focusable spoiler, or its highlighted source | read as a span |
25
+ * | a `.secret` block | a section the GM reads | a closed disclosure | a closed disclosure | a labelled box |
26
+ * | a `.secret` heading | a page withheld by ownership | a closed disclosure | a closed disclosure | a labelled box |
27
+ * | a heading's id | as authored | as authored, or its text's | as authored | as authored |
28
+ * | quotes, dashes and ellipses | as written | typographic | as written | as written, set by Typst |
29
+ * | raw HTML in a note | passed through | passed through | shown as text | shown as text |
30
+ * | the output | HTML | HTML | HTML | the token stream, for the Typst stage |
31
+ *
32
+ * A target differs from the others only in that table. The shared plugins are
33
+ * footnotes, definition lists, dictionary rows, bracket spans, dictionary
34
+ * entries, heading attributes, icons and body images; each renderer's `plugins`
35
+ * lists what its target installs.
36
+ *
37
+ * ## The stages of a render
38
+ *
39
+ * A note reaches {@link Renderer#render} with its index-dependent constructs
40
+ * already resolved by {@link module:engine/content-expand.expandContent}.
41
+ * {@link Renderer#prepare} then writes the constructs markdown-it has no
42
+ * syntax for as HTML between blank lines, in this order:
43
+ *
44
+ * 1. **Marked text** — a run's language, face and mapping, from the
45
+ * configuration's script notes.
46
+ * 2. **Captioned items** — each figure's wrapper and label. Its number, kind
47
+ * and role come from the figure scan expansion made, so a figure is
48
+ * numbered once; see {@link module:engine/content-figures.renderFigureBlocks}.
49
+ * 3. **Named blocks and verse** — `:::` blocks and verse fences.
50
+ * 4. **Alerts.**
51
+ * 5. **Spans and withheld sections** — written here on the `web` target; the
52
+ * other targets read spans and headings as tokens.
53
+ *
54
+ * The book skips these stages. It reads the parser's tokens and writes Typst,
55
+ * so its own output stage replaces them.
56
+ *
57
+ * ## Code
58
+ *
59
+ * A fenced block that names a language highlight.js knows is highlighted at
60
+ * build time, with class-based markup that `content-base.css` themes.
61
+ *
62
+ * @module
63
+ */
64
+
65
+ import MarkdownIt from "markdown-it";
66
+ import footnotePlugin from "markdown-it-footnote";
67
+ import deflistPlugin from "markdown-it-deflist";
68
+ import hljs from "highlight.js/lib/common";
69
+
70
+ import { dictionaryListPlugin } from "./definition-list.mjs";
71
+ import { alertMarkdownPlugin, renderAlerts, scanAlerts } from "./content-alerts.mjs";
72
+ import { renderBlocks, renderWithheldSections } from "./content-blocks.mjs";
73
+ import { renderFigureBlocks } from "./content-figures.mjs";
74
+ import { iconPlugin } from "./content-icons.mjs";
75
+ import { imagePlugin } from "./content-images.mjs";
76
+ import { renderRuns } from "./content-scripts.mjs";
77
+ import { renderSpans, spanMarkdownPlugin } from "./content-spans.mjs";
78
+ import { cartoucheFigures, renderSymbolSpans } from "./content-symbols.mjs";
79
+ import { inlineVerse, verseStanzas } from "./content-verse.mjs";
80
+ import { replaceOutsideCode } from "./code-fences.mjs";
81
+ import { dictionaryEntryPlugin } from "./dictionary-layout.mjs";
82
+ import { headingAttributesPlugin, headingIdsPlugin } from "./heading-attributes.mjs";
83
+ import { INERT_BRACKET, previewSourcePlugin } from "./preview-source.mjs";
84
+ import { typographerPlugin } from "./typographer.mjs";
85
+
86
+ /** The targets a renderer is built for. */
87
+ export const RENDER_TARGETS = Object.freeze(["foundry", "web", "preview", "book"]);
88
+
89
+ /**
90
+ * What each target writes into {@link renderRuns} as its target: only the
91
+ * site's pages carry the search marker.
92
+ */
93
+ const RUN_TARGET = Object.freeze({
94
+ foundry: "foundry",
95
+ web: "site",
96
+ preview: "foundry",
97
+ book: "book",
98
+ });
99
+
100
+ /**
101
+ * Highlight a fenced block's code for the language it names.
102
+ *
103
+ * @param {string} code - The block's text.
104
+ * @param {string} language - The fence's first word.
105
+ * @returns {string} The code as highlight.js's class-based markup, or `""` when
106
+ * no language is named or highlight.js does not know it, which markdown-it
107
+ * reads as "escape it as plain text".
108
+ */
109
+ export function highlightCode(code, language) {
110
+ const name = String(language ?? "").toLowerCase();
111
+ if (!name || !hljs.getLanguage(name)) return "";
112
+ return hljs.highlight(code, { language: name, ignoreIllegals: true }).value;
113
+ }
114
+
115
+ /**
116
+ * Parse the verse a `verse` fence holds as inline Markdown.
117
+ *
118
+ * A fence is literal to the block parser; a poem's lines are prose with
119
+ * inline Markdown. The lines are parsed before footnote definitions are
120
+ * collected, with the surrounding note's env, so a reference in a verse
121
+ * resolves against definitions written outside the fence.
122
+ *
123
+ * @param {import("markdown-it")} md - Markdown parser to extend.
124
+ */
125
+ function verseInlinePlugin(md) {
126
+ md.core.ruler.after("inline", "verse_inline", (state) => {
127
+ for (const token of state.tokens) {
128
+ if (token.type !== "fence" || !/^verse(?:\s|$)/.test(token.info?.trim() || ""))
129
+ continue;
130
+ token.meta ??= {};
131
+ token.meta.verseStanzas = verseStanzas(token.content).map((stanza) =>
132
+ stanza.map(({ text, level }) => {
133
+ const children = [];
134
+ state.md.inline.parse(inlineVerse(text), state.md, state.env, children);
135
+ return { children, level };
136
+ }),
137
+ );
138
+ }
139
+ });
140
+ }
141
+
142
+ /** The tag {@link renderRuns} writes for emphasis in a script. */
143
+ const SCRIPT_EMPHASIS_OPEN = /^<(em|strong) class="emph-(rubric|overline)">/;
144
+
145
+ /**
146
+ * Read the emphasis a script run's convention sets: an `<em>` or a `<strong>`
147
+ * carrying `emph-rubric` or `emph-overline`, which {@link renderRuns} writes,
148
+ * and the closing tag that matches one. Nothing else that looks like HTML is
149
+ * read. The book parses with HTML off, and this is the one tag it reads.
150
+ *
151
+ * @param {import("markdown-it")} md - The parser.
152
+ */
153
+ function scriptEmphasisPlugin(md) {
154
+ md.inline.ruler.before("text", "script_emphasis", (state, silent) => {
155
+ if (state.src.charCodeAt(state.pos) !== 0x3c) return false;
156
+ const rest = state.src.slice(state.pos);
157
+ const open = SCRIPT_EMPHASIS_OPEN.exec(rest);
158
+ if (open) {
159
+ if (!silent) {
160
+ const token = state.push("script_emphasis_open", open[1], 1);
161
+ token.meta = { convention: open[2] };
162
+ }
163
+ state.pos += open[0].length;
164
+ return true;
165
+ }
166
+ const close = /^<\/(em|strong)>/.exec(rest);
167
+ if (!close) return false;
168
+ let depth = 0;
169
+ for (const token of state.tokens) {
170
+ if (token.type === "script_emphasis_open" && token.tag === close[1]) depth++;
171
+ else if (token.type === "script_emphasis_close" && token.tag === close[1]) depth--;
172
+ }
173
+ if (depth <= 0) return false;
174
+ if (!silent) state.push("script_emphasis_close", close[1], -1);
175
+ state.pos += close[0].length;
176
+ return true;
177
+ });
178
+ }
179
+
180
+ /**
181
+ * The footnote section's markup, shared by every HTML target: a heading, a
182
+ * list, and anchors and numbers that follow the note-wide numbering `env`
183
+ * carries.
184
+ *
185
+ * @param {import("markdown-it")} md - Markdown parser to extend.
186
+ */
187
+ function footnoteMarkup(md) {
188
+ const rules = md.renderer.rules;
189
+ rules.footnote_block_open = () =>
190
+ '<section class="footnotes"><h2>Footnotes</h2><ol class="footnotes-list">\n';
191
+ rules.footnote_caption = (tokens, idx, _options, env) => {
192
+ const meta = tokens[idx].meta;
193
+ const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
194
+ return `[${number}${meta.subId > 0 ? `:${meta.subId}` : ""}]`;
195
+ };
196
+ rules.footnote_anchor_name = (tokens, idx, _options, env) => {
197
+ const meta = tokens[idx].meta;
198
+ const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
199
+ return `${env.docId ? `-${env.docId}-` : ""}${number}`;
200
+ };
201
+ rules.footnote_open = (tokens, idx, options, env, renderer) => {
202
+ const meta = tokens[idx].meta;
203
+ const number = env.footnoteNumbers?.get(meta.label) ?? meta.id + 1;
204
+ const id = renderer.rules.footnote_anchor_name(tokens, idx, options, env, renderer);
205
+ return `<li id="fn${id}" class="footnote-item" value="${number}">`;
206
+ };
207
+ }
208
+
209
+ /**
210
+ * An autolink's opening: `<scheme:…>` or `<name@host>`, which markdown reads
211
+ * as a link and never as a tag.
212
+ */
213
+ const AUTOLINK = /<(?:[A-Za-z][A-Za-z0-9+.-]{1,31}:[^\s<>]*|[^\s<>@]+@[^\s<>]+)>/y;
214
+
215
+ /**
216
+ * Make the raw HTML an author wrote inert, leaving what markdown reads as a
217
+ * link and anything inside code as written.
218
+ *
219
+ * The `preview` target parses with HTML on, because the stages of
220
+ * {@link Renderer#prepare} write HTML between blank lines. A tag the author
221
+ * wrote must still read as text, so its opening bracket becomes
222
+ * {@link INERT_BRACKET} first, before the stages run and before expansion
223
+ * writes its own markup; the preview renderer writes it back as `&lt;`, and
224
+ * its source view as `<`.
225
+ *
226
+ * @param {string} markdown - Authored markdown.
227
+ * @returns {string} The same markdown with each authored `<` that opens a tag,
228
+ * comment, declaration or processing instruction replaced.
229
+ */
230
+ export function inertHtml(markdown) {
231
+ return replaceOutsideCode(markdown, /<(?=[A-Za-z/!?])/g, (match, ...rest) => {
232
+ const offset = rest.at(-2);
233
+ const source = rest.at(-1);
234
+ // `](<destination>)` is a link destination, never a tag.
235
+ if (source.slice(offset - 2, offset) === "](") return match;
236
+ AUTOLINK.lastIndex = offset;
237
+ return AUTOLINK.test(source) ? match : INERT_BRACKET;
238
+ });
239
+ }
240
+
241
+ /**
242
+ * The preview's alerts that have a finding, written back as their authored
243
+ * source in a block, so an editor shows an unfinished alert instead of a
244
+ * guess at one.
245
+ *
246
+ * @param {string} markdown - The buffer's markdown.
247
+ * @returns {string} The markdown with each alert holding a finding as a
248
+ * `<pre>` of its lines.
249
+ */
250
+ function sourceMalformedAlerts(markdown) {
251
+ const { errors, blocks } = scanAlerts(markdown);
252
+ if (!errors.length) return markdown;
253
+ const lines = markdown.split("\n");
254
+ const faulted = blocks
255
+ .filter((block) =>
256
+ errors.some((error) => error.line >= block.start + 1 && error.line <= block.end + 1),
257
+ )
258
+ .sort((a, b) => b.start - a.start);
259
+ const escape = (value) =>
260
+ value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
261
+ for (const block of faulted) {
262
+ const raw = lines.slice(block.start, block.end + 1).join("\n");
263
+ lines.splice(
264
+ block.start,
265
+ block.end - block.start + 1,
266
+ "",
267
+ `<pre>${escape(raw).replaceAll(INERT_BRACKET, "&lt;")}</pre>`,
268
+ "",
269
+ );
270
+ }
271
+ return lines.join("\n");
272
+ }
273
+
274
+ /**
275
+ * How a target's pictures resolve.
276
+ *
277
+ * @typedef {object} ImageSources
278
+ * @property {(src: string) => string} [resolveSrc] - From a body image's address
279
+ * to the `src` the target draws.
280
+ * @property {(src: string) => ({role?: string, width?: number|"", height?: number|""}|undefined)} [lookupAsset] -
281
+ * From an address to the asset it names.
282
+ */
283
+
284
+ /**
285
+ * What a renderer is built with.
286
+ *
287
+ * @typedef {object} RendererOptions
288
+ * @property {() => (object|undefined)|object} [icons] - The icon registry, or a
289
+ * function that returns it, called at each render so a configuration read
290
+ * late is honoured. `foundry`, `web`, `preview` and `book`.
291
+ * @property {ImageSources} [images] - How body images resolve. `foundry`, `web`
292
+ * and `preview`.
293
+ * @property {boolean} [source] - `preview`: show wikilinks, spans, expressions
294
+ * and queries as highlighted authored source, for a buffer with no index.
295
+ * @property {object} [sourceContext] - `preview` with `source`: the buffer's
296
+ * `fm`, `file` and `findings`, which the source view reads.
297
+ */
298
+
299
+ /**
300
+ * @typedef {object} RenderEnv
301
+ * @property {import("./content-scripts.mjs").RunContext} [runs] - The package's
302
+ * languages, faces and maps.
303
+ * @property {Array<object>} [figures] - The note's numbered figures, from the
304
+ * figure scan expansion made: matched in order when they are as many as the
305
+ * text holds, by id otherwise.
306
+ * @property {(address: string) => string|undefined} [resolveRole] - From a
307
+ * picture's address to the role its asset declares, used when `figures` is
308
+ * not given.
309
+ * @property {Map<string, number>} [footnoteNumbers] - Footnote numbers shared
310
+ * across the pages of one note.
311
+ * @property {string} [docId] - Threaded into footnote anchors.
312
+ * @property {(src: string) => string} [resolveSrc] - From a glyph's or
313
+ * cartouche's authored pathname to the address this render serves, taking
314
+ * the place of the renderer's own image resolver.
315
+ */
316
+
317
+ /**
318
+ * @typedef {object} Renderer
319
+ * @property {RendererTarget} target - The target it renders for.
320
+ * @property {import("markdown-it")} md - The configured parser.
321
+ * @property {readonly string[]} plugins - The names of the plugins installed, in
322
+ * order. Targets that share a configuration share its names.
323
+ * @property {(markdown: string, env?: RenderEnv) => string} prepare - Write the
324
+ * constructs markdown-it has no syntax for as HTML between blank lines.
325
+ * @property {(markdown: string, env?: RenderEnv) => string} render - Prepare,
326
+ * then render to HTML.
327
+ * @property {(markdown: string) => string} renderInline - Render one inline run.
328
+ */
329
+
330
+ /** @typedef {"foundry"|"web"|"preview"|"book"} RendererTarget */
331
+
332
+ /**
333
+ * Build the renderer for one target.
334
+ *
335
+ * @param {RendererTarget} target - What the markdown is rendered for.
336
+ * @param {RendererOptions} [options] - Per-target inputs.
337
+ * @returns {Renderer} The configured parser and its stages.
338
+ * @throws {Error} When `target` is not one of {@link RENDER_TARGETS}.
339
+ */
340
+ export function createRenderer(target, options = {}) {
341
+ if (!RENDER_TARGETS.includes(target))
342
+ throw new Error(
343
+ `\`${target}\` is not a render target; expected ` +
344
+ RENDER_TARGETS.slice(0, -1)
345
+ .map((name) => `\`${name}\``)
346
+ .join(", ") +
347
+ ` or \`${RENDER_TARGETS.at(-1)}\``,
348
+ );
349
+ const book = target === "book";
350
+ const preview = target === "preview";
351
+ const source = preview && Boolean(options.source);
352
+ const md =
353
+ book ?
354
+ new MarkdownIt({ html: false, linkify: false, typographer: false })
355
+ : new MarkdownIt({ html: true, highlight: highlightCode });
356
+ const plugins = [];
357
+ const use = (name, plugin, ...args) => {
358
+ md.use(plugin, ...args);
359
+ plugins.push(name);
360
+ };
361
+
362
+ use("footnotes", footnotePlugin);
363
+ if (book) {
364
+ use("alerts", alertMarkdownPlugin);
365
+ use("spans", spanMarkdownPlugin);
366
+ use("verse-inline", verseInlinePlugin);
367
+ use("script-emphasis", scriptEmphasisPlugin);
368
+ use("definition-lists", deflistPlugin);
369
+ use("icons", iconPlugin(options.icons));
370
+ use("images", imagePlugin());
371
+ } else {
372
+ use("cartouche-figures", cartoucheFigures);
373
+ use("definition-lists", deflistPlugin);
374
+ use("dictionary-rows", dictionaryListPlugin);
375
+ // A `web` page's spans are written as HTML by `prepare`, which this
376
+ // parser reads as inline HTML; the other HTML targets read them as tokens.
377
+ if (!(target === "web"))
378
+ use("spans", spanMarkdownPlugin, source ? { source: true } : { spoilers: true });
379
+ use("dictionary-entries", dictionaryEntryPlugin);
380
+ use("heading-attributes", headingAttributesPlugin);
381
+ // A web page links its contents list to every heading, and sets its
382
+ // punctuation as a printed page does.
383
+ if (target === "web") {
384
+ use("heading-ids", headingIdsPlugin);
385
+ use("typographer", typographerPlugin);
386
+ }
387
+ // A buffer with no index has no registry to draw an icon from.
388
+ if (!source) use("icons", iconPlugin(options.icons));
389
+ use("images", imagePlugin(options.images?.resolveSrc, options.images?.lookupAsset));
390
+ footnoteMarkup(md);
391
+ if (preview)
392
+ use("preview-source", previewSourcePlugin, { ...options.sourceContext, source });
393
+ }
394
+
395
+ const unsupported = () => {
396
+ throw new Error(
397
+ "the `book` target is parsed, not rendered: markdownToTypst reads its tokens",
398
+ );
399
+ };
400
+
401
+ // The preview sets titles and captions with its own parser, so an address
402
+ // in one reads as source like any other.
403
+ const renderInline = preview ? (text) => md.renderInline(text, {}) : undefined;
404
+ const restore = (html) => (preview ? html.replaceAll(INERT_BRACKET, "&lt;") : html);
405
+
406
+ /** Write the constructs markdown-it has no syntax for as HTML. */
407
+ const prepare = (markdown, env = {}) => {
408
+ let text = String(markdown ?? "");
409
+ if (preview) text = sourceMalformedAlerts(text);
410
+ if (env.runs)
411
+ text = renderRuns(text, env.runs, undefined, { target: RUN_TARGET[target] }).markdown;
412
+ // A glyph and a cartouche are pictures on every target that draws
413
+ // HTML, so they are written before any stage renders a fragment. The
414
+ // buffer-only preview shows a glyph as the source it was written in
415
+ // and draws a cartouche.
416
+ const resolveSrc = env.resolveSrc ?? options.images?.resolveSrc;
417
+ text = renderSymbolSpans(text, resolveSrc, { glyphs: !source });
418
+ const figured = renderFigureBlocks(
419
+ text,
420
+ target === "web" ? undefined : (block) => md.render(block),
421
+ env.figures,
422
+ { resolveRole: env.resolveRole, renderInline },
423
+ );
424
+ text = renderAlerts(
425
+ renderBlocks(figured.markdown, target, { renderInline }).markdown,
426
+ target,
427
+ {
428
+ renderInline,
429
+ },
430
+ ).markdown;
431
+ if (target === "web") text = renderSpans(text).markdown;
432
+ return target === "web" ? renderWithheldSections(text) : text;
433
+ };
434
+
435
+ return {
436
+ target,
437
+ md,
438
+ plugins: Object.freeze(plugins),
439
+ prepare: book ? unsupported : prepare,
440
+ render:
441
+ book ? unsupported : (
442
+ (markdown, env = {}) =>
443
+ restore(
444
+ md.render(prepare(markdown, env), {
445
+ footnoteNumbers: env.footnoteNumbers,
446
+ docId: env.docId,
447
+ }),
448
+ )
449
+ ),
450
+ renderInline:
451
+ book ? unsupported : (markdown) => restore(md.renderInline(String(markdown ?? ""))),
452
+ };
453
+ }