@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/image.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
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
|
-
}
|
package/src/elements/link.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderers for Wikidot link elements.
|
|
4
|
-
*
|
|
5
|
-
* Wikidot supports several link syntaxes:
|
|
6
|
-
* - `[[[page-name]]]` -- page link with automatic label
|
|
7
|
-
* - `[[[page-name | label]]]` -- page link with custom label
|
|
8
|
-
* - `[# label]` -- anchor-type link (JavaScript void)
|
|
9
|
-
* - `[http://url label]` -- external URL link
|
|
10
|
-
* - `[[a]]...[[/a]]` -- HTML anchor element with attributes
|
|
11
|
-
* - `[[#anchor-name]]` -- named anchor (bookmark target)
|
|
12
|
-
*
|
|
13
|
-
* All link types are checked for dangerous URL schemes. Page links
|
|
14
|
-
* may receive a `class="newpage"` attribute when the target page does
|
|
15
|
-
* not exist (the standard Wikidot "red link" convention). External
|
|
16
|
-
* links opened in new tabs automatically receive `rel="noopener noreferrer"`
|
|
17
|
-
* to prevent tabnabbing.
|
|
18
|
-
*
|
|
19
|
-
* @module
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import type { LinkData, AnchorData } from "@wdprlib/ast";
|
|
23
|
-
import type { RenderContext } from "../context";
|
|
24
|
-
import { escapeAttr, isDangerousUrl, sanitizeAttributes } from "../escape";
|
|
25
|
-
import { renderElements } from "../render";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Render a link element (`[[[page]]]`, `[url label]`, `[# label]`).
|
|
29
|
-
*
|
|
30
|
-
* The link's `href` is resolved via `ctx.resolvePageLink()`, with an
|
|
31
|
-
* optional `extra` suffix (anchor fragment) appended. Anchor-type links
|
|
32
|
-
* with `javascript:;` are allowed as a special case for Wikidot
|
|
33
|
-
* compatibility; all other dangerous URL schemes are blocked.
|
|
34
|
-
*
|
|
35
|
-
* For page-type links, a `class="newpage"` is added when the
|
|
36
|
-
* `pageExists` resolver indicates the target page does not exist.
|
|
37
|
-
*
|
|
38
|
-
* @param ctx - The current render context.
|
|
39
|
-
* @param data - Link data with link target, label, type, target window, and extra suffix.
|
|
40
|
-
*/
|
|
41
|
-
export function renderLink(ctx: RenderContext, data: LinkData): void {
|
|
42
|
-
let href = ctx.resolvePageLink(data.link);
|
|
43
|
-
|
|
44
|
-
// Append extra (anchor suffix)
|
|
45
|
-
if (data.extra) {
|
|
46
|
-
href += data.extra;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Validate URL scheme
|
|
50
|
-
// Exception: anchor-type links with "javascript:;" are valid Wikidot syntax ([# label])
|
|
51
|
-
const isAnchorJsVoid = data.type === "anchor" && href === "javascript:;";
|
|
52
|
-
if (!isAnchorJsVoid && isDangerousUrl(href)) {
|
|
53
|
-
href = "#invalid-url";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Build <a> tag
|
|
57
|
-
const attrs: string[] = [`href="${escapeAttr(href)}"`];
|
|
58
|
-
|
|
59
|
-
// Add "newpage" class for page links that don't exist
|
|
60
|
-
// Only for page-type links (not direct URLs, anchors, etc.)
|
|
61
|
-
if (data.type === "page" && typeof data.link === "object") {
|
|
62
|
-
const page = data.link.page;
|
|
63
|
-
// Skip newpage class for special pages:
|
|
64
|
-
// - //path (protocol-relative or special routing)
|
|
65
|
-
// - paths with #/ (hash routing like MAIN/#/page)
|
|
66
|
-
// category-prefixed pages (`category:name`) are NOT skipped — pageExists is
|
|
67
|
-
// expected to handle them (e.g. share:<ULID>, private:<ULID>, system:Recent).
|
|
68
|
-
const isSpecialPage = page.startsWith("//") || page.includes("#/");
|
|
69
|
-
if (!isSpecialPage) {
|
|
70
|
-
// For anchor links (page#anchor), check if the page part exists
|
|
71
|
-
const hashIdx = page.indexOf("#");
|
|
72
|
-
const pageToCheck = hashIdx !== -1 ? page.slice(0, hashIdx) : page;
|
|
73
|
-
const pageExists = ctx.page?.pageExists;
|
|
74
|
-
// If pageExists is not provided, assume page doesn't exist (show newpage class)
|
|
75
|
-
const exists = pageExists ? pageExists(pageToCheck) : false;
|
|
76
|
-
if (!exists) {
|
|
77
|
-
attrs.push(`class="newpage"`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Target attribute
|
|
83
|
-
if (data.target) {
|
|
84
|
-
const targetMap: Record<string, string> = {
|
|
85
|
-
"new-tab": "_blank",
|
|
86
|
-
parent: "_parent",
|
|
87
|
-
top: "_top",
|
|
88
|
-
same: "_self",
|
|
89
|
-
};
|
|
90
|
-
const targetValue = targetMap[data.target] ?? "_blank";
|
|
91
|
-
attrs.push(`target="${targetValue}"`);
|
|
92
|
-
// Prevent tabnabbing for _blank targets
|
|
93
|
-
if (targetValue === "_blank") {
|
|
94
|
-
attrs.push(`rel="noopener noreferrer"`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
ctx.push(`<a ${attrs.join(" ")}>`);
|
|
99
|
-
|
|
100
|
-
// Render label
|
|
101
|
-
renderLinkLabel(ctx, data);
|
|
102
|
-
|
|
103
|
-
ctx.push("</a>");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Render the label content of a link element.
|
|
108
|
-
*
|
|
109
|
-
* Label types:
|
|
110
|
-
* - `"page"` -- use the page name as the label text
|
|
111
|
-
* - `{ text: string }` -- use a custom text label
|
|
112
|
-
* - `{ url: string }` -- use the URL itself as the label
|
|
113
|
-
*
|
|
114
|
-
* @param ctx - The current render context.
|
|
115
|
-
* @param data - Link data containing the label descriptor.
|
|
116
|
-
*/
|
|
117
|
-
function renderLinkLabel(ctx: RenderContext, data: LinkData): void {
|
|
118
|
-
if (data.label === "page") {
|
|
119
|
-
// Use page name as label
|
|
120
|
-
if (typeof data.link === "string") {
|
|
121
|
-
ctx.pushEscaped(data.link);
|
|
122
|
-
} else {
|
|
123
|
-
ctx.pushEscaped(data.link.page);
|
|
124
|
-
}
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if ("text" in data.label) {
|
|
129
|
-
ctx.pushEscaped(data.label.text);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if ("url" in data.label) {
|
|
134
|
-
// Use the URL itself as label
|
|
135
|
-
const href = ctx.resolvePageLink(data.link);
|
|
136
|
-
ctx.pushEscaped(data.label.url ?? href);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Render a `[[a]]...[[/a]]` anchor element with full attribute support.
|
|
142
|
-
*
|
|
143
|
-
* Unlike `renderLink`, this handles the block-level anchor syntax where
|
|
144
|
-
* arbitrary attributes can be specified. Dangerous `href` values are
|
|
145
|
-
* replaced with `#invalid-url`. The `target` attribute is mapped from
|
|
146
|
-
* Wikidot's abstract values to HTML values, with tabnabbing protection.
|
|
147
|
-
*
|
|
148
|
-
* @param ctx - The current render context.
|
|
149
|
-
* @param data - Anchor data with attributes, target, and child elements.
|
|
150
|
-
*/
|
|
151
|
-
export function renderAnchor(ctx: RenderContext, data: AnchorData): void {
|
|
152
|
-
const safe = sanitizeAttributes(data.attributes);
|
|
153
|
-
const attrs: string[] = [];
|
|
154
|
-
|
|
155
|
-
// Validate href for dangerous URLs
|
|
156
|
-
if (safe.href && isDangerousUrl(safe.href)) {
|
|
157
|
-
safe.href = "#invalid-url";
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Always include href attribute
|
|
161
|
-
const href = safe.href ?? "";
|
|
162
|
-
attrs.push(`href="${escapeAttr(href)}"`);
|
|
163
|
-
|
|
164
|
-
// Handle target attribute from AST data
|
|
165
|
-
if (data.target) {
|
|
166
|
-
const targetMap: Record<string, string> = {
|
|
167
|
-
"new-tab": "_blank",
|
|
168
|
-
parent: "_parent",
|
|
169
|
-
top: "_top",
|
|
170
|
-
same: "_self",
|
|
171
|
-
};
|
|
172
|
-
const targetValue = targetMap[data.target] ?? "_blank";
|
|
173
|
-
attrs.push(`target="${targetValue}"`);
|
|
174
|
-
// Prevent tabnabbing for _blank targets
|
|
175
|
-
if (targetValue === "_blank") {
|
|
176
|
-
attrs.push(`rel="noopener noreferrer"`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
for (const [key, value] of Object.entries(safe)) {
|
|
181
|
-
if (key === "href" || key === "target") continue; // already handled above
|
|
182
|
-
attrs.push(`${key}="${escapeAttr(value)}"`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
ctx.push(`<a ${attrs.join(" ")}>`);
|
|
186
|
-
renderElements(ctx, data.elements);
|
|
187
|
-
ctx.push("</a>");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Render a named anchor (bookmark target) element: `[[#anchor-name]]`.
|
|
192
|
-
*
|
|
193
|
-
* Produces `<a name="anchor-name"></a>` which serves as a link target
|
|
194
|
-
* for `#anchor-name` URL fragments.
|
|
195
|
-
*
|
|
196
|
-
* @param ctx - The current render context.
|
|
197
|
-
* @param name - The anchor name (fragment identifier).
|
|
198
|
-
*/
|
|
199
|
-
export function renderAnchorName(ctx: RenderContext, name: string): void {
|
|
200
|
-
ctx.push(`<a name="${escapeAttr(name)}"></a>`);
|
|
201
|
-
}
|
package/src/elements/list.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderers for Wikidot ordered/unordered lists and definition lists.
|
|
4
|
-
*
|
|
5
|
-
* Wikidot list syntax uses `*` (unordered) and `#` (ordered) prefixes
|
|
6
|
-
* with indentation controlling nesting depth. The parser produces a
|
|
7
|
-
* recursive `ListData` structure with items that can be either
|
|
8
|
-
* "elements" (content) or "sub-list" (nested list).
|
|
9
|
-
*
|
|
10
|
-
* Special behaviors replicated from Wikidot:
|
|
11
|
-
* - Empty lists are silently dropped (no output at all).
|
|
12
|
-
* - Items with `_noMarker` have `list-style: none` and the first
|
|
13
|
-
* paragraph is unwrapped (no `<p>` tags).
|
|
14
|
-
* - Sub-lists without a preceding content item get an inline hidden `<li>`.
|
|
15
|
-
* - Leading/trailing whitespace-only text nodes are trimmed from items.
|
|
16
|
-
*
|
|
17
|
-
* @module
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { ListData, DefinitionListItem, Element, ContainerData } from "@wdprlib/ast";
|
|
21
|
-
import type { RenderContext } from "../context";
|
|
22
|
-
import { escapeAttr, sanitizeAttributes } from "../escape";
|
|
23
|
-
import { renderElements, renderElement } from "../render";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Trim leading and trailing whitespace-only text elements from an array.
|
|
27
|
-
*
|
|
28
|
-
* @param elements - Array of AST elements.
|
|
29
|
-
* @returns A slice of the array with whitespace-only text nodes removed
|
|
30
|
-
* from both ends.
|
|
31
|
-
*/
|
|
32
|
-
function trimTextElements(elements: Element[]): Element[] {
|
|
33
|
-
if (elements.length === 0) return elements;
|
|
34
|
-
|
|
35
|
-
let start = 0;
|
|
36
|
-
let end = elements.length;
|
|
37
|
-
|
|
38
|
-
// Trim leading whitespace-only text elements
|
|
39
|
-
while (start < end) {
|
|
40
|
-
const el = elements[start]!;
|
|
41
|
-
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
42
|
-
start++;
|
|
43
|
-
} else {
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Trim trailing whitespace-only text elements
|
|
49
|
-
while (end > start) {
|
|
50
|
-
const el = elements[end - 1]!;
|
|
51
|
-
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
52
|
-
end--;
|
|
53
|
-
} else {
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return elements.slice(start, end);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check whether a paragraph element contains only the text `[[/li]]`.
|
|
63
|
-
*
|
|
64
|
-
* The parser sometimes wraps stray `[[/li]]` closing tags in a paragraph.
|
|
65
|
-
* When found as the last paragraph in a `_noMarker` item, the paragraph
|
|
66
|
-
* wrapper is removed to match Wikidot output.
|
|
67
|
-
*
|
|
68
|
-
* @param el - An AST element to check.
|
|
69
|
-
* @returns `true` if the element is a paragraph containing only `[[/li]]`.
|
|
70
|
-
*/
|
|
71
|
-
function isLiCloseTextParagraph(el: Element): boolean {
|
|
72
|
-
if (el.element !== "container") return false;
|
|
73
|
-
const data = el.data as ContainerData;
|
|
74
|
-
if (data.type !== "paragraph") return false;
|
|
75
|
-
// Check if content is just [[/li]] (possibly with whitespace)
|
|
76
|
-
const texts = data.elements
|
|
77
|
-
.filter((e): e is { element: "text"; data: string } => e.element === "text")
|
|
78
|
-
.map((e) => e.data);
|
|
79
|
-
const combined = texts.join("").trim();
|
|
80
|
-
return combined === "[[/li]]";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Render elements for `_noMarker` list items with special paragraph handling.
|
|
85
|
-
*
|
|
86
|
-
* Wikidot treats bare content (without `[[li]]`) differently:
|
|
87
|
-
* - The first paragraph is unwrapped (children rendered without `<p>` tags).
|
|
88
|
-
* - Middle paragraphs retain their `<p>` wrappers.
|
|
89
|
-
* - The last paragraph is unwrapped if it contains only `[[/li]]` text.
|
|
90
|
-
*
|
|
91
|
-
* @param ctx - The current render context.
|
|
92
|
-
* @param elements - The list item's child elements.
|
|
93
|
-
*/
|
|
94
|
-
function renderNoMarkerElements(ctx: RenderContext, elements: Element[]): void {
|
|
95
|
-
const trimmed = trimTextElements(elements);
|
|
96
|
-
if (trimmed.length === 0) return;
|
|
97
|
-
|
|
98
|
-
// Find paragraph indices
|
|
99
|
-
const paragraphIndices: number[] = [];
|
|
100
|
-
for (let i = 0; i < trimmed.length; i++) {
|
|
101
|
-
const el = trimmed[i]!;
|
|
102
|
-
if (el.element === "container" && (el.data as ContainerData).type === "paragraph") {
|
|
103
|
-
paragraphIndices.push(i);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// If no paragraphs, render normally
|
|
108
|
-
if (paragraphIndices.length === 0) {
|
|
109
|
-
renderElements(ctx, trimmed);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const firstParagraphIdx = paragraphIndices[0]!;
|
|
114
|
-
const lastParagraphIdx = paragraphIndices[paragraphIndices.length - 1]!;
|
|
115
|
-
|
|
116
|
-
for (let i = 0; i < trimmed.length; i++) {
|
|
117
|
-
const el = trimmed[i]!;
|
|
118
|
-
if (el.element === "container" && (el.data as ContainerData).type === "paragraph") {
|
|
119
|
-
const data = el.data as ContainerData;
|
|
120
|
-
// First paragraph: unwrap
|
|
121
|
-
if (i === firstParagraphIdx) {
|
|
122
|
-
renderElements(ctx, data.elements);
|
|
123
|
-
}
|
|
124
|
-
// Last paragraph if it's [[/li]]: unwrap
|
|
125
|
-
else if (i === lastParagraphIdx && isLiCloseTextParagraph(el)) {
|
|
126
|
-
renderElements(ctx, data.elements);
|
|
127
|
-
}
|
|
128
|
-
// Other paragraphs: keep <p> tags
|
|
129
|
-
else {
|
|
130
|
-
ctx.push("<p>");
|
|
131
|
-
renderElements(ctx, data.elements);
|
|
132
|
-
ctx.push("</p>");
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
renderElement(ctx, el);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Render an ordered or unordered list.
|
|
142
|
-
*
|
|
143
|
-
* Wikidot drops empty lists entirely (no HTML output). Sub-lists
|
|
144
|
-
* following a content item are rendered inside the same `<li>`.
|
|
145
|
-
* Sub-lists without a preceding content item get a hidden `<li>` wrapper.
|
|
146
|
-
*
|
|
147
|
-
* @param ctx - The current render context.
|
|
148
|
-
* @param data - List data with type (numbered/bulleted), items, and attributes.
|
|
149
|
-
*/
|
|
150
|
-
export function renderList(ctx: RenderContext, data: ListData): void {
|
|
151
|
-
// Wikidot behavior: empty lists or lists with only empty items are ignored
|
|
152
|
-
// and converted to <br />
|
|
153
|
-
const hasContent = data.items.some((item) => {
|
|
154
|
-
if (item["item-type"] === "sub-list") return true;
|
|
155
|
-
if (item["item-type"] === "elements") {
|
|
156
|
-
const trimmed = trimTextElements(item.elements);
|
|
157
|
-
return trimmed.length > 0;
|
|
158
|
-
}
|
|
159
|
-
return false;
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (!hasContent) {
|
|
163
|
-
// Empty list - Wikidot outputs nothing (just whitespace)
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const tag = data.type === "numbered" ? "ol" : "ul";
|
|
168
|
-
ctx.push(`<${tag}${renderListAttrs(data.attributes)}>`);
|
|
169
|
-
|
|
170
|
-
const items = data.items;
|
|
171
|
-
let i = 0;
|
|
172
|
-
while (i < items.length) {
|
|
173
|
-
const item = items[i]!;
|
|
174
|
-
if (item["item-type"] === "elements") {
|
|
175
|
-
// Check for _noMarker flag (bare content without [[li]])
|
|
176
|
-
const hasNoMarker = item.attributes._noMarker === "true";
|
|
177
|
-
const styleAttr = hasNoMarker ? ' style="list-style: none"' : "";
|
|
178
|
-
ctx.push(`<li${renderListAttrs(item.attributes)}${styleAttr}>`);
|
|
179
|
-
// Trim leading/trailing whitespace from li content (Wikidot behavior)
|
|
180
|
-
if (hasNoMarker) {
|
|
181
|
-
// Special handling for bare content paragraphs
|
|
182
|
-
renderNoMarkerElements(ctx, item.elements);
|
|
183
|
-
} else {
|
|
184
|
-
renderElements(ctx, trimTextElements(item.elements));
|
|
185
|
-
}
|
|
186
|
-
// Consume following sub-lists inside this <li>
|
|
187
|
-
while (i + 1 < items.length && items[i + 1]!["item-type"] === "sub-list") {
|
|
188
|
-
i++;
|
|
189
|
-
const subItem = items[i] as { "item-type": "sub-list"; data: ListData };
|
|
190
|
-
renderList(ctx, subItem.data);
|
|
191
|
-
}
|
|
192
|
-
ctx.push("</li>");
|
|
193
|
-
} else {
|
|
194
|
-
// Sub-list without preceding elements item - hide bullet/number
|
|
195
|
-
const subItem = item as { "item-type": "sub-list"; data: ListData };
|
|
196
|
-
ctx.push(`<li style="list-style: none; display: inline">`);
|
|
197
|
-
renderList(ctx, subItem.data);
|
|
198
|
-
ctx.push("</li>");
|
|
199
|
-
}
|
|
200
|
-
i++;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
ctx.push(`</${tag}>`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Sanitize and render list-specific attributes, excluding internal `_`-prefixed keys.
|
|
208
|
-
*
|
|
209
|
-
* @param attributes - Raw attribute map from the AST.
|
|
210
|
-
* @returns An HTML attribute string with leading space, or `""` if empty.
|
|
211
|
-
*/
|
|
212
|
-
function renderListAttrs(attributes: Record<string, string>): string {
|
|
213
|
-
const safe = sanitizeAttributes(attributes);
|
|
214
|
-
let result = "";
|
|
215
|
-
for (const [key, value] of Object.entries(safe)) {
|
|
216
|
-
if (key.startsWith("_")) continue;
|
|
217
|
-
result += ` ${key}="${escapeAttr(value)}"`;
|
|
218
|
-
}
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Render a definition list (`:`-prefixed items in Wikidot markup).
|
|
224
|
-
*
|
|
225
|
-
* Produces `<dl>` with `<dt>`/`<dd>` pairs for each definition item.
|
|
226
|
-
*
|
|
227
|
-
* @param ctx - The current render context.
|
|
228
|
-
* @param items - Array of definition list items, each with key and value elements.
|
|
229
|
-
*/
|
|
230
|
-
export function renderDefinitionList(ctx: RenderContext, items: DefinitionListItem[]): void {
|
|
231
|
-
ctx.push("<dl>");
|
|
232
|
-
for (const item of items) {
|
|
233
|
-
ctx.push("<dt>");
|
|
234
|
-
renderElements(ctx, item.key);
|
|
235
|
-
ctx.push("</dt>");
|
|
236
|
-
ctx.push("<dd>");
|
|
237
|
-
renderElements(ctx, item.value);
|
|
238
|
-
ctx.push("</dd>");
|
|
239
|
-
}
|
|
240
|
-
ctx.push("</dl>");
|
|
241
|
-
}
|