fix: sanitize markdown renderer

This commit is contained in:
aqilaziz 2026-05-10 21:27:38 +07:00
parent c0dcc9497b
commit bed7f35777
3 changed files with 60 additions and 41 deletions

View file

@ -13,6 +13,7 @@
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",
@ -472,6 +473,8 @@
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
"hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
@ -690,6 +693,8 @@
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
"rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],

View file

@ -24,6 +24,7 @@
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",

View file

@ -1,6 +1,7 @@
import type { CSSProperties } from 'react';
import ReactMarkdown, { type Components } from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
interface MarkdownRendererProps {
content: string;
@ -8,54 +9,66 @@ interface MarkdownRendererProps {
style?: CSSProperties;
}
function withoutNode<T extends { node?: unknown }>(props: T) {
const rest = { ...props };
delete rest.node;
return rest;
}
const rehypePlugins = [rehypeRaw, rehypeSanitize];
const markdownComponents: Components = {
img: ({ ...props }) => (
<img
{...withoutNode(props)}
style={{
maxWidth: '100%',
borderRadius: '4px',
border: '1px solid var(--border-subtle)',
}}
/>
),
a: ({ ...props }) => (
<a
{...withoutNode(props)}
style={{ color: 'var(--text)', textDecoration: 'underline' }}
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ ...props }) => (
<p {...withoutNode(props)} style={{ marginTop: '0.5em', marginBottom: '0.5em' }} />
),
pre: ({ ...props }) => (
<pre
{...withoutNode(props)}
style={{
background: 'var(--bg-secondary)',
padding: '8px',
borderRadius: '4px',
overflowX: 'auto',
}}
/>
),
code: ({ ...props }) => (
<code {...withoutNode(props)} style={{ fontFamily: 'var(--font-mono)', fontSize: '11px' }} />
),
img: ({ node, ...props }) => {
void node;
return (
<img
{...props}
style={{
maxWidth: '100%',
borderRadius: '4px',
border: '1px solid var(--border-subtle)',
}}
/>
);
},
a: ({ node, ...props }) => {
void node;
return (
<a
{...props}
style={{ color: 'var(--text)', textDecoration: 'underline' }}
target="_blank"
rel="noopener noreferrer"
/>
);
},
p: ({ node, ...props }) => {
void node;
return <p {...props} style={{ marginTop: '0.5em', marginBottom: '0.5em' }} />;
},
pre: ({ node, ...props }) => {
void node;
return (
<pre
{...props}
style={{
background: 'var(--bg-secondary)',
padding: '8px',
borderRadius: '4px',
overflowX: 'auto',
}}
/>
);
},
code: ({ node, ...props }) => {
void node;
return <code {...props} style={{ fontFamily: 'var(--font-mono)', fontSize: '11px' }} />;
},
};
export function MarkdownRenderer({ content, className, style }: MarkdownRendererProps) {
return (
<div className={className} style={style}>
<ReactMarkdown rehypePlugins={[rehypeRaw]} components={markdownComponents}>
<ReactMarkdown rehypePlugins={rehypePlugins} components={markdownComponents}>
{content}
</ReactMarkdown>
</div>