@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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ImageSource } from "@wdprlib/ast";
|
|
2
|
+
import { getImageAttributes } from "./img-attributes";
|
|
3
|
+
|
|
4
|
+
export function buildImageTag(
|
|
5
|
+
src: string,
|
|
6
|
+
source: ImageSource,
|
|
7
|
+
attributes: Record<string, string>,
|
|
8
|
+
): string {
|
|
9
|
+
return `<img ${getImageAttributes(src, source, attributes).join(" ")} />`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ImageSource } from "@wdprlib/ast";
|
|
2
|
+
import { escapeAttr, sanitizeAttributes } from "../../escape";
|
|
3
|
+
import { getFilenameFromSource } from "./source";
|
|
4
|
+
|
|
5
|
+
export function getImageAttributes(
|
|
6
|
+
src: string,
|
|
7
|
+
source: ImageSource,
|
|
8
|
+
attributes: Record<string, string>,
|
|
9
|
+
): string[] {
|
|
10
|
+
const safeAttrs = sanitizeAttributes(attributes);
|
|
11
|
+
const imgAttrs: string[] = [`src="${escapeAttr(src)}"`];
|
|
12
|
+
|
|
13
|
+
appendPassedImageAttributes(imgAttrs, safeAttrs);
|
|
14
|
+
imgAttrs.push(`alt="${escapeAttr(safeAttrs.alt ?? getFilenameFromSource(source))}"`);
|
|
15
|
+
imgAttrs.push(`class="${escapeAttr(safeAttrs.class ?? "image")}"`);
|
|
16
|
+
|
|
17
|
+
return imgAttrs;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function appendPassedImageAttributes(imgAttrs: string[], safeAttrs: Record<string, string>): void {
|
|
21
|
+
for (const key in safeAttrs) {
|
|
22
|
+
if (key === "alt" || key === "class" || key === "src" || key === "srcset") continue;
|
|
23
|
+
const value = safeAttrs[key]!;
|
|
24
|
+
imgAttrs.push(`${key}="${escapeAttr(value)}"`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 { ImageData } from "@wdprlib/ast";
|
|
14
|
+
import type { RenderContext } from "../../context";
|
|
15
|
+
import { isDangerousUrl } from "../../escape";
|
|
16
|
+
import { buildImageTag } from "./attributes";
|
|
17
|
+
import { pushAlignedImage } from "./alignment";
|
|
18
|
+
import { wrapImageLink } from "./link";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render an image element with optional link wrapper and alignment container.
|
|
22
|
+
*
|
|
23
|
+
* Dangerous URLs are replaced with `#invalid-url`. Local paths blocked
|
|
24
|
+
* by settings cause the entire image to be silently dropped.
|
|
25
|
+
*/
|
|
26
|
+
export function renderImage(ctx: RenderContext, data: ImageData): void {
|
|
27
|
+
let src = ctx.resolveImageSource(data.source);
|
|
28
|
+
if (src === null) return;
|
|
29
|
+
if (isDangerousUrl(src)) {
|
|
30
|
+
src = "#invalid-url";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const imageTag = buildImageTag(src, data.source, data.attributes);
|
|
34
|
+
const output = wrapImageLink(imageTag, data.link);
|
|
35
|
+
pushAlignedImage(ctx, output, data.alignment);
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LinkLocation } from "@wdprlib/ast";
|
|
2
|
+
import { isDangerousUrl } from "../../escape";
|
|
3
|
+
|
|
4
|
+
export function resolveSafeImageLinkHref(link: LinkLocation): string {
|
|
5
|
+
const href = resolveImageLinkHref(link);
|
|
6
|
+
return isDangerousUrl(href) ? "#invalid-url" : href;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function resolveImageLinkHref(link: LinkLocation): string {
|
|
10
|
+
if (typeof link !== "string") {
|
|
11
|
+
return `/${link.page}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
!link.startsWith("/") &&
|
|
16
|
+
!link.startsWith("#") &&
|
|
17
|
+
!link.startsWith("http://") &&
|
|
18
|
+
!link.startsWith("https://")
|
|
19
|
+
) {
|
|
20
|
+
return `/${link}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return link;
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LinkLocation } from "@wdprlib/ast";
|
|
2
|
+
import { escapeAttr } from "../../escape";
|
|
3
|
+
import { resolveSafeImageLinkHref } from "./link-href";
|
|
4
|
+
|
|
5
|
+
export function wrapImageLink(imageTag: string, link: LinkLocation | null): string {
|
|
6
|
+
if (!link) {
|
|
7
|
+
return imageTag;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const href = resolveSafeImageLinkHref(link);
|
|
11
|
+
|
|
12
|
+
return `<a href="${escapeAttr(href)}">${imageTag}</a>`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ImageSource } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
export function getFilenameFromSource(source: ImageSource): string {
|
|
4
|
+
switch (source.type) {
|
|
5
|
+
case "url": {
|
|
6
|
+
const slashIndex = source.data.lastIndexOf("/");
|
|
7
|
+
return slashIndex === -1 ? source.data : source.data.slice(slashIndex + 1);
|
|
8
|
+
}
|
|
9
|
+
case "file1":
|
|
10
|
+
return source.data.file;
|
|
11
|
+
case "file2":
|
|
12
|
+
return source.data.file;
|
|
13
|
+
case "file3":
|
|
14
|
+
return source.data.file;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { IncludeData } from "@wdprlib/ast";
|
|
14
|
-
import type { RenderContext } from "
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import type { RenderContext } from "../../context";
|
|
15
|
+
import { renderElements } from "../../render";
|
|
16
|
+
import { renderMissingInclude } from "./missing";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Render an `[[include]]` element.
|
|
@@ -26,18 +26,10 @@ import { renderElements } from "../render";
|
|
|
26
26
|
* @param data - Include data with the target page location and resolved elements.
|
|
27
27
|
*/
|
|
28
28
|
export function renderInclude(ctx: RenderContext, data: IncludeData): void {
|
|
29
|
-
// If elements is empty, the include was not resolved - show error
|
|
30
29
|
if (data.elements.length === 0) {
|
|
31
|
-
|
|
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
|
-
);
|
|
30
|
+
renderMissingInclude(ctx, data.location.page);
|
|
40
31
|
return;
|
|
41
32
|
}
|
|
33
|
+
|
|
42
34
|
renderElements(ctx, data.elements);
|
|
43
35
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RenderContext } from "../../context";
|
|
2
|
+
import { escapeAttr, escapeHtml } from "../../escape";
|
|
3
|
+
|
|
4
|
+
export function renderMissingInclude(ctx: RenderContext, page: string): void {
|
|
5
|
+
const pageName = page.toLowerCase();
|
|
6
|
+
const safePath = encodeIncludeEditPath(pageName);
|
|
7
|
+
ctx.push(
|
|
8
|
+
`<div class="error-block"><p>Included page "${escapeHtml(pageName)}" does not exist (<a href="/${escapeAttr(safePath)}/edit/true">create it now</a>)</p></div>`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function encodeIncludeEditPath(pageName: string): string {
|
|
13
|
+
const encodedPageName = pageName.replace(/[^a-z0-9\-_:/]/g, (c) => encodeURIComponent(c));
|
|
14
|
+
return encodedPageName.startsWith("/") ? encodedPageName.slice(1) : encodedPageName;
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AnchorData } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { escapeAttr, isDangerousUrl, sanitizeAttributes } from "../../escape";
|
|
4
|
+
import { renderElements } from "../../render";
|
|
5
|
+
import { renderTargetAttributes } from "./target";
|
|
6
|
+
|
|
7
|
+
export function renderAnchor(ctx: RenderContext, data: AnchorData): void {
|
|
8
|
+
const safe = sanitizeAttributes(data.attributes);
|
|
9
|
+
const attrs: string[] = [];
|
|
10
|
+
|
|
11
|
+
if (safe.href && isDangerousUrl(safe.href)) {
|
|
12
|
+
safe.href = "#invalid-url";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const href = safe.href ?? "";
|
|
16
|
+
attrs.push(`href="${escapeAttr(href)}"`);
|
|
17
|
+
renderTargetAttributes(attrs, data.target);
|
|
18
|
+
|
|
19
|
+
for (const [key, value] of Object.entries(safe)) {
|
|
20
|
+
if (key === "href" || key === "target") continue;
|
|
21
|
+
attrs.push(`${key}="${escapeAttr(value)}"`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ctx.push(`<a ${attrs.join(" ")}>`);
|
|
25
|
+
renderElements(ctx, data.elements);
|
|
26
|
+
ctx.push("</a>");
|
|
27
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { LinkData } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { escapeAttr, isDangerousUrl } from "../../escape";
|
|
4
|
+
import { renderTargetAttributes } from "./target";
|
|
5
|
+
|
|
6
|
+
export function getLinkAttributes(ctx: RenderContext, data: LinkData): string[] {
|
|
7
|
+
const attrs: string[] = [`href="${escapeAttr(resolveSafeHref(ctx, data))}"`];
|
|
8
|
+
|
|
9
|
+
if (shouldAddNewPageClass(ctx, data)) {
|
|
10
|
+
attrs.push(`class="newpage"`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
renderTargetAttributes(attrs, data.target);
|
|
14
|
+
return attrs;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveSafeHref(ctx: RenderContext, data: LinkData): string {
|
|
18
|
+
let href = ctx.resolvePageLink(data.link);
|
|
19
|
+
|
|
20
|
+
if (data.extra) {
|
|
21
|
+
href += data.extra;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isAnchorJsVoid = data.type === "anchor" && href === "javascript:;";
|
|
25
|
+
if (!isAnchorJsVoid && isDangerousUrl(href)) {
|
|
26
|
+
return "#invalid-url";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return href;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shouldAddNewPageClass(ctx: RenderContext, data: LinkData): boolean {
|
|
33
|
+
if (data.type !== "page" || typeof data.link !== "object") {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const page = data.link.page;
|
|
38
|
+
const isSpecialPage = page.startsWith("//") || page.includes("#/");
|
|
39
|
+
if (isSpecialPage) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const hashIdx = page.indexOf("#");
|
|
44
|
+
const pageToCheck = hashIdx !== -1 ? page.slice(0, hashIdx) : page;
|
|
45
|
+
const pageExists = ctx.page?.pageExists;
|
|
46
|
+
return pageExists ? !pageExists(pageToCheck) : true;
|
|
47
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderers for Wikidot link elements.
|
|
4
|
+
*
|
|
5
|
+
* Wikidot supports page links, external links, anchor-type links,
|
|
6
|
+
* `[[a]]...[[/a]]` anchors, and named anchors.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { LinkData } from "@wdprlib/ast";
|
|
12
|
+
import type { RenderContext } from "../../context";
|
|
13
|
+
import { renderAnchor } from "./anchor";
|
|
14
|
+
import { renderAnchorName } from "./anchor-name";
|
|
15
|
+
import { getLinkAttributes } from "./attributes";
|
|
16
|
+
import { renderLinkLabel } from "./label";
|
|
17
|
+
|
|
18
|
+
export { renderAnchor, renderAnchorName };
|
|
19
|
+
|
|
20
|
+
export function renderLink(ctx: RenderContext, data: LinkData): void {
|
|
21
|
+
const attrs = getLinkAttributes(ctx, data);
|
|
22
|
+
|
|
23
|
+
ctx.push(`<a ${attrs.join(" ")}>`);
|
|
24
|
+
renderLinkLabel(ctx, data);
|
|
25
|
+
ctx.push("</a>");
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LinkData } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
|
|
4
|
+
export function renderLinkLabel(ctx: RenderContext, data: LinkData): void {
|
|
5
|
+
if (data.label === "page") {
|
|
6
|
+
if (typeof data.link === "string") {
|
|
7
|
+
ctx.pushEscaped(data.link);
|
|
8
|
+
} else {
|
|
9
|
+
ctx.pushEscaped(data.link.page);
|
|
10
|
+
}
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if ("text" in data.label) {
|
|
15
|
+
ctx.pushEscaped(data.label.text);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if ("url" in data.label) {
|
|
20
|
+
const href = ctx.resolvePageLink(data.link);
|
|
21
|
+
ctx.pushEscaped(data.label.url ?? href);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnchorTarget } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
const TARGET_VALUES: Record<AnchorTarget, string> = {
|
|
4
|
+
"new-tab": "_blank",
|
|
5
|
+
parent: "_parent",
|
|
6
|
+
top: "_top",
|
|
7
|
+
same: "_self",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function renderTargetAttributes(attrs: string[], target: AnchorTarget | null): void {
|
|
11
|
+
if (!target) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const targetValue = TARGET_VALUES[target] ?? "_blank";
|
|
16
|
+
attrs.push(`target="${targetValue}"`);
|
|
17
|
+
if (targetValue === "_blank") {
|
|
18
|
+
attrs.push(`rel="noopener noreferrer"`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { escapeAttr, sanitizeAttributes } from "../../escape";
|
|
2
|
+
|
|
3
|
+
export function renderListAttrs(attributes: Record<string, string>): string {
|
|
4
|
+
let hasAttributes = false;
|
|
5
|
+
for (const _ in attributes) {
|
|
6
|
+
hasAttributes = true;
|
|
7
|
+
break;
|
|
8
|
+
}
|
|
9
|
+
if (!hasAttributes) return "";
|
|
10
|
+
|
|
11
|
+
const safe = sanitizeAttributes(attributes);
|
|
12
|
+
let result = "";
|
|
13
|
+
for (const key in safe) {
|
|
14
|
+
if (key.startsWith("_")) continue;
|
|
15
|
+
const value = safe[key]!;
|
|
16
|
+
result += ` ${key}="${escapeAttr(value)}"`;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DefinitionListItem } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { renderElements } from "../../render";
|
|
4
|
+
|
|
5
|
+
export function renderDefinitionList(ctx: RenderContext, items: DefinitionListItem[]): void {
|
|
6
|
+
ctx.push("<dl>");
|
|
7
|
+
for (const item of items) {
|
|
8
|
+
ctx.push("<dt>");
|
|
9
|
+
renderElements(ctx, item.key);
|
|
10
|
+
ctx.push("</dt>");
|
|
11
|
+
ctx.push("<dd>");
|
|
12
|
+
renderElements(ctx, item.value);
|
|
13
|
+
ctx.push("</dd>");
|
|
14
|
+
}
|
|
15
|
+
ctx.push("</dl>");
|
|
16
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ListData } from "@wdprlib/ast";
|
|
14
|
+
import type { RenderContext } from "../../context";
|
|
15
|
+
import { renderDefinitionList } from "./definition-list";
|
|
16
|
+
import { renderListAttrs } from "./attributes";
|
|
17
|
+
import { renderListItems } from "./items";
|
|
18
|
+
import { hasNonWhitespaceElement } from "./trim";
|
|
19
|
+
|
|
20
|
+
export { renderDefinitionList };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render an ordered or unordered list.
|
|
24
|
+
*
|
|
25
|
+
* Wikidot drops empty lists entirely (no HTML output). Sub-lists
|
|
26
|
+
* following a content item are rendered inside the same `<li>`.
|
|
27
|
+
* Sub-lists without a preceding content item get a hidden `<li>` wrapper.
|
|
28
|
+
*/
|
|
29
|
+
export function renderList(ctx: RenderContext, data: ListData): void {
|
|
30
|
+
const hasContent = data.items.some((item) => {
|
|
31
|
+
if (item["item-type"] === "sub-list") return true;
|
|
32
|
+
if (item["item-type"] === "elements") {
|
|
33
|
+
return hasNonWhitespaceElement(item.elements);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!hasContent) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tag = data.type === "numbered" ? "ol" : "ul";
|
|
43
|
+
ctx.push(`<${tag}${renderListAttrs(data.attributes)}>`);
|
|
44
|
+
|
|
45
|
+
renderListItems(ctx, data.items, renderList);
|
|
46
|
+
|
|
47
|
+
ctx.push(`</${tag}>`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ListData } from "@wdprlib/ast";
|
|
2
|
+
import { renderListAttrs } from "./attributes";
|
|
3
|
+
|
|
4
|
+
type ListItem = ListData["items"][number];
|
|
5
|
+
type SubListItem = Extract<ListItem, { "item-type": "sub-list" }>;
|
|
6
|
+
|
|
7
|
+
export function getListItemOpenTag(attributes: Record<string, string>): string {
|
|
8
|
+
const styleAttr = hasNoMarker(attributes) ? ' style="list-style: none"' : "";
|
|
9
|
+
return `<li${renderListAttrs(attributes)}${styleAttr}>`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasNoMarker(attributes: Record<string, string>): boolean {
|
|
13
|
+
return attributes._noMarker === "true";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderFollowingSubLists(
|
|
17
|
+
items: ListData["items"],
|
|
18
|
+
index: number,
|
|
19
|
+
renderNestedList: (data: ListData) => void,
|
|
20
|
+
): number {
|
|
21
|
+
let nextIndex = index;
|
|
22
|
+
|
|
23
|
+
while (nextIndex + 1 < items.length) {
|
|
24
|
+
const nextItem = getSubListItem(items[nextIndex + 1]);
|
|
25
|
+
if (!nextItem) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
nextIndex++;
|
|
30
|
+
renderNestedList(nextItem.data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return nextIndex;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getSubListItem(item: ListItem | undefined): SubListItem | null {
|
|
37
|
+
return item?.["item-type"] === "sub-list" ? item : null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ListData } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { renderElements } from "../../render";
|
|
4
|
+
import { getListItemOpenTag, hasNoMarker, renderFollowingSubLists } from "./item-rendering";
|
|
5
|
+
import { renderNoMarkerElements } from "./no-marker";
|
|
6
|
+
import { trimTextElements } from "./trim";
|
|
7
|
+
|
|
8
|
+
type ListItem = ListData["items"][number];
|
|
9
|
+
type ElementsListItem = Extract<ListItem, { "item-type": "elements" }>;
|
|
10
|
+
type SubListItem = Extract<ListItem, { "item-type": "sub-list" }>;
|
|
11
|
+
|
|
12
|
+
export type NestedListRenderer = (ctx: RenderContext, data: ListData) => void;
|
|
13
|
+
|
|
14
|
+
export function renderListItems(
|
|
15
|
+
ctx: RenderContext,
|
|
16
|
+
items: ListData["items"],
|
|
17
|
+
renderNestedList: NestedListRenderer,
|
|
18
|
+
): void {
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < items.length) {
|
|
21
|
+
const item = items[i]!;
|
|
22
|
+
if (item["item-type"] === "elements") {
|
|
23
|
+
i = renderElementsListItem(ctx, item, items, i, renderNestedList);
|
|
24
|
+
} else {
|
|
25
|
+
renderOrphanSubListItem(ctx, item, renderNestedList);
|
|
26
|
+
}
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderElementsListItem(
|
|
32
|
+
ctx: RenderContext,
|
|
33
|
+
item: ElementsListItem,
|
|
34
|
+
items: ListData["items"],
|
|
35
|
+
index: number,
|
|
36
|
+
renderNestedList: NestedListRenderer,
|
|
37
|
+
): number {
|
|
38
|
+
const noMarker = hasNoMarker(item.attributes);
|
|
39
|
+
ctx.push(getListItemOpenTag(item.attributes));
|
|
40
|
+
|
|
41
|
+
if (noMarker) {
|
|
42
|
+
renderNoMarkerElements(ctx, item.elements);
|
|
43
|
+
} else {
|
|
44
|
+
renderElements(ctx, trimTextElements(item.elements));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nextIndex = renderFollowingSubLists(items, index, (data) => renderNestedList(ctx, data));
|
|
48
|
+
|
|
49
|
+
ctx.push("</li>");
|
|
50
|
+
return nextIndex;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderOrphanSubListItem(
|
|
54
|
+
ctx: RenderContext,
|
|
55
|
+
item: SubListItem,
|
|
56
|
+
renderNestedList: NestedListRenderer,
|
|
57
|
+
): void {
|
|
58
|
+
ctx.push(`<li style="list-style: none; display: inline">`);
|
|
59
|
+
renderNestedList(ctx, item.data);
|
|
60
|
+
ctx.push("</li>");
|
|
61
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Element } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { renderElement, renderElements } from "../../render";
|
|
4
|
+
import {
|
|
5
|
+
getParagraphIndices,
|
|
6
|
+
isLiCloseTextParagraph,
|
|
7
|
+
isParagraphElement,
|
|
8
|
+
type ParagraphElement,
|
|
9
|
+
} from "./paragraphs";
|
|
10
|
+
import { trimTextElements } from "./trim";
|
|
11
|
+
|
|
12
|
+
export function renderNoMarkerElements(ctx: RenderContext, elements: Element[]): void {
|
|
13
|
+
const trimmed = trimTextElements(elements);
|
|
14
|
+
if (trimmed.length === 0) return;
|
|
15
|
+
|
|
16
|
+
const paragraphIndices = getParagraphIndices(trimmed);
|
|
17
|
+
if (paragraphIndices.length === 0) {
|
|
18
|
+
renderElements(ctx, trimmed);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const firstParagraphIdx = paragraphIndices[0]!;
|
|
23
|
+
const lastParagraphIdx = paragraphIndices[paragraphIndices.length - 1]!;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
26
|
+
const el = trimmed[i]!;
|
|
27
|
+
if (isParagraphElement(el)) {
|
|
28
|
+
renderNoMarkerParagraph(ctx, el, i, firstParagraphIdx, lastParagraphIdx);
|
|
29
|
+
} else {
|
|
30
|
+
renderElement(ctx, el);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderNoMarkerParagraph(
|
|
36
|
+
ctx: RenderContext,
|
|
37
|
+
element: ParagraphElement,
|
|
38
|
+
index: number,
|
|
39
|
+
firstParagraphIdx: number,
|
|
40
|
+
lastParagraphIdx: number,
|
|
41
|
+
): void {
|
|
42
|
+
if (
|
|
43
|
+
index === firstParagraphIdx ||
|
|
44
|
+
(index === lastParagraphIdx && isLiCloseTextParagraph(element))
|
|
45
|
+
) {
|
|
46
|
+
renderElements(ctx, element.data.elements);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ctx.push("<p>");
|
|
51
|
+
renderElements(ctx, element.data.elements);
|
|
52
|
+
ctx.push("</p>");
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ContainerData, Element } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
export type ParagraphElement = Element & {
|
|
4
|
+
element: "container";
|
|
5
|
+
data: ContainerData & { type: "paragraph" };
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function getParagraphIndices(elements: Element[]): number[] {
|
|
9
|
+
const indices: number[] = [];
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < elements.length; i++) {
|
|
12
|
+
if (isParagraphElement(elements[i])) {
|
|
13
|
+
indices.push(i);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return indices;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isParagraphElement(element: Element | undefined): element is ParagraphElement {
|
|
21
|
+
return element?.element === "container" && element.data.type === "paragraph";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isLiCloseTextParagraph(element: ParagraphElement): boolean {
|
|
25
|
+
let combined = "";
|
|
26
|
+
|
|
27
|
+
for (const child of element.data.elements) {
|
|
28
|
+
if (child.element === "text") {
|
|
29
|
+
combined += child.data;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return combined.trim() === "[[/li]]";
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Element } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
export function trimTextElements(elements: Element[]): Element[] {
|
|
4
|
+
if (elements.length === 0) return elements;
|
|
5
|
+
|
|
6
|
+
let start = 0;
|
|
7
|
+
let end = elements.length;
|
|
8
|
+
|
|
9
|
+
while (start < end) {
|
|
10
|
+
const el = elements[start]!;
|
|
11
|
+
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
12
|
+
start++;
|
|
13
|
+
} else {
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
while (end > start) {
|
|
19
|
+
const el = elements[end - 1]!;
|
|
20
|
+
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
21
|
+
end--;
|
|
22
|
+
} else {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (start === 0 && end === elements.length) return elements;
|
|
28
|
+
return elements.slice(start, end);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hasNonWhitespaceElement(elements: Element[]): boolean {
|
|
32
|
+
for (const el of elements) {
|
|
33
|
+
if (el.element !== "text" || typeof el.data !== "string" || el.data.trim() !== "") {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|