@wdprlib/render 2.1.0 → 3.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 (173) hide show
  1. package/dist/index.cjs +2344 -1668
  2. package/dist/index.d.cts +15 -13
  3. package/dist/index.d.ts +15 -13
  4. package/dist/index.js +2375 -1699
  5. package/package.json +1 -1
  6. package/src/context/attributes.ts +14 -0
  7. package/src/context/bibliography.ts +109 -0
  8. package/src/context/counters.ts +51 -0
  9. package/src/context/image-urls.ts +31 -0
  10. package/src/context/index.ts +285 -0
  11. package/src/context/output.ts +17 -0
  12. package/src/context/page-urls.ts +81 -0
  13. package/src/context/style-slots.ts +29 -0
  14. package/src/context/urls.ts +2 -0
  15. package/src/elements/bibliography/block.ts +27 -0
  16. package/src/elements/bibliography/cite.ts +23 -0
  17. package/src/elements/bibliography/ids.ts +9 -0
  18. package/src/elements/bibliography/index.ts +9 -0
  19. package/src/elements/code/contents.ts +18 -0
  20. package/src/elements/code/index.ts +29 -0
  21. package/src/elements/collapsible/index.ts +31 -0
  22. package/src/elements/collapsible/labels.ts +35 -0
  23. package/src/elements/collapsible/link.ts +11 -0
  24. package/src/elements/collapsible/sections.ts +39 -0
  25. package/src/elements/container/attributes.ts +28 -0
  26. package/src/elements/container/header.ts +27 -0
  27. package/src/elements/container/index.ts +35 -0
  28. package/src/elements/container/string-container.ts +40 -0
  29. package/src/elements/container/string-types.ts +63 -0
  30. package/src/elements/container/wrappers.ts +32 -0
  31. package/src/elements/date/format.ts +20 -0
  32. package/src/elements/{date.ts → date/index.ts} +4 -29
  33. package/src/elements/date/output.ts +6 -0
  34. package/src/elements/embed/iframe.ts +8 -0
  35. package/src/elements/embed/index.ts +28 -0
  36. package/src/elements/embed/providers.ts +43 -0
  37. package/src/elements/embed/validation.ts +15 -0
  38. package/src/elements/embed-block/allowlist.ts +60 -0
  39. package/src/elements/embed-block/boolean-attributes.ts +38 -0
  40. package/src/elements/embed-block/iframe.ts +33 -0
  41. package/src/elements/embed-block/index.ts +31 -0
  42. package/src/elements/embed-block/sanitize-config.ts +22 -0
  43. package/src/elements/embed-block/sanitize.ts +44 -0
  44. package/src/elements/expr/branch.ts +29 -0
  45. package/src/elements/expr/index.ts +63 -0
  46. package/src/elements/expr/result.ts +19 -0
  47. package/src/elements/footnote/body.ts +11 -0
  48. package/src/elements/footnote/index.ts +35 -0
  49. package/src/elements/footnote/ref.ts +16 -0
  50. package/src/elements/html/attributes.ts +24 -0
  51. package/src/elements/html/index.ts +39 -0
  52. package/src/elements/html/url.ts +19 -0
  53. package/src/elements/iframe/attributes.ts +28 -0
  54. package/src/elements/iframe/index.ts +22 -0
  55. package/src/elements/iftags/condition.ts +42 -0
  56. package/src/elements/iftags/index.ts +39 -0
  57. package/src/elements/iftags/style-slot.ts +23 -0
  58. package/src/elements/iftags/tokens.ts +36 -0
  59. package/src/elements/image/alignment.ts +44 -0
  60. package/src/elements/image/attributes.ts +10 -0
  61. package/src/elements/image/img-attributes.ts +26 -0
  62. package/src/elements/image/index.ts +36 -0
  63. package/src/elements/image/link-href.ts +24 -0
  64. package/src/elements/image/link.ts +13 -0
  65. package/src/elements/image/source.ts +16 -0
  66. package/src/elements/{include.ts → include/index.ts} +5 -13
  67. package/src/elements/include/missing.ts +15 -0
  68. package/src/elements/link/anchor-name.ts +6 -0
  69. package/src/elements/link/anchor.ts +27 -0
  70. package/src/elements/link/attributes.ts +47 -0
  71. package/src/elements/link/index.ts +26 -0
  72. package/src/elements/link/label.ts +23 -0
  73. package/src/elements/link/target.ts +20 -0
  74. package/src/elements/list/attributes.ts +19 -0
  75. package/src/elements/list/definition-list.ts +16 -0
  76. package/src/elements/list/index.ts +48 -0
  77. package/src/elements/list/item-rendering.ts +38 -0
  78. package/src/elements/list/items.ts +61 -0
  79. package/src/elements/list/no-marker.ts +53 -0
  80. package/src/elements/list/paragraphs.ts +34 -0
  81. package/src/elements/list/trim.ts +38 -0
  82. package/src/elements/math/block.ts +29 -0
  83. package/src/elements/math/equation-ref.ts +12 -0
  84. package/src/elements/math/index.ts +14 -0
  85. package/src/elements/math/inline.ts +19 -0
  86. package/src/elements/math/latex.ts +27 -0
  87. package/src/elements/math/source.ts +18 -0
  88. package/src/elements/module/backlinks.ts +2 -1
  89. package/src/elements/module/categories.ts +2 -2
  90. package/src/elements/module/empty-container.ts +10 -0
  91. package/src/elements/module/index.ts +2 -4
  92. package/src/elements/module/join-markup.ts +10 -0
  93. package/src/elements/module/join.ts +2 -7
  94. package/src/elements/module/listpages.ts +2 -2
  95. package/src/elements/module/listusers.ts +2 -2
  96. package/src/elements/module/page-tree.ts +2 -2
  97. package/src/elements/module/rate-markup.ts +10 -0
  98. package/src/elements/module/rate.ts +4 -13
  99. package/src/elements/module/unknown.ts +11 -0
  100. package/src/elements/tab-view/ids.ts +16 -0
  101. package/src/elements/tab-view/index.ts +31 -0
  102. package/src/elements/tab-view/navigation.ts +15 -0
  103. package/src/elements/tab-view/panels.ts +16 -0
  104. package/src/elements/table/attributes.ts +23 -0
  105. package/src/elements/table/cell-attributes.ts +62 -0
  106. package/src/elements/table/cell.ts +13 -0
  107. package/src/elements/table/index.ts +27 -0
  108. package/src/elements/text/email.ts +20 -0
  109. package/src/elements/text/index.ts +11 -0
  110. package/src/elements/text/plain.ts +11 -0
  111. package/src/elements/text/raw.ts +20 -0
  112. package/src/elements/toc/body.ts +12 -0
  113. package/src/elements/toc/entries.ts +34 -0
  114. package/src/elements/toc/frame.ts +27 -0
  115. package/src/elements/toc/index.ts +17 -0
  116. package/src/elements/toc/link.ts +26 -0
  117. package/src/elements/user/index.ts +40 -0
  118. package/src/elements/user/markup.ts +34 -0
  119. package/src/elements/user/resolve.ts +6 -0
  120. package/src/escape/attribute-allowlists.ts +101 -0
  121. package/src/escape/attributes.ts +62 -0
  122. package/src/escape/css-color-functions.ts +18 -0
  123. package/src/escape/css-colors.ts +183 -0
  124. package/src/escape/css-danger.ts +22 -0
  125. package/src/escape/css-normalize.ts +54 -0
  126. package/src/escape/css-style.ts +78 -0
  127. package/src/escape/css-urls.ts +76 -0
  128. package/src/escape/css.ts +4 -0
  129. package/src/escape/email.ts +22 -0
  130. package/src/escape/html.ts +68 -0
  131. package/src/escape/index.ts +15 -0
  132. package/src/escape/url.ts +18 -0
  133. package/src/libs/highlighter/engine/end-pattern.ts +26 -0
  134. package/src/libs/highlighter/engine/html.ts +19 -0
  135. package/src/libs/highlighter/engine/index.ts +3 -0
  136. package/src/libs/highlighter/engine/keywords.ts +22 -0
  137. package/src/libs/highlighter/engine/parts.ts +36 -0
  138. package/src/libs/highlighter/engine/preprocess.ts +10 -0
  139. package/src/libs/highlighter/engine/render.ts +31 -0
  140. package/src/libs/highlighter/engine/token.ts +7 -0
  141. package/src/libs/highlighter/engine/tokenizer.ts +266 -0
  142. package/src/libs/highlighter/engine/utils.ts +38 -0
  143. package/src/render/collected-styles.ts +22 -0
  144. package/src/render/dispatch.ts +181 -0
  145. package/src/render/index.ts +28 -0
  146. package/src/render/primitives.ts +17 -0
  147. package/src/render/style-tag.ts +6 -0
  148. package/src/render/style.ts +15 -0
  149. package/src/types.ts +6 -2
  150. package/src/context.ts +0 -422
  151. package/src/elements/bibliography.ts +0 -123
  152. package/src/elements/code.ts +0 -49
  153. package/src/elements/collapsible.ts +0 -105
  154. package/src/elements/container.ts +0 -302
  155. package/src/elements/embed-block.ts +0 -327
  156. package/src/elements/embed.ts +0 -166
  157. package/src/elements/expr.ts +0 -102
  158. package/src/elements/footnote.ts +0 -76
  159. package/src/elements/html.ts +0 -79
  160. package/src/elements/iframe.ts +0 -44
  161. package/src/elements/iftags.ts +0 -118
  162. package/src/elements/image.ts +0 -154
  163. package/src/elements/link.ts +0 -201
  164. package/src/elements/list.ts +0 -241
  165. package/src/elements/math.ts +0 -177
  166. package/src/elements/tab-view.ts +0 -75
  167. package/src/elements/table.ts +0 -101
  168. package/src/elements/text.ts +0 -57
  169. package/src/elements/toc.ts +0 -147
  170. package/src/elements/user.ts +0 -79
  171. package/src/escape.ts +0 -829
  172. package/src/libs/highlighter/engine.ts +0 -352
  173. package/src/render.ts +0 -231
@@ -1,352 +0,0 @@
1
- /**
2
- *
3
- * Tokenizer and renderer for the Text_Highlighter-compatible syntax
4
- * highlighting engine. This is a faithful TypeScript port of the
5
- * PEAR Text_Highlighter 0.5.1 PHP library's `_getToken` algorithm and
6
- * HTML renderer.
7
- *
8
- * The engine processes source code through a state-machine-based tokenizer
9
- * that assigns CSS class names to each token, then renders the tokens as
10
- * `<span class="hl-*">` elements.
11
- *
12
- * @module
13
- */
14
-
15
- import type { LanguageDefinition } from "./types";
16
-
17
- /** A single highlighted token with its CSS class and text content. */
18
- interface Token {
19
- /** CSS class name suffix (used as `hl-{class}`). */
20
- class: string;
21
- /** The literal text content of this token. */
22
- content: string;
23
- }
24
-
25
- /**
26
- * Tokenize source code using a language definition's state machine.
27
- *
28
- * This is a faithful port of PEAR Text_Highlighter's `_getToken` algorithm.
29
- * The key difference from PHP is that JavaScript lacks `PREG_OFFSET_CAPTURE`,
30
- * so capture group positions are computed from the match result.
31
- *
32
- * The input is preprocessed to normalize line endings, replace tabs with
33
- * spaces, and ensure empty lines have at least one space character
34
- * (matching PHP's behavior).
35
- *
36
- * @param def - The language definition describing the state machine.
37
- * @param input - Raw source code string to tokenize.
38
- * @returns Array of tokens, each with a CSS class and content string.
39
- */
40
- export function tokenize(def: LanguageDefinition, input: string): Token[] {
41
- // Preprocess: same as PHP Html renderer's preprocess()
42
- let str = input.replace(/\r\n/g, "\n");
43
- // Replace empty lines with a space (PHP: preg_replace('~^$~m', " ", $str))
44
- str = str.replace(/^$/gm, " ");
45
- str = str.replace(/\t/g, " ");
46
- // rtrim
47
- str = str.replace(/\s+$/, "");
48
-
49
- const len = str.length;
50
- if (len === 0) return [];
51
-
52
- let state = -1;
53
- let pos = 0;
54
- let lastinner = def.defClass;
55
- let lastdelim = def.defClass;
56
- let endpattern: RegExp | null = null;
57
- const stateStack: {
58
- state: number;
59
- lastdelim: string;
60
- lastinner: string;
61
- endpattern: RegExp | null;
62
- }[] = [];
63
- const tokenStack: Token[] = [];
64
- const result: Token[] = [];
65
-
66
- function getToken(): Token | null {
67
- if (tokenStack.length > 0) {
68
- return tokenStack.pop()!;
69
- }
70
- if (pos >= len) {
71
- return null;
72
- }
73
-
74
- // Check for end of current state
75
- let endpos = -1;
76
- let endmatch = "";
77
- if (state !== -1 && endpattern) {
78
- endpattern.lastIndex = pos;
79
- const em = endpattern.exec(str);
80
- if (em) {
81
- endpos = em.index;
82
- endmatch = em[0];
83
- }
84
- }
85
-
86
- // Try to match patterns for current state
87
- const reg = def.regs[state];
88
- if (reg) {
89
- reg.lastIndex = pos;
90
- const m = reg.exec(str);
91
-
92
- if (m) {
93
- // Find which pattern (alternative) matched by checking capture groups
94
- const countsArr = def.counts[state]!;
95
- const statesArr = def.states[state]!;
96
- const delimArr = def.delim[state]!;
97
- const innerArr = def.inner[state]!;
98
- let n = 1;
99
- for (let i = 0; i < countsArr.length; i++) {
100
- const count = countsArr[i]!;
101
- if (n >= m.length) break;
102
-
103
- // PHP: $m[$n][1] > -1 means the group captured something at a valid position
104
- // JS: m[n] != null means the group participated in the match (including empty string captures)
105
- if (m[n] != null && (endpos === -1 || m.index < endpos)) {
106
- const matchStart = m.index;
107
- const matchStr = m[n]!;
108
-
109
- // Find actual position of this specific group within the match
110
- // For alternation patterns, the matched group starts at m.index
111
- // because only one alternative matches at a time
112
- const groupStart = findGroupPosition(str, m, n, matchStart);
113
-
114
- if (statesArr[i] !== -1) {
115
- // State transition - push delimiter token
116
- tokenStack.push({ class: delimArr[i]!, content: matchStr });
117
- } else {
118
- // Non-transitioning match
119
- let inner = innerArr[i]!;
120
-
121
- // Check parts first
122
- const partDef = def.parts[state]?.[i];
123
- if (partDef) {
124
- const parts: Token[] = [];
125
- let partpos = groupStart;
126
- for (let j = 1; j <= count; j++) {
127
- const subIdx = j + n;
128
- if (subIdx >= m.length || m[subIdx] == null || m[subIdx] === "") continue;
129
- const subStr = m[subIdx]!;
130
- const subStart = str.indexOf(subStr, partpos);
131
- if (subStart < 0) continue;
132
- if (partDef[j]) {
133
- if (subStart > partpos) {
134
- parts.unshift({ class: inner, content: str.substring(partpos, subStart) });
135
- }
136
- parts.unshift({ class: partDef[j]!, content: subStr });
137
- }
138
- partpos = subStart + subStr.length;
139
- }
140
- if (partpos < groupStart + matchStr.length) {
141
- parts.unshift({
142
- class: inner,
143
- content: str.substring(partpos, groupStart + matchStr.length),
144
- });
145
- }
146
- tokenStack.push(...parts);
147
- } else {
148
- // Check keywords (fallback to state -1 if current state has no keyword def)
149
- let kwDef = def.keywords[state]?.[i];
150
- if (
151
- !kwDef ||
152
- kwDef === -1 ||
153
- typeof kwDef !== "object" ||
154
- Object.keys(kwDef).length === 0
155
- ) {
156
- kwDef = def.keywords[-1]?.[i];
157
- }
158
- if (kwDef && kwDef !== -1 && typeof kwDef === "object") {
159
- for (const [group, re] of Object.entries(kwDef)) {
160
- if ((re as RegExp).test(matchStr)) {
161
- inner = def.kwmap[group] ?? inner;
162
- break;
163
- }
164
- }
165
- }
166
- tokenStack.push({ class: inner, content: matchStr });
167
- }
168
- }
169
-
170
- // Emit text before match (pushed after so it pops first)
171
- if (groupStart > pos) {
172
- tokenStack.push({ class: lastinner, content: str.substring(pos, groupStart) });
173
- }
174
-
175
- pos = groupStart + matchStr.length;
176
-
177
- // Handle state transition
178
- if (statesArr[i] !== -1) {
179
- stateStack.push({ state, lastdelim, lastinner, endpattern });
180
- lastinner = innerArr[i]!;
181
- lastdelim = delimArr[i]!;
182
- const prevState = state;
183
- state = statesArr[i]!;
184
-
185
- // Get end pattern for new state
186
- const endRe = def.end[state];
187
-
188
- // Handle substitution in end pattern (requires new RegExp)
189
- if (def.subst[prevState]?.[i] && endRe) {
190
- let epSource = endRe.source;
191
- for (let k = 0; k <= count; k++) {
192
- const subIdx = n + k;
193
- if (subIdx >= m.length || m[subIdx] == null) break;
194
- const quoted = escapeRegex(m[subIdx]!);
195
- epSource = epSource.replace(`%${k}%`, quoted);
196
- epSource = epSource.replace(`%b${k}%`, matchingBrackets(quoted));
197
- }
198
- endpattern = new RegExp(epSource, endRe.flags);
199
- } else {
200
- // Reuse existing RegExp object (no substitution needed)
201
- endpattern = endRe ?? null;
202
- }
203
- }
204
-
205
- return tokenStack.pop()!;
206
- }
207
- n += count + 1;
208
- }
209
- }
210
- }
211
-
212
- // Handle end of state
213
- if (endpos > -1) {
214
- // Always push delimiter token (even for zero-width matches) to match PHP behavior
215
- tokenStack.push({ class: lastdelim, content: endmatch });
216
- if (endpos > pos) {
217
- tokenStack.push({ class: lastinner, content: str.substring(pos, endpos) });
218
- }
219
- const prev = stateStack.pop()!;
220
- state = prev.state;
221
- lastdelim = prev.lastdelim;
222
- lastinner = prev.lastinner;
223
- endpattern = prev.endpattern;
224
- pos = endpos + endmatch.length;
225
- if (tokenStack.length > 0) {
226
- return tokenStack.pop()!;
227
- }
228
- // Zero-width end pattern with no preceding content: continue to next token
229
- return getToken();
230
- }
231
-
232
- // No match - consume rest as default class
233
- const p = pos;
234
- pos = len;
235
- return { class: lastinner, content: str.substring(p) };
236
- }
237
-
238
- let token: Token | null;
239
- while ((token = getToken()) !== null) {
240
- result.push(token);
241
- }
242
-
243
- return result;
244
- }
245
-
246
- /**
247
- * Find the actual position of capture group `n` within the source string.
248
- *
249
- * For alternation patterns (`a|b|c`), the matched alternative starts at
250
- * the overall match position (`m.index`). This function locates the
251
- * capture group's substring within the source, searching from `matchStart`.
252
- *
253
- * @param str - The full source string.
254
- * @param m - The regex match result.
255
- * @param n - The capture group index.
256
- * @param matchStart - The starting position of the overall match.
257
- * @returns The position of the capture group within the source string.
258
- */
259
- function findGroupPosition(str: string, m: RegExpExecArray, n: number, matchStart: number): number {
260
- // The overall match m[0] starts at m.index
261
- // The capture group m[n] is a substring of m[0]
262
- // Find where m[n] starts within the string, searching from matchStart
263
- const groupStr = m[n]!;
264
- const idx = str.indexOf(groupStr, matchStart);
265
- return idx >= 0 ? idx : matchStart;
266
- }
267
-
268
- /**
269
- * Render an array of tokens to HTML with `hl-*` class spans.
270
- *
271
- * This is a faithful port of Text_Highlighter's HTML renderer:
272
- * - Adjacent tokens with the same class are merged into a single `<span>`.
273
- * - All text is wrapped in spans (no unwrapped text nodes).
274
- * - The output is wrapped in `<div class="hl-main"><pre>...</pre></div>`.
275
- *
276
- * @param tokens - Array of tokens produced by {@link tokenize}.
277
- * @returns Complete HTML string for the highlighted code block.
278
- */
279
- export function renderTokens(tokens: Token[]): string {
280
- if (tokens.length === 0) return "";
281
-
282
- let html = "";
283
- let lastClass = "";
284
-
285
- for (const token of tokens) {
286
- if (token.content.length === 0) continue;
287
- const escaped = escapeHtml(token.content);
288
- if (token.class !== lastClass) {
289
- if (lastClass) {
290
- html += "</span>";
291
- }
292
- html += `<span class="hl-${token.class}">`;
293
- lastClass = token.class;
294
- }
295
- html += escaped;
296
- }
297
-
298
- if (lastClass) {
299
- html += "</span>";
300
- }
301
-
302
- return `<div class="hl-main"><pre>${html}</pre></div>`;
303
- }
304
-
305
- /**
306
- * Escape HTML special characters for use inside highlighted code spans.
307
- *
308
- * @param str - Raw text to escape.
309
- * @returns HTML-safe string.
310
- */
311
- function escapeHtml(str: string): string {
312
- return str
313
- .replace(/&/g, "&amp;")
314
- .replace(/</g, "&lt;")
315
- .replace(/>/g, "&gt;")
316
- .replace(/"/g, "&quot;");
317
- }
318
-
319
- /**
320
- * Escape regex special characters in a string for safe use in `new RegExp()`.
321
- *
322
- * @param str - Raw string to escape.
323
- * @returns Regex-safe string.
324
- */
325
- function escapeRegex(str: string): string {
326
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
327
- }
328
-
329
- /**
330
- * Swap bracket characters to their matching counterparts.
331
- *
332
- * Used for end-pattern substitution where the closing delimiter is the
333
- * mirror of the opening delimiter (e.g., `<` becomes `>`).
334
- *
335
- * @param str - String containing bracket characters.
336
- * @returns String with each bracket replaced by its counterpart.
337
- */
338
- function matchingBrackets(str: string): string {
339
- return str.replace(/[()<>[\]{}]/g, (c) => {
340
- const map: Record<string, string> = {
341
- "(": ")",
342
- ")": "(",
343
- "<": ">",
344
- ">": "<",
345
- "[": "]",
346
- "]": "[",
347
- "{": "}",
348
- "}": "{",
349
- };
350
- return map[c] ?? c;
351
- });
352
- }
package/src/render.ts DELETED
@@ -1,231 +0,0 @@
1
- import type { Element, SyntaxTree } from "@wdprlib/ast";
2
- import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
3
- import { RenderContext } from "./context";
4
- import { escapeStyleContent } from "./escape";
5
- import type { RenderOptions } from "./types";
6
- import { renderContainer } from "./elements/container";
7
- import { renderText, renderRaw, renderEmail } from "./elements/text";
8
- import { renderLink, renderAnchor, renderAnchorName } from "./elements/link";
9
- import { renderImage } from "./elements/image";
10
- import { renderList, renderDefinitionList } from "./elements/list";
11
- import { renderTable } from "./elements/table";
12
- import { renderCollapsible } from "./elements/collapsible";
13
- import { renderCode } from "./elements/code";
14
- import { renderTabView } from "./elements/tab-view";
15
- import { renderFootnoteRef, renderFootnoteBlock } from "./elements/footnote";
16
- import { renderMath, renderMathInline, renderEquationRef } from "./elements/math";
17
- import { renderModule } from "./elements/module/index";
18
- import { renderEmbed } from "./elements/embed";
19
- import { renderEmbedBlock } from "./elements/embed-block";
20
- import { renderUser } from "./elements/user";
21
- import { renderBibliographyCite, renderBibliographyBlock } from "./elements/bibliography";
22
- import { renderTableOfContents } from "./elements/toc";
23
- import { renderLineBreaks } from "./elements/line-break";
24
- import { renderClearFloat } from "./elements/clear-float";
25
- import { renderIframe } from "./elements/iframe";
26
- import { renderHtmlBlock } from "./elements/html";
27
- import { renderInclude } from "./elements/include";
28
- import { renderIfTags } from "./elements/iftags";
29
- import { renderColor } from "./elements/color";
30
- import { renderDate } from "./elements/date";
31
- import { renderExpr, renderIf, renderIfExpr } from "./elements/expr";
32
-
33
- /**
34
- * Render a {@link SyntaxTree} to an HTML string.
35
- *
36
- * This is the main entry point of `@wdprlib/render`. It walks the AST
37
- * produced by `@wdprlib/parser`, serialises each element to HTML, and
38
- * appends any collected `[[module CSS]]` styles at the end (when
39
- * `WikitextSettings.allowStyleElements` is `true`).
40
- *
41
- * @param tree - Parsed AST (from `parse()` or `resolveModules()`)
42
- * @param options - Rendering configuration
43
- * @returns Complete HTML string
44
- *
45
- * @group Render
46
- */
47
- export function renderToHtml(tree: SyntaxTree, options: RenderOptions = {}): string {
48
- const ctx = new RenderContext(tree, options);
49
- renderElements(ctx, tree.elements);
50
-
51
- // Append styles (with tag breakout prevention).
52
- // Sentinel entries (STYLE_SLOT_PREFIX) mark positions where unresolved
53
- // iftags styles should be spliced in, preserving source order.
54
- if (ctx.settings.allowStyleElements && tree.styles?.length) {
55
- for (const style of tree.styles) {
56
- if (style.startsWith(STYLE_SLOT_PREFIX)) {
57
- const slotId = parseInt(style.slice(STYLE_SLOT_PREFIX.length), 10);
58
- for (const css of ctx.getStyleSlotContents(slotId)) {
59
- ctx.push(`<style>${escapeStyleContent(css)}</style>`);
60
- }
61
- } else {
62
- ctx.push(`<style>${escapeStyleContent(style)}</style>`);
63
- }
64
- }
65
- }
66
-
67
- return ctx.getOutput();
68
- }
69
-
70
- /**
71
- * Render a list of sibling AST elements in document order.
72
- *
73
- * Used internally by container renderers that need to emit their
74
- * children. Not exported from the package barrel — call
75
- * {@link renderToHtml} instead for top-level rendering.
76
- */
77
- export function renderElements(ctx: RenderContext, elements: Element[]): void {
78
- for (const element of elements) {
79
- renderElement(ctx, element);
80
- }
81
- }
82
-
83
- /**
84
- * Dispatch a single AST element to its type-specific renderer.
85
- *
86
- * The switch covers every `ElementName` value defined by
87
- * `@wdprlib/ast`. Unknown element types are silently ignored.
88
- */
89
- export function renderElement(ctx: RenderContext, element: Element): void {
90
- switch (element.element) {
91
- case "text":
92
- ctx.pushEscaped(element.data);
93
- break;
94
- case "raw":
95
- renderRaw(ctx, element.data);
96
- break;
97
- case "variable":
98
- renderText(ctx, element.data);
99
- break;
100
- case "email":
101
- renderEmail(ctx, element.data);
102
- break;
103
- case "container":
104
- renderContainer(ctx, element.data);
105
- break;
106
- case "link":
107
- renderLink(ctx, element.data);
108
- break;
109
- case "anchor":
110
- renderAnchor(ctx, element.data);
111
- break;
112
- case "anchor-name":
113
- renderAnchorName(ctx, element.data);
114
- break;
115
- case "image":
116
- renderImage(ctx, element.data);
117
- break;
118
- case "list":
119
- renderList(ctx, element.data);
120
- break;
121
- case "definition-list":
122
- renderDefinitionList(ctx, element.data);
123
- break;
124
- case "table":
125
- renderTable(ctx, element.data);
126
- break;
127
- case "collapsible":
128
- renderCollapsible(ctx, element.data);
129
- break;
130
- case "code":
131
- renderCode(ctx, element.data);
132
- break;
133
- case "tab-view":
134
- renderTabView(ctx, element.data);
135
- break;
136
- case "footnote":
137
- renderFootnoteRef(ctx, ctx.nextFootnoteIndex() + 1);
138
- break;
139
- case "footnote-ref":
140
- renderFootnoteRef(ctx, element.data);
141
- break;
142
- case "footnote-block":
143
- renderFootnoteBlock(ctx, element.data);
144
- break;
145
- case "bibliography-cite":
146
- renderBibliographyCite(ctx, element.data);
147
- break;
148
- case "bibliography-block":
149
- renderBibliographyBlock(ctx, element.data, renderElements);
150
- break;
151
- case "table-of-contents":
152
- renderTableOfContents(ctx, element.data);
153
- break;
154
- case "math":
155
- renderMath(ctx, element.data);
156
- break;
157
- case "math-inline":
158
- renderMathInline(ctx, element.data);
159
- break;
160
- case "module":
161
- renderModule(ctx, element.data);
162
- break;
163
- case "embed":
164
- renderEmbed(ctx, element.data);
165
- break;
166
- case "embed-block":
167
- renderEmbedBlock(ctx, element.data);
168
- break;
169
- case "user":
170
- renderUser(ctx, element.data);
171
- break;
172
- case "date":
173
- renderDate(ctx, element.data);
174
- break;
175
- case "color":
176
- renderColor(ctx, element.data);
177
- break;
178
- case "html":
179
- renderHtmlBlock(ctx, element.data);
180
- break;
181
- case "iframe":
182
- renderIframe(ctx, element.data);
183
- break;
184
- case "include":
185
- renderInclude(ctx, element.data);
186
- break;
187
- case "if-tags":
188
- renderIfTags(ctx, element.data);
189
- break;
190
- case "style":
191
- // Styles are collected into tree.styles during resolve and rendered
192
- // at the end of renderToHtml. Style elements remaining in the AST
193
- // (inside unresolved iftags) are either collected into a style slot
194
- // (preserving source order) or rendered inline as a fallback.
195
- if (ctx.renderInlineStyles && ctx.settings.allowStyleElements) {
196
- if (ctx.hasActiveStyleSlot()) {
197
- ctx.pushToStyleSlot(element.data);
198
- } else {
199
- ctx.push(`<style>${escapeStyleContent(element.data)}</style>`);
200
- }
201
- }
202
- break;
203
- case "line-break":
204
- ctx.push("<br />");
205
- break;
206
- case "line-breaks":
207
- renderLineBreaks(ctx, element.data);
208
- break;
209
- case "clear-float":
210
- renderClearFloat(ctx, element.data);
211
- break;
212
- case "horizontal-rule":
213
- ctx.push("<hr />");
214
- break;
215
- case "content-separator":
216
- ctx.push(`<div class="content-separator" style="display: none:"></div>`);
217
- break;
218
- case "expr":
219
- renderExpr(ctx, element.data);
220
- break;
221
- case "if":
222
- renderIf(ctx, element.data);
223
- break;
224
- case "ifexpr":
225
- renderIfExpr(ctx, element.data);
226
- break;
227
- case "equation-reference":
228
- renderEquationRef(ctx, element.data);
229
- break;
230
- }
231
- }