Skip to content

Commit 02089b1

Browse files
committed
chore(markdown): move out of md-to-react-email (#2569)
1 parent e3b81fc commit 02089b1

File tree

7 files changed

+500
-83
lines changed

7 files changed

+500
-83
lines changed

.changeset/proud-animals-show.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-email/markdown": patch
3+
---
4+
5+
move out of md-to-react-email

packages/markdown/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"license": "MIT",
4848
"dependencies": {
49-
"md-to-react-email": "^5.0.5"
49+
"marked": "^15.0.12"
5050
},
5151
"peerDependencies": {
5252
"react": "^18.0 || ^19.0 || ^19.0.0-rc"

packages/markdown/src/__snapshots__/markdown.spec.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ exports[`<Markdown> component renders correctly > renders the markdown in the co
4242
<li>Author</li>
4343
</ul>
4444
</blockquote>
45-
<h2 style="font-weight:500;padding-top:20px;font-size:2rem">Code Blocks</h2><pre style="color:#212529;font-size:87.5%;display:inline;background: #f8f8f8;font-family:SFMono-Regular,Menlo,Monaco,Consolas,monospace;padding-top:10px;padding-right:10px;padding-left:10px;padding-bottom:1px;margin-bottom:20px;word-wrap:break-word"><code>function greet(name) {
45+
<h2 style="font-weight:500;padding-top:20px;font-size:2rem">Code Blocks</h2><pre style="color:#212529;font-size:87.5%;display:block;background: #f8f8f8;font-family:SFMono-Regular,Menlo,Monaco,Consolas,monospace;padding-top:10px;padding-right:10px;padding-left:10px;padding-bottom:1px;margin-bottom:20px;word-wrap:break-word"><code>function greet(name) {
4646
console.log(\`Hello, \${name}!\`);
4747
}
4848
</code></pre>

packages/markdown/src/markdown.tsx

Lines changed: 209 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { StylesType } from 'md-to-react-email';
2-
import { parseMarkdownToJSX } from 'md-to-react-email';
1+
import { marked, Renderer } from 'marked';
32
import * as React from 'react';
3+
import { type StylesType, styles } from './styles';
4+
import { parseCssInJsToInlineCss } from './utils/parse-css-in-js-to-inline-css';
45

56
export type MarkdownProps = Readonly<{
67
children: string;
@@ -13,15 +14,216 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(
1314
{ children, markdownContainerStyles, markdownCustomStyles, ...props },
1415
ref,
1516
) => {
16-
const parsedMarkdown = parseMarkdownToJSX({
17-
markdown: children,
18-
customStyles: markdownCustomStyles,
19-
});
17+
const finalStyles = { ...styles, ...markdownCustomStyles };
18+
19+
const renderer = new Renderer();
20+
renderer.blockquote = ({ tokens }) => {
21+
const text = renderer.parser.parse(tokens);
22+
23+
return `<blockquote${
24+
parseCssInJsToInlineCss(finalStyles.blockQuote) !== ''
25+
? ` style="${parseCssInJsToInlineCss(finalStyles.blockQuote)}"`
26+
: ''
27+
}>\n${text}</blockquote>\n`;
28+
};
29+
30+
renderer.br = () => {
31+
return `<br${
32+
parseCssInJsToInlineCss(finalStyles.br) !== ''
33+
? ` style="${parseCssInJsToInlineCss(finalStyles.br)}"`
34+
: ''
35+
} />`;
36+
};
37+
38+
// TODO: Support all options
39+
renderer.code = ({ text }) => {
40+
text = `${text.replace(/\n$/, '')}\n`;
41+
42+
return `<pre${
43+
parseCssInJsToInlineCss(finalStyles.codeBlock) !== ''
44+
? ` style="${parseCssInJsToInlineCss(finalStyles.codeBlock)}"`
45+
: ''
46+
}><code>${text}</code></pre>\n`;
47+
};
48+
49+
renderer.codespan = ({ text }) => {
50+
return `<code${
51+
parseCssInJsToInlineCss(finalStyles.codeInline) !== ''
52+
? ` style="${parseCssInJsToInlineCss(finalStyles.codeInline)}"`
53+
: ''
54+
}>${text}</code>`;
55+
};
56+
57+
renderer.del = ({ tokens }) => {
58+
const text = renderer.parser.parseInline(tokens);
59+
60+
return `<del${
61+
parseCssInJsToInlineCss(finalStyles.strikethrough) !== ''
62+
? ` style="${parseCssInJsToInlineCss(finalStyles.strikethrough)}"`
63+
: ''
64+
}>${text}</del>`;
65+
};
66+
67+
renderer.em = ({ tokens }) => {
68+
const text = renderer.parser.parseInline(tokens);
69+
70+
return `<em${
71+
parseCssInJsToInlineCss(finalStyles.italic) !== ''
72+
? ` style="${parseCssInJsToInlineCss(finalStyles.italic)}"`
73+
: ''
74+
}>${text}</em>`;
75+
};
76+
77+
renderer.heading = ({ tokens, depth }) => {
78+
const text = renderer.parser.parseInline(tokens);
79+
80+
return `<h${depth}${
81+
parseCssInJsToInlineCss(
82+
finalStyles[`h${depth}` as keyof StylesType],
83+
) !== ''
84+
? ` style="${parseCssInJsToInlineCss(
85+
finalStyles[`h${depth}` as keyof StylesType],
86+
)}"`
87+
: ''
88+
}>${text}</h${depth}>`;
89+
};
90+
91+
renderer.hr = () => {
92+
return `<hr${
93+
parseCssInJsToInlineCss(finalStyles.hr) !== ''
94+
? ` style="${parseCssInJsToInlineCss(finalStyles.hr)}"`
95+
: ''
96+
} />\n`;
97+
};
98+
99+
renderer.image = ({ href, text, title }) => {
100+
return `<img src="${href.replaceAll('"', '&quot;')}" alt="${text.replaceAll('"', '&quot;')}"${
101+
title ? ` title="${title}"` : ''
102+
}${
103+
parseCssInJsToInlineCss(finalStyles.image) !== ''
104+
? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"`
105+
: ''
106+
}>`;
107+
};
108+
109+
renderer.link = ({ href, title, tokens }) => {
110+
const text = renderer.parser.parseInline(tokens);
111+
112+
return `<a href="${href}" target="_blank"${
113+
title ? ` title="${title}"` : ''
114+
}${
115+
parseCssInJsToInlineCss(finalStyles.link) !== ''
116+
? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"`
117+
: ''
118+
}>${text}</a>`;
119+
};
120+
121+
renderer.listitem = ({ tokens }) => {
122+
const text = renderer.parser.parseInline(tokens);
123+
124+
return `<li${
125+
parseCssInJsToInlineCss(finalStyles.li) !== ''
126+
? ` style="${parseCssInJsToInlineCss(finalStyles.li)}"`
127+
: ''
128+
}>${text}</li>\n`;
129+
};
130+
131+
renderer.list = ({ items, ordered, start }) => {
132+
const type = ordered ? 'ol' : 'ul';
133+
const startAt = ordered && start !== 1 ? ` start="${start}"` : '';
134+
const styles = parseCssInJsToInlineCss(
135+
finalStyles[ordered ? 'ol' : 'ul'],
136+
);
137+
138+
return (
139+
'<' +
140+
type +
141+
startAt +
142+
`${styles !== '' ? ` style="${styles}"` : ''}>\n` +
143+
items.map((item) => renderer.listitem(item)).join('') +
144+
'</' +
145+
type +
146+
'>\n'
147+
);
148+
};
149+
150+
renderer.paragraph = ({ tokens }) => {
151+
const text = renderer.parser.parseInline(tokens);
152+
153+
return `<p${
154+
parseCssInJsToInlineCss(finalStyles.p) !== ''
155+
? ` style="${parseCssInJsToInlineCss(finalStyles.p)}"`
156+
: ''
157+
}>${text}</p>\n`;
158+
};
159+
160+
renderer.strong = ({ tokens }) => {
161+
const text = renderer.parser.parseInline(tokens);
162+
163+
return `<strong${
164+
parseCssInJsToInlineCss(finalStyles.bold) !== ''
165+
? ` style="${parseCssInJsToInlineCss(finalStyles.bold)}"`
166+
: ''
167+
}>${text}</strong>`;
168+
};
169+
170+
renderer.table = ({ header, rows }) => {
171+
const styleTable = parseCssInJsToInlineCss(finalStyles.table);
172+
const styleThead = parseCssInJsToInlineCss(finalStyles.thead);
173+
const styleTbody = parseCssInJsToInlineCss(finalStyles.tbody);
174+
175+
const theadRow = renderer.tablerow({
176+
text: header.map((cell) => renderer.tablecell(cell)).join(''),
177+
});
178+
179+
const tbodyRows = rows
180+
.map((row) =>
181+
renderer.tablerow({
182+
text: row.map((cell) => renderer.tablecell(cell)).join(''),
183+
}),
184+
)
185+
.join('');
186+
187+
const thead = `<thead${styleThead ? ` style="${styleThead}"` : ''}>\n${theadRow}</thead>`;
188+
const tbody = `<tbody${styleTbody ? ` style="${styleTbody}"` : ''}>${tbodyRows}</tbody>`;
189+
190+
return `<table${styleTable ? ` style="${styleTable}"` : ''}>\n${thead}\n${tbody}</table>\n`;
191+
};
192+
193+
renderer.tablecell = ({ tokens, align, header }) => {
194+
const text = renderer.parser.parseInline(tokens);
195+
const type = header ? 'th' : 'td';
196+
const tag = align
197+
? `<${type} align="${align}"${
198+
parseCssInJsToInlineCss(finalStyles.td) !== ''
199+
? ` style="${parseCssInJsToInlineCss(finalStyles.td)}"`
200+
: ''
201+
}>`
202+
: `<${type}${
203+
parseCssInJsToInlineCss(finalStyles.td) !== ''
204+
? ` style="${parseCssInJsToInlineCss(finalStyles.td)}"`
205+
: ''
206+
}>`;
207+
return `${tag}${text}</${type}>\n`;
208+
};
209+
210+
renderer.tablerow = ({ text }) => {
211+
return `<tr${
212+
parseCssInJsToInlineCss(finalStyles.tr) !== ''
213+
? ` style="${parseCssInJsToInlineCss(finalStyles.tr)}"`
214+
: ''
215+
}>\n${text}</tr>\n`;
216+
};
20217

21218
return (
22219
<div
23220
{...props}
24-
dangerouslySetInnerHTML={{ __html: parsedMarkdown }}
221+
dangerouslySetInnerHTML={{
222+
__html: marked.parse(children, {
223+
renderer,
224+
async: false,
225+
}),
226+
}}
25227
data-id="react-email-markdown"
26228
ref={ref}
27229
style={markdownContainerStyles}

packages/markdown/src/styles.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
const emptyStyle = {};
2+
3+
const baseHeaderStyles = {
4+
fontWeight: '500',
5+
paddingTop: 20,
6+
};
7+
8+
const h1 = {
9+
...baseHeaderStyles,
10+
fontSize: '2.5rem',
11+
};
12+
13+
const h2 = {
14+
...baseHeaderStyles,
15+
fontSize: '2rem',
16+
};
17+
const h3 = {
18+
...baseHeaderStyles,
19+
fontSize: '1.75rem',
20+
};
21+
const h4 = {
22+
...baseHeaderStyles,
23+
fontSize: '1.5rem',
24+
};
25+
const h5 = {
26+
...baseHeaderStyles,
27+
fontSize: '1.25rem',
28+
};
29+
const h6 = {
30+
...baseHeaderStyles,
31+
fontSize: '1rem',
32+
};
33+
34+
const bold = {
35+
fontWeight: 'bold',
36+
};
37+
38+
const italic = {
39+
fontStyle: 'italic',
40+
};
41+
42+
const blockQuote = {
43+
background: '#f9f9f9',
44+
borderLeft: '10px solid #ccc',
45+
margin: '1.5em 10px',
46+
padding: '1em 10px',
47+
};
48+
49+
const codeInline = {
50+
color: '#212529',
51+
fontSize: '87.5%',
52+
display: 'inline',
53+
background: ' #f8f8f8',
54+
fontFamily: 'SFMono-Regular,Menlo,Monaco,Consolas,monospace',
55+
};
56+
57+
const codeBlock = {
58+
...codeInline,
59+
display: 'block',
60+
paddingTop: 10,
61+
paddingRight: 10,
62+
paddingLeft: 10,
63+
paddingBottom: 1,
64+
marginBottom: 20,
65+
background: ' #f8f8f8',
66+
};
67+
68+
const link = {
69+
color: '#007bff',
70+
textDecoration: 'underline',
71+
backgroundColor: 'transparent',
72+
};
73+
74+
export type StylesType = {
75+
h1?: React.CSSProperties;
76+
h2?: React.CSSProperties;
77+
h3?: React.CSSProperties;
78+
h4?: React.CSSProperties;
79+
h5?: React.CSSProperties;
80+
h6?: React.CSSProperties;
81+
blockQuote?: React.CSSProperties;
82+
bold?: React.CSSProperties;
83+
italic?: React.CSSProperties;
84+
link?: React.CSSProperties;
85+
codeBlock?: React.CSSProperties;
86+
codeInline?: React.CSSProperties;
87+
p?: React.CSSProperties;
88+
li?: React.CSSProperties;
89+
ul?: React.CSSProperties;
90+
ol?: React.CSSProperties;
91+
image?: React.CSSProperties;
92+
br?: React.CSSProperties;
93+
hr?: React.CSSProperties;
94+
table?: React.CSSProperties;
95+
thead?: React.CSSProperties;
96+
tbody?: React.CSSProperties;
97+
tr?: React.CSSProperties;
98+
th?: React.CSSProperties;
99+
td?: React.CSSProperties;
100+
strikethrough?: React.CSSProperties;
101+
};
102+
103+
export const styles: StylesType = {
104+
h1,
105+
h2,
106+
h3,
107+
h4,
108+
h5,
109+
h6,
110+
blockQuote,
111+
bold,
112+
italic,
113+
link,
114+
codeBlock: { ...codeBlock, wordWrap: 'break-word' },
115+
codeInline: { ...codeInline, wordWrap: 'break-word' },
116+
p: emptyStyle,
117+
li: emptyStyle,
118+
ul: emptyStyle,
119+
ol: emptyStyle,
120+
image: emptyStyle,
121+
br: emptyStyle,
122+
hr: emptyStyle,
123+
table: emptyStyle,
124+
thead: emptyStyle,
125+
tbody: emptyStyle,
126+
th: emptyStyle,
127+
td: emptyStyle,
128+
tr: emptyStyle,
129+
strikethrough: emptyStyle,
130+
};

0 commit comments

Comments
 (0)