@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.
- package/dist/index.cjs +2344 -1668
- package/dist/index.d.cts +15 -13
- package/dist/index.d.ts +15 -13
- package/dist/index.js +2375 -1699
- package/package.json +1 -1
- package/src/context/attributes.ts +14 -0
- package/src/context/bibliography.ts +109 -0
- package/src/context/counters.ts +51 -0
- package/src/context/image-urls.ts +31 -0
- package/src/context/index.ts +285 -0
- package/src/context/output.ts +17 -0
- package/src/context/page-urls.ts +81 -0
- package/src/context/style-slots.ts +29 -0
- package/src/context/urls.ts +2 -0
- package/src/elements/bibliography/block.ts +27 -0
- package/src/elements/bibliography/cite.ts +23 -0
- package/src/elements/bibliography/ids.ts +9 -0
- package/src/elements/bibliography/index.ts +9 -0
- package/src/elements/code/contents.ts +18 -0
- package/src/elements/code/index.ts +29 -0
- package/src/elements/collapsible/index.ts +31 -0
- package/src/elements/collapsible/labels.ts +35 -0
- package/src/elements/collapsible/link.ts +11 -0
- package/src/elements/collapsible/sections.ts +39 -0
- package/src/elements/container/attributes.ts +28 -0
- package/src/elements/container/header.ts +27 -0
- package/src/elements/container/index.ts +35 -0
- package/src/elements/container/string-container.ts +40 -0
- package/src/elements/container/string-types.ts +63 -0
- package/src/elements/container/wrappers.ts +32 -0
- package/src/elements/date/format.ts +20 -0
- package/src/elements/{date.ts → date/index.ts} +4 -29
- package/src/elements/date/output.ts +6 -0
- package/src/elements/embed/iframe.ts +8 -0
- package/src/elements/embed/index.ts +28 -0
- package/src/elements/embed/providers.ts +43 -0
- package/src/elements/embed/validation.ts +15 -0
- package/src/elements/embed-block/allowlist.ts +60 -0
- package/src/elements/embed-block/boolean-attributes.ts +38 -0
- package/src/elements/embed-block/iframe.ts +33 -0
- package/src/elements/embed-block/index.ts +31 -0
- package/src/elements/embed-block/sanitize-config.ts +22 -0
- package/src/elements/embed-block/sanitize.ts +44 -0
- package/src/elements/expr/branch.ts +29 -0
- package/src/elements/expr/index.ts +63 -0
- package/src/elements/expr/result.ts +19 -0
- package/src/elements/footnote/body.ts +11 -0
- package/src/elements/footnote/index.ts +35 -0
- package/src/elements/footnote/ref.ts +16 -0
- package/src/elements/html/attributes.ts +24 -0
- package/src/elements/html/index.ts +39 -0
- package/src/elements/html/url.ts +19 -0
- package/src/elements/iframe/attributes.ts +28 -0
- package/src/elements/iframe/index.ts +22 -0
- package/src/elements/iftags/condition.ts +42 -0
- package/src/elements/iftags/index.ts +39 -0
- package/src/elements/iftags/style-slot.ts +23 -0
- package/src/elements/iftags/tokens.ts +36 -0
- package/src/elements/image/alignment.ts +44 -0
- package/src/elements/image/attributes.ts +10 -0
- package/src/elements/image/img-attributes.ts +26 -0
- package/src/elements/image/index.ts +36 -0
- package/src/elements/image/link-href.ts +24 -0
- package/src/elements/image/link.ts +13 -0
- package/src/elements/image/source.ts +16 -0
- package/src/elements/{include.ts → include/index.ts} +5 -13
- package/src/elements/include/missing.ts +15 -0
- package/src/elements/link/anchor-name.ts +6 -0
- package/src/elements/link/anchor.ts +27 -0
- package/src/elements/link/attributes.ts +47 -0
- package/src/elements/link/index.ts +26 -0
- package/src/elements/link/label.ts +23 -0
- package/src/elements/link/target.ts +20 -0
- package/src/elements/list/attributes.ts +19 -0
- package/src/elements/list/definition-list.ts +16 -0
- package/src/elements/list/index.ts +48 -0
- package/src/elements/list/item-rendering.ts +38 -0
- package/src/elements/list/items.ts +61 -0
- package/src/elements/list/no-marker.ts +53 -0
- package/src/elements/list/paragraphs.ts +34 -0
- package/src/elements/list/trim.ts +38 -0
- package/src/elements/math/block.ts +29 -0
- package/src/elements/math/equation-ref.ts +12 -0
- package/src/elements/math/index.ts +14 -0
- package/src/elements/math/inline.ts +19 -0
- package/src/elements/math/latex.ts +27 -0
- package/src/elements/math/source.ts +18 -0
- package/src/elements/module/backlinks.ts +2 -1
- package/src/elements/module/categories.ts +2 -2
- package/src/elements/module/empty-container.ts +10 -0
- package/src/elements/module/index.ts +2 -4
- package/src/elements/module/join-markup.ts +10 -0
- package/src/elements/module/join.ts +2 -7
- package/src/elements/module/listpages.ts +2 -2
- package/src/elements/module/listusers.ts +2 -2
- package/src/elements/module/page-tree.ts +2 -2
- package/src/elements/module/rate-markup.ts +10 -0
- package/src/elements/module/rate.ts +4 -13
- package/src/elements/module/unknown.ts +11 -0
- package/src/elements/tab-view/ids.ts +16 -0
- package/src/elements/tab-view/index.ts +31 -0
- package/src/elements/tab-view/navigation.ts +15 -0
- package/src/elements/tab-view/panels.ts +16 -0
- package/src/elements/table/attributes.ts +23 -0
- package/src/elements/table/cell-attributes.ts +62 -0
- package/src/elements/table/cell.ts +13 -0
- package/src/elements/table/index.ts +27 -0
- package/src/elements/text/email.ts +20 -0
- package/src/elements/text/index.ts +11 -0
- package/src/elements/text/plain.ts +11 -0
- package/src/elements/text/raw.ts +20 -0
- package/src/elements/toc/body.ts +12 -0
- package/src/elements/toc/entries.ts +34 -0
- package/src/elements/toc/frame.ts +27 -0
- package/src/elements/toc/index.ts +17 -0
- package/src/elements/toc/link.ts +26 -0
- package/src/elements/user/index.ts +40 -0
- package/src/elements/user/markup.ts +34 -0
- package/src/elements/user/resolve.ts +6 -0
- package/src/escape/attribute-allowlists.ts +101 -0
- package/src/escape/attributes.ts +62 -0
- package/src/escape/css-color-functions.ts +18 -0
- package/src/escape/css-colors.ts +183 -0
- package/src/escape/css-danger.ts +22 -0
- package/src/escape/css-normalize.ts +54 -0
- package/src/escape/css-style.ts +78 -0
- package/src/escape/css-urls.ts +76 -0
- package/src/escape/css.ts +4 -0
- package/src/escape/email.ts +22 -0
- package/src/escape/html.ts +68 -0
- package/src/escape/index.ts +15 -0
- package/src/escape/url.ts +18 -0
- package/src/libs/highlighter/engine/end-pattern.ts +26 -0
- package/src/libs/highlighter/engine/html.ts +19 -0
- package/src/libs/highlighter/engine/index.ts +3 -0
- package/src/libs/highlighter/engine/keywords.ts +22 -0
- package/src/libs/highlighter/engine/parts.ts +36 -0
- package/src/libs/highlighter/engine/preprocess.ts +10 -0
- package/src/libs/highlighter/engine/render.ts +31 -0
- package/src/libs/highlighter/engine/token.ts +7 -0
- package/src/libs/highlighter/engine/tokenizer.ts +266 -0
- package/src/libs/highlighter/engine/utils.ts +38 -0
- package/src/render/collected-styles.ts +22 -0
- package/src/render/dispatch.ts +181 -0
- package/src/render/index.ts +28 -0
- package/src/render/primitives.ts +17 -0
- package/src/render/style-tag.ts +6 -0
- package/src/render/style.ts +15 -0
- package/src/types.ts +6 -2
- package/src/context.ts +0 -422
- package/src/elements/bibliography.ts +0 -123
- package/src/elements/code.ts +0 -49
- package/src/elements/collapsible.ts +0 -105
- package/src/elements/container.ts +0 -302
- package/src/elements/embed-block.ts +0 -327
- package/src/elements/embed.ts +0 -166
- package/src/elements/expr.ts +0 -102
- package/src/elements/footnote.ts +0 -76
- package/src/elements/html.ts +0 -79
- package/src/elements/iframe.ts +0 -44
- package/src/elements/iftags.ts +0 -118
- package/src/elements/image.ts +0 -154
- package/src/elements/link.ts +0 -201
- package/src/elements/list.ts +0 -241
- package/src/elements/math.ts +0 -177
- package/src/elements/tab-view.ts +0 -75
- package/src/elements/table.ts +0 -101
- package/src/elements/text.ts +0 -57
- package/src/elements/toc.ts +0 -147
- package/src/elements/user.ts +0 -79
- package/src/escape.ts +0 -829
- package/src/libs/highlighter/engine.ts +0 -352
- package/src/render.ts +0 -231
package/src/elements/math.ts
DELETED
|
@@ -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
|
-
}
|
package/src/elements/tab-view.ts
DELETED
|
@@ -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
|
-
}
|
package/src/elements/table.ts
DELETED
|
@@ -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
|
-
}
|
package/src/elements/text.ts
DELETED
|
@@ -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 ` ` 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   in raw content
|
|
36
|
-
ctx.push(escapeHtml(data).replace(/ /g, " "));
|
|
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
|
-
}
|
package/src/elements/toc.ts
DELETED
|
@@ -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
|
-
}
|
package/src/elements/user.ts
DELETED
|
@@ -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
|
-
}
|