Skip to content

Commit 837171a

Browse files
committed
add markdown rendering support to PortableTextRenderer
1 parent dc65253 commit 837171a

File tree

1 file changed

+123
-3
lines changed

1 file changed

+123
-3
lines changed

components/portable-text-renderer.tsx

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,118 @@ import Link from "next/link";
44
import { YouTubeEmbed } from "@next/third-parties/google";
55
import { Highlight, themes } from "prism-react-renderer";
66
import { CopyButton } from "@/components/ui/copy-button";
7+
import ReactMarkdown from "react-markdown";
8+
import { ReactNode, Children, isValidElement } from "react";
9+
10+
const mdComponents = {
11+
h1: ({ children }: { children?: ReactNode }) => (
12+
<h1 className="text-3xl font-semibold mb-4 mt-6">{children}</h1>
13+
),
14+
h2: ({ children }: { children?: ReactNode }) => (
15+
<h2 className="text-2xl font-semibold mb-4 mt-6">{children}</h2>
16+
),
17+
h3: ({ children }: { children?: ReactNode }) => (
18+
<h3 className="text-xl font-semibold mb-4 mt-6">{children}</h3>
19+
),
20+
p: ({ children }: { children?: ReactNode }) => (
21+
<p className="mb-4 leading-7">{children}</p>
22+
),
23+
ul: ({ children }: { children?: ReactNode }) => (
24+
<ul className="pl-6 mb-4 list-disc list-inside">{children}</ul>
25+
),
26+
ol: ({ children }: { children?: ReactNode }) => (
27+
<ol className="pl-6 mb-4 list-decimal list-inside">{children}</ol>
28+
),
29+
li: ({ children }: { children?: ReactNode }) => (
30+
<li className="mb-2">{children}</li>
31+
),
32+
a: ({ href, children, ...props }: any) => {
33+
const isExternal =
34+
(href || "").startsWith("http") ||
35+
(href || "").startsWith("https") ||
36+
(href || "").startsWith("mailto");
37+
return (
38+
<Link
39+
href={href || "#"}
40+
target={isExternal ? "_blank" : undefined}
41+
rel={isExternal ? "noopener" : undefined}
42+
className="underline"
43+
{...props}
44+
>
45+
{children}
46+
</Link>
47+
);
48+
},
49+
code: ({
50+
node,
51+
inline,
52+
className,
53+
children,
54+
...props
55+
}: {
56+
node?: any;
57+
inline?: boolean;
58+
className?: string;
59+
children?: ReactNode;
60+
}) => {
61+
const match = /language-(\w+)/.exec(className || "");
62+
const lang = match ? match[1] : "";
63+
const codeStr = String(children).replace(/\n$/, "");
64+
65+
const isInlineCode =
66+
inline || (!className && !codeStr.includes("\n") && codeStr.length < 100);
67+
68+
if (isInlineCode) {
69+
return (
70+
<code
71+
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
72+
{...props}
73+
>
74+
{children}
75+
</code>
76+
);
77+
}
78+
79+
return (
80+
<div className="grid my-4 overflow-x-auto rounded-lg border border-border text-xs lg:text-sm bg-primary/80 dark:bg-muted/80">
81+
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-primary/80 dark:bg-muted">
82+
<div className="text-muted-foreground font-mono">{lang}</div>
83+
<CopyButton code={codeStr} />
84+
</div>
85+
<Highlight
86+
theme={themes.vsDark}
87+
code={codeStr}
88+
language={lang || "text"}
89+
>
90+
{({ style, tokens, getLineProps, getTokenProps }) => (
91+
<pre
92+
style={{
93+
...style,
94+
padding: "1.5rem",
95+
margin: 0,
96+
overflow: "auto",
97+
backgroundColor: "transparent",
98+
}}
99+
>
100+
{tokens.map((line, i) => (
101+
<div key={i} {...getLineProps({ line })}>
102+
{line.map((token, key) => (
103+
<span key={key} {...getTokenProps({ token })} />
104+
))}
105+
</div>
106+
))}
107+
</pre>
108+
)}
109+
</Highlight>
110+
</div>
111+
);
112+
},
113+
pre: ({ children }: { children?: ReactNode }) => <>{children}</>,
114+
};
115+
116+
const renderMarkdownText = (text: string) => {
117+
return <ReactMarkdown components={mdComponents}>{text}</ReactMarkdown>;
118+
};
7119

8120
const portableTextComponents: PortableTextProps["components"] = {
9121
types: {
@@ -74,9 +186,17 @@ const portableTextComponents: PortableTextProps["components"] = {
74186
},
75187
},
76188
block: {
77-
normal: ({ children }) => (
78-
<p style={{ marginBottom: "1rem" }}>{children}</p>
79-
),
189+
normal: ({ children }) => {
190+
const text = Children.toArray(children)
191+
.map((child: any) => {
192+
if (isValidElement(child) && (child.props as any)?.children) {
193+
return (child.props as any).children;
194+
}
195+
return child;
196+
})
197+
.join("");
198+
return renderMarkdownText(text);
199+
},
80200
h1: ({ children }) => (
81201
<h1 style={{ marginBottom: "1rem", marginTop: "1rem" }}>{children}</h1>
82202
),

0 commit comments

Comments
 (0)