@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,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete set of CSS Level 4 named colors plus CSS-wide keywords
|
|
3
|
+
* (`transparent`, `currentcolor`, `inherit`, `initial`, `unset`).
|
|
4
|
+
*/
|
|
5
|
+
import { isValidCssColorFunction } from "./css-color-functions";
|
|
6
|
+
|
|
7
|
+
const CSS_NAMED_COLORS = new Set([
|
|
8
|
+
"aliceblue",
|
|
9
|
+
"antiquewhite",
|
|
10
|
+
"aqua",
|
|
11
|
+
"aquamarine",
|
|
12
|
+
"azure",
|
|
13
|
+
"beige",
|
|
14
|
+
"bisque",
|
|
15
|
+
"black",
|
|
16
|
+
"blanchedalmond",
|
|
17
|
+
"blue",
|
|
18
|
+
"blueviolet",
|
|
19
|
+
"brown",
|
|
20
|
+
"burlywood",
|
|
21
|
+
"cadetblue",
|
|
22
|
+
"chartreuse",
|
|
23
|
+
"chocolate",
|
|
24
|
+
"coral",
|
|
25
|
+
"cornflowerblue",
|
|
26
|
+
"cornsilk",
|
|
27
|
+
"crimson",
|
|
28
|
+
"cyan",
|
|
29
|
+
"darkblue",
|
|
30
|
+
"darkcyan",
|
|
31
|
+
"darkgoldenrod",
|
|
32
|
+
"darkgray",
|
|
33
|
+
"darkgreen",
|
|
34
|
+
"darkgrey",
|
|
35
|
+
"darkkhaki",
|
|
36
|
+
"darkmagenta",
|
|
37
|
+
"darkolivegreen",
|
|
38
|
+
"darkorange",
|
|
39
|
+
"darkorchid",
|
|
40
|
+
"darkred",
|
|
41
|
+
"darksalmon",
|
|
42
|
+
"darkseagreen",
|
|
43
|
+
"darkslateblue",
|
|
44
|
+
"darkslategray",
|
|
45
|
+
"darkslategrey",
|
|
46
|
+
"darkturquoise",
|
|
47
|
+
"darkviolet",
|
|
48
|
+
"deeppink",
|
|
49
|
+
"deepskyblue",
|
|
50
|
+
"dimgray",
|
|
51
|
+
"dimgrey",
|
|
52
|
+
"dodgerblue",
|
|
53
|
+
"firebrick",
|
|
54
|
+
"floralwhite",
|
|
55
|
+
"forestgreen",
|
|
56
|
+
"fuchsia",
|
|
57
|
+
"gainsboro",
|
|
58
|
+
"ghostwhite",
|
|
59
|
+
"gold",
|
|
60
|
+
"goldenrod",
|
|
61
|
+
"gray",
|
|
62
|
+
"green",
|
|
63
|
+
"greenyellow",
|
|
64
|
+
"grey",
|
|
65
|
+
"honeydew",
|
|
66
|
+
"hotpink",
|
|
67
|
+
"indianred",
|
|
68
|
+
"indigo",
|
|
69
|
+
"ivory",
|
|
70
|
+
"khaki",
|
|
71
|
+
"lavender",
|
|
72
|
+
"lavenderblush",
|
|
73
|
+
"lawngreen",
|
|
74
|
+
"lemonchiffon",
|
|
75
|
+
"lightblue",
|
|
76
|
+
"lightcoral",
|
|
77
|
+
"lightcyan",
|
|
78
|
+
"lightgoldenrodyellow",
|
|
79
|
+
"lightgray",
|
|
80
|
+
"lightgreen",
|
|
81
|
+
"lightgrey",
|
|
82
|
+
"lightpink",
|
|
83
|
+
"lightsalmon",
|
|
84
|
+
"lightseagreen",
|
|
85
|
+
"lightskyblue",
|
|
86
|
+
"lightslategray",
|
|
87
|
+
"lightslategrey",
|
|
88
|
+
"lightsteelblue",
|
|
89
|
+
"lightyellow",
|
|
90
|
+
"lime",
|
|
91
|
+
"limegreen",
|
|
92
|
+
"linen",
|
|
93
|
+
"magenta",
|
|
94
|
+
"maroon",
|
|
95
|
+
"mediumaquamarine",
|
|
96
|
+
"mediumblue",
|
|
97
|
+
"mediumorchid",
|
|
98
|
+
"mediumpurple",
|
|
99
|
+
"mediumseagreen",
|
|
100
|
+
"mediumslateblue",
|
|
101
|
+
"mediumspringgreen",
|
|
102
|
+
"mediumturquoise",
|
|
103
|
+
"mediumvioletred",
|
|
104
|
+
"midnightblue",
|
|
105
|
+
"mintcream",
|
|
106
|
+
"mistyrose",
|
|
107
|
+
"moccasin",
|
|
108
|
+
"navajowhite",
|
|
109
|
+
"navy",
|
|
110
|
+
"oldlace",
|
|
111
|
+
"olive",
|
|
112
|
+
"olivedrab",
|
|
113
|
+
"orange",
|
|
114
|
+
"orangered",
|
|
115
|
+
"orchid",
|
|
116
|
+
"palegoldenrod",
|
|
117
|
+
"palegreen",
|
|
118
|
+
"paleturquoise",
|
|
119
|
+
"palevioletred",
|
|
120
|
+
"papayawhip",
|
|
121
|
+
"peachpuff",
|
|
122
|
+
"peru",
|
|
123
|
+
"pink",
|
|
124
|
+
"plum",
|
|
125
|
+
"powderblue",
|
|
126
|
+
"purple",
|
|
127
|
+
"rebeccapurple",
|
|
128
|
+
"red",
|
|
129
|
+
"rosybrown",
|
|
130
|
+
"royalblue",
|
|
131
|
+
"saddlebrown",
|
|
132
|
+
"salmon",
|
|
133
|
+
"sandybrown",
|
|
134
|
+
"seagreen",
|
|
135
|
+
"seashell",
|
|
136
|
+
"sienna",
|
|
137
|
+
"silver",
|
|
138
|
+
"skyblue",
|
|
139
|
+
"slateblue",
|
|
140
|
+
"slategray",
|
|
141
|
+
"slategrey",
|
|
142
|
+
"snow",
|
|
143
|
+
"springgreen",
|
|
144
|
+
"steelblue",
|
|
145
|
+
"tan",
|
|
146
|
+
"teal",
|
|
147
|
+
"thistle",
|
|
148
|
+
"tomato",
|
|
149
|
+
"turquoise",
|
|
150
|
+
"violet",
|
|
151
|
+
"wheat",
|
|
152
|
+
"white",
|
|
153
|
+
"whitesmoke",
|
|
154
|
+
"yellow",
|
|
155
|
+
"yellowgreen",
|
|
156
|
+
"transparent",
|
|
157
|
+
"currentcolor",
|
|
158
|
+
"inherit",
|
|
159
|
+
"initial",
|
|
160
|
+
"unset",
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validate that a string is a safe CSS color value.
|
|
165
|
+
*/
|
|
166
|
+
export function isValidCssColor(color: string): boolean {
|
|
167
|
+
const trimmed = color.trim().toLowerCase();
|
|
168
|
+
|
|
169
|
+
if (!trimmed) return false;
|
|
170
|
+
if (CSS_NAMED_COLORS.has(trimmed)) return true;
|
|
171
|
+
|
|
172
|
+
if (/^#[0-9a-f]{3}([0-9a-f])?$/.test(trimmed) || /^#[0-9a-f]{6}([0-9a-f]{2})?$/.test(trimmed)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isValidCssColorFunction(trimmed)) return true;
|
|
177
|
+
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function sanitizeCssColor(color: string, fallback = "inherit"): string {
|
|
182
|
+
return isValidCssColor(color) ? color : fallback;
|
|
183
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { normalizeCssValue } from "./css-normalize";
|
|
2
|
+
import { isCssUrlAllowed, iterateCssUrls } from "./css-urls";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check whether a CSS property value contains dangerous patterns that
|
|
6
|
+
* could enable script execution or disallowed external resource loading.
|
|
7
|
+
*/
|
|
8
|
+
export function isDangerousCssValue(value: string): boolean {
|
|
9
|
+
const normalized = normalizeCssValue(value);
|
|
10
|
+
|
|
11
|
+
for (const { inner, malformed } of iterateCssUrls(normalized)) {
|
|
12
|
+
if (malformed) return true;
|
|
13
|
+
if (!isCssUrlAllowed(inner)) return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (normalized.includes("expression(")) return true;
|
|
17
|
+
if (normalized.includes("-moz-binding")) return true;
|
|
18
|
+
if (normalized.includes("behavior:")) return true;
|
|
19
|
+
if (normalized.includes("@import")) return true;
|
|
20
|
+
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a CSS value by resolving escape sequences, removing comments,
|
|
3
|
+
* stripping whitespace and control characters, and lowercasing.
|
|
4
|
+
*/
|
|
5
|
+
export function normalizeCssValue(value: string): string {
|
|
6
|
+
let result = value;
|
|
7
|
+
|
|
8
|
+
result = stripCssComments(result);
|
|
9
|
+
result = result.replace(/\\(?:\r\n|[\n\r\f])/g, "");
|
|
10
|
+
result = result.replace(/\\([0-9a-f]{1,6})\s?/gi, (_, hex) => {
|
|
11
|
+
const code = Number.parseInt(hex, 16);
|
|
12
|
+
return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : "";
|
|
13
|
+
});
|
|
14
|
+
result = result.replace(/\\(.)/g, "$1");
|
|
15
|
+
result = stripControlAndWhitespace(result);
|
|
16
|
+
|
|
17
|
+
return result.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const WHITESPACE = /\s/;
|
|
21
|
+
|
|
22
|
+
function stripCssComments(value: string): string {
|
|
23
|
+
let result = "";
|
|
24
|
+
let cursor = 0;
|
|
25
|
+
|
|
26
|
+
while (cursor < value.length) {
|
|
27
|
+
const start = value.indexOf("/*", cursor);
|
|
28
|
+
if (start === -1) {
|
|
29
|
+
result += value.slice(cursor);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
result += value.slice(cursor, start);
|
|
34
|
+
const end = value.indexOf("*/", start + 2);
|
|
35
|
+
if (end === -1) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
cursor = end + 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stripControlAndWhitespace(value: string): string {
|
|
45
|
+
let result = "";
|
|
46
|
+
for (const char of value) {
|
|
47
|
+
const code = char.charCodeAt(0);
|
|
48
|
+
if (WHITESPACE.test(char) || code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
result += char;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
@@ -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
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Pure-JavaScript hash functions for generating deterministic element IDs.
|
|
4
|
+
*
|
|
5
|
+
* These functions use FNV-1a internally and produce hex strings whose
|
|
6
|
+
* lengths match SHA-1 (40 chars) and MD5 (32 chars) for compatibility
|
|
7
|
+
* with Wikidot's ID generation patterns. Cryptographic security is not
|
|
8
|
+
* required; the hashes only need to be deterministic and well-distributed.
|
|
9
|
+
*
|
|
10
|
+
* `node:crypto` is intentionally avoided because `bunup`'s ESM build
|
|
11
|
+
* injects `createRequire` from `node:module`, which is incompatible
|
|
12
|
+
* with browser environments.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a 40-character hex hash (same length as SHA-1) from the input string.
|
|
19
|
+
*
|
|
20
|
+
* @param input - The string to hash.
|
|
21
|
+
* @returns A 40-character lowercase hex string.
|
|
22
|
+
*/
|
|
23
|
+
export function syncHashSha1(input: string): string {
|
|
24
|
+
return fnv1aHash(input, 40);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a 32-character hex hash (same length as MD5) from the input string.
|
|
29
|
+
*
|
|
30
|
+
* @param input - The string to hash.
|
|
31
|
+
* @returns A 32-character lowercase hex string.
|
|
32
|
+
*/
|
|
33
|
+
export function syncHashMd5(input: string): string {
|
|
34
|
+
return fnv1aHash(input, 32);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute an FNV-1a hash of the given input and return a hex string of
|
|
39
|
+
* the requested length.
|
|
40
|
+
*
|
|
41
|
+
* Because a single FNV-1a pass produces only 32 bits (8 hex chars), the
|
|
42
|
+
* function runs multiple rounds with different initial seeds (XOR of
|
|
43
|
+
* the round index into the offset basis) and concatenates the results
|
|
44
|
+
* to reach the desired length.
|
|
45
|
+
*
|
|
46
|
+
* @param input - The string to hash.
|
|
47
|
+
* @param hexLen - Desired length of the output hex string (e.g. 32 or 40).
|
|
48
|
+
* @returns A lowercase hex string of exactly `hexLen` characters.
|
|
49
|
+
*/
|
|
50
|
+
function fnv1aHash(input: string, hexLen: number): string {
|
|
51
|
+
let result = "";
|
|
52
|
+
const rounds = Math.ceil(hexLen / 8);
|
|
53
|
+
for (let round = 0; round < rounds; round++) {
|
|
54
|
+
let h = 0x811c9dc5 ^ round;
|
|
55
|
+
for (let i = 0; i < input.length; i++) {
|
|
56
|
+
h ^= input.charCodeAt(i);
|
|
57
|
+
h = Math.imul(h, 0x01000193);
|
|
58
|
+
}
|
|
59
|
+
result += (h >>> 0).toString(16).padStart(8, "0");
|
|
60
|
+
}
|
|
61
|
+
return result.substring(0, hexLen);
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML renderer for the Wikidot AST.
|
|
3
|
+
*
|
|
4
|
+
* Takes a `SyntaxTree` produced by `@wdprlib/parser` and serialises
|
|
5
|
+
* it to an HTML string. Page context, user resolution, and security
|
|
6
|
+
* settings (embed allowlists, iframe sandboxing) are configurable via
|
|
7
|
+
* {@link RenderOptions}.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { parse } from "@wdprlib/parser";
|
|
11
|
+
* import { renderToHtml } from "@wdprlib/render";
|
|
12
|
+
*
|
|
13
|
+
* const html = renderToHtml(parse("**hello**"));
|
|
14
|
+
* // => "<p><strong>hello</strong></p>"
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { renderToHtml } from "./render";
|
|
21
|
+
export type { RenderOptions, RenderResolvers, PageContext, ResolvedUser } from "./types";
|
|
22
|
+
export { DEFAULT_EMBED_ALLOWLIST } from "./elements/embed-block";
|
|
23
|
+
|
|
24
|
+
// Wikitext settings (re-exported from @wdprlib/ast)
|
|
25
|
+
export type { WikitextMode, WikitextSettings } from "@wdprlib/ast";
|
|
26
|
+
export { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
@@ -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
|
+
}
|