@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
@@ -13,528 +13,23 @@
13
13
 
14
14
  /** Buffer-only HTML preview shared by editors and external renderers. @module */
15
15
 
16
- import MarkdownIt from "markdown-it";
17
- import footnotePlugin from "markdown-it-footnote";
18
- import deflistPlugin from "markdown-it-deflist";
19
- import { dictionaryListPlugin } from "./definition-list.mjs";
20
- import Handlebars from "handlebars";
21
- import matter from "gray-matter";
22
- import { ALERT_TYPES, alertMarkdownPlugin, scanAlerts } from "./content-alerts.mjs";
23
- import { spanMarkdownPlugin } from "./content-spans.mjs";
24
- import { scanPoetry, poetryStanzas } from "./content-poetry.mjs";
16
+ import { sharedContentRules } from "./content-styles.mjs";
17
+ import { readMatter } from "./note-matter.mjs";
25
18
  import { scanFigures } from "./content-figures.mjs";
26
- import { scanBlocks } from "./content-blocks.mjs";
27
- import { splitHeadingAttributes } from "./heading-attributes.mjs";
28
- import { EXPRESSION, renderMarkdownExpressions } from "./markdown-expressions.mjs";
29
- import { WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
19
+ import { createRenderer, inertHtml } from "./content-render.mjs";
20
+ import { PREVIEW_HELPERS, colored, wikiSource } from "./preview-source.mjs";
30
21
  import { dataFields } from "./note-vocabulary.mjs";
31
22
  import { applyComputedBeingAge } from "./being-age.mjs";
32
23
  import { DEFAULT_CURRENT_DATE, normalizeCurrentDate } from "./current-date.mjs";
33
24
 
34
- /** Explicit dependency policy: a new registered helper requires a preview decision. */
35
- export const PREVIEW_HELPERS = Object.freeze({
36
- eq: "local",
37
- gt: "local",
38
- gte: "local",
39
- lt: "local",
40
- lte: "local",
41
- not: "local",
42
- and: "local",
43
- or: "local",
44
- words: "local",
45
- digits: "local",
46
- sql: "dependent",
47
- dateformat: "dependent",
48
- ref: "reference",
49
- });
25
+ export { PREVIEW_HELPERS };
50
26
 
51
- const escapeHtml = new MarkdownIt().utils.escapeHtml;
52
- const escape = (value) => escapeHtml(String(value ?? ""));
53
- const colored = (role, source) => `<span class="hl-preview-${role}">${escape(source)}</span>`;
54
-
55
- /** Highlight without interpreting any Markdown or HTML inside the source. */
56
- function wikiSource(raw) {
57
- const open = raw.startsWith("!") ? "![[" : "[[";
58
- const close = raw.indexOf("]]", open.length);
59
- const end = close < 0 ? raw.length : close;
60
- const inner = raw.slice(open.length, end);
61
- const pipe = inner.indexOf("|");
62
- const secret = close >= 0 && parseWikilink(inner, raw).attributes?.classes.includes("secret");
63
- const label =
64
- colored("address", pipe < 0 ? inner : inner.slice(0, pipe)) +
65
- (pipe < 0 ? "" : colored("bracket", "|") + colored("label", inner.slice(pipe + 1)));
66
- const html =
67
- colored("bracket", open) +
68
- (secret ? `<span class="hl-preview-secret-content">${label}</span>` : label) +
69
- (close < 0 ? "" : colored("bracket", "]]") + colored("attribute", raw.slice(close + 2)));
70
- return secret ? `<span class="hl-preview-secret" tabindex="0">${html}</span>` : html;
71
- }
72
-
73
- /** Read a dependent call anywhere in the Handlebars AST, including hash values. */
74
- function dependentExpression(source, fm) {
75
- const authoredValue = (node) => {
76
- if (node?.type === "StringLiteral") return node.value;
77
- if (node?.type !== "PathExpression" || node.depth || node.data || !node.parts?.length)
78
- return undefined;
79
- let value = fm;
80
- for (const part of node.parts) {
81
- if (!value || typeof value !== "object" || !Object.hasOwn(value, part))
82
- return undefined;
83
- value = value[part];
84
- }
85
- return value;
86
- };
87
- const visit = (node) => {
88
- if (!node || typeof node !== "object") return false;
89
- if (["MustacheStatement", "SubExpression"].includes(node.type)) {
90
- const name = node.path?.original;
91
- const policy = PREVIEW_HELPERS[name];
92
- if (policy === "dependent") return true;
93
- if (policy === "reference") {
94
- const target = authoredValue(node.params?.[0]);
95
- if (typeof target !== "string" || !target.startsWith("#")) return true;
96
- // Date-valued event fields need calendar context even on this note.
97
- if (
98
- node.hash?.pairs?.some(
99
- (pair) =>
100
- pair.key === "field" &&
101
- !["kind", "summary", "name"].includes(pair.value?.value),
102
- )
103
- )
104
- return true;
105
- }
106
- if ((node.params?.length || node.hash?.pairs?.length) && !policy) return true;
107
- }
108
- return Object.values(node).some((value) =>
109
- Array.isArray(value) ? value.some(visit) : visit(value),
110
- );
111
- };
112
- try {
113
- return visit(Handlebars.parse(source));
114
- } catch {
115
- return true;
116
- }
117
- }
118
-
119
- function parserFor(fm, findings, file) {
120
- const md = new MarkdownIt({ html: false, linkify: false })
121
- .use(footnotePlugin)
122
- .use(deflistPlugin)
123
- .use(dictionaryListPlugin)
124
- .use(alertMarkdownPlugin)
125
- .use(spanMarkdownPlugin, { source: true });
126
- const figures = new Map();
127
- const inline = (text, env = {}) => {
128
- const tokens = [];
129
- md.inline.parse(text, md, env, tokens);
130
- return md.renderer.renderInline(tokens, md.options, env);
131
- };
132
- md.renderer.rules.hl_preview_source = (tokens, idx) => tokens[idx].meta.html;
133
- // Run before ordinary links and text so protected source becomes one opaque token.
134
- md.inline.ruler.before("link", "hl_preview_source", (state, silent) => {
135
- const rest = state.src.slice(state.pos);
136
- let raw, html;
137
- if (rest.startsWith("[[") || rest.startsWith("![[")) {
138
- const match = new RegExp(`^(?:!)?${WIKILINK.source.replace("(?<!!)", "")}`).exec(rest);
139
- raw = match?.[0] ?? rest.split("\n")[0];
140
- // Preserve a partially entered trailing attribute too.
141
- if (rest[raw.length] === "{") {
142
- let quoted = false,
143
- escaped = false,
144
- end = raw.length + 1;
145
- for (; end < rest.length && rest[end] !== "\n"; end++) {
146
- const char = rest[end];
147
- if (escaped) {
148
- escaped = false;
149
- continue;
150
- }
151
- if (char === "\\") {
152
- escaped = true;
153
- continue;
154
- }
155
- if (char === '"') quoted = !quoted;
156
- if (char === "}" && !quoted) {
157
- end++;
158
- break;
159
- }
160
- }
161
- raw = rest.slice(0, end);
162
- }
163
- html = wikiSource(raw);
164
- } else if (rest.startsWith("{{")) {
165
- const match = new RegExp(`^${EXPRESSION.source.replace("(?<![\\\\{])", "")}`).exec(
166
- rest,
167
- );
168
- raw = match?.[0] ?? rest.split("\n")[0];
169
- if (!dependentExpression(raw, fm) && match) {
170
- const entries =
171
- Array.isArray(fm.data?.events) ?
172
- fm.data.events.filter((entry) => entry && typeof entry === "object")
173
- : [];
174
- const own = {
175
- events: entries.map((entry) => ({ id: entry.id, entry })),
176
- anchors: entries
177
- .filter((entry) => entry.id)
178
- .map((entry) => ({ slug: entry.id, kind: "event" })),
179
- };
180
- const events = { note: () => undefined, own: () => own };
181
- const result = renderMarkdownExpressions(raw, { fm, figures, events, file });
182
- if (!result.findings.length)
183
- html = /^{{\s*ref\b/.test(raw) ? inline(result.markdown) : result.markdown;
184
- }
185
- html ??= colored("dependent", raw);
186
- } else return false;
187
- if (!silent) {
188
- const token = state.push("hl_preview_source", "", 0);
189
- token.meta = { html };
190
- }
191
- state.pos += raw.length;
192
- return true;
193
- });
194
- md.renderer.rules.span_source = (tokens, idx) => {
195
- const token = tokens[idx];
196
- const end = token.meta.labelEnd;
197
- const secret = Boolean(token.meta.secret);
198
- const classes = `hl-preview-span${secret ? " hl-preview-secret" : ""}`;
199
- const focus = secret ? ' tabindex="0"' : "";
200
- const label = colored("label", token.content.slice(1, end));
201
- return `<span class="${classes}"${focus}>${
202
- colored("bracket", "[") +
203
- (secret ? `<span class="hl-preview-secret-content">${label}</span>` : label) +
204
- colored("bracket", "]") +
205
- colored("attribute", token.content.slice(end + 1))
206
- }</span>`;
207
- };
208
- // Keep extension pipes inside their cell. Markdown's table splitter only
209
- // understands escaped pipes, before inline rules have a chance to see them.
210
- md.block.ruler.before(
211
- "table",
212
- "hl_preview_table",
213
- (state, start, end, silent) => {
214
- const lineAt = (line) =>
215
- state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]);
216
- const cells = (line) => {
217
- const result = [];
218
- let cell = "",
219
- wiki = false,
220
- code = "",
221
- quote = false,
222
- braces = 0;
223
- for (let at = 0; at < line.length; at++) {
224
- const char = line[at];
225
- if (char === "\\" && at + 1 < line.length) {
226
- cell += char + line[++at];
227
- continue;
228
- }
229
- if (char === "`" && !wiki) {
230
- const marks = /^`+/.exec(line.slice(at))[0];
231
- if (!code) code = marks;
232
- else if (code === marks) code = "";
233
- cell += marks;
234
- at += marks.length - 1;
235
- continue;
236
- }
237
- if (!code) {
238
- if (line.slice(at, at + 2) === "[[") wiki = true;
239
- if (line.slice(at - 1, at + 1) === "]]") wiki = false;
240
- if (char === '"' && braces) quote = !quote;
241
- if (!quote && char === "{") braces++;
242
- if (!quote && char === "}" && braces) braces--;
243
- }
244
- if (char === "|" && !wiki && !code && !braces) {
245
- result.push(cell.trim());
246
- cell = "";
247
- } else cell += char;
248
- }
249
- result.push(cell.trim());
250
- if (!result[0]) result.shift();
251
- if (!result.at(-1)) result.pop();
252
- return result;
253
- };
254
- if (start + 1 >= end || state.sCount[start] - state.blkIndent >= 4) return false;
255
- const header = cells(lineAt(start)),
256
- separators = cells(lineAt(start + 1));
257
- if (
258
- !lineAt(start).includes("|") ||
259
- !header.length ||
260
- separators.length !== header.length ||
261
- !separators.every((cell) => /^:?-+:?$/.test(cell))
262
- )
263
- return false;
264
- if (silent) return true;
265
- const row = (values, tag) => {
266
- state.push("tr_open", "tr", 1);
267
- for (let col = 0; col < header.length; col++) {
268
- const cell = state.push(`${tag}_open`, tag, 1);
269
- const alignment =
270
- separators[col].startsWith(":") ?
271
- separators[col].endsWith(":") ?
272
- "center"
273
- : "left"
274
- : separators[col].endsWith(":") ? "right"
275
- : "";
276
- if (alignment) cell.attrSet("class", `hl-preview-align-${alignment}`);
277
- const token = state.push("inline", "", 0);
278
- token.content = values[col] ?? "";
279
- token.children = [];
280
- state.push(`${tag}_close`, tag, -1);
281
- }
282
- state.push("tr_close", "tr", -1);
283
- };
284
- const open = state.push("table_open", "table", 1);
285
- open.map = [start, start + 2];
286
- state.push("thead_open", "thead", 1);
287
- row(header, "th");
288
- state.push("thead_close", "thead", -1);
289
- state.push("tbody_open", "tbody", 1);
290
- let line = start + 2;
291
- for (; line < end && lineAt(line).trim() && lineAt(line).includes("|"); line++)
292
- row(cells(lineAt(line)), "td");
293
- state.push("tbody_close", "tbody", -1);
294
- state.push("table_close", "table", -1);
295
- open.map[1] = line;
296
- state.line = line;
297
- return true;
298
- },
299
- { alt: ["paragraph", "reference"] },
300
- );
301
- const quoteOpen = md.renderer.rules.blockquote_open;
302
- md.renderer.rules.blockquote_open = (tokens, idx, options, env, self) => {
303
- const block = tokens[idx].meta?.alert;
304
- if (!block)
305
- return quoteOpen ? quoteOpen(tokens, idx, options, env, self) : "<blockquote>\n";
306
- const id = block.id ? ` id="${escape(block.id)}"` : "";
307
- return `<blockquote class="alert alert-${block.type}"${id}>\n`;
308
- };
309
- md.renderer.rules.hl_preview_alert_title_open = (tokens, idx) =>
310
- `<p class="alert-title">${ALERT_TYPES[tokens[idx].meta.type].svg}`;
311
- const fence = md.renderer.rules.fence;
312
- md.renderer.rules.fence = (tokens, idx, options, env, self) => {
313
- const token = tokens[idx];
314
- const name = token.info.trim().split(/\s/)[0];
315
- if (["sql", "pagelist", "map"].includes(name)) {
316
- const raw =
317
- token.meta?.source ??
318
- `${token.markup}${token.info}\n${token.content}${token.markup}`;
319
- return `<pre class="hl-preview-${name === "map" ? "dependent" : "fence"}"><code>${escape(raw)}</code></pre>\n`;
320
- }
321
- return fence(tokens, idx, options, env, self);
322
- };
323
- // Capture fences from their original source lines, retaining all delimiters.
324
- md.core.ruler.before("inline", "hl_preview_structure", (state) => {
325
- const lines = state.src.split("\n");
326
- const alertErrors = scanAlerts(state.src).errors;
327
- for (let i = state.tokens.length - 1; i >= 0; i--) {
328
- const token = state.tokens[i],
329
- alert = token.meta?.alert;
330
- if (
331
- !alert ||
332
- !alertErrors.some(
333
- (error) => error.line >= alert.start + 1 && error.line <= alert.end + 1,
334
- )
335
- )
336
- continue;
337
- let depth = 1,
338
- end = i + 1;
339
- for (; end < state.tokens.length; end++) {
340
- if (state.tokens[end].type === "blockquote_open") depth++;
341
- if (state.tokens[end].type === "blockquote_close" && --depth === 0) break;
342
- }
343
- token.type = "hl_preview_source";
344
- token.nesting = 0;
345
- token.meta = {
346
- html: `<pre>${escape(lines.slice(alert.start, alert.end + 1).join("\n"))}</pre>`,
347
- };
348
- state.tokens.splice(i + 1, end - i);
349
- }
350
- for (let i = state.tokens.length - 1; i >= 0; i--) {
351
- const block = state.tokens[i].meta?.alert;
352
- if (!block) continue;
353
- const open = new state.Token("hl_preview_alert_title_open", "p", 1);
354
- open.meta = block;
355
- const title = new state.Token("inline", "", 0);
356
- title.content = block.title;
357
- title.children = [];
358
- const close = new state.Token("paragraph_close", "p", -1);
359
- state.tokens.splice(i + 1, 0, open, title, close);
360
- }
361
- const poetry = scanPoetry(state.src).blocks;
362
- const localFigures = scanFigures(state.src).figures;
363
- for (const fig of localFigures) if (fig.id) figures.set(fig.id, fig);
364
- for (const token of state.tokens) {
365
- if (token.type === "fence")
366
- token.meta = {
367
- ...token.meta,
368
- source: lines.slice(token.map[0], token.map[1]).join("\n"),
369
- poem: poetry.find((poem) => poem.start === token.map[0]),
370
- };
371
- }
372
- // Poetry must become inline tokens before footnote collection, so a
373
- // definition outside the fence remains visible to a verse reference.
374
- for (let i = state.tokens.length - 1; i >= 0; i--) {
375
- const token = state.tokens[i];
376
- const poem = token.meta?.poem;
377
- if (!poem) continue;
378
- const replacement = [];
379
- const push = (type, tag, nesting, attrs) => {
380
- const part = new state.Token(type, tag, nesting);
381
- if (attrs) part.attrs = attrs;
382
- replacement.push(part);
383
- return part;
384
- };
385
- push("hl_preview_poetry_open", "div", 1, [["class", "poetry"]]).map = token.map;
386
- for (const stanza of poetryStanzas(poem.body)) {
387
- push("paragraph_open", "p", 1);
388
- stanza.forEach(({ text, level }, row) => {
389
- if (row) push("hardbreak", "br", 0);
390
- push("hl_preview_poetry_line_open", "span", 1, [
391
- ["class", `poetry-line i${level}`],
392
- ]);
393
- const line = push("inline", "", 0);
394
- line.content = text;
395
- line.children = [];
396
- push("hl_preview_poetry_line_close", "span", -1);
397
- });
398
- push("paragraph_close", "p", -1);
399
- }
400
- push("hl_preview_poetry_close", "div", -1);
401
- state.tokens.splice(i, 1, ...replacement);
402
- }
403
- // Work backwards so figure ranges and heading removals retain source maps.
404
- for (let i = state.tokens.length - 1; i >= 0; i--) {
405
- const token = state.tokens[i];
406
- if (token.type !== "heading_open") continue;
407
- const content = state.tokens[i + 1];
408
- const parsed = splitHeadingAttributes(content.content);
409
- if (/^(?:see also|related pages)$/i.test(parsed.text)) {
410
- const level = Number(token.tag.slice(1));
411
- let end = i + 3;
412
- while (
413
- end < state.tokens.length &&
414
- !(
415
- state.tokens[end].type === "heading_open" &&
416
- Number(state.tokens[end].tag.slice(1)) <= level
417
- )
418
- )
419
- end++;
420
- state.tokens.splice(i, end - i);
421
- } else if (!parsed.problems.length) {
422
- content.content = parsed.text;
423
- if (parsed.id) token.attrSet("id", parsed.id);
424
- token.meta = { ...token.meta, secret: parsed.classes.includes("secret") };
425
- }
426
- }
427
- for (const fig of localFigures.reverse()) {
428
- const first = state.tokens.findIndex(
429
- (token) => token.type === "paragraph_open" && token.map?.[0] === fig.captionStart,
430
- );
431
- if (first < 0) continue;
432
- let after = first + 3;
433
- while (
434
- after < state.tokens.length &&
435
- (state.tokens[after].map?.[0] ?? -1) < fig.bodyEnd
436
- )
437
- after++;
438
- // Match closing tokens of the last block as well.
439
- while (
440
- after < state.tokens.length &&
441
- !state.tokens[after].map &&
442
- state.tokens[after].nesting < 0
443
- )
444
- after++;
445
- const open = new state.Token("hl_preview_figure_open", "div", 1);
446
- open.meta = fig;
447
- const caption = new state.Token("inline", "", 0);
448
- caption.content = `${fig.label}${fig.label && fig.hasCaption ? ": " : ""}${fig.caption}`;
449
- caption.children = [];
450
- const titleOpen = new state.Token("paragraph_open", "p", 1);
451
- titleOpen.attrSet("class", "content-figure-label");
452
- const titleClose = new state.Token("paragraph_close", "p", -1);
453
- const close = new state.Token("hl_preview_figure_close", "div", -1);
454
- state.tokens.splice(after, 0, titleOpen, caption, titleClose, close);
455
- state.tokens.splice(first, 3, open);
456
- }
457
- // Use parsed heading tokens so fenced examples are never section boundaries.
458
- // Keep all inline tokens in this document for shared footnote collection.
459
- const output = [],
460
- sections = [];
461
- const closeSection = () => {
462
- output.push(new state.Token("details_close", "details", -1));
463
- sections.pop();
464
- };
465
- for (const token of state.tokens) {
466
- const level = token.type === "heading_open" ? Number(token.tag.slice(1)) : null;
467
- while (
468
- sections.length &&
469
- ((token.nesting === -1 && token.level < sections.at(-1).scope) ||
470
- (level !== null &&
471
- token.level <= sections.at(-1).scope &&
472
- level <= sections.at(-1).level))
473
- )
474
- closeSection();
475
- if (token.type === "heading_open" && token.meta?.secret) {
476
- const open = new state.Token("details_open", "details", 1);
477
- open.attrSet("class", "secret");
478
- output.push(open, new state.Token("summary_open", "summary", 1));
479
- const title = new state.Token("inline", "", 0);
480
- title.content = "Secret";
481
- title.children = [];
482
- output.push(title, new state.Token("summary_close", "summary", -1));
483
- sections.push({ level, scope: token.level });
484
- }
485
- output.push(token);
486
- }
487
- while (sections.length) closeSection();
488
- state.tokens = output;
489
- });
490
- md.renderer.rules.hl_preview_figure_open = (tokens, idx) => {
491
- const fig = tokens[idx].meta;
492
- return `<div class="content-figure content-figure-${escape(fig.kind)}"${fig.slug ? ` id="${escape(fig.slug)}"` : ""}>\n`;
493
- };
494
- md.renderer.rules.hl_preview_figure_close = () => "</div>\n";
495
- // Shared container parser determines complete nested ranges; tokenizing each
496
- // body within the same state keeps document footnotes in scope.
497
- md.block.ruler.before(
498
- "fence",
499
- "hl_preview_container",
500
- (state, start, end, silent) => {
501
- if (!state.src.slice(state.bMarks[start] + state.tShift[start]).startsWith(":::"))
502
- return false;
503
- const block = scanBlocks(state.src).blocks.find((block) => block.start === start);
504
- if (!block || block.end >= end) return false;
505
- if (silent) return true;
506
- const open = state.push(
507
- "hl_preview_container_open",
508
- block.name === "secret" ? "details" : "div",
509
- 1,
510
- );
511
- open.meta = block;
512
- if (block.name === "secret") {
513
- state.push("summary_open", "summary", 1);
514
- const title = state.push("inline", "", 0);
515
- title.content = block.title;
516
- title.children = [];
517
- state.push("summary_close", "summary", -1);
518
- }
519
- const oldMax = state.lineMax;
520
- state.lineMax = block.end;
521
- state.md.block.tokenize(state, start + 1, block.end);
522
- state.lineMax = oldMax;
523
- const close = state.push("hl_preview_container_close", open.tag, -1);
524
- close.meta = block;
525
- state.line = block.end + 1;
526
- return true;
527
- },
528
- { alt: ["paragraph", "reference", "blockquote", "list"] },
529
- );
530
- md.renderer.rules.hl_preview_container_open = (tokens, idx, _options, env) => {
531
- const block = tokens[idx].meta;
532
- const attrs = `${block.id ? ` id="${escape(block.id)}"` : ""} class="${escape([block.name, ...block.classes].join(" "))}"`;
533
- return block.name === "secret" ? `<details${attrs}>` : `<div${attrs}>\n`;
534
- };
535
- md.renderer.rules.hl_preview_container_close = (tokens, idx) => `</${tokens[idx].tag}>\n`;
536
- return md;
537
- }
27
+ const escape = (value) =>
28
+ String(value ?? "")
29
+ .replace(/&/g, "&amp;")
30
+ .replace(/</g, "&lt;")
31
+ .replace(/>/g, "&gt;")
32
+ .replace(/"/g, "&quot;");
538
33
 
539
34
  /** A small profile from authored fields; references never acquire resolved data. */
540
35
  function localInfobox(fm, md) {
@@ -569,21 +64,30 @@ function localInfobox(fm, md) {
569
64
  }
570
65
 
571
66
  /**
572
- * Render one complete buffer with no configuration, index, filesystem or database.
67
+ * Render one complete buffer.
68
+ *
69
+ * Without `index` the buffer is rendered with no configuration, content index,
70
+ * filesystem or database: wikilinks, spans and expressions show as highlighted
71
+ * authored source. With `index` the buffer is expanded as the website expands
72
+ * a page and rendered as every target renders it, so links, embeds, marked
73
+ * text, icons and figure numbers read as they will on the page; only what a
74
+ * buffer cannot answer, such as a query, stays highlighted source.
75
+ *
573
76
  * @param {string} text - Markdown, optionally beginning with YAML frontmatter.
574
- * @param {{dark?: boolean, infobox?: boolean, file?: string, currentDate?: string}} [options] - Explicit palette, profile visibility, diagnostic filename and neutral current date (default "720.1").
77
+ * @param {{dark?: boolean, infobox?: boolean, file?: string, currentDate?: string,
78
+ * index?: import("./preview-index.mjs").PreviewIndex}} [options] - Explicit palette, profile visibility, diagnostic filename, neutral current date (default "720.1") and the content index, from {@link module:engine/preview-index.previewIndex}.
575
79
  * @returns {{ok: boolean, html: string, frontmatter: object, findings: object[]}} HTML and metadata with locally calculable ages; unavailable ages remain absent or null.
576
80
  */
577
81
  export function renderPreview(
578
82
  text,
579
- { dark = false, infobox = true, file = "", currentDate = DEFAULT_CURRENT_DATE } = {},
83
+ { dark = false, infobox = true, file = "", currentDate = DEFAULT_CURRENT_DATE, index } = {},
580
84
  ) {
581
85
  const date = normalizeCurrentDate(currentDate);
582
86
  const findings = [];
583
87
  let frontmatter = {},
584
88
  body = String(text ?? "");
585
89
  try {
586
- const parsed = matter(body);
90
+ const parsed = readMatter(body);
587
91
  frontmatter =
588
92
  parsed.data && typeof parsed.data === "object" && !Array.isArray(parsed.data) ?
589
93
  structuredClone(parsed.data)
@@ -602,9 +106,33 @@ export function renderPreview(
602
106
  )
603
107
  frontmatter.data.age = null;
604
108
  }
605
- const md = parserFor(frontmatter, findings, file);
606
- const content = md.render(body);
607
- const profile = infobox ? localInfobox(frontmatter, md) : "";
109
+ // A tag the author wrote reads as text; the stages below write the only HTML.
110
+ const authored = inertHtml(body);
111
+ const indexed = Boolean(index);
112
+ const scan = scanFigures(authored);
113
+ const renderer = createRenderer("preview", {
114
+ source: !indexed,
115
+ sourceContext: {
116
+ fm: frontmatter,
117
+ file,
118
+ figures: new Map(scan.figures.filter((f) => f.id).map((f) => [f.id, f])),
119
+ },
120
+ icons: index?.icons,
121
+ images: index && { lookupAsset: index.lookupAsset },
122
+ });
123
+ let markdown = authored;
124
+ let figures = scan.figures;
125
+ if (indexed) {
126
+ const expanded = index.expand(authored, { fm: frontmatter, file });
127
+ markdown = expanded.markdown;
128
+ figures = expanded.figures;
129
+ }
130
+ const content = renderer.render(markdown, {
131
+ runs: index?.runs,
132
+ figures,
133
+ resolveSrc: index?.resolveSrc,
134
+ });
135
+ const profile = infobox ? localInfobox(frontmatter, renderer.md) : "";
608
136
  return {
609
137
  ok: true,
610
138
  html: `<div class="hl-preview hl-preview-${dark ? "dark" : "light"}">${profile}<main class="hl-preview-main">${content}</main></div>`,
@@ -665,7 +193,6 @@ export function previewStyles() {
665
193
  .hl-preview .alert-warning { --hl-accent:var(--hl-warning); --hl-alert-bg:var(--hl-warning-bg); }
666
194
  .hl-preview .alert .alert-title { color:var(--hl-accent); font-weight:700; opacity:1; }
667
195
  .hl-preview .alert .alert-icon { width:1em; height:1em; margin-right:.5em; vertical-align:-.1em; color:var(--hl-accent); fill:currentColor; }
668
- .hl-preview .poetry-line { white-space:pre-wrap; }
669
- ${Array.from({ length: 9 }, (_, n) => `.hl-preview .poetry-line.i${n} { padding-left:${n * 2}em; }`).join("\n")}
196
+ ${sharedContentRules(".hl-preview")}
670
197
  `;
671
198
  }