Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
registry-url: "https://registry.npmjs.org"
registry-url: 'https://registry.npmjs.org'

- name: Get current version
id: current_version
Expand Down
21 changes: 18 additions & 3 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"": {
"name": "better-svelte-email",
"dependencies": {
"css-tree": "^3.1.0",
"html-to-text": "^9.0.5",
"magic-string": "^0.30.21",
"parse5": "^8.0.0",
"tw-to-css": "^0.0.12",
},
"devDependencies": {
Expand All @@ -16,6 +18,7 @@
"@sveltejs/package": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.16",
"@types/css-tree": "^2.3.11",
"@types/html-to-text": "^9.0.4",
"@types/node": "^24.9.1",
"eslint": "^9.38.0",
Expand All @@ -29,7 +32,7 @@
"publint": "^0.3.15",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"svelte": "5.42.2",
"svelte": "5.43.3",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
Expand Down Expand Up @@ -274,6 +277,8 @@

"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],

"@types/css-tree": ["@types/css-tree@2.3.11", "", {}, "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg=="],

"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],

"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
Expand Down Expand Up @@ -406,6 +411,8 @@

"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],

"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],

"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],

"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
Expand Down Expand Up @@ -442,7 +449,7 @@

"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],

"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],

"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],

Expand Down Expand Up @@ -636,6 +643,8 @@

"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],

"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],

"mdsvex": ["mdsvex@0.12.6", "", { "dependencies": { "@types/mdast": "^4.0.4", "@types/unist": "^2.0.3", "prism-svelte": "^0.4.7", "prismjs": "^1.17.1", "unist-util-visit": "^2.0.1", "vfile-message": "^2.0.4" }, "peerDependencies": { "svelte": "^3.56.0 || ^4.0.0 || ^5.0.0-next.120" } }, "sha512-pupx2gzWh3hDtm/iDW4WuCpljmyHbHi34r7ktOqpPGvyiM4MyfNgdJ3qMizXdgCErmvYC9Nn/qyjePy+4ss9Wg=="],

"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
Expand Down Expand Up @@ -698,6 +707,8 @@

"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],

"parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],

"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],

"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
Expand Down Expand Up @@ -838,7 +849,7 @@

"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],

"svelte": ["svelte@5.42.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g=="],
"svelte": ["svelte@5.43.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ=="],

"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],

Expand Down Expand Up @@ -976,12 +987,16 @@

"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],

"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],

"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],

"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],

"hast-util-to-html/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],

"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],

"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],

"mdast-util-to-hast/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
}
},
"dependencies": {
"css-tree": "^3.1.0",
"html-to-text": "^9.0.5",
"magic-string": "^0.30.21",
"parse5": "^8.0.0",
"tw-to-css": "^0.0.12"
},
"optionalDependencies": {
Expand All @@ -34,6 +36,7 @@
"@sveltejs/package": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.16",
"@types/css-tree": "^2.3.11",
"@types/html-to-text": "^9.0.4",
"@types/node": "^24.9.1",
"eslint": "^9.38.0",
Expand Down Expand Up @@ -85,13 +88,19 @@
"import": "./dist/utils/index.js",
"default": "./dist/utils/index.js"
},
"./render": {
"types": "./dist/render/index.d.ts",
"import": "./dist/render/index.js",
"default": "./dist/render/index.js"
},
"./package.json": "./package.json"
},
"description": "Svelte email renderer with Tailwind support",
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*",
"!dist/**/__fixtures__",
"!dist/emails"
],
"keywords": [
Expand All @@ -112,6 +121,7 @@
"build": "bun run prepack && vite build",
"preview": "vite preview",
"package": "svelte-package",
"package:watch": "nodemon -x \"bun run package\" -i dist -e ts,svelte",
"prepare": "svelte-kit sync || echo ''",
"prepack": "svelte-kit sync && svelte-package && publint",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
Expand Down
10 changes: 7 additions & 3 deletions src/lib/components/Body.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';

let { children, style, ...restProps }: { children?: any } & HTMLAttributes<HTMLBodyElement> =
$props();
let {
children,
style,
class: className,
...restProps
}: { children?: any } & HTMLAttributes<HTMLBodyElement> = $props();
</script>

<body {...restProps}>
<table align="center" width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tbody>
<tr>
<td {style}>
<td {style} class={className}>
{@render children?.()}
</td>
</tr>
Expand Down
22 changes: 10 additions & 12 deletions src/lib/components/Button.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { styleToString, pxToPt, combineStyles } from '$lib/utils/index.js';
import type { HTMLAttributes } from 'svelte/elements';
import type { HTMLAnchorAttributes } from 'svelte/elements';

let {
href = '#',
Expand All @@ -16,7 +16,7 @@
pX?: number;
pY?: number;
children: any;
} & HTMLAttributes<HTMLAnchorElement> = $props();
} & HTMLAnchorAttributes = $props();

