@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
@@ -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
+ }