-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathMarkdown.jsx
168 lines (151 loc) · 4.56 KB
/
Markdown.jsx
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
* TEAM: shipment_data
*
* @flow strict-local
*/
import * as React from "react";
import ReactMarkdown from "react-markdown";
import type {
Renderers,
HtmlRendererProps,
BaseRendererProps,
} from "react-markdown";
import ReactDOMServer from "react-dom/server";
import xss from "xss";
export type MarkdownProps = {|
+source: ?string,
+renderer?: Renderers,
+enableHtml?: boolean,
+breaks?: boolean,
|};
export default function Markdown(props: MarkdownProps) {
const {source, enableHtml, breaks = true} = props;
let renderer = {...defaultRenderer, ...props.renderer};
if (breaks) {
const breakRenderer = renderer.break;
const textRenderer = renderer.text;
renderer = {
...renderer,
text: wrapTextWithBreaks(textRenderer, breakRenderer),
};
}
return (
<ReactMarkdown
source={source || ""}
escapeHtml={!enableHtml}
// enableHtml is only used to control escapeHtml, because it is a safer
// failure mode to be able to see the content in the escaped HTML, whereas
// if content just disappears entirely someone might be blocked.
skipHtml={false}
renderers={renderer}
/>
);
}
export function renderMarkdownToHtmlString(props: MarkdownProps): string {
const renderer: Renderers = {...defaultRenderer, ...props.renderer};
const newProps = {...props, renderer};
return ReactDOMServer.renderToStaticMarkup(<Markdown {...newProps} />);
}
export function renderMarkdownToPlainString(props: MarkdownProps): string {
const renderer: Renderers = {...plainTextRenderer, ...props.renderer};
return renderMarkdownToHtmlString({...props, renderer});
}
const defaultRenderer = {
break: () => <br />,
text: ({children}) => children,
html: XSSSafeHtml,
};
function wrapTextWithBreaks(
TextRenderer: BaseRendererProps => React.Node,
BreakRenderer: BaseRendererProps => React.Node
) {
function wrappedTextRenderer({children}: {|+children: string|}) {
const parts = children.split(/(\n+)/).map((part, index) => {
if (part.includes("\n")) {
return (
// eslint-disable-next-line react/no-array-index-key
<BreakRenderer key={`br-${index}`}>{undefined}</BreakRenderer>
);
}
return (
// eslint-disable-next-line react/no-array-index-key
<TextRenderer key={`str-${index}`}>{part}</TextRenderer>
);
});
// Wrap with span as a workaround for removeChild error
// See: https://github.com/facebook/react/issues/11538#issuecomment-390386520
return <span>{parts}</span>;
}
return wrappedTextRenderer;
}
function ChildPassthroughRenderer(props: {+children: React.Node}) {
return <>{props.children}</>;
}
function nullRenderer() {
return null;
}
const plainTextRenderer: Renderers = {
break: () => "\n",
paragraph: ChildPassthroughRenderer,
emphasis: ChildPassthroughRenderer,
strong: ChildPassthroughRenderer,
thematicBreak: () => "\n",
blockquote: ChildPassthroughRenderer,
delete: ChildPassthroughRenderer,
link: ChildPassthroughRenderer,
linkReference: ChildPassthroughRenderer,
image: nullRenderer,
imageReference: nullRenderer,
table: ChildPassthroughRenderer,
tableHead: ChildPassthroughRenderer,
tableBody: ChildPassthroughRenderer,
tableRow: props => (
<>
{props.children}
{"\n"}
</>
),
tableCell: props => <> {props.children}</>,
root: ChildPassthroughRenderer,
text: ChildPassthroughRenderer,
list: ChildPassthroughRenderer,
listItem: props => (
<>
{props.children}
{"\n"}
</>
),
heading: ChildPassthroughRenderer,
inlineCode: ChildPassthroughRenderer,
code: props => props.value,
html: nullRenderer,
};
const xssOpts = {
whiteList: xss.getDefaultWhiteList(),
onTagAttr: (tag: string, name: string) => {
// If the link has a rel tag, we overwrite it with noreferrer/noopener to
// prevent tabnapping. We can't just insert it into all tags because xss
// does not provide a callback that allows you to insert new attributes.
if (tag === "a" && name === "rel") {
return 'rel="noreferrer noopener"';
}
return undefined;
},
};
function XSSSafeHtml(props: HtmlRendererProps) {
const safeHtml = React.useMemo(() => xss(props.value, xssOpts), [
props.value,
]);
if (props.skipHtml) {
return null;
}
if (props.escapeHtml) {
return <>{safeHtml}</>;
}
if (props.isBlock) {
// eslint-disable-next-line react/no-danger
return <div dangerouslySetInnerHTML={{__html: safeHtml}} />;
}
// eslint-disable-next-line react/no-danger
return <span dangerouslySetInnerHTML={{__html: safeHtml}} />;
}