@wdprlib/render 2.0.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.
- package/dist/index.cjs +11 -387
- package/dist/index.js +2 -378
- package/package.json +5 -3
- package/src/context.ts +422 -0
- package/src/elements/bibliography.ts +123 -0
- package/src/elements/clear-float.ts +27 -0
- package/src/elements/code.ts +49 -0
- package/src/elements/collapsible.ts +105 -0
- package/src/elements/color.ts +32 -0
- package/src/elements/container.ts +302 -0
- package/src/elements/date.ts +59 -0
- package/src/elements/embed-block.ts +327 -0
- package/src/elements/embed.ts +166 -0
- package/src/elements/expr.ts +102 -0
- package/src/elements/footnote.ts +76 -0
- package/src/elements/html.ts +79 -0
- package/src/elements/iframe.ts +44 -0
- package/src/elements/iftags.ts +118 -0
- package/src/elements/image.ts +154 -0
- package/src/elements/include.ts +43 -0
- package/src/elements/index.ts +35 -0
- package/src/elements/line-break.ts +22 -0
- package/src/elements/link.ts +201 -0
- package/src/elements/list.ts +241 -0
- package/src/elements/math.ts +177 -0
- package/src/elements/module/backlinks.ts +28 -0
- package/src/elements/module/categories.ts +27 -0
- package/src/elements/module/index.ts +67 -0
- package/src/elements/module/join.ts +33 -0
- package/src/elements/module/listpages.ts +27 -0
- package/src/elements/module/listusers.ts +27 -0
- package/src/elements/module/page-tree.ts +27 -0
- package/src/elements/module/rate.ts +44 -0
- package/src/elements/tab-view.ts +75 -0
- package/src/elements/table.ts +101 -0
- package/src/elements/text.ts +57 -0
- package/src/elements/toc.ts +147 -0
- package/src/elements/user.ts +79 -0
- package/src/escape.ts +829 -0
- package/src/hash.ts +62 -0
- package/src/index.ts +26 -0
- package/src/libs/highlighter/engine.ts +352 -0
- package/src/libs/highlighter/index.ts +70 -0
- package/src/libs/highlighter/languages/cpp.ts +345 -0
- package/src/libs/highlighter/languages/css.ts +104 -0
- package/src/libs/highlighter/languages/diff.ts +154 -0
- package/src/libs/highlighter/languages/dtd.ts +99 -0
- package/src/libs/highlighter/languages/html.ts +59 -0
- package/src/libs/highlighter/languages/java.ts +251 -0
- package/src/libs/highlighter/languages/javascript.ts +213 -0
- package/src/libs/highlighter/languages/php.ts +433 -0
- package/src/libs/highlighter/languages/python.ts +308 -0
- package/src/libs/highlighter/languages/ruby.ts +360 -0
- package/src/libs/highlighter/languages/sql.ts +125 -0
- package/src/libs/highlighter/languages/xml.ts +68 -0
- package/src/libs/highlighter/types.ts +44 -0
- package/src/render.ts +231 -0
- 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 ` ` 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, " ");
|
|
93
|
+
return `${prefix} ${encoded}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Format a custom collapsible link label by escaping HTML and
|
|
98
|
+
* replacing spaces with ` ` (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, " ");
|
|
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
|
+
}
|