@heroiclands/package-build 23.13.0 → 24.0.1

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 (236) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -1
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +150 -1
  5. package/bin/content-commands.mjs +356 -210
  6. package/bin/package-build.mjs +3 -2
  7. package/ci/check-banners.mjs +7 -7
  8. package/config.mjs +21 -6
  9. package/content-config.mjs +199 -80
  10. package/docs/api.md +93 -72
  11. package/docs/authoring/links-and-markup.md +316 -78
  12. package/docs/commands.md +248 -80
  13. package/docs/configuration.md +210 -102
  14. package/docs/content-format.md +2 -2
  15. package/docs/diagnostics.md +2 -2
  16. package/docs/getting-started.md +1 -1
  17. package/docs/guides/icons.md +1 -1
  18. package/docs/guides/module-project.md +5 -8
  19. package/docs/guides/release-and-deploy.md +2 -2
  20. package/docs/guides/site.md +155 -23
  21. package/docs/guides/system-project.md +1 -1
  22. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  23. package/docs/index.md +1 -0
  24. package/docs/project-setup.md +28 -30
  25. package/docs/reference/format-details.md +589 -308
  26. package/docs/reference/note-types.md +40 -10
  27. package/docs/reference/theme-pages.md +127 -108
  28. package/docs/source-preview.md +15 -1
  29. package/engine/asset-shipping.mjs +22 -10
  30. package/engine/base-compiler.mjs +62 -104
  31. package/engine/changelog-lint.mjs +1 -1
  32. package/engine/content-address.mjs +1 -1
  33. package/engine/content-alerts.mjs +8 -4
  34. package/engine/content-blocks.mjs +26 -11
  35. package/engine/content-embeds.mjs +1 -1
  36. package/engine/content-expand.mjs +376 -0
  37. package/engine/content-figures.mjs +13 -6
  38. package/engine/content-format.yaml +8 -0
  39. package/engine/content-glyphs.mjs +213 -0
  40. package/engine/content-images.mjs +6 -7
  41. package/engine/content-links.mjs +3 -3
  42. package/engine/content-lint.mjs +1 -1
  43. package/engine/content-render.mjs +453 -0
  44. package/engine/content-scripts.mjs +326 -27
  45. package/engine/content-spans.mjs +4 -1
  46. package/engine/content-styles.mjs +91 -0
  47. package/engine/content-symbols.mjs +186 -0
  48. package/engine/content-verse.mjs +6 -18
  49. package/engine/core-report.mjs +169 -0
  50. package/engine/core-vocabulary.mjs +208 -0
  51. package/engine/dictionary-html.mjs +119 -0
  52. package/engine/field-reference.mjs +1 -1
  53. package/engine/frontmatter-lint.mjs +1 -1
  54. package/engine/heading-attributes.mjs +89 -0
  55. package/engine/helpers.mjs +61 -101
  56. package/engine/homepage.mjs +17 -29
  57. package/engine/html-minify.mjs +70 -0
  58. package/engine/index.mjs +8 -5
  59. package/engine/language-rows.mjs +8 -2
  60. package/engine/language-terms.mjs +115 -14
  61. package/engine/language-words.mjs +12 -0
  62. package/engine/logographic.mjs +255 -0
  63. package/engine/map-lexicon.mjs +221 -25
  64. package/engine/map-places.mjs +5 -4
  65. package/engine/markdown-expressions.mjs +0 -2
  66. package/engine/markdownlint-run.mjs +9 -3
  67. package/engine/markup-validation.mjs +21 -0
  68. package/engine/metre-declaration.mjs +134 -0
  69. package/engine/note-matter.mjs +2 -2
  70. package/engine/note-vocabulary.mjs +187 -3
  71. package/engine/pdf-build.mjs +68 -133
  72. package/engine/pdf-render.mjs +102 -44
  73. package/engine/post-fields.mjs +157 -0
  74. package/engine/preview-index.mjs +130 -0
  75. package/engine/preview-source.mjs +450 -0
  76. package/engine/project-init.mjs +5 -12
  77. package/engine/prose-config.mjs +3 -3
  78. package/engine/run-words.mjs +3 -2
  79. package/engine/script-emphasis-terms.mjs +38 -0
  80. package/engine/script-emphasis.mjs +295 -0
  81. package/engine/script-font-delivery.mjs +4 -4
  82. package/engine/site-build.mjs +265 -313
  83. package/engine/site-config.mjs +26 -238
  84. package/engine/site-files.mjs +175 -0
  85. package/engine/site-index.mjs +9 -4
  86. package/engine/site-preview.mjs +50 -523
  87. package/engine/site-reach.mjs +213 -0
  88. package/engine/site-render.mjs +668 -0
  89. package/engine/site-root.mjs +90 -39
  90. package/engine/site-search.mjs +161 -0
  91. package/engine/site-serve.mjs +274 -0
  92. package/engine/site-theme-banner.mjs +154 -0
  93. package/engine/site-theme-urls.mjs +125 -0
  94. package/engine/site-theme.mjs +939 -0
  95. package/engine/sql-tables.mjs +9 -6
  96. package/engine/text-maps.mjs +57 -10
  97. package/engine/typographer.mjs +248 -0
  98. package/engine/verse-metre.mjs +357 -0
  99. package/engine/wikilink-syntax.mjs +29 -47
  100. package/engine/wikilinks.mjs +758 -279
  101. package/engine/word-forms.mjs +39 -24
  102. package/engine/word-rows.mjs +12 -2
  103. package/engine/word-terms.mjs +29 -4
  104. package/manifest.mjs +3 -4
  105. package/package.json +10 -7
  106. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  107. package/site-theme/layouts/base.hbs +31 -0
  108. package/site-theme/layouts/blog-list.hbs +30 -0
  109. package/site-theme/layouts/blog-post.hbs +40 -0
  110. package/site-theme/layouts/home.hbs +35 -0
  111. package/site-theme/layouts/list.hbs +20 -0
  112. package/site-theme/layouts/not-found.hbs +31 -0
  113. package/site-theme/layouts/page.hbs +7 -0
  114. package/site-theme/partials/copy-buttons.hbs +37 -0
  115. package/site-theme/partials/draft-notice.hbs +5 -0
  116. package/site-theme/partials/exturl.hbs +2 -0
  117. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  118. package/site-theme/partials/header.hbs +44 -0
  119. package/site-theme/partials/hero-banner.hbs +11 -0
  120. package/site-theme/partials/infobox-entry.hbs +2 -0
  121. package/site-theme/partials/infobox-section.hbs +31 -0
  122. package/site-theme/partials/infobox-value.hbs +2 -0
  123. package/site-theme/partials/infobox.hbs +18 -0
  124. package/site-theme/partials/list-members.hbs +9 -0
  125. package/site-theme/partials/nav-links.hbs +5 -0
  126. package/site-theme/partials/page-body.hbs +48 -0
  127. package/site-theme/partials/page-list.hbs +14 -0
  128. package/site-theme/partials/related.hbs +24 -0
  129. package/site-theme/partials/search-meta.hbs +6 -0
  130. package/site-theme/partials/search-names.hbs +2 -0
  131. package/site-theme/partials/search.hbs +52 -0
  132. package/site-theme/partials/tags.hbs +4 -0
  133. package/site-theme/partials/toc.hbs +7 -0
  134. package/{hugo-theme → site-theme}/static/css/style.css +14 -0
  135. package/stage.mjs +1 -2
  136. package/types/config.d.mts +8 -4
  137. package/types/content-config.d.mts +15 -15
  138. package/types/engine/asset-shipping.d.mts +5 -2
  139. package/types/engine/base-compiler.d.mts +20 -2
  140. package/types/engine/content-address.d.mts +1 -1
  141. package/types/engine/content-alerts.d.mts +8 -2
  142. package/types/engine/content-blocks.d.mts +11 -4
  143. package/types/engine/content-expand.d.mts +288 -0
  144. package/types/engine/content-figures.d.mts +11 -3
  145. package/types/engine/content-glyphs.d.mts +85 -0
  146. package/types/engine/content-images.d.mts +6 -7
  147. package/types/engine/content-links.d.mts +1 -1
  148. package/types/engine/content-render.d.mts +199 -0
  149. package/types/engine/content-scripts.d.mts +62 -0
  150. package/types/engine/content-spans.d.mts +1 -1
  151. package/types/engine/content-styles.d.mts +33 -0
  152. package/types/engine/content-symbols.d.mts +35 -0
  153. package/types/engine/core-report.d.mts +45 -0
  154. package/types/engine/core-vocabulary.d.mts +53 -0
  155. package/types/engine/dictionary-html.d.mts +8 -0
  156. package/types/engine/field-reference.d.mts +1 -1
  157. package/types/engine/heading-attributes.d.mts +20 -0
  158. package/types/engine/helpers.d.mts +3 -10
  159. package/types/engine/homepage.d.mts +11 -24
  160. package/types/engine/html-minify.d.mts +9 -0
  161. package/types/engine/index.d.mts +2 -1
  162. package/types/engine/language-terms.d.mts +24 -2
  163. package/types/engine/logographic.d.mts +45 -0
  164. package/types/engine/map-lexicon.d.mts +48 -8
  165. package/types/engine/map-places.d.mts +8 -0
  166. package/types/engine/markdown-expressions.d.mts +0 -2
  167. package/types/engine/metre-declaration.d.mts +39 -0
  168. package/types/engine/pdf-render.d.mts +6 -2
  169. package/types/engine/post-fields.d.mts +44 -0
  170. package/types/engine/preview-index.d.mts +72 -0
  171. package/types/engine/preview-source.d.mts +51 -0
  172. package/types/engine/script-emphasis-terms.d.mts +21 -0
  173. package/types/engine/script-emphasis.d.mts +84 -0
  174. package/types/engine/script-font-delivery.d.mts +4 -4
  175. package/types/engine/site-build.d.mts +71 -113
  176. package/types/engine/site-config.d.mts +14 -134
  177. package/types/engine/site-files.d.mts +73 -0
  178. package/types/engine/site-index.d.mts +11 -3
  179. package/types/engine/site-preview.d.mts +15 -19
  180. package/types/engine/site-reach.d.mts +76 -0
  181. package/types/engine/site-render.d.mts +173 -0
  182. package/types/engine/site-root.d.mts +14 -4
  183. package/types/engine/site-search.d.mts +54 -0
  184. package/types/engine/site-serve.d.mts +44 -0
  185. package/types/engine/site-theme-banner.d.mts +51 -0
  186. package/types/engine/site-theme-urls.d.mts +60 -0
  187. package/types/engine/site-theme.d.mts +127 -0
  188. package/types/engine/text-maps.d.mts +17 -3
  189. package/types/engine/typographer.d.mts +11 -0
  190. package/types/engine/verse-metre.d.mts +131 -0
  191. package/types/engine/wikilink-syntax.d.mts +30 -48
  192. package/types/engine/wikilinks.d.mts +330 -14
  193. package/types/engine/word-forms.d.mts +3 -2
  194. package/types/manifest.d.mts +3 -4
  195. package/types/stage.d.mts +1 -2
  196. package/types/vendor/markdownlint.d.mts +8 -0
  197. package/vendor/markdownlint.mjs +20388 -0
  198. package/engine/web-wikilinks.mjs +0 -594
  199. package/hugo-theme/layouts/404.html +0 -85
  200. package/hugo-theme/layouts/_default/baseof.html +0 -67
  201. package/hugo-theme/layouts/_default/list.html +0 -139
  202. package/hugo-theme/layouts/_default/single.html +0 -4
  203. package/hugo-theme/layouts/blog/list.html +0 -54
  204. package/hugo-theme/layouts/blog-post/single.html +0 -36
  205. package/hugo-theme/layouts/index.html +0 -75
  206. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  207. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  208. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  209. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  210. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  211. package/hugo-theme/layouts/partials/header.html +0 -35
  212. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  213. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  214. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  215. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  216. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  217. package/hugo-theme/layouts/partials/infobox.html +0 -57
  218. package/hugo-theme/layouts/partials/page-body.html +0 -152
  219. package/hugo-theme/layouts/partials/related.html +0 -138
  220. package/hugo-theme/layouts/partials/search.html +0 -84
  221. package/hugo-theme/layouts/partials/site-url.html +0 -32
  222. package/hugo-theme/layouts/partials/tags.html +0 -30
  223. package/hugo-theme/layouts/partials/toc.html +0 -28
  224. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  225. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  226. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  227. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  228. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  229. package/hugo-theme/theme.toml +0 -11
  230. package/types/engine/web-wikilinks.d.mts +0 -107
  231. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  232. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  233. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  234. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  235. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  236. /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";
