@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
- return (_jsx("div", { className: `md${className ? ` ${className}` : ''}`,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/design",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "wtfalch's design system: themeable components on a fixed token vocabulary.",
5
5
  "keywords": [
6
6
  "design-system",