const y = pY * 2;
const textRaise = pxToPt(y.toString());
Expand All @@ -43,17 +43,15 @@
</script>

<a {...restProps} {href} {target} style={combineStyles(buttonStyle, style)}>
{#if pX}
<span>
{@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%;mso-text-raise:${textRaise}" hidden>&nbsp;</i><![endif]-->`}
</span>
{/if}
<span>
{@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%;mso-text-raise:${textRaise}" hidden>&nbsp;</i><![endif]-->`}
</span>

<span style={buttonTextStyle}>
{@render children?.()}
</span>
{#if pX}
<span>
{@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%" hidden>&nbsp;</i><![endif]-->`}
</span>
{/if}

<span style="display: none;">
{@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%" hidden>&nbsp;</i><![endif]-->`}
</span>
</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import { Container, Heading } from '$lib/components/index.js';
</script>

<Container>
<Heading as="h1" m="10" style="font-weight: bold;" class="text-blue-600">Template title</Heading>
</Container>
112 changes: 112 additions & 0 deletions src/lib/components/__tests__/__fixtures__/test-email.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script>
import {
Html,
Head,
Body,
Text,
Button,
Container,
Heading,
Link,
Hr,
Preview,
Section,
Row,
Column,
Img
} from '../../index.js';
import Nested from './nested/nested.svelte';

const padding = 'px-4 py-2';
const fontSemibold = 'font-semibold';
const paddingStyle = 'padding: 2rem;';
</script>

<Html>
<Head />
<Body>
<!-- Preview text (edge case: long text truncation) -->
<Preview preview="This is a preview text that should be visible in email clients" />

<Container class="bg-gray-100" style={paddingStyle}>
<!-- Nested component with margin props + Tailwind classes -->
<Nested />

<!-- Edge case: Responsive classes (should be kept as sanitized classes) -->
<Heading as="h2" class="font-semibold md:text-3xl lg:text-4xl">Responsive Heading</Heading>

<!-- Edge case: Multiple utility combinations -->
<Text class="mb-4 text-center text-lg text-gray-700">Multiple utilities text</Text>

<!-- Edge case: Mixed inline styles + Tailwind -->
<Text style="font-weight: bold;" class="mt-2 text-red-600">Bold red text with margin</Text>

<!-- Horizontal rule -->
<Hr />

<!-- Edge case: Section with Row and Column layout -->
<Section class="rounded-lg bg-white p-6">
<Row>
<Column class="w-1/2 p-4">
<Text class="font-bold">Column 1</Text>
<Text class="text-sm text-gray-600">Left column content</Text>
</Column>
<Column class="w-1/2 p-4">
<Text class="font-bold">Column 2</Text>
<Text class="text-sm text-gray-600">Right column content</Text>
</Column>
</Row>
</Section>

<Hr />

<!-- Edge case: Image with Tailwind classes -->
<Img
src="https://example.com/logo.png"
alt="Logo"
width="200"
height="100"
class="mx-auto rounded-md"
/>

<!-- Edge case: Link with Tailwind + inline styles -->
<Link href="https://example.com" class="font-semibold text-blue-600 underline">
Visit our website
</Link>

<!-- Edge case: Button with custom brand color + multiple utilities -->
<Button
class="bg-brand rounded {padding} text-white hover:bg-red-600"
style="width: 33.33%"
href="https://example.com"
>
Click Me
</Button>

<!-- Edge case: Button with gradient-like classes -->
<Button
class="rounded-full bg-linear-to-r from-blue-500 to-purple-600 px-6 py-3 text-white"
href="#"
>
Gradient Button
</Button>

<!-- Edge case: Complex spacing utilities -->
<Container class="mt-8 mb-4 border border-blue-200 bg-blue-50 px-6 py-8 shadow-sm">
<Heading as="h3" class="mb-2 text-xl font-bold text-blue-900">Nested Container</Heading>
<Text class="leading-relaxed text-blue-700">
This tests complex spacing, borders, and shadows
</Text>
</Container>

<!-- Edge case: Arbitrary values (if supported) -->
<Text class="p-[12px] text-[#123456]">Custom arbitrary values</Text>

<!-- Edge case: Empty style attribute -->
<Text style="" class="text-green-600">Text with empty style attribute</Text>

<!-- Edge case: No classes, only inline styles -->
<Text style="color: purple; font-size: 18px;">Pure inline styles</Text>
</Container>
</Body>
</Html>
Loading