@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,376 @@
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 expansion step.** {@link expandContent} turns a note's authored body
16
+ * into markdown with every construct that reads the content index resolved,
17
+ * before a target renders it. In order:
18
+ *
19
+ * 1. **Tables.** Each prepared `sql` fence and page list is replaced by the
20
+ * markdown its query returned, and the note's generated sections are
21
+ * appended — see {@link module:engine/content-tables.expandContentTables}.
22
+ * This runs outside code protection, because a table is authored as a
23
+ * fence.
24
+ * 2. **Figure numbers.** The expanded body's captions are numbered by
25
+ * {@link module:engine/content-figures.scanFigures}; a `ref` and a
26
+ * `[[#id]]` link read the label.
27
+ * 3. **Expressions.** `{{…}}` is evaluated — `sql`, `ref`, dates and the
28
+ * helpers — see {@link module:engine/markdown-expressions.renderMarkdownExpressions}.
29
+ * 4. **Wikilinks.** Each `[[…]]` outside code is resolved against the index by
30
+ * {@link module:engine/wikilinks.resolveWikilinks}, and written in the
31
+ * target's form.
32
+ *
33
+ * Steps 1 and 2 are {@link expandTables}, which a caller numbering figures
34
+ * across more than one note — the book — runs on its own first.
35
+ *
36
+ * **What the target decides.** Every step reads the same on each target except
37
+ * where the target's own medium differs:
38
+ *
39
+ * | | `foundry` | `web` | `book` |
40
+ * | --- | --- | --- | --- |
41
+ * | a resolved link | an `@UUID[…]{label}` content link | a markdown link to the page's URL | a markdown link to the page's URL, which the book sets as a cross-reference to the entry it prints |
42
+ * | an unresolved link | the error that fails the note, as `linkError` | a `sohl-unresolved-link` span, and a finding | the label, then `(unresolved link)`, and a finding |
43
+ * | a `ref` | a `[[#anchor\|label]]` link, which the link pass addresses by its journal page | a markdown link to the anchor, or to another note's page | as on the web |
44
+ * | the generated sections | part of the one body | resolved apart, and returned as `generated` | resolved with the body, and returned as `generated` |
45
+ * | the date context | the compile's link index | the site index's `dateContext`, which carries the present | as on the web |
46
+ * | a figure's number | counted on the note | counted on the note | counted across the book, by the caller |
47
+ *
48
+ * Expansion reports and never throws on what it finds: each step's findings
49
+ * are returned for the caller to report in its own form.
50
+ *
51
+ * @module
52
+ */
53
+
54
+ import { collectAnchors, eventAnchors } from "./anchors.mjs";
55
+ import { protectCode } from "./code-fences.mjs";
56
+ import { scanFigures } from "./content-figures.mjs";
57
+ import { expandContentTables } from "./content-tables.mjs";
58
+ import { markGenerated, splitGenerated } from "./generated-sections.mjs";
59
+ import { convertNoteWikilinks } from "./helpers.mjs";
60
+ import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
61
+ import { reckoningContext } from "./reckoning-markers.mjs";
62
+ import { resolveCrossNoteFigures, wikiContext } from "./site-index.mjs";
63
+ import { resolveWebWikilinks } from "./wikilinks.mjs";
64
+
65
+ /** The targets a body is expanded for. */
66
+ export const EXPANSION_TARGETS = Object.freeze(["foundry", "web", "book"]);
67
+
68
+ /**
69
+ * A `ref` call's Foundry rendering: a same-page wikilink rather than a
70
+ * Markdown link, so the wikilink pass that runs after expressions render
71
+ * addresses it exactly as an authored `[[#anchor|Text]]` is — by the
72
+ * JournalEntryPage the figure's own fence became, whether that page holds
73
+ * only the figure or carries it alongside other prose. A Markdown link's
74
+ * `#anchor` fragment resolves nowhere in Foundry, which addresses a page by
75
+ * UUID and an anchor within a page by that page's own anchor.
76
+ *
77
+ * @param {{anchor: string}} target - The figure's own anchor.
78
+ * @param {string} label - The link's text, which may itself carry Markdown.
79
+ * @returns {string} `[[#anchor|label]]`, with a literal `|` in the label
80
+ * escaped so it cannot be read as the wikilink's own separator.
81
+ */
82
+ function foundryRefLink({ anchor }, label) {
83
+ return `[[#${anchor}|${label.replace(/\|/g, "\\|")}]]`;
84
+ }
85
+
86
+ /**
87
+ * The note a body belongs to.
88
+ *
89
+ * @typedef {object} ExpandedNote
90
+ * @property {Record<string, any>} fm - Its frontmatter.
91
+ * @property {string} [file] - Its file, which findings name.
92
+ * @property {string} source - What a table's or a link's finding names the
93
+ * note by: its path on the web, its name in Foundry.
94
+ * @property {number} [bodyLine] - The file line its body starts on.
95
+ * @property {number} [bodyColumn] - The column its body starts at on that line.
96
+ */
97
+
98
+ /**
99
+ * How a target resolves links, beyond the index.
100
+ *
101
+ * @typedef {object} ExpandLinks
102
+ * @property {string|null} [pack] - `foundry`: the pack the note's own document
103
+ * lands in, which a `[[#slug]]` self-link addresses.
104
+ * @property {string|null} [docPack] - `foundry`: the pack its journal lands in.
105
+ * @property {Map<string, object>} [foreign] - `web` and `book`: the merged
106
+ * foreign index.
107
+ * @property {object} [assets] - `web` and `book`: the address space an embed
108
+ * resolves against, from {@link module:engine/art-fields.assetAddressIndex}.
109
+ * @property {(text: string) => string} [beforeLinks] - `web` and `book`: a pass
110
+ * run on the text outside code before its links resolve.
111
+ * @property {(text: string) => string} [afterLinks] - `web` and `book`: a pass
112
+ * run on the text outside code once its links resolve.
113
+ */
114
+
115
+ /**
116
+ * The first two steps of an expansion: the tables, and the figures of the
117
+ * table-expanded body.
118
+ *
119
+ * @typedef {object} ExpandedTables
120
+ * @property {{markdown: string, errors: object[], lineMap: object[],
121
+ * generatedFrom: number|undefined}} tables - From
122
+ * {@link module:engine/content-tables.expandContentTables}.
123
+ * @property {{figures: object[], errors: object[]}} figures - The figure scan
124
+ * of `tables.markdown`. A caller numbering figures across notes replaces
125
+ * `figures.figures` with its numbering before expanding the rest.
126
+ */
127
+
128
+ /**
129
+ * Expand a note's tables and scan the result for figures — steps 1 and 2 of
130
+ * {@link expandContent}.
131
+ *
132
+ * @param {string} markdown - The body as authored.
133
+ * @param {object} options
134
+ * @param {string} options.source - What a table's finding names the note by.
135
+ * @param {{sqlTables?: object[], pageLists?: object[]}} [options.prepared] -
136
+ * This note's prepared SQL tables and page lists.
137
+ * @param {(address: string) => string|undefined} [options.resolveRole] - The
138
+ * role an embedded picture takes, which numbers a map apart from a figure.
139
+ * @returns {ExpandedTables} The two steps' results.
140
+ */
141
+ export function expandTables(markdown, { source, prepared = {}, resolveRole }) {
142
+ const tables = expandContentTables(markdown ?? "", {
143
+ source,
144
+ sqlTables: prepared.sqlTables,
145
+ pageLists: prepared.pageLists,
146
+ });
147
+ return { tables, figures: scanFigures(tables.markdown, { resolveRole }) };
148
+ }
149
+
150
+ /**
151
+ * The expanded body, and what each step found.
152
+ *
153
+ * @typedef {object} Expansion
154
+ * @property {string} markdown - The whole body, expanded.
155
+ * @property {string} authored - The author's text, expanded. In Foundry it is
156
+ * the whole body.
157
+ * @property {string} generated - The generated sections, expanded apart from
158
+ * the author's text on the web; `""` in Foundry, and wherever there are none.
159
+ * @property {Array<{line: number, generated: boolean}>} lineMap - The authored
160
+ * line, 0-based, each line of the table-expanded body came from.
161
+ * @property {number|undefined} generatedFrom - The 0-based line of the
162
+ * table-expanded body the generated sections start on.
163
+ * @property {{figures: object[], errors: object[]}} figures - The figure scan
164
+ * of the table-expanded body.
165
+ * @property {{tables: object[], captions: object[], expressions: object[], links: object[]}} findings -
166
+ * Each step's findings, as the step reports them: a table's and a caption's
167
+ * by expanded body line, an expression's in file coordinates, a link's by
168
+ * occurrence of its literal.
169
+ * @property {Error|undefined} linkError - `foundry`: the error
170
+ * {@link module:engine/helpers.convertNoteWikilinks} fails a note with,
171
+ * carrying `file` and `position`.
172
+ * @property {object[]} unresolved - `foundry`: the links that did not resolve.
173
+ * @property {object[]} resolved - `web` and `book`: every link that resolved,
174
+ * in order.
175
+ * @property {number} authoredLinks - `web`: how many of `resolved` the
176
+ * author's text made, before the generated sections'.
177
+ */
178
+
179
+ /**
180
+ * Expand a note's body for one target.
181
+ *
182
+ * @param {string} markdown - The body as authored.
183
+ * @param {object} options
184
+ * @param {"foundry"|"web"|"book"} options.target - What the body is expanded
185
+ * for.
186
+ * @param {object} options.index - What links resolve against: the compile's
187
+ * link index for `foundry`, the {@link module:engine/site-index.buildSiteIndex}
188
+ * index for `web` and `book`.
189
+ * @param {ExpandedNote} options.note - The note.
190
+ * @param {{sqlTables?: object[], pageLists?: object[], inline?: object[]}} [options.prepared] -
191
+ * This note's prepared SQL tables, page lists and inline query results.
192
+ * @param {object} [options.events] - Every note's events, from
193
+ * {@link module:engine/event-fields.eventNoteIndex}.
194
+ * @param {(address: string) => string|undefined} [options.resolveRole] - The
195
+ * role an embedded picture takes, which numbers a map apart from a figure.
196
+ * @param {Map<string, Map<string, object>>} [options.figuresByAddress] - `web`
197
+ * and `book`: every page's figures, by the address a cross-note `ref` writes.
198
+ * @param {ExpandedTables} [options.tables] - The note's {@link expandTables},
199
+ * already run — with the figures numbered, for the book. Run here when
200
+ * omitted.
201
+ * @param {ExpandLinks} [options.links] - How the target resolves links.
202
+ * @returns {Expansion} The expanded body and the findings.
203
+ * @throws {Error} When `target` is not one of {@link EXPANSION_TARGETS}.
204
+ */
205
+ export function expandContent(
206
+ markdown,
207
+ {
208
+ target,
209
+ index,
210
+ note,
211
+ prepared = {},
212
+ events,
213
+ resolveRole,
214
+ figuresByAddress = new Map(),
215
+ tables: staged,
216
+ links = {},
217
+ },
218
+ ) {
219
+ if (!EXPANSION_TARGETS.includes(target))
220
+ throw new Error(
221
+ `\`${target}\` is not an expansion target; expected ` +
222
+ EXPANSION_TARGETS.slice(0, -1)
223
+ .map((name) => `\`${name}\``)
224
+ .join(", ") +
225
+ ` or \`${EXPANSION_TARGETS.at(-1)}\``,
226
+ );
227
+ const { fm, file, source, bodyLine, bodyColumn } = note;
228
+ // The website and the book read the site index, and resolve the same way.
229
+ const web = target !== "foundry";
230
+
231
+ const { tables, figures } = staged ?? expandTables(markdown, { source, prepared, resolveRole });
232
+ const { lineMap, generatedFrom } = tables;
233
+ const figuresById = new Map(
234
+ figures.figures
235
+ .filter((figure) => figure.id)
236
+ .map((figure) => [
237
+ figure.id,
238
+ { label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
239
+ ]),
240
+ );
241
+
242
+ const expressions = renderMarkdownExpressions(
243
+ web ? markGenerated(tables.markdown, generatedFrom) : tables.markdown,
244
+ {
245
+ fm,
246
+ dates: web ? index.dateContext : reckoningContext(index),
247
+ file,
248
+ bodyLine,
249
+ sqlResults: prepared.inline,
250
+ events,
251
+ figures:
252
+ web ?
253
+ {
254
+ get: (id) => figuresById.get(id),
255
+ // A cross-note `ref` resolves the written address
256
+ // exactly as a wikilink does, against every page's
257
+ // figures scanned before any page's expressions render.
258
+ note: (address) =>
259
+ resolveCrossNoteFigures(address, {
260
+ contentTypes: index.contentTypes,
261
+ packages: index.packages,
262
+ noIndexPackages: index.noIndexPackages,
263
+ contentPackage: index.contentPackage,
264
+ siteIndexMap: index.index,
265
+ figuresByAddress,
266
+ }),
267
+ }
268
+ : { get: (id) => figuresById.get(id), link: foundryRefLink },
269
+ },
270
+ );
271
+
272
+ const findings = {
273
+ tables: tables.errors,
274
+ captions: figures.errors,
275
+ expressions: expressions.findings,
276
+ links: [],
277
+ };
278
+
279
+ if (!web) {
280
+ let converted;
281
+ let linkError;
282
+ try {
283
+ converted = convertNoteWikilinks(expressions.markdown, {
284
+ type: fm.type,
285
+ id: fm.id,
286
+ file,
287
+ bodyLine,
288
+ bodyColumn,
289
+ lineMap,
290
+ pack: links.pack,
291
+ docPack: links.docPack,
292
+ index,
293
+ name: source,
294
+ });
295
+ } catch (error) {
296
+ linkError = error;
297
+ }
298
+ const body = converted?.markdown ?? expressions.markdown;
299
+ return {
300
+ markdown: body,
301
+ authored: body,
302
+ generated: "",
303
+ lineMap,
304
+ generatedFrom,
305
+ figures,
306
+ findings,
307
+ linkError,
308
+ unresolved: converted?.unresolved ?? [],
309
+ resolved: [],
310
+ authoredLinks: 0,
311
+ };
312
+ }
313
+
314
+ const resolved = [];
315
+ const book = target === "book";
316
+ const ctx = wikiContext(index, {
317
+ src: source,
318
+ file,
319
+ type: fm.type ?? null,
320
+ errors: findings.links,
321
+ foreignIndex: links.foreign,
322
+ assets: links.assets,
323
+ resolved,
324
+ });
325
+ ctx.captionLabels = new Map(
326
+ figures.figures.filter((figure) => figure.id).map((figure) => [figure.id, figure.label]),
327
+ );
328
+ // This page's own anchors, so a `[[#slug]]` self-link is checked against
329
+ // what the page actually declares rather than trusted unconditionally.
330
+ ctx.anchors = new Set(collectAnchors(tables.markdown).map((anchor) => anchor.slug));
331
+ ctx.eventAnchors = new Set(eventAnchors(fm).map((anchor) => anchor.slug));
332
+ if (book) ctx.output = "book";
333
+ const resolve = (text) => {
334
+ const before = links.beforeLinks ? links.beforeLinks(text) : text;
335
+ const linked = resolveWebWikilinks(before, ctx);
336
+ return links.afterLinks ? links.afterLinks(linked) : linked;
337
+ };
338
+ // The book resolves the body whole, so a link's occurrence counts across
339
+ // the author's text and the generated sections it prints after it.
340
+ if (book) {
341
+ const split = splitGenerated(protectCode(expressions.markdown, resolve));
342
+ return {
343
+ markdown: split.generated ? `${split.authored}\n${split.generated}` : split.authored,
344
+ authored: split.authored,
345
+ generated: split.generated ?? "",
346
+ lineMap,
347
+ generatedFrom,
348
+ figures,
349
+ findings,
350
+ linkError: undefined,
351
+ unresolved: [],
352
+ resolved,
353
+ authoredLinks: 0,
354
+ };
355
+ }
356
+ // The author's text and the generated sections are resolved apart, so a
357
+ // caller can tell the links the author wrote from the ones a section
358
+ // derives.
359
+ const split = splitGenerated(expressions.markdown);
360
+ const authored = protectCode(split.authored, resolve);
361
+ const authoredLinks = resolved.length;
362
+ const generated = split.generated ? protectCode(split.generated, resolve) : "";
363
+ return {
364
+ markdown: split.generated ? `${authored}\n${generated}` : authored,
365
+ authored,
366
+ generated,
367
+ lineMap,
368
+ generatedFrom,
369
+ figures,
370
+ findings,
371
+ linkError: undefined,
372
+ unresolved: [],
373
+ resolved,
374
+ authoredLinks,
375
+ };
376
+ }
@@ -154,7 +154,7 @@ function picturesAreMap(contents, resolveRole) {
154
154
  * @returns {"code"|"table"|"figure"|"map"|"poem"|"prose"} The kind.
155
155
  */
156
156
  function figureKind(first, contents, resolveRole) {
157
- if (/^(?:<div\b[^>]*\bclass="poetry(?:\s|"))/.test(contents)) return "poem";
157
+ if (/^(?:<div\b[^>]*\bclass="verse(?:\s|"))/.test(contents)) return "poem";
158
158
  if (first?.type === "table_open") return "table";
159
159
  if (first?.type === "fence" && /^\s*sql\b/i.test(first.info ?? "")) return "table";
160
160
  if (first?.type === "fence" || first?.type === "code_block") return "code";
@@ -220,7 +220,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
220
220
  const explicit = attributes.values.type;
221
221
  if (
222
222
  explicit &&
223
- !["figure", "table", "poetry", "code", "prose", "example"].includes(explicit)
223
+ !["figure", "table", "verse", "code", "prose", "example"].includes(explicit)
224
224
  )
225
225
  fault(i, `unsupported caption type "${explicit}"`);
226
226
  if (!caption) fault(i, "a caption needs text");
@@ -281,7 +281,7 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
281
281
  ) {
282
282
  fault(
283
283
  bodyStart,
284
- "a caption must be followed by an image, table, poetry or code fence, paragraph, quote, or fenced div",
284
+ "a caption must be followed by an image, table, verse or code fence, paragraph, quote, or fenced div",
285
285
  );
286
286
  continue;
287
287
  }
@@ -306,11 +306,11 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
306
306
  continue;
307
307
  }
308
308
  kind =
309
- first.type === "fence" && /^poetry(?:\s|$)/.test(first.info) ?
309
+ first.type === "fence" && /^verse(?:\s|$)/.test(first.info) ?
310
310
  "poem"
311
311
  : figureKind(first, contents.trim(), resolveRole);
312
312
  }
313
- if (explicit) kind = explicit === "poetry" ? "poem" : explicit;
313
+ if (explicit) kind = explicit === "verse" ? "poem" : explicit;
314
314
  const id = attributes.id;
315
315
  if (id && ids.has(slugify(id))) fault(i, `duplicate figure id "${id}"`);
316
316
  if (id) ids.add(slugify(id));
@@ -374,18 +374,24 @@ export function scanFigures(source, { resolveRole = () => undefined } = {}) {
374
374
  * @param {string} source - A note body with expanded tables.
375
375
  * @param {(markdown: string) => string} [renderMarkdown] - How to render a
376
376
  * figure's own contents.
377
- * @param {Array<{id: string, label: string}>} [numbers] - Labels assigned by a
378
- * surface that counts across documents, matched by id.
377
+ * @param {Array<{id: string, kind: string, label: string}>} [numbers] - The
378
+ * figures as {@link scanFigures} numbered them, with roles, where the
379
+ * surface counted them: across a note's pages, or in expansion, where a
380
+ * `ref` reads the same label. They are matched in order when they are as many
381
+ * as the text holds, by id otherwise, and the caption takes its number, kind
382
+ * and label from them. Omitted, the text is numbered as it stands.
379
383
  * @param {object} [options] - Options.
380
384
  * @param {(address: string) => string|undefined} [options.resolveRole] - See
381
385
  * {@link scanFigures}.
386
+ * @param {(markdown: string) => string} [options.renderInline] - How a caption
387
+ * is rendered; omitted, a caption is plain inline Markdown.
382
388
  * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
383
389
  */
384
390
  export function renderFigureBlocks(
385
391
  source,
386
392
  renderMarkdown = parser.render.bind(parser),
387
393
  numbers,
388
- { resolveRole } = {},
394
+ { resolveRole, renderInline = parser.renderInline.bind(parser) } = {},
389
395
  ) {
390
396
  const { figures, errors } = scanFigures(source, { resolveRole });
391
397
  if (errors.length) return { markdown: source, errors };
@@ -393,12 +399,13 @@ export function renderFigureBlocks(
393
399
  numbers ?
394
400
  new Map(numbers.filter((entry) => entry.id).map((entry) => [entry.id, entry]))
395
401
  : new Map();
402
+ const inOrder = numbers?.length === figures.length ? numbers : undefined;
396
403
  const lines = String(source ?? "").split("\n");
397
404
  const output = [];
398
405
  let cursor = 0;
399
- for (const figure of figures) {
406
+ for (const [at, figure] of figures.entries()) {
400
407
  output.push(...lines.slice(cursor, figure.line - 1));
401
- const numbered = (figure.id ? byId.get(figure.id) : null) ?? figure;
408
+ const numbered = inOrder?.[at] ?? (figure.id ? byId.get(figure.id) : null) ?? figure;
402
409
  const contents = lines.slice(figure.bodyStart, figure.bodyEnd).join("\n");
403
410
  const classes = ["content-figure", `content-figure-${numbered.kind}`, ...figure.classes];
404
411
  const id = figure.slug ? ` id="${escapeHtml(figure.slug)}"` : "";
@@ -416,7 +423,7 @@ export function renderFigureBlocks(
416
423
  output.push(
417
424
  `<p class="content-figure-label">${
418
425
  figure.hasCaption ?
419
- `${label ? `${label}: ` : ""}${parser.renderInline(figure.caption)}`
426
+ `${label ? `${label}: ` : ""}${renderInline(figure.caption)}`
420
427
  : label
421
428
  }</p>`,
422
429
  "</div>",
@@ -207,6 +207,7 @@ types:
207
207
  - names
208
208
  - family
209
209
  - abbreviation
210
+ - core
210
211
  - classModifiers
211
212
  - fields
212
213
  - words
@@ -224,6 +225,8 @@ types:
224
225
  - older
225
226
  - negation
226
227
  - naming
228
+ - emphasis
229
+ - script
227
230
  subTypes:
228
231
  - social
229
232
  - nature
@@ -363,7 +366,11 @@ types:
363
366
  doc:
364
367
  data:
365
368
  - culture
369
+ - concepts
370
+ - date
371
+ - author
366
372
  - map
373
+ - metres
367
374
  subTypes:
368
375
  - rules
369
376
  - userguide
@@ -371,6 +378,7 @@ types:
371
378
  - howto
372
379
  - concept
373
380
  - settingguide
381
+ - post
374
382
  macro:
375
383
  data: []
376
384
  subTypes: []