Fix: lesbare PDFs – HTML sauber entkernen (style/script raus, Absätze erhalten) + Typografie-Zeichen via WinAnsi statt '?'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8d09f0ef16
commit
dad16d6ba2
47
lib/mail.js
47
lib/mail.js
@ -72,10 +72,55 @@ const Mail = {
|
|||||||
};
|
};
|
||||||
walk(fullPart);
|
walk(fullPart);
|
||||||
if (plain.trim()) return plain;
|
if (plain.trim()) return plain;
|
||||||
if (html.trim()) return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
if (html.trim()) return this._htmlToText(html);
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macht aus HTML lesbaren Klartext: entfernt style/script/head-INHALTE komplett,
|
||||||
|
* leitet aus Block-Elementen Zeilenumbrüche ab, dekodiert Entities und glättet
|
||||||
|
* Whitespace – ohne Absatzstruktur platt zu walzen.
|
||||||
|
*/
|
||||||
|
_htmlToText(html) {
|
||||||
|
let s = String(html);
|
||||||
|
s = s.replace(/<!--[\s\S]*?-->/g, " ");
|
||||||
|
// style/script/head/title inkl. Inhalt raus (sonst landet CSS/JS im Text).
|
||||||
|
s = s.replace(/<(style|script|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ");
|
||||||
|
// Block-Grenzen -> Zeilenumbruch; Listenpunkte -> "- ".
|
||||||
|
s = s.replace(/<\s*br\s*\/?>/gi, "\n");
|
||||||
|
s = s.replace(/<\s*li\b[^>]*>/gi, "\n- ");
|
||||||
|
s = s.replace(/<\/\s*(p|div|li|tr|h[1-6]|ul|ol|table|blockquote)\s*>/gi, "\n");
|
||||||
|
s = s.replace(/<\/\s*td\s*>/gi, " ");
|
||||||
|
// Restliche Tags entfernen, Entities dekodieren.
|
||||||
|
s = s.replace(/<[^>]+>/g, " ");
|
||||||
|
s = this._decodeEntities(s);
|
||||||
|
// Whitespace glätten: Spaces/Tabs/NBSP zusammenfassen, Leerzeilen begrenzen.
|
||||||
|
s = s.replace(/[ \t ]+/g, " ");
|
||||||
|
s = s.replace(/ *\n */g, "\n");
|
||||||
|
s = s.replace(/\n{3,}/g, "\n\n");
|
||||||
|
return s.trim();
|
||||||
|
},
|
||||||
|
|
||||||
|
_decodeEntities(s) {
|
||||||
|
const named = {
|
||||||
|
amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ",
|
||||||
|
auml: "ä", ouml: "ö", uuml: "ü", Auml: "Ä", Ouml: "Ö", Uuml: "Ü", szlig: "ß",
|
||||||
|
euro: "€", copy: "©", reg: "®", trade: "™", hellip: "…", middot: "·", bull: "•",
|
||||||
|
ndash: "–", mdash: "—", laquo: "«", raquo: "»", deg: "°", eacute: "é", agrave: "à",
|
||||||
|
ldquo: "“", rdquo: "”", lsquo: "‘", rsquo: "’",
|
||||||
|
bdquo: "„", sbquo: "‚",
|
||||||
|
};
|
||||||
|
return String(s)
|
||||||
|
.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => this._fromCp(parseInt(h, 16)))
|
||||||
|
.replace(/&#(\d+);/g, (_, d) => this._fromCp(parseInt(d, 10)))
|
||||||
|
.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (m, n) => (named[n] != null ? named[n] : m));
|
||||||
|
},
|
||||||
|
|
||||||
|
_fromCp(cp) {
|
||||||
|
try { return Number.isFinite(cp) ? String.fromCodePoint(cp) : ""; }
|
||||||
|
catch (_) { return ""; }
|
||||||
|
},
|
||||||
|
|
||||||
_parseAddress(str) {
|
_parseAddress(str) {
|
||||||
if (!str) return { name: "", email: "" };
|
if (!str) return { name: "", email: "" };
|
||||||
const m = str.match(/^\s*"?([^"<]*)"?\s*<([^>]+)>\s*$/);
|
const m = str.match(/^\s*"?([^"<]*)"?\s*<([^>]+)>\s*$/);
|
||||||
|
|||||||
22
lib/pdf.js
22
lib/pdf.js
@ -129,9 +129,27 @@ const Pdf = {
|
|||||||
return this._toLatin1(s).replace(/[\\()]/g, "\\$&");
|
return this._toLatin1(s).replace(/[\\()]/g, "\\$&");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Unicode-Sonderzeichen, die NICHT in Latin-1, aber in WinAnsiEncoding liegen
|
||||||
|
// (0x80–0x9F). Da die Schrift /WinAnsiEncoding nutzt und Bytes per charCodeAt&0xFF
|
||||||
|
// geschrieben werden, rendern diese so als echte Typografie-Zeichen statt als '?'.
|
||||||
|
_WINANSI: {
|
||||||
|
"€": 0x80, "‚": 0x82, "ƒ": 0x83, "„": 0x84, "…": 0x85,
|
||||||
|
"†": 0x86, "‡": 0x87, "ˆ": 0x88, "‰": 0x89, "Š": 0x8A,
|
||||||
|
"‹": 0x8B, "Œ": 0x8C, "Ž": 0x8E, "‘": 0x91, "’": 0x92,
|
||||||
|
"“": 0x93, "”": 0x94, "•": 0x95, "–": 0x96, "—": 0x97,
|
||||||
|
"˜": 0x98, "™": 0x99, "š": 0x9A, "›": 0x9B, "œ": 0x9C,
|
||||||
|
"ž": 0x9E, "Ÿ": 0x9F,
|
||||||
|
},
|
||||||
|
|
||||||
_toLatin1(s) {
|
_toLatin1(s) {
|
||||||
// Zeichen außerhalb Latin-1 durch '?' ersetzen.
|
let out = "";
|
||||||
return (s || "").replace(/[^\x00-\xFF]/g, "?");
|
for (const ch of (s || "")) {
|
||||||
|
const cp = ch.codePointAt(0);
|
||||||
|
if (cp <= 0xff) out += ch; // Latin-1 direkt
|
||||||
|
else if (this._WINANSI[ch] != null) out += String.fromCharCode(this._WINANSI[ch]);
|
||||||
|
else out += "?"; // nicht darstellbar
|
||||||
|
}
|
||||||
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
_latin1Bytes(str) {
|
_latin1Bytes(str) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user