From 95fc6b01d59a0cfff1a0c052c65236f3363d296a Mon Sep 17 00:00:00 2001 From: eastagiletracker <310448263+eastagiletracker@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:47:16 +0700 Subject: [PATCH] fix(shared): decode common named HTML entities in plaintext output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit htmlToPlainText (behind the public renderToPlainText, which builds the text/plain MIME part of an email) only mapped ~17 named entities, so common ones such as £, €, é, ° and × leaked into the plaintext as literal entity text (e.g. "Price £5" instead of "Price £5"). Expand the named-entity map to cover currency, typographic punctuation, common symbols, and the Latin-1 accented letters used in European names/words. Kept as a curated map (not the full HTML5 named-reference set) so the ESM bundle stays within its CI size budget; anything omitted still decodes when written as a numeric entity. Unknown entities are still left untouched. --- .../shared/src/utils/html-to-text.test.ts | 33 ++++++++++ packages/shared/src/utils/html-to-text.ts | 62 ++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/utils/html-to-text.test.ts b/packages/shared/src/utils/html-to-text.test.ts index 66ec89f4..a2e77da1 100644 --- a/packages/shared/src/utils/html-to-text.test.ts +++ b/packages/shared/src/utils/html-to-text.test.ts @@ -77,6 +77,39 @@ describe("htmlToPlainText", () => { expect(htmlToPlainText("©")).toBe("\u00A9"); // © }); + it("decodes common currency and symbol named entities", () => { + expect(htmlToPlainText("Price £5, €9, ¥100, ¢50")).toBe( + "Price £5, €9, ¥100, ¢50" + ); // pound euro yen cent + expect(htmlToPlainText("5° × ½ ÷ ±")).toBe( + "5° × ½ ÷ ±" + ); // deg times frac12 divide plusmn + expect(htmlToPlainText("§2 ¶3 µg")).toBe( + "§2 ¶3 µg" + ); // sect para micro + }); + + it("decodes typographic quote named entities", () => { + expect(htmlToPlainText("“Hi” ‘yo’")).toBe( + "“Hi” ‘yo’" + ); + }); + + it("decodes Latin-1 accented named entities", () => { + // Cafe resume naive Zoe Francois Munoz Strasse (with accents) + expect( + htmlToPlainText( + "Café résumé naïve Zoë François Muñoz Straße" + ) + ).toBe( + "Café résumé naïve Zoë François Muñoz Straße" + ); + }); + + it("leaves unknown named entities untouched", () => { + expect(htmlToPlainText("¬anentity; x")).toBe("¬anentity; x"); + }); + // Skip elements it("skips elements with data-skip-in-text attribute", () => { const html = '
Visible content
'; diff --git a/packages/shared/src/utils/html-to-text.ts b/packages/shared/src/utils/html-to-text.ts index 51be77a9..5fe0b02d 100644 --- a/packages/shared/src/utils/html-to-text.ts +++ b/packages/shared/src/utils/html-to-text.ts @@ -8,9 +8,18 @@ */ /** - * Common HTML entity map for decoding. + * Common HTML named entity map for decoding. + * + * Covers the entities that actually appear in email/marketing content — + * currency, typographic punctuation, common symbols, and the Latin-1 accented + * letters used in European names and words. Kept as a curated map (rather than + * the full HTML5 named-reference set, which is ~2000 entries) so the bundle + * stays within its size budget. Numeric entities (`{`, ``) are + * handled separately below, so anything missing here still decodes if written + * numerically. */ const HTML_ENTITIES: Record