@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,23 @@
|
|
|
1
|
+
import { escapeAttr, sanitizeAttributes } from "../../escape";
|
|
2
|
+
|
|
3
|
+
export { renderTableCellAttrs } from "./cell-attributes";
|
|
4
|
+
|
|
5
|
+
export function renderTableAttrs(attributes: Record<string, string>): string {
|
|
6
|
+
let hasRenderableAttributes = false;
|
|
7
|
+
for (const key in attributes) {
|
|
8
|
+
if (!key.startsWith("_")) {
|
|
9
|
+
hasRenderableAttributes = true;
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (!hasRenderableAttributes) return "";
|
|
14
|
+
|
|
15
|
+
const safe = sanitizeAttributes(attributes);
|
|
16
|
+
let result = "";
|
|
17
|
+
for (const key in safe) {
|
|
18
|
+
if (key.startsWith("_")) continue;
|
|
19
|
+
const value = safe[key]!;
|
|
20
|
+
result += ` ${key}="${escapeAttr(value)}"`;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TableCell } from "@wdprlib/ast";
|
|
2
|
+
import { escapeAttr, sanitizeAttributes } from "../../escape";
|
|
3
|
+
|
|
4
|
+
export function renderTableCellAttrs(cell: TableCell): string {
|
|
5
|
+
const attrs: string[] = [];
|
|
6
|
+
const safeCellAttrs = sanitizeAttributes(cell.attributes);
|
|
7
|
+
|
|
8
|
+
appendColumnSpan(attrs, cell);
|
|
9
|
+
appendRowSpan(attrs, safeCellAttrs);
|
|
10
|
+
appendAlignmentStyle(attrs, cell, safeCellAttrs);
|
|
11
|
+
appendRemainingCellAttributes(attrs, cell, safeCellAttrs);
|
|
12
|
+
|
|
13
|
+
return attrs.length > 0 ? " " + attrs.join(" ") : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function appendColumnSpan(attrs: string[], cell: TableCell): void {
|
|
17
|
+
if (cell["column-span"] > 1) {
|
|
18
|
+
attrs.push(`colspan="${cell["column-span"]}"`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function appendRowSpan(attrs: string[], safeCellAttrs: Record<string, string>): void {
|
|
23
|
+
if (!safeCellAttrs.rowspan) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rowspan = parseInt(safeCellAttrs.rowspan, 10);
|
|
28
|
+
if (rowspan > 1) {
|
|
29
|
+
attrs.push(`rowspan="${rowspan}"`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendAlignmentStyle(
|
|
34
|
+
attrs: string[],
|
|
35
|
+
cell: TableCell,
|
|
36
|
+
safeCellAttrs: Record<string, string>,
|
|
37
|
+
): void {
|
|
38
|
+
if (!cell.align) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const existingStyle = safeCellAttrs.style ?? "";
|
|
43
|
+
const alignStyle = `text-align: ${cell.align};`;
|
|
44
|
+
if (existingStyle) {
|
|
45
|
+
attrs.push(`style="${escapeAttr(existingStyle + "; " + alignStyle)}"`);
|
|
46
|
+
} else {
|
|
47
|
+
attrs.push(`style="${alignStyle}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function appendRemainingCellAttributes(
|
|
52
|
+
attrs: string[],
|
|
53
|
+
cell: TableCell,
|
|
54
|
+
safeCellAttrs: Record<string, string>,
|
|
55
|
+
): void {
|
|
56
|
+
for (const key in safeCellAttrs) {
|
|
57
|
+
if (key === "style" && cell.align) continue;
|
|
58
|
+
if (key === "rowspan") continue;
|
|
59
|
+
const value = safeCellAttrs[key]!;
|
|
60
|
+
attrs.push(`${key}="${escapeAttr(value)}"`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TableCell } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { renderElements } from "../../render";
|
|
4
|
+
import { renderTableCellAttrs } from "./attributes";
|
|
5
|
+
|
|
6
|
+
export function renderTableCell(ctx: RenderContext, cell: TableCell): void {
|
|
7
|
+
const tag = cell.header ? "th" : "td";
|
|
8
|
+
const attrStr = renderTableCellAttrs(cell);
|
|
9
|
+
|
|
10
|
+
ctx.push(`<${tag}${attrStr}>`);
|
|
11
|
+
renderElements(ctx, cell.elements);
|
|
12
|
+
ctx.push(`</${tag}>`);
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for Wikidot table elements.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TableData } from "@wdprlib/ast";
|
|
9
|
+
import type { RenderContext } from "../../context";
|
|
10
|
+
import { renderTableAttrs } from "./attributes";
|
|
11
|
+
import { renderTableCell } from "./cell";
|
|
12
|
+
|
|
13
|
+
export function renderTable(ctx: RenderContext, data: TableData): void {
|
|
14
|
+
const isPipeTable = data.attributes._source === "pipe";
|
|
15
|
+
const classAttr = isPipeTable ? ' class="wiki-content-table"' : "";
|
|
16
|
+
ctx.push(`<table${classAttr}${renderTableAttrs(data.attributes)}>`);
|
|
17
|
+
|
|
18
|
+
for (const row of data.rows) {
|
|
19
|
+
ctx.push(`<tr${renderTableAttrs(row.attributes)}>`);
|
|
20
|
+
for (const cell of row.cells) {
|
|
21
|
+
renderTableCell(ctx, cell);
|
|
22
|
+
}
|
|
23
|
+
ctx.push("</tr>");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx.push("</table>");
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RenderContext } from "../../context";
|
|
2
|
+
import { escapeAttr, escapeHtml, isValidEmail } from "../../escape";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render an email address element as a `mailto:` link.
|
|
6
|
+
*
|
|
7
|
+
* The email is validated before creating the link. Invalid email addresses
|
|
8
|
+
* are rendered as plain escaped text to prevent `mailto:` injection.
|
|
9
|
+
*
|
|
10
|
+
* @param ctx - The current render context.
|
|
11
|
+
* @param email - The email address string.
|
|
12
|
+
*/
|
|
13
|
+
export function renderEmail(ctx: RenderContext, email: string): void {
|
|
14
|
+
if (!isValidEmail(email)) {
|
|
15
|
+
ctx.pushEscaped(email);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ctx.push(`<a href="mailto:${escapeAttr(email)}">${escapeHtml(email)}</a>`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RenderContext } from "../../context";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render a plain text node by HTML-escaping and appending to the output.
|
|
5
|
+
*
|
|
6
|
+
* @param ctx - The current render context.
|
|
7
|
+
* @param data - The raw text content.
|
|
8
|
+
*/
|
|
9
|
+
export function renderText(ctx: RenderContext, data: string): void {
|
|
10
|
+
ctx.pushEscaped(data);
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RenderContext } from "../../context";
|
|
2
|
+
import { escapeHtml } from "../../escape";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render raw/literal text (Wikidot `@@...@@` syntax).
|
|
6
|
+
*
|
|
7
|
+
* Raw text is rendered inside a `<span style="white-space: pre-wrap;">` with
|
|
8
|
+
* spaces encoded as ` ` to preserve Wikidot's exact formatting. Empty
|
|
9
|
+
* strings produce no output.
|
|
10
|
+
*
|
|
11
|
+
* @param ctx - The current render context.
|
|
12
|
+
* @param data - The raw text content.
|
|
13
|
+
*/
|
|
14
|
+
export function renderRaw(ctx: RenderContext, data: string): void {
|
|
15
|
+
if (data === "") return;
|
|
16
|
+
|
|
17
|
+
ctx.push(`<span style="white-space: pre-wrap;">`);
|
|
18
|
+
ctx.push(escapeHtml(data).replace(/ /g, " "));
|
|
19
|
+
ctx.push("</span>");
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RenderContext } from "../../context";
|
|
2
|
+
import { renderTocEntries } from "./entries";
|
|
3
|
+
|
|
4
|
+
export function renderTocBody(ctx: RenderContext): void {
|
|
5
|
+
ctx.push(
|
|
6
|
+
`<div id="toc-action-bar"><a href="javascript:;">Fold</a><a style="display: none" href="javascript:;">Unfold</a></div>`,
|
|
7
|
+
);
|
|
8
|
+
ctx.push(`<div class="title">Table of Contents</div>`);
|
|
9
|
+
ctx.push(`<div id="toc-list">`);
|
|
10
|
+
renderTocEntries(ctx, ctx.tocElements);
|
|
11
|
+
ctx.push("</div>");
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Element, ListData, ListItem } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { escapeAttr, escapeHtml } from "../../escape";
|
|
4
|
+
import { extractTocLink, rewriteTocAnchor } from "./link";
|
|
5
|
+
|
|
6
|
+
export function renderTocEntries(ctx: RenderContext, elements: Element[]): void {
|
|
7
|
+
for (const element of elements) {
|
|
8
|
+
if (element.element === "list") {
|
|
9
|
+
renderTocList(ctx, element.data, 1);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderTocList(ctx: RenderContext, listData: ListData, depth: number): void {
|
|
15
|
+
for (const item of listData.items) {
|
|
16
|
+
renderTocItem(ctx, item, depth);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderTocItem(ctx: RenderContext, item: ListItem, depth: number): void {
|
|
21
|
+
if (item["item-type"] === "elements") {
|
|
22
|
+
for (const el of item.elements) {
|
|
23
|
+
const link = extractTocLink(el);
|
|
24
|
+
if (link) {
|
|
25
|
+
const href = rewriteTocAnchor(ctx, link.href);
|
|
26
|
+
ctx.push(
|
|
27
|
+
`<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else if (item["item-type"] === "sub-list") {
|
|
32
|
+
renderTocList(ctx, item.data, depth + 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Alignment } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
|
|
4
|
+
export function isFloatingToc(align: Alignment | null): boolean {
|
|
5
|
+
return align === "left" || align === "right";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function openTocFrame(ctx: RenderContext, align: Alignment | null): void {
|
|
9
|
+
if (!isFloatingToc(align)) {
|
|
10
|
+
ctx.push(`<table style="margin:0; padding:0"><tr><td style="margin:0; padding:0">`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (isFloatingToc(align)) {
|
|
14
|
+
const floatClass = align === "left" ? "floatleft" : "floatright";
|
|
15
|
+
ctx.push(`<div id="toc" class="${floatClass}">`);
|
|
16
|
+
} else {
|
|
17
|
+
ctx.push(`<div id="toc">`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function closeTocFrame(ctx: RenderContext, align: Alignment | null): void {
|
|
22
|
+
ctx.push("</div>");
|
|
23
|
+
|
|
24
|
+
if (!isFloatingToc(align)) {
|
|
25
|
+
ctx.push(`</td></tr></table>`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for `[[toc]]` (Table of Contents) elements.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TableOfContentsData } from "@wdprlib/ast";
|
|
9
|
+
import type { RenderContext } from "../../context";
|
|
10
|
+
import { renderTocBody } from "./body";
|
|
11
|
+
import { closeTocFrame, openTocFrame } from "./frame";
|
|
12
|
+
|
|
13
|
+
export function renderTableOfContents(ctx: RenderContext, data: TableOfContentsData): void {
|
|
14
|
+
openTocFrame(ctx, data.align);
|
|
15
|
+
renderTocBody(ctx);
|
|
16
|
+
closeTocFrame(ctx, data.align);
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Element } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
|
|
4
|
+
export interface TocLink {
|
|
5
|
+
href: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function extractTocLink(element: Element): TocLink | null {
|
|
10
|
+
if (element.element !== "link") return null;
|
|
11
|
+
|
|
12
|
+
const label = element.data.label;
|
|
13
|
+
let text = "";
|
|
14
|
+
if (typeof label === "object" && label !== null && "text" in label) {
|
|
15
|
+
text = label.text;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const href = typeof element.data.link === "string" ? element.data.link : "";
|
|
19
|
+
return { href, text };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function rewriteTocAnchor(ctx: RenderContext, href: string): string {
|
|
23
|
+
const match = /^#toc(\d+)$/.exec(href);
|
|
24
|
+
if (!match) return href;
|
|
25
|
+
return `#${ctx.generateId("toc", Number(match[1]))}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Renderer for `[[user username]]` elements.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { UserData } from "@wdprlib/ast";
|
|
9
|
+
import type { RenderContext } from "../../context";
|
|
10
|
+
import { escapeHtml } from "../../escape";
|
|
11
|
+
import { getResolvedUser } from "./resolve";
|
|
12
|
+
import { renderAvatarUser, renderLinkedUser } from "./markup";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render a `[[user username]]` element.
|
|
16
|
+
*
|
|
17
|
+
* @param ctx - The current render context.
|
|
18
|
+
* @param data - User element data with username and show-avatar flag.
|
|
19
|
+
*/
|
|
20
|
+
export function renderUser(ctx: RenderContext, data: UserData): void {
|
|
21
|
+
const normalized = data.name.toLowerCase().trim();
|
|
22
|
+
|
|
23
|
+
if (normalized === "anonymous") {
|
|
24
|
+
ctx.push("Anonymous");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const resolved = getResolvedUser(ctx, data.name);
|
|
29
|
+
if (resolved === null) {
|
|
30
|
+
ctx.push(escapeHtml(data.name));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const showAvatar = data["show-avatar"] && resolved.url && resolved.avatarUrl;
|
|
35
|
+
if (showAvatar) {
|
|
36
|
+
renderAvatarUser(ctx, data.name, resolved);
|
|
37
|
+
} else {
|
|
38
|
+
renderLinkedUser(ctx, data.name, resolved);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RenderContext } from "../../context";
|
|
2
|
+
import type { ResolvedUser } from "../../types";
|
|
3
|
+
import { escapeAttr, escapeHtml } from "../../escape";
|
|
4
|
+
|
|
5
|
+
export function renderLinkedUser(ctx: RenderContext, username: string, user: ResolvedUser): void {
|
|
6
|
+
const displayName = user.name ?? username;
|
|
7
|
+
const hrefAttr = user.url ? ` href="${escapeAttr(user.url)}"` : "";
|
|
8
|
+
|
|
9
|
+
ctx.push(`<span class="printuser">`);
|
|
10
|
+
ctx.push(`<a${hrefAttr}>`);
|
|
11
|
+
ctx.push(escapeHtml(displayName));
|
|
12
|
+
ctx.push("</a>");
|
|
13
|
+
ctx.push("</span>");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderAvatarUser(ctx: RenderContext, username: string, user: ResolvedUser): void {
|
|
17
|
+
const displayName = user.name ?? username;
|
|
18
|
+
const hrefAttr = user.url ? ` href="${escapeAttr(user.url)}"` : "";
|
|
19
|
+
const avatarUrl = user.avatarUrl ?? "";
|
|
20
|
+
const styleAttr = user.karmaUrl
|
|
21
|
+
? ` style="background-image:url(${escapeAttr(user.karmaUrl)})"`
|
|
22
|
+
: "";
|
|
23
|
+
|
|
24
|
+
ctx.push(`<span class="printuser avatarhover">`);
|
|
25
|
+
ctx.push(`<a${hrefAttr}>`);
|
|
26
|
+
ctx.push(
|
|
27
|
+
`<img class="small" src="${escapeAttr(avatarUrl)}" alt="${escapeAttr(displayName)}"${styleAttr} />`,
|
|
28
|
+
);
|
|
29
|
+
ctx.push("</a>");
|
|
30
|
+
ctx.push(`<a${hrefAttr}>`);
|
|
31
|
+
ctx.push(escapeHtml(displayName));
|
|
32
|
+
ctx.push("</a>");
|
|
33
|
+
ctx.push("</span>");
|
|
34
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML attribute names considered safe for rendering.
|
|
3
|
+
*/
|
|
4
|
+
export const SAFE_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
5
|
+
"accept",
|
|
6
|
+
"align",
|
|
7
|
+
"alt",
|
|
8
|
+
"autocapitalize",
|
|
9
|
+
"autoplay",
|
|
10
|
+
"background",
|
|
11
|
+
"bgcolor",
|
|
12
|
+
"border",
|
|
13
|
+
"buffered",
|
|
14
|
+
"checked",
|
|
15
|
+
"cite",
|
|
16
|
+
"class",
|
|
17
|
+
"cols",
|
|
18
|
+
"colspan",
|
|
19
|
+
"contenteditable",
|
|
20
|
+
"controls",
|
|
21
|
+
"coords",
|
|
22
|
+
"datetime",
|
|
23
|
+
"decoding",
|
|
24
|
+
"default",
|
|
25
|
+
"dir",
|
|
26
|
+
"dirname",
|
|
27
|
+
"disabled",
|
|
28
|
+
"download",
|
|
29
|
+
"draggable",
|
|
30
|
+
"for",
|
|
31
|
+
"form",
|
|
32
|
+
"headers",
|
|
33
|
+
"height",
|
|
34
|
+
"hidden",
|
|
35
|
+
"high",
|
|
36
|
+
"href",
|
|
37
|
+
"hreflang",
|
|
38
|
+
"id",
|
|
39
|
+
"inputmode",
|
|
40
|
+
"ismap",
|
|
41
|
+
"itemprop",
|
|
42
|
+
"kind",
|
|
43
|
+
"label",
|
|
44
|
+
"lang",
|
|
45
|
+
"list",
|
|
46
|
+
"loop",
|
|
47
|
+
"low",
|
|
48
|
+
"max",
|
|
49
|
+
"maxlength",
|
|
50
|
+
"min",
|
|
51
|
+
"minlength",
|
|
52
|
+
"multiple",
|
|
53
|
+
"muted",
|
|
54
|
+
"name",
|
|
55
|
+
"optimum",
|
|
56
|
+
"pattern",
|
|
57
|
+
"placeholder",
|
|
58
|
+
"poster",
|
|
59
|
+
"preload",
|
|
60
|
+
"readonly",
|
|
61
|
+
"required",
|
|
62
|
+
"reversed",
|
|
63
|
+
"role",
|
|
64
|
+
"rows",
|
|
65
|
+
"rowspan",
|
|
66
|
+
"scope",
|
|
67
|
+
"selected",
|
|
68
|
+
"shape",
|
|
69
|
+
"size",
|
|
70
|
+
"sizes",
|
|
71
|
+
"span",
|
|
72
|
+
"spellcheck",
|
|
73
|
+
"src",
|
|
74
|
+
"srclang",
|
|
75
|
+
"srcset",
|
|
76
|
+
"start",
|
|
77
|
+
"step",
|
|
78
|
+
"style",
|
|
79
|
+
"tabindex",
|
|
80
|
+
"target",
|
|
81
|
+
"title",
|
|
82
|
+
"translate",
|
|
83
|
+
"type",
|
|
84
|
+
"usemap",
|
|
85
|
+
"value",
|
|
86
|
+
"width",
|
|
87
|
+
"wrap",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Attribute names whose values are interpreted as URLs by browsers.
|
|
92
|
+
*/
|
|
93
|
+
export const URL_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
94
|
+
"href",
|
|
95
|
+
"src",
|
|
96
|
+
"action",
|
|
97
|
+
"formaction",
|
|
98
|
+
"srcset",
|
|
99
|
+
"poster",
|
|
100
|
+
"background",
|
|
101
|
+
]);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { sanitizeStyleValue } from "./css";
|
|
2
|
+
import { SAFE_ATTRIBUTES, URL_ATTRIBUTES } from "./attribute-allowlists";
|
|
3
|
+
import { isDangerousUrl } from "./url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check whether an HTML attribute name is safe to include in rendered output.
|
|
7
|
+
*
|
|
8
|
+
* The check applies three rules in order:
|
|
9
|
+
* 1. Block all event handlers (`on*` prefix) unconditionally
|
|
10
|
+
* 2. Allow accessibility (`aria-*`) and custom data (`data-*`) attributes
|
|
11
|
+
* 3. Allow only attributes in the `SAFE_ATTRIBUTES` allowlist
|
|
12
|
+
*
|
|
13
|
+
* @param name - The attribute name to validate (case-insensitive).
|
|
14
|
+
* @returns `true` if the attribute is safe to render.
|
|
15
|
+
*/
|
|
16
|
+
export function isSafeAttribute(name: string): boolean {
|
|
17
|
+
const lower = name.toLowerCase();
|
|
18
|
+
return isSafeAttributeLower(lower);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isSafeAttributeLower(lower: string): boolean {
|
|
22
|
+
// Block all event handlers
|
|
23
|
+
if (lower.startsWith("on")) return false;
|
|
24
|
+
// Allow aria-* and data-* prefixes
|
|
25
|
+
if (lower.startsWith("aria-") || lower.startsWith("data-")) return true;
|
|
26
|
+
return SAFE_ATTRIBUTES.has(lower);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sanitize a map of HTML attributes, returning a new map containing
|
|
31
|
+
* only entries that pass all safety checks.
|
|
32
|
+
*
|
|
33
|
+
* For each attribute, this function:
|
|
34
|
+
* 1. Drops attributes that fail {@link isSafeAttribute} (event handlers, unknown names)
|
|
35
|
+
* 2. Drops URL-bearing attributes whose values fail {@link isDangerousUrl}
|
|
36
|
+
* 3. Sanitizes `style` values via {@link sanitizeStyleValue}, dropping them entirely
|
|
37
|
+
* if the result is empty
|
|
38
|
+
* 4. Passes all other safe attributes through unchanged
|
|
39
|
+
*
|
|
40
|
+
* @param attributes - The raw attribute name-value map to sanitize.
|
|
41
|
+
* @returns A new map containing only the safe attributes and their (possibly sanitized) values.
|
|
42
|
+
*/
|
|
43
|
+
export function sanitizeAttributes(attributes: Record<string, string>): Record<string, string> {
|
|
44
|
+
const result: Record<string, string> = {};
|
|
45
|
+
for (const key in attributes) {
|
|
46
|
+
const value = attributes[key]!;
|
|
47
|
+
const lower = key.toLowerCase();
|
|
48
|
+
if (!isSafeAttributeLower(lower)) continue;
|
|
49
|
+
// Check URL attributes for dangerous schemes
|
|
50
|
+
if (URL_ATTRIBUTES.has(lower) && isDangerousUrl(value)) continue;
|
|
51
|
+
// Sanitize style attribute
|
|
52
|
+
if (lower === "style") {
|
|
53
|
+
const sanitized = sanitizeStyleValue(value);
|
|
54
|
+
if (sanitized) {
|
|
55
|
+
result[key] = sanitized;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
result[key] = value;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function isValidCssColorFunction(color: string): boolean {
|
|
2
|
+
const fnMatch = color.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
|
|
3
|
+
if (!fnMatch) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const fn = fnMatch[1]!;
|
|
8
|
+
const args = fnMatch[2]!
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((s) => s.trim())
|
|
11
|
+
.join(",");
|
|
12
|
+
|
|
13
|
+
if (fn.startsWith("rgb")) {
|
|
14
|
+
return /^\d{1,3},\d{1,3},\d{1,3}(,(0|1|0?\.\d+))?$/.test(args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return /^\d{1,3},\d{1,3}%,\d{1,3}%(,(0|1|0?\.\d+))?$/.test(args);
|
|
18
|
+
}
|