Skip to content

Decode common named HTML entities in renderToPlainText output - #54

Merged
ivoIturrieta merged 1 commit into
unlayer:mainfrom
eastagiletracker:agile-board/plaintext-named-entities
Aug 13, 2026
Merged

Decode common named HTML entities in renderToPlainText output#54
ivoIturrieta merged 1 commit into
unlayer:mainfrom
eastagiletracker:agile-board/plaintext-named-entities

Conversation

@eastagiletracker

Copy link
Copy Markdown
Contributor

This PR proposes decoding the common named HTML entities (currency, typographic punctuation, symbols, and Latin-1 accented letters) that renderToPlainText currently leaks into the text/plain output as literal entity text. We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/280. You can sign in with your GitHub ID to claim ownership of the project.

What this fixes

renderToPlainText builds the text/plain MIME part of an email — the part html-to-text.ts itself calls out as "critical for email deliverability: spam filters penalize HTML-only emails." Its decoder only mapped ~17 named entities, so common ones written in the html prop or the <Html> component — &pound;, &euro;, &yen;, &eacute;, &ccedil;, &deg;, &times;, &frac12;, smart quotes — were passed straight through as literal entity text. The plaintext part of the email then showed Total &pound;42.50 instead of Total £42.50.

Reproduced on main at HEAD (401e8bd) through the public API:

import { renderToPlainText, Body, Row, Column, Paragraph } from "@unlayer/react-elements";

renderToPlainText(
  <Body><Row><Column>
    <Paragraph html="Total &pound;42.50 &frac12; off  Caf&eacute; Mu&ntilde;oz" />
  </Column></Row></Body>
);
// HEAD:   "Total &pound;42.50 &frac12; off — Caf&eacute; Mu&ntilde;oz"
// fixed:  "Total £42.50 ½ off — Café Muñoz"

The fix expands the named-entity map in packages/shared/src/utils/html-to-text.ts to cover currency, typographic punctuation, common symbols, and the Latin-1 accented letters used in European names and words. It is kept as a curated map rather than the full HTML5 named-reference set (~2000 entries) so the shipped ESM bundle stays under the CI size budget; anything omitted still decodes when written as a numeric entity (&#163;), and the numeric-entity path is untouched.

How I verified it

The change is additive: unknown entities are still returned untouched, numeric entities are unaffected, and every previously-decoded entity is unchanged — so existing callers keep their output.

  • Added targeted tests to packages/shared/src/utils/html-to-text.test.ts that fail on main and pass with the fix (currency/symbols, typographic quotes, and Latin-1 accents), plus a null-case control asserting unknown entities are left as-is.
  • Full suite green before and after with no new failures: pnpm test → react 450 passed, shared 92 passed; pnpm --filter @unlayer/react-elements typecheck and the CSP safety gate both pass.
  • Bundle-size budget respected: the ESM packages/react/dist/index.js is 74,629 bytes, within the 75,000-byte CI budget.

How this was managed

This work was tracked on a live agile board imported from this repository's own issues and pull requests, on the story https://eastagiletracker.com/projects/280/stories/174647, with the board itself at https://eastagiletracker.com/projects/280.

board

If you'd rather not receive contributions like this, reply no-more-prs on this pull request and we won't open any further ones on your repositories.


Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com

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 &pound;, &euro;, &eacute;, &deg; and &times; leaked into the
plaintext as literal entity text (e.g. "Price &pound;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.
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.50%. Comparing base (401e8bd) to head (95fc6b0).

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #54   +/-   ##
=======================================
  Coverage   95.50%   95.50%           
=======================================
  Files          31       31           
  Lines        1937     1937           
  Branches      382      382           
=======================================
  Hits         1850     1850           
  Misses         86       86           
  Partials        1        1           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ivoIturrieta
ivoIturrieta merged commit a4195f9 into unlayer:main Aug 13, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants