feat(render): toPlainText function to convert existing HTML to plain text (#2412)

This commit is contained in:
Gabriel Miranda
2025-08-06 14:41:17 -03:00
committed by GitHub
parent ca716e7233
commit fc2f4076ae
16 changed files with 51 additions and 32 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@react-email/render": minor
---
add toPlainText utility and deprecate plainText option on `render`
+5
View File
@@ -0,0 +1,5 @@
---
"@react-email/components": minor
---
add toPlainText utility and deprecate plainText option on `render`
+1 -1
View File
@@ -9,6 +9,6 @@ export const renderAsync = (element: React.ReactElement, options?: Options) => {
};
export * from '../shared/options';
export * from '../shared/plain-text-selectors';
export * from '../shared/utils/pretty';
export * from '../shared/utils/to-plain-text';
export * from './render';
@@ -4,8 +4,8 @@
import { createElement } from 'react';
import usePromise from 'react-promise-suspense';
import { Preview } from '../shared/utils/preview';
import { Template } from '../shared/utils/template';
import { Preview } from '../shared/utils/testing/preview';
import { Template } from '../shared/utils/testing/template';
import { render } from './render';
type Import = typeof import('react-dom/server') & {
+2 -7
View File
@@ -1,9 +1,7 @@
import { convert } from 'html-to-text';
import { Suspense } from 'react';
import type { ReactDOMServerReadableStream } from 'react-dom/server';
import { pretty } from '../node';
import { pretty, toPlainText } from '../node';
import type { Options } from '../shared/options';
import { plainTextSelectors } from '../shared/plain-text-selectors';
const decoder = new TextDecoder('utf-8');
@@ -59,10 +57,7 @@ export const render = async (node: React.ReactNode, options?: Options) => {
});
if (options?.plainText) {
return convert(html, {
selectors: plainTextSelectors,
...options.htmlToTextOptions,
});
return toPlainText(html, options.htmlToTextOptions);
}
const doctype =
+1 -1
View File
@@ -9,6 +9,6 @@ export const renderAsync = (element: React.ReactElement, options?: Options) => {
};
export * from '../shared/options';
export * from '../shared/plain-text-selectors';
export * from '../shared/utils/pretty';
export * from '../shared/utils/to-plain-text';
export * from './render';
@@ -1,5 +1,5 @@
import { Preview } from '../shared/utils/preview';
import { Template } from '../shared/utils/template';
import { Preview } from '../shared/utils/testing/preview';
import { Template } from '../shared/utils/testing/template';
import { render } from './render';
type Import = typeof import('react-dom/server') & {
@@ -4,8 +4,8 @@
import { Suspense } from 'react';
import usePromise from 'react-promise-suspense';
import { Preview } from '../shared/utils/preview';
import { Template } from '../shared/utils/template';
import { Preview } from '../shared/utils/testing/preview';
import { Template } from '../shared/utils/testing/template';
import { render } from './render';
type Import = typeof import('react-dom/server') & {
+2 -6
View File
@@ -1,8 +1,7 @@
import { convert } from 'html-to-text';
import { Suspense } from 'react';
import type { Options } from '../shared/options';
import { plainTextSelectors } from '../shared/plain-text-selectors';
import { pretty } from '../shared/utils/pretty';
import { toPlainText } from '../shared/utils/to-plain-text';
import { readStream } from './read-stream';
export const render = async (node: React.ReactNode, options?: Options) => {
@@ -35,10 +34,7 @@ export const render = async (node: React.ReactNode, options?: Options) => {
}
if (options?.plainText) {
return convert(html, {
selectors: plainTextSelectors,
...options.htmlToTextOptions,
});
return toPlainText(html, options.htmlToTextOptions);
}
const doctype =
+7
View File
@@ -1,5 +1,6 @@
import type { HtmlToTextOptions } from 'html-to-text';
import type { pretty } from './utils/pretty';
import type { toPlainText } from './utils/to-plain-text';
export type Options = {
/**
@@ -8,9 +9,15 @@ export type Options = {
pretty?: boolean;
} & (
| {
/**
* @deprecated use {@link toPlainText} instead
*/
plainText?: false;
}
| {
/**
* @deprecated use {@link toPlainText} instead
*/
plainText?: true;
/**
* These are options you can pass down directly to the library we use for
@@ -1,10 +0,0 @@
import type { SelectorDefinition } from 'html-to-text';
export const plainTextSelectors: SelectorDefinition[] = [
{ selector: 'img', format: 'skip' },
{ selector: '[data-skip-in-text=true]', format: 'skip' },
{
selector: 'a',
options: { linkBrackets: false },
},
];
@@ -5,7 +5,7 @@ import { pretty } from './pretty';
describe('pretty', () => {
it("should prettify Preview component's complex characters correctly", async () => {
const stripeHTML = await fs.readFile(
path.resolve(__dirname, './stripe-email.html'),
path.resolve(__dirname, './testing/stripe-email.html'),
'utf8',
);
@@ -0,0 +1,21 @@
import {
convert,
type HtmlToTextOptions,
type SelectorDefinition,
} from 'html-to-text';
export const plainTextSelectors: SelectorDefinition[] = [
{ selector: 'img', format: 'skip' },
{ selector: '[data-skip-in-text=true]', format: 'skip' },
{
selector: 'a',
options: { linkBrackets: false },
},
];
export function toPlainText(html: string, options?: HtmlToTextOptions) {
return convert(html, {
selectors: plainTextSelectors,
...options,
});
}