From 2cd741eca9db891e7922a27e35d76d5125aaad1a Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Thu, 15 May 2025 06:53:16 -0300 Subject: [PATCH 01/50] remove dependencies that are not going to be used anymore --- packages/render/package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/render/package.json b/packages/render/package.json index 0da8592afb..0f076aa989 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -84,18 +84,16 @@ "node": ">=18.0.0" }, "dependencies": { - "html-to-text": "^9.0.5", - "prettier": "^3.5.3", - "react-promise-suspense": "^0.3.4" + "html-to-text": "^9.0.5" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" }, "devDependencies": { + "react-promise-suspense": "^0.3.4", "@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", From 1d8691cff38195304c39dbb1189d293b52890cc9 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Thu, 15 May 2025 06:53:39 -0300 Subject: [PATCH 02/50] bring parsing and prettying in-house --- packages/render/src/shared/utils/pretty.ts | 330 ++++++++++++++++----- 1 file changed, 251 insertions(+), 79 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 43beead749..fcc9edc37b 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -1,101 +1,273 @@ -import type { Options, Plugin } from 'prettier'; -import type { builders } from 'prettier/doc'; -import * as html from 'prettier/plugins/html'; -import { format } from 'prettier/standalone'; - -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 HtmlTagProperty { + name: string; + value: string; } -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)); - } +interface HtmlTag { + type: 'tag'; + name: string; + /** + * Whether the html tag is self-closing, or a void element in spec nomenclature. + */ + void: boolean; + properties: HtmlTagProperty[]; + children: HtmlNode[]; +} - if (typeof doc === 'object') { - if (doc.type === 'group') { - return { - ...doc, - contents: recursivelyMapDoc(doc.contents, callback), - expandedStates: recursivelyMapDoc( - doc.expandedStates, - callback, - ) as builders.Doc[], - }; - } +/** + * Something like the DOCTYPE for the document, or comments. + */ +interface HtmlDeclaration { + type: 'declaration'; + content: string; +} + +interface HtmlText { + type: 'text'; + content: string; +} + +type HtmlNode = HtmlTag | HtmlDeclaration | HtmlText; + +export const lenientParse = (html: string): HtmlNode[] => { + const result: HtmlNode[] = []; + + const stack: HtmlTag[] = []; // Stack to keep track of parent tags + let index = 0; // Current parsing index + while (index < html.length) { + const currentParent = stack.length > 0 ? stack[stack.length - 1] : null; + const addToTree = (node: HtmlNode) => { + if (currentParent) { + currentParent.children.push(node); + } else { + result.push(node); + } + }; + + const htmlObjectStart = html.indexOf('<', index); + if (htmlObjectStart === -1) { + if (index < html.length) { + const content = html.slice(index); + addToTree({ type: 'text', content }); + } - if ('contents' in doc) { - return { - ...doc, - contents: recursivelyMapDoc(doc.contents, callback), - }; + break; } + if (htmlObjectStart > index) { + const content = html.slice(index, htmlObjectStart); + addToTree({ type: 'text', content }); + index = htmlObjectStart; + } + + if (html.startsWith('', index + 2); + if (declEnd === -1) { + // Assumes the rest of the document is part of this declaration + const content = html.slice(index); + addToTree({ type: 'declaration', content }); + break; + } - if ('parts' in doc) { - return { - ...doc, - parts: recursivelyMapDoc(doc.parts, callback) as builders.Doc[], - }; + const content = html.substring(index, declEnd + 1); + addToTree({ type: 'declaration', content }); + index = declEnd + 1; + continue; } - if (doc.type === 'if-break') { - return { - ...doc, - breakContents: recursivelyMapDoc(doc.breakContents, callback), - flatContents: recursivelyMapDoc(doc.flatContents, callback), - }; + if (html.startsWith('', index + 2); + const tagName = html.slice(index + 2, bracketEnd); + + if (stack.length > 0 && stack[stack.length - 1].name === tagName) { + stack.pop(); + } else { + // Mismatched closing tag. In a simple lenient parser, we might just ignore it + // or log a warning. For now, it's effectively ignored if no match on stack top. + } + index += 3 + tagName.length; + continue; } - } - return callback(doc); -} + const tag: HtmlTag = { + type: 'tag', + name: '', + void: false, + properties: [], + children: [], + }; -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; + index++; + while (!html.startsWith('>', index) && !html.startsWith('/>', index)) { + const character = html[index]; + if (character !== ' ' && tag.name.length === 0) { + const tagNameEndIndex = Math.min( + html.indexOf(' ', index), + html.indexOf('>', index), + ); + tag.name = html.slice(index, tagNameEndIndex); + index = tagNameEndIndex; + continue; + } - const rawPrintingResult = previousPrint(path, options, print, args); + if (character !== ' ') { + const propertyName = html.slice(index, html.indexOf('=', index)); + index = html.indexOf('=', index) + 1; - if (node.type === 'ieConditionalComment') { - const printingResult = recursivelyMapDoc(rawPrintingResult, (doc) => { - if (typeof doc === 'object' && doc.type === 'line') { - return doc.soft ? '' : ' '; - } + index = html.indexOf('"', index); + const propertyValue = html.slice( + index, + html.indexOf('"', index + 1) + 1, + ); + index = html.indexOf('"', index + 1) + 1; - return doc; - }); + tag.properties.push({ + name: propertyName, + value: propertyValue, + }); + continue; + } - return printingResult; + index++; + } + if (html.startsWith('/>', index)) { + index++; + tag.void = true; } + if (html.startsWith('>', index)) { + addToTree(tag); + if (!tag.void) { + stack.push(tag); + } + index++; + } + } - return rawPrintingResult; - }; + return result; +}; + +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; + + lineBreak: '\n' | '\r\n'; } -const defaults: Options = { - endOfLine: 'lf', - tabWidth: 2, - plugins: [modifiedHtml], - bracketSameLine: true, - parser: 'html', +export const getIndentationOfLine = (line: string) => { + const match = line.match(/^\s+/); + if (match === null) return ''; + return match[0]; +}; + +export const pretty = (html: string, options: Options) => { + const nodes = lenientParse(html); + + return prettyNodes(nodes, options); +}; + +export const wrapText = ( + text: string, + linePrefix: string, + maxLineLength: number, + lineBreak: string, +): string => { + let wrappedText = linePrefix + text; + let nextLineStartIndex = 0; + while (wrappedText.length - nextLineStartIndex > maxLineLength) { + const overflowingCharacterIndex = Math.min( + nextLineStartIndex + maxLineLength - 1, + wrappedText.length, + ); + for (let i = overflowingCharacterIndex; i >= nextLineStartIndex; i--) { + const char = wrappedText[i]; + if (char === ' ') { + wrappedText = + wrappedText.slice(0, i) + + lineBreak + + linePrefix + + wrappedText.slice(i + 1); + nextLineStartIndex = lineBreak.length + linePrefix.length + i; + break; + } + if (i === nextLineStartIndex) { + const nextSpaceIndex = wrappedText.indexOf(' ', nextLineStartIndex); + wrappedText = + wrappedText.slice(0, nextSpaceIndex) + + lineBreak + + linePrefix + + wrappedText.slice(nextSpaceIndex + 1); + nextLineStartIndex = + lineBreak.length + linePrefix.length + nextSpaceIndex; + } + } + } + return wrappedText; }; -export const pretty = (str: string, options: Options = {}) => { - return format(str.replaceAll('\0', ''), { - ...defaults, - ...options, - }); +const prettyNodes = ( + nodes: HtmlNode[], + options: Options, + currentIndentationSize = 0, +) => { + const { preserveLinebreaks = false, maxLineLength = 80, lineBreak } = options; + const indentation = ' '.repeat(currentIndentationSize); + + let formatted = ''; + for (const node of nodes) { + if (node.type === 'text') { + if (preserveLinebreaks) { + formatted += node.content; + } else { + const rawText = node.content.replaceAll(/(\r|\n|\r\n)\s*/g, ''); + formatted += wrapText( + rawText, + indentation, + maxLineLength - currentIndentationSize, + lineBreak, + ); + } + } else if (node.type === 'tag') { + const propertiesRawString = node.properties + .map((property) => ` ${property.name}=${property.value}`) + .join(''); + + const rawTagStart = `${indentation}<${node.name}${propertiesRawString}${node.void ? '/' : ''}>`; + if (rawTagStart.length > maxLineLength) { + let tagStart = `${indentation}<${node.name}`; + for (const property of node.properties) { + tagStart += `${indentation} ${property.name}=${property.value}${lineBreak}`; + } + tagStart += `${indentation}${node.void ? '/' : ''}>`; + formatted += tagStart; + } else { + formatted += `${rawTagStart}`; + } + + if (!node.void) { + if (node.children.length > 0) { + formatted += `${lineBreak}${prettyNodes( + node.children, + options, + currentIndentationSize + 2, + )}`; + formatted += `${lineBreak}${indentation}`; + } + + formatted += `${lineBreak}`; + } + } else if (node.type === 'declaration') { + formatted = `${indentation}${node.content}${lineBreak}`; + } + } + return formatted; }; From c8ddd414bd36f849e11ce0e00086a7f735894713 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Thu, 15 May 2025 06:53:54 -0300 Subject: [PATCH 03/50] add tests for some individual functions and comment others --- .../utils/__snapshots__/pretty.spec.ts.snap | 82 +++++++++++++++++++ .../render/src/shared/utils/pretty.spec.ts | 66 +++++++++++---- 2 files changed, 133 insertions(+), 15 deletions(-) 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..6682f756c9 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -1,5 +1,66 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`lenientParse() > should parse base doucment correctly 1`] = ` +[ + { + "content": "", + "type": "declaration", + }, + { + "children": [ + { + "children": [], + "name": "head", + "properties": [], + "type": "tag", + "void": false, + }, + { + "children": [ + { + "children": [ + { + "content": "whatever", + "type": "text", + }, + ], + "name": "h1", + "properties": [], + "type": "tag", + "void": false, + }, + { + "children": [], + "name": "input", + "properties": [ + { + "name": "placeholder", + "value": ""hello world"", + }, + ], + "type": "tag", + "void": true, + }, + ], + "name": "body", + "properties": [ + { + "name": "style", + "value": ""background-color:#fff;"", + }, + ], + "type": "tag", + "void": false, + }, + ], + "name": "html", + "properties": [], + "type": "tag", + "void": false, + }, +] +`; + exports[`pretty > if mso syntax does not wrap 1`] = ` " should prettify Preview component's complex characters correct " `; + +exports[`wrapText() 1`] = ` +"Lorem +ipsum +dolor sit +amet, +consectetur +adipiscing +elit. +Vestibulum +tristique." +`; + +exports[`wrapText() 2`] = ` +" 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." +`; diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 1b79ca970d..d728442ca4 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -1,22 +1,58 @@ -import { promises as fs } from 'node:fs'; +import fs from 'node:fs'; import path from 'node:path'; -import { pretty } from './pretty'; +import { lenientParse, pretty, wrapText } from './pretty'; -describe('pretty', () => { - it("should prettify Preview component's complex characters correctly", async () => { - const stripeHTML = await fs.readFile( - path.resolve(__dirname, './stripe-email.html'), - 'utf8', - ); +const stripeHTML = fs.readFileSync( + path.resolve(__dirname, './stripe-email.html'), + 'utf8', +); - expect(await pretty(stripeHTML)).toMatchSnapshot(); +describe('lenientParse()', () => { + it('should parse base doucment correctly', () => { + const document = `

whatever

`; + expect(lenientParse(document)).toMatchSnapshot(); }); +}); - test('if mso syntax does not wrap', async () => { - expect( - await pretty( - ``, - ), - ).toMatchSnapshot(); +describe('pretty', () => { + it('should prettify base doucment correctly', () => { + const document = `

whatever

`; + expect(pretty(document, { lineBreak: '\n' })).toBe(''); }); + + // it("should prettify Preview component's complex characters correctly", async () => { + // const stripeHTML = await fs.readFile( + // path.resolve(__dirname, './stripe-email.html'), + // 'utf8', + // ); + // + // expect(await pretty(stripeHTML)).toMatchSnapshot(); + // }); + // + // test('if mso syntax does not wrap', async () => { + // expect( + // await pretty( + // ``, + // ), + // ).toMatchSnapshot(); + // }); +}); + +test('wrapText()', () => { + expect( + wrapText( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum tristique.', + '', + 10, + '\n', + ), + ).toMatchSnapshot(); + 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(); }); From 41f6879bb2eb300770c63db612befae2449bec8a Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 14:14:55 -0300 Subject: [PATCH 04/50] fix infinite loop when there are no white space characters in text to wrap --- packages/render/src/shared/utils/pretty.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index fcc9edc37b..09ea7f0e33 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -181,14 +181,17 @@ export const wrapText = ( maxLineLength: number, lineBreak: string, ): string => { + if (!text.includes(' ')) { + return `${linePrefix}${text}`; + } let wrappedText = linePrefix + text; - let nextLineStartIndex = 0; - while (wrappedText.length - nextLineStartIndex > maxLineLength) { + let currentLineStartIndex = 0; + while (wrappedText.length - currentLineStartIndex > maxLineLength) { const overflowingCharacterIndex = Math.min( - nextLineStartIndex + maxLineLength - 1, + currentLineStartIndex + maxLineLength - 1, wrappedText.length, ); - for (let i = overflowingCharacterIndex; i >= nextLineStartIndex; i--) { + for (let i = overflowingCharacterIndex; i >= currentLineStartIndex; i--) { const char = wrappedText[i]; if (char === ' ') { wrappedText = @@ -196,17 +199,17 @@ export const wrapText = ( lineBreak + linePrefix + wrappedText.slice(i + 1); - nextLineStartIndex = lineBreak.length + linePrefix.length + i; + currentLineStartIndex = lineBreak.length + linePrefix.length + i; break; } - if (i === nextLineStartIndex) { - const nextSpaceIndex = wrappedText.indexOf(' ', nextLineStartIndex); + if (i === currentLineStartIndex) { + const nextSpaceIndex = wrappedText.indexOf(' ', currentLineStartIndex); wrappedText = wrappedText.slice(0, nextSpaceIndex) + lineBreak + linePrefix + wrappedText.slice(nextSpaceIndex + 1); - nextLineStartIndex = + currentLineStartIndex = lineBreak.length + linePrefix.length + nextSpaceIndex; } } From 1e842c00807c7c39a960859467827927316b6322 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 14:15:15 -0300 Subject: [PATCH 05/50] fixed extra line break in between two closing tags --- packages/render/src/shared/utils/pretty.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 09ea7f0e33..11950bbf3c 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -239,6 +239,7 @@ const prettyNodes = ( lineBreak, ); } + formatted += lineBreak; } else if (node.type === 'tag') { const propertiesRawString = node.properties .map((property) => ` ${property.name}=${property.value}`) @@ -248,13 +249,16 @@ const prettyNodes = ( if (rawTagStart.length > maxLineLength) { let tagStart = `${indentation}<${node.name}`; for (const property of node.properties) { - tagStart += `${indentation} ${property.name}=${property.value}${lineBreak}`; + tagStart += `${indentation} ${property.name}=${property.value}${lineBreak}`; } tagStart += `${indentation}${node.void ? '/' : ''}>`; formatted += tagStart; } else { formatted += `${rawTagStart}`; } + if (node.void) { + formatted += lineBreak; + } if (!node.void) { if (node.children.length > 0) { @@ -263,13 +267,13 @@ const prettyNodes = ( options, currentIndentationSize + 2, )}`; - formatted += `${lineBreak}${indentation}`; + formatted += `${indentation}`; } formatted += `${lineBreak}`; } } else if (node.type === 'declaration') { - formatted = `${indentation}${node.content}${lineBreak}`; + formatted += `${indentation}${node.content}${lineBreak}`; } } return formatted; From 3592816d427b6b4a4a035ed0eb11b7e20f1b21cd Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 14:15:27 -0300 Subject: [PATCH 06/50] add basic test --- .../shared/utils/__snapshots__/pretty.spec.ts.snap | 13 +++++++++++++ packages/render/src/shared/utils/pretty.spec.ts | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) 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 6682f756c9..bdf85282ed 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -223,6 +223,19 @@ exports[`pretty > should prettify Preview component's complex characters correct " `; +exports[`pretty > should prettify base doucment correctly 1`] = ` +" + + + +

+ whatever +

+ + + +" +`; exports[`wrapText() 1`] = ` "Lorem ipsum diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index d728442ca4..fc48eef77f 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -17,7 +17,7 @@ describe('lenientParse()', () => { describe('pretty', () => { it('should prettify base doucment correctly', () => { const document = `

whatever

`; - expect(pretty(document, { lineBreak: '\n' })).toBe(''); + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); // it("should prettify Preview component's complex characters correctly", async () => { From 5c9665c580663e710ae93987c48d3acb9d510013 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 14:16:06 -0300 Subject: [PATCH 07/50] add more granular tests for wrapText --- .../utils/__snapshots__/pretty.spec.ts.snap | 22 +++++++++ .../render/src/shared/utils/pretty.spec.ts | 46 ++++++++++++------- 2 files changed, 51 insertions(+), 17 deletions(-) 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 bdf85282ed..e2d9c4b2a6 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -236,6 +236,28 @@ exports[`pretty > should prettify base doucment correctly 1`] = ` " `; + +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." +`; + exports[`wrapText() 1`] = ` "Lorem ipsum diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index fc48eef77f..4fc5c8a470 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -38,21 +38,33 @@ describe('pretty', () => { // }); }); -test('wrapText()', () => { - expect( - wrapText( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum tristique.', - '', - 10, - '\n', - ), - ).toMatchSnapshot(); - 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(); +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); + }); }); From d26cd40290435e1c4c9b1df16b0e30fb54789cd1 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:09:42 -0300 Subject: [PATCH 08/50] add extra line break on tag start to avoid hanging properties --- packages/render/src/shared/utils/pretty.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 11950bbf3c..0e95d91ec9 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -247,9 +247,9 @@ const prettyNodes = ( const rawTagStart = `${indentation}<${node.name}${propertiesRawString}${node.void ? '/' : ''}>`; if (rawTagStart.length > maxLineLength) { - let tagStart = `${indentation}<${node.name}`; + let tagStart = `${indentation}<${node.name}${lineBreak}`; for (const property of node.properties) { - tagStart += `${indentation} ${property.name}=${property.value}${lineBreak}`; + tagStart += `${indentation} ${property.name}=${property.value}${lineBreak}`; } tagStart += `${indentation}${node.void ? '/' : ''}>`; formatted += tagStart; From 56e2b466d5f121836f6507e17903ffb1f341cd51 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:09:45 -0300 Subject: [PATCH 09/50] update lock --- pnpm-lock.yaml | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d691705e1d..78a61f7bcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -855,18 +855,12 @@ importers: html-to-text: specifier: ^9.0.5 version: 9.0.5 - prettier: - specifier: ^3.5.3 - version: 3.5.3 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 +868,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 +877,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 +4194,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 +12090,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': From ee43b591f9bd63732f540ed294a7b0a58ac04fc1 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:10:02 -0300 Subject: [PATCH 10/50] add tests for stripe's html and for property line-breaking --- .../render/src/shared/utils/pretty.spec.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 4fc5c8a470..bec79ff734 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -16,19 +16,21 @@ describe('lenientParse()', () => { describe('pretty', () => { it('should prettify base doucment correctly', () => { - const document = `

whatever

`; + const document = + '

whatever

'; expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); - // it("should prettify Preview component's complex characters correctly", async () => { - // const stripeHTML = await fs.readFile( - // path.resolve(__dirname, './stripe-email.html'), - // 'utf8', - // ); - // - // expect(await pretty(stripeHTML)).toMatchSnapshot(); - // }); - // + it('should print properties per-line once they get too wide', () => { + const document = + '
'; + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); + }); + + it("should prettify Preview component's complex characters correctly", () => { + expect(pretty(stripeHTML, { lineBreak: '\n' })).toMatchSnapshot(); + }); + // test('if mso syntax does not wrap', async () => { // expect( // await pretty( From b78233d618d05c6b13ca7b35fdc35ccbc4cb8266 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:10:06 -0300 Subject: [PATCH 11/50] update snap --- .../utils/__snapshots__/pretty.spec.ts.snap | Bin 12250 -> 13763 bytes 1 file changed, 0 insertions(+), 0 deletions(-) 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 e2d9c4b2a64ca9371dba61c6e72a66d7956cfcab..ff69d947cd7fb359b57861db186f0b87b3146f4d 100644 GIT binary patch delta 1926 zcmb7FOKcle6lGlhOA^;f?0B3wH~!TjA2+Z7DNZmFf~1rx)G7#}s2Pu6;zy?Q+?h8? zEfo>5V?~nN1q;LmBqUS`P=tikf+}^z0;x!>DyWJC8#b_E$>+`3Gi?O!$D4WId(S=h zyyIKvZft)3ZUEO`D2t&X~lY>sgTl z--@jySCmGB>KhB$6ZVq*Q;DY4@R!(Lz*#`+*q71@PQ}OkwU(mdY&?N~#$>z{&qz`H zEH`@aX}l`oUSe1(;}^vwej{&tvv=$fUd@eQVmK?tS!os_oD-vDiW>rrqH@Y~6SgUD zLX8`=MJDj)L>W(~H@&-S3BY}MI1aF-7?kifuytU(NeuiXQLuv@e3!T(i99L4*-~@? zBZ1c;aCBm3<_)?I=S{Ziu1OTVV&p;C_cJjs(6LPRahu{|?&6bA&uf~yA-`#v9CSh&z!_L3 z>GVOPTf{x$f%tEM3H&Ov)T6)%Jh!q8o232FMGB$@sBUti2!hugATEYEP6SLh;4Hju zi$zgYCY~FpiVh|{Z)sf73A0SC4fVU|k3z|B$HLp=X@{{h@PhMwANt(z_bIW6e?hKC zi37kb&J3z(pwyYgop7RxeivC?F=?+RKT<34Qr44UU(4d6SsTQDn1Fk7!jayyW04** ze5D8c7+kQZx(RBNsGAx!JNSj{9;C+Vc%+X3(M`+f=*8plw;Ukk!Qr=>@4Tw}y{@0~ zaqsRQ`CJ6HmA2DeuUjx0z)B$>6ct-)(>>hp_?Tq^(;xCJCh>>DbPpo>z_eN|W^f@R z?kVRm3y;>WmyVYXaqzX=^??xHmS5{u@&6VS#}`X;?g`I^c1w4O`lyt{A4?rEyCe%w z^p*3tTaX>!5dIMswpqT{rF~^;Tg<)dNuOgoL)brg86#5z2UjOog}qP47pQnwErYd) z!6`98d}lPO6T!DiKnSl5LVuzsO#GuX2+Xam!UC*Cvy}$jilRKU7K#ef6_bO|)W^Pm E0r7uqKL7v# delta 1237 zcmZuxO>7%Q6xIgCj$CRxj^m&FIIn9rPUF9|lZucyD~eEQBz{7pAn{Y^cz0}%yxDPg zX6sl;w5Ljl3-C?|0pi91AtWO%JyZxTh;l%P3wKUP+@S~9-K45X*u%cn?0fI~c{B4} z@3%Lk+jn`oxX^=*@GL%4 zA%_;-R=p0DHn(MEz)acQtZUl&Eq_hYLs@YM6LhoQP@&6AliK=nexso^wt|39Wiv_y zA84!i;p8S>uD^vZh8zX(+t7yh{wu^sz3@LFycPWdKaD1_6e}u4+>2elsP54Yr?9`H zmap;_m%OZAq1H_*nL*$M>Qc45-*Ro~mIbjzMNo&ei}-tNH{Bk$3G4qr{V4Cv?B1E>6b>y#IAy*n!)#G4uiO!v{2D9_)}6n3(RhF z=YUAY?JEA9N#l+1dJx~3{Vd2Tr(hax5{FSa1l|TNyVMCXWU4UwGWESOakMcg43FF)G2==o!yv*nThTL^vPdcnm;Ta;jElwP>Sv&4KUV0kh zqqznAb*}6~Jf=5mSTA{IET&E=-d~u(I|~7osW!7s@G$ZA$`Wd&dCV0T@zupSpV;$- zOL(ui64p&_h@fA_>zDCTr$Sv~n?v8KS-hXi;zKh(S&HNRrMyq!j3;F|f7p0p%d#*F z^J556{1L8I2C3=UQBTTcsi;98Q{l)J(jLW|-L>IMH# z*aHQ_n?WZ3EOzo|$3Sy9U&xPvf-YUj9Y!o5E$&JnhQYl(2Q4DlL%kFFX^s2KGco9U z1(J8DzYTzl!R(AdB=j3DGkRcjsnN5TkpDF?DmhSFxDGv1_shyTUqeKA%1m$U;xzW3 z7ZR9TE#TAT^y33EtimhDdJk4t{o17DuF-`7lU;BHaQnn;W7sN{#~8+R;r9F7ksz3) zNA&(rv#hKHbAEJqO89lP9vls|mcXmEf)79W Date: Fri, 16 May 2025 16:47:36 -0300 Subject: [PATCH 12/50] add test for case where we had infinite loop --- .../utils/__snapshots__/pretty.spec.ts.snap | Bin 13763 -> 13948 bytes .../render/src/shared/utils/pretty.spec.ts | 11 +++++++++++ 2 files changed, 11 insertions(+) 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 ff69d947cd7fb359b57861db186f0b87b3146f4d..b2916c9d08a25839c93eee45d25c41aba3cb80b4 100644 GIT binary patch delta 154 zcmWlRF$%&!5CFk)!9T1D_L{;%#70{S8!I8Jx5mR{H|||bV&h+Yf#4JTmW8Gn7#QYr z`R1=p@?ddZGOX2Hz<8#JWQ;jk#@4y~!B|j(xfjOZ?~Idlj|)s;xFd-e6*Gkcw<@lS vAzZ8OajXu!gq2YZUP9@OqE~^FX#s1Are(Y3{*5iL8;_dlY#!c& { ); 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(); + }); }); From f5ed891198c9d42d5f819eebc42dc6e79bbcedd9 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:50:39 -0300 Subject: [PATCH 13/50] fix text wrapping issues --- packages/render/src/shared/utils/pretty.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 0e95d91ec9..f57dcd337e 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -185,12 +185,15 @@ export const wrapText = ( return `${linePrefix}${text}`; } let wrappedText = linePrefix + text; - let currentLineStartIndex = 0; + let currentLineStartIndex = linePrefix.length; while (wrappedText.length - currentLineStartIndex > maxLineLength) { const overflowingCharacterIndex = Math.min( currentLineStartIndex + maxLineLength - 1, wrappedText.length, ); + if (!wrappedText.includes(' ', currentLineStartIndex)) { + return wrappedText; + } for (let i = overflowingCharacterIndex; i >= currentLineStartIndex; i--) { const char = wrappedText[i]; if (char === ' ') { From 94005a5b0f9ad46e654963c42bf80790bca7af33 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:50:56 -0300 Subject: [PATCH 14/50] dont count indentation as part of the length of the line --- packages/render/src/shared/utils/pretty.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index f57dcd337e..10e1c1cc73 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -235,12 +235,7 @@ const prettyNodes = ( formatted += node.content; } else { const rawText = node.content.replaceAll(/(\r|\n|\r\n)\s*/g, ''); - formatted += wrapText( - rawText, - indentation, - maxLineLength - currentIndentationSize, - lineBreak, - ); + formatted += wrapText(rawText, indentation, maxLineLength, lineBreak); } formatted += lineBreak; } else if (node.type === 'tag') { @@ -249,7 +244,7 @@ const prettyNodes = ( .join(''); const rawTagStart = `${indentation}<${node.name}${propertiesRawString}${node.void ? '/' : ''}>`; - if (rawTagStart.length > maxLineLength) { + if (rawTagStart.length - currentIndentationSize > maxLineLength) { let tagStart = `${indentation}<${node.name}${lineBreak}`; for (const property of node.properties) { tagStart += `${indentation} ${property.name}=${property.value}${lineBreak}`; From 2f25fe55c48e5a5020a186ebef373ccf0006e046 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:51:09 -0300 Subject: [PATCH 15/50] add the missing lineBreak option to the pretty call in the renders --- packages/render/src/browser/render.tsx | 4 +++- packages/render/src/node/render.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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; From d2f5ca4fa2d1ad01ce336988e5acb84d4f1e95b2 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 16:53:32 -0300 Subject: [PATCH 16/50] add test that ensures quality of codepen template's pretty printing --- .../utils/__snapshots__/pretty.spec.ts.snap | Bin 13948 -> 55454 bytes .../render/src/shared/utils/pretty.spec.ts | 33 ++++++++++++------ .../utils/tests/codepen-challengers.html | 1 + .../utils/{ => tests}/stripe-email.html | Bin 6326 -> 6325 bytes 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 packages/render/src/shared/utils/tests/codepen-challengers.html rename packages/render/src/shared/utils/{ => tests}/stripe-email.html (99%) 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 b2916c9d08a25839c93eee45d25c41aba3cb80b4..81c105cd1084048036c2d93d95d1ab1cee55f8d1 100644 GIT binary patch literal 55454 zcmeHQ-HzKvmfq`rin213nE_f-tK03^YIOpq?Zh6~lZLw;&maiKC9xziO_2(JTCFS= z@YP=I`~kCz#qL$+Zh^hs%efd}9>p&(d4&B=6%R>Kq(n&_yW_}COp2^3R-HO^>ipF? z&8GO=4pk6}Ggpp-p%;qF)~*u_MGk z<9{J)J&aDTZot*#X^dY)4a>VKLkDoj~5w&`=`QqTD*?e&RK>YgA`O_z& z)z}l~zH|dSv^`fk&E~^j)kKX3fiX-blg8wr;rWB+`LpH~wbi1^@ohcK)NELxRr|2E zYx)vS$BH^)sh%T!Nsa4Q|3dG%zeC%xeC47Xje`D|H*{C)W!`>iTGh{8Inpf;m9*lo z>E~nL8!JDwRY2`!Yj9seImlMH^lS@7`_c&%x39ZL&6RW{?&fR>{rPNnGo|KBn$qc1 zO@=a5m&(s{jf&8pTGDfM70$g+`f3ViMy%^5aBAgh)=D8Y`^_0>=~qP%qZczhNg=1} zWZ_cQMcW-m`SIm?xg4ZC-mxQlYUnvuF-R|^6KVXZ4VB|~V&eIZRm*`WHzXnQJx?a4 z6iA^TC0Lb%QKqpP_qyv@(@ZDIN1p>P*AK#}qcRO>*r#Ld%HGAm_afJ-_dLh*+wb-J z{iE4oXJ#?m!{VXVif|!rA+DW_jmSpawcNP4G#a2zmZU0xvRHlerCv>yC>^#~!%HwH{Pa z5W(Yzn(sWWwf6RY%8$C94;lTqruklC9eWVSPo-snSI<2?BZhY7sqZ;p`ymwt_>566 z#b1gUX2a;x_X-&A);;>0Fy2B&p?5SkpRR+a54}l;~AEx4}L8~l;=t)O17&E%99Ddi+RnUw4 z02;8dXKMl_LWtkcI@EsKg&yWY>tM`x_}|ZMH4)*^4n!gW3(wVRCP>A(3F$QDZ2vmV zaqhvem8%o4+;ozlp4+ny?zEE0(8MSnT}-_6luwM%e|s+v!`!l*?Eyd*gudqv@{ff$ zr(sT%x@hP6*nys@u*$r4AYT*u7Gd-)3{ic}7t5n|qWegZ`(VEaxno=do&($PmvM(s zjgUXaykcZaN*7^Tssl-To>hJD)A>1W4qLOp?PTd1*DKsA`DILEvs)&=n%g@0Me!Dz zeLKIE^4sz)l}p5U%4<7ARg6A4-a*@Yf{4!SmN5$r+9A^ofZL~QZX@Maa~o>%V$6#_ zMH5Xx=4fDs0A(pmhUgTmee;>~A@qJjIffpYbzoY9X}ggMZsic!RGCAGH`?JR5@tzS z#=H1eT-dz}Wf2K;2Z8YVeKE1aq0sgKSQ{~}pdnr;Vbq%Xv9in$5zQ}4Gm;Q60r?9= z-}grPTOf)`*sky*zYrvt+!9j2uqmsSC-AMq-qAG@c>er6DHG4gW>VIzJgMh)a0RV) zV5o)Jvr$<4?V?9Q^t$4^Z~y1tZeXuZdXW3 zFEJ&K4cMK-cBL9$1vh$ey#L@A492z(3@Stb5tG`U=Q%N*g@*{R2Vm8Z2Vrf9`-IUGIR&^0*AiPMF#W3t^j*?* zG{&@)e1s^yW^)_j6QvwbYNaRHb|Ln``-BXm)H1DiBHa4Zz+Ej*?+VDnV&q{E%2DKf z60ny9&of`qYijx%Q2!9rpWIYnc2%K*pNd{W1VBMnE3;W(eaNskTzTUx8D`9uEZhHM z>6fGtNHVr1-cw!c_6G+Elf6$8KQlLABySrDD8n}-%wjPwNfnseEM_MS#PYll!|}Us z|NOtAWRRsCJ6HaVUa`!cXF4}O(84*V-E3%6^mKC&0WnOcVmEZTet=7Ig z$|xjQI66240U}Z(NT}C$NAsjy?OB>Fb=%l3(z(K!?e% z_*!MnaM{AO!_FclODCyI?oP)w_KMg**SV*azh*8-?^z6T{;q)J1P6-a+Wuiq!;+RT zcO`3!i*<^`R#H}6r`XCLi~Tpm>9fyhYwkiJ4Aw$)Beyps<~8AGG{GeXSMxB^ozi{{ zKNtl*7$g{aeN`sf@wbREggc`{&X@w^n;LqW6w6TnQHb(X8?@ zUifuiT6Pp@WfhZ&7OA@Uo21E_d0_aDnMbV0$kx+L2$fof)j1vL!qrzsei-_lU@YBb zg^VuX3%f4>+b=7Wwiouve4B|kW=n%zuT>awU<0BV)YI=vJAjt_?lOo_i^jd`!}s62 zyT7-86kj?``@_zyFR&!+{+Cc|f}^7G>QS>QDu+IWC^mzg`*9a}!WqeO-M+VcCW};- zJTvKaDL0uG72=7KzAK(W5~wf-OBu{e%M~$&YLMorI=1KMuNa~NB~7Q89iH;A^)pi|>;hCKBTt^L~{Ff}oj&5Tr2rVXty+tsj* zV6+m(QH67lPC&qw(jG%jEt6-cWcJT0=%?#d8YH^^hUA~@Sm<8syjUK3{jYzn+TAii zDBnJQlZ1|=5s=-hHoioQoBu}DX5SKsSH?`z?rLA_#28RcH2aL73@rudbEi6@l=*PB9s2j8YPcZm1k5q(vjGEG^-I zgeuRcU!>C0mYG-&dy?29pwlxNZ7NrBL{J7-Ecv!*P^ z={@k}sOs=n0P4@5<3uCj=$yGtZ$rShctSO@COsi__XHkdzgR3irsQj(8Y?Zvca;=d zTsXZ}P<5q+`EIH5iwmaLYU;4GJl|2h**c1e;Eo4+#UV{2G&zj23NP*liglTkH#WFZG4fYO{

O|+O@RK#k4p-lh{BScHks)qm19HSo zY)Y20?QBAxxSh?(6t|QpvUqpRo3pjeX)mX8CNJNNj{1OB%VNtG4vydM-DQ&%j+?SI zy?>juOmBW+t-jl*ATxI>@BZ@*EHRzsE6o4??%UsgTanqGuWX)dKxVngd2(CmPM*OK zGBL2v!}maMxnt|oU2Y-qc_WWV?eeA_7x&@E1m_m=5JR{C4}u9iS#F7^atZ+(4l&U6 zP%svCrTiW&2gXNc!->L<__lN<42`*K#_etM(XAj2&UGYt0EZ-UK*O9QP9g9pBV|lO zeCF?($$_mhBtJiLI%4-glrnB2@EzHBEU5?Eu7DpQ)GHe|dGjX!h3bC#Wb<)d%e2FQ zoOm0`cM;g^`v^BtferXm)1ty+J z)Gj`aP+leb2G!F8Y}psp?zbFz7EW6Z-H18t6td;efgM{8otMZv&7mtlwr>T-@tk-+ zeIf;pX|8d44{QjWUq=#d=>dG^ad7DzW-nOC@&Bbn%q2Q=gF{Z6bADu^&_2h;D^806 zkO}C#1o}nMn9MOM9IJ|tOumcU3Lt?0k@2Y0%;-bXV%3Gc~gRbz$~=QXQ(%<|ODUsv=LoNpj`vN4Nw^Hip1WAe7^fqgq6 z31#yUZqDWa+~0lsU;ii0Jm-?o(m{k5xN#XAMG)K}EdCUl))AvA9giml6`~7{Ty>$o z(n@qWAvtQZG1}N=G@~ zyaoQ*!?VwxKRbE2b#KXpH=LtCOCN0ATNZ*S<}JTx_ZI73{|;PK&0J74b#`yLx8Lns zOAh^(_kVr=g?Gih<=_8vLv~{8-cpWUd`L5ifV3g3p>|vGKtp-oHt))fbC!AJO|);y zX(m?G3(vo}DPjPf@)@F!V05)Fg$c$)-ZWZ1)Q(Sa5FU#AbAN=mJ!eD2tYR?|*&uMj zEgWt0&R(6*<|DS zDWV@&K2=!W&b7?;R+N8hqC7ckEQlXfKJol!d*Xm0F>@u$(~tAS!B!-jK?MC{jq`{3 zM!x%YE0X15lKH*9#LkVpF#aWC04`ijJ@Dk@QWQHc5hMPQK+?DgIB<#;svVb%jp!kT zs2xwAQCiRdk(f;!(k=H=J!C&mLG|lt&w~mwYB|w@cE!aeI4$oST&+OK=GyHj8$%LhF{!P z%2_aHn@c$lurF%gH|<}e-HS-JLA-Vk0e_MBVQhaL#pkAV#w`_uABl&tbI-G>-29}* z)5t;2L77-D)+U4usGH|*27VMNh_naO=A*kFvE*sw+CI{u_w6k&`UZGWoN71oPgXsR z4)#@ zTPK2vTqJS5fVZg~Y;MWD7V$~jq;-ND^5$k|ZrZ$f?~2cp;WQHtia~cbd$Nlx>?Y8} z#w$&4j5sc3jz*cC*A}=*w6RzUj`L%g+*qO&2NlZ%gqqF!%^Y>vwYvPVx*z1q z;^!Jhwa&6!B=ZB00XY+}pHft480Ir_7p9KLrI#qmPLc*p+^3LkaULSdn>mxWWPd*w z4~|mMnA{mAvvAjQtbEcE_P}jXCL^7hm}5oi0AQ5ngddpTkuzmD2xGRGcEqPrtNX66 zlDTAlE~=v+*IglJ1y?aw@*P?&gGH`>)qEDS)p*&?=0rIx$ceJQ-#AIqmE1G>mkO-uyVzKyY21eaX+p%IwU10~<#v zElgWi7$USnexdb&$sLfJUvHF;@5YLSgdE2lV7x$byk?zt zSCbAe_oj_dHp-{hIWBPB6_f?|X1rUrC3C+yLJ6dCMpSfYsZ<|_g<-(BIY&CCB1EKG z@&efyE4DZu;(QB|U~BdHj}FJ-aEPz~AszXLeGcbuHbAEWP~n=^q{1;sT1< zhWM3IIL}0B)pgJdq!h(Vckq#TgmX*L`pE^Df^3(=k9s)-(Rl3nH)IiPHU8`9!FeSc z;kWq{(k_lK0ZoDnE zN#R}jio?q?`%>{2i@sF&g@Tr6Tg(oCjaL z1Q#EMQ}H3Jv^sjL{%!ZC;?!4{wwl1qI~pSvmmB_!+?D9y@fC$BqQI3nd4Wg1uX@BM zwO)QHj>SuSpmXcNXD8>sIejSTlj8LG$4?%gh+4hbd~tBnY(6-DAb$Pm{OJ?XYV46K z2BLnFi-vTX&4<6LiP|s>$L(fwGMO|c2My03G|!(kuc)mSRgQ1#VWwuo3N7SbA}&P} zwvPU?W&6i9C-iH=k?!EQrd*w$sWVhE7V|qJ6-qK?hZx~;jU+n_rz1T^oGCbde*Q`Q zZVe*|$945}WM3ZFPE7Io`E;ymaT0sekSj;(xOP>SW5B5nB`sk_eT35z&~nxxFnK=Q zQRoPTsfS64yV9}jOHJNl)gUW)UBLdx!y%Wx<4xK|i#(gi7^VadZUc5WZSO&ib5$Q2 zvxNC!GHmIX9ES8lA7Ffu;I{3;I*-V#7Q`k`jN}D!&)b&>Zbg8_{OTydnw>lgeS54- zpi=57Q}Mt5<@KxIzkc<**RTHY`qdv_zxvZZ{`$A=vn>p^c(7dyTY9jii8pCq_@|rH znRWW2*@I3q-hR^?;_tGoLu&V-%$1(seh=Zg?)7`dX`wGiwli(VEJ{F8E=Ri`LwkMk zRQAsF&riVL?7shXSkf zs3W|&-6tiuVHalnk_yd<1;)WSQ(v*+uGrhB+?R(9uwa)Ahjj2k<4}z z&G=9nMDT5c7l6^GY^|5jP#w!D5>ti&MXnNMZibZdMnT5`(}{K%M=*&5+Csu#T?blD zrbuXqyb=)WuyFd*mJEhnPx@A|Ho6?TW!*$l&Hl00eFRW`q|F;b(tz%z31VWq#VDB= zv59c73V?Q$#XggU-sGIiikv0qnTk7yca-8Jx15QKJQkVhz^<8FiF00)+LEp}u(ok5 zI?9k2-XbWwmze#h3smX`Y_b{6@B#p{Q_Yal@fliNjphOLEOhO@qe zq&)51`hw!@_rHWvKh~7$;V^@dn_yNh*{;SGg9I~(vk$~6LIZlHC(W10^x6i~#B2_e zmF%?))KunkQZDM1WVT)LfEQHB(xXaC=13Q*G*7zZqPe01>?0z9W~RdAc1-7+#8h2`~vWjLDPH@iI@-sxF)^4mq~ z_O!gcD%6(&&O%}Od;X3_u?$ERoGO+z0fHfx8$-d`S<~XZo_k)p=O+cq7*{TE(-zWa z@b01|!4`uhpiFEDkb!*UmFv;rc8V9pe41WOrm>}GZ?(IkOKvSNPEh^}y;+cfVo5LF z*7_@F**@|WOi?hC9shLy6FIgo)bt%#p7&253mVC?>3CyHlzg^e%_2shaAirq^odm- zgO<8d5uis0FYHm|zz0luQ2_I1cNy9NMJ+A*K}Cinhjbp6q!MjW^IX zF-y8fHc8t>R0wK`*3e{VJILXgtSQ|vFRaCcN#MK%hml?mn76fla{E^*0xjQpV!C-_ zcd{hJw-z^-%sk>S8)bp$%D}#kEpRb*ynEZ$CpMf3oawHZ#JO^p?iph!0YYz}=so`chrO(cYTO@K5ak9G z>aN1h63+&?m5`!IXX@%fQzTp!PLKitf+cnaJP3Yl3-d+19vuh!DKzT-vP~6m*G|o} zXR>26z^BGIkA~E3I^x7cPWAR(VN=x-986Oaa$O>y7Z1C%-kPmS0hI3{^rGIf z5Qe%#5hc>T$x@;5+-+ob*B}0V8oMKdM+=u>Pz4(CV_SusSsmeKBJ&061!@rXZt{mQNVoKZ7CvfWP*~nSBD^BiLl-(<#>#^T>eWAX1~WAX1$EdHHLEdCvxbLwGXEdG@`PRty- zNWDV#Sy;~DL!osZ67jyYV|vdU=@hslI(SMh>xhy@8j?XeG05B&sVi6(AuO{v$rn^Lw^{9tbJ9 zfF!#D{lTeMT8H=qYz7VmM)*3CqlS1YBLr?k4VQopd{w?j$}#vyh|9trQR!9!WSs)N#KHbRe;WN89$_j#Q!D&ETk_xCBIT7vCq!;xX;tblRK#hR4 z!oje>hDs6P(`2PUSshz+5M{onV9*eko)d**i4kj1p}94Hb?Re>0q=|)WLP;6jO-D* zLaC!h1_TCPG?Or-_*OJ(tb`0WiU94g!G&binf~hD?j%#Yqo;N!GqpSE)b8l1-T6EP N9%d0e2ET9m^Z!-U2EPCR delta 13 VcmbQYk@-)~hMBCJcQ1cz001&a2QdHu diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 63b86c91de..c21e251eac 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -2,8 +2,12 @@ import fs from 'node:fs'; import path from 'node:path'; import { lenientParse, pretty, wrapText } from './pretty'; -const stripeHTML = fs.readFileSync( - path.resolve(__dirname, './stripe-email.html'), +const stripeHtml = fs.readFileSync( + path.resolve(__dirname, './tests/stripe-email.html'), + 'utf8', +); +const codepenHtml = fs.readFileSync( + path.resolve(__dirname, './tests/codepen-challengers.html'), 'utf8', ); @@ -27,17 +31,24 @@ describe('pretty', () => { expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); - it("should prettify Preview component's complex characters correctly", () => { - expect(pretty(stripeHTML, { lineBreak: '\n' })).toMatchSnapshot(); + it("should prettify Stripe's template correctly", () => { + expect(pretty(stripeHtml, { lineBreak: '\n' })).toMatchSnapshot(); + }); + + it.only("should prettify Code Pen's template correctly", () => { + expect(pretty(codepenHtml, { lineBreak: '\n' })).toMatchSnapshot(); }); - // test('if mso syntax does not wrap', async () => { - // expect( - // await pretty( - // ``, - // ), - // ).toMatchSnapshot(); - // }); + it('should not wrap [if mso] syntax', () => { + expect( + pretty( + ``, + { + lineBreak: '\n', + }, + ), + ).toMatchSnapshot(); + }); }); describe('wrapText()', () => { 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
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
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 6345cd7593389cd236c92938f2c5ac298e07a14b..5b5c2882e1def611f7349d0da707dcdf04f88967 100644 GIT binary patch delta 8 PcmdmHxYckY<5md(5Q_s% delta 9 QcmdmLxXqA}Yr|Fv01?dtRsaA1 From fcd6352766081b6a561b8b002efbd640d2528099 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 17:14:50 -0300 Subject: [PATCH 17/50] separate comment and doctype for proper parsing --- packages/render/src/shared/utils/pretty.ts | 49 +++++++++++++++------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 10e1c1cc73..05f33cff80 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -17,8 +17,13 @@ interface HtmlTag { /** * Something like the DOCTYPE for the document, or comments. */ -interface HtmlDeclaration { - type: 'declaration'; +interface HtmlDoctype { + type: 'doctype'; + content: string; +} + +interface HtmlComment { + type: 'comment'; content: string; } @@ -27,7 +32,7 @@ interface HtmlText { content: string; } -type HtmlNode = HtmlTag | HtmlDeclaration | HtmlText; +type HtmlNode = HtmlTag | HtmlDoctype | HtmlComment | HtmlText; export const lenientParse = (html: string): HtmlNode[] => { const result: HtmlNode[] = []; @@ -59,19 +64,33 @@ export const lenientParse = (html: string): HtmlNode[] => { index = htmlObjectStart; } - if (html.startsWith('', index + 2); + if (html.startsWith('', index + ''.length; + continue; + } + + if (html.startsWith('', index + ''.length; continue; } @@ -270,8 +289,10 @@ const prettyNodes = ( formatted += `${lineBreak}`; } - } else if (node.type === 'declaration') { - formatted += `${indentation}${node.content}${lineBreak}`; + } else if (node.type === 'comment') { + formatted += `${indentation}${lineBreak}`; + } else if (node.type === 'doctype') { + formatted += `${indentation}${lineBreak}`; } } return formatted; From fe68ab4ec268f45fb4ad7a5b1b4a86c48f8667e2 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 17:14:53 -0300 Subject: [PATCH 18/50] remove .only --- packages/render/src/shared/utils/pretty.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index c21e251eac..9555335b67 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -35,7 +35,7 @@ describe('pretty', () => { expect(pretty(stripeHtml, { lineBreak: '\n' })).toMatchSnapshot(); }); - it.only("should prettify Code Pen's template correctly", () => { + it("should prettify Code Pen's template correctly", () => { expect(pretty(codepenHtml, { lineBreak: '\n' })).toMatchSnapshot(); }); From ca91744638ff7f1bed33ecc479f8f94076427586 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 16 May 2025 17:15:00 -0300 Subject: [PATCH 19/50] add test snapshot for if mso test --- .../src/shared/utils/__snapshots__/pretty.spec.ts.snap | 7 +++++++ 1 file changed, 7 insertions(+) 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 81c105cd10..307e91710e 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -61,6 +61,13 @@ exports[`lenientParse() > should parse base doucment correctly 1`] = ` ] `; +exports[`pretty > should not wrap [if mso] syntax 1`] = ` +" + + +" +`; + exports[`pretty > should prettify Code Pen's template correctly 1`] = ` " From 9917d07714a3a03ea243f0b2dad59d38678415db Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 19 May 2025 09:18:11 -0300 Subject: [PATCH 20/50] update snaps --- .../utils/__snapshots__/pretty.spec.ts.snap | 1011 ++++++++--------- 1 file changed, 495 insertions(+), 516 deletions(-) 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 307e91710e..478f8d4d25 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -3,8 +3,8 @@ exports[`lenientParse() > should parse base doucment correctly 1`] = ` [ { - "content": "", - "type": "declaration", + "content": " html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"", + "type": "doctype", }, { "children": [ @@ -234,407 +234,323 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` target="_blank" > - - - Learn More - - -

+ + + + 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). +

+ + + +
+ + + + + @@ -643,18 +559,97 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = `
+

+ 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 + + . +

+
+ + + + + + +
+ 🌟 +

- - To participate: - + 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: + - Create a Pen → + Spherical Cubes - and tag it + 😳 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 - - codepenchallenge - + 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 - and + - - - cpc-cubes - + Posing - . We'll be watching and gathering the Pens into a Collection, and sharing - on + . +

+
+ + + + + + +
+ 📖 +

+ Want to go beyond the square cube? Draw inspiration from EntropyReversed's + + - Twitter + Pulsating Tesseract - and + , Josetxu's - Instagram + Rainbow Cuboid Loader - (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 - - . -

-
- - - - - + , 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 Preview component's complex characters correctly 1`] = ` -" - +exports[`pretty > should prettify Stripe's template correctly 1`] = ` +" @@ -709,16 +704,14 @@ exports[`pretty > should prettify Preview component's complex characters correct

- Thanks for submitting your account - information. You're now ready to make live - transactions with Stripe! + 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. + You can view your payments and a variety of other information about your + account right from your dashboard.

should prettify Preview component's complex characters correct target="_blank" > - - View your Stripe Dashboard - -
-

- If you haven't - finished your integration, you might find our - - - - docs - - - - handy. -

-

- 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 - - - - tutorial about - account basics - - . -

-

- Finally, we've - put together a - - - - 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 - - - - support site - - . -

-

- — The Stripe team -

-
-

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

- - -
- + + + + View your Stripe Dashboard + + + +
+

+ If you haven't finished your integration, you might find our + + + + docs + + + + handy. +

+

+ 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 + + + + tutorial about account basics + + . +

+

+ Finally, we've put together a + + + + 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 + + + + support site + + . +

+

+ — The Stripe team +

+
+

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

@@ -857,6 +834,8 @@ exports[`pretty > should prettify Preview component's complex characters correct + + " `; @@ -892,9 +871,9 @@ 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 +" 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." From fefd9df0b573a0ea7e885bbb9cc55a30ce8d83d3 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 11 Jul 2025 11:57:12 -0300 Subject: [PATCH 21/50] separate parser from formatter --- .../src/shared/utils/lenient-parse.spec.ts | 8 + .../render/src/shared/utils/lenient-parse.ts | 166 +++++++++++++++++ .../render/src/shared/utils/pretty.spec.ts | 9 +- packages/render/src/shared/utils/pretty.ts | 167 +----------------- 4 files changed, 176 insertions(+), 174 deletions(-) create mode 100644 packages/render/src/shared/utils/lenient-parse.spec.ts create mode 100644 packages/render/src/shared/utils/lenient-parse.ts diff --git a/packages/render/src/shared/utils/lenient-parse.spec.ts b/packages/render/src/shared/utils/lenient-parse.spec.ts new file mode 100644 index 0000000000..adf1acea5b --- /dev/null +++ b/packages/render/src/shared/utils/lenient-parse.spec.ts @@ -0,0 +1,8 @@ +import { lenientParse } from './lenient-parse'; + +describe('lenientParse()', () => { + it('should parse base doucment correctly', () => { + const document = `

whatever

`; + expect(lenientParse(document)).toMatchSnapshot(); + }); +}); diff --git a/packages/render/src/shared/utils/lenient-parse.ts b/packages/render/src/shared/utils/lenient-parse.ts new file mode 100644 index 0000000000..0f68b331f3 --- /dev/null +++ b/packages/render/src/shared/utils/lenient-parse.ts @@ -0,0 +1,166 @@ +export interface HtmlTagProperty { + name: string; + value: string; +} + +export interface HtmlTag { + type: 'tag'; + name: string; + /** + * Whether the html tag is self-closing, or a void element in spec nomenclature. + */ + void: boolean; + properties: HtmlTagProperty[]; + children: HtmlNode[]; +} + +/** + * Something like the DOCTYPE for the document, or comments. + */ +export interface HtmlDoctype { + type: 'doctype'; + content: string; +} + +export interface HtmlComment { + type: 'comment'; + content: string; +} + +export interface HtmlText { + type: 'text'; + content: string; +} + +export type HtmlNode = HtmlTag | HtmlDoctype | HtmlComment | HtmlText; + +export const lenientParse = (html: string): HtmlNode[] => { + const result: HtmlNode[] = []; + + const stack: HtmlTag[] = []; // Stack to keep track of parent tags + let index = 0; // Current parsing index + while (index < html.length) { + const currentParent = stack.length > 0 ? stack[stack.length - 1] : null; + const addToTree = (node: HtmlNode) => { + if (currentParent) { + currentParent.children.push(node); + } else { + result.push(node); + } + }; + + const htmlObjectStart = html.indexOf('<', index); + if (htmlObjectStart === -1) { + if (index < html.length) { + const content = html.slice(index); + addToTree({ type: 'text', content }); + } + + break; + } + if (htmlObjectStart > index) { + const content = html.slice(index, htmlObjectStart); + addToTree({ type: 'text', content }); + index = htmlObjectStart; + } + + if (html.startsWith('', index + ''.length; + continue; + } + + if (html.startsWith('', index + ''.length; + continue; + } + + if (html.startsWith('', index + 2); + const tagName = html.slice(index + 2, bracketEnd); + + if (stack.length > 0 && stack[stack.length - 1].name === tagName) { + stack.pop(); + } else { + // Mismatched closing tag. In a simple lenient parser, we might just ignore it + // or log a warning. For now, it's effectively ignored if no match on stack top. + } + index += 3 + tagName.length; + continue; + } + + const tag: HtmlTag = { + type: 'tag', + name: '', + void: false, + properties: [], + children: [], + }; + + index++; + while (!html.startsWith('>', index) && !html.startsWith('/>', index)) { + const character = html[index]; + if (character !== ' ' && tag.name.length === 0) { + const tagNameEndIndex = Math.min( + html.indexOf(' ', index), + html.indexOf('>', index), + ); + tag.name = html.slice(index, tagNameEndIndex); + index = tagNameEndIndex; + continue; + } + + if (character !== ' ') { + const propertyName = html.slice(index, html.indexOf('=', index)); + index = html.indexOf('=', index) + 1; + + index = html.indexOf('"', index); + const propertyValue = html.slice( + index, + html.indexOf('"', index + 1) + 1, + ); + index = html.indexOf('"', index + 1) + 1; + + tag.properties.push({ + name: propertyName, + value: propertyValue, + }); + continue; + } + + index++; + } + if (html.startsWith('/>', index)) { + index++; + tag.void = true; + } + if (html.startsWith('>', index)) { + addToTree(tag); + if (!tag.void) { + stack.push(tag); + } + index++; + } + } + + return result; +}; diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 9555335b67..29d1122fc7 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import { lenientParse, pretty, wrapText } from './pretty'; +import { pretty, wrapText } from './pretty'; const stripeHtml = fs.readFileSync( path.resolve(__dirname, './tests/stripe-email.html'), @@ -11,13 +11,6 @@ const codepenHtml = fs.readFileSync( 'utf8', ); -describe('lenientParse()', () => { - it('should parse base doucment correctly', () => { - const document = `

whatever

`; - expect(lenientParse(document)).toMatchSnapshot(); - }); -}); - describe('pretty', () => { it('should prettify base doucment correctly', () => { const document = diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 05f33cff80..3e3996be20 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -1,169 +1,4 @@ -interface HtmlTagProperty { - name: string; - value: string; -} - -interface HtmlTag { - type: 'tag'; - name: string; - /** - * Whether the html tag is self-closing, or a void element in spec nomenclature. - */ - void: boolean; - properties: HtmlTagProperty[]; - children: HtmlNode[]; -} - -/** - * Something like the DOCTYPE for the document, or comments. - */ -interface HtmlDoctype { - type: 'doctype'; - content: string; -} - -interface HtmlComment { - type: 'comment'; - content: string; -} - -interface HtmlText { - type: 'text'; - content: string; -} - -type HtmlNode = HtmlTag | HtmlDoctype | HtmlComment | HtmlText; - -export const lenientParse = (html: string): HtmlNode[] => { - const result: HtmlNode[] = []; - - const stack: HtmlTag[] = []; // Stack to keep track of parent tags - let index = 0; // Current parsing index - while (index < html.length) { - const currentParent = stack.length > 0 ? stack[stack.length - 1] : null; - const addToTree = (node: HtmlNode) => { - if (currentParent) { - currentParent.children.push(node); - } else { - result.push(node); - } - }; - - const htmlObjectStart = html.indexOf('<', index); - if (htmlObjectStart === -1) { - if (index < html.length) { - const content = html.slice(index); - addToTree({ type: 'text', content }); - } - - break; - } - if (htmlObjectStart > index) { - const content = html.slice(index, htmlObjectStart); - addToTree({ type: 'text', content }); - index = htmlObjectStart; - } - - if (html.startsWith('', index + ''.length; - continue; - } - - if (html.startsWith('', index + ''.length; - continue; - } - - if (html.startsWith('', index + 2); - const tagName = html.slice(index + 2, bracketEnd); - - if (stack.length > 0 && stack[stack.length - 1].name === tagName) { - stack.pop(); - } else { - // Mismatched closing tag. In a simple lenient parser, we might just ignore it - // or log a warning. For now, it's effectively ignored if no match on stack top. - } - index += 3 + tagName.length; - continue; - } - - const tag: HtmlTag = { - type: 'tag', - name: '', - void: false, - properties: [], - children: [], - }; - - index++; - while (!html.startsWith('>', index) && !html.startsWith('/>', index)) { - const character = html[index]; - if (character !== ' ' && tag.name.length === 0) { - const tagNameEndIndex = Math.min( - html.indexOf(' ', index), - html.indexOf('>', index), - ); - tag.name = html.slice(index, tagNameEndIndex); - index = tagNameEndIndex; - continue; - } - - if (character !== ' ') { - const propertyName = html.slice(index, html.indexOf('=', index)); - index = html.indexOf('=', index) + 1; - - index = html.indexOf('"', index); - const propertyValue = html.slice( - index, - html.indexOf('"', index + 1) + 1, - ); - index = html.indexOf('"', index + 1) + 1; - - tag.properties.push({ - name: propertyName, - value: propertyValue, - }); - continue; - } - - index++; - } - if (html.startsWith('/>', index)) { - index++; - tag.void = true; - } - if (html.startsWith('>', index)) { - addToTree(tag); - if (!tag.void) { - stack.push(tag); - } - index++; - } - } - - return result; -}; +import { type HtmlNode, lenientParse } from './lenient-parse'; interface Options { /** From 0ba5c2acf1825543286af74da68fdbb08a7545b1 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 11 Jul 2025 11:59:20 -0300 Subject: [PATCH 22/50] make the options optional --- packages/render/src/shared/utils/pretty.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 3e3996be20..36b8a0ef5f 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -23,7 +23,10 @@ export const getIndentationOfLine = (line: string) => { return match[0]; }; -export const pretty = (html: string, options: Options) => { +export const pretty = ( + html: string, + options: Options = { lineBreak: '\n' }, +) => { const nodes = lenientParse(html); return prettyNodes(nodes, options); From 0b70377d4b03355824136b479ba298bf9e779ebd Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 14 Jul 2025 16:12:51 -0300 Subject: [PATCH 23/50] update snapshots --- .../__snapshots__/lenient-parse.spec.ts.snap | 62 +++++++++++++++++++ .../utils/__snapshots__/pretty.spec.ts.snap | 61 ------------------ 2 files changed, 62 insertions(+), 61 deletions(-) create mode 100644 packages/render/src/shared/utils/__snapshots__/lenient-parse.spec.ts.snap diff --git a/packages/render/src/shared/utils/__snapshots__/lenient-parse.spec.ts.snap b/packages/render/src/shared/utils/__snapshots__/lenient-parse.spec.ts.snap new file mode 100644 index 0000000000..6854b84a60 --- /dev/null +++ b/packages/render/src/shared/utils/__snapshots__/lenient-parse.spec.ts.snap @@ -0,0 +1,62 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`lenientParse() > should parse base doucment correctly 1`] = ` +[ + { + "content": " html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"", + "type": "doctype", + }, + { + "children": [ + { + "children": [], + "name": "head", + "properties": [], + "type": "tag", + "void": false, + }, + { + "children": [ + { + "children": [ + { + "content": "whatever", + "type": "text", + }, + ], + "name": "h1", + "properties": [], + "type": "tag", + "void": false, + }, + { + "children": [], + "name": "input", + "properties": [ + { + "name": "placeholder", + "value": ""hello world"", + }, + ], + "type": "tag", + "void": true, + }, + ], + "name": "body", + "properties": [ + { + "name": "style", + "value": ""background-color:#fff;"", + }, + ], + "type": "tag", + "void": false, + }, + ], + "name": "html", + "properties": [], + "type": "tag", + "void": false, + }, +] +`; 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 478f8d4d25..3c7cb08ad4 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -1,66 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`lenientParse() > should parse base doucment correctly 1`] = ` -[ - { - "content": " html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"", - "type": "doctype", - }, - { - "children": [ - { - "children": [], - "name": "head", - "properties": [], - "type": "tag", - "void": false, - }, - { - "children": [ - { - "children": [ - { - "content": "whatever", - "type": "text", - }, - ], - "name": "h1", - "properties": [], - "type": "tag", - "void": false, - }, - { - "children": [], - "name": "input", - "properties": [ - { - "name": "placeholder", - "value": ""hello world"", - }, - ], - "type": "tag", - "void": true, - }, - ], - "name": "body", - "properties": [ - { - "name": "style", - "value": ""background-color:#fff;"", - }, - ], - "type": "tag", - "void": false, - }, - ], - "name": "html", - "properties": [], - "type": "tag", - "void": false, - }, -] -`; - exports[`pretty > should not wrap [if mso] syntax 1`] = ` " From f69535698f5f21359125f9a8873288143408c5ac Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 14 Jul 2025 16:22:15 -0300 Subject: [PATCH 24/50] fix snapshot for style wrapping --- .../src/shared/utils/__snapshots__/pretty.spec.ts.snap | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 3c7cb08ad4..09b45d70a4 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -794,7 +794,13 @@ exports[`pretty > should prettify base doucment correctly 1`] = ` exports[`pretty > should print properties per-line once they get too wide 1`] = ` "
" `; From c4ff2fa22db18991f6f4e76886489d177b5c7a9f Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 15 Jul 2025 12:40:17 -0300 Subject: [PATCH 25/50] add initial verison of style multi-line formatting --- .../utils/__snapshots__/pretty.spec.ts.snap | 459 ++++++++++++++++-- .../render/src/shared/utils/pretty.spec.ts | 4 +- packages/render/src/shared/utils/pretty.ts | 71 ++- 3 files changed, 465 insertions(+), 69 deletions(-) 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 09b45d70a4..bd9cac0f95 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -19,10 +19,23 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = `
#CodePenChallenge: Cubes @@ -37,7 +50,13 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="width:100%;background-color:#191919;margin:0 auto;padding-bottom:30px;z-index:999" + style=" + width:100%; + background-color:#191919; + margin:0 auto; + padding-bottom:30px; + z-index:999 + " > @@ -45,7 +64,14 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` codepen @@ -65,14 +91,35 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = `

View this Challenge on CodePen

This week: @@ -81,7 +128,15 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = `

Cubes

@@ -129,7 +184,17 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` or freeze it out completely and start over from scratch!

💪 @@ -143,7 +208,13 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` codepen should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="margin-top:40px;margin-bottom:24px;text-align:center;background:#0b112a;color:#fff;padding:35px 20px 30px 20px;border:6px solid #2138c6" + style=" + margin-top:40px; + margin-bottom:24px; + text-align:center; + background:#0b112a; + color:#fff; + padding:35px 20px 30px 20px; + border:6px solid #2138c6 + " > @@ -161,7 +240,13 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` codepen

@@ -169,14 +254,37 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` or developer at any experience level.

Learn More @@ -195,7 +303,14 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = `

To participate: @@ -258,7 +373,13 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = `

IDEAS!

@@ -269,14 +390,28 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="padding:20px;margin:0 0 20px 0;border-radius:10px;font-size:36px;text-align:center;background:#fff4c8;border:1px solid #f4d247" + style=" + padding:20px; + margin:0 0 20px 0; + border-radius:10px; + font-size:36px; + text-align:center; + background:#fff4c8; + border:1px solid #f4d247 + " > 🌟

This week we move from 2 dimensions to three! Maybe you could exercise your @@ -304,14 +439,28 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="padding:20px;margin:0 0 20px 0;border-radius:10px;font-size:36px;text-align:center;background:#fff4c8;border:1px solid #f4d247" + style=" + padding:20px; + margin:0 0 20px 0; + border-radius:10px; + font-size:36px; + text-align:center; + background:#fff4c8; + border:1px solid #f4d247 + " > 🌟

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: @@ -335,14 +484,28 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="padding:20px;margin:0 0 20px 0;border-radius:10px;font-size:36px;text-align:center;background:#fff4c8;border:1px solid #f4d247" + style=" + padding:20px; + margin:0 0 20px 0; + border-radius:10px; + font-size:36px; + text-align:center; + background:#fff4c8; + border:1px solid #f4d247 + " > 🌟

Here's a mind-bending idea that can combine the round shapes from week one with this week's cube theme: @@ -362,7 +525,13 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = `

RESOURCES!

@@ -373,14 +542,28 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="padding:20px;margin:0 0 20px 0;border-radius:10px;font-size:36px;text-align:center;background:#d9f6ff;border:1px solid #92bfd0" + style=" + padding:20px; + margin:0 0 20px 0; + border-radius:10px; + font-size:36px; + text-align:center; + background:#d9f6ff; + border:1px solid #92bfd0 + " > 📖

Learn all about @@ -415,14 +598,28 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="padding:20px;margin:0 0 20px 0;border-radius:10px;font-size:36px;text-align:center;background:#d9f6ff;border:1px solid #92bfd0" + style=" + padding:20px; + margin:0 0 20px 0; + border-radius:10px; + font-size:36px; + text-align:center; + background:#d9f6ff; + border:1px solid #92bfd0 + " > 📖

Want to go beyond the square cube? Draw inspiration from EntropyReversed's @@ -455,14 +652,28 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="padding:20px;margin:0 0 20px 0;border-radius:10px;font-size:36px;text-align:center;background:#d9f6ff;border:1px solid #92bfd0" + style=" + padding:20px; + margin:0 0 20px 0; + border-radius:10px; + font-size:36px; + text-align:center; + background:#d9f6ff; + border:1px solid #92bfd0 + " > 📖

Did that spherical cubes concept pique your interest? Explore Ryan Mulligan's @@ -511,14 +722,37 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` Go to Challenge Page @@ -547,7 +781,12 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` email preferences @@ -556,7 +795,12 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` instantly opt out @@ -565,7 +809,12 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` support @@ -596,7 +845,14 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = `

should prettify Stripe's template correctly 1`] = ` cellPadding="0" cellSpacing="0" role="presentation" - style="max-width:37.5em;background-color:#ffffff;margin:0 auto;padding:20px 0 48px;margin-bottom:64px" + style=" + max-width:37.5em; + background-color:#ffffff; + margin:0 auto; + padding:20px 0 48px; + margin-bottom:64px + " > @@ -638,30 +907,68 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = ` width="49" />

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.

View your Stripe Dashboard @@ -670,10 +977,22 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = `

If you haven't finished your integration, you might find our @@ -690,7 +1009,13 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = ` handy.

Once you're ready to start accepting payments, you'll just need to use your live @@ -720,7 +1045,13 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = ` .

Finally, we've put together a @@ -737,7 +1068,13 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = ` 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 @@ -753,12 +1090,24 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = ` .

— The Stripe team


Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080 @@ -805,6 +1154,20 @@ exports[`pretty > should print properties per-line once they get too wide 1`] = " `; +exports[`pretty > should print style properties per-line once they get too wide 1`] = ` +"

+" +`; + exports[`wrapText() > should work with ending words that are larger than the max line size 1`] = ` "Want to go beyond the diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 29d1122fc7..360c34d3ab 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -18,9 +18,9 @@ describe('pretty', () => { expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); - it('should print properties per-line once they get too wide', () => { + it('should print style properties per-line once they get too wide', () => { const document = - '
'; + '
'; expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 36b8a0ef5f..167898ad6f 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -1,4 +1,9 @@ -import { type HtmlNode, lenientParse } from './lenient-parse'; +import { + type HtmlNode, + type HtmlTag, + type HtmlTagProperty, + lenientParse, +} from './lenient-parse'; interface Options { /** @@ -85,6 +90,45 @@ const prettyNodes = ( const { preserveLinebreaks = false, maxLineLength = 80, lineBreak } = options; const indentation = ' '.repeat(currentIndentationSize); + const printProperty = (property: HtmlTagProperty) => { + const singleLineProperty = `${property.name}=${property.value}`; + if ( + property.name === 'style' && + singleLineProperty.length > maxLineLength + ) { + const styles = property.value.slice(1, -1).split(/;/); + const wrappedStyles = styles + .map((style) => ` ${style}`) + .join(`;${lineBreak}`); + + let multiLineProperty = `${property.name}="${lineBreak}`; + multiLineProperty += `${wrappedStyles}${lineBreak}`; + multiLineProperty += ` "`; + + return multiLineProperty; + } + return singleLineProperty; + }; + + const printTagStart = (node: HtmlTag) => { + const singleLineProperties = node.properties + .map((property) => ` ${property.name}=${property.value}`) + .join(''); + const singleLineTagStart = `<${node.name}${singleLineProperties}${node.void ? '/' : ''}>`; + + if (singleLineTagStart.length <= maxLineLength) { + return singleLineTagStart; + } + + let multilineTagStart = `<${node.name}${lineBreak}`; + for (const property of node.properties) { + const printedProperty = printProperty(property); + multilineTagStart += ` ${printedProperty}${lineBreak}`; + } + multilineTagStart += `${node.void ? '/' : ''}>`; + return multilineTagStart; + }; + let formatted = ''; for (const node of nodes) { if (node.type === 'text') { @@ -96,26 +140,15 @@ const prettyNodes = ( } formatted += lineBreak; } else if (node.type === 'tag') { - const propertiesRawString = node.properties - .map((property) => ` ${property.name}=${property.value}`) - .join(''); - - const rawTagStart = `${indentation}<${node.name}${propertiesRawString}${node.void ? '/' : ''}>`; - if (rawTagStart.length - currentIndentationSize > maxLineLength) { - let tagStart = `${indentation}<${node.name}${lineBreak}`; - for (const property of node.properties) { - tagStart += `${indentation} ${property.name}=${property.value}${lineBreak}`; - } - tagStart += `${indentation}${node.void ? '/' : ''}>`; - formatted += tagStart; - } else { - formatted += `${rawTagStart}`; - } + formatted += `${indentation}`; + formatted += printTagStart(node).replaceAll( + lineBreak, + `${lineBreak}${indentation}`, + ); + if (node.void) { formatted += lineBreak; - } - - if (!node.void) { + } else { if (node.children.length > 0) { formatted += `${lineBreak}${prettyNodes( node.children, From f85fe7e2be6d41fa831ae05377ac3a81a6e2c855 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 15 Jul 2025 12:43:39 -0300 Subject: [PATCH 26/50] update snapshots --- .../utils/__snapshots__/pretty.spec.ts.snap | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) 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 bd9cac0f95..d40b4a116d 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -20,9 +20,7 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` should prettify Stripe's template correctly 1`] = `
should prettify base doucment correctly 1`] = ` " `; -exports[`pretty > should print properties per-line once they get too wide 1`] = ` -"
-" -`; - exports[`pretty > should print style properties per-line once they get too wide 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..862e131a45 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 @@ -4,17 +4,35 @@ 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.

From 85d05add90ef3c096cd0e3e582f22051872a1399 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 15 Jul 2025 14:30:35 -0300 Subject: [PATCH 29/50] add test for broken parsing --- packages/render/src/shared/utils/pretty.spec.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 360c34d3ab..c82ca58fc3 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -18,10 +18,18 @@ describe('pretty', () => { expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); - it('should print style properties per-line once they get too wide', () => { - const document = - '
'; - 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.only('should work with an img element', () => { + const document = + 'Stagg Electric Kettle'; + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); + }); }); it("should prettify Stripe's template correctly", () => { From 919c3c19156ed7f091685c9474afa4f82c221c5f Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 15 Jul 2025 17:51:39 -0300 Subject: [PATCH 30/50] fix infinte loops when missing portions of properties --- packages/render/src/shared/utils/lenient-parse.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/render/src/shared/utils/lenient-parse.ts b/packages/render/src/shared/utils/lenient-parse.ts index 0f68b331f3..e5a3f64c03 100644 --- a/packages/render/src/shared/utils/lenient-parse.ts +++ b/packages/render/src/shared/utils/lenient-parse.ts @@ -130,6 +130,11 @@ export const lenientParse = (html: string): HtmlNode[] => { } if (character !== ' ') { + if (html.indexOf('=', index) === -1) { + index = + html.indexOf('/>') === -1 ? html.indexOf('>') : html.indexOf('/>'); + break; + } const propertyName = html.slice(index, html.indexOf('=', index)); index = html.indexOf('=', index) + 1; From 84e541aa7b49c5d2bfb974cc19bf59c506cb2d80 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 15 Jul 2025 17:53:12 -0300 Subject: [PATCH 31/50] update tests --- .../utils/__snapshots__/pretty.spec.ts.snap | 19 +++++++++++++++++++ .../render/src/shared/utils/pretty.spec.ts | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) 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 d40b4a116d..4acaa85a0d 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -1149,6 +1149,25 @@ exports[`pretty > should print style properties per-line once they get too wide " `; +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 diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index c82ca58fc3..95d09d6af5 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -27,7 +27,7 @@ describe('pretty', () => { it.only('should work with an img element', () => { const document = - 'Stagg Electric Kettle'; + 'Stagg Electric Kettle'; expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); }); From ae56a015049c1a2c00fa7bdc25e2f8de01aa7199 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Thu, 17 Jul 2025 08:51:12 -0300 Subject: [PATCH 32/50] wip --- .../ensure-matching-variants.spec.tsx | 17 +++------ .../four-images-in-a-grid/inline-styles.tsx | 2 +- apps/web/tsconfig.json | 5 ++- .../utils/__snapshots__/pretty.spec.ts.snap | 2 +- .../utils/{pretty.spec.ts => pretty.spec.tsx} | 16 +++++++- .../render/tailwind copy-paste component.html | 37 +++++++++++++++++++ 6 files changed, 63 insertions(+), 16 deletions(-) rename packages/render/src/shared/utils/{pretty.spec.ts => pretty.spec.tsx} (87%) create mode 100644 packages/render/tailwind copy-paste component.html diff --git a/apps/web/components/ensure-matching-variants.spec.tsx b/apps/web/components/ensure-matching-variants.spec.tsx index 349ecfdde2..450a87d5d6 100644 --- a/apps/web/components/ensure-matching-variants.spec.tsx +++ b/apps/web/components/ensure-matching-variants.spec.tsx @@ -1,9 +1,6 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; import { pretty, render } from '@react-email/components'; -import { parse, stringify } from 'html-to-ast'; -import type { Attr, IDoc as Doc } from 'html-to-ast/dist/types'; -import postcss from 'postcss'; import { getComponentElement } from '../src/app/components/get-imported-components-for'; import { Layout } from './_components/layout'; import { componentsStructure, getComponentPathFromSlug } from './structure'; @@ -78,16 +75,14 @@ describe.skip('copy-paste components', () => { ) { const tailwindElement = await getComponentElement(tailwindVariantPath); const inlineStylesElement = await getComponentElement( - inlineStylesVariantPath, + inlineStylesVariantPath ); - const tailwindHtml = getComparableHtml( - await pretty(await render({tailwindElement})), + const tailwindHtml = pretty( + await render({tailwindElement}), ); - const inlineStylesHtml = getComparableHtml( - await pretty( - await render( - {inlineStylesElement}, - ), + const inlineStylesHtml = pretty( + await render( + {inlineStylesElement}, ), ); expect(tailwindHtml).toBe(inlineStylesHtml); diff --git a/apps/web/components/four-images-in-a-grid/inline-styles.tsx b/apps/web/components/four-images-in-a-grid/inline-styles.tsx index d9d564355e..571ee9d733 100644 --- a/apps/web/components/four-images-in-a-grid/inline-styles.tsx +++ b/apps/web/components/four-images-in-a-grid/inline-styles.tsx @@ -7,9 +7,9 @@ export const component = ( should prettify base doucment correctly 1`] = ` " `; -exports[`pretty > should print style properties per-line once they get too wide 1`] = ` +exports[`pretty > style attribute formatting > should print properties per-line once they get too wide 1`] = ` "
'; expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); diff --git a/packages/render/tailwind copy-paste component.html b/packages/render/tailwind copy-paste component.html new file mode 100644 index 0000000000..afdc5a0ef7 --- /dev/null +++ b/packages/render/tailwind copy-paste component.html @@ -0,0 +1,37 @@ +

Our products

Elegant Style

We spent two years in development to bring you the next generation of our award-winning home brew grinder. From the finest pour-overs to the coarsest cold brews, your coffee will never be the same again.

Stagg Electric KettleOde Grinder
Atmos Vacuum CanisterClyde Electric Kettle
\ No newline at end of file From 4db64aad3054fd93a44b6ccee89bbf95fb625e21 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 17 Jul 2025 11:44:51 -0300 Subject: [PATCH 33/50] don't wrap text inside style or script tags --- .../utils/__snapshots__/pretty.spec.tsx.snap | 1217 +++++++++++++++++ .../render/src/shared/utils/pretty.spec.tsx | 3 + packages/render/src/shared/utils/pretty.ts | 11 +- 3 files changed, 1229 insertions(+), 2 deletions(-) create mode 100644 packages/render/src/shared/utils/__snapshots__/pretty.spec.tsx.snap diff --git a/packages/render/src/shared/utils/__snapshots__/pretty.spec.tsx.snap b/packages/render/src/shared/utils/__snapshots__/pretty.spec.tsx.snap new file mode 100644 index 0000000000..132261adb9 --- /dev/null +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.tsx.snap @@ -0,0 +1,1217 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pretty > should not wrap [if mso] syntax 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`] = ` +" + + + + + + +
+ You're now ready to make live transactions with Stripe! +
+  ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ +
+
+ + + + + + + +
+ + + + + + +
+ Stripe +
+

+ 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. +

+ + + + + + View your Stripe Dashboard + + + + + +
+

+ If you haven't finished your integration, you might find our + + + + docs + + + + handy. +

+

+ 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 + + + + tutorial about account basics + + . +

+

+ Finally, we've put together a + + + + 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 + + + + support site + + . +

+

+ — The Stripe team +

+
+

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

+
+
+ + + + +" +`; + +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.tsx b/packages/render/src/shared/utils/pretty.spec.tsx index 6a1c3c5270..6f287ccf0f 100644 --- a/packages/render/src/shared/utils/pretty.spec.tsx +++ b/packages/render/src/shared/utils/pretty.spec.tsx @@ -30,6 +30,9 @@ describe('pretty', () => { ); console.log(pretty(html)); + it('should not wrap text inside of `; + expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); describe('style attribute formatting', () => { diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index c43194c0c7..79d46ad0ce 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -85,6 +85,7 @@ export const wrapText = ( const prettyNodes = ( nodes: HtmlNode[], options: Options, + stack: HtmlNode[] = [], currentIndentationSize = 0, ) => { const { preserveLinebreaks = false, maxLineLength = 80, lineBreak } = options; @@ -137,8 +138,13 @@ const prettyNodes = ( let formatted = ''; for (const node of nodes) { if (node.type === 'text') { - if (preserveLinebreaks) { - formatted += node.content; + if ( + preserveLinebreaks || + (stack.length > 0 && + stack[0].type === 'tag' && + ['script', 'style'].includes(stack[0].name)) + ) { + formatted += `${indentation}${node.content}`; } else { const rawText = node.content.replaceAll(/(\r|\n|\r\n)\s*/g, ''); formatted += wrapText(rawText, indentation, maxLineLength, lineBreak); @@ -158,6 +164,7 @@ const prettyNodes = ( formatted += `${lineBreak}${prettyNodes( node.children, options, + [node, ...stack], currentIndentationSize + 2, )}`; formatted += `${indentation}`; From 40739dc2044466f8a838849243dc96e3efb66dbe Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 17 Jul 2025 11:48:17 -0300 Subject: [PATCH 34/50] Revert "wip" This reverts commit 007f66b420a172e17420574a65aeb72deb0ee210. --- AGENTS.md | 29 +++++++++++++++ .../ensure-matching-variants.spec.tsx | 17 ++++++--- .../four-images-in-a-grid/inline-styles.tsx | 2 +- apps/web/tsconfig.json | 5 +-- .../utils/__snapshots__/pretty.spec.ts.snap | 2 +- .../utils/{pretty.spec.tsx => pretty.spec.ts} | 14 +------ .../render/tailwind copy-paste component.html | 37 ------------------- 7 files changed, 45 insertions(+), 61 deletions(-) create mode 100644 AGENTS.md rename packages/render/src/shared/utils/{pretty.spec.tsx => pretty.spec.ts} (88%) delete mode 100644 packages/render/tailwind copy-paste component.html diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..d7062da480 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# React Email - Agent Guidelines + +## Build/Test Commands +- `pnpm build` - Build all packages using Turbo +- `pnpm test` - Run all tests using Vitest +- `pnpm test:watch` - Run tests in watch mode +- `pnpm lint` - Check code with Biome linter +- `pnpm lint:fix` - Fix linting issues automatically +- Run single test: `cd packages/[package-name] && pnpm test [test-file]` + +## Code Style (Biome Config) +- **Formatting**: 2 spaces, 80 char line width, single quotes, use `pnpm lint` for checking formatting and linting, and use `pnpm lint:fix` to lint and format all files +- **Imports**: Use `import type` for types, organize imports automatically +- **Components**: Use `React.forwardRef` for all package components in `packages` if they support React 18 with proper displayName +- **Functions**: Always prefer `const ... = () => {...}` rather than `function` +- **Exports**: Use named exports, avoid default exports because they are hard to refactor +- **Error Handling**: Use proper TypeScript types, avoid `any`, and only if necessary, use `unknown` + +## Project Structure +- Monorepo with packages in `packages/*`, `apps/*` +- Each package has `src/index.ts`, component file, and `.spec.tsx` test +- Tests use Vitest with `@react-email/render` for HTML output testing +- Use `turbo run` commands for cross-package operations +- Documentation is written using Mintlify +- There are apps in `apps` that we publish + - `apps/demo`: https://demo.react.email + - `apps/docs`: https://react.email/docs + - `apps/web`: https://react.email + diff --git a/apps/web/components/ensure-matching-variants.spec.tsx b/apps/web/components/ensure-matching-variants.spec.tsx index 450a87d5d6..349ecfdde2 100644 --- a/apps/web/components/ensure-matching-variants.spec.tsx +++ b/apps/web/components/ensure-matching-variants.spec.tsx @@ -1,6 +1,9 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; import { pretty, render } from '@react-email/components'; +import { parse, stringify } from 'html-to-ast'; +import type { Attr, IDoc as Doc } from 'html-to-ast/dist/types'; +import postcss from 'postcss'; import { getComponentElement } from '../src/app/components/get-imported-components-for'; import { Layout } from './_components/layout'; import { componentsStructure, getComponentPathFromSlug } from './structure'; @@ -75,14 +78,16 @@ describe.skip('copy-paste components', () => { ) { const tailwindElement = await getComponentElement(tailwindVariantPath); const inlineStylesElement = await getComponentElement( - inlineStylesVariantPath + inlineStylesVariantPath, ); - const tailwindHtml = pretty( - await render({tailwindElement}), + const tailwindHtml = getComparableHtml( + await pretty(await render({tailwindElement})), ); - const inlineStylesHtml = pretty( - await render( - {inlineStylesElement}, + const inlineStylesHtml = getComparableHtml( + await pretty( + await render( + {inlineStylesElement}, + ), ), ); expect(tailwindHtml).toBe(inlineStylesHtml); diff --git a/apps/web/components/four-images-in-a-grid/inline-styles.tsx b/apps/web/components/four-images-in-a-grid/inline-styles.tsx index 571ee9d733..d9d564355e 100644 --- a/apps/web/components/four-images-in-a-grid/inline-styles.tsx +++ b/apps/web/components/four-images-in-a-grid/inline-styles.tsx @@ -7,9 +7,9 @@ export const component = ( should prettify base doucment correctly 1`] = ` " `; -exports[`pretty > style attribute formatting > should print properties per-line once they get too wide 1`] = ` +exports[`pretty > should print style properties per-line once they get too wide 1`] = ` "
'; expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); diff --git a/packages/render/tailwind copy-paste component.html b/packages/render/tailwind copy-paste component.html deleted file mode 100644 index afdc5a0ef7..0000000000 --- a/packages/render/tailwind copy-paste component.html +++ /dev/null @@ -1,37 +0,0 @@ -

Our products

Elegant Style

We spent two years in development to bring you the next generation of our award-winning home brew grinder. From the finest pour-overs to the coarsest cold brews, your coffee will never be the same again.

Stagg Electric KettleOde Grinder
Atmos Vacuum CanisterClyde Electric Kettle
\ No newline at end of file From f77574cfb460f455e07714499577c8ad0c5d849f Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 17 Jul 2025 16:14:44 -0300 Subject: [PATCH 35/50] move the printing functions up --- packages/render/src/shared/utils/pretty.ts | 95 ++++++++++++---------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 79d46ad0ce..749f0388b6 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -82,6 +82,55 @@ export const wrapText = ( return wrappedText; }; +const printProperty = ( + property: HtmlTagProperty, + maxLineLength: number, + lineBreak: string, +) => { + const singleLineProperty = `${property.name}=${property.value}`; + if (property.name === '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 = `${property.name}="${lineBreak}`; + multiLineProperty += `${wrappedStyles}${lineBreak}`; + multiLineProperty += ` "`; + + return multiLineProperty; + } + return singleLineProperty; +}; + +const printTagStart = ( + node: HtmlTag, + maxLineLength: number, + lineBreak: string, +) => { + const singleLineProperties = node.properties + .map((property) => ` ${property.name}=${property.value}`) + .join(''); + const singleLineTagStart = `<${node.name}${singleLineProperties}${node.void ? '/' : ''}>`; + + if (singleLineTagStart.length <= maxLineLength) { + return singleLineTagStart; + } + + let multilineTagStart = `<${node.name}${lineBreak}`; + for (const property of node.properties) { + const printedProperty = printProperty(property, maxLineLength, lineBreak); + multilineTagStart += ` ${printedProperty}${lineBreak}`; + } + multilineTagStart += `${node.void ? '/' : ''}>`; + return multilineTagStart; +}; + const prettyNodes = ( nodes: HtmlNode[], options: Options, @@ -91,50 +140,6 @@ const prettyNodes = ( const { preserveLinebreaks = false, maxLineLength = 80, lineBreak } = options; const indentation = ' '.repeat(currentIndentationSize); - const printProperty = (property: HtmlTagProperty) => { - const singleLineProperty = `${property.name}=${property.value}`; - if ( - property.name === '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 = `${property.name}="${lineBreak}`; - multiLineProperty += `${wrappedStyles}${lineBreak}`; - multiLineProperty += ` "`; - - return multiLineProperty; - } - return singleLineProperty; - }; - - const printTagStart = (node: HtmlTag) => { - const singleLineProperties = node.properties - .map((property) => ` ${property.name}=${property.value}`) - .join(''); - const singleLineTagStart = `<${node.name}${singleLineProperties}${node.void ? '/' : ''}>`; - - if (singleLineTagStart.length <= maxLineLength) { - return singleLineTagStart; - } - - let multilineTagStart = `<${node.name}${lineBreak}`; - for (const property of node.properties) { - const printedProperty = printProperty(property); - multilineTagStart += ` ${printedProperty}${lineBreak}`; - } - multilineTagStart += `${node.void ? '/' : ''}>`; - return multilineTagStart; - }; - let formatted = ''; for (const node of nodes) { if (node.type === 'text') { @@ -152,7 +157,7 @@ const prettyNodes = ( formatted += lineBreak; } else if (node.type === 'tag') { formatted += `${indentation}`; - formatted += printTagStart(node).replaceAll( + formatted += printTagStart(node, maxLineLength, lineBreak).replaceAll( lineBreak, `${lineBreak}${indentation}`, ); From f13ed85bd4ff92ae0885b7032277ee26956ebf91 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 17 Jul 2025 16:39:52 -0300 Subject: [PATCH 36/50] remove only --- packages/render/src/shared/utils/pretty.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index bd6acc158a..510b3652f5 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -30,7 +30,7 @@ describe('pretty', () => { expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); }); - it.only('should work with an img element', () => { + it('should work with an img element', () => { const document = 'Stagg Electric Kettle'; expect(pretty(document, { lineBreak: '\n' })).toMatchSnapshot(); From 2e8370eba799017516f930f0c01bf06e56a4b90d Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 17 Jul 2025 16:40:01 -0300 Subject: [PATCH 37/50] add space to the single line void tags --- packages/render/src/shared/utils/pretty.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 749f0388b6..51ed1decbc 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -116,7 +116,7 @@ const printTagStart = ( const singleLineProperties = node.properties .map((property) => ` ${property.name}=${property.value}`) .join(''); - const singleLineTagStart = `<${node.name}${singleLineProperties}${node.void ? '/' : ''}>`; + const singleLineTagStart = `<${node.name}${singleLineProperties}${node.void ? ' /' : ''}>`; if (singleLineTagStart.length <= maxLineLength) { return singleLineTagStart; From 179ccd11d4c4c9dc2722bae679ab2fb441cf1621 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 17 Jul 2025 16:40:06 -0300 Subject: [PATCH 38/50] update snapshots --- .../utils/__snapshots__/pretty.spec.ts.snap | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) 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 4acaa85a0d..a81cb65eb8 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -7,15 +7,32 @@ exports[`pretty > should not wrap [if mso] syntax 1`] = ` " `; +exports[`pretty > should not wrap text inside of + + + + + +" +`; + exports[`pretty > should prettify Code Pen's template correctly 1`] = ` " - - - - - + + + + + should prettify Stripe's template correctly 1`] = ` - - + +
+ " `; -exports[`pretty > should print style properties per-line once they get too wide 1`] = ` +exports[`pretty > style attribute formatting > should print properties per-line once they get too wide 1`] = ` "
" -`; From f3e74cc9255c986c98e685ebc252a339b9e5941d Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Fri, 18 Jul 2025 10:28:48 -0300 Subject: [PATCH 40/50] update snapshots --- .../__snapshots__/get-email-component.spec.ts.snap | 12 ++++++------ .../testing/__snapshots__/export.spec.ts.snap | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) 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 f334aeb6af..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 @@ -4,12 +4,12 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` " - - - - - - + + + + + + - - - - + + + + Date: Fri, 18 Jul 2025 11:19:25 -0300 Subject: [PATCH 41/50] remove agents --- AGENTS.md | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index d7062da480..0000000000 --- a/AGENTS.md +++ /dev/null @@ -1,29 +0,0 @@ -# React Email - Agent Guidelines - -## Build/Test Commands -- `pnpm build` - Build all packages using Turbo -- `pnpm test` - Run all tests using Vitest -- `pnpm test:watch` - Run tests in watch mode -- `pnpm lint` - Check code with Biome linter -- `pnpm lint:fix` - Fix linting issues automatically -- Run single test: `cd packages/[package-name] && pnpm test [test-file]` - -## Code Style (Biome Config) -- **Formatting**: 2 spaces, 80 char line width, single quotes, use `pnpm lint` for checking formatting and linting, and use `pnpm lint:fix` to lint and format all files -- **Imports**: Use `import type` for types, organize imports automatically -- **Components**: Use `React.forwardRef` for all package components in `packages` if they support React 18 with proper displayName -- **Functions**: Always prefer `const ... = () => {...}` rather than `function` -- **Exports**: Use named exports, avoid default exports because they are hard to refactor -- **Error Handling**: Use proper TypeScript types, avoid `any`, and only if necessary, use `unknown` - -## Project Structure -- Monorepo with packages in `packages/*`, `apps/*` -- Each package has `src/index.ts`, component file, and `.spec.tsx` test -- Tests use Vitest with `@react-email/render` for HTML output testing -- Use `turbo run` commands for cross-package operations -- Documentation is written using Mintlify -- There are apps in `apps` that we publish - - `apps/demo`: https://demo.react.email - - `apps/docs`: https://react.email/docs - - `apps/web`: https://react.email - From f4473b08354a5c687cd1e4690800b1a0bfb29286 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Fri, 18 Jul 2025 15:36:42 -0300 Subject: [PATCH 42/50] update snapshot for new tests --- .../tailwind/src/__snapshots__/tailwind.spec.tsx.snap | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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`] = ` - + + " `; From 0ccf93bf950793929e03216d78dc45c825885be6 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 22 Jul 2025 16:41:14 -0300 Subject: [PATCH 43/50] first effort at simplifying wrapText function --- .../render/src/shared/utils/lenient-parse.ts | 2 +- .../render/src/shared/utils/pretty.spec.ts | 4 +- packages/render/src/shared/utils/pretty.ts | 48 ++++++++++--------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/render/src/shared/utils/lenient-parse.ts b/packages/render/src/shared/utils/lenient-parse.ts index e5a3f64c03..b3bf79475e 100644 --- a/packages/render/src/shared/utils/lenient-parse.ts +++ b/packages/render/src/shared/utils/lenient-parse.ts @@ -61,7 +61,7 @@ export const lenientParse = (html: string): HtmlNode[] => { if (htmlObjectStart > index) { const content = html.slice(index, htmlObjectStart); addToTree({ type: 'text', content }); - index = htmlObjectStart; + index = htmlObjectStart } if (html.startsWith(' - -" -`; - -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`] = ` -" - - - - - - -
- You're now ready to make live transactions with Stripe! -
-  ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ -
-
- - - - - - - -
- - - - - - -
- Stripe -
-

- 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. -

- - - - - - View your Stripe Dashboard - - - - - -
-

- If you haven't finished your integration, you might find our - - - - docs - - - - handy. -

-

- 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 - - - - tutorial about account basics - - . -

-

- Finally, we've put together a - - - - 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 - - - - support site - - . -

-

- — The Stripe team -

-
-

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

-
-
- - - - -" -`; - -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." -`; From 42141b0d4dcda50d45dc96200172fc171644c85b Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 23 Jul 2025 11:12:19 -0300 Subject: [PATCH 45/50] remove line prefix from wrapText, fix issue with spaces being removed in subsequent line breaks --- .../utils/__snapshots__/pretty.spec.ts.snap | 12 ++++---- .../render/src/shared/utils/pretty.spec.ts | 5 +--- packages/render/src/shared/utils/pretty.ts | 29 ++++++------------- 3 files changed, 16 insertions(+), 30 deletions(-) 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 a81cb65eb8..2c410e0b73 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -1196,12 +1196,12 @@ 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." +"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`] = ` diff --git a/packages/render/src/shared/utils/pretty.spec.ts b/packages/render/src/shared/utils/pretty.spec.ts index 0aa2fc931b..fbb905ce4c 100644 --- a/packages/render/src/shared/utils/pretty.spec.ts +++ b/packages/render/src/shared/utils/pretty.spec.ts @@ -62,7 +62,6 @@ describe('wrapText()', () => { expect( wrapText( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum tristique.', - '', 10, '\n', ), @@ -73,7 +72,6 @@ describe('wrapText()', () => { 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', ), @@ -84,14 +82,13 @@ describe('wrapText()', () => { const spaceCharacters = '\xa0\u200C\u200B\u200D\u200E\u200F\uFEFF'.repeat( 150 - 50, ); - expect(wrapText(spaceCharacters, '', 80, '\n')).toBe(spaceCharacters); + 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', ), diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index c544c12187..5a7dbf344d 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -39,17 +39,15 @@ export const pretty = ( export const wrapText = ( text: string, - linePrefix: string, maxLineLength: number, lineBreak: string, ): string => { if (!text.includes(' ')) { - return `${linePrefix}${text}`; + return `${text}`; } - let wrappedText = linePrefix + text; - let currentLineStartIndex = linePrefix.length; + let wrappedText = text; + let currentLineStartIndex = 0; while (wrappedText.length - currentLineStartIndex > maxLineLength) { - console.log('current wrapped text', wrappedText); const overflowingCharacterIndex = Math.min( currentLineStartIndex + maxLineLength - 1, wrappedText.length, @@ -58,29 +56,18 @@ export const wrapText = ( ' ', overflowingCharacterIndex + 1, ); - console.log('overflowingCharacterIndex', overflowingCharacterIndex); - // YOU ARE HERE: there was an issue happening when falling back to lines larger than the maxLineLength, - // and you were debugging why it was happening to fix it - if (lineBreakIndex === -1) { + if (lineBreakIndex === -1 || lineBreakIndex < currentLineStartIndex) { lineBreakIndex = wrappedText.indexOf(' ', overflowingCharacterIndex); - console.log( - 'text after overflow:', - wrappedText.slice(overflowingCharacterIndex), - ); if (lineBreakIndex === -1) { return wrappedText; } } - console.log('lineBreakIndex', lineBreakIndex); wrappedText = wrappedText.slice(0, lineBreakIndex) + lineBreak + - linePrefix + wrappedText.slice(lineBreakIndex + 1); - currentLineStartIndex = - lineBreak.length + linePrefix.length + lineBreakIndex; + currentLineStartIndex = lineBreak.length + lineBreakIndex; } - console.log(wrappedText); return wrappedText; }; @@ -100,7 +87,6 @@ const printProperty = ( const wrappedStyles = styles .map((style) => ` ${style}`) .join(`;${lineBreak}`); - let multiLineProperty = `${property.name}="${lineBreak}`; multiLineProperty += `${wrappedStyles}${lineBreak}`; multiLineProperty += ` "`; @@ -154,7 +140,10 @@ const prettyNodes = ( formatted += `${indentation}${node.content}`; } else { const rawText = node.content.replaceAll(/(\r|\n|\r\n)\s*/g, ''); - formatted += wrapText(rawText, indentation, maxLineLength, lineBreak); + formatted += wrapText(rawText, maxLineLength, lineBreak) + .split(lineBreak) + .map((line) => `${indentation}${line}`) + .join(lineBreak); } formatted += lineBreak; } else if (node.type === 'tag') { From f841d13b1c52a70c37b5dc201baec4e8ebf8526e Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 23 Jul 2025 11:13:40 -0300 Subject: [PATCH 46/50] update with improved result on text wrapping --- .../render/src/shared/utils/__snapshots__/pretty.spec.ts.snap | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 2c410e0b73..eb3786022c 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -1189,8 +1189,7 @@ exports[`wrapText() > should work with ending words that are larger than the max "Want to go beyond the square cube? -Draw -inspiration +Draw inspiration from EntropyReversed's" `; From 99253e2e31a8ab23d93427e54d4a1078fb7a881c Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 23 Jul 2025 11:14:40 -0300 Subject: [PATCH 47/50] update snapshots --- .../utils/__snapshots__/pretty.spec.ts.snap | 28 +++++++++---------- .../render/src/shared/utils/pretty.spec.ts | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) 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 eb3786022c..4fd455cc74 100644 --- a/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap +++ b/packages/render/src/shared/utils/__snapshots__/pretty.spec.ts.snap @@ -172,8 +172,8 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` The Shape challenge continues!

- Last week, we kicked things off with round shapes. We "rounded" up - the Pens from week one in our + Last week, we kicked things off with round shapes. We "rounded" up the + Pens from week one in our @@ -189,8 +189,8 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` 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 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 @@ -265,8 +265,8 @@ exports[`pretty > should prettify Code Pen's template correctly 1`] = ` width="250" />

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

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 + 😳 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.

@@ -947,8 +947,8 @@ exports[`pretty > should prettify Stripe's template correctly 1`] = ` text-align:left " > - You can view your payments and a variety of other information about your - account right from your dashboard. + You can view your payments and a variety of other information about your account + right from your dashboard.

should prettify Stripe's template correctly 1`] = ` text-align:left " > - Once you're ready to start accepting payments, you'll just need to - use your live + Once you're ready to start accepting payments, you'll just need to use + your live should prettify Stripe's template correctly 1`] = ` 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 + 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 { }); describe('wrapText()', () => { - it.only('should work with short lines', () => { + it('should work with short lines', () => { expect( wrapText( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum tristique.', @@ -77,7 +77,7 @@ describe('wrapText()', () => { ), ).toMatchSnapshot(); }); -;; + it('should work with space characters from Preview component', () => { const spaceCharacters = '\xa0\u200C\u200B\u200D\u200E\u200F\uFEFF'.repeat( 150 - 50, From 7008a51cf9234ec8a1b8539fa89431c2316e5fa3 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 23 Jul 2025 14:03:13 -0300 Subject: [PATCH 48/50] remove in-hosue parser in favor of node-html-parser even though it has node in the name it actually runs fine in the browser --- packages/render/package.json | 5 +- .../__snapshots__/lenient-parse.spec.ts.snap | 62 ------- .../src/shared/utils/lenient-parse.spec.ts | 8 - .../render/src/shared/utils/lenient-parse.ts | 171 ------------------ packages/render/src/shared/utils/pretty.ts | 110 ++++++----- pnpm-lock.yaml | 3 + 6 files changed, 60 insertions(+), 299 deletions(-) delete mode 100644 packages/render/src/shared/utils/__snapshots__/lenient-parse.spec.ts.snap delete mode 100644 packages/render/src/shared/utils/lenient-parse.spec.ts delete mode 100644 packages/render/src/shared/utils/lenient-parse.ts diff --git a/packages/render/package.json b/packages/render/package.json index 0f076aa989..7b14262bd5 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -84,19 +84,20 @@ "node": ">=18.0.0" }, "dependencies": { - "html-to-text": "^9.0.5" + "html-to-text": "^9.0.5", + "node-html-parser": "^7.0.1" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" }, "devDependencies": { - "react-promise-suspense": "^0.3.4", "@edge-runtime/vm": "5.0.0", "@types/html-to-text": "9.0.4", "@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/shared/utils/__snapshots__/lenient-parse.spec.ts.snap b/packages/render/src/shared/utils/__snapshots__/lenient-parse.spec.ts.snap deleted file mode 100644 index 6854b84a60..0000000000 --- a/packages/render/src/shared/utils/__snapshots__/lenient-parse.spec.ts.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`lenientParse() > should parse base doucment correctly 1`] = ` -[ - { - "content": " html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"", - "type": "doctype", - }, - { - "children": [ - { - "children": [], - "name": "head", - "properties": [], - "type": "tag", - "void": false, - }, - { - "children": [ - { - "children": [ - { - "content": "whatever", - "type": "text", - }, - ], - "name": "h1", - "properties": [], - "type": "tag", - "void": false, - }, - { - "children": [], - "name": "input", - "properties": [ - { - "name": "placeholder", - "value": ""hello world"", - }, - ], - "type": "tag", - "void": true, - }, - ], - "name": "body", - "properties": [ - { - "name": "style", - "value": ""background-color:#fff;"", - }, - ], - "type": "tag", - "void": false, - }, - ], - "name": "html", - "properties": [], - "type": "tag", - "void": false, - }, -] -`; diff --git a/packages/render/src/shared/utils/lenient-parse.spec.ts b/packages/render/src/shared/utils/lenient-parse.spec.ts deleted file mode 100644 index adf1acea5b..0000000000 --- a/packages/render/src/shared/utils/lenient-parse.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { lenientParse } from './lenient-parse'; - -describe('lenientParse()', () => { - it('should parse base doucment correctly', () => { - const document = `

whatever

`; - expect(lenientParse(document)).toMatchSnapshot(); - }); -}); diff --git a/packages/render/src/shared/utils/lenient-parse.ts b/packages/render/src/shared/utils/lenient-parse.ts deleted file mode 100644 index b3bf79475e..0000000000 --- a/packages/render/src/shared/utils/lenient-parse.ts +++ /dev/null @@ -1,171 +0,0 @@ -export interface HtmlTagProperty { - name: string; - value: string; -} - -export interface HtmlTag { - type: 'tag'; - name: string; - /** - * Whether the html tag is self-closing, or a void element in spec nomenclature. - */ - void: boolean; - properties: HtmlTagProperty[]; - children: HtmlNode[]; -} - -/** - * Something like the DOCTYPE for the document, or comments. - */ -export interface HtmlDoctype { - type: 'doctype'; - content: string; -} - -export interface HtmlComment { - type: 'comment'; - content: string; -} - -export interface HtmlText { - type: 'text'; - content: string; -} - -export type HtmlNode = HtmlTag | HtmlDoctype | HtmlComment | HtmlText; - -export const lenientParse = (html: string): HtmlNode[] => { - const result: HtmlNode[] = []; - - const stack: HtmlTag[] = []; // Stack to keep track of parent tags - let index = 0; // Current parsing index - while (index < html.length) { - const currentParent = stack.length > 0 ? stack[stack.length - 1] : null; - const addToTree = (node: HtmlNode) => { - if (currentParent) { - currentParent.children.push(node); - } else { - result.push(node); - } - }; - - const htmlObjectStart = html.indexOf('<', index); - if (htmlObjectStart === -1) { - if (index < html.length) { - const content = html.slice(index); - addToTree({ type: 'text', content }); - } - - break; - } - if (htmlObjectStart > index) { - const content = html.slice(index, htmlObjectStart); - addToTree({ type: 'text', content }); - index = htmlObjectStart - } - - if (html.startsWith('', index + ''.length; - continue; - } - - if (html.startsWith('', index + ''.length; - continue; - } - - if (html.startsWith('', index + 2); - const tagName = html.slice(index + 2, bracketEnd); - - if (stack.length > 0 && stack[stack.length - 1].name === tagName) { - stack.pop(); - } else { - // Mismatched closing tag. In a simple lenient parser, we might just ignore it - // or log a warning. For now, it's effectively ignored if no match on stack top. - } - index += 3 + tagName.length; - continue; - } - - const tag: HtmlTag = { - type: 'tag', - name: '', - void: false, - properties: [], - children: [], - }; - - index++; - while (!html.startsWith('>', index) && !html.startsWith('/>', index)) { - const character = html[index]; - if (character !== ' ' && tag.name.length === 0) { - const tagNameEndIndex = Math.min( - html.indexOf(' ', index), - html.indexOf('>', index), - ); - tag.name = html.slice(index, tagNameEndIndex); - index = tagNameEndIndex; - continue; - } - - if (character !== ' ') { - if (html.indexOf('=', index) === -1) { - index = - html.indexOf('/>') === -1 ? html.indexOf('>') : html.indexOf('/>'); - break; - } - const propertyName = html.slice(index, html.indexOf('=', index)); - index = html.indexOf('=', index) + 1; - - index = html.indexOf('"', index); - const propertyValue = html.slice( - index, - html.indexOf('"', index + 1) + 1, - ); - index = html.indexOf('"', index + 1) + 1; - - tag.properties.push({ - name: propertyName, - value: propertyValue, - }); - continue; - } - - index++; - } - if (html.startsWith('/>', index)) { - index++; - tag.void = true; - } - if (html.startsWith('>', index)) { - addToTree(tag); - if (!tag.void) { - stack.push(tag); - } - index++; - } - } - - return result; -}; diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 5a7dbf344d..1d135a901e 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -1,11 +1,6 @@ -import { - type HtmlNode, - type HtmlTag, - type HtmlTagProperty, - lenientParse, -} from './lenient-parse'; +import { HTMLElement, CommentNode, NodeType, parse } from 'node-html-parser'; -interface Options { +export interface Options { /** * Disables the word wrapping we do to ensure the maximum line length is kept. * @@ -19,7 +14,7 @@ interface Options { */ maxLineLength?: number; - lineBreak: '\n' | '\r\n'; + lineBreak?: '\n' | '\r\n'; } export const getIndentationOfLine = (line: string) => { @@ -28,15 +23,6 @@ export const getIndentationOfLine = (line: string) => { return match[0]; }; -export const pretty = ( - html: string, - options: Options = { lineBreak: '\n' }, -) => { - const nodes = lenientParse(html); - - return prettyNodes(nodes, options); -}; - export const wrapText = ( text: string, maxLineLength: number, @@ -72,22 +58,21 @@ export const wrapText = ( }; const printProperty = ( - property: HtmlTagProperty, + propertyName: string, + propertyValue: string, maxLineLength: number, lineBreak: string, ) => { - const singleLineProperty = `${property.name}=${property.value}`; - if (property.name === 'style' && singleLineProperty.length > maxLineLength) { + 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 = `${property.name}="${lineBreak}`; + let multiLineProperty = `${propertyName}="${lineBreak}`; multiLineProperty += `${wrappedStyles}${lineBreak}`; multiLineProperty += ` "`; @@ -97,81 +82,94 @@ const printProperty = ( }; const printTagStart = ( - node: HtmlTag, + element: HTMLElement, maxLineLength: number, lineBreak: string, ) => { - const singleLineProperties = node.properties - .map((property) => ` ${property.name}=${property.value}`) + const singleLineProperties = Object.entries(element.rawAttributes) + .map(([name, value]) => ` ${name}="${value}"`) .join(''); - const singleLineTagStart = `<${node.name}${singleLineProperties}${node.void ? ' /' : ''}>`; + const singleLineTagStart = `<${element.tagName.toLowerCase()}${singleLineProperties}${element.isVoidElement ? ' /' : ''}>`; if (singleLineTagStart.length <= maxLineLength) { return singleLineTagStart; } - let multilineTagStart = `<${node.name}${lineBreak}`; - for (const property of node.properties) { - const printedProperty = printProperty(property, maxLineLength, lineBreak); + 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 += `${node.void ? '/' : ''}>`; + multilineTagStart += `${element.isVoidElement ? '/' : ''}>`; return multilineTagStart; }; -const prettyNodes = ( - nodes: HtmlNode[], - options: Options, - stack: HtmlNode[] = [], +export const pretty = (html: string, options: Options = {}) => { + const root = parse(html, { comment: true }); + + return printChildrenOf(root, { + preserveLinebreaks: false, + maxLineLength: 80, + lineBreak: '\n', + ...options, + }); +}; + +const printChildrenOf = ( + element: HTMLElement, + options: Required, currentIndentationSize = 0, ) => { - const { preserveLinebreaks = false, maxLineLength = 80, lineBreak } = options; + const { preserveLinebreaks, lineBreak, maxLineLength } = options; const indentation = ' '.repeat(currentIndentationSize); let formatted = ''; - for (const node of nodes) { - if (node.type === 'text') { + for (const node of element.childNodes) { + if (node.nodeType === NodeType.TEXT_NODE) { if ( preserveLinebreaks || - (stack.length > 0 && - stack[0].type === 'tag' && - ['script', 'style'].includes(stack[0].name)) + ['script', 'style'].includes( + (node.parentNode.tagName ?? '').toLowerCase(), + ) || + node.rawText.startsWith(' `${indentation}${line}`) .join(lineBreak); } formatted += lineBreak; - } else if (node.type === 'tag') { + } else if (node instanceof HTMLElement) { formatted += `${indentation}`; formatted += printTagStart(node, maxLineLength, lineBreak).replaceAll( lineBreak, `${lineBreak}${indentation}`, ); - if (node.void) { + if (node.isVoidElement) { formatted += lineBreak; } else { - if (node.children.length > 0) { - formatted += `${lineBreak}${prettyNodes( - node.children, + if (node.childNodes.length > 0) { + formatted += `${lineBreak}${printChildrenOf( + node, options, - [node, ...stack], currentIndentationSize + 2, )}`; formatted += `${indentation}`; } - formatted += `${lineBreak}`; + formatted += `${lineBreak}`; } - } else if (node.type === 'comment') { - formatted += `${indentation}${lineBreak}`; - } else if (node.type === 'doctype') { - formatted += `${indentation}${lineBreak}`; + } else if (node instanceof CommentNode) { + formatted += `${indentation}${lineBreak}`; } } return formatted; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c59755fa9c..d07ccfa0a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -855,6 +855,9 @@ importers: html-to-text: specifier: ^9.0.5 version: 9.0.5 + node-html-parser: + specifier: ^7.0.1 + version: 7.0.1 react: specifier: ^19.0.0 version: 19.0.0 From bbb6d8525c5fc52cd06c93da412997c0293bc42d Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 23 Jul 2025 14:03:37 -0300 Subject: [PATCH 49/50] lint --- packages/render/src/shared/utils/pretty.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 1d135a901e..20adc481fb 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -1,4 +1,4 @@ -import { HTMLElement, CommentNode, NodeType, parse } from 'node-html-parser'; +import { CommentNode, HTMLElement, NodeType, parse } from 'node-html-parser'; export interface Options { /** From ad774ef73c3f106c97cd5b31ae4f28760e72ed20 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 23 Jul 2025 14:04:12 -0300 Subject: [PATCH 50/50] fix type issue --- packages/render/src/shared/utils/pretty.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/render/src/shared/utils/pretty.ts b/packages/render/src/shared/utils/pretty.ts index 20adc481fb..3370c22544 100644 --- a/packages/render/src/shared/utils/pretty.ts +++ b/packages/render/src/shared/utils/pretty.ts @@ -1,6 +1,6 @@ import { CommentNode, HTMLElement, NodeType, parse } from 'node-html-parser'; -export interface Options { +interface Options { /** * Disables the word wrapping we do to ensure the maximum line length is kept. *