mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: mark quotes and list items in the plain-text part of sent mail
This commit is contained in:
@@ -84,9 +84,10 @@ import { toUnicodeEmail } from "@/lib/idn";
|
||||
* Derives the text/plain alternative from the composer's HTML body, preserving
|
||||
* line structure from block elements and <br> tags. Paragraph spacing is on so
|
||||
* <p> blocks are separated by a blank line, matching their visual rendering (#421).
|
||||
* Quotes get "> " and list items their markers, like any plain-text mail.
|
||||
*/
|
||||
function htmlToPlainText(html: string): string {
|
||||
return htmlToPlainTextShared(html, { paragraphSpacing: true });
|
||||
return htmlToPlainTextShared(html, { paragraphSpacing: true, mailMarkup: true });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,4 +60,28 @@ describe('htmlToPlainText', () => {
|
||||
// recognise as the start of a signature.
|
||||
expect(htmlToPlainText('<p>Hi</p><p>-- </p><p>User A</p>')).toBe('Hi\n-- \nUser A');
|
||||
});
|
||||
|
||||
describe('mailMarkup', () => {
|
||||
// The text part of a reply showed the quoted original as ordinary text,
|
||||
// and list items without their bullets.
|
||||
const mail = { paragraphSpacing: true, mailMarkup: true };
|
||||
|
||||
it('prefixes quoted text with "> "', () => {
|
||||
const html = '<p>Sure.</p><p>On Sun, B wrote:</p><div data-quoted-html=""><p>Hi A,</p><p>can you?</p></div>';
|
||||
expect(htmlToPlainText(html, mail)).toBe('Sure.\n\nOn Sun, B wrote:\n\n> Hi A,\n>\n> can you?');
|
||||
});
|
||||
|
||||
it('nests quotes', () => {
|
||||
expect(htmlToPlainText('<blockquote><p>a</p><blockquote><p>b</p></blockquote></blockquote>', mail)).toBe('> a\n>\n> > b');
|
||||
});
|
||||
|
||||
it('marks list items', () => {
|
||||
expect(htmlToPlainText('<ul><li>Revenue</li><li>Costs</li></ul><ol start="3"><li>x</li><li>y</li></ol>', mail))
|
||||
.toBe('- Revenue\n- Costs\n\n3. x\n4. y');
|
||||
});
|
||||
|
||||
it('leaves the default conversion unchanged', () => {
|
||||
expect(htmlToPlainText('<blockquote>a</blockquote><ul><li>b</li></ul>')).toBe('a\nb');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+32
-1
@@ -55,6 +55,12 @@ export interface HtmlToPlainTextOptions {
|
||||
* are visually spaced; leave off for compact output like signatures.
|
||||
*/
|
||||
paragraphSpacing?: boolean;
|
||||
/**
|
||||
* Mark quoted text (<blockquote>, the composer's quoted original) with "> "
|
||||
* and list items with "- " or "1. ", as plain-text mail does. Without them
|
||||
* the text part of a reply could not tell the reply from the quote.
|
||||
*/
|
||||
mailMarkup?: boolean;
|
||||
}
|
||||
|
||||
function normalizeLineBreaks(value: string): string {
|
||||
@@ -76,7 +82,7 @@ function normalizeLineBreaks(value: string): string {
|
||||
* is the href. Used to derive the text/plain alternative of an HTML email.
|
||||
*/
|
||||
export function htmlToPlainText(html: string, options: HtmlToPlainTextOptions = {}): string {
|
||||
const { paragraphSpacing = false } = options;
|
||||
const { paragraphSpacing = false, mailMarkup = false } = options;
|
||||
const document = parseHtmlSafely(html);
|
||||
const chunks: string[] = [];
|
||||
|
||||
@@ -152,6 +158,31 @@ export function htmlToPlainText(html: string, options: HtmlToPlainTextOptions =
|
||||
const separator = paragraphSpacing && PARAGRAPH_TAGS.has(tagName) ? 2 : 1;
|
||||
const isBlock = BLOCK_TAGS.has(tagName);
|
||||
|
||||
if (mailMarkup && (tagName === 'blockquote' || element.hasAttribute('data-quoted-html'))) {
|
||||
const quoted = htmlToPlainText(element.innerHTML, options);
|
||||
if (quoted) {
|
||||
ensureNewlines(separator);
|
||||
chunks.push(quoted.split('\n').map((line) => (line ? `> ${line}` : '>')).join('\n'));
|
||||
ensureNewlines(separator);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mailMarkup && tagName === 'li') {
|
||||
const list = element.parentElement;
|
||||
let marker = '- ';
|
||||
if (list?.tagName.toLowerCase() === 'ol') {
|
||||
const start = Number(list.getAttribute('start') ?? 1) || 1;
|
||||
const position = Array.from(list.children).filter((child) => child.tagName.toLowerCase() === 'li').indexOf(element);
|
||||
marker = `${start + position}. `;
|
||||
}
|
||||
ensureNewlines(1);
|
||||
chunks.push(marker);
|
||||
Array.from(element.childNodes).forEach(walk);
|
||||
ensureNewlines(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBlock) {
|
||||
ensureNewlines(separator);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user