16
+ import { sharedContentRules } from "./content-styles.mjs";
21
17
  import { readMatter } from "./note-matter.mjs";
22
- import { ALERT_TYPES, alertMarkdownPlugin, scanAlerts } from "./content-alerts.mjs";
23
- import { spanMarkdownPlugin } from "./content-spans.mjs";
24
- import { scanVerse, verseStanzas } from "./content-verse.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 verse = scanVerse(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: verse.find((poem) => poem.start === token.map[0]),
370
- };
371
- }
372
- // Verse 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_verse_open", "div", 1, [["class", "verse"]]).map = token.map;
386
- for (const stanza of verseStanzas(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_verse_line_open", "span", 1, [
391
- ["class", `verse-line i${level}`],
392
- ]);
393
- const line = push("inline", "", 0);
394
- line.content = text;
395
- line.children = [];
396
- push("hl_preview_verse_line_close", "span", -1);
397
- });
398
- push("paragraph_close", "p", -1);
399
- }
400
- push("hl_preview_verse_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,14 +64,23 @@ 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 = [];
@@ -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 .verse-line { white-space:pre-wrap; }
669
- ${Array.from({ length: 9 }, (_, n) => `.hl-preview .verse-line.i${n} { padding-left:${n * 2}em; }`).join("\n")}
196
+ ${sharedContentRules(".hl-preview")}
670
197
  `;
671
198
  }