@wtfalch/design 0.3.1 → 0.3.2
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.
|
@@ -4,6 +4,14 @@ import DOMPurify from 'dompurify';
|
|
|
4
4
|
*
|
|
5
5
|
* Sanitised without exception: this is text produced by a model, which may be
|
|
6
6
|
* echoing content it read from a file, so it is never trusted HTML.
|
|
7
|
+
*
|
|
8
|
+
* **On the server, the text itself.** DOMPurify sanitises through a DOM, and a
|
|
9
|
+
* server rendering React has none: `sanitize` is not even a function there,
|
|
10
|
+
* and the first app to server-render a comment found out. Rather than a second
|
|
11
|
+
* sanitiser for the server, which would be two behaviours for one string, the
|
|
12
|
+
* server (and the first client paint, so hydration agrees) renders the text
|
|
13
|
+
* escaped by React, and the sanitised HTML takes its place once mounted. What
|
|
14
|
+
* reaches a browser is never HTML that DOMPurify has not seen.
|
|
7
15
|
*/
|
|
8
16
|
export default function Markdown({ text, sanitize, className, }: {
|
|
9
17
|
text: string;
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import DOMPurify from 'dompurify';
|
|
3
3
|
import { marked } from 'marked';
|
|
4
|
-
import { useMemo } from 'react';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
5
|
marked.setOptions({ gfm: true, breaks: true });
|
|
6
6
|
/**
|
|
7
7
|
* Render model output as markdown.
|
|
8
8
|
*
|
|
9
9
|
* Sanitised without exception: this is text produced by a model, which may be
|
|
10
10
|
* echoing content it read from a file, so it is never trusted HTML.
|
|
11
|
+
*
|
|
12
|
+
* **On the server, the text itself.** DOMPurify sanitises through a DOM, and a
|
|
13
|
+
* server rendering React has none: `sanitize` is not even a function there,
|
|
14
|
+
* and the first app to server-render a comment found out. Rather than a second
|
|
15
|
+
* sanitiser for the server, which would be two behaviours for one string, the
|
|
16
|
+
* server (and the first client paint, so hydration agrees) renders the text
|
|
17
|
+
* escaped by React, and the sanitised HTML takes its place once mounted. What
|
|
18
|
+
* reaches a browser is never HTML that DOMPurify has not seen.
|
|
11
19
|
*/
|
|
12
20
|
export default function Markdown({ text, sanitize, className, }) {
|
|
21
|
+
const [mounted, setMounted] = useState(false);
|
|
22
|
+
useEffect(() => setMounted(true), []);
|
|
13
23
|
const html = useMemo(() => {
|
|
14
|
-
if (!text)
|
|
15
|
-
return
|
|
24
|
+
if (!text || !mounted)
|
|
25
|
+
return null;
|
|
16
26
|
const raw = marked.parse(text, { async: false });
|
|
17
27
|
return DOMPurify.sanitize(raw, {
|
|
18
28
|
// No iframes, no forms, no event handlers -- prose, code and tables only.
|
|
@@ -20,10 +30,15 @@ export default function Markdown({ text, sanitize, className, }) {
|
|
|
20
30
|
FORBID_ATTR: ['style', 'onerror', 'onload', 'onclick'],
|
|
21
31
|
...sanitize,
|
|
22
32
|
});
|
|
23
|
-
}, [text, sanitize]);
|
|
33
|
+
}, [text, mounted, sanitize]);
|
|
24
34
|
if (!text)
|
|
25
35
|
return null;
|
|
26
|
-
|
|
36
|
+
const classes = `md${className ? ` ${className}` : ''}`;
|
|
37
|
+
if (html === null) {
|
|
38
|
+
// The server, and the client until its first effect: the words, escaped.
|
|
39
|
+
return (_jsx("div", { className: classes, "data-md": "plain", children: _jsx("p", { children: text }) }));
|
|
40
|
+
}
|
|
41
|
+
return (_jsx("div", { className: classes,
|
|
27
42
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: the string is DOMPurify output, with the config above.
|
|
28
43
|
dangerouslySetInnerHTML: { __html: html } }));
|
|
29
44
|
}
|