render: unstableToPlainText ignores mixed-case HTML attributes

Open Beginner friendly
#3,876 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Domain
backend

Research direction

Start by reading the shared conversion boundary and the lookups for href, start, and data-skip-in-text; the issue does not name file paths or tests. Normalize attribute names while preserving their values, then verify mixed-case attributes match lowercase behavior for links, ordered lists, and skip annotations. Done when those cases preserve the URL, numbering, and skipped text as expected.

Written by the indexing model from the issue text.

Description

What versions are you using? (if relevant)

Latest stable @react-email/render 2.2.0 and current canary branch 674d8ec2804b86be5d99156383fb1810179847bf both reproduce, with html5parser 3.0.0 and Node 24.13.0 (also reproduced on 22.16.0).

I also checked the published canary dist-tag, which currently resolves to 2.0.7-canary.0. That version predates unstableToPlainText and has no such export, so this feature-specific test is not applicable there. Its default converter handles the sample inputs correctly.

Describe the Bug

unstableToPlainText recognizes attribute names only when they are lowercase. Valid mixed-case HTML attributes lose their behavior: link URLs disappear from plain text, ordered-list start offsets become 1, and data-skip-in-text is not recognized. The default toPlainText handles these inputs correctly.

What is affected (leave empty if unsure)

Render Utility.

Link to the code that reproduces this issue

Self-contained reproduction below uses the public @react-email/render API; no provider credentials or email delivery are needed.

To Reproduce

Save this as repro.mjs:

import * as renderApi from '@react-email/render';

if (typeof renderApi.unstableToPlainText !== 'function') {
  console.log('unstableToPlainText is not available in this version');
}

const cases = [
  '<a HrEf="https://example.com/pay">Pay invoice</a>',
  '<ol StArT="5"><li>Five</li><li>Six</li></ol>',
  '<span DATA-SKIP-IN-TEXT="true">Preview</span>Body',
];

for (const html of cases) {
  console.log(JSON.stringify(renderApi.toPlainText(html)));
  if (typeof renderApi.unstableToPlainText === 'function') {
    console.log(JSON.stringify(renderApi.unstableToPlainText(html)));
  }
}

Run in a disposable directory:

npm init -y
npm install --save-exact @react-email/[email protected] [email protected] [email protected]
node repro.mjs
npm install --save-exact @react-email/[email protected]
node repro.mjs

Actual pairs on 2.2.0 (the older published canary prints the availability notice and stable results only):

"Pay invoice https://example.com/pay"
"Pay invoice"
" 5. Five\n 6. Six"
" 1. Five\n 2. Six"
"Body"
"PreviewBody"

Lowercasing only the attribute names makes the two converters agree in all three examples. The missing URL also occurs through render() with plainText: true, unstableTextConversion: true when a component includes the same HTML through dangerouslySetInnerHTML. Existing React-comment text in that rendering path is the separate issue #3864, not part of this report.

Expected Behavior

Attribute names are case-insensitive in HTML. These inputs should behave like their lowercase equivalents, preserving the URL, list numbering and skip annotation. Attribute values should keep their existing decoding/case behavior.

The parser preserves attribute names in its attributeMap, while the converter looks up only href, start and data-skip-in-text. Normalizing attribute names at the shared conversion boundary appears sufficient; no table-separator or default-converter change is needed. Attribute values should not be lowercased.

What's your node version? (if relevant)

24.13.0; also reproduced on 22.16.0.

Prepared with Codex using GPT-6.1 Sol at high reasoning.

Dominant language
TypeScript
Stars
19.9k
Forks
1.1k
Avg merge
1d 15h
Merged PRs (30d)
49

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from resend/react-email

All issues in resend/react-email

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.