-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
135 lines (118 loc) · 3.89 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
const hljs = require("highlight.js");
const { marked } = require("marked");
const { parseFragment, serialize } = require("parse5");
const xss = require("xss");
const {
compile,
serialize: cssSerialize,
stringify,
middleware,
} = require("stylis");
const onTagAttr = (tag, name, value) => {
if (tag === "link" && name === "rel")
// Only allow link tags if they're intended for stylesheets.
return value === "stylesheet" ? 'rel="stylesheet"' : "";
if (tag === "input" && name === "type")
// Allow checkboxes from `- [ ]`
return value === "checkbox" ? 'type="checkbox"' : "";
};
const whiteList = {
...xss.whiteList,
// Allow marquees
marquee: [
"behavior",
"direction",
"hspace",
"loop",
"scrollamount",
"scrolldelay",
"truespeed",
"vspace",
],
style: [], // Allow plain style tags
link: ["rel", "href"], // Allow link tags for external stylesheets
input: ["type"], // Allow checkbox inputs
};
// Enable custom classes & ids to assist with CSS
for (const key of Object.keys(whiteList)) whiteList[key].push("class", "id");
const escapeCSS = (text, urlTransform) => {
return cssSerialize(
compile(text),
middleware([
(elem) => {
if (elem.type === "decl" && elem.children.startsWith("url(")) {
const quote = ['"', "'"].includes(elem.children.charAt(4)) ? elem.children.charAt(4) : ")";
const start = quote === ")" ? 4 : 5;
const url = elem.children.slice(start, elem.children.indexOf(quote, start));
const surround = quote === ")" ? "" : quote;
elem.return = `${elem.props}:url(${surround}${urlTransform(
url,
"style"
)}${surround})${elem.children.slice(elem.children.indexOf(quote, start) + 1 + (quote !== ")" ? 1 : 0))};`;
}
},
stringify,
])
)
}
const parseNode = (node, urlTransform) => {
if (node.nodeName === "#text" && node.parentNode.nodeName === "style") {
node.value = escapeCSS(node.value, urlTransform);
}
if (node.childNodes)
node.childNodes.forEach((node) => parseNode(node, urlTransform));
};
/** Safely render markdown. */
exports.render = (md, urlTransform = undefined) => {
if (!urlTransform) urlTransform = (url) => url;
const closureOnTagAttr = (tag, name, value, isWhiteAttr) => {
const original = onTagAttr(tag, name, value);
if (original) return original;
if (!isWhiteAttr) return undefined;
if (["img", "audio", "video"].includes(tag) && name === "src")
return urlTransform(value, tag)
? `src="${urlTransform(value, tag)}"`
: "";
if (tag === "link" && name === "href")
return urlTransform(value, tag)
? `href="${urlTransform(value, tag)}"`
: "";
if (tag === "a" && name === "href")
return urlTransform(value, tag)
? `href="${urlTransform(value, tag)}"`
: "";
};
const tree = parseFragment(
xss(
marked(md, {
highlight: (code, lang) =>
lang && hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang, ignoreIllegals: true })
.value
: code,
smartypants: true,
smartLists: true,
}),
{
onTagAttr: closureOnTagAttr,
whiteList,
}
)
);
tree.childNodes.forEach((node) => parseNode(node, urlTransform));
return serialize(tree);
};
exports.escapeCSS = escapeCSS;
exports.toPlainText = (str) =>
xss(str, {
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: ["script", "style"],
})
// Handle the main entities that will occur, cba pulling in an entire lib for all.
.replaceAll(">", ">")
.replaceAll("<", "<")
.replaceAll("&", "&")
.replaceAll(""", '"')
.replaceAll("'", "'")
.replaceAll(" ", " ");