html-email-to-text
Version:
Convert html emails to (formatted) plain text
184 lines (152 loc) • 3.91 kB
JavaScript
const htmlparser = require('htmlparser2');
var output = [''];
var lastType = '';
const htmlToText = html => {
if (typeof(html) !== 'string') {
return new Error(`Expected a string but got ${typeof(html)}`);
}
parser.write(html);
parser.end();
return output.join('');
};
const parser = new htmlparser.Parser({
onopentag: (name, attribs) => {
formatElement(name, attribs, false);
},
ontext: text => {
formatText(text);
},
onclosetag: tagname => {
formatElement(tagname, {}, true);
}
}, {decodeEntities: true});
const elements = {
a: {
isOpen: false,
href: '',
text: ''
},
img: {
isOpen: false,
alt: ''
},
style: {
isOpen: false,
ignore: true
},
script: {
isOpen: false
},
title: {
isOpen: false
}
};
const formatText = text => {
if (/^\s*$/.test(text)) {
// ignore text with only whitespace
return;
}
if (elements.script.isOpen || elements.style.isOpen) {
// ignore script and style elements
return;
}
if (elements.title.isOpen) {
// set uppercase title and add divider
output.push(`${text.toUpperCase()}\n\n--------------------\n\n`);
lastType = 'title';
return;
}
if (lastType === 'title') {
// Don't break divider after title
text = text.replace(/^\s+/, '');
}
// Always end text with a space
if (!/\s+$/.test(text) && text.length > 1) {
text += ' ';
} else {
text = text.replace(/\s+$/, ' ');
}
if (elements.a.isOpen) {
elements.a.text = `${text}`;
return;
}
if (lastType === 'a.href') {
if (text.startsWith('.') || text.startsWith(',') || text.startsWith('!') || text.startsWith('?')) {
// Remove space if matching any of [.,!?]
output[output.length - 1] = output[output.length - 1].replace(/ $/, '');
} else {
text = text.replace(/^\s+/, '');
}
}
// Use a maximum of two line breaks for whitespace in beginning of text
if (/^\s{2,}/.test(text)) {
if (lastType === 'img') {
text = text.replace(/^\s+/, '');
} else {
text = text.replace(/^\s+/, '\n\n');
}
}
if (lastType === 'img.href') {
// If img link was used last string, add line breaks
if (/^\s/.test(text)) {
text.replace(/^\s+/, '\n\n');
} else {
text = `\n\n${text}`;
}
}
output.push(text);
lastType = 'text';
};
const formatElement = (name, attribs, isClosingTag) => {
switch (name) {
case 'a':
elements.a.isOpen = !isClosingTag;
if (elements.a.isOpen) {
if (!attribs) {
return;
}
if (attribs.href) {
elements.a.href = `[${attribs.href}] `;
}
} else {
if (elements.a.text) {
output.push(elements.a.text);
elements.a.text = '';
lastType = 'a.text';
}
if (elements.a.href) {
output.push(elements.a.href);
elements.a.href = '';
lastType = lastType === 'img' ? 'img.href' : 'a.href';
}
}
break;
case 'img':
elements.img.isOpen = !isClosingTag;
if (elements.img.isOpen) {
if (!attribs) {
return;
}
if (attribs.alt) {
elements.img.alt = `\n\n${attribs.alt}\n`;
}
} else {
if (elements.img.alt) {
output.push(`${elements.img.alt}`);
elements.img.alt = '';
lastType = 'img';
}
}
break;
case 'style':
elements.style.isOpen = !isClosingTag;
break;
case 'script':
elements.script.isOpen = !isClosingTag;
break;
case 'title':
elements.title.isOpen = !isClosingTag;
break;
}
}
module.exports = htmlToText;