Skip to content

Commit 159f6e4

Browse files
fix(render): custom selectors override defaults instead of merging (#2752)
Co-authored-by: gabriel miranda <gabrielmfern@outlook.com>
1 parent 576a4e1 commit 159f6e4

File tree

3 files changed

+118
-1
lines changed

3 files changed

+118
-1
lines changed

.changeset/silly-chicken-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-email/render": patch
3+
---
4+
5+
fix custom selectors overwriting the ones we defined
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { toPlainText } from './to-plain-text';
2+
3+
describe('toPlainText', () => {
4+
describe('default behavior', () => {
5+
it('should skip images by default', () => {
6+
const html =
7+
'<p>Hello</p><img src="test.jpg" alt="Test Image"><p>World</p>';
8+
const result = toPlainText(html);
9+
10+
expect(result).not.toContain('Test Image');
11+
expect(result).not.toContain('test.jpg');
12+
expect(result).toContain('Hello');
13+
expect(result).toContain('World');
14+
});
15+
16+
it('should skip elements with data-skip-in-text="true"', () => {
17+
const html =
18+
'<p>Visible</p><span data-skip-in-text="true">Hidden</span><p>Also visible</p>';
19+
const result = toPlainText(html);
20+
21+
expect(result).not.toContain('Hidden');
22+
expect(result).toContain('Visible');
23+
expect(result).toContain('Also visible');
24+
});
25+
26+
it('should format links without brackets', () => {
27+
const html = '<a href="https://example.com">Click here</a>';
28+
const result = toPlainText(html);
29+
30+
expect(result).toContain('Click here');
31+
expect(result).toContain('https://example.com');
32+
expect(result).not.toContain('[');
33+
expect(result).not.toContain(']');
34+
});
35+
36+
it('should hide link href if same as text', () => {
37+
const html = '<a href="https://example.com">https://example.com</a>';
38+
const result = toPlainText(html);
39+
40+
expect(result.match(/https:\/\/example\.com/g)?.length).toBe(1);
41+
});
42+
});
43+
44+
describe('custom selectors', () => {
45+
it('should preserve default img skipping when custom selectors are provided', () => {
46+
const html =
47+
'<p>Hello</p><img src="test.jpg" alt="Test Image"><p>World</p>';
48+
const result = toPlainText(html, {
49+
selectors: [
50+
{
51+
selector: 'p',
52+
options: { leadingLineBreaks: 1, trailingLineBreaks: 1 },
53+
},
54+
],
55+
});
56+
57+
expect(result).not.toContain('Test Image');
58+
expect(result).not.toContain('test.jpg');
59+
});
60+
61+
it('should preserve default data-skip-in-text behavior when custom selectors are provided', () => {
62+
const html = '<p>Visible</p><span data-skip-in-text="true">Hidden</span>';
63+
const result = toPlainText(html, {
64+
selectors: [
65+
{
66+
selector: 'p',
67+
options: { leadingLineBreaks: 1, trailingLineBreaks: 1 },
68+
},
69+
],
70+
});
71+
72+
expect(result).not.toContain('Hidden');
73+
expect(result).toContain('Visible');
74+
});
75+
76+
it('should preserve default link formatting when custom selectors are provided', () => {
77+
const html = '<p>Text</p><a href="https://example.com">Click here</a>';
78+
const result = toPlainText(html, {
79+
selectors: [
80+
{
81+
selector: 'p',
82+
options: { leadingLineBreaks: 1, trailingLineBreaks: 1 },
83+
},
84+
],
85+
});
86+
87+
expect(result).toContain('Click here');
88+
expect(result).not.toContain('[');
89+
expect(result).not.toContain(']');
90+
});
91+
92+
it('should allow custom selectors to add new behavior', () => {
93+
const html = '<p>Normal</p><em>Emphasized text</em>';
94+
const result = toPlainText(html, {
95+
selectors: [{ selector: 'em', format: 'skip' }],
96+
});
97+
98+
expect(result).toContain('Normal');
99+
expect(result).not.toContain('Emphasized text');
100+
});
101+
102+
it('should apply custom selectors for elements not covered by defaults', () => {
103+
const html = '<p>Text</p><div class="custom">Custom div</div>';
104+
const result = toPlainText(html, {
105+
selectors: [{ selector: 'div.custom', format: 'skip' }],
106+
});
107+
108+
expect(result).toContain('Text');
109+
expect(result).not.toContain('Custom div');
110+
});
111+
});
112+
});

packages/render/src/shared/utils/to-plain-text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export const plainTextSelectors: SelectorDefinition[] = [
1515

1616
export function toPlainText(html: string, options?: HtmlToTextOptions) {
1717
return convert(html, {
18-
selectors: plainTextSelectors,
1918
wordwrap: false,
2019
...options,
20+
selectors: [...plainTextSelectors, ...(options?.selectors ?? [])],
2121
});
2222
}

0 commit comments

Comments
 (0)