@wdprlib/render 2.1.0 → 3.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 (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,177 +0,0 @@
1
- /**
2
- *
3
- * Renderers for Wikidot mathematical notation elements.
4
- *
5
- * - `[[math]]...[[/math]]` -- display-mode (block) math
6
- * - `[[$ ... $]]` -- inline math
7
- * - `[[eref name]]` -- equation reference (link to named equation)
8
- *
9
- * LaTeX source is converted to MathML using the `temml` library at
10
- * render time. A hidden `<code class="math-source">` element preserves
11
- * the original LaTeX for use by the runtime `math` module's SVG polyfill
12
- * (for browsers without MathML support).
13
- *
14
- * Named equations receive an `(N)` equation number and can be
15
- * cross-referenced via `[[eref]]`.
16
- *
17
- * @module
18
- */
19
-
20
- import type { MathData, MathInlineData } from "@wdprlib/ast";
21
- import type { RenderContext } from "../context";
22
- import { escapeAttr, escapeHtml } from "../escape";
23
- import temml from "temml";
24
-
25
- /**
26
- * Determine whether a LaTeX string needs to be wrapped in an `aligned`
27
- * environment.
28
- *
29
- * Wikidot-style multi-line equations use `&` as alignment markers without
30
- * explicitly declaring an `aligned` environment. If the LaTeX contains
31
- * unescaped `&` characters but no `\begin{...}` environment declaration,
32
- * an `aligned` wrapper is added to make the alignment work correctly.
33
- *
34
- * @param latex - Raw LaTeX source string.
35
- * @returns `true` if the LaTeX needs an `aligned` environment wrapper.
36
- */
37
- function needsAlignedWrapper(latex: string): boolean {
38
- // Already has an environment
39
- if (/\\begin\s*\{/.test(latex)) {
40
- return false;
41
- }
42
- // Has alignment marker (&) but not escaped (\&) - needs aligned environment
43
- // Remove escaped ampersands first, then check for unescaped ones
44
- const withoutEscaped = latex.replace(/\\&/g, "");
45
- return withoutEscaped.includes("&");
46
- }
47
-
48
- /**
49
- * Render a LaTeX string to MathML using the `temml` library.
50
- *
51
- * For display-mode equations with alignment markers, the LaTeX is
52
- * automatically wrapped in an `aligned` environment.
53
- *
54
- * @param latex - LaTeX source string.
55
- * @param displayMode - Whether to render in display mode (block) or inline.
56
- * @returns MathML string, or `""` if rendering fails.
57
- */
58
- function renderLatexToMathML(latex: string, displayMode: boolean): string {
59
- try {
60
- // Wrap in aligned environment if needed for Wikidot-style alignment
61
- let processedLatex = latex;
62
- if (displayMode && needsAlignedWrapper(latex)) {
63
- processedLatex = `\\begin{aligned}\n${latex}\n\\end{aligned}`;
64
- }
65
- return temml.renderToString(processedLatex, {
66
- displayMode,
67
- throwOnError: false,
68
- annotate: false,
69
- });
70
- } catch {
71
- return "";
72
- }
73
- }
74
-
75
- /**
76
- * Render a `[[math]]` display-mode block equation.
77
- *
78
- * Produces a `<div class="math-block">` containing:
79
- * - An optional equation number `<span class="equation-number">` for named equations
80
- * - A hidden `<code class="math-source">` with the raw LaTeX (for polyfill use)
81
- * - A `<span class="math-render">` with the MathML output (or error fallback)
82
- *
83
- * @param ctx - The current render context.
84
- * @param data - Math block data with LaTeX source and optional equation name.
85
- */
86
- export function renderMath(ctx: RenderContext, data: MathData): void {
87
- const index = ctx.nextEquationIndex() + 1;
88
- const latex = data["latex-source"];
89
- const mathml = renderLatexToMathML(latex, true);
90
-
91
- const id = data.name
92
- ? ctx.generateId("equation-", data.name)
93
- : ctx.generateId("equation-", index);
94
- const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
95
-
96
- ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
97
-
98
- // Equation number (only for named equations)
99
- if (data.name) {
100
- ctx.push(`<span class="equation-number">(${index})</span>`);
101
- }
102
-
103
- // Hidden LaTeX source (for polyfill)
104
- ctx.push(`<code class="math-source" hidden aria-hidden="true">`);
105
- ctx.push(escapeHtml(latex));
106
- ctx.push(`</code>`);
107
-
108
- // MathML output
109
- ctx.push(`<span class="math-render">`);
110
- if (mathml) {
111
- ctx.push(mathml);
112
- } else {
113
- // Fallback: display error
114
- ctx.push(`<span class="math-error">`);
115
- ctx.push(escapeHtml(latex));
116
- ctx.push(`</span>`);
117
- }
118
- ctx.push(`</span>`);
119
-
120
- ctx.push("</div>");
121
- }
122
-
123
- /**
124
- * Render an inline math element (`[[$...$]]`).
125
- *
126
- * Produces a `<span class="math-inline">` containing:
127
- * - A hidden `<code class="math-source">` with the raw LaTeX
128
- * - A `<span class="math-render">` with the MathML output (or `$...$` error fallback)
129
- *
130
- * @param ctx - The current render context.
131
- * @param data - Inline math data with LaTeX source.
132
- */
133
- export function renderMathInline(ctx: RenderContext, data: MathInlineData): void {
134
- const latex = data["latex-source"];
135
- const mathml = renderLatexToMathML(latex, false);
136
-
137
- ctx.push(`<span class="math-inline">`);
138
-
139
- // Hidden LaTeX source (for polyfill)
140
- ctx.push(`<code class="math-source" hidden aria-hidden="true">`);
141
- ctx.push(escapeHtml(latex));
142
- ctx.push(`</code>`);
143
-
144
- // MathML output
145
- ctx.push(`<span class="math-render">`);
146
- if (mathml) {
147
- ctx.push(mathml);
148
- } else {
149
- // Fallback: display with $ delimiters
150
- ctx.push(`<span class="math-error">$`);
151
- ctx.push(escapeHtml(latex));
152
- ctx.push(`$</span>`);
153
- }
154
- ctx.push(`</span>`);
155
-
156
- ctx.push("</span>");
157
- }
158
-
159
- /**
160
- * Render an equation reference (`[[eref name]]`) that links to a named equation.
161
- *
162
- * Produces a `<span class="eref">` containing a link to the equation's
163
- * `#equation-{name}` ID and an empty tooltip span that the runtime
164
- * `math` module populates on hover with a preview of the equation.
165
- *
166
- * @param ctx - The current render context.
167
- * @param name - The equation name to reference.
168
- */
169
- export function renderEquationRef(ctx: RenderContext, name: string): void {
170
- const id = ctx.generateId("equation-", name);
171
- ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
172
- ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
173
- ctx.push(escapeHtml(name));
174
- ctx.push(`</a>`);
175
- ctx.push(`<span class="eref-tooltip" aria-hidden="true"></span>`);
176
- ctx.push("</span>");
177
- }
@@ -1,75 +0,0 @@
1
- /**
2
- *
3
- * Renderer for `[[tabview]]...[[/tabview]]` tab containers.
4
- *
5
- * Wikidot uses a YUI-compatible tabview widget. The rendered HTML follows
6
- * the YUI class naming convention (`yui-navset`, `yui-nav`, `yui-content`)
7
- * and uses inline `display` styles for tab visibility. Tab switching is
8
- * handled at runtime by the `tabview` runtime module.
9
- *
10
- * A deterministic widget ID is generated from an MD5-length hash of the
11
- * concatenated tab labels, ensuring stable IDs across renders.
12
- *
13
- * @module
14
- */
15
-
16
- import type { TabData } from "@wdprlib/ast";
17
- import type { RenderContext } from "../context";
18
- import { escapeHtml } from "../escape";
19
- import { syncHashMd5 } from "../hash";
20
- import { renderElements } from "../render";
21
-
22
- /**
23
- * Render a `[[tabview]]` element with YUI-compatible HTML structure.
24
- *
25
- * The first tab is selected by default (visible, with the `selected` class
26
- * on its nav item). All other tabs have `display:none` on their content divs.
27
- *
28
- * @param ctx - The current render context.
29
- * @param tabs - Array of tab data, each with a label and child elements.
30
- */
31
- export function renderTabView(ctx: RenderContext, tabs: TabData[]): void {
32
- // Generate MD5 hash from tab labels
33
- const labelString = tabs.map((t) => t.label).join("");
34
- const hash = md5Hash(labelString);
35
-
36
- const widgetId = ctx.generateFixedId(`wiki-tabview-${hash}`);
37
-
38
- // Container
39
- ctx.push(`<div id="${widgetId}" class="yui-navset">`);
40
-
41
- // Navigation tabs
42
- ctx.push(`<ul class="yui-nav">`);
43
- for (let i = 0; i < tabs.length; i++) {
44
- const tab = tabs[i]!;
45
- const selectedClass = i === 0 ? ` class="selected"` : "";
46
- ctx.push(`<li${selectedClass}>`);
47
- ctx.push(`<a href="javascript:;"><em>${escapeHtml(tab.label)}</em></a>`);
48
- ctx.push("</li>");
49
- }
50
- ctx.push("</ul>");
51
-
52
- // Content panels
53
- ctx.push(`<div class="yui-content">`);
54
- for (let i = 0; i < tabs.length; i++) {
55
- const tab = tabs[i]!;
56
- const displayStyle = i === 0 ? "" : ` style="display:none"`;
57
- const tabId = ctx.generateId("wiki-tab-0-", i);
58
- ctx.push(`<div id="${tabId}"${displayStyle}>`);
59
- renderElements(ctx, tab.elements);
60
- ctx.push("</div>");
61
- }
62
- ctx.push("</div>");
63
-
64
- ctx.push("</div>"); // close yui-navset
65
- }
66
-
67
- /**
68
- * Compute an MD5-length hash of the input string for widget ID generation.
69
- *
70
- * @param input - String to hash (typically concatenated tab labels).
71
- * @returns A 32-character hex hash string.
72
- */
73
- function md5Hash(input: string): string {
74
- return syncHashMd5(input);
75
- }
@@ -1,101 +0,0 @@
1
- /**
2
- *
3
- * Renderer for Wikidot table elements.
4
- *
5
- * Wikidot supports two table syntaxes:
6
- * - Pipe syntax (`||cell||cell||`) -- adds `class="wiki-content-table"`
7
- * - Block syntax (`[[table]]...[[/table]]`) -- no default class
8
- *
9
- * Both syntaxes support cell alignment (via tildes), column/row spans,
10
- * header cells (marked with `~`), and custom attributes on rows and cells.
11
- *
12
- * @module
13
- */
14
-
15
- import type { TableData } from "@wdprlib/ast";
16
- import type { RenderContext } from "../context";
17
- import { escapeAttr, sanitizeAttributes } from "../escape";
18
- import { renderElements } from "../render";
19
-
20
- /**
21
- * Render a table element.
22
- *
23
- * Pipe-syntax tables receive `class="wiki-content-table"` on the `<table>`
24
- * element. Cell alignment is rendered as inline `text-align` styles.
25
- * Header cells use `<th>` instead of `<td>`. Column and row spans are
26
- * applied from the AST data and attributes, respectively.
27
- *
28
- * @param ctx - The current render context.
29
- * @param data - Table data with rows, cells, attributes, and source type.
30
- */
31
- export function renderTable(ctx: RenderContext, data: TableData): void {
32
- // Only add wiki-content-table class for pipe syntax tables
33
- const isPipeTable = data.attributes._source === "pipe";
34
- const classAttr = isPipeTable ? ' class="wiki-content-table"' : "";
35
- ctx.push(`<table${classAttr}${renderTableAttrs(data.attributes)}>`);
36
-
37
- for (const row of data.rows) {
38
- ctx.push(`<tr${renderTableAttrs(row.attributes)}>`);
39
-
40
- for (const cell of row.cells) {
41
- const tag = cell.header ? "th" : "td";
42
- const attrs: string[] = [];
43
- const safeCellAttrs = sanitizeAttributes(cell.attributes);
44
-
45
- if (cell["column-span"] > 1) {
46
- attrs.push(`colspan="${cell["column-span"]}"`);
47
- }
48
-
49
- // Handle rowspan from attributes
50
- if (safeCellAttrs.rowspan) {
51
- const rowspan = parseInt(safeCellAttrs.rowspan, 10);
52
- if (rowspan > 1) {
53
- attrs.push(`rowspan="${rowspan}"`);
54
- }
55
- }
56
-
57
- if (cell.align) {
58
- const existingStyle = safeCellAttrs.style ?? "";
59
- const alignStyle = `text-align: ${cell.align};`;
60
- if (existingStyle) {
61
- attrs.push(`style="${escapeAttr(existingStyle + "; " + alignStyle)}"`);
62
- } else {
63
- attrs.push(`style="${alignStyle}"`);
64
- }
65
- }
66
-
67
- // Additional cell attributes
68
- for (const [key, value] of Object.entries(safeCellAttrs)) {
69
- if (key === "style" && cell.align) continue; // Already handled
70
- if (key === "rowspan") continue; // Already handled
71
- attrs.push(`${key}="${escapeAttr(value)}"`);
72
- }
73
-
74
- const attrStr = attrs.length > 0 ? " " + attrs.join(" ") : "";
75
- ctx.push(`<${tag}${attrStr}>`);
76
- renderElements(ctx, cell.elements);
77
- ctx.push(`</${tag}>`);
78
- }
79
-
80
- ctx.push("</tr>");
81
- }
82
-
83
- ctx.push("</table>");
84
- }
85
-
86
- /**
87
- * Sanitize and render table-level or row-level attributes, excluding
88
- * internal `_`-prefixed keys.
89
- *
90
- * @param attributes - Raw attribute map from the AST.
91
- * @returns An HTML attribute string with leading space, or `""` if empty.
92
- */
93
- function renderTableAttrs(attributes: Record<string, string>): string {
94
- const safe = sanitizeAttributes(attributes);
95
- let result = "";
96
- for (const [key, value] of Object.entries(safe)) {
97
- if (key.startsWith("_")) continue;
98
- result += ` ${key}="${escapeAttr(value)}"`;
99
- }
100
- return result;
101
- }
@@ -1,57 +0,0 @@
1
- /**
2
- *
3
- * Renderers for text-level AST nodes: plain text, raw/literal text,
4
- * and email addresses.
5
- *
6
- * @module
7
- */
8
-
9
- import type { RenderContext } from "../context";
10
- import { escapeAttr, escapeHtml, isValidEmail } from "../escape";
11
-
12
- /**
13
- * Render a plain text node by HTML-escaping and appending to the output.
14
- *
15
- * @param ctx - The current render context.
16
- * @param data - The raw text content.
17
- */
18
- export function renderText(ctx: RenderContext, data: string): void {
19
- ctx.pushEscaped(data);
20
- }
21
-
22
- /**
23
- * Render raw/literal text (Wikidot `@@...@@` syntax).
24
- *
25
- * Raw text is rendered inside a `<span style="white-space: pre-wrap;">` with
26
- * spaces encoded as `&#32;` to preserve Wikidot's exact formatting. Empty
27
- * strings produce no output.
28
- *
29
- * @param ctx - The current render context.
30
- * @param data - The raw text content.
31
- */
32
- export function renderRaw(ctx: RenderContext, data: string): void {
33
- if (data === "") return;
34
- ctx.push(`<span style="white-space: pre-wrap;">`);
35
- // Wikidot encodes spaces as &#32; in raw content
36
- ctx.push(escapeHtml(data).replace(/ /g, "&#32;"));
37
- ctx.push("</span>");
38
- }
39
-
40
- /**
41
- * Render an email address element as a `mailto:` link.
42
- *
43
- * The email is validated before creating the link. Invalid email addresses
44
- * are rendered as plain escaped text to prevent `mailto:` injection.
45
- *
46
- * @param ctx - The current render context.
47
- * @param email - The email address string.
48
- */
49
- export function renderEmail(ctx: RenderContext, email: string): void {
50
- // Validate email format before creating link
51
- if (!isValidEmail(email)) {
52
- // Invalid email: render as plain text
53
- ctx.pushEscaped(email);
54
- return;
55
- }
56
- ctx.push(`<a href="mailto:${escapeAttr(email)}">${escapeHtml(email)}</a>`);
57
- }
@@ -1,147 +0,0 @@
1
- /**
2
- *
3
- * Renderer for `[[toc]]` (Table of Contents) elements.
4
- *
5
- * The table of contents is built from the pre-collected `tocElements`
6
- * in the render context (populated from the `table-of-contents` field
7
- * of the syntax tree). Each entry is rendered as a `<div>` with
8
- * `margin-left` indentation based on heading depth, matching Wikidot's
9
- * flat-div TOC format.
10
- *
11
- * The TOC supports fold/unfold toggling via a `#toc-action-bar` with
12
- * Fold/Unfold links, handled at runtime by the `toc` runtime module.
13
- * Alignment options (`left`/`right`) produce a floated container.
14
- *
15
- * @module
16
- */
17
-
18
- import type { Element, ListData, ListItem, TableOfContentsData } from "@wdprlib/ast";
19
- import type { RenderContext } from "../context";
20
- import { escapeAttr, escapeHtml } from "../escape";
21
-
22
- /**
23
- * Extract text content and href from a link element for TOC rendering.
24
- *
25
- * @param element - An AST element (expected to be a link).
26
- * @returns An object with `href` and `text`, or `null` if not a link.
27
- */
28
- function extractLinkText(element: Element): { href: string; text: string } | null {
29
- if (element.element !== "link") return null;
30
- const label = element.data.label;
31
- let text = "";
32
- if (typeof label === "object" && label !== null && "text" in label) {
33
- text = label.text;
34
- }
35
- const href = typeof element.data.link === "string" ? element.data.link : "";
36
- return { href, text };
37
- }
38
-
39
- /**
40
- * Render TOC entries as flat `<div>` elements with `margin-left` indentation.
41
- *
42
- * @param ctx - The current render context.
43
- * @param elements - Top-level TOC elements (expected to contain list elements).
44
- */
45
- function renderTocEntries(ctx: RenderContext, elements: Element[]): void {
46
- for (const element of elements) {
47
- if (element.element === "list") {
48
- renderTocList(ctx, element.data, 1);
49
- }
50
- }
51
- }
52
-
53
- /**
54
- * Recursively render a TOC list at a given nesting depth.
55
- *
56
- * @param ctx - The current render context.
57
- * @param listData - The list data representing this level of the TOC.
58
- * @param depth - Current nesting depth (1-based), used for `margin-left` calculation.
59
- */
60
- function renderTocList(ctx: RenderContext, listData: ListData, depth: number): void {
61
- for (const item of listData.items) {
62
- renderTocItem(ctx, item, depth);
63
- }
64
- }
65
-
66
- /**
67
- * Rewrite a TOC anchor `href` (e.g., `"#toc0"`) so that it matches
68
- * the rendered heading's actual ID.
69
- *
70
- * When `useTrueIds` is false, heading IDs have a random suffix appended
71
- * (e.g., `toc0-a1b2c3`). This function regenerates the ID through the
72
- * context to ensure the TOC link targets the correct heading.
73
- *
74
- * @param ctx - The current render context.
75
- * @param href - The original href from the TOC link (e.g., `"#toc0"`).
76
- * @returns The rewritten href with the correct ID.
77
- */
78
- function rewriteTocAnchor(ctx: RenderContext, href: string): string {
79
- const match = /^#toc(\d+)$/.exec(href);
80
- if (!match) return href;
81
- return `#${ctx.generateId("toc", Number(match[1]))}`;
82
- }
83
-
84
- /**
85
- * Render a single TOC list item as a `<div>` with indented margin.
86
- *
87
- * @param ctx - The current render context.
88
- * @param item - The list item (elements or sub-list).
89
- * @param depth - Current nesting depth for margin calculation.
90
- */
91
- function renderTocItem(ctx: RenderContext, item: ListItem, depth: number): void {
92
- if (item["item-type"] === "elements") {
93
- for (const el of item.elements) {
94
- const link = extractLinkText(el);
95
- if (link) {
96
- const href = rewriteTocAnchor(ctx, link.href);
97
- ctx.push(
98
- `<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`,
99
- );
100
- }
101
- }
102
- } else if (item["item-type"] === "sub-list") {
103
- renderTocList(ctx, item.data, depth + 1);
104
- }
105
- }
106
-
107
- /**
108
- * Render a `[[toc]]` table of contents block.
109
- *
110
- * Non-floating TOC is wrapped in a `<table>` for Wikidot layout compatibility.
111
- * Floating TOC (align `left` or `right`) uses a `<div>` with a float class.
112
- *
113
- * The TOC container uses fixed IDs (`#toc`, `#toc-action-bar`, `#toc-list`)
114
- * that the runtime `toc` module queries for fold/unfold toggling.
115
- *
116
- * @param ctx - The current render context.
117
- * @param data - TOC configuration data with optional alignment.
118
- */
119
- export function renderTableOfContents(ctx: RenderContext, data: TableOfContentsData): void {
120
- const isFloat = data.align === "left" || data.align === "right";
121
-
122
- // Non-float: wrap in table (Wikidot behavior)
123
- if (!isFloat) {
124
- ctx.push(`<table style="margin:0; padding:0"><tr><td style="margin:0; padding:0">`);
125
- }
126
-
127
- // TOC container IDs are fixed — the runtime queries them by ID (#toc, #toc-action-bar, #toc-list)
128
- if (isFloat) {
129
- const floatClass = data.align === "left" ? "floatleft" : "floatright";
130
- ctx.push(`<div id="toc" class="${floatClass}">`);
131
- } else {
132
- ctx.push(`<div id="toc">`);
133
- }
134
-
135
- ctx.push(
136
- `<div id="toc-action-bar"><a href="javascript:;">Fold</a><a style="display: none" href="javascript:;">Unfold</a></div>`,
137
- );
138
- ctx.push(`<div class="title">Table of Contents</div>`);
139
- ctx.push(`<div id="toc-list">`);
140
- renderTocEntries(ctx, ctx.tocElements);
141
- ctx.push("</div>");
142
- ctx.push("</div>");
143
-
144
- if (!isFloat) {
145
- ctx.push(`</td></tr></table>`);
146
- }
147
- }
@@ -1,79 +0,0 @@
1
- /**
2
- *
3
- * Renderer for `[[user username]]` elements.
4
- *
5
- * User elements display a username with an optional avatar image and
6
- * karma badge. The user profile data is resolved via the
7
- * `resolvers.user` callback; when no resolver is provided or the user
8
- * is not found, the raw username is rendered as plain text.
9
- *
10
- * The special username `"anonymous"` is always rendered as the literal
11
- * text "Anonymous" without any link or avatar.
12
- *
13
- * @module
14
- */
15
-
16
- import type { UserData } from "@wdprlib/ast";
17
- import type { RenderContext } from "../context";
18
- import { escapeHtml, escapeAttr } from "../escape";
19
-
20
- /**
21
- * Render a `[[user username]]` element.
22
- *
23
- * Rendering modes:
24
- * - "anonymous" username: plain text "Anonymous"
25
- * - Unresolved user: plain escaped username text
26
- * - Resolved without avatar: `<span class="printuser"><a>name</a></span>`
27
- * - Resolved with avatar: `<span class="printuser avatarhover">` with
28
- * avatar image, optional karma badge, and linked display name
29
- *
30
- * @param ctx - The current render context.
31
- * @param data - User element data with username and show-avatar flag.
32
- */
33
- export function renderUser(ctx: RenderContext, data: UserData): void {
34
- const normalized = data.name.toLowerCase().trim();
35
-
36
- // Special case: "anonymous" renders as "Anonymous" text only
37
- if (normalized === "anonymous") {
38
- ctx.push("Anonymous");
39
- return;
40
- }
41
-
42
- const resolved = ctx.options.resolvers?.user?.(data.name) ?? null;
43
-
44
- if (resolved === null) {
45
- // User not resolved - render as simple text
46
- ctx.push(escapeHtml(data.name));
47
- return;
48
- }
49
-
50
- const displayName = resolved.name ?? data.name;
51
- const hrefAttr = resolved.url ? ` href="${escapeAttr(resolved.url)}"` : "";
52
-
53
- // Avatar only shown when both url and avatarUrl are provided
54
- const showAvatar = data["show-avatar"] && resolved.url && resolved.avatarUrl;
55
-
56
- if (showAvatar) {
57
- // With avatar
58
- const styleAttr = resolved.karmaUrl
59
- ? ` style="background-image:url(${escapeAttr(resolved.karmaUrl)})"`
60
- : "";
61
- ctx.push(`<span class="printuser avatarhover">`);
62
- ctx.push(`<a${hrefAttr}>`);
63
- ctx.push(
64
- `<img class="small" src="${escapeAttr(resolved.avatarUrl!)}" alt="${escapeAttr(displayName)}"${styleAttr} />`,
65
- );
66
- ctx.push("</a>");
67
- ctx.push(`<a${hrefAttr}>`);
68
- ctx.push(escapeHtml(displayName));
69
- ctx.push("</a>");
70
- ctx.push("</span>");
71
- } else {
72
- // Without avatar
73
- ctx.push(`<span class="printuser">`);
74
- ctx.push(`<a${hrefAttr}>`);
75
- ctx.push(escapeHtml(displayName));
76
- ctx.push("</a>");
77
- ctx.push("</span>");
78
- }
79
- }