@wdprlib/render 2.0.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 +2332 -2032
- package/dist/index.d.cts +15 -13
- package/dist/index.d.ts +15 -13
- package/dist/index.js +2336 -2036
- package/package.json +5 -3
- 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/clear-float.ts +27 -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/color.ts +32 -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/index.ts +34 -0
- 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/index.ts +35 -0
- package/src/elements/include/missing.ts +15 -0
- package/src/elements/index.ts +35 -0
- package/src/elements/line-break.ts +22 -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 +29 -0
- package/src/elements/module/categories.ts +27 -0
- package/src/elements/module/empty-container.ts +10 -0
- package/src/elements/module/index.ts +65 -0
- package/src/elements/module/join-markup.ts +10 -0
- package/src/elements/module/join.ts +28 -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-markup.ts +10 -0
- package/src/elements/module/rate.ts +35 -0
- 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/hash.ts +62 -0
- package/src/index.ts +26 -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/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/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 +144 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface IfTagsConditionTokens {
|
|
2
|
+
required: string[];
|
|
3
|
+
excluded: string[];
|
|
4
|
+
optional: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseIfTagsConditionTokens(condition: string): IfTagsConditionTokens {
|
|
8
|
+
const tokens: IfTagsConditionTokens = {
|
|
9
|
+
required: [],
|
|
10
|
+
excluded: [],
|
|
11
|
+
optional: [],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
for (const token of condition.split(/\s+/).filter(Boolean)) {
|
|
15
|
+
if (token.startsWith("+")) {
|
|
16
|
+
addTag(tokens.required, token.slice(1));
|
|
17
|
+
} else if (token.startsWith("-")) {
|
|
18
|
+
addTag(tokens.excluded, token.slice(1));
|
|
19
|
+
} else {
|
|
20
|
+
addTag(tokens.optional, token);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return tokens;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hasAnyIfTagsConditionToken(tokens: IfTagsConditionTokens): boolean {
|
|
28
|
+
return tokens.required.length > 0 || tokens.excluded.length > 0 || tokens.optional.length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addTag(tags: string[], rawTag: string): void {
|
|
32
|
+
const tag = rawTag.toLowerCase();
|
|
33
|
+
if (tag) {
|
|
34
|
+
tags.push(tag);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { FloatAlignment } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
|
|
4
|
+
export function pushAlignedImage(
|
|
5
|
+
ctx: RenderContext,
|
|
6
|
+
output: string,
|
|
7
|
+
alignment: FloatAlignment | null,
|
|
8
|
+
): void {
|
|
9
|
+
if (!alignment) {
|
|
10
|
+
ctx.push(output);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const alignClass = getAlignmentClass(alignment.align, alignment.float);
|
|
15
|
+
ctx.push(`<div class="image-container ${alignClass}">`);
|
|
16
|
+
ctx.push(output);
|
|
17
|
+
ctx.push("</div>");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getAlignmentClass(align: string, isFloat: boolean): string {
|
|
21
|
+
if (isFloat) {
|
|
22
|
+
switch (align) {
|
|
23
|
+
case "left":
|
|
24
|
+
return "floatleft";
|
|
25
|
+
case "right":
|
|
26
|
+
return "floatright";
|
|
27
|
+
case "center":
|
|
28
|
+
return "floatcenter";
|
|
29
|
+
default:
|
|
30
|
+
return `float${align}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
switch (align) {
|
|
35
|
+
case "left":
|
|
36
|
+
return "alignleft";
|
|
37
|
+
case "right":
|
|
38
|
+
return "alignright";
|
|
39
|
+
case "center":
|
|
40
|
+
return "aligncenter";
|
|
41
|
+
default:
|
|
42
|
+
return `align${align}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { renderElements } from "../../render";
|
|
16
|
+
import { renderMissingInclude } from "./missing";
|
|
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 (data.elements.length === 0) {
|
|
30
|
+
renderMissingInclude(ctx, data.location.page);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
renderElements(ctx, data.elements);
|
|
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,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
|
+
}
|
|
@@ -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
|
+
}
|