@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,78 @@
|
|
|
1
|
+
import { isDangerousCssValue } from "./css-danger";
|
|
2
|
+
import { normalizeCssValue } from "./css-normalize";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize a `style` attribute value by removing dangerous declarations
|
|
6
|
+
* while preserving safe ones.
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeStyleValue(style: string): string {
|
|
9
|
+
const endsWithSemicolon = style.trimEnd().endsWith(";");
|
|
10
|
+
|
|
11
|
+
if (style.indexOf(";") === -1) {
|
|
12
|
+
return sanitizeSingleDeclaration(style.trim());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const safe: string[] = [];
|
|
16
|
+
for (const rawDecl of splitDeclarations(style)) {
|
|
17
|
+
const decl = sanitizeSingleDeclaration(rawDecl.trim());
|
|
18
|
+
if (decl) safe.push(decl);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (safe.length === 0) return "";
|
|
22
|
+
return endsWithSemicolon ? safe.join(";") + ";" : safe.join(";");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sanitizeSingleDeclaration(decl: string): string {
|
|
26
|
+
if (decl === "") return "";
|
|
27
|
+
|
|
28
|
+
const colonIdx = decl.indexOf(":");
|
|
29
|
+
if (colonIdx === -1) return "";
|
|
30
|
+
|
|
31
|
+
const property = decl.slice(0, colonIdx).trim();
|
|
32
|
+
const value = decl.slice(colonIdx + 1).trim();
|
|
33
|
+
if (isDangerousCssValue(value)) return "";
|
|
34
|
+
|
|
35
|
+
const normalisedProperty = normalizeCssValue(property);
|
|
36
|
+
if (normalisedProperty.startsWith("-moz-binding")) return "";
|
|
37
|
+
if (normalisedProperty === "behavior") return "";
|
|
38
|
+
|
|
39
|
+
return decl;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Split a CSS style attribute value into declarations, respecting
|
|
44
|
+
* parentheses and quoted strings.
|
|
45
|
+
*/
|
|
46
|
+
function splitDeclarations(style: string): string[] {
|
|
47
|
+
const out: string[] = [];
|
|
48
|
+
let start = 0;
|
|
49
|
+
let parenDepth = 0;
|
|
50
|
+
let quoteChar: string | null = null;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < style.length; i++) {
|
|
53
|
+
const ch = style[i]!;
|
|
54
|
+
if (quoteChar !== null) {
|
|
55
|
+
if (ch === quoteChar) quoteChar = null;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (ch === '"' || ch === "'") {
|
|
59
|
+
quoteChar = ch;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === "(") {
|
|
63
|
+
parenDepth++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === ")") {
|
|
67
|
+
if (parenDepth > 0) parenDepth--;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === ";" && parenDepth === 0) {
|
|
71
|
+
out.push(style.slice(start, i));
|
|
72
|
+
start = i + 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (start < style.length) out.push(style.slice(start));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface CssUrlToken {
|
|
2
|
+
inner: string;
|
|
3
|
+
malformed: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Allowlist check for a raw URL string extracted from a normalized `url(...)` token.
|
|
8
|
+
*/
|
|
9
|
+
export function isCssUrlAllowed(rawUrl: string): boolean {
|
|
10
|
+
let url = rawUrl;
|
|
11
|
+
|
|
12
|
+
if (url.length >= 2) {
|
|
13
|
+
const first = url[0];
|
|
14
|
+
const last = url[url.length - 1];
|
|
15
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
16
|
+
url = url.slice(1, -1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (url === "") return true;
|
|
21
|
+
if (url.startsWith("#")) return true;
|
|
22
|
+
if (url.startsWith("./") || url.startsWith("../")) return true;
|
|
23
|
+
if (url.startsWith("//")) return true;
|
|
24
|
+
if (url.startsWith("/")) return true;
|
|
25
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return true;
|
|
26
|
+
|
|
27
|
+
if (url.startsWith("data:image/")) {
|
|
28
|
+
const after = url.slice("data:image/".length);
|
|
29
|
+
const sep = Math.min(
|
|
30
|
+
after.indexOf(";") === -1 ? after.length : after.indexOf(";"),
|
|
31
|
+
after.indexOf(",") === -1 ? after.length : after.indexOf(","),
|
|
32
|
+
);
|
|
33
|
+
const mime = after.slice(0, sep);
|
|
34
|
+
if (mime === "png" || mime === "jpeg" || mime === "jpg" || mime === "gif" || mime === "webp") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract every `url(...)` invocation from a normalized CSS value.
|
|
44
|
+
*/
|
|
45
|
+
export function* iterateCssUrls(normalized: string): Generator<CssUrlToken> {
|
|
46
|
+
let searchPos = 0;
|
|
47
|
+
while (searchPos < normalized.length) {
|
|
48
|
+
const idx = normalized.indexOf("url(", searchPos);
|
|
49
|
+
if (idx === -1) return;
|
|
50
|
+
|
|
51
|
+
let depth = 1;
|
|
52
|
+
let quoteChar: string | null = null;
|
|
53
|
+
let i = idx + 4;
|
|
54
|
+
while (i < normalized.length && depth > 0) {
|
|
55
|
+
const ch = normalized[i];
|
|
56
|
+
if (quoteChar !== null) {
|
|
57
|
+
if (ch === quoteChar) quoteChar = null;
|
|
58
|
+
} else if (ch === '"' || ch === "'") {
|
|
59
|
+
quoteChar = ch;
|
|
60
|
+
} else if (ch === "(") {
|
|
61
|
+
depth++;
|
|
62
|
+
} else if (ch === ")") {
|
|
63
|
+
depth--;
|
|
64
|
+
}
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (depth > 0) {
|
|
69
|
+
yield { inner: normalized.slice(idx + 4), malformed: true };
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
yield { inner: normalized.slice(idx + 4, i - 1), malformed: false };
|
|
74
|
+
searchPos = i;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate that a string looks like a safe email address.
|
|
3
|
+
*
|
|
4
|
+
* Uses a deliberately simple pattern that accepts the vast majority of
|
|
5
|
+
* real-world addresses while blocking characters that could enable
|
|
6
|
+
* injection attacks when the address is used in a `mailto:` link.
|
|
7
|
+
*
|
|
8
|
+
* The percent character (`%`) is intentionally disallowed because
|
|
9
|
+
* `mailto:` URLs undergo percent-decoding, allowing an attacker to
|
|
10
|
+
* inject headers (e.g. `a%0d%0abcc%3aevil@example.com` decodes to
|
|
11
|
+
* a BCC header injection).
|
|
12
|
+
*
|
|
13
|
+
* @param email - The email string to validate.
|
|
14
|
+
* @returns `true` if the email matches the safe pattern.
|
|
15
|
+
*/
|
|
16
|
+
export function isValidEmail(email: string): boolean {
|
|
17
|
+
// Simple email pattern: local@domain
|
|
18
|
+
// - local: alphanumeric, dots, underscores, hyphens, plus signs (NO percent)
|
|
19
|
+
// - domain: alphanumeric, dots, hyphens
|
|
20
|
+
// Does NOT allow: spaces, colons, angle brackets, percent, or other special chars
|
|
21
|
+
return /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);
|
|
22
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function escapeHtml(text: string): string {
|
|
2
|
+
let start = 0;
|
|
3
|
+
let escaped = "";
|
|
4
|
+
|
|
5
|
+
for (let i = 0; i < text.length; i++) {
|
|
6
|
+
const char = text[i];
|
|
7
|
+
let replacement: string | null = null;
|
|
8
|
+
if (char === "&") {
|
|
9
|
+
replacement = "&";
|
|
10
|
+
} else if (char === "<") {
|
|
11
|
+
replacement = "<";
|
|
12
|
+
} else if (char === ">") {
|
|
13
|
+
replacement = ">";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (replacement) {
|
|
17
|
+
if (start < i) {
|
|
18
|
+
escaped += text.slice(start, i);
|
|
19
|
+
}
|
|
20
|
+
escaped += replacement;
|
|
21
|
+
start = i + 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (start === 0) {
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
return start < text.length ? escaped + text.slice(start) : escaped;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function escapeAttr(value: string): string {
|
|
32
|
+
if (
|
|
33
|
+
value.indexOf("&") === -1 &&
|
|
34
|
+
value.indexOf("<") === -1 &&
|
|
35
|
+
value.indexOf(">") === -1 &&
|
|
36
|
+
value.indexOf('"') === -1 &&
|
|
37
|
+
value.indexOf("'") === -1
|
|
38
|
+
) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return value
|
|
42
|
+
.replace(/&/g, "&")
|
|
43
|
+
.replace(/</g, "<")
|
|
44
|
+
.replace(/>/g, ">")
|
|
45
|
+
.replace(/"/g, """)
|
|
46
|
+
.replace(/'/g, "'");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function escapeStyleContent(css: string): string {
|
|
50
|
+
if (css.indexOf("</") === -1) {
|
|
51
|
+
return css;
|
|
52
|
+
}
|
|
53
|
+
return css.replace(/<\/style/gi, "<\\/style");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function escapeJsString(value: string): string {
|
|
57
|
+
return value
|
|
58
|
+
.replace(/\\/g, "\\\\")
|
|
59
|
+
.replace(/'/g, "\\x27")
|
|
60
|
+
.replace(/"/g, "\\x22")
|
|
61
|
+
.replace(/</g, "\\x3c")
|
|
62
|
+
.replace(/>/g, "\\x3e")
|
|
63
|
+
.replace(/&/g, "\\x26")
|
|
64
|
+
.replace(/\n/g, "\\n")
|
|
65
|
+
.replace(/\r/g, "\\r")
|
|
66
|
+
.replace(/\u2028/g, "\\u2028")
|
|
67
|
+
.replace(/\u2029/g, "\\u2029");
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* HTML, CSS, URL, and attribute sanitization utilities for the render pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Every piece of user-supplied content that flows into the HTML output must
|
|
6
|
+
* pass through one of these functions to prevent Cross-Site Scripting (XSS)
|
|
7
|
+
* and CSS injection attacks.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
export { escapeAttr, escapeHtml, escapeJsString, escapeStyleContent } from "./html";
|
|
12
|
+
export { isDangerousUrl } from "./url";
|
|
13
|
+
export { isValidCssColor, sanitizeCssColor, isDangerousCssValue, sanitizeStyleValue } from "./css";
|
|
14
|
+
export { isValidEmail } from "./email";
|
|
15
|
+
export { isSafeAttribute, sanitizeAttributes } from "./attributes";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function isDangerousUrl(value: string): boolean {
|
|
2
|
+
const normalized = stripControlAndWhitespace(value);
|
|
3
|
+
return /^(javascript|data|vbscript):/i.test(normalized);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const WHITESPACE = /\s/;
|
|
7
|
+
|
|
8
|
+
function stripControlAndWhitespace(value: string): string {
|
|
9
|
+
let result = "";
|
|
10
|
+
for (const char of value) {
|
|
11
|
+
const code = char.charCodeAt(0);
|
|
12
|
+
if (WHITESPACE.test(char) || code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
result += char;
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LanguageDefinition } from "../types";
|
|
2
|
+
import { escapeRegex, matchingBrackets } from "./utils";
|
|
3
|
+
|
|
4
|
+
export function buildEndPattern(
|
|
5
|
+
def: LanguageDefinition,
|
|
6
|
+
prevState: number,
|
|
7
|
+
patternIndex: number,
|
|
8
|
+
count: number,
|
|
9
|
+
captureIndex: number,
|
|
10
|
+
match: RegExpExecArray,
|
|
11
|
+
endRe: RegExp | undefined,
|
|
12
|
+
): RegExp | null {
|
|
13
|
+
if (!def.subst[prevState]?.[patternIndex] || !endRe) {
|
|
14
|
+
return endRe ?? null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let epSource = endRe.source;
|
|
18
|
+
for (let k = 0; k <= count; k++) {
|
|
19
|
+
const subIdx = captureIndex + k;
|
|
20
|
+
if (subIdx >= match.length || match[subIdx] == null) break;
|
|
21
|
+
const quoted = escapeRegex(match[subIdx]!);
|
|
22
|
+
epSource = epSource.replace(`%${k}%`, quoted);
|
|
23
|
+
epSource = epSource.replace(`%b${k}%`, matchingBrackets(quoted));
|
|
24
|
+
}
|
|
25
|
+
return new RegExp(epSource, endRe.flags);
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape HTML special characters for use inside highlighted code spans.
|
|
3
|
+
*/
|
|
4
|
+
export function escapeHighlightHtml(str: string): string {
|
|
5
|
+
if (
|
|
6
|
+
str.indexOf("&") === -1 &&
|
|
7
|
+
str.indexOf("<") === -1 &&
|
|
8
|
+
str.indexOf(">") === -1 &&
|
|
9
|
+
str.indexOf('"') === -1
|
|
10
|
+
) {
|
|
11
|
+
return str;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return str
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """);
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LanguageDefinition } from "../types";
|
|
2
|
+
|
|
3
|
+
export function resolveKeywordClass(
|
|
4
|
+
def: LanguageDefinition,
|
|
5
|
+
state: number,
|
|
6
|
+
patternIndex: number,
|
|
7
|
+
matchStr: string,
|
|
8
|
+
fallback: string,
|
|
9
|
+
): string {
|
|
10
|
+
let kwDef = def.keywords[state]?.[patternIndex];
|
|
11
|
+
if (!kwDef || kwDef === -1 || typeof kwDef !== "object" || Object.keys(kwDef).length === 0) {
|
|
12
|
+
kwDef = def.keywords[-1]?.[patternIndex];
|
|
13
|
+
}
|
|
14
|
+
if (kwDef && kwDef !== -1 && typeof kwDef === "object") {
|
|
15
|
+
for (const [group, re] of Object.entries(kwDef)) {
|
|
16
|
+
if ((re as RegExp).test(matchStr)) {
|
|
17
|
+
return def.kwmap[group] ?? fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HighlightToken } from "./token";
|
|
2
|
+
|
|
3
|
+
export function buildPartTokens(
|
|
4
|
+
str: string,
|
|
5
|
+
match: RegExpExecArray,
|
|
6
|
+
partDef: Record<number, string>,
|
|
7
|
+
captureIndex: number,
|
|
8
|
+
count: number,
|
|
9
|
+
groupStart: number,
|
|
10
|
+
matchStr: string,
|
|
11
|
+
inner: string,
|
|
12
|
+
): HighlightToken[] {
|
|
13
|
+
const parts: HighlightToken[] = [];
|
|
14
|
+
let partpos = groupStart;
|
|
15
|
+
for (let j = 1; j <= count; j++) {
|
|
16
|
+
const subIdx = j + captureIndex;
|
|
17
|
+
if (subIdx >= match.length || match[subIdx] == null || match[subIdx] === "") continue;
|
|
18
|
+
const subStr = match[subIdx]!;
|
|
19
|
+
const subStart = str.indexOf(subStr, partpos);
|
|
20
|
+
if (subStart < 0) continue;
|
|
21
|
+
if (partDef[j]) {
|
|
22
|
+
if (subStart > partpos) {
|
|
23
|
+
parts.unshift({ class: inner, content: str.substring(partpos, subStart) });
|
|
24
|
+
}
|
|
25
|
+
parts.unshift({ class: partDef[j]!, content: subStr });
|
|
26
|
+
}
|
|
27
|
+
partpos = subStart + subStr.length;
|
|
28
|
+
}
|
|
29
|
+
if (partpos < groupStart + matchStr.length) {
|
|
30
|
+
parts.unshift({
|
|
31
|
+
class: inner,
|
|
32
|
+
content: str.substring(partpos, groupStart + matchStr.length),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return parts;
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preprocess source code the same way Text_Highlighter's HTML renderer does.
|
|
3
|
+
*/
|
|
4
|
+
export function preprocessHighlightInput(input: string): string {
|
|
5
|
+
return input
|
|
6
|
+
.replace(/\r\n/g, "\n")
|
|
7
|
+
.replace(/^$/gm, " ")
|
|
8
|
+
.replace(/\t/g, " ")
|
|
9
|
+
.replace(/\s+$/, "");
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { escapeHighlightHtml } from "./html";
|
|
2
|
+
import type { HighlightToken } from "./token";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render an array of tokens to HTML with `hl-*` class spans.
|
|
6
|
+
*/
|
|
7
|
+
export function renderTokens(tokens: HighlightToken[]): string {
|
|
8
|
+
if (tokens.length === 0) return "";
|
|
9
|
+
|
|
10
|
+
let html = "";
|
|
11
|
+
let lastClass = "";
|
|
12
|
+
|
|
13
|
+
for (const token of tokens) {
|
|
14
|
+
if (token.content.length === 0) continue;
|
|
15
|
+
const escaped = escapeHighlightHtml(token.content);
|
|
16
|
+
if (token.class !== lastClass) {
|
|
17
|
+
if (lastClass) {
|
|
18
|
+
html += "</span>";
|
|
19
|
+
}
|
|
20
|
+
html += `<span class="hl-${token.class}">`;
|
|
21
|
+
lastClass = token.class;
|
|
22
|
+
}
|
|
23
|
+
html += escaped;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (lastClass) {
|
|
27
|
+
html += "</span>";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return `<div class="hl-main"><pre>${html}</pre></div>`;
|
|
31
|
+
}
|