diff --git a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap index 25bca802b6..777d15da8d 100644 --- a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap +++ b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap @@ -13,10 +13,28 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` + style=" + margin-left:auto; + margin-right:auto; + margin-top:auto; + margin-bottom:auto; + background-color:rgb(255,255,255); + padding-left:8px; + padding-right:8px; + font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" + " + >
+ style=" + display:none; + overflow:hidden; + line-height:1px; + opacity:0; + max-height:0; + max-width:0 + " + data-skip-in-text="true" + > Join Alan on Vercel
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ @@ -26,10 +44,22 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" + cellPadding="0" + cellSpacing="0" role="presentation" - style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px"> + style=" + margin-left:auto; + margin-right:auto; + margin-top:40px; + margin-bottom:40px; + max-width:465px; + border-radius:0.25rem; + border-width:1px; + border-color:rgb(234,234,234); + border-style:solid; + padding:20px + " + > @@ -37,10 +67,11 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" + cellPadding="0" + cellSpacing="0" role="presentation" - style="margin-top:32px"> + style="margin-top:32px" + > @@ -48,38 +79,99 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="Vercel Logo" height="37" src="/static/vercel-logo.png" - style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none" - width="40" /> + style=" + margin-left:auto; + margin-right:auto; + margin-top:0; + margin-bottom:0; + display:block; + outline:none; + border:none; + text-decoration:none + " + width="40" + />

- Join Enigma on Vercel + style=" + margin-left:0; + margin-right:0; + margin-top:30px; + margin-bottom:30px; + padding:0; + text-align:center; + font-weight:400; + font-size:24px; + color:rgb(0,0,0) + " + > + Join + + Enigma + + on + + Vercel +

- Hello - alanturing, + style=" + font-size:14px; + color:rgb(0,0,0); + line-height:24px; + margin-top:16px; + margin-bottom:16px + " + > + Hello + + alanturing + + ,

- Alan ( + + Alan + + ( + alan.turing@example.com) has invited you to the Enigma team on - Vercel. + > + alan.turing@example.com + + ) has invited you to the + + Enigma + + team on + + + + Vercel + + .

+ cellPadding="0" + cellSpacing="0" + role="presentation" + > @@ -127,52 +235,122 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" + cellPadding="0" + cellSpacing="0" role="presentation" - style="margin-top:32px;margin-bottom:32px;text-align:center"> + style="margin-top:32px;margin-bottom:32px;text-align:center" + >
@@ -87,9 +179,10 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" - role="presentation"> + cellPadding="0" + cellSpacing="0" + role="presentation" + >
@@ -97,8 +190,15 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="alanturing's profile picture" height="64" src="/static/vercel-user.png" - style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" - width="64" /> + style=" + border-radius:9999px; + display:block; + outline:none; + border:none; + text-decoration:none + " + width="64" + /> with a demo email template 1`] = ` height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" - width="12" /> + width="12" + /> Enigma team logo + style=" + border-radius:9999px; + display:block; + outline:none; + border:none; + text-decoration:none + " + width="64" + />
Join the team + + + + + Join the team + + + + +

- or copy and paste this URL into your browser: + style=" + font-size:14px; + color:rgb(0,0,0); + line-height:24px; + margin-top:16px; + margin-bottom:16px + " + > + or copy and paste this URL into your browser: + + https://vercel.com + https://vercel.com +


+ style=" + margin-left:0; + margin-right:0; + margin-top:26px; + margin-bottom:26px; + width:100%; + border-width:1px; + border-color:rgb(234,234,234); + border-style:solid; + border:none; + border-top:1px solid #eaeaea + " + />

- This invitation was intended for - alanturing. This invite was - sent from 204.13.186.218 - located in - São Paulo, Brazil. If you - were not expecting this invitation, you can ignore this email. If - you are concerned about your account's safety, please reply - to this email to get in touch with us. + style=" + color:rgb(102,102,102); + font-size:12px; + line-height:24px; + margin-top:16px; + margin-bottom:16px + " + > + This invitation was intended for + + + + alanturing + + . This invite was sent from + + 204.13.186.218 + + + + located in + + + + São Paulo, Brazil + + . If you were not expecting this invitation, you can ignore this email. If you + are concerned about your account's safety, please reply to this email to + get in touch with us.

diff --git a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap index a3b5bd3042..9fdf8574ff 100644 --- a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap +++ b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap @@ -11,10 +11,28 @@ exports[`email export 1`] = ` + style=" + margin-left:auto; + margin-right:auto; + margin-top:auto; + margin-bottom:auto; + background-color:rgb(255,255,255); + padding-left:8px; + padding-right:8px; + font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" + " + >
+ style=" + display:none; + overflow:hidden; + line-height:1px; + opacity:0; + max-height:0; + max-width:0 + " + data-skip-in-text="true" + > Join undefined on Vercel
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ @@ -24,10 +42,22 @@ exports[`email export 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" + cellPadding="0" + cellSpacing="0" role="presentation" - style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px"> + style=" + margin-left:auto; + margin-right:auto; + margin-top:40px; + margin-bottom:40px; + max-width:465px; + border-radius:0.25rem; + border-width:1px; + border-color:rgb(234,234,234); + border-style:solid; + padding:20px + " + > @@ -35,10 +65,11 @@ exports[`email export 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" + cellPadding="0" + cellSpacing="0" role="presentation" - style="margin-top:32px"> + style="margin-top:32px" + > @@ -46,37 +77,89 @@ exports[`email export 1`] = ` alt="Vercel Logo" height="37" src="/static/vercel-logo.png" - style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none" - width="40" /> + style=" + margin-left:auto; + margin-right:auto; + margin-top:0; + margin-bottom:0; + display:block; + outline:none; + border:none; + text-decoration:none + " + width="40" + />

- Join on Vercel + style=" + margin-left:0; + margin-right:0; + margin-top:30px; + margin-bottom:30px; + padding:0; + text-align:center; + font-weight:400; + font-size:24px; + color:rgb(0,0,0) + " + > + Join + + on + + Vercel +

- Hello - , + style=" + font-size:14px; + color:rgb(0,0,0); + line-height:24px; + margin-top:16px; + margin-bottom:16px + " + > + Hello + + ,

- ( + + ( + ) has invited you to the team on - Vercel. + target="_blank" + > + ) has invited you to the + + team on + + + + Vercel + + .

+ cellPadding="0" + cellSpacing="0" + role="presentation" + > @@ -122,47 +221,108 @@ exports[`email export 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" + cellPadding="0" + cellSpacing="0" role="presentation" - style="margin-top:32px;margin-bottom:32px;text-align:center"> + style="margin-top:32px;margin-bottom:32px;text-align:center" + >
@@ -84,17 +167,25 @@ exports[`email export 1`] = ` align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" - role="presentation"> + cellPadding="0" + cellSpacing="0" + role="presentation" + >
undefined's profile picture + style=" + border-radius:9999px; + display:block; + outline:none; + border:none; + text-decoration:none + " + width="64" + /> + width="12" + /> undefined team logo + style=" + border-radius:9999px; + display:block; + outline:none; + border:none; + text-decoration:none + " + width="64" + />
Join the team + + + + + Join the team + + + + +

- or copy and paste this URL into your browser: - + style=" + font-size:14px; + color:rgb(0,0,0); + line-height:24px; + margin-top:16px; + margin-bottom:16px + " + > + or copy and paste this URL into your browser: + + +


+ style=" + margin-left:0; + margin-right:0; + margin-top:26px; + margin-bottom:26px; + width:100%; + border-width:1px; + border-color:rgb(234,234,234); + border-style:solid; + border:none; + border-top:1px solid #eaeaea + " + />

- This invitation was intended for - . This invite was sent from + style=" + color:rgb(102,102,102); + font-size:12px; + line-height:24px; + margin-top:16px; + margin-bottom:16px + " + > + This invitation was intended for + + + + . This invite was sent from + + + + located in + + - located in - . If you were not expecting - this invitation, you can ignore this email. If you are concerned - about your account's safety, please reply to this email to + . If you were not expecting this invitation, you can ignore this email. If you + are concerned about your account's safety, please reply to this email to get in touch with us.

diff --git a/packages/render/package.json b/packages/render/package.json index 0da8592afb..7b14262bd5 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -85,8 +85,7 @@ }, "dependencies": { "html-to-text": "^9.0.5", - "prettier": "^3.5.3", - "react-promise-suspense": "^0.3.4" + "node-html-parser": "^7.0.1" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", @@ -95,10 +94,10 @@ "devDependencies": { "@edge-runtime/vm": "5.0.0", "@types/html-to-text": "9.0.4", - "@types/prettier": "3.0.0", "@types/react": "npm:types-react@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0", "jsdom": "26.1.0", + "react-promise-suspense": "^0.3.4", "tsconfig": "workspace:*", "tsup": "8.4.0", "typescript": "5.8.3" diff --git a/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap b/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap index 882f165b0d..45240e6774 100644 --- a/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap +++ b/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap @@ -53,53 +53,3 @@ exports[`render on the browser environment > should properly wait for Suepsense `; exports[`render on the browser environment > should throw error of rendering an invalid element instead of writing them into a template tag 1`] = `[Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.]`; - -exports[`render on the browser environment > that it properly waits for Suepsense boundaries to resolve before resolving 1`] = ` -"" -`; diff --git a/packages/render/src/browser/render.tsx b/packages/render/src/browser/render.tsx index 4d71696e6d..4da861e0dc 100644 --- a/packages/render/src/browser/render.tsx +++ b/packages/render/src/browser/render.tsx @@ -70,7 +70,9 @@ export const render = async (node: React.ReactNode, options?: Options) => { const document = `${doctype}${html.replace(//, '')}`; if (options?.pretty) { - return pretty(document); + return pretty(document, { + lineBreak: '\n', + }); } return document; diff --git a/packages/render/src/node/render.tsx b/packages/render/src/node/render.tsx index b743d56131..de66572853 100644 --- a/packages/render/src/node/render.tsx +++ b/packages/render/src/node/render.tsx @@ -44,7 +44,9 @@ export const render = async (node: React.ReactNode, options?: Options) => { const document = `${doctype}${html.replace(//, '')}`; if (options?.pretty) { - return pretty(document); + return pretty(document, { + lineBreak: '\n', + }); } return document; diff --git a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap index de5ca3a77e..4fd455cc74 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -1,13 +1,857 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`pretty > if mso syntax does not wrap 1`] = ` -" +exports[`pretty > should not wrap [if mso] syntax 1`] = ` +" + + " `; -exports[`pretty > should prettify Preview component's complex characters correctly 1`] = ` +exports[`pretty > should not wrap text inside of + + + + + +" +`; + +exports[`pretty > should prettify Code Pen's template correctly 1`] = ` +" + + + + + + + + + + +
+ #CodePenChallenge: Cubes +
+  ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ +
+
+ + + + + + +
+ codepen +
+ + + + + + +
+

+ + View this Challenge on CodePen + +

+

+ + This week: + + #CodePenChallenge: + + +

+ Cubes +

+

+ + + + + + +
+

+ The Shape challenge continues! +

+

+ Last week, we kicked things off with round shapes. We "rounded" up the + Pens from week one in our + + + + #CodePenChallenge: Round + + collection. +

+

+ This week, we move on to cubes 🧊 +

+

+ Creating cubes in the browser is all about mastery of illusion. Take control of + perspective and shadows and you can make the magic of 3D on a flat screen 🧙 +

+

+ This week is a fun chance to work on your CSS shape-building skills, or dig into + a 3D JavaScript library like Three.js. +

+

+ This week's starter template features an ice cube emoji to help inspire a + "cool" idea for your Pen. As always, the template is just as jumping + off point. Feel free to incorporate the 🧊 in your creation, add more elements, + or freeze it out completely and start over from scratch! +

+

+ 💪 + + Your Challenge: + + + + create a Pen that includes cube shapes. + +

+ codepen + + + + + + +
+ codepen +

+ CodePen PRO combines a bunch of features that can help any front-end designer or + developer at any experience level. +

+ + + + + + + Learn More + + + + + + +
+
+

+ + To participate: + + + + Create a Pen → + + and tag it + + + + + codepenchallenge + + + + + and + + + + cpc-cubes + + + . We'll be watching and gathering the Pens into a Collection, and sharing + on + + Twitter + + and + + + + Instagram + + (Use the #CodePenChallenge tag on Twitter and Instagram as well). +

+ + + + + + +
+ + + + + + + +
+

+ IDEAS! +

+ + + + + + +
+ 🌟 +

+ This week we move from 2 dimensions to three! Maybe you could exercise your + + perspective + + in CSS to create a 3D cube. Or, you can try out creating 3D shapes in + JavaScript, using + + WebGL + + or building a + + Three.js scene + + . +

+
+ + + + + + +
+ 🌟 +

+ There's more to cubes than just six square sides. There are variations on + the cube that could be fun to play with this week: + + cuboid shapes + + are hexahedrons with faces that aren't always squares. And if you want to + really push the boundaries of shape, consider the 4 dimensional + + tesseract! + +

+
+ + + + + + +
+ 🌟 +

+ Here's a mind-bending idea that can combine the round shapes from week one + with this week's cube theme: + + + + Spherical Cubes + + 😳 Solving longstanding mathematical mysteries is probably outside the scope of + a CodePen challenge, but you could use front-end tools to explore fitting + spheres into cubes, or vice-versa. +

+
+
+

+ RESOURCES! +

+ + + + + + +
+ 📖 +

+ Learn all about + + + + How CSS Perspective Works + + and how to build a 3D CSS cube from scratch in Amit Sheen's in-depth + tutorial for CSS-Tricks. Or, check out stunning examples of WebGL cubes from + Matthias Hurrle: + + + + Just Ice + + and + + + + Posing + + . +

+
+ + + + + + +
+ 📖 +

+ Want to go beyond the square cube? Draw inspiration from EntropyReversed's + + + + Pulsating Tesseract + + , Josetxu's + + + + Rainbow Cuboid Loader + + , or Ana Tudor's + + + + Pure CSS cuboid jellyfish + + . +

+
+ + + + + + +
+ 📖 +

+ Did that spherical cubes concept pique your interest? Explore Ryan + Mulligan's + + Cube Sphere + + , Munir Safi's + + + + 3D Sphere to Cube Animation With Virtual Trackball + + + + and Ana Tudor's + + + + Infinitely unpack prism + + for more mindbending cube concepts that test the boundaries of how shapes + interact with each other. +

+
+
+
+ + + + + + +
+ + + + + + Go to Challenge Page + + + + + +
+ + + + + + +
+

+ You can adjust your + + + + email preferences + + any time, or + + + + instantly opt out + + of emails of this kind. Need help with anything? Hit up + + + + support + + . +

+
+
+ + + + +" +`; + +exports[`pretty > should prettify Stripe's template correctly 1`] = ` " @@ -16,22 +860,41 @@ exports[`pretty > should prettify Preview component's complex characters correct
+ style=" + display:none; + overflow:hidden; + line-height:1px; + opacity:0; + max-height:0; + max-width:0 + " + > You're now ready to make live transactions with Stripe!
-  ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ +  ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
+ style=" + background-color:#f6f9fc; + font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif + " + > + style=" + max-width:37.5em; + background-color:#ffffff; + margin:0 auto; + padding:20px 0 48px; + margin-bottom:64px + " + > @@ -159,5 +1134,83 @@ exports[`pretty > should prettify Preview component's complex characters correct + " `; + +exports[`pretty > should prettify base doucment correctly 1`] = ` +" + + + +

+ whatever +

+ + + +" +`; + +exports[`pretty > style attribute formatting > should print properties per-line once they get too wide 1`] = ` +"
+" +`; + +exports[`pretty > style attribute formatting > should work with an img element 1`] = ` +"Stagg Electric Kettle +" +`; + +exports[`wrapText() > should work with ending words that are larger than the max line size 1`] = ` +"Want to go +beyond the +square cube? +Draw inspiration +from +EntropyReversed's" +`; + +exports[`wrapText() > should work with longer lines imitating what would come from pretty printing 1`] = ` +"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis laoreet tortor +in orci ultricies, at fermentum nisl aliquam. Mauris ornare ut eros non +vulputate. Aliquam quam massa, sagittis et nunc at, tincidunt vestibulum +justo. Sed semper lectus a urna finibus congue. Aliquam erat volutpat. Lorem +ipsum dolor sit amet, consectetur adipiscing elit. Proin molestie enim sed +mauris ultrices interdum." +`; + +exports[`wrapText() > should work with short lines 1`] = ` +"Lorem +ipsum +dolor sit +amet, +consectetur +adipiscing +elit. +Vestibulum +tristique." +`; diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 1b79ca970d..df46d46982 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -1,21 +1,96 @@ -import { promises as fs } from 'node:fs'; +import fs from 'node:fs'; import path from 'node:path'; -import { pretty } from './pretty'; +import { pretty, wrapText } from './pretty'; + +const stripeHtml = fs.readFileSync( + path.resolve(__dirname, './tests/stripe-email.html'), + 'utf8', +); +const codepenHtml = fs.readFileSync( + path.resolve(__dirname, './tests/codepen-challengers.html'), + 'utf8', +); describe('pretty', () => { - it("should prettify Preview component's complex characters correctly", async () => { - const stripeHTML = await fs.readFile( - path.resolve(__dirname, './stripe-email.html'), - 'utf8', - ); + it('should prettify base doucment correctly', () => { + const document = + '

whatever

'; + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); + }); + + it('should not wrap text inside of `; + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); + }); + + describe('style attribute formatting', () => { + it('should print properties per-line once they get too wide', () => { + const document = + '
'; + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); + }); + + it('should work with an img element', () => { + const document = + 'Stagg Electric Kettle'; + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); + }); + }); - expect(await pretty(stripeHTML)).toMatchSnapshot(); + it("should prettify Stripe's template correctly", () => { + expect(pretty(stripeHtml, { lineBreak: '\n' })).toMatchSnapshot(); }); - test('if mso syntax does not wrap', async () => { + it("should prettify Code Pen's template correctly", () => { + expect(pretty(codepenHtml, { lineBreak: '\n' })).toMatchSnapshot(); + }); + + it('should not wrap [if mso] syntax', () => { expect( - await pretty( + pretty( ``, + { + lineBreak: '\n', + }, + ), + ).toMatchSnapshot(); + }); +}); + +describe('wrapText()', () => { + it('should work with short lines', () => { + expect( + wrapText( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum tristique.', + 10, + '\n', + ), + ).toMatchSnapshot(); + }); + + it('should work with longer lines imitating what would come from pretty printing', () => { + expect( + wrapText( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis laoreet tortor in orci ultricies, at fermentum nisl aliquam. Mauris ornare ut eros non vulputate. Aliquam quam massa, sagittis et nunc at, tincidunt vestibulum justo. Sed semper lectus a urna finibus congue. Aliquam erat volutpat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin molestie enim sed mauris ultrices interdum.', + 78, + '\n', + ), + ).toMatchSnapshot(); + }); + + it('should work with space characters from Preview component', () => { + const spaceCharacters = '\xa0\u200C\u200B\u200D\u200E\u200F\uFEFF'.repeat( + 150 - 50, + ); + expect(wrapText(spaceCharacters, 80, '\n')).toBe(spaceCharacters); + }); + + it('should work with ending words that are larger than the max line size', () => { + expect( + wrapText( + 'Want to go beyond the square cube? Draw inspiration from EntropyReversed's', + 16, + '\n', ), ).toMatchSnapshot(); }); diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 43beead749..3370c22544 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -1,101 +1,176 @@ -import type { Options, Plugin } from 'prettier'; -import type { builders } from 'prettier/doc'; -import * as html from 'prettier/plugins/html'; -import { format } from 'prettier/standalone'; +import { CommentNode, HTMLElement, NodeType, parse } from 'node-html-parser'; -interface HtmlNode { - type: 'element' | 'text' | 'ieConditionalComment'; - name?: string; - sourceSpan: { - start: { file: unknown[]; offset: number; line: number; col: number }; - end: { file: unknown[]; offset: number; line: number; col: number }; - details: null; - }; - parent?: HtmlNode; -} +interface Options { + /** + * Disables the word wrapping we do to ensure the maximum line length is kept. + * + * @default false + */ + preserveLinebreaks?: boolean; + /** + * The maximum line length before wrapping some piece of the document. + * + * @default 80 + */ + maxLineLength?: number; -function recursivelyMapDoc( - doc: builders.Doc, - callback: (innerDoc: string | builders.DocCommand) => builders.Doc, -): builders.Doc { - if (Array.isArray(doc)) { - return doc.map((innerDoc) => recursivelyMapDoc(innerDoc, callback)); - } + lineBreak?: '\n' | '\r\n'; +} - if (typeof doc === 'object') { - if (doc.type === 'group') { - return { - ...doc, - contents: recursivelyMapDoc(doc.contents, callback), - expandedStates: recursivelyMapDoc( - doc.expandedStates, - callback, - ) as builders.Doc[], - }; - } +export const getIndentationOfLine = (line: string) => { + const match = line.match(/^\s+/); + if (match === null) return ''; + return match[0]; +}; - if ('contents' in doc) { - return { - ...doc, - contents: recursivelyMapDoc(doc.contents, callback), - }; +export const wrapText = ( + text: string, + maxLineLength: number, + lineBreak: string, +): string => { + if (!text.includes(' ')) { + return `${text}`; + } + let wrappedText = text; + let currentLineStartIndex = 0; + while (wrappedText.length - currentLineStartIndex > maxLineLength) { + const overflowingCharacterIndex = Math.min( + currentLineStartIndex + maxLineLength - 1, + wrappedText.length, + ); + let lineBreakIndex = wrappedText.lastIndexOf( + ' ', + overflowingCharacterIndex + 1, + ); + if (lineBreakIndex === -1 || lineBreakIndex < currentLineStartIndex) { + lineBreakIndex = wrappedText.indexOf(' ', overflowingCharacterIndex); + if (lineBreakIndex === -1) { + return wrappedText; + } } + wrappedText = + wrappedText.slice(0, lineBreakIndex) + + lineBreak + + wrappedText.slice(lineBreakIndex + 1); + currentLineStartIndex = lineBreak.length + lineBreakIndex; + } + return wrappedText; +}; - if ('parts' in doc) { - return { - ...doc, - parts: recursivelyMapDoc(doc.parts, callback) as builders.Doc[], - }; - } +const printProperty = ( + propertyName: string, + propertyValue: string, + maxLineLength: number, + lineBreak: string, +) => { + const singleLineProperty = `${propertyName}="${propertyValue}"`; + if (propertyName === 'style' && singleLineProperty.length > maxLineLength) { + // This uses a negative lookbehing to ensure that the semicolon is not + // part of an HTML entity (e.g., `&`, `"`, ` `, etc.). + const nonHtmlEntitySemicolonRegex = /(? ` ${style}`) + .join(`;${lineBreak}`); + let multiLineProperty = `${propertyName}="${lineBreak}`; + multiLineProperty += `${wrappedStyles}${lineBreak}`; + multiLineProperty += ` "`; - if (doc.type === 'if-break') { - return { - ...doc, - breakContents: recursivelyMapDoc(doc.breakContents, callback), - flatContents: recursivelyMapDoc(doc.flatContents, callback), - }; - } + return multiLineProperty; } + return singleLineProperty; +}; - return callback(doc); -} +const printTagStart = ( + element: HTMLElement, + maxLineLength: number, + lineBreak: string, +) => { + const singleLineProperties = Object.entries(element.rawAttributes) + .map(([name, value]) => ` ${name}="${value}"`) + .join(''); + const singleLineTagStart = `<${element.tagName.toLowerCase()}${singleLineProperties}${element.isVoidElement ? ' /' : ''}>`; -const modifiedHtml = { ...html } as Plugin; -if (modifiedHtml.printers) { - // eslint-disable-next-line @typescript-eslint/unbound-method - const previousPrint = modifiedHtml.printers.html.print; - modifiedHtml.printers.html.print = (path, options, print, args) => { - const node = path.getNode() as HtmlNode; + if (singleLineTagStart.length <= maxLineLength) { + return singleLineTagStart; + } - const rawPrintingResult = previousPrint(path, options, print, args); + let multilineTagStart = `<${element.tagName.toLowerCase()}${lineBreak}`; + for (const [name, value] of Object.entries(element.rawAttributes)) { + const printedProperty = printProperty( + name, + value, + maxLineLength, + lineBreak, + ); + multilineTagStart += ` ${printedProperty}${lineBreak}`; + } + multilineTagStart += `${element.isVoidElement ? '/' : ''}>`; + return multilineTagStart; +}; - if (node.type === 'ieConditionalComment') { - const printingResult = recursivelyMapDoc(rawPrintingResult, (doc) => { - if (typeof doc === 'object' && doc.type === 'line') { - return doc.soft ? '' : ' '; - } +export const pretty = (html: string, options: Options = {}) => { + const root = parse(html, { comment: true }); - return doc; - }); + return printChildrenOf(root, { + preserveLinebreaks: false, + maxLineLength: 80, + lineBreak: '\n', + ...options, + }); +}; - return printingResult; - } +const printChildrenOf = ( + element: HTMLElement, + options: Required, + currentIndentationSize = 0, +) => { + const { preserveLinebreaks, lineBreak, maxLineLength } = options; + const indentation = ' '.repeat(currentIndentationSize); - return rawPrintingResult; - }; -} + let formatted = ''; + for (const node of element.childNodes) { + if (node.nodeType === NodeType.TEXT_NODE) { + if ( + preserveLinebreaks || + ['script', 'style'].includes( + (node.parentNode.tagName ?? '').toLowerCase(), + ) || + node.rawText.startsWith(' `${indentation}${line}`) + .join(lineBreak); + } + formatted += lineBreak; + } else if (node instanceof HTMLElement) { + formatted += `${indentation}`; + formatted += printTagStart(node, maxLineLength, lineBreak).replaceAll( + lineBreak, + `${lineBreak}${indentation}`, + ); -const defaults: Options = { - endOfLine: 'lf', - tabWidth: 2, - plugins: [modifiedHtml], - bracketSameLine: true, - parser: 'html', -}; + if (node.isVoidElement) { + formatted += lineBreak; + } else { + if (node.childNodes.length > 0) { + formatted += `${lineBreak}${printChildrenOf( + node, + options, + currentIndentationSize + 2, + )}`; + formatted += `${indentation}`; + } -export const pretty = (str: string, options: Options = {}) => { - return format(str.replaceAll('\0', ''), { - ...defaults, - ...options, - }); + formatted += `${lineBreak}`; + } + } else if (node instanceof CommentNode) { + formatted += `${indentation}${lineBreak}`; + } + } + return formatted; }; diff --git a/packages/render/src/shared/utils/tests/codepen-challengers.html b/packages/render/src/shared/utils/tests/codepen-challengers.html new file mode 100644 index 0000000000..b480d3cdfd --- /dev/null +++ b/packages/render/src/shared/utils/tests/codepen-challengers.html @@ -0,0 +1 @@ +
#CodePenChallenge: Cubes
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
@@ -39,10 +902,11 @@ exports[`pretty > should prettify Preview component's complex characters correct align="center" width="100%" border="0" - cellpadding="0" - cellspacing="0" + cellPadding="0" + cellSpacing="0" role="presentation" - style="padding:0 48px"> + style="padding:0 48px" + >
@@ -51,102 +915,213 @@ exports[`pretty > should prettify Preview component's complex characters correct height="21" src="/static/stripe-logo.png" style="display:block;outline:none;border:none;text-decoration:none" - width="49" /> + width="49" + />
+ style=" + width:100%; + border:none; + border-top:1px solid #eaeaea; + border-color:#e6ebf1; + margin:20px 0 + " + />

- Thanks for submitting your account information. - You're now ready to make live transactions with - Stripe! + style=" + font-size:16px; + line-height:24px; + margin:16px 0; + color:#525f7f; + text-align:left + " + > + Thanks for submitting your account information. You're now ready to make + live transactions with Stripe!

- You can view your payments and a variety of other - information about your account right from your dashboard. + style=" + font-size:16px; + line-height:24px; + margin:16px 0; + color:#525f7f; + text-align:left + " + > + You can view your payments and a variety of other information about your account + right from your dashboard.

View your Stripe Dashboard + + + + + View your Stripe Dashboard + + + + +
+ style=" + width:100%; + border:none; + border-top:1px solid #eaeaea; + border-color:#e6ebf1; + margin:20px 0 + " + />

- If you haven't finished your integration, you might - find our + style=" + font-size:16px; + line-height:24px; + margin:16px 0; + color:#525f7f; + text-align:left + " + > + If you haven't finished your integration, you might find our + + docs - handy. + docs + + + + handy.

- Once you're ready to start accepting payments, - you'll just need to use your live + style=" + font-size:16px; + line-height:24px; + margin:16px 0; + color:#525f7f; + text-align:left + " + > + Once you're ready to start accepting payments, you'll just need to use + your live + + API keys - instead of your test API keys. Your account can - simultaneously be used for both test and live requests, so - you can continue testing while accepting live payments. - Check out our + API keys + + + + instead of your test API keys. Your account can simultaneously be used for both + test and live requests, so you can continue testing while accepting live + payments. Check out our + + tutorial about account basics. + > + tutorial about account basics + + .

- Finally, we've put together a + style=" + font-size:16px; + line-height:24px; + margin:16px 0; + color:#525f7f; + text-align:left + " + > + Finally, we've put together a + + quick checklist - to ensure your website conforms to card network - standards. + quick checklist + + + + to ensure your website conforms to card network standards.

- We'll be here to help you with any step along the - way. You can find answers to most questions and get in - touch with us on our + style=" + font-size:16px; + line-height:24px; + margin:16px 0; + color:#525f7f; + text-align:left + " + > + We'll be here to help you with any step along the way. You can find answers + to most questions and get in touch with us on our + + support site. + > + support site + + .

+ style=" + font-size:16px; + line-height:24px; + margin:16px 0; + color:#525f7f; + text-align:left + " + > — The Stripe team


-

- Stripe, 354 Oyster Point Blvd, South San Francisco, CA - 94080 + style=" + width:100%; + border:none; + border-top:1px solid #eaeaea; + border-color:#e6ebf1; + margin:20px 0 + " + /> +

+ Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080

codepen

View this Challenge on CodePen

This week: #CodePenChallenge:

Cubes

The Shape challenge continues!

Last week, we kicked things off with round shapes. We "rounded" up the Pens from week one in our #CodePenChallenge: Round collection.

This week, we move on to cubes 🧊

Creating cubes in the browser is all about mastery of illusion. Take control of perspective and shadows and you can make the magic of 3D on a flat screen 🧙

This week is a fun chance to work on your CSS shape-building skills, or dig into a 3D JavaScript library like Three.js.

This week's starter template features an ice cube emoji to help inspire a "cool" idea for your Pen. As always, the template is just as jumping off point. Feel free to incorporate the 🧊 in your creation, add more elements, or freeze it out completely and start over from scratch!

💪 Your Challenge: create a Pen that includes cube shapes.

codepen
codepen

CodePen PRO combines a bunch of features that can help any front-end designer or developer at any experience level.

Learn More

To participate: Create a Pen → and tag it codepenchallenge and cpc-cubes. We'll be watching and gathering the Pens into a Collection, and sharing on Twitter and Instagram (Use the #CodePenChallenge tag on Twitter and Instagram as well).

IDEAS!

🌟

This week we move from 2 dimensions to three! Maybe you could exercise your perspective in CSS to create a 3D cube. Or, you can try out creating 3D shapes in JavaScript, using WebGL or building a Three.js scene.

🌟

There's more to cubes than just six square sides. There are variations on the cube that could be fun to play with this week: cuboid shapes are hexahedrons with faces that aren't always squares. And if you want to really push the boundaries of shape, consider the 4 dimensional tesseract!

🌟

Here's a mind-bending idea that can combine the round shapes from week one with this week's cube theme: Spherical Cubes 😳 Solving longstanding mathematical mysteries is probably outside the scope of a CodePen challenge, but you could use front-end tools to explore fitting spheres into cubes, or vice-versa.

RESOURCES!

📖

Learn all about How CSS Perspective Works and how to build a 3D CSS cube from scratch in Amit Sheen's in-depth tutorial for CSS-Tricks. Or, check out stunning examples of WebGL cubes from Matthias Hurrle: Just Ice and Posing.

📖

Want to go beyond the square cube? Draw inspiration from EntropyReversed's Pulsating Tesseract, Josetxu's Rainbow Cuboid Loader, or Ana Tudor's Pure CSS cuboid jellyfish.

📖

Did that spherical cubes concept pique your interest? Explore Ryan Mulligan's Cube Sphere, Munir Safi's 3D Sphere to Cube Animation With Virtual Trackball and Ana Tudor's Infinitely unpack prism for more mindbending cube concepts that test the boundaries of how shapes interact with each other.

Go to Challenge Page

You can adjust your email preferences any time, or instantly opt out of emails of this kind. Need help with anything? Hit up support.

diff --git a/packages/render/src/shared/utils/stripe-email.html b/packages/render/src/shared/utils/tests/stripe-email.html similarity index 99% rename from packages/render/src/shared/utils/stripe-email.html rename to packages/render/src/shared/utils/tests/stripe-email.html index 6345cd7593..5b5c2882e1 100644 Binary files a/packages/render/src/shared/utils/stripe-email.html and b/packages/render/src/shared/utils/tests/stripe-email.html differ diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index fc2d4fa1a1..c45e765fda 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -40,8 +40,9 @@ exports[`Tailwind component > should warn about safelist not being supported 1`] + Click me + + " `; @@ -61,8 +62,10 @@ exports[`Tailwind component > should work with blocklist 1`] = ` - + + " `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbcd15cd67..d07ccfa0a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -855,18 +855,15 @@ importers: html-to-text: specifier: ^9.0.5 version: 9.0.5 - prettier: - specifier: ^3.5.3 - version: 3.5.3 + node-html-parser: + specifier: ^7.0.1 + version: 7.0.1 react: specifier: ^19.0.0 version: 19.0.0 react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) - react-promise-suspense: - specifier: ^0.3.4 - version: 0.3.4 devDependencies: '@edge-runtime/vm': specifier: 5.0.0 @@ -874,9 +871,6 @@ importers: '@types/html-to-text': specifier: 9.0.4 version: 9.0.4 - '@types/prettier': - specifier: 3.0.0 - version: 3.0.0 '@types/react': specifier: ^19.0.1 version: 19.0.1 @@ -886,6 +880,9 @@ importers: jsdom: specifier: 26.1.0 version: 26.1.0 + react-promise-suspense: + specifier: ^0.3.4 + version: 0.3.4 tsconfig: specifier: workspace:* version: link:../tsconfig @@ -4200,10 +4197,6 @@ packages: '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} - '@types/prettier@3.0.0': - resolution: {integrity: sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA==} - deprecated: This is a stub types definition. prettier provides its own type definitions, so you do not need this installed. - '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -12100,10 +12093,6 @@ snapshots: '@types/phoenix@1.6.6': {} - '@types/prettier@3.0.0': - dependencies: - prettier: 3.5.3 - '@types/prismjs@1.26.5': {} '@types/prompts@2.4.9':