Skip to content

Commit 6762361

Browse files
authored
Merge pull request #30 from maxbrereton/fix/memory-leak-marked-renderer
fix: memory leak caused by how marked library custom renderer being used
2 parents 8228085 + 612003c commit 6762361

File tree

5 files changed

+122
-123
lines changed

5 files changed

+122
-123
lines changed

.changeset/nine-steaks-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"md-to-react-email": patch
3+
---
4+
5+
Fix memory leak caused by how Marked custom renderer was used

__tests__/emailMarkdown.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe("ReactEmailMarkdown component renders correctly", () => {
1616
);
1717

1818
expect(actualOutput).toMatchInlineSnapshot(
19-
`"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><div><h1 style="font:700 23px / 32px 'Roobert PRO', system-ui, sans-serif">Hello, World!</h1><h2 style="background:url('path/to/image')">Hi, World</h2></div>"`
19+
`"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><div><h1 style="font:700 23px / 32px &#x27;Roobert PRO&#x27;, system-ui, sans-serif">Hello, World!</h1><h2 style="background:url(&#x27;path/to/image&#x27;)">Hi, World</h2></div>"`
2020
);
2121
});
2222
});

__tests__/parseCssInJsToInlineCss.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("parseCssInJsToInlineCss", () => {
4141
};
4242

4343
const expectedOutput =
44-
"font:700 23px / 32px 'Roobert PRO', system-ui, sans-serif;background:url('path/to/image')";
44+
"font:700 23px / 32px &#x27;Roobert PRO&#x27;, system-ui, sans-serif;background:url('path/to/image')";
4545
expect(parseCssInJsToInlineCss(cssProperties)).toBe(expectedOutput);
4646
});
4747
});

src/parser.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
import { marked, RendererObject } from "marked";
1+
import { marked, Renderer } from "marked";
22
import { StylesType } from "./types";
33
import { initRenderer } from "./utils";
44

55
export class MarkdownParser {
6-
private readonly renderer: RendererObject;
6+
private readonly renderer: Renderer;
77

88
constructor({ customStyles }: { customStyles?: StylesType }) {
99
this.renderer = initRenderer({ customStyles });
1010
}
1111

1212
parse(markdown: string) {
13-
marked.use({
14-
renderer: this.renderer,
15-
});
16-
17-
return marked.parse(markdown);
13+
return marked.parse(markdown, { renderer: this.renderer });
1814
}
1915
}

src/utils.ts

Lines changed: 112 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CSSProperties } from "react";
22
import { StylesType, initRendererProps } from "./types";
3-
import { RendererObject } from "marked";
3+
import { Renderer } from "marked";
44
import { styles } from "./styles";
55

66
function escapeQuotes(value: unknown) {
@@ -80,100 +80,99 @@ export function parseCssInJsToInlineCss(
8080

8181
export const initRenderer = ({
8282
customStyles,
83-
}: initRendererProps): RendererObject => {
83+
}: initRendererProps): Renderer => {
8484
const finalStyles = { ...styles, ...customStyles };
8585

86-
const customRenderer: RendererObject = {
87-
blockquote(quote) {
88-
return `<blockquote${
89-
parseCssInJsToInlineCss(finalStyles.blockQuote) !== ""
90-
? ` style="${parseCssInJsToInlineCss(finalStyles.blockQuote)}"`
91-
: ""
92-
}>\n${quote}</blockquote>\n`;
93-
},
86+
const customRenderer = new Renderer();
9487

95-
br() {
96-
return `<br${
97-
parseCssInJsToInlineCss(finalStyles.br) !== ""
98-
? ` style="${parseCssInJsToInlineCss(finalStyles.br)}"`
99-
: ""
100-
} />`;
101-
},
88+
customRenderer.blockquote = (quote) => {
89+
return `<blockquote${
90+
parseCssInJsToInlineCss(finalStyles.blockQuote) !== ""
91+
? ` style="${parseCssInJsToInlineCss(finalStyles.blockQuote)}"`
92+
: ""
93+
}>\n${quote}</blockquote>\n`;
94+
}
10295

103-
code(code) {
104-
code = code.replace(/\n$/, "") + "\n";
96+
customRenderer.br = () => {
97+
return `<br${
98+
parseCssInJsToInlineCss(finalStyles.br) !== ""
99+
? ` style="${parseCssInJsToInlineCss(finalStyles.br)}"`
100+
: ""
101+
} />`;
102+
}
105103

106-
return `<pre${
107-
parseCssInJsToInlineCss(finalStyles.codeBlock) !== ""
108-
? ` style="${parseCssInJsToInlineCss(finalStyles.codeBlock)}"`
109-
: ""
110-
}><code>${code}</code></pre>\n`;
111-
},
104+
customRenderer.code = (code) => {
105+
code = code.replace(/\n$/, "") + "\n";
112106

113-
codespan(text) {
114-
return `<code${
115-
parseCssInJsToInlineCss(finalStyles.codeInline) !== ""
116-
? ` style="${parseCssInJsToInlineCss(finalStyles.codeInline)}"`
117-
: ""
118-
}>${text}</code>`;
119-
},
107+
return `<pre${
108+
parseCssInJsToInlineCss(finalStyles.codeBlock) !== ""
109+
? ` style="${parseCssInJsToInlineCss(finalStyles.codeBlock)}"`
110+
: ""
111+
}><code>${code}</code></pre>\n`;
112+
}
120113

121-
del(text) {
122-
return `<del${
123-
parseCssInJsToInlineCss(finalStyles.strikethrough) !== ""
124-
? ` style="${parseCssInJsToInlineCss(finalStyles.strikethrough)}"`
125-
: ""
126-
}>${text}</del>`;
127-
},
114+
customRenderer.codespan = (text) => {
115+
return `<code${
116+
parseCssInJsToInlineCss(finalStyles.codeInline) !== ""
117+
? ` style="${parseCssInJsToInlineCss(finalStyles.codeInline)}"`
118+
: ""
119+
}>${text}</code>`;
120+
}
128121

129-
em(text) {
130-
return `<em${
131-
parseCssInJsToInlineCss(finalStyles.italic) !== ""
132-
? ` style="${parseCssInJsToInlineCss(finalStyles.italic)}"`
133-
: ""
134-
}>${text}</em>`;
135-
},
122+
customRenderer.del = (text) => {
123+
return `<del${
124+
parseCssInJsToInlineCss(finalStyles.strikethrough) !== ""
125+
? ` style="${parseCssInJsToInlineCss(finalStyles.strikethrough)}"`
126+
: ""
127+
}>${text}</del>`;
128+
}
136129

137-
heading(text, level) {
138-
return `<h${level}${
139-
parseCssInJsToInlineCss(
140-
finalStyles[`h${level}` as keyof StylesType]
141-
) !== ""
142-
? ` style="${parseCssInJsToInlineCss(
143-
finalStyles[`h${level}` as keyof StylesType]
144-
)}"`
145-
: ""
146-
}>${text}</h${level}>`;
147-
},
130+
customRenderer.em = (text) => {
131+
return `<em${
132+
parseCssInJsToInlineCss(finalStyles.italic) !== ""
133+
? ` style="${parseCssInJsToInlineCss(finalStyles.italic)}"`
134+
: ""
135+
}>${text}</em>`;
136+
}
148137

149-
hr() {
150-
return `<hr${
151-
parseCssInJsToInlineCss(finalStyles.hr) !== ""
152-
? ` style="${parseCssInJsToInlineCss(finalStyles.hr)}"`
153-
: ""
154-
} />\n`;
155-
},
138+
customRenderer.heading = (text, level) => {
139+
return `<h${level}${
140+
parseCssInJsToInlineCss(
141+
finalStyles[`h${level}` as keyof StylesType]
142+
) !== ""
143+
? ` style="${parseCssInJsToInlineCss(
144+
finalStyles[`h${level}` as keyof StylesType]
145+
)}"`
146+
: ""
147+
}>${text}</h${level}>`;
148+
}
156149

157-
image(href, _, text) {
158-
let out = `<img src="${href}" alt="${text}"${
159-
parseCssInJsToInlineCss(finalStyles.image) !== ""
160-
? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"`
161-
: ""
162-
}>`;
163-
return out;
164-
},
150+
customRenderer.hr = () => {
151+
return `<hr${
152+
parseCssInJsToInlineCss(finalStyles.hr) !== ""
153+
? ` style="${parseCssInJsToInlineCss(finalStyles.hr)}"`
154+
: ""
155+
} />\n`;
156+
}
157+
158+
customRenderer.image = (href, _, text) => {
159+
return `<img src="${href}" alt="${text}"${
160+
parseCssInJsToInlineCss(finalStyles.image) !== ""
161+
? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"`
162+
: ""
163+
}>`;
164+
}
165165

166-
link(href, _, text) {
167-
let out = `<a href="${href}" target="_blank"${
166+
customRenderer.link = (href, _, text) => {
167+
return `<a href="${href}" target="_blank"${
168168
parseCssInJsToInlineCss(finalStyles.link) !== ""
169169
? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"`
170170
: ""
171171
}>${text}</a>`;
172-
return out;
173-
},
172+
}
174173

175-
list(body, ordered, start) {
176-
const type = ordered ? "ol" : "ul";
174+
customRenderer.list = (body, ordered, start) => {
175+
const type = ordered ? "ol" : "ul";
177176
const startatt = ordered && start !== 1 ? ' start="' + start + '"' : "";
178177
const styles = parseCssInJsToInlineCss(
179178
finalStyles[ordered ? "ol" : "ul"]
@@ -188,34 +187,34 @@ export const initRenderer = ({
188187
type +
189188
">\n"
190189
);
191-
},
190+
}
192191

193-
listitem(text) {
194-
return `<li${
195-
parseCssInJsToInlineCss(finalStyles.li) !== ""
196-
? ` style="${parseCssInJsToInlineCss(finalStyles.li)}"`
197-
: ""
198-
}>${text}</li>\n`;
199-
},
192+
customRenderer.listitem = (text) => {
193+
return `<li${
194+
parseCssInJsToInlineCss(finalStyles.li) !== ""
195+
? ` style="${parseCssInJsToInlineCss(finalStyles.li)}"`
196+
: ""
197+
}>${text}</li>\n`;
198+
}
200199

201-
paragraph(text) {
202-
return `<p${
203-
parseCssInJsToInlineCss(finalStyles.p) !== ""
204-
? ` style="${parseCssInJsToInlineCss(finalStyles.p)}"`
205-
: ""
206-
}>${text}</p>\n`;
207-
},
200+
customRenderer.paragraph = (text) => {
201+
return `<p${
202+
parseCssInJsToInlineCss(finalStyles.p) !== ""
203+
? ` style="${parseCssInJsToInlineCss(finalStyles.p)}"`
204+
: ""
205+
}>${text}</p>\n`;
206+
}
208207

209-
strong(text) {
210-
return `<strong${
211-
parseCssInJsToInlineCss(finalStyles.bold) !== ""
212-
? ` style="${parseCssInJsToInlineCss(finalStyles.bold)}"`
213-
: ""
214-
}>${text}</strong>`;
215-
},
208+
customRenderer.strong = (text) => {
209+
return `<strong${
210+
parseCssInJsToInlineCss(finalStyles.bold) !== ""
211+
? ` style="${parseCssInJsToInlineCss(finalStyles.bold)}"`
212+
: ""
213+
}>${text}</strong>`;
214+
}
216215

217-
table(header, body) {
218-
if (body) body = `<tbody>${body}</tbody>`;
216+
customRenderer.table = (header, body) => {
217+
if (body) body = `<tbody>${body}</tbody>`;
219218

220219
return `<table${
221220
parseCssInJsToInlineCss(finalStyles.table) !== ""
@@ -226,10 +225,10 @@ export const initRenderer = ({
226225
? ` style="${parseCssInJsToInlineCss(finalStyles.thead)}"`
227226
: ""
228227
}>\n${header}</thead>\n${body}</table>\n`;
229-
},
228+
}
230229

231-
tablecell(content, flags) {
232-
const type = flags.header ? "th" : "td";
230+
customRenderer.tablecell = (content, flags) => {
231+
const type = flags.header ? "th" : "td";
233232
const tag = flags.align
234233
? `<${type} align="${flags.align}"${
235234
parseCssInJsToInlineCss(finalStyles.td) !== ""
@@ -242,16 +241,15 @@ export const initRenderer = ({
242241
: ""
243242
}>`;
244243
return tag + content + `</${type}>\n`;
245-
},
244+
}
246245

247-
tablerow(content) {
248-
return `<tr${
249-
parseCssInJsToInlineCss(finalStyles.tr) !== ""
250-
? ` style="${parseCssInJsToInlineCss(finalStyles.tr)}"`
251-
: ""
252-
}>\n${content}</tr>\n`;
253-
},
254-
};
246+
customRenderer.tablerow = (content) => {
247+
return `<tr${
248+
parseCssInJsToInlineCss(finalStyles.tr) !== ""
249+
? ` style="${parseCssInJsToInlineCss(finalStyles.tr)}"`
250+
: ""
251+
}>\n${content}</tr>\n`;
252+
}
255253

256254
return customRenderer;
257255
};

0 commit comments

Comments
 (0)