From 612b3945acefeb6e97b7e80d37fe1ccb0d9fa5f4 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 5 Sep 2026 13:09:06 +0530 Subject: [PATCH] fix(email-renderer): preserve multiline text across email formats --- packages/email-renderer/package.json | 2 +- packages/email-renderer/src/blocks.ts | 7 ++++--- packages/email-renderer/test/email.test.ts | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index 2cf80f48..c6530bb9 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.14", + "version": "0.1.15", "type": "module", "description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.", "exports": { diff --git a/packages/email-renderer/src/blocks.ts b/packages/email-renderer/src/blocks.ts index 0dfc737c..0b043c14 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -32,6 +32,7 @@ const html = (value: unknown) => .replaceAll('"', """) .replaceAll("'", "'"); const attrs = (node: BlockNode) => node.props as Record; +const multilineText = (value: unknown) => html(value).replace(/\r\n?|\n/g, "
"); const imageDimension = (fallback: string): ValueSchema => ({ kind: "string", defaultValue: fallback, @@ -290,11 +291,11 @@ export const textBlock = leaf( { emailHtml: ({ node }) => emailCell( - `

${html(attrs(node).text)}

`, + `

${multilineText(attrs(node).text)}

`, node, ), - ampEmail: ({ node }) => emailCell(`

${html(attrs(node).text)}

`, node), - mjml: ({ node }) => `${html(attrs(node).text)}`, + ampEmail: ({ node }) => emailCell(`

${multilineText(attrs(node).text)}

`, node), + mjml: ({ node }) => `${multilineText(attrs(node).text)}`, }, ({ node }) => `${String(attrs(node).text)}\n\n`, ); diff --git a/packages/email-renderer/test/email.test.ts b/packages/email-renderer/test/email.test.ts index 710e05c5..faa5fa36 100644 --- a/packages/email-renderer/test/email.test.ts +++ b/packages/email-renderer/test/email.test.ts @@ -57,6 +57,21 @@ const document: BlockDocument = { }; describe("email renderer", () => { + test("text line breaks survive all formats without interpreting entered markup", async () => { + const draft = structuredClone(document); + const text = 'First line\r\nSecond line\n\n\rLast line'; + draft.root.slots.content[0]!.slots.content = [{id:"text",type:"content.text",version:1,props:{text},styles:{},slots:{}}]; + const rendered = await renderEmail(draft,{amp:true,mjml:true}); + expect(rendered.ok).toBe(true); + if (!rendered.ok) return; + for (const output of [rendered.value.parts.html.content,rendered.value.parts.amp?.content,rendered.value.exports?.mjml]) { + expect(output).toContain('First line
Second line

<script>alert("x")</script>
Last line'); + expect(output).not.toContain('