- 
                Notifications
    You must be signed in to change notification settings 
- Fork 841
fix(render): decode HTML entities in style attributes #2552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Changes from 2 commits
2fbd1d3
              c82c45c
              a991373
              08b225e
              9c37138
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@react-email/render': patch | ||
| --- | ||
|  | ||
| Fix HTML entity encoding in style attributes | ||
|  | ||
| Fixes #1767 - Decodes HTML entities in style attributes to fix font-family declarations with quoted font names. Only decodes ampersands in href attributes to preserve HTML structure and avoid breaking attribute syntax. | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { Suspense } from 'react'; | ||
| import type { Options } from '../shared/options'; | ||
| import { decodeAttributeEntities } from '../shared/utils/decode-html-entities'; | ||
| import { pretty } from '../shared/utils/pretty'; | ||
| import { toPlainText } from '../shared/utils/to-plain-text'; | ||
| import { readStream } from './read-stream'; | ||
|  | @@ -43,7 +44,8 @@ export const render = async (node: React.ReactNode, options?: Options) => { | |
| const doctype = | ||
| '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'; | ||
|  | ||
| const document = `${doctype}${html.replace(/<!DOCTYPE.*?>/, '')}`; | ||
| const decodedHtml = decodeAttributeEntities(html); | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decoding the rendered HTML here replaces  Prompt for AI agents | ||
| const document = `${doctype}${decodedHtml.replace(/<!DOCTYPE.*?>/, '')}`; | ||
|  | ||
| if (options?.pretty) { | ||
| return pretty(document); | ||
|  | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * Decodes HTML entities in href and style attributes | ||
| * This is necessary because React's rendering encodes characters like ampersands | ||
| * and quotes in attribute values, which can break: | ||
| * - Links with query parameters (e.g., ?param1=value1¶m2=value2) | ||
| * - CSS font-family declarations with quotes | ||
| * | ||
| * Note: We only decode safe entities and avoid decoding < and > to prevent | ||
| * breaking HTML structure. Quotes are only decoded in style attributes to avoid | ||
| * breaking href attribute syntax. | ||
| */ | ||
| export const decodeAttributeEntities = (html: string): string => { | ||
| const decodeHrefValue = (value: string): string => { | ||
| // Only decode ampersands in hrefs to fix URL query parameters | ||
| // Do NOT decode quotes to avoid breaking the attribute syntax | ||
| return value.replace(/&/g, '&'); | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you elaborate on why you didn't use  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using  | ||
| }; | ||
|  | ||
| const decodeStyleValue = (value: string): string => { | ||
| // Decode entities in style attributes | ||
| // Note: We decode " to single quotes to avoid breaking the style="..." syntax | ||
| // since the style attribute is wrapped in double quotes | ||
| return value | ||
|         
                  iagocavalcante marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| .replace(/&/g, '&') | ||
| .replace(/"/g, "'") // Decode to single quote to avoid breaking attribute | ||
| .replace(/'/g, "'") | ||
| .replace(/'/g, "'"); | ||
| }; | ||
|  | ||
| // Match href and style attributes more carefully to avoid breaking HTML structure | ||
| // Use a regex that matches the attribute name, =, opening quote, content, closing quote | ||
| return html | ||
| .replace(/\bhref="([^"]*)"/g, (_match, hrefContent) => { | ||
| return `href="${decodeHrefValue(hrefContent)}"`; | ||
| }) | ||
| .replace(/\bstyle="([^"]*)"/g, (_match, styleContent) => { | ||
|         
                  iagocavalcante marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| return `style="${decodeStyleValue(styleContent)}"`; | ||
| }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -42,6 +42,11 @@ export function mapReactTree( | |
| : (processed.type as React.FC); | ||
|  | ||
| const rendered = OriginalComponent(processed.props); | ||
| // Handle async Server Components | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems unrelated to the pull request There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I tried to fix because of build failing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not do that here. | ||
| if (rendered && typeof rendered === 'object' && 'then' in rendered) { | ||
| // For now, return the unprocessed component for async components | ||
| return processed; | ||
| } | ||
| const mappedRenderedNode = mapReactTree(rendered, process); | ||
|  | ||
| return mappedRenderedNode; | ||
|  | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we stick with just this initial portion here, and can you make it all lowercase?