@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,450 @@
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
+ * **What the preview target adds to the shared renderer.** The editor preview
16
+ * renders a buffer that may have no content index. Where it has none, a
17
+ * wikilink, a span, an expression and a query cannot be resolved, so the
18
+ * preview shows each as highlighted authored source. Where it has one, links
19
+ * and spans render as every target renders them, and only what a buffer cannot
20
+ * answer — a query, a date that needs a calendar — stays highlighted source.
21
+ *
22
+ * {@link previewSourcePlugin} installs:
23
+ *
24
+ * - **Source highlighting** of wikilinks, embeds, bracket spans and
25
+ * expressions, when the buffer has no index; of expressions alone otherwise.
26
+ * - **Table cells that keep their pipes**, so an extension's `|` stays inside
27
+ * its cell, with alignment written as a class a sanitizing host keeps.
28
+ * - **Query fences as source**, since a buffer runs no query.
29
+ * - **No generated sections**: a `See also` or `Related pages` heading and
30
+ * what it holds are absent, since a buffer has no related pages.
31
+ * - **Secret headings as disclosures**: a `.secret` heading opens a closed
32
+ * disclosure that runs to the next heading at its level or above, wherever
33
+ * the heading sits. An editor shows the passage a reader will not see, and
34
+ * the build refuses a withheld heading that opens no page.
35
+ *
36
+ * @module
37
+ */
38
+
39
+ import MarkdownIt from "markdown-it";
40
+ import Handlebars from "handlebars";
41
+ import { EXPRESSION, renderMarkdownExpressions } from "./markdown-expressions.mjs";
42
+ import { WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
43
+ import { splitHeadingAttributes } from "./heading-attributes.mjs";
44
+
45
+ /** Explicit dependency policy: a new registered helper requires a preview decision. */
46
+ export const PREVIEW_HELPERS = Object.freeze({
47
+ eq: "local",
48
+ gt: "local",
49
+ gte: "local",
50
+ lt: "local",
51
+ lte: "local",
52
+ not: "local",
53
+ and: "local",
54
+ or: "local",
55
+ words: "local",
56
+ digits: "local",
57
+ sql: "dependent",
58
+ dateformat: "dependent",
59
+ ref: "reference",
60
+ });
61
+
62
+ /**
63
+ * What stands for an authored `<` in the preview between
64
+ * {@link module:engine/content-render.inertHtml} and the rendered HTML, where
65
+ * it is written `&lt;`. A private-use character, so it can neither be read as
66
+ * markup nor collide with prose.
67
+ */
68
+ export const INERT_BRACKET = "\uE000";
69
+
70
+ const escapeHtml = new MarkdownIt().utils.escapeHtml;
71
+ // An authored `<` stands as a private-use character until the HTML is written;
72
+ // source is shown as it was authored.
73
+ const escape = (value) => escapeHtml(String(value ?? "").replaceAll(INERT_BRACKET, "<"));
74
+
75
+ /**
76
+ * A span of authored source, coloured by what it is.
77
+ *
78
+ * @param {string} role - `bracket`, `address`, `label`, `attribute`, `dependent` or `fence`.
79
+ * @param {string} source - The authored text.
80
+ * @returns {string} The span, with the text escaped.
81
+ */
82
+ export const colored = (role, source) =>
83
+ `<span class="hl-preview-${role}">${escape(source)}</span>`;
84
+
85
+ /**
86
+ * Highlight a wikilink or an embed without interpreting any Markdown or HTML
87
+ * inside the source.
88
+ *
89
+ * @param {string} raw - The link as written, from `[[` or `![[` to its end.
90
+ * @returns {string} The highlighted source.
91
+ */
92
+ export function wikiSource(raw) {
93
+ const open = raw.startsWith("!") ? "![[" : "[[";
94
+ const close = raw.indexOf("]]", open.length);
95
+ const end = close < 0 ? raw.length : close;
96
+ const inner = raw.slice(open.length, end);
97
+ const pipe = inner.indexOf("|");
98
+ const secret = close >= 0 && parseWikilink(inner, raw).attributes?.classes.includes("secret");
99
+ const label =
100
+ colored("address", pipe < 0 ? inner : inner.slice(0, pipe)) +
101
+ (pipe < 0 ? "" : colored("bracket", "|") + colored("label", inner.slice(pipe + 1)));
102
+ const html =
103
+ colored("bracket", open) +
104
+ (secret ? `<span class="hl-preview-secret-content">${label}</span>` : label) +
105
+ (close < 0 ? "" : colored("bracket", "]]") + colored("attribute", raw.slice(close + 2)));
106
+ return secret ? `<span class="hl-preview-secret" tabindex="0">${html}</span>` : html;
107
+ }
108
+
109
+ /** Read a dependent call anywhere in the Handlebars AST, including hash values. */
110
+ function dependentExpression(source, fm) {
111
+ const authoredValue = (node) => {
112
+ if (node?.type === "StringLiteral") return node.value;
113
+ if (node?.type !== "PathExpression" || node.depth || node.data || !node.parts?.length)
114
+ return undefined;
115
+ let value = fm;
116
+ for (const part of node.parts) {
117
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, part))
118
+ return undefined;
119
+ value = value[part];
120
+ }
121
+ return value;
122
+ };
123
+ const visit = (node) => {
124
+ if (!node || typeof node !== "object") return false;
125
+ if (["MustacheStatement", "SubExpression"].includes(node.type)) {
126
+ const name = node.path?.original;
127
+ const policy = PREVIEW_HELPERS[name];
128
+ if (policy === "dependent") return true;
129
+ if (policy === "reference") {
130
+ const target = authoredValue(node.params?.[0]);
131
+ if (typeof target !== "string" || !target.startsWith("#")) return true;
132
+ // Date-valued event fields need calendar context even on this note.
133
+ if (
134
+ node.hash?.pairs?.some(
135
+ (pair) =>
136
+ pair.key === "field" &&
137
+ !["kind", "summary", "name"].includes(pair.value?.value),
138
+ )
139
+ )
140
+ return true;
141
+ }
142
+ if ((node.params?.length || node.hash?.pairs?.length) && !policy) return true;
143
+ }
144
+ return Object.values(node).some((value) =>
145
+ Array.isArray(value) ? value.some(visit) : visit(value),
146
+ );
147
+ };
148
+ try {
149
+ return visit(Handlebars.parse(source));
150
+ } catch {
151
+ return true;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * The end of a wikilink's partially entered trailing attribute block, so an
157
+ * unfinished `{…` stays with the link it follows.
158
+ *
159
+ * @param {string} rest - The source from the link's start.
160
+ * @param {string} raw - The link itself.
161
+ * @returns {string} The link with its attribute block, as far as it goes on the line.
162
+ */
163
+ function withTrailingAttributes(rest, raw) {
164
+ if (rest[raw.length] !== "{") return raw;
165
+ let quoted = false,
166
+ escaped = false,
167
+ end = raw.length + 1;
168
+ for (; end < rest.length && rest[end] !== "\n"; end++) {
169
+ const char = rest[end];
170
+ if (escaped) {
171
+ escaped = false;
172
+ continue;
173
+ }
174
+ if (char === "\\") {
175
+ escaped = true;
176
+ continue;
177
+ }
178
+ if (char === '"') quoted = !quoted;
179
+ if (char === "}" && !quoted) {
180
+ end++;
181
+ break;
182
+ }
183
+ }
184
+ return rest.slice(0, end);
185
+ }
186
+
187
+ /**
188
+ * The cells of one table row, keeping a pipe inside a wikilink, a code span or
189
+ * an extension's braces in its cell.
190
+ *
191
+ * @param {string} line - One table line.
192
+ * @returns {string[]} The trimmed cells, without the empty edges.
193
+ */
194
+ function tableCells(line) {
195
+ const result = [];
196
+ let cell = "",
197
+ wiki = false,
198
+ code = "",
199
+ quote = false,
200
+ braces = 0;
201
+ for (let at = 0; at < line.length; at++) {
202
+ const char = line[at];
203
+ if (char === "\\" && at + 1 < line.length) {
204
+ cell += char + line[++at];
205
+ continue;
206
+ }
207
+ if (char === "`" && !wiki) {
208
+ const marks = /^`+/.exec(line.slice(at))[0];
209
+ if (!code) code = marks;
210
+ else if (code === marks) code = "";
211
+ cell += marks;
212
+ at += marks.length - 1;
213
+ continue;
214
+ }
215
+ if (!code) {
216
+ if (line.slice(at, at + 2) === "[[") wiki = true;
217
+ if (line.slice(at - 1, at + 1) === "]]") wiki = false;
218
+ if (char === '"' && braces) quote = !quote;
219
+ if (!quote && char === "{") braces++;
220
+ if (!quote && char === "}" && braces) braces--;
221
+ }
222
+ if (char === "|" && !wiki && !code && !braces) {
223
+ result.push(cell.trim());
224
+ cell = "";
225
+ } else cell += char;
226
+ }
227
+ result.push(cell.trim());
228
+ if (!result[0]) result.shift();
229
+ if (!result.at(-1)) result.pop();
230
+ return result;
231
+ }
232
+
233
+ /**
234
+ * Install the preview target's rules.
235
+ *
236
+ * @param {import("markdown-it")} md - The shared parser.
237
+ * @param {object} [options]
238
+ * @param {boolean} [options.source] - The buffer has no index: show wikilinks,
239
+ * embeds, spans and local expressions as authored source.
240
+ * @param {object} [options.fm] - The buffer's frontmatter, which a local
241
+ * expression reads.
242
+ * @param {string} [options.file] - The buffer's name, for a finding.
243
+ * @param {Map<string, object>} [options.figures] - The buffer's captioned items
244
+ * by id, which a local `ref` reads.
245
+ */
246
+ export function previewSourcePlugin(md, { source = false, fm = {}, file = "", figures } = {}) {
247
+ const inline = (text) => {
248
+ const tokens = [];
249
+ md.inline.parse(text, md, {}, tokens);
250
+ return md.renderer.renderInline(tokens, md.options, {});
251
+ };
252
+ md.renderer.rules.hl_preview_source = (tokens, idx) => tokens[idx].meta.html;
253
+ // Run before ordinary links and text so protected source becomes one opaque token.
254
+ md.inline.ruler.before("link", "hl_preview_source", (state, silent) => {
255
+ const rest = state.src.slice(state.pos);
256
+ let raw, html;
257
+ if (source && (rest.startsWith("[[") || rest.startsWith("![["))) {
258
+ const match = new RegExp(`^(?:!)?${WIKILINK.source.replace("(?<!!)", "")}`).exec(rest);
259
+ raw = withTrailingAttributes(rest, match?.[0] ?? rest.split("\n")[0]);
260
+ html = wikiSource(raw);
261
+ } else if (rest.startsWith("{{")) {
262
+ const match = new RegExp(`^${EXPRESSION.source.replace("(?<![\\\\{])", "")}`).exec(
263
+ rest,
264
+ );
265
+ raw = match?.[0] ?? rest.split("\n")[0];
266
+ if (source && match && !dependentExpression(raw, fm)) {
267
+ const entries =
268
+ Array.isArray(fm.data?.events) ?
269
+ fm.data.events.filter((entry) => entry && typeof entry === "object")
270
+ : [];
271
+ const own = {
272
+ events: entries.map((entry) => ({ id: entry.id, entry })),
273
+ anchors: entries
274
+ .filter((entry) => entry.id)
275
+ .map((entry) => ({ slug: entry.id, kind: "event" })),
276
+ };
277
+ const events = { note: () => undefined, own: () => own };
278
+ const result = renderMarkdownExpressions(raw, { fm, figures, events, file });
279
+ if (!result.findings.length)
280
+ html = /^{{\s*ref\b/.test(raw) ? inline(result.markdown) : result.markdown;
281
+ }
282
+ html ??= colored("dependent", raw);
283
+ } else return false;
284
+ if (!silent) {
285
+ const token = state.push("hl_preview_source", "", 0);
286
+ token.meta = { html };
287
+ }
288
+ state.pos += raw.length;
289
+ return true;
290
+ });
291
+ if (source)
292
+ md.renderer.rules.span_source = (tokens, idx) => {
293
+ const token = tokens[idx];
294
+ const end = token.meta.labelEnd;
295
+ const secret = Boolean(token.meta.secret);
296
+ const classes = `hl-preview-span${secret ? " hl-preview-secret" : ""}`;
297
+ const focus = secret ? ' tabindex="0"' : "";
298
+ // A secret's label sits behind a cover, so it is set as the reader
299
+ // will read it; any other label is the authored text.
300
+ const text = token.content.slice(1, end);
301
+ const label =
302
+ secret ?
303
+ `<span class="hl-preview-label">${inline(text)}</span>`
304
+ : colored("label", text);
305
+ return `<span class="${classes}"${focus}>${
306
+ colored("bracket", "[") +
307
+ (secret ? `<span class="hl-preview-secret-content">${label}</span>` : label) +
308
+ colored("bracket", "]") +
309
+ colored("attribute", token.content.slice(end + 1))
310
+ }</span>`;
311
+ };
312
+
313
+ // Keep extension pipes inside their cell. Markdown's table splitter only
314
+ // understands escaped pipes, before inline rules have a chance to see them.
315
+ md.block.ruler.before(
316
+ "table",
317
+ "hl_preview_table",
318
+ (state, start, end, silent) => {
319
+ const lineAt = (line) =>
320
+ state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]);
321
+ if (start + 1 >= end || state.sCount[start] - state.blkIndent >= 4) return false;
322
+ const header = tableCells(lineAt(start)),
323
+ separators = tableCells(lineAt(start + 1));
324
+ if (
325
+ !lineAt(start).includes("|") ||
326
+ !header.length ||
327
+ separators.length !== header.length ||
328
+ !separators.every((cell) => /^:?-+:?$/.test(cell))
329
+ )
330
+ return false;
331
+ if (silent) return true;
332
+ const row = (values, tag) => {
333
+ state.push("tr_open", "tr", 1);
334
+ for (let col = 0; col < header.length; col++) {
335
+ const cell = state.push(`${tag}_open`, tag, 1);
336
+ const alignment =
337
+ separators[col].startsWith(":") ?
338
+ separators[col].endsWith(":") ?
339
+ "center"
340
+ : "left"
341
+ : separators[col].endsWith(":") ? "right"
342
+ : "";
343
+ if (alignment) cell.attrSet("class", `hl-preview-align-${alignment}`);
344
+ const token = state.push("inline", "", 0);
345
+ token.content = values[col] ?? "";
346
+ token.children = [];
347
+ state.push(`${tag}_close`, tag, -1);
348
+ }
349
+ state.push("tr_close", "tr", -1);
350
+ };
351
+ const open = state.push("table_open", "table", 1);
352
+ open.map = [start, start + 2];
353
+ state.push("thead_open", "thead", 1);
354
+ row(header, "th");
355
+ state.push("thead_close", "thead", -1);
356
+ state.push("tbody_open", "tbody", 1);
357
+ let line = start + 2;
358
+ for (; line < end && lineAt(line).trim() && lineAt(line).includes("|"); line++)
359
+ row(tableCells(lineAt(line)), "td");
360
+ state.push("tbody_close", "tbody", -1);
361
+ state.push("table_close", "table", -1);
362
+ open.map[1] = line;
363
+ state.line = line;
364
+ return true;
365
+ },
366
+ { alt: ["paragraph", "reference"] },
367
+ );
368
+
369
+ // A buffer runs no query: the fence is the authored source.
370
+ const fence = md.renderer.rules.fence;
371
+ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
372
+ const token = tokens[idx];
373
+ const name = token.info.trim().split(/\s/)[0];
374
+ if (["sql", "pagelist", "map"].includes(name)) {
375
+ const raw =
376
+ token.meta?.source ??
377
+ `${token.markup}${token.info}\n${token.content}${token.markup}`;
378
+ return `<pre class="hl-preview-${name === "map" ? "dependent" : "fence"}"><code>${escape(raw)}</code></pre>\n`;
379
+ }
380
+ return fence(tokens, idx, options, env, self);
381
+ };
382
+ // Capture fences from their original source lines, retaining all delimiters.
383
+ md.core.ruler.before("inline", "hl_preview_structure", (state) => {
384
+ const lines = state.src.split("\n");
385
+ for (const token of state.tokens)
386
+ if (token.type === "fence")
387
+ token.meta = {
388
+ ...token.meta,
389
+ source: lines.slice(token.map[0], token.map[1]).join("\n"),
390
+ };
391
+ // Use parsed heading tokens so a fenced example is never a section boundary.
392
+ for (let i = state.tokens.length - 1; i >= 0; i--) {
393
+ const token = state.tokens[i];
394
+ if (token.type !== "heading_open") continue;
395
+ const content = state.tokens[i + 1];
396
+ const parsed = splitHeadingAttributes(content.content);
397
+ if (/^(?:see also|related pages)$/i.test(parsed.text)) {
398
+ const level = Number(token.tag.slice(1));
399
+ let end = i + 3;
400
+ while (
401
+ end < state.tokens.length &&
402
+ !(
403
+ state.tokens[end].type === "heading_open" &&
404
+ Number(state.tokens[end].tag.slice(1)) <= level
405
+ )
406
+ )
407
+ end++;
408
+ state.tokens.splice(i, end - i);
409
+ } else if (!parsed.problems.length && parsed.classes.includes("secret")) {
410
+ // A withheld heading names only its anchor; the disclosure
411
+ // below carries the withholding.
412
+ content.content = parsed.text;
413
+ if (parsed.id) token.attrSet("id", parsed.id);
414
+ token.meta = { ...token.meta, secret: true };
415
+ }
416
+ }
417
+ // A `.secret` heading hides its section through the next heading at the
418
+ // same or a higher level, wherever the heading sits.
419
+ const output = [],
420
+ sections = [];
421
+ const closeSection = () => {
422
+ output.push(new state.Token("details_close", "details", -1));
423
+ sections.pop();
424
+ };
425
+ for (const token of state.tokens) {
426
+ const level = token.type === "heading_open" ? Number(token.tag.slice(1)) : null;
427
+ while (
428
+ sections.length &&
429
+ ((token.nesting === -1 && token.level < sections.at(-1).scope) ||
430
+ (level !== null &&
431
+ token.level <= sections.at(-1).scope &&
432
+ level <= sections.at(-1).level))
433
+ )
434
+ closeSection();
435
+ if (token.type === "heading_open" && token.meta?.secret) {
436
+ const open = new state.Token("details_open", "details", 1);
437
+ open.attrSet("class", "secret");
438
+ output.push(open, new state.Token("summary_open", "summary", 1));
439
+ const title = new state.Token("inline", "", 0);
440
+ title.content = "Secret";
441
+ title.children = [];
442
+ output.push(title, new state.Token("summary_close", "summary", -1));
443
+ sections.push({ level, scope: token.level });
444
+ }
445
+ output.push(token);
446
+ }
447
+ while (sections.length) closeSection();
448
+ state.tokens = output;
449
+ });
450
+ }
@@ -85,13 +85,10 @@ function projectScripts(answers) {
85
85
  "build:local": "npm install && npm run build:noci",
86
86
  "build:content-index": "package-build content-index",
87
87
  "build:deps": "package-build deps fetch",
88
- "build:site-content": "package-build site",
89
- "build:site-html": "hugo --source build/hugo --minify --gc --cleanDestinationDir",
90
- "build:site-root": "package-build site-root",
91
- "build:site":
92
- "run-s build:deps build:content-index build:site-content build:site-html build:site-root",
93
- "serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
94
- "serve:site-html": "hugo server --source build/hugo",
88
+ "build:site-render": "package-build site build",
89
+ "build:site": "run-s build:deps build:content-index build:site-render",
90
+ "serve:site-render": "package-build site serve",
91
+ "serve:site": "run-s build:deps build:content-index serve:site-render",
95
92
  "build:book": "package-build pdf",
96
93
  lint: "run-s lint:format lint:markdown lint:addresses lint:content-links",
97
94
  "lint:format": "package-build format",
@@ -199,7 +196,6 @@ function projectManifest(answers) {
199
196
  scripts: projectScripts(answers),
200
197
  devDependencies: {
201
198
  "@changesets/cli": "^3.0.0",
202
- "@heroiclands/hugo-theme": "^0.9.0",
203
199
  "@heroiclands/package-build": `^${answers.version}`,
204
200
  "npm-run-all": "^4.1.5",
205
201
  },
@@ -268,7 +264,7 @@ function projectText(answers) {
268
264
  "prettier.config.js":
269
265
  '/* SPDX-License-Identifier: GPL-3.0-or-later */\nexport { default } from "@heroiclands/package-build/prettier";\n',
270
266
  ".prettierignore": "CHANGELOG.md\n",
271
- "README.md": `# ${answers.title}\n\n${answers.description}\n\nRun \`npm install\`, then \`npm run lint\`. Build the website with \`npm run build:site\`, serve it with \`npm run serve:site\`, and build the book with \`npm run build:book\`. The site requires Hugo Extended and the book requires Typst on your PATH. Site deployment needs its own hosting configuration.\n`,
267
+ "README.md": `# ${answers.title}\n\n${answers.description}\n\nRun \`npm install\`, then \`npm run lint\`. Build the website with \`npm run build:site\`, serve it with \`npm run serve:site\`, and build the book with \`npm run build:book\`. The book requires Typst on your PATH. Site deployment needs its own hosting configuration.\n`,
272
268
  "LICENSE.md": license,
273
269
  "lang/en.json": JSON.stringify({
274
270
  [answers.contentPackage.toUpperCase()]: { Title: answers.title },
@@ -444,9 +440,6 @@ export function checkProject(rootDir) {
444
440
  if (!manifest.devDependencies?.["@heroiclands/package-build"]) {
445
441
  add("package.json", "the package-build development dependency is required");
446
442
  }
447
- if (!manifest.devDependencies?.["@heroiclands/hugo-theme"]) {
448
- add("package.json", "the Hugo theme development dependency is required");
449
- }
450
443
  if (!nonempty(manifest.homepage))
451
444
  add("package.json", "homepage is required for the website");
452
445
  } catch (err) {
@@ -228,9 +228,9 @@ export const MARKDOWNLINT_CONFIG = Object.freeze({
228
228
  MD011: true,
229
229
 
230
230
  // MD034 — no bare URLs. Whether a bare URL becomes a link depends on the
231
- // renderer, and this content is rendered by three of them (Hugo/goldmark
232
- // for a knowledgebase, Foundry's own markdown for compendium journals, and
233
- // GitHub for the repository pages). Writing the link explicitly is the only
231
+ // renderer, and this content is rendered by three of them (the site
232
+ // renderer for a knowledgebase, Foundry's own markdown for compendium
233
+ // journals, and GitHub for the repository pages). Writing the link explicitly is the only
234
234
  // form that renders the same everywhere.
235
235
  MD034: true,
236
236
 
@@ -0,0 +1,29 @@
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
+ * Reads standard input to its end as UTF-8 text.
16
+ *
17
+ * The stream is read asynchronously, so a pipe whose writer is slower than the
18
+ * reader, or a non-blocking descriptor, delivers the whole input instead of
19
+ * failing with `EAGAIN`.
20
+ *
21
+ * @param {NodeJS.ReadableStream} [stream] The stream to drain; standard input by default.
22
+ * @returns {Promise<string>} The complete input.
23
+ */
24
+ export async function readStdin(stream = process.stdin) {
25
+ stream.setEncoding("utf8");
26
+ let text = "";
27
+ for await (const chunk of stream) text += chunk;
28
+ return text;
29
+ }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Spans, divs and poetry fences marked with `lang=`, `font=` or `map=`, projected
15
+ * Spans, divs and verse fences marked with `lang=`, `font=` or `map=`, projected
16
16
  * from own-note index records into the SQL `runs` relation. Every value is a
17
17
  * column value and none is a key, so two notes writing one tag in different
18
18
  * case are two rows. Documentation records and assets contribute no rows.
@@ -41,7 +41,7 @@ export const RUN_COLUMNS = Object.freeze([
41
41
  * One run as an index record carries it.
42
42
  *
43
43
  * @typedef {object} RecordRun
44
- * @property {"span"|"block"|"poetry"} kind - The construct.
44
+ * @property {"span"|"block"|"verse"} kind - The construct.
45
45
  * @property {string|null} lang - The language it is set in, as written: its
46
46
  * own `lang=`, else the one the div or fence around it states.
47
47
  * @property {string|null} tag - The BCP 47 tag it resolves to, or null.