import { parseHtmlSafely } from '@/lib/email-sanitization'; // Block-level tags whose boundaries become line breaks in the plain-text // rendering. Without this, the DOM's textContent would run every block // together on a single line (#421). const BLOCK_TAGS = new Set([ 'address', 'article', 'aside', 'blockquote', 'div', 'dd', 'dl', 'dt', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'li', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'tr', 'ul', ]); // Paragraph-level blocks that, with `paragraphSpacing` enabled, are separated by // a blank line rather than a single newline - matching how they render visually. const PARAGRAPH_TAGS = new Set([ 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'p', 'pre', 'table', 'ul', ]); export interface HtmlToPlainTextOptions { /** * Separate paragraph-level blocks (
, headings, lists, ...) with a blank
* line instead of a single newline. Use for email bodies where paragraphs
* are visually spaced; leave off for compact output like signatures.
*/
paragraphSpacing?: boolean;
}
function normalizeLineBreaks(value: string): string {
return value
.replace(/\r\n?/g, '\n')
.replace(new RegExp(String.fromCharCode(160), 'g'), ' ')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
/**
* Converts HTML into readable plain text, preserving line structure from block
* elements and
tags. Links render as `text
can form a blank line (capped by normalize).
if (chunks.length > 0) chunks.push('\n');
return;
}
if (tagName === 'a') {
const text = element.textContent?.replace(/\s+/g, ' ').trim() || '';
const href = element.getAttribute('href')?.trim() || '';
const normalizedHref = href.replace(/^mailto:/i, '');
if (text && normalizedHref && text === normalizedHref) {
appendText(text);
return;
}
if (text && href && text !== href) {
appendText(`${text} <${href}>`);
return;
}
}
const separator = paragraphSpacing && PARAGRAPH_TAGS.has(tagName) ? 2 : 1;
const isBlock = BLOCK_TAGS.has(tagName);
if (isBlock) {
ensureNewlines(separator);
}
Array.from(element.childNodes).forEach(walk);
if (isBlock) {
ensureNewlines(separator);
}
};
Array.from(document.body.childNodes).forEach(walk);
return normalizeLineBreaks(chunks.join(''));
}