@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
@@ -14,10 +14,11 @@
14
14
  import MarkdownIt from "markdown-it";
15
15
  import { alertMarkdownPlugin } from "./content-alerts.mjs";
16
16
  import { slugify } from "./content-slug.mjs";
17
- import { scanPoetry } from "./content-poetry.mjs";
17
+ import { scanVerse } from "./content-verse.mjs";
18
18
  import footnotePlugin from "markdown-it-footnote";
19
19
  import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
20
20
  import { inlineText } from "./name-markup.mjs";
21
+ import { cartoucheProblems, glyphProblems } from "./content-glyphs.mjs";
21
22
 
22
23
  /** Add bracket spans after recognized Markdown links and footnotes.
23
24
  * @param {import("markdown-it")} md - Markdown parser to extend.
@@ -91,6 +92,8 @@ export function spanMarkdownPlugin(md, { spoilers = false, source = false } = {}
91
92
  }
92
93
  }
93
94
  const problems = [...parsed.problems, ...refusedAttributes(parsed.values)];
95
+ if (parsed.classes.includes("glyph")) problems.push(...glyphProblems(parsed));
96
+ else if (parsed.classes.includes("cartouche")) problems.push(...cartoucheProblems(parsed));
94
97
  if (silent) {
95
98
  state.pos = finish;
96
99
  return true;
@@ -263,7 +266,7 @@ export function scanSpans(source) {
263
266
  }
264
267
  // Verse fences preserve inline Markdown even though the block parser
265
268
  // normally treats every code fence as literal text.
266
- for (const poem of scanPoetry(text).blocks) {
269
+ for (const poem of scanVerse(text).blocks) {
267
270
  for (let line = poem.start + 1; line < poem.end; line++) {
268
271
  const children = [];
269
272
  parser.inline.parse(lines[line], parser, env, children);
@@ -283,7 +286,7 @@ export function scanSpans(source) {
283
286
  };
284
287
  }
285
288
 
286
- /** Emit span tags while leaving their inline content as Markdown for Hugo.
289
+ /** Emit span tags while leaving their inline content as Markdown.
287
290
  * @param {string} source - Authored Markdown.
288
291
  */
289
292
  export function renderSpans(source) {
@@ -0,0 +1,91 @@
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 shared rules of the content stylesheet**, rescoped for a surface that
16
+ * is not Foundry.
17
+ *
18
+ * `assets/styles/content-base.css` styles what the renderer emits, scoped to
19
+ * the roots a Foundry journal and its editors use. Two other surfaces reuse its
20
+ * rules without a second copy: the editor preview takes the stretches marked
21
+ * `preview:shared`, and a site takes the highlighted-code rules. Both read the
22
+ * one file and replace the scope, so a colour changed there changes everywhere.
23
+ *
24
+ * @module
25
+ */
26
+
27
+ import fs from "node:fs";
28
+
29
+ import { baseStylesheetSource } from "./base-styles.mjs";
30
+
31
+ /** The roots the shipped content stylesheet scopes its rules to. */
32
+ export const CONTENT_ROOTS =
33
+ ":is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)";
34
+
35
+ /** The stretches of the shipped content stylesheet other surfaces share. */
36
+ const SHARED_RULES = /\/\* preview:shared [^]*?\*\/([^]*?)\/\* \/preview:shared \*\//g;
37
+
38
+ /**
39
+ * The rules the Foundry content stylesheet shares with other surfaces, scoped
40
+ * to a selector.
41
+ *
42
+ * @param {string} scope - The selector that replaces the Foundry roots.
43
+ * @returns {string} The rules.
44
+ * @throws {Error} When the stylesheet marks no shared rules.
45
+ */
46
+ export function sharedContentRules(scope) {
47
+ const css = fs.readFileSync(baseStylesheetSource(), "utf8");
48
+ const shared = [...css.matchAll(SHARED_RULES)].map((match) => match[1]);
49
+ if (!shared.length) throw new Error("the content stylesheet marks no rules for the preview");
50
+ return shared.join("").replaceAll(CONTENT_ROOTS, scope);
51
+ }
52
+
53
+ /**
54
+ * The rules that colour highlighted code, scoped to a selector.
55
+ *
56
+ * A rule belongs to the set when one of its selectors names a `hljs-` token
57
+ * class. The rules are taken from the shared stretches, so the colours, their
58
+ * custom properties and their `light-dark()` pairs are the stylesheet's own.
59
+ *
60
+ * @param {string} scope - The selector that replaces the Foundry roots.
61
+ * @returns {string} The token rules, one per block.
62
+ */
63
+ export function highlightRules(scope) {
64
+ const shared = sharedContentRules(scope)
65
+ .replace(/\/\*[^]*?\*\//g, "")
66
+ .replace(/\s*\n\s+(?=\.hljs-)/g, " ");
67
+ return (
68
+ shared
69
+ .split("}")
70
+ .map((rule) => rule.trim())
71
+ .filter(
72
+ (rule) => rule.includes("{") && /\.hljs-/.test(rule.slice(0, rule.indexOf("{"))),
73
+ )
74
+ .map((rule) => `${rule}\n}`)
75
+ .join("\n\n") + "\n"
76
+ );
77
+ }
78
+
79
+ /**
80
+ * The stylesheet a site links for highlighted code.
81
+ *
82
+ * A site sets no colour scheme, and the token colours are `light-dark()` pairs
83
+ * that read the scheme in force, so code is given the dark one its pages are
84
+ * drawn in.
85
+ *
86
+ * @param {string} [scope] - The element the page body sits in.
87
+ * @returns {string} The stylesheet.
88
+ */
89
+ export function siteCodeStylesheet(scope = ".single-body") {
90
+ return `${scope} :is(pre, code) {\n color-scheme: dark;\n}\n\n${highlightRules(scope)}`;
91
+ }
@@ -0,0 +1,186 @@
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 glyph and cartouche spans of a note, read in one place for the stages
16
+ * that need their pictures: the renderer that draws them and the book that
17
+ * stages their files.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ import {
23
+ glyphHtml,
24
+ glyphPathname,
25
+ cartoucheHtml,
26
+ cartouchePathname,
27
+ cartoucheProblems,
28
+ glyphProblems,
29
+ } from "./content-glyphs.mjs";
30
+ import { scanSpans } from "./content-spans.mjs";
31
+
32
+ /**
33
+ * A span's attributes as the construct's checker reads them.
34
+ * @param {Array<[string, string]>} attrs - The span's attribute pairs.
35
+ * @returns {{id: string, classes: string[], values: Record<string, string>}} Parsed form.
36
+ */
37
+ function parsedAttributes(attrs) {
38
+ const find = (name) => attrs.find(([key]) => key === name)?.[1] ?? "";
39
+ return {
40
+ id: find("id"),
41
+ classes: find("class").split(/\s+/).filter(Boolean),
42
+ values: Object.fromEntries(attrs.filter(([key]) => !["id", "class"].includes(key))),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * The well-formed glyph and cartouche spans of a note, outermost only, outside
48
+ * code.
49
+ * @param {string} source - Authored Markdown.
50
+ * @returns {Array<{kind: "glyph"|"cartouche", start: number, end: number, slug: string, alt: string, frame?: string, size?: string, align?: string, height?: string, caption?: string}>}
51
+ * One entry per sign, in source order. A sign with a finding is left out: it
52
+ * is reported and draws as an ordinary span.
53
+ */
54
+ function symbolSpans(source) {
55
+ if (!/\.(glyph|cartouche)\b/.test(source)) return [];
56
+ const { spans } = scanSpans(source);
57
+ const signs = spans
58
+ .map((span) => ({ span, parsed: parsedAttributes(span.attrs) }))
59
+ .flatMap(({ span, parsed }) => {
60
+ const kind = ["glyph", "cartouche"].find((name) => parsed.classes.includes(name));
61
+ const problems = { glyph: glyphProblems, cartouche: cartoucheProblems }[kind];
62
+ if (!problems || problems(parsed).length) return [];
63
+ const { slug, frame, size, align, height, caption } = parsed.values;
64
+ return [
65
+ {
66
+ kind,
67
+ start: span.start,
68
+ end: span.end,
69
+ slug,
70
+ alt: span.visible.trim(),
71
+ ...(frame ? { frame } : {}),
72
+ ...(size ? { size } : {}),
73
+ ...(align ? { align } : {}),
74
+ ...(height ? { height } : {}),
75
+ ...(caption ? { caption } : {}),
76
+ },
77
+ ];
78
+ });
79
+ return signs.filter(
80
+ (sign) => !signs.some((outer) => outer.start < sign.start && outer.end >= sign.end),
81
+ );
82
+ }
83
+
84
+ const OPEN = "\uE000",
85
+ CLOSE = "\uE001";
86
+ const PLACEHOLDER = new RegExp(`${OPEN}([0-9a-f]+)${CLOSE}`, "g");
87
+
88
+ /**
89
+ * A cartouche as the inert text a Markdown parser passes through untouched.
90
+ * A figure written as HTML at the start of a line would open a CommonMark HTML
91
+ * block and leave the rest of its paragraph unparsed, so the figure is written
92
+ * as inline HTML by {@link cartoucheFigures} after the paragraph is parsed.
93
+ * @param {object} cartouche - The figure's attributes, with `src` the address of its picture.
94
+ * @returns {string} Private-use delimiters around the hex of the attributes' JSON.
95
+ */
96
+ function cartouchePlaceholder(cartouche) {
97
+ const hex = [...new TextEncoder().encode(JSON.stringify(cartouche))]
98
+ .map((byte) => byte.toString(16).padStart(2, "0"))
99
+ .join("");
100
+ return `${OPEN}${hex}${CLOSE}`;
101
+ }
102
+
103
+ /**
104
+ * Markdown-it plugin: write each cartouche placeholder as the inline HTML of
105
+ * its figure, in whatever paragraph, cell or title it stands.
106
+ * @param {import("markdown-it")} md - Parser to extend.
107
+ */
108
+ export function cartoucheFigures(md) {
109
+ md.core.ruler.after("inline", "cartouche_figures", (state) => {
110
+ for (const token of state.tokens) {
111
+ if (token.type !== "inline" || !token.children) continue;
112
+ token.children = token.children.flatMap((child) => {
113
+ if (child.type !== "text" || !child.content.includes(OPEN)) return [child];
114
+ const pieces = [];
115
+ let last = 0;
116
+ const piece = (type, content) => {
117
+ const made = new state.Token(type, "", 0);
118
+ made.content = content;
119
+ made.level = child.level;
120
+ pieces.push(made);
121
+ };
122
+ for (const match of child.content.matchAll(PLACEHOLDER)) {
123
+ if (match.index > last) piece("text", child.content.slice(last, match.index));
124
+ const { src, ...cartouche } = JSON.parse(
125
+ new TextDecoder().decode(
126
+ Uint8Array.from(match[1].match(/../g), (pair) => parseInt(pair, 16)),
127
+ ),
128
+ );
129
+ piece("html_inline", cartoucheHtml(cartouche, src));
130
+ last = match.index + match[0].length;
131
+ }
132
+ if (last < child.content.length) piece("text", child.content.slice(last));
133
+ return pieces;
134
+ });
135
+ }
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Write each glyph and cartouche span as the HTML that draws its picture. A
141
+ * cartouche is written as a placeholder that {@link cartoucheFigures} turns
142
+ * into its figure.
143
+ *
144
+ * @param {string} source - Markdown that may contain glyph and cartouche spans.
145
+ * @param {(src: string) => string} [resolveSrc] - From a sign's authored
146
+ * pathname to the address the target serves; omitted, the pathname.
147
+ * @param {object} [options]
148
+ * @param {boolean} [options.glyphs] - Write glyph spans; `false` leaves them
149
+ * as the source they were written in. Default `true`.
150
+ * @returns {string} The Markdown with each well-formed sign written as HTML or a placeholder.
151
+ */
152
+ export function renderSymbolSpans(source, resolveSrc, { glyphs = true } = {}) {
153
+ let markdown = String(source ?? "");
154
+ for (const sign of symbolSpans(markdown).sort((a, b) => b.start - a.start)) {
155
+ if (sign.kind === "glyph" && !glyphs) continue;
156
+ const picture = (sign.kind === "glyph" ? glyphPathname : cartouchePathname)(sign.slug);
157
+ const src = resolveSrc?.(picture) ?? picture;
158
+ const html =
159
+ sign.kind === "glyph" ?
160
+ glyphHtml(sign, src)
161
+ : cartouchePlaceholder({ ...sign, alt: sign.alt || undefined, src });
162
+ markdown = markdown.slice(0, sign.start) + html + markdown.slice(sign.end);
163
+ }
164
+ return markdown;
165
+ }
166
+
167
+ /**
168
+ * Every picture the glyphs and cartouches of a body name, for a stage that has
169
+ * to place the files before a compiler reads them.
170
+ *
171
+ * @param {string} body - The note's Markdown.
172
+ * @returns {Array<{src: string, locate: string}>} The authored pathname of each
173
+ * picture and the slug that locates it in the note's text, in document order
174
+ * within glyphs and then cartouches.
175
+ */
176
+ export function symbolImagesIn(body) {
177
+ const signs = symbolSpans(String(body ?? ""));
178
+ return ["glyph", "cartouche"].flatMap((kind) =>
179
+ signs
180
+ .filter((sign) => sign.kind === kind)
181
+ .map(({ slug }) => ({
182
+ src: (kind === "glyph" ? glyphPathname : cartouchePathname)(slug),
183
+ locate: slug,
184
+ })),
185
+ );
186
+ }