Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/components/html-ssr.tsx
5808 views
1
/*
2
React component for rendering an HTML string.
3
4
- suitable for server side rendering (e.g., nextjs)
5
- parses and displays math using KaTeX
6
- sanitizes the HTML for XSS attacks, etc., so it is safe to display to users
7
- optionally transforms links
8
9
TODO: This should eventually completely replace ./html.tsx:
10
- syntax highlighting
11
- searching
12
- opens links in a new tab, or makes clicking anchor tags runs a function
13
instead of opening a new tab so can open internal cocalc links inside cocalc.
14
*/
15
16
import React from "react";
17
import htmlReactParser, {
18
attributesToProps,
19
domToReact,
20
Element,
21
Text,
22
} from "html-react-parser";
23
import sanitizeHtml from "sanitize-html";
24
import { useFileContext } from "@cocalc/frontend/lib/file-context";
25
import DefaultMath from "@cocalc/frontend/components/math/ssr";
26
import { MathJaxConfig } from "@cocalc/util/mathjax-config";
27
import { decodeHTML } from "entities";
28
29
const URL_ATTRIBS = ["src", "href", "data"];
30
const MATH_SKIP_TAGS = new Set<string>(MathJaxConfig.tex2jax.skipTags);
31
32
export default function HTML({
33
value,
34
style,
35
inline,
36
}: {
37
value: string;
38
style?: React.CSSProperties;
39
inline?: boolean;
40
}) {
41
const { urlTransform, AnchorTagComponent, noSanitize, MathComponent } =
42
useFileContext();
43
if (!noSanitize) {
44
value = sanitizeHtml(value, {
45
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img", "iframe"]),
46
allowedAttributes: {
47
...sanitizeHtml.defaults.allowedAttributes,
48
iframe: [
49
"src",
50
"width",
51
"height",
52
"title",
53
"allow",
54
"allowfullscreen",
55
"referrerpolicy",
56
"loading",
57
"frameborder",
58
],
59
},
60
allowedIframeHostnames: [
61
"www.youtube.com",
62
"youtube.com",
63
"www.youtube-nocookie.com",
64
"youtube-nocookie.com",
65
"player.vimeo.com",
66
],
67
});
68
}
69
if (value.trimLeft().startsWith("<html>")) {
70
// Sage output formulas are wrapped in "<html>" for some stupid reason, which
71
// probably originates with a ridiculous design choice that Tom Boothby or I
72
// made in 2006 related to "wiki" formatting in Sage notebooks. If we don't strip
73
// this, then htmlReactParser just deletes the whole documents, since html is
74
// not a valid tag inside the DOM. We do this in a really minimally flexible way
75
// to reduce the chances to 0 that we apply this when we shouldn't.
76
value = value.trim().slice("<html>".length, -"</html>".length);
77
}
78
let options: any = {};
79
options.replace = (domNode) => {
80
if (!/^[a-zA-Z]+[0-9]?$/.test(domNode.name)) {
81
// Without this, if user gives html input that is a malformed tag then all of React
82
// completely crashes, which is not desirable for us. On the other hand, I prefer not
83
// to always completely sanitize input, since that can do a lot we don't want to do
84
// and may be expensive. See
85
// https://github.com/remarkablemark/html-react-parser/issues/60#issuecomment-398588573
86
return React.createElement(React.Fragment);
87
}
88
if (domNode instanceof Text) {
89
if (hasAncestor(domNode, MATH_SKIP_TAGS)) {
90
// Do NOT convert Text to math inside a pre/code tree environment.
91
return;
92
}
93
const { data } = domNode;
94
if (MathComponent != null) {
95
return <MathComponent data={decodeHTML(data)} />;
96
}
97
return <DefaultMath data={decodeHTML(data)} />;
98
}
99
100
try {
101
if (!(domNode instanceof Element)) return;
102
const { name, children, attribs } = domNode;
103
104
if (name == "script") {
105
const type = domNode.attribs?.type?.toLowerCase();
106
if (type?.startsWith("math/tex")) {
107
const child = domNode.children?.[0];
108
if (child instanceof Text && child.data) {
109
let data = "$" + decodeHTML(child.data) + "$";
110
if (type.includes("display")) {
111
data = "$" + data + "$";
112
}
113
if (MathComponent != null) {
114
return <MathComponent data={data} />;
115
}
116
return <DefaultMath data={data} />;
117
}
118
}
119
}
120
121
if (AnchorTagComponent != null && name == "a") {
122
return (
123
<AnchorTagComponent {...attribs}>
124
{domToReact(children as any, options)}
125
</AnchorTagComponent>
126
);
127
}
128
129
if (noSanitize && urlTransform != null && attribs != null) {
130
// since we did not sanitize the HTML (which also does urlTransform),
131
// we have to do the urlTransform here instead.
132
for (const attrib of URL_ATTRIBS) {
133
if (attribs[attrib] != null) {
134
const x = urlTransform(attribs[attrib]);
135
if (x != null) {
136
const props = attributesToProps(attribs);
137
props[attrib] = x;
138
return React.createElement(
139
name,
140
props,
141
children && children?.length > 0
142
? domToReact(children as any, options)
143
: undefined,
144
);
145
}
146
}
147
}
148
}
149
} catch (err) {
150
console.log("WARNING -- issue parsing HTML", err);
151
}
152
};
153
154
if (inline) {
155
return <span style={style}>{htmlReactParser(value, options)}</span>;
156
} else {
157
return <div style={style}>{htmlReactParser(value, options)}</div>;
158
}
159
}
160
161
function hasAncestor(domNode, tags: Set<string>): boolean {
162
const { parent } = domNode;
163
if (!(parent instanceof Element)) return false;
164
if (tags.has(parent.name)) return true;
165
return hasAncestor(parent, tags);
166
}
167
168