@wdprlib/render 1.4.0 → 2.1.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 (58) hide show
  1. package/dist/index.cjs +126 -393
  2. package/dist/index.js +117 -384
  3. package/package.json +5 -3
  4. package/src/context.ts +422 -0
  5. package/src/elements/bibliography.ts +123 -0
  6. package/src/elements/clear-float.ts +27 -0
  7. package/src/elements/code.ts +49 -0
  8. package/src/elements/collapsible.ts +105 -0
  9. package/src/elements/color.ts +32 -0
  10. package/src/elements/container.ts +302 -0
  11. package/src/elements/date.ts +59 -0
  12. package/src/elements/embed-block.ts +327 -0
  13. package/src/elements/embed.ts +166 -0
  14. package/src/elements/expr.ts +102 -0
  15. package/src/elements/footnote.ts +76 -0
  16. package/src/elements/html.ts +79 -0
  17. package/src/elements/iframe.ts +44 -0
  18. package/src/elements/iftags.ts +118 -0
  19. package/src/elements/image.ts +154 -0
  20. package/src/elements/include.ts +43 -0
  21. package/src/elements/index.ts +35 -0
  22. package/src/elements/line-break.ts +22 -0
  23. package/src/elements/link.ts +201 -0
  24. package/src/elements/list.ts +241 -0
  25. package/src/elements/math.ts +177 -0
  26. package/src/elements/module/backlinks.ts +28 -0
  27. package/src/elements/module/categories.ts +27 -0
  28. package/src/elements/module/index.ts +67 -0
  29. package/src/elements/module/join.ts +33 -0
  30. package/src/elements/module/listpages.ts +27 -0
  31. package/src/elements/module/listusers.ts +27 -0
  32. package/src/elements/module/page-tree.ts +27 -0
  33. package/src/elements/module/rate.ts +44 -0
  34. package/src/elements/tab-view.ts +75 -0
  35. package/src/elements/table.ts +101 -0
  36. package/src/elements/text.ts +57 -0
  37. package/src/elements/toc.ts +147 -0
  38. package/src/elements/user.ts +79 -0
  39. package/src/escape.ts +829 -0
  40. package/src/hash.ts +62 -0
  41. package/src/index.ts +26 -0
  42. package/src/libs/highlighter/engine.ts +352 -0
  43. package/src/libs/highlighter/index.ts +70 -0
  44. package/src/libs/highlighter/languages/cpp.ts +345 -0
  45. package/src/libs/highlighter/languages/css.ts +104 -0
  46. package/src/libs/highlighter/languages/diff.ts +154 -0
  47. package/src/libs/highlighter/languages/dtd.ts +99 -0
  48. package/src/libs/highlighter/languages/html.ts +59 -0
  49. package/src/libs/highlighter/languages/java.ts +251 -0
  50. package/src/libs/highlighter/languages/javascript.ts +213 -0
  51. package/src/libs/highlighter/languages/php.ts +433 -0
  52. package/src/libs/highlighter/languages/python.ts +308 -0
  53. package/src/libs/highlighter/languages/ruby.ts +360 -0
  54. package/src/libs/highlighter/languages/sql.ts +125 -0
  55. package/src/libs/highlighter/languages/xml.ts +68 -0
  56. package/src/libs/highlighter/types.ts +44 -0
  57. package/src/render.ts +231 -0
  58. package/src/types.ts +140 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[collapsible]]...[[/collapsible]]` blocks.
4
+ *
5
+ * Wikidot's collapsible markup produces a two-state widget: a "folded"
6
+ * state showing a "show" link and an "unfolded" state showing the
7
+ * content plus a "hide" link. Toggle behavior is handled at runtime
8
+ * by the `collapsible` runtime module.
9
+ *
10
+ * This renderer outputs the full DOM structure for both states, with
11
+ * visibility controlled via inline `display` styles based on the
12
+ * `start-open` flag. The "hide" link can appear at the top, bottom,
13
+ * or both positions.
14
+ *
15
+ * @module
16
+ */
17
+
18
+ import type { CollapsibleData } from "@wdprlib/ast";
19
+ import type { RenderContext } from "../context";
20
+ import { escapeHtml } from "../escape";
21
+ import { renderElements } from "../render";
22
+
23
+ /**
24
+ * Render a `[[collapsible]]` block with Wikidot-compatible HTML structure.
25
+ *
26
+ * The output contains both folded and unfolded states. Spaces in
27
+ * show/hide labels are encoded as ` ` to match Wikidot's behavior.
28
+ *
29
+ * @param ctx - The current render context.
30
+ * @param data - Collapsible block data with show/hide text, start-open
31
+ * flag, and top/bottom link placement options.
32
+ */
33
+ export function renderCollapsible(ctx: RenderContext, data: CollapsibleData): void {
34
+ const startOpen = data["start-open"];
35
+ const showTop = data["show-top"];
36
+ const showBottom = data["show-bottom"];
37
+
38
+ // Determine show/hide link text with   encoding
39
+ // When custom text is provided, use it as-is (it already contains the prefix like "+ Show")
40
+ // When not provided, use default prefix + text
41
+ const showLabel = data["show-text"]
42
+ ? formatLabelText(data["show-text"])
43
+ : formatCollapsibleText("+", "show block");
44
+ const hideLabel = data["hide-text"]
45
+ ? formatLabelText(data["hide-text"])
46
+ : formatCollapsibleText("\u2013", "hide block");
47
+
48
+ ctx.push(`<div class="collapsible-block">`);
49
+
50
+ // Folded state
51
+ const foldedStyle = startOpen ? ` style="display:none"` : "";
52
+ ctx.push(`<div class="collapsible-block-folded"${foldedStyle}>`);
53
+ ctx.push(`<a class="collapsible-block-link" href="javascript:;">${showLabel}</a>`);
54
+ ctx.push("</div>");
55
+
56
+ // Unfolded state
57
+ const unfoldedStyle = startOpen ? "" : ` style="display:none"`;
58
+ ctx.push(`<div class="collapsible-block-unfolded"${unfoldedStyle}>`);
59
+
60
+ // Hide link at top (default position)
61
+ if (showTop || (!showTop && !showBottom)) {
62
+ ctx.push(`<div class="collapsible-block-unfolded-link">`);
63
+ ctx.push(`<a class="collapsible-block-link" href="javascript:;">${hideLabel}</a>`);
64
+ ctx.push("</div>");
65
+ }
66
+
67
+ // Content
68
+ ctx.push(`<div class="collapsible-block-content">`);
69
+ renderElements(ctx, data.elements);
70
+ ctx.push("</div>");
71
+
72
+ // Hide link at bottom
73
+ if (showBottom) {
74
+ ctx.push(`<div class="collapsible-block-unfolded-link">`);
75
+ ctx.push(`<a class="collapsible-block-link" href="javascript:;">${hideLabel}</a>`);
76
+ ctx.push("</div>");
77
+ }
78
+
79
+ ctx.push("</div>"); // close unfolded
80
+ ctx.push("</div>"); // close collapsible-block
81
+ }
82
+
83
+ /**
84
+ * Format a default collapsible link label by prepending a prefix symbol
85
+ * (e.g. "+" or en-dash) with `&nbsp;` encoding for spaces.
86
+ *
87
+ * @param prefix - Symbol character prepended before the label text.
88
+ * @param text - Default label text (e.g. "show block").
89
+ * @returns HTML-safe label string with non-breaking spaces.
90
+ */
91
+ function formatCollapsibleText(prefix: string, text: string): string {
92
+ const encoded = escapeHtml(text).replace(/ /g, "&nbsp;");
93
+ return `${prefix}&nbsp;${encoded}`;
94
+ }
95
+
96
+ /**
97
+ * Format a custom collapsible link label by escaping HTML and
98
+ * replacing spaces with `&nbsp;` (matching Wikidot behavior).
99
+ *
100
+ * @param text - Custom label text provided by the user.
101
+ * @returns HTML-safe label string with non-breaking spaces.
102
+ */
103
+ function formatLabelText(text: string): string {
104
+ return escapeHtml(text).replace(/ /g, "&nbsp;");
105
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ *
3
+ * Renderer for `##color|text##` inline color markup in Wikidot syntax.
4
+ *
5
+ * The color value is sanitized to prevent CSS injection before being
6
+ * injected into an inline `style` attribute.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { ColorData } from "@wdprlib/ast";
12
+ import type { RenderContext } from "../context";
13
+ import { escapeAttr, sanitizeCssColor } from "../escape";
14
+ import { renderElements } from "../render";
15
+
16
+ /**
17
+ * Render an inline color element (`##color|text##`).
18
+ *
19
+ * Wraps the child elements in a `<span>` with an inline `color` style.
20
+ * The user-supplied color value is validated and sanitized; invalid
21
+ * values fall back to `"inherit"`.
22
+ *
23
+ * @param ctx - The current render context.
24
+ * @param data - Color element data with the color value and child elements.
25
+ */
26
+ export function renderColor(ctx: RenderContext, data: ColorData): void {
27
+ // Sanitize color value to prevent CSS injection
28
+ const safeColor = sanitizeCssColor(data.color, "inherit");
29
+ ctx.push(`<span style="color: ${escapeAttr(safeColor)}">`);
30
+ renderElements(ctx, data.elements);
31
+ ctx.push("</span>");
32
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ *
3
+ * Renderer for "container" AST elements -- the most general wrapper node
4
+ * in the Wikidot AST.
5
+ *
6
+ * A container can represent many different HTML constructs depending on
7
+ * its `type` discriminant:
8
+ * - Headers (`h1`..`h6`) with optional TOC anchor IDs
9
+ * - Text alignment wrappers (`left`, `center`, `right`, `justify`)
10
+ * - Inline formatting (`bold`, `italics`, `underline`, `strikethrough`,
11
+ * `superscript`, `subscript`, `monospace`, `mark`, `insertion`, `deletion`)
12
+ * - Block containers (`paragraph`, `div`, `blockquote`, `span`)
13
+ * - Visibility modifiers (`hidden`, `invisible`)
14
+ * - Ruby annotations (`ruby`, `ruby-text`)
15
+ * - Definition lists (`definition-list`, `definition-list-item`, etc.)
16
+ * - Table sub-elements (`table-row`, `table-cell`)
17
+ * - Size containers with inline `font-size` styling
18
+ *
19
+ * All attributes are sanitized before rendering to prevent XSS.
20
+ *
21
+ * @module
22
+ */
23
+
24
+ import type { ContainerData } from "@wdprlib/ast";
25
+ import { isStringContainerType, isHeaderType, isAlignType } from "@wdprlib/ast";
26
+ import type { RenderContext } from "../context";
27
+ import { escapeAttr, sanitizeAttributes } from "../escape";
28
+ import { renderElements } from "../render";
29
+
30
+ /**
31
+ * Render a container element by dispatching on its `type` discriminant.
32
+ *
33
+ * Headers, alignment wrappers, and string-typed containers each follow
34
+ * different rendering paths. Unknown container types render their
35
+ * children without a wrapping element.
36
+ *
37
+ * @param ctx - The current render context.
38
+ * @param data - Container data including type, attributes, and child elements.
39
+ */
40
+ export function renderContainer(ctx: RenderContext, data: ContainerData): void {
41
+ const { type, attributes, elements } = data;
42
+
43
+ if (isHeaderType(type)) {
44
+ renderHeader(ctx, type.header.level, type.header["has-toc"], attributes, elements);
45
+ return;
46
+ }
47
+
48
+ if (isAlignType(type)) {
49
+ ctx.push(`<div style="text-align: ${type.align};">`);
50
+ renderElements(ctx, elements);
51
+ ctx.push("</div>");
52
+ return;
53
+ }
54
+
55
+ if (isStringContainerType(type)) {
56
+ renderStringContainer(ctx, type, attributes, elements);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Render a heading element (`h1`..`h6`).
62
+ *
63
+ * When the heading participates in the table of contents (`hasToc` is true),
64
+ * a `toc{N}` ID attribute is generated so that TOC links can target it.
65
+ * The heading content is wrapped in a `<span>` to match Wikidot's output.
66
+ *
67
+ * @param ctx - The current render context.
68
+ * @param level - Heading level (1-6).
69
+ * @param hasToc - Whether this heading has a corresponding TOC entry.
70
+ * @param attributes - Sanitized HTML attributes from the AST.
71
+ * @param elements - Child elements to render inside the heading.
72
+ */
73
+ function renderHeader(
74
+ ctx: RenderContext,
75
+ level: number,
76
+ hasToc: boolean,
77
+ attributes: Record<string, string>,
78
+ elements: import("@wdprlib/ast").Element[],
79
+ ): void {
80
+ const tag = `h${level}`;
81
+ if (hasToc) {
82
+ const tocId = ctx.generateId("toc", ctx.nextTocIndex());
83
+ ctx.push(`<${tag} id="${tocId}"${renderAttrs(attributes)}>`);
84
+ } else {
85
+ ctx.push(`<${tag}${renderAttrs(attributes)}>`);
86
+ }
87
+ ctx.push("<span>");
88
+ renderElements(ctx, elements);
89
+ ctx.push("</span>");
90
+ ctx.push(`</${tag}>`);
91
+ }
92
+
93
+ /**
94
+ * Render a container whose type is a plain string identifier.
95
+ *
96
+ * Dispatches to the appropriate HTML element based on the type string.
97
+ * Each case wraps child elements in the correct HTML tag with sanitized
98
+ * attributes. Empty divs without attributes are skipped (matching Wikidot).
99
+ *
100
+ * @param ctx - The current render context.
101
+ * @param type - Container type string (e.g. "paragraph", "bold", "div").
102
+ * @param attributes - Sanitized HTML attributes from the AST.
103
+ * @param elements - Child elements to render inside the container.
104
+ */
105
+ function renderStringContainer(
106
+ ctx: RenderContext,
107
+ type: string,
108
+ attributes: Record<string, string>,
109
+ elements: import("@wdprlib/ast").Element[],
110
+ ): void {
111
+ switch (type) {
112
+ case "paragraph":
113
+ ctx.push(`<p${renderAttrs(attributes)}>`);
114
+ renderElements(ctx, elements);
115
+ ctx.push("</p>");
116
+ break;
117
+ case "bold":
118
+ ctx.push(`<strong${renderAttrs(attributes)}>`);
119
+ renderElements(ctx, elements);
120
+ ctx.push("</strong>");
121
+ break;
122
+ case "italics":
123
+ ctx.push(`<em${renderAttrs(attributes)}>`);
124
+ renderElements(ctx, elements);
125
+ ctx.push("</em>");
126
+ break;
127
+ case "underline":
128
+ ctx.push(`<span style="text-decoration: underline;"${renderAttrs(attributes)}>`);
129
+ renderElements(ctx, elements);
130
+ ctx.push("</span>");
131
+ break;
132
+ case "strikethrough":
133
+ ctx.push(`<span style="text-decoration: line-through;"${renderAttrs(attributes)}>`);
134
+ renderElements(ctx, elements);
135
+ ctx.push("</span>");
136
+ break;
137
+ case "superscript":
138
+ ctx.push(`<sup${renderAttrs(attributes)}>`);
139
+ renderElements(ctx, elements);
140
+ ctx.push("</sup>");
141
+ break;
142
+ case "subscript":
143
+ ctx.push(`<sub${renderAttrs(attributes)}>`);
144
+ renderElements(ctx, elements);
145
+ ctx.push("</sub>");
146
+ break;
147
+ case "monospace":
148
+ ctx.push(`<tt${renderAttrs(attributes)}>`);
149
+ renderElements(ctx, elements);
150
+ ctx.push("</tt>");
151
+ break;
152
+ case "span":
153
+ ctx.push(`<span${renderAttrs(attributes)}>`);
154
+ renderElements(ctx, elements);
155
+ ctx.push("</span>");
156
+ break;
157
+ case "div":
158
+ // Wikidot skips empty divs without attributes
159
+ if (elements.length === 0 && Object.keys(attributes).length === 0) {
160
+ break;
161
+ }
162
+ ctx.push(`<div${renderAttrs(attributes)}>`);
163
+ renderElements(ctx, elements);
164
+ ctx.push("</div>");
165
+ break;
166
+ case "blockquote":
167
+ ctx.push(`<blockquote${renderAttrs(attributes)}>`);
168
+ renderElements(ctx, elements);
169
+ ctx.push("</blockquote>");
170
+ break;
171
+ case "mark":
172
+ ctx.push(`<mark${renderAttrs(attributes)}>`);
173
+ renderElements(ctx, elements);
174
+ ctx.push("</mark>");
175
+ break;
176
+ case "insertion":
177
+ ctx.push(`<ins${renderAttrs(attributes)}>`);
178
+ renderElements(ctx, elements);
179
+ ctx.push("</ins>");
180
+ break;
181
+ case "deletion":
182
+ ctx.push(`<del${renderAttrs(attributes)}>`);
183
+ renderElements(ctx, elements);
184
+ ctx.push("</del>");
185
+ break;
186
+ case "size":
187
+ // Size uses style attribute with font-size
188
+ renderSizeContainer(ctx, attributes, elements);
189
+ break;
190
+ case "hidden":
191
+ ctx.push(`<span style="display: none"${renderAttrs(attributes)}>`);
192
+ renderElements(ctx, elements);
193
+ ctx.push("</span>");
194
+ break;
195
+ case "invisible":
196
+ ctx.push(`<span style="visibility: hidden"${renderAttrs(attributes)}>`);
197
+ renderElements(ctx, elements);
198
+ ctx.push("</span>");
199
+ break;
200
+ case "ruby":
201
+ ctx.push(`<ruby${renderAttrs(attributes)}>`);
202
+ renderElements(ctx, elements);
203
+ ctx.push("</ruby>");
204
+ break;
205
+ case "ruby-text":
206
+ ctx.push(`<rt${renderAttrs(attributes)}>`);
207
+ renderElements(ctx, elements);
208
+ ctx.push("</rt>");
209
+ break;
210
+ case "heading":
211
+ // Heading as container type (used in definition-list context)
212
+ renderElements(ctx, elements);
213
+ break;
214
+ case "collapsible":
215
+ // Collapsible as container type
216
+ renderElements(ctx, elements);
217
+ break;
218
+ case "definition-list":
219
+ ctx.push("<dl>");
220
+ renderElements(ctx, elements);
221
+ ctx.push("</dl>");
222
+ break;
223
+ case "definition-list-item":
224
+ renderElements(ctx, elements);
225
+ break;
226
+ case "definition-list-key":
227
+ ctx.push("<dt>");
228
+ renderElements(ctx, elements);
229
+ ctx.push("</dt>");
230
+ break;
231
+ case "definition-list-value":
232
+ ctx.push("<dd>");
233
+ renderElements(ctx, elements);
234
+ ctx.push("</dd>");
235
+ break;
236
+ case "table-row":
237
+ ctx.push(`<tr${renderAttrs(attributes)}>`);
238
+ renderElements(ctx, elements);
239
+ ctx.push("</tr>");
240
+ break;
241
+ case "table-cell":
242
+ ctx.push(`<td${renderAttrs(attributes)}>`);
243
+ renderElements(ctx, elements);
244
+ ctx.push("</td>");
245
+ break;
246
+ default:
247
+ // Unknown container types: just render children
248
+ renderElements(ctx, elements);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Render a `[[size]]` container element.
254
+ *
255
+ * The font size value is expected to be pre-encoded in the `style`
256
+ * attribute as a `font-size` declaration by the parser. The element
257
+ * is rendered as a `<span>` with the full attribute set.
258
+ *
259
+ * @param ctx - The current render context.
260
+ * @param attributes - Attributes including the `style` with `font-size`.
261
+ * @param elements - Child elements to render inside the span.
262
+ */
263
+ function renderSizeContainer(
264
+ ctx: RenderContext,
265
+ attributes: Record<string, string>,
266
+ elements: import("@wdprlib/ast").Element[],
267
+ ): void {
268
+ const style = attributes.style ?? "";
269
+ // The size value is stored in the style attribute as font-size
270
+ const existingAttrs = { ...attributes };
271
+ if (!style.includes("font-size")) {
272
+ // Fallback: render without font-size if not in style
273
+ ctx.push(`<span${renderAttrs(existingAttrs)}>`);
274
+ } else {
275
+ ctx.push(`<span${renderAttrs(existingAttrs)}>`);
276
+ }
277
+ renderElements(ctx, elements);
278
+ ctx.push("</span>");
279
+ }
280
+
281
+ /**
282
+ * Sanitize and format an attribute map into an HTML attribute string.
283
+ *
284
+ * Internal attributes (prefixed with `_`) are excluded from the output.
285
+ *
286
+ * @param attributes - Raw attribute map from the AST.
287
+ * @returns An HTML attribute string with a leading space, or `""` if empty.
288
+ */
289
+ function renderAttrs(attributes: Record<string, string>): string {
290
+ const safe = sanitizeAttributes(attributes);
291
+ let result = "";
292
+ for (const [key, value] of Object.entries(safe)) {
293
+ // Skip internal attributes
294
+ if (key.startsWith("_")) continue;
295
+ if (value !== "") {
296
+ result += ` ${key}="${escapeAttr(value)}"`;
297
+ } else {
298
+ result += ` ${key}=""`;
299
+ }
300
+ }
301
+ return result;
302
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ *
3
+ * Renderer for Wikidot's date/time elements.
4
+ *
5
+ * Wikidot stores dates as Unix timestamps and optionally provides a
6
+ * `strftime`-style format string. The renderer formats the date
7
+ * server-side; when the `hover` flag is set, the output is wrapped in
8
+ * `<span class="odate">` so that the runtime `odate` module can
9
+ * reformat it to the user's local timezone on the client.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import type { DateData } from "@wdprlib/ast";
15
+ import type { RenderContext } from "../context";
16
+ import { escapeHtml } from "../escape";
17
+
18
+ /**
19
+ * Render a date element, optionally wrapping in an `odate` span for
20
+ * client-side timezone conversion.
21
+ *
22
+ * When `data.format` is provided, the date is formatted using a subset
23
+ * of `strftime` specifiers (`%Y`, `%m`, `%d`, `%H`, `%M`, `%S`).
24
+ * Otherwise, `Date.toLocaleString()` is used as a fallback.
25
+ *
26
+ * @param ctx - The current render context.
27
+ * @param data - Date element data with timestamp, optional format, and hover flag.
28
+ */
29
+ export function renderDate(ctx: RenderContext, data: DateData): void {
30
+ const date = new Date(data.value.timestamp * 1000);
31
+ const formatted = data.format ? formatDate(date, data.format) : date.toLocaleString();
32
+
33
+ if (data.hover) {
34
+ ctx.push(`<span class="odate">${escapeHtml(formatted)}</span>`);
35
+ } else {
36
+ ctx.push(escapeHtml(formatted));
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Format a `Date` using a subset of `strftime` specifiers.
42
+ *
43
+ * Supported specifiers: `%Y` (4-digit year), `%m` (zero-padded month),
44
+ * `%d` (zero-padded day), `%H` (zero-padded hours), `%M` (zero-padded
45
+ * minutes), `%S` (zero-padded seconds).
46
+ *
47
+ * @param date - The date to format.
48
+ * @param format - A strftime-compatible format string.
49
+ * @returns The formatted date string.
50
+ */
51
+ function formatDate(date: Date, format: string): string {
52
+ return format
53
+ .replace(/%Y/g, String(date.getFullYear()))
54
+ .replace(/%m/g, String(date.getMonth() + 1).padStart(2, "0"))
55
+ .replace(/%d/g, String(date.getDate()).padStart(2, "0"))
56
+ .replace(/%H/g, String(date.getHours()).padStart(2, "0"))
57
+ .replace(/%M/g, String(date.getMinutes()).padStart(2, "0"))
58
+ .replace(/%S/g, String(date.getSeconds()).padStart(2, "0"));
59
+ }