@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for `[[html]]...[[/html]]` blocks in Wikidot markup.
|
|
4
|
+
*
|
|
5
|
+
* HTML blocks are rendered as sandboxed iframes. The iframe `src` URL
|
|
6
|
+
* can be provided by a resolver callback; when absent, a deterministic
|
|
7
|
+
* default URL is generated from the content hash.
|
|
8
|
+
*
|
|
9
|
+
* The actual HTML content is not inlined into the page -- it is served
|
|
10
|
+
* separately at the iframe URL. The runtime `html-block` module handles
|
|
11
|
+
* auto-resizing of the iframe via `postMessage`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { HtmlData } from "@wdprlib/ast";
|
|
17
|
+
import type { RenderContext } from "../context";
|
|
18
|
+
import { escapeAttr, sanitizeStyleValue } from "../escape";
|
|
19
|
+
import { syncHashSha1 } from "../hash";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a default iframe `src` URL for an HTML block when no
|
|
23
|
+
* resolver callback is provided.
|
|
24
|
+
*
|
|
25
|
+
* The URL follows Wikidot's pattern: `/{pageName}/html/{hash}-{nonce}`.
|
|
26
|
+
* The hash is a SHA-1-length FNV hash of the content, and the nonce
|
|
27
|
+
* is derived from the content length and first hash character to
|
|
28
|
+
* provide additional uniqueness.
|
|
29
|
+
*
|
|
30
|
+
* @param pageName - The current page name (may be empty).
|
|
31
|
+
* @param contents - Raw HTML content to hash.
|
|
32
|
+
* @returns A URL path string, always starting with `/`.
|
|
33
|
+
*/
|
|
34
|
+
function generateDefaultUrl(pageName: string, contents: string): string {
|
|
35
|
+
const hash = syncHashSha1(contents);
|
|
36
|
+
const nonce = BigInt(contents.length) * 1000000000n + BigInt(hash.charCodeAt(0)) * 100000000n;
|
|
37
|
+
// Ensure leading slash even when pageName is empty (avoid protocol-relative URL)
|
|
38
|
+
const path = pageName ? `/${pageName}/html/${hash}-${nonce}` : `/html/${hash}-${nonce}`;
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render a `[[html]]` block as an iframe wrapped in a `<p>` element.
|
|
44
|
+
*
|
|
45
|
+
* The iframe uses `class="html-block-iframe"` so the runtime can
|
|
46
|
+
* identify it for auto-resize. An optional `sandbox` attribute and
|
|
47
|
+
* custom `style` attribute (from `[[html style="..."]]`) are applied.
|
|
48
|
+
*
|
|
49
|
+
* @param ctx - The current render context.
|
|
50
|
+
* @param data - HTML block data containing the raw HTML contents and optional style.
|
|
51
|
+
*/
|
|
52
|
+
export function renderHtmlBlock(ctx: RenderContext, data: HtmlData): void {
|
|
53
|
+
// Settings-level enforcement boundary: skip rendering entirely when
|
|
54
|
+
// `[[html]]` is disabled. Placed before any counter advance or
|
|
55
|
+
// resolver invocation so a disabled-but-still-in-AST block (manually
|
|
56
|
+
// built trees, cached ASTs, foreign parsers) has no observable effect.
|
|
57
|
+
if (ctx.settings.allowHtmlBlocks === false) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const index = ctx.nextHtmlBlockIndex();
|
|
62
|
+
const pageName = ctx.page?.pageName ?? "";
|
|
63
|
+
|
|
64
|
+
// Use callback URL if provided, otherwise generate default URL
|
|
65
|
+
const callbackUrl = ctx.options.resolvers?.htmlBlockUrl?.(index);
|
|
66
|
+
const src = callbackUrl || generateDefaultUrl(pageName, data.contents);
|
|
67
|
+
|
|
68
|
+
// Build sandbox attribute (null/undefined = no sandbox, Wikidot compatible)
|
|
69
|
+
const sandbox = ctx.options.htmlBlockSandbox;
|
|
70
|
+
const sandboxAttr = sandbox ? ` sandbox="${escapeAttr(sandbox)}"` : "";
|
|
71
|
+
|
|
72
|
+
// Build style attribute (from [[html style="..."]]) with CSS injection protection
|
|
73
|
+
const styleAttr = data.style ? ` style="${escapeAttr(sanitizeStyleValue(data.style))}"` : "";
|
|
74
|
+
|
|
75
|
+
// Wikidot wraps html block iframe in a paragraph
|
|
76
|
+
ctx.push(
|
|
77
|
+
`<p><iframe src="${escapeAttr(src)}"${sandboxAttr} allowtransparency="true" frameborder="0" class="html-block-iframe"${styleAttr}></iframe></p>`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for `[[iframe URL]]` inline iframe elements.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `[[embed]]` blocks, `[[iframe]]` directly references a URL.
|
|
6
|
+
* The URL is checked for dangerous schemes, and standard iframe
|
|
7
|
+
* attributes (align, frameborder, height, etc.) are extracted from the
|
|
8
|
+
* AST's attribute map and rendered with proper escaping.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { IframeData } from "@wdprlib/ast";
|
|
14
|
+
import type { RenderContext } from "../context";
|
|
15
|
+
import { escapeAttr, isDangerousUrl, sanitizeStyleValue } from "../escape";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render an `[[iframe URL]]` element.
|
|
19
|
+
*
|
|
20
|
+
* The iframe is wrapped in a `<p>` element to match Wikidot's output.
|
|
21
|
+
* Standard iframe attributes are rendered from the AST's attribute map,
|
|
22
|
+
* with the `style` attribute sanitized against CSS injection. Dangerous
|
|
23
|
+
* URL schemes are replaced with `#invalid-url`.
|
|
24
|
+
*
|
|
25
|
+
* @param ctx - The current render context.
|
|
26
|
+
* @param data - Iframe data containing the URL and attribute map.
|
|
27
|
+
*/
|
|
28
|
+
export function renderIframe(ctx: RenderContext, data: IframeData): void {
|
|
29
|
+
const url = isDangerousUrl(data.url) ? "#invalid-url" : data.url;
|
|
30
|
+
const attrs: string[] = [`src="${escapeAttr(url)}"`];
|
|
31
|
+
|
|
32
|
+
// Standard iframe attributes from the attributes map
|
|
33
|
+
const iframeAttrs = ["align", "frameborder", "height", "scrolling", "width", "class", "style"];
|
|
34
|
+
for (const attr of iframeAttrs) {
|
|
35
|
+
let value = data.attributes[attr] ?? "";
|
|
36
|
+
// Sanitize style attribute to prevent CSS injection
|
|
37
|
+
if (attr === "style") {
|
|
38
|
+
value = sanitizeStyleValue(value);
|
|
39
|
+
}
|
|
40
|
+
attrs.push(`${attr}="${escapeAttr(value)}"`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ctx.push(`<p><iframe ${attrs.join(" ")}></iframe></p>`);
|
|
44
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for `[[iftags]]...[[/iftags]]` conditional blocks.
|
|
4
|
+
*
|
|
5
|
+
* Wikidot's `iftags` construct conditionally renders content based on
|
|
6
|
+
* whether the current page's tags match a condition string. The condition
|
|
7
|
+
* supports three kinds of tag tokens:
|
|
8
|
+
*
|
|
9
|
+
* - `+tag` -- required: the tag must be present
|
|
10
|
+
* - `-tag` -- excluded: the tag must NOT be present
|
|
11
|
+
* - `tag` (no prefix) -- optional group: at least one unprefixed tag must be present
|
|
12
|
+
*
|
|
13
|
+
* All three categories must independently be satisfied for the condition
|
|
14
|
+
* to evaluate to true.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { IfTagsData } from "@wdprlib/ast";
|
|
20
|
+
import type { RenderContext } from "../context";
|
|
21
|
+
import { renderElements } from "../render";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Evaluate an iftags condition string against a list of page tags.
|
|
25
|
+
*
|
|
26
|
+
* The condition is a space-separated list of tokens. All required tags
|
|
27
|
+
* (`+tag`) must be present, all excluded tags (`-tag`) must be absent,
|
|
28
|
+
* and at least one optional tag (bare `tag`) must be present (if any
|
|
29
|
+
* optional tags are specified).
|
|
30
|
+
*
|
|
31
|
+
* An empty condition always evaluates to `false`.
|
|
32
|
+
*
|
|
33
|
+
* @param condition - The condition string (e.g. `"+scp -joke tale"`).
|
|
34
|
+
* @param pageTags - Array of tags currently assigned to the page.
|
|
35
|
+
* @returns `true` if the condition is satisfied.
|
|
36
|
+
*/
|
|
37
|
+
function evaluateIfTagsCondition(condition: string, pageTags: string[]): boolean {
|
|
38
|
+
const pageTagSet = new Set(pageTags.map((t) => t.toLowerCase()));
|
|
39
|
+
const tokens = condition.split(/\s+/).filter(Boolean);
|
|
40
|
+
|
|
41
|
+
// Empty condition = never show
|
|
42
|
+
if (tokens.length === 0) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const required: string[] = [];
|
|
47
|
+
const excluded: string[] = [];
|
|
48
|
+
const optional: string[] = [];
|
|
49
|
+
|
|
50
|
+
for (const token of tokens) {
|
|
51
|
+
if (token.startsWith("+")) {
|
|
52
|
+
const tag = token.slice(1).toLowerCase();
|
|
53
|
+
if (tag) required.push(tag);
|
|
54
|
+
} else if (token.startsWith("-")) {
|
|
55
|
+
const tag = token.slice(1).toLowerCase();
|
|
56
|
+
if (tag) excluded.push(tag);
|
|
57
|
+
} else {
|
|
58
|
+
optional.push(token.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If all tokens had empty tag names (e.g. "+" or "-"), treat as empty condition
|
|
63
|
+
if (required.length === 0 && excluded.length === 0 && optional.length === 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// All required tags must be present
|
|
68
|
+
for (const tag of required) {
|
|
69
|
+
if (!pageTagSet.has(tag)) return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// All excluded tags must NOT be present
|
|
73
|
+
for (const tag of excluded) {
|
|
74
|
+
if (pageTagSet.has(tag)) return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If there are optional tags, at least one must be present
|
|
78
|
+
if (optional.length > 0) {
|
|
79
|
+
const hasAnyOptional = optional.some((tag) => pageTagSet.has(tag));
|
|
80
|
+
if (!hasAnyOptional) return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Render an `[[iftags]]` block.
|
|
88
|
+
*
|
|
89
|
+
* Evaluates the condition against the page's tags (from `ctx.page.tags`).
|
|
90
|
+
* If no page tags are available, an empty array is used (all conditions
|
|
91
|
+
* requiring present tags will fail).
|
|
92
|
+
*
|
|
93
|
+
* @param ctx - The current render context.
|
|
94
|
+
* @param data - IfTags data with condition string and child elements.
|
|
95
|
+
*/
|
|
96
|
+
export function renderIfTags(ctx: RenderContext, data: IfTagsData): void {
|
|
97
|
+
const pageTags = ctx.page?.tags ?? [];
|
|
98
|
+
|
|
99
|
+
if (evaluateIfTagsCondition(data.condition, pageTags)) {
|
|
100
|
+
const prev = ctx.renderInlineStyles;
|
|
101
|
+
ctx.renderInlineStyles = true;
|
|
102
|
+
|
|
103
|
+
// If a style slot was assigned during resolve, collect styles into
|
|
104
|
+
// it so they appear at the correct source-order position in the
|
|
105
|
+
// final output. Otherwise fall back to inline rendering.
|
|
106
|
+
const slotId = (data as IfTagsData & { _styleSlot?: number })._styleSlot;
|
|
107
|
+
if (slotId !== undefined) {
|
|
108
|
+
ctx.enterStyleSlot(slotId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
renderElements(ctx, data.elements);
|
|
112
|
+
|
|
113
|
+
if (slotId !== undefined) {
|
|
114
|
+
ctx.exitStyleSlot();
|
|
115
|
+
}
|
|
116
|
+
ctx.renderInlineStyles = prev;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for Wikidot image elements (`[[image source]]` and `[[f<image source]]`).
|
|
4
|
+
*
|
|
5
|
+
* Images can be sourced from URLs, page-attached files, or cross-site
|
|
6
|
+
* files. The renderer resolves the source to a URL, sanitizes all
|
|
7
|
+
* attributes, optionally wraps the image in a link (`link` attribute),
|
|
8
|
+
* and optionally wraps everything in an alignment container div.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ImageSource, ImageData } from "@wdprlib/ast";
|
|
14
|
+
import type { RenderContext } from "../context";
|
|
15
|
+
import { escapeAttr, isDangerousUrl, sanitizeAttributes } from "../escape";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render an image element with optional link wrapper and alignment container.
|
|
19
|
+
*
|
|
20
|
+
* Processing steps:
|
|
21
|
+
* 1. Resolve the image source to a URL via `ctx.resolveImageSource()`.
|
|
22
|
+
* 2. Sanitize user-supplied attributes.
|
|
23
|
+
* 3. Build the `<img>` tag with safe attributes.
|
|
24
|
+
* 4. Optionally wrap in an `<a>` tag if a link target is specified.
|
|
25
|
+
* 5. Optionally wrap in a `<div class="image-container ...">` for alignment.
|
|
26
|
+
*
|
|
27
|
+
* Dangerous URLs are replaced with `#invalid-url`. Local paths blocked
|
|
28
|
+
* by settings cause the entire image to be silently dropped.
|
|
29
|
+
*
|
|
30
|
+
* @param ctx - The current render context.
|
|
31
|
+
* @param data - Image element data with source, attributes, optional link, and alignment.
|
|
32
|
+
*/
|
|
33
|
+
export function renderImage(ctx: RenderContext, data: ImageData): void {
|
|
34
|
+
let src = ctx.resolveImageSource(data.source);
|
|
35
|
+
if (src === null) return; // Local path blocked by settings
|
|
36
|
+
if (isDangerousUrl(src)) {
|
|
37
|
+
src = "#invalid-url";
|
|
38
|
+
}
|
|
39
|
+
const safeAttrs = sanitizeAttributes(data.attributes);
|
|
40
|
+
const alt = safeAttrs.alt ?? getFilenameFromSource(data.source);
|
|
41
|
+
const className = safeAttrs.class ?? "image";
|
|
42
|
+
|
|
43
|
+
// Build img attributes
|
|
44
|
+
const imgAttrs: string[] = [`src="${escapeAttr(src)}"`];
|
|
45
|
+
|
|
46
|
+
// Custom attributes (title, style, etc.) before alt/class
|
|
47
|
+
// Skip src/srcset to prevent override of resolved source
|
|
48
|
+
for (const [key, value] of Object.entries(safeAttrs)) {
|
|
49
|
+
if (key === "alt" || key === "class" || key === "src" || key === "srcset") continue;
|
|
50
|
+
imgAttrs.push(`${key}="${escapeAttr(value)}"`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
imgAttrs.push(`alt="${escapeAttr(alt)}"`);
|
|
54
|
+
|
|
55
|
+
// Only override class if not custom
|
|
56
|
+
if (!safeAttrs.class) {
|
|
57
|
+
imgAttrs.push(`class="${escapeAttr(className)}"`);
|
|
58
|
+
} else {
|
|
59
|
+
imgAttrs.push(`class="${escapeAttr(safeAttrs.class)}"`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const imgTag = `<img ${imgAttrs.join(" ")} />`;
|
|
63
|
+
|
|
64
|
+
// Wrap in link if needed
|
|
65
|
+
let output = imgTag;
|
|
66
|
+
if (data.link) {
|
|
67
|
+
let href: string;
|
|
68
|
+
if (typeof data.link === "string") {
|
|
69
|
+
// Add leading slash for page links (not URLs or anchors)
|
|
70
|
+
if (
|
|
71
|
+
!data.link.startsWith("/") &&
|
|
72
|
+
!data.link.startsWith("#") &&
|
|
73
|
+
!data.link.startsWith("http://") &&
|
|
74
|
+
!data.link.startsWith("https://")
|
|
75
|
+
) {
|
|
76
|
+
href = `/${data.link}`;
|
|
77
|
+
} else {
|
|
78
|
+
href = data.link;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
href = `/${data.link.page}`;
|
|
82
|
+
}
|
|
83
|
+
if (isDangerousUrl(href)) {
|
|
84
|
+
href = "#invalid-url";
|
|
85
|
+
}
|
|
86
|
+
output = `<a href="${escapeAttr(href)}">${imgTag}</a>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Wrap in alignment container if needed
|
|
90
|
+
if (data.alignment) {
|
|
91
|
+
const alignClass = getAlignmentClass(data.alignment.align, data.alignment.float);
|
|
92
|
+
ctx.push(`<div class="image-container ${alignClass}">`);
|
|
93
|
+
ctx.push(output);
|
|
94
|
+
ctx.push("</div>");
|
|
95
|
+
} else {
|
|
96
|
+
ctx.push(output);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Map an alignment direction and float flag to a Wikidot CSS class name.
|
|
102
|
+
*
|
|
103
|
+
* @param align - Alignment direction (`"left"`, `"right"`, `"center"`).
|
|
104
|
+
* @param isFloat - Whether the image uses float positioning.
|
|
105
|
+
* @returns CSS class name (e.g. `"floatleft"`, `"aligncenter"`).
|
|
106
|
+
*/
|
|
107
|
+
function getAlignmentClass(align: string, isFloat: boolean): string {
|
|
108
|
+
if (isFloat) {
|
|
109
|
+
switch (align) {
|
|
110
|
+
case "left":
|
|
111
|
+
return "floatleft";
|
|
112
|
+
case "right":
|
|
113
|
+
return "floatright";
|
|
114
|
+
case "center":
|
|
115
|
+
return "floatcenter";
|
|
116
|
+
default:
|
|
117
|
+
return `float${align}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
switch (align) {
|
|
121
|
+
case "left":
|
|
122
|
+
return "alignleft";
|
|
123
|
+
case "right":
|
|
124
|
+
return "alignright";
|
|
125
|
+
case "center":
|
|
126
|
+
return "aligncenter";
|
|
127
|
+
default:
|
|
128
|
+
return `align${align}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract a filename from an image source for use as the default `alt` text.
|
|
134
|
+
*
|
|
135
|
+
* For URL sources, the last path segment is returned. For file-type sources,
|
|
136
|
+
* the file name field is returned directly.
|
|
137
|
+
*
|
|
138
|
+
* @param source - The image source descriptor.
|
|
139
|
+
* @returns The extracted filename string.
|
|
140
|
+
*/
|
|
141
|
+
function getFilenameFromSource(source: ImageSource): string {
|
|
142
|
+
switch (source.type) {
|
|
143
|
+
case "url": {
|
|
144
|
+
const parts = source.data.split("/");
|
|
145
|
+
return parts[parts.length - 1] ?? source.data;
|
|
146
|
+
}
|
|
147
|
+
case "file1":
|
|
148
|
+
return source.data.file;
|
|
149
|
+
case "file2":
|
|
150
|
+
return source.data.file;
|
|
151
|
+
case "file3":
|
|
152
|
+
return source.data.file;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for `[[include page-name]]` transclusion elements.
|
|
4
|
+
*
|
|
5
|
+
* Includes are resolved by the parser before rendering: if the target
|
|
6
|
+
* page exists, its parsed elements are injected into the AST. At render
|
|
7
|
+
* time, the renderer either outputs the pre-resolved elements or shows
|
|
8
|
+
* a Wikidot-compatible error message with a "create it now" link.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { IncludeData } from "@wdprlib/ast";
|
|
14
|
+
import type { RenderContext } from "../context";
|
|
15
|
+
import { escapeAttr, escapeHtml } from "../escape";
|
|
16
|
+
import { renderElements } from "../render";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render an `[[include]]` element.
|
|
20
|
+
*
|
|
21
|
+
* If the include was resolved by the parser (i.e., `data.elements` is
|
|
22
|
+
* non-empty), the resolved elements are rendered directly. Otherwise,
|
|
23
|
+
* a Wikidot-compatible error block with a "create it now" link is shown.
|
|
24
|
+
*
|
|
25
|
+
* @param ctx - The current render context.
|
|
26
|
+
* @param data - Include data with the target page location and resolved elements.
|
|
27
|
+
*/
|
|
28
|
+
export function renderInclude(ctx: RenderContext, data: IncludeData): void {
|
|
29
|
+
// If elements is empty, the include was not resolved - show error
|
|
30
|
+
if (data.elements.length === 0) {
|
|
31
|
+
// Wikidot normalizes page names to lowercase
|
|
32
|
+
const pageName = data.location.page.toLowerCase();
|
|
33
|
+
// Encode page name for URL path (/ should not be encoded, but special chars should)
|
|
34
|
+
const encodedPageName = pageName.replace(/[^a-z0-9\-_:/]/g, (c) => encodeURIComponent(c));
|
|
35
|
+
// Prevent protocol-relative URLs
|
|
36
|
+
const safePath = encodedPageName.startsWith("/") ? encodedPageName.slice(1) : encodedPageName;
|
|
37
|
+
ctx.push(
|
|
38
|
+
`<div class="error-block"><p>Included page "${escapeHtml(pageName)}" does not exist (<a href="/${escapeAttr(safePath)}/edit/true">create it now</a>)</p></div>`,
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
renderElements(ctx, data.elements);
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Barrel export for all Wikidot element renderers.
|
|
4
|
+
*
|
|
5
|
+
* Each renderer function accepts a `RenderContext` and the
|
|
6
|
+
* element-specific AST data, then pushes the resulting HTML fragments
|
|
7
|
+
* into the context's output buffer.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { renderContainer } from "./container";
|
|
13
|
+
export { renderText, renderRaw, renderEmail } from "./text";
|
|
14
|
+
export { renderLink, renderAnchor, renderAnchorName } from "./link";
|
|
15
|
+
export { renderImage } from "./image";
|
|
16
|
+
export { renderList, renderDefinitionList } from "./list";
|
|
17
|
+
export { renderTable } from "./table";
|
|
18
|
+
export { renderCollapsible } from "./collapsible";
|
|
19
|
+
export { renderCode } from "./code";
|
|
20
|
+
export { renderTabView } from "./tab-view";
|
|
21
|
+
export { renderFootnoteRef, renderFootnoteBlock } from "./footnote";
|
|
22
|
+
export { renderMath, renderMathInline } from "./math";
|
|
23
|
+
export { renderModule } from "./module/index";
|
|
24
|
+
export { renderEmbed } from "./embed";
|
|
25
|
+
export { renderUser } from "./user";
|
|
26
|
+
export { renderBibliographyCite, renderBibliographyBlock } from "./bibliography";
|
|
27
|
+
export { renderTableOfContents } from "./toc";
|
|
28
|
+
export { renderLineBreaks } from "./line-break";
|
|
29
|
+
export { renderClearFloat } from "./clear-float";
|
|
30
|
+
export { renderIframe } from "./iframe";
|
|
31
|
+
export { renderHtmlBlock } from "./html";
|
|
32
|
+
export { renderInclude } from "./include";
|
|
33
|
+
export { renderIfTags } from "./iftags";
|
|
34
|
+
export { renderColor } from "./color";
|
|
35
|
+
export { renderDate } from "./date";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for explicit line breaks (`_` at end of line in Wikidot markup).
|
|
4
|
+
*
|
|
5
|
+
* Multiple consecutive line-break elements produce multiple `<br />` tags.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RenderContext } from "../context";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render one or more consecutive line breaks as `<br />` tags.
|
|
14
|
+
*
|
|
15
|
+
* @param ctx - The current render context.
|
|
16
|
+
* @param count - Number of `<br />` tags to emit.
|
|
17
|
+
*/
|
|
18
|
+
export function renderLineBreaks(ctx: RenderContext, count: number): void {
|
|
19
|
+
for (let i = 0; i < count; i++) {
|
|
20
|
+
ctx.push("<br />");
|
|
21
|
+
}
|
|
22
|
+
}
|