better-svelte-email 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/components/Body.svelte +4 -4
- package/dist/components/Body.svelte.d.ts +6 -12
- package/dist/components/Button.svelte +11 -4
- package/dist/components/Button.svelte.d.ts +5 -15
- package/dist/components/Container.svelte +10 -3
- package/dist/components/Container.svelte.d.ts +5 -11
- package/dist/components/Hr.svelte +14 -0
- package/dist/components/Hr.svelte.d.ts +4 -0
- package/dist/components/Html.svelte +3 -3
- package/dist/components/Html.svelte.d.ts +1 -1
- package/dist/components/Section.svelte +10 -11
- package/dist/components/Section.svelte.d.ts +5 -11
- package/dist/components/Text.svelte +13 -3
- package/dist/components/Text.svelte.d.ts +6 -12
- package/dist/components/index.d.ts +4 -3
- package/dist/components/index.js +4 -3
- package/dist/emails/demo-email.svelte +31 -31
- package/dist/emails/test-email.svelte +7 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/preprocessor/index.js +24 -13
- package/dist/preprocessor/parser.d.ts +5 -2
- package/dist/preprocessor/parser.js +87 -21
- package/dist/preprocessor/types.d.ts +21 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# better-svelte-email
|
|
2
2
|
|
|
3
|
-
[](https://github.com/Konixy/better-svelte-email/actions/workflows/ci.yml)
|
|
4
3
|
[](https://github.com/Konixy/better-svelte-email/actions/workflows/release.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/better-svelte-email)
|
|
5
|
+
[](https://github.com/Konixy/better-svelte-email/stargazers)
|
|
5
6
|
|
|
6
7
|
A Svelte preprocessor that transforms Tailwind CSS classes in email components to inline styles with responsive media query support.
|
|
7
8
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import {
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
|
|
4
|
-
let {
|
|
4
|
+
let { children, ...restProps }: { children?: any } & HTMLAttributes<HTMLBodyElement> = $props();
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
|
-
<body {...restProps}
|
|
7
|
+
<body {...restProps}>
|
|
8
8
|
{@render children?.()}
|
|
9
9
|
</body>
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
type Body = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const Body: import("svelte").Component<{
|
|
7
|
-
styleString?: string;
|
|
8
|
-
children: any;
|
|
9
|
-
} & Record<string, any>, {}, "">;
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
10
2
|
type $$ComponentProps = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
children?: any;
|
|
4
|
+
} & HTMLAttributes<HTMLBodyElement>;
|
|
5
|
+
declare const Body: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type Body = ReturnType<typeof Body>;
|
|
7
|
+
export default Body;
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { styleToString, pxToPt } from '../utils/index.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
href = '#',
|
|
6
7
|
target = '_blank',
|
|
7
|
-
|
|
8
|
+
style = '',
|
|
8
9
|
pX = 0,
|
|
9
10
|
pY = 0,
|
|
10
11
|
children,
|
|
11
12
|
...restProps
|
|
12
|
-
}
|
|
13
|
+
}: {
|
|
14
|
+
href?: string;
|
|
15
|
+
target?: string;
|
|
16
|
+
pX?: number;
|
|
17
|
+
pY?: number;
|
|
18
|
+
children: any;
|
|
19
|
+
} & HTMLAttributes<HTMLAnchorElement> = $props();
|
|
13
20
|
|
|
14
21
|
const y = pY * 2;
|
|
15
22
|
const textRaise = pxToPt(y.toString());
|
|
@@ -34,7 +41,7 @@
|
|
|
34
41
|
msoTextRaise: pY ? pxToPt(pY.toString()) : undefined
|
|
35
42
|
});
|
|
36
43
|
|
|
37
|
-
const finalStyle = buttonStyle + (
|
|
44
|
+
const finalStyle = buttonStyle + (style ? ';' + style : '');
|
|
38
45
|
</script>
|
|
39
46
|
|
|
40
47
|
<a {...restProps} {href} {target} style={finalStyle}>
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
type Button = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const Button: import("svelte").Component<{
|
|
7
|
-
href?: string;
|
|
8
|
-
target?: string;
|
|
9
|
-
styleString?: string;
|
|
10
|
-
pX?: number;
|
|
11
|
-
pY?: number;
|
|
12
|
-
children: any;
|
|
13
|
-
} & Record<string, any>, {}, "">;
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
14
2
|
type $$ComponentProps = {
|
|
15
3
|
href?: string;
|
|
16
4
|
target?: string;
|
|
17
|
-
styleString?: string;
|
|
18
5
|
pX?: number;
|
|
19
6
|
pY?: number;
|
|
20
7
|
children: any;
|
|
21
|
-
} &
|
|
8
|
+
} & HTMLAttributes<HTMLAnchorElement>;
|
|
9
|
+
declare const Button: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type Button = ReturnType<typeof Button>;
|
|
11
|
+
export default Button;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { styleToString } from '../utils/index.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
|
|
4
|
-
let {
|
|
5
|
+
let {
|
|
6
|
+
children,
|
|
7
|
+
style,
|
|
8
|
+
...restProps
|
|
9
|
+
}: {
|
|
10
|
+
children: any;
|
|
11
|
+
} & HTMLAttributes<HTMLTableElement> = $props();
|
|
5
12
|
|
|
6
13
|
// Default max-width for email containers (600px = 37.5em)
|
|
7
14
|
const baseStyle = styleToString({ maxWidth: '37.5em' });
|
|
8
|
-
const finalStyle = baseStyle + (
|
|
15
|
+
const finalStyle = baseStyle + (style ? ';' + style : '');
|
|
9
16
|
</script>
|
|
10
17
|
|
|
11
18
|
<table
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
type Container = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const Container: import("svelte").Component<{
|
|
7
|
-
styleString?: string;
|
|
8
|
-
children: any;
|
|
9
|
-
} & Record<string, any>, {}, "">;
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
10
2
|
type $$ComponentProps = {
|
|
11
|
-
styleString?: string;
|
|
12
3
|
children: any;
|
|
13
|
-
} &
|
|
4
|
+
} & HTMLAttributes<HTMLTableElement>;
|
|
5
|
+
declare const Container: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type Container = ReturnType<typeof Container>;
|
|
7
|
+
export default Container;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { styleToString } from '../utils/index.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
let props: HTMLAttributes<HTMLHRElement> = $props();
|
|
6
|
+
|
|
7
|
+
const style = styleToString({
|
|
8
|
+
width: '100%',
|
|
9
|
+
border: 'none',
|
|
10
|
+
borderTop: '1px solid #eaeaea'
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<hr {...props} style={style + (props.style ? ';' + props.style : '')} />
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
interface Props {
|
|
3
3
|
lang?: string;
|
|
4
4
|
dir?: 'ltr' | 'rtl' | 'auto' | null | undefined;
|
|
5
|
-
|
|
5
|
+
style?: string;
|
|
6
6
|
children?: any;
|
|
7
7
|
[key: string]: any;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
let { lang = 'en', dir = 'ltr',
|
|
10
|
+
let { lang = 'en', dir = 'ltr', children, ...restProps }: Props = $props();
|
|
11
11
|
|
|
12
12
|
const doctype =
|
|
13
13
|
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
{@html doctype}
|
|
17
|
-
<html {...restProps} id="__svelte-email" {lang} {dir}
|
|
17
|
+
<html {...restProps} id="__svelte-email" {lang} {dir}>
|
|
18
18
|
{@render children?.()}
|
|
19
19
|
</html>
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
children,
|
|
6
|
+
...restProps
|
|
7
|
+
}: {
|
|
8
|
+
children: any;
|
|
9
|
+
} & HTMLAttributes<HTMLTableElement> = $props();
|
|
3
10
|
</script>
|
|
4
11
|
|
|
5
|
-
<table
|
|
6
|
-
width="100%"
|
|
7
|
-
role="presentation"
|
|
8
|
-
cellspacing="0"
|
|
9
|
-
cellpadding="0"
|
|
10
|
-
border="0"
|
|
11
|
-
{...restProps}
|
|
12
|
-
style={styleString}
|
|
13
|
-
>
|
|
12
|
+
<table width="100%" role="presentation" cellspacing="0" cellpadding="0" border="0" {...restProps}>
|
|
14
13
|
<tbody>
|
|
15
14
|
<tr>
|
|
16
15
|
<td>
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
type Section = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const Section: import("svelte").Component<{
|
|
7
|
-
styleString?: string;
|
|
8
|
-
children: any;
|
|
9
|
-
} & Record<string, any>, {}, "">;
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
10
2
|
type $$ComponentProps = {
|
|
11
|
-
styleString?: string;
|
|
12
3
|
children: any;
|
|
13
|
-
} &
|
|
4
|
+
} & HTMLAttributes<HTMLTableElement>;
|
|
5
|
+
declare const Section: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type Section = ReturnType<typeof Section>;
|
|
7
|
+
export default Section;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { styleToString } from '../utils/index.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
|
|
4
|
-
let {
|
|
5
|
+
let {
|
|
6
|
+
as = 'p',
|
|
7
|
+
style = '',
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: {
|
|
11
|
+
as?: string;
|
|
12
|
+
style?: string;
|
|
13
|
+
children: any;
|
|
14
|
+
} & HTMLAttributes<HTMLParagraphElement> = $props();
|
|
5
15
|
|
|
6
16
|
// Default email-safe text styles
|
|
7
17
|
const baseStyle = styleToString({
|
|
@@ -9,7 +19,7 @@
|
|
|
9
19
|
lineHeight: '24px',
|
|
10
20
|
margin: '16px 0'
|
|
11
21
|
});
|
|
12
|
-
const finalStyle = baseStyle + (
|
|
22
|
+
const finalStyle = baseStyle + (style ? ';' + style : '');
|
|
13
23
|
</script>
|
|
14
24
|
|
|
15
25
|
<svelte:element this={as} {...restProps} style={finalStyle}>
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
type Text = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const Text: import("svelte").Component<{
|
|
7
|
-
as?: string;
|
|
8
|
-
styleString?: string;
|
|
9
|
-
children: any;
|
|
10
|
-
} & Record<string, any>, {}, "">;
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
11
2
|
type $$ComponentProps = {
|
|
12
3
|
as?: string;
|
|
13
|
-
|
|
4
|
+
style?: string;
|
|
14
5
|
children: any;
|
|
15
|
-
} &
|
|
6
|
+
} & HTMLAttributes<HTMLParagraphElement>;
|
|
7
|
+
declare const Text: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Text = ReturnType<typeof Text>;
|
|
9
|
+
export default Text;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { default as Html } from './Html.svelte';
|
|
2
|
-
export { default as Head } from './Head.svelte';
|
|
3
1
|
export { default as Body } from './Body.svelte';
|
|
2
|
+
export { default as Button } from './Button.svelte';
|
|
4
3
|
export { default as Container } from './Container.svelte';
|
|
4
|
+
export { default as Head } from './Head.svelte';
|
|
5
|
+
export { default as Hr } from './Hr.svelte';
|
|
6
|
+
export { default as Html } from './Html.svelte';
|
|
5
7
|
export { default as Section } from './Section.svelte';
|
|
6
8
|
export { default as Text } from './Text.svelte';
|
|
7
|
-
export { default as Button } from './Button.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Email Components for better-svelte-email
|
|
2
2
|
// These components work with the preprocessor's styleString prop
|
|
3
|
-
export { default as Html } from './Html.svelte';
|
|
4
|
-
export { default as Head } from './Head.svelte';
|
|
5
3
|
export { default as Body } from './Body.svelte';
|
|
4
|
+
export { default as Button } from './Button.svelte';
|
|
6
5
|
export { default as Container } from './Container.svelte';
|
|
6
|
+
export { default as Head } from './Head.svelte';
|
|
7
|
+
export { default as Hr } from './Hr.svelte';
|
|
8
|
+
export { default as Html } from './Html.svelte';
|
|
7
9
|
export { default as Section } from './Section.svelte';
|
|
8
10
|
export { default as Text } from './Text.svelte';
|
|
9
|
-
export { default as Button } from './Button.svelte';
|
|
@@ -10,25 +10,25 @@
|
|
|
10
10
|
@media(min-width: 640px){.sm\:bg-green-600{background-color:rgb(22,163,74) !important}}
|
|
11
11
|
</style>
|
|
12
12
|
</Head>
|
|
13
|
-
<Body
|
|
14
|
-
<Container
|
|
13
|
+
<Body style="background-color:rgb(243,244,246); font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";">
|
|
14
|
+
<Container style="margin-left:auto;margin-right:auto; max-width:42rem; background-color:rgb(255,255,255); padding:2rem;">
|
|
15
15
|
<!-- Header -->
|
|
16
|
-
<Section
|
|
17
|
-
<Text as="h1"
|
|
18
|
-
<Text
|
|
16
|
+
<Section style="margin-bottom:1.5rem; border-bottom-width:1px; border-color:rgb(229,231,235); padding-bottom:1.5rem;">
|
|
17
|
+
<Text as="h1" style="margin-bottom:0.5rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(17,24,39);">better-svelte-email Demo</Text>
|
|
18
|
+
<Text style="color:rgb(75,85,99);">
|
|
19
19
|
This email was generated using Tailwind classes and transformed by the preprocessor!
|
|
20
20
|
</Text>
|
|
21
21
|
</Section>
|
|
22
22
|
|
|
23
23
|
<!-- Main Content -->
|
|
24
|
-
<Section
|
|
25
|
-
<Text
|
|
24
|
+
<Section style="margin-bottom:1.5rem;">
|
|
25
|
+
<Text style="margin-bottom:1rem; font-size:1.125rem;line-height:1.75rem; color:rgb(31,41,55);">Hello {userName}! 👋</Text>
|
|
26
26
|
|
|
27
|
-
<Text
|
|
27
|
+
<Text style="margin-bottom:1rem; color:rgb(55,65,81);">{testMessage}</Text>
|
|
28
28
|
|
|
29
|
-
<Container
|
|
30
|
-
<Text
|
|
31
|
-
<ul
|
|
29
|
+
<Container style="margin-bottom:1.5rem; border-radius:0.5rem; background-color:rgb(239,246,255); padding:1rem;">
|
|
30
|
+
<Text style="margin-bottom:0.5rem; font-weight:600; color:rgb(30,58,138);">✨ Features Demonstrated:</Text>
|
|
31
|
+
<ul style="list-style-type:disc; padding-left:1.25rem; color:rgb(30,64,175);">
|
|
32
32
|
<li>Tailwind classes converted to inline styles</li>
|
|
33
33
|
<li>Responsive design with media queries</li>
|
|
34
34
|
<li>Email-safe CSS transformations</li>
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
</Container>
|
|
38
38
|
|
|
39
39
|
<!-- Buttons showcase -->
|
|
40
|
-
<Section
|
|
40
|
+
<Section style="margin-bottom:1.5rem;">
|
|
41
41
|
<Button
|
|
42
42
|
href="https://github.com/Konixy/better-svelte-email"
|
|
43
|
-
class="sm_bg_green_600"
|
|
43
|
+
class="sm_bg_green_600" style="display:inline-block; border-radius:0.25rem; background-color:rgb(37,99,235); padding-left:1.5rem;padding-right:1.5rem; padding-top:0.75rem;padding-bottom:0.75rem; font-weight:600; color:rgb(255,255,255);"
|
|
44
44
|
>
|
|
45
45
|
View on GitHub
|
|
46
46
|
</Button>
|
|
47
47
|
|
|
48
48
|
<Button
|
|
49
49
|
href="https://svelte.dev"
|
|
50
|
-
|
|
50
|
+
style="margin-left:0.75rem; display:inline-block; border-radius:0.25rem; border-width:1px; border-color:rgb(209,213,219); background-color:rgb(255,255,255); padding-left:1.5rem;padding-right:1.5rem; padding-top:0.75rem;padding-bottom:0.75rem; font-weight:600; color:rgb(55,65,81);"
|
|
51
51
|
>
|
|
52
52
|
Learn Svelte 5
|
|
53
53
|
</Button>
|
|
@@ -55,23 +55,23 @@
|
|
|
55
55
|
</Section>
|
|
56
56
|
|
|
57
57
|
<!-- Stats Grid -->
|
|
58
|
-
<Section
|
|
58
|
+
<Section style="margin-bottom:1.5rem;">
|
|
59
59
|
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
60
60
|
<tbody>
|
|
61
61
|
<tr>
|
|
62
|
-
<td
|
|
63
|
-
<Text as="div"
|
|
64
|
-
<Text as="div"
|
|
62
|
+
<td style="border-radius:0.5rem; background-color:rgb(250,245,255); padding:1rem; text-align:center;width: 33.33%;" >
|
|
63
|
+
<Text as="div" style="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(147,51,234);">52+</Text>
|
|
64
|
+
<Text as="div" style="font-size:0.875rem;line-height:1.25rem; color:rgb(126,34,206);">Tests Passing</Text>
|
|
65
65
|
</td>
|
|
66
66
|
<td style="width: 10px;"></td>
|
|
67
|
-
<td
|
|
68
|
-
<Text as="div"
|
|
69
|
-
<Text as="div"
|
|
67
|
+
<td style="border-radius:0.5rem; background-color:rgb(240,253,244); padding:1rem; text-align:center;width: 33.33%;" >
|
|
68
|
+
<Text as="div" style="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(22,163,74);">100%</Text>
|
|
69
|
+
<Text as="div" style="font-size:0.875rem;line-height:1.25rem; color:rgb(21,128,61);">TypeScript</Text>
|
|
70
70
|
</td>
|
|
71
71
|
<td style="width: 10px;"></td>
|
|
72
|
-
<td
|
|
73
|
-
<Text as="div"
|
|
74
|
-
<Text as="div"
|
|
72
|
+
<td style="border-radius:0.5rem; background-color:rgb(255,247,237); padding:1rem; text-align:center;width: 33.33%;" >
|
|
73
|
+
<Text as="div" style="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(234,88,12);">0ms</Text>
|
|
74
|
+
<Text as="div" style="font-size:0.875rem;line-height:1.25rem; color:rgb(194,65,12);">Runtime Cost</Text>
|
|
75
75
|
</td>
|
|
76
76
|
</tr>
|
|
77
77
|
</tbody>
|
|
@@ -79,11 +79,11 @@
|
|
|
79
79
|
</Section>
|
|
80
80
|
|
|
81
81
|
<!-- Code Example -->
|
|
82
|
-
<Container
|
|
83
|
-
<Text
|
|
82
|
+
<Container style="margin-bottom:1.5rem; border-radius:0.5rem; background-color:rgb(249,250,251); padding:1rem;">
|
|
83
|
+
<Text style="margin-bottom:0.5rem; font-size:0.875rem;line-height:1.25rem; font-weight:600; color:rgb(55,65,81);">How it works:</Text>
|
|
84
84
|
<pre
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
style="overflow-x:auto; border-radius:0.25rem; background-color:rgb(17,24,39); padding:0.75rem; font-size:0.75rem;line-height:1rem; color:rgb(243,244,246);margin: 0;"
|
|
86
|
+
><code
|
|
87
87
|
>// Input
|
|
88
88
|
<Button class="bg-blue-500 text-white p-4">
|
|
89
89
|
Click Me
|
|
@@ -97,9 +97,9 @@
|
|
|
97
97
|
</Container>
|
|
98
98
|
|
|
99
99
|
<!-- Footer -->
|
|
100
|
-
<Section
|
|
101
|
-
<Text
|
|
102
|
-
<Text
|
|
100
|
+
<Section style="border-top-width:1px; border-color:rgb(229,231,235); padding-top:1.5rem; text-align:center;">
|
|
101
|
+
<Text style="margin-bottom:0.5rem; font-size:0.875rem;line-height:1.25rem; color:rgb(75,85,99);">Built with Svelte 5 & Tailwind CSS</Text>
|
|
102
|
+
<Text style="font-size:0.75rem;line-height:1rem; color:rgb(107,114,128);">
|
|
103
103
|
You're receiving this email because you tested the better-svelte-email demo.
|
|
104
104
|
</Text>
|
|
105
105
|
</Section>
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
<Html>
|
|
6
6
|
<Head />
|
|
7
7
|
<Body>
|
|
8
|
-
<Container
|
|
9
|
-
<Text
|
|
10
|
-
<Button
|
|
8
|
+
<Container style="background-color:rgb(243,244,246);padding: 2rem;" >
|
|
9
|
+
<Text style="font-size:1.125rem;line-height:1.75rem; font-weight:700; color:rgb(37,99,235);">Hello World</Text>
|
|
10
|
+
<Button
|
|
11
|
+
style="border-radius:0.25rem; background-color:rgb(59,130,246); padding-left:1rem;padding-right:1rem; padding-top:0.5rem;padding-bottom:0.5rem; color:rgb(255,255,255);width: 33.33%"
|
|
12
|
+
|
|
13
|
+
href="https://example.com"
|
|
14
|
+
>
|
|
11
15
|
Click Me
|
|
12
16
|
</Button>
|
|
13
17
|
</Container>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { Body, Button, Container, Head, Hr, Html, Section, Text } from './components/index.js';
|
|
2
2
|
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
3
3
|
export type { PreprocessorOptions, ComponentTransform } from './preprocessor/index.js';
|
|
4
4
|
export type { ClassAttribute, TransformResult, MediaQueryStyle } from './preprocessor/types.js';
|
|
5
|
-
export {
|
|
5
|
+
export type { TailwindConfig } from 'tw-to-css';
|
|
6
|
+
export { parseAttributes as parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
|
|
6
7
|
export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
|
|
7
8
|
export { injectMediaQueries } from './preprocessor/head-injector.js';
|
|
8
9
|
export { styleToString, pxToPt } from './utils/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Export email components
|
|
2
|
-
export {
|
|
2
|
+
export { Body, Button, Container, Head, Hr, Html, Section, Text } from './components/index.js';
|
|
3
3
|
// Export the preprocessor
|
|
4
4
|
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
5
5
|
// Export individual functions for advanced usage
|
|
6
|
-
export { parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
|
|
6
|
+
export { parseAttributes as parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
|
|
7
7
|
export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
|
|
8
8
|
export { injectMediaQueries } from './preprocessor/head-injector.js';
|
|
9
9
|
// Export utilities
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import MagicString from 'magic-string';
|
|
2
|
-
import {
|
|
2
|
+
import { parseAttributes } from './parser.js';
|
|
3
3
|
import { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './transformer.js';
|
|
4
4
|
import { injectMediaQueries } from './head-injector.js';
|
|
5
5
|
/**
|
|
@@ -76,8 +76,8 @@ function processEmailComponent(source, _filename, tailwindConverter, tailwindCon
|
|
|
76
76
|
let transformedCode = source;
|
|
77
77
|
const allMediaQueries = [];
|
|
78
78
|
// Step 1: Parse and find all class attributes
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
79
|
+
const attributes = parseAttributes(source);
|
|
80
|
+
if (attributes.length === 0) {
|
|
81
81
|
// No classes to transform
|
|
82
82
|
return {
|
|
83
83
|
originalCode: source,
|
|
@@ -90,19 +90,19 @@ function processEmailComponent(source, _filename, tailwindConverter, tailwindCon
|
|
|
90
90
|
// Step 2: Transform each class attribute
|
|
91
91
|
const s = new MagicString(transformedCode);
|
|
92
92
|
// Process in reverse order to maintain correct positions
|
|
93
|
-
const sortedAttributes = [...
|
|
94
|
-
for (const
|
|
95
|
-
if (!
|
|
93
|
+
const sortedAttributes = [...attributes].sort((a, b) => b.class.start - a.class.start);
|
|
94
|
+
for (const attr of sortedAttributes) {
|
|
95
|
+
if (!attr.class.isStatic) {
|
|
96
96
|
// Skip dynamic classes for now
|
|
97
|
-
warnings.push(`Dynamic class expression detected in ${
|
|
97
|
+
warnings.push(`Dynamic class expression detected in ${attr.class.elementName}. ` +
|
|
98
98
|
`Only static classes can be transformed at build time.`);
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
101
101
|
// Transform the classes
|
|
102
|
-
const transformed = transformTailwindClasses(
|
|
102
|
+
const transformed = transformTailwindClasses(attr.class.raw, tailwindConverter);
|
|
103
103
|
// Collect warnings about invalid classes
|
|
104
104
|
if (transformed.invalidClasses.length > 0) {
|
|
105
|
-
warnings.push(`Invalid Tailwind classes in ${
|
|
105
|
+
warnings.push(`Invalid Tailwind classes in ${attr.class.elementName}: ${transformed.invalidClasses.join(', ')}`);
|
|
106
106
|
}
|
|
107
107
|
// Generate media queries for responsive classes
|
|
108
108
|
if (transformed.responsiveClasses.length > 0) {
|
|
@@ -110,9 +110,13 @@ function processEmailComponent(source, _filename, tailwindConverter, tailwindCon
|
|
|
110
110
|
allMediaQueries.push(...mediaQueries);
|
|
111
111
|
}
|
|
112
112
|
// Build the new attribute value
|
|
113
|
-
const newAttributes = buildNewAttributes(transformed.inlineStyles, transformed.responsiveClasses);
|
|
113
|
+
const newAttributes = buildNewAttributes(transformed.inlineStyles, transformed.responsiveClasses, attr.style?.raw);
|
|
114
|
+
// Remove the already existing style attribute if it exists
|
|
115
|
+
if (attr.style) {
|
|
116
|
+
removeStyleAttribute(s, attr.style);
|
|
117
|
+
}
|
|
114
118
|
// Replace the class attribute with new attributes
|
|
115
|
-
replaceClassAttribute(s,
|
|
119
|
+
replaceClassAttribute(s, attr.class, newAttributes);
|
|
116
120
|
}
|
|
117
121
|
transformedCode = s.toString();
|
|
118
122
|
// Step 3: Inject media queries into <Head>
|
|
@@ -136,7 +140,7 @@ function processEmailComponent(source, _filename, tailwindConverter, tailwindCon
|
|
|
136
140
|
/**
|
|
137
141
|
* Build new attribute string from transformation result
|
|
138
142
|
*/
|
|
139
|
-
function buildNewAttributes(inlineStyles, responsiveClasses) {
|
|
143
|
+
function buildNewAttributes(inlineStyles, responsiveClasses, existingStyles) {
|
|
140
144
|
const parts = [];
|
|
141
145
|
// Add responsive classes if any
|
|
142
146
|
if (responsiveClasses.length > 0) {
|
|
@@ -147,7 +151,8 @@ function buildNewAttributes(inlineStyles, responsiveClasses) {
|
|
|
147
151
|
if (inlineStyles) {
|
|
148
152
|
// Escape quotes in styles
|
|
149
153
|
const escapedStyles = inlineStyles.replace(/"/g, '"');
|
|
150
|
-
|
|
154
|
+
const withExisting = escapedStyles + (existingStyles ? existingStyles : '');
|
|
155
|
+
parts.push(`style="${withExisting}"`);
|
|
151
156
|
}
|
|
152
157
|
return parts.join(' ');
|
|
153
158
|
}
|
|
@@ -194,3 +199,9 @@ function replaceClassAttribute(s, classAttr, newAttributes) {
|
|
|
194
199
|
s.remove(removeStart, removeEnd);
|
|
195
200
|
}
|
|
196
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove style attribute with MagicString
|
|
204
|
+
*/
|
|
205
|
+
function removeStyleAttribute(s, styleAttr) {
|
|
206
|
+
s.remove(styleAttr.start, styleAttr.end);
|
|
207
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { ClassAttribute } from './types.js';
|
|
1
|
+
import type { ClassAttribute, StyleAttribute } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Parse Svelte 5 source code and extract all class attributes
|
|
4
4
|
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#parse
|
|
5
5
|
*/
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function parseAttributes(source: string): {
|
|
7
|
+
class: ClassAttribute;
|
|
8
|
+
style?: StyleAttribute;
|
|
9
|
+
}[];
|
|
7
10
|
/**
|
|
8
11
|
* Find the <Head> component in Svelte 5 AST
|
|
9
12
|
* Returns the position where we should inject styles
|
|
@@ -3,8 +3,8 @@ import { parse } from 'svelte/compiler';
|
|
|
3
3
|
* Parse Svelte 5 source code and extract all class attributes
|
|
4
4
|
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#parse
|
|
5
5
|
*/
|
|
6
|
-
export function
|
|
7
|
-
const
|
|
6
|
+
export function parseAttributes(source) {
|
|
7
|
+
const attributes = [];
|
|
8
8
|
try {
|
|
9
9
|
// Parse the Svelte file into an AST
|
|
10
10
|
// Svelte 5 parse returns a Root node with modern AST structure
|
|
@@ -12,7 +12,7 @@ export function parseClassAttributes(source) {
|
|
|
12
12
|
// Walk the html fragment (template portion) of the AST
|
|
13
13
|
if (ast.html && ast.html.children) {
|
|
14
14
|
for (const child of ast.html.children) {
|
|
15
|
-
walkNode(child,
|
|
15
|
+
walkNode(child, attributes, source);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -20,12 +20,12 @@ export function parseClassAttributes(source) {
|
|
|
20
20
|
console.error('Failed to parse Svelte file:', error);
|
|
21
21
|
throw error;
|
|
22
22
|
}
|
|
23
|
-
return
|
|
23
|
+
return attributes;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Recursively walk Svelte 5 AST nodes to find class attributes
|
|
27
27
|
*/
|
|
28
|
-
function walkNode(node,
|
|
28
|
+
function walkNode(node, attributes, source) {
|
|
29
29
|
if (!node)
|
|
30
30
|
return;
|
|
31
31
|
// Svelte 5 AST structure:
|
|
@@ -37,18 +37,33 @@ function walkNode(node, classAttributes, source) {
|
|
|
37
37
|
node.type === 'SlotElement' ||
|
|
38
38
|
node.type === 'Component') {
|
|
39
39
|
const elementName = node.name || 'unknown';
|
|
40
|
-
// Look for class attribute in Svelte 5 AST
|
|
40
|
+
// Look for class and style attribute in Svelte 5 AST
|
|
41
41
|
const classAttr = node.attributes?.find((attr) => attr.type === 'Attribute' && attr.name === 'class');
|
|
42
|
+
const styleAttr = node.attributes?.find((attr) => attr.type === 'Attribute' && attr.name === 'style');
|
|
42
43
|
if (classAttr && classAttr.value) {
|
|
43
44
|
// Extract class value
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
const extractedClass = extractClassValue(classAttr, source);
|
|
46
|
+
let extractedStyle = null;
|
|
47
|
+
if (styleAttr && styleAttr.value) {
|
|
48
|
+
extractedStyle = extractStyleValue(styleAttr, source);
|
|
49
|
+
}
|
|
50
|
+
if (extractedClass) {
|
|
51
|
+
attributes.push({
|
|
52
|
+
class: {
|
|
53
|
+
raw: extractedClass.value,
|
|
54
|
+
start: extractedClass.start,
|
|
55
|
+
end: extractedClass.end,
|
|
56
|
+
elementName,
|
|
57
|
+
isStatic: extractedClass.isStatic
|
|
58
|
+
},
|
|
59
|
+
style: extractedStyle
|
|
60
|
+
? {
|
|
61
|
+
raw: extractedStyle.value,
|
|
62
|
+
start: extractedStyle.start,
|
|
63
|
+
end: extractedStyle.end,
|
|
64
|
+
elementName
|
|
65
|
+
}
|
|
66
|
+
: undefined
|
|
52
67
|
});
|
|
53
68
|
}
|
|
54
69
|
}
|
|
@@ -56,21 +71,21 @@ function walkNode(node, classAttributes, source) {
|
|
|
56
71
|
// Recursively process children
|
|
57
72
|
if (node.children) {
|
|
58
73
|
for (const child of node.children) {
|
|
59
|
-
walkNode(child,
|
|
74
|
+
walkNode(child, attributes, source);
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
// Handle conditional blocks (#if, #each, etc.)
|
|
63
78
|
if (node.consequent) {
|
|
64
79
|
if (node.consequent.children) {
|
|
65
80
|
for (const child of node.consequent.children) {
|
|
66
|
-
walkNode(child,
|
|
81
|
+
walkNode(child, attributes, source);
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
if (node.alternate) {
|
|
71
86
|
if (node.alternate.children) {
|
|
72
87
|
for (const child of node.alternate.children) {
|
|
73
|
-
walkNode(child,
|
|
88
|
+
walkNode(child, attributes, source);
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
}
|
|
@@ -78,7 +93,7 @@ function walkNode(node, classAttributes, source) {
|
|
|
78
93
|
if (node.body) {
|
|
79
94
|
if (node.body.children) {
|
|
80
95
|
for (const child of node.body.children) {
|
|
81
|
-
walkNode(child,
|
|
96
|
+
walkNode(child, attributes, source);
|
|
82
97
|
}
|
|
83
98
|
}
|
|
84
99
|
}
|
|
@@ -130,8 +145,8 @@ function extractClassValue(classAttr, source) {
|
|
|
130
145
|
// Mixed content (both Text and ExpressionTag)
|
|
131
146
|
// Extract only the static Text portions for partial transformation
|
|
132
147
|
let combinedValue = '';
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
const start = classAttr.value[0].start;
|
|
149
|
+
const end = classAttr.value[classAttr.value.length - 1].end;
|
|
135
150
|
let hasStaticContent = false;
|
|
136
151
|
for (const part of classAttr.value) {
|
|
137
152
|
if (part.type === 'Text' && part.data) {
|
|
@@ -150,6 +165,57 @@ function extractClassValue(classAttr, source) {
|
|
|
150
165
|
}
|
|
151
166
|
return null;
|
|
152
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Extract the actual style value from a Svelte 5 attribute node
|
|
170
|
+
*/
|
|
171
|
+
function extractStyleValue(styleAttr, source) {
|
|
172
|
+
// Svelte 5 attribute value formats:
|
|
173
|
+
// 1. Static string: style="color: red;"
|
|
174
|
+
// → value: [{ type: 'Text', data: 'color: red;' }]
|
|
175
|
+
//
|
|
176
|
+
// 2. Expression: style={someVar}
|
|
177
|
+
// → value: [{ type: 'ExpressionTag', expression: {...} }]
|
|
178
|
+
//
|
|
179
|
+
// 3. Mixed: style="color: red; {dynamicStyle}"
|
|
180
|
+
// → value: [{ type: 'Text' }, { type: 'ExpressionTag' }]
|
|
181
|
+
if (!styleAttr.value || styleAttr.value.length === 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
// Check if entirely static (only Text nodes)
|
|
185
|
+
const hasOnlyText = styleAttr.value.every((v) => v.type === 'Text');
|
|
186
|
+
if (hasOnlyText) {
|
|
187
|
+
// Fully static - we can extract this
|
|
188
|
+
const textContent = styleAttr.value.map((v) => v.data || '').join('');
|
|
189
|
+
return {
|
|
190
|
+
value: textContent,
|
|
191
|
+
start: styleAttr.start,
|
|
192
|
+
end: styleAttr.end
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Check if entirely dynamic (only ExpressionTag or MustacheTag)
|
|
196
|
+
const hasOnlyExpression = styleAttr.value.length === 1 &&
|
|
197
|
+
(styleAttr.value[0].type === 'ExpressionTag' || styleAttr.value[0].type === 'MustacheTag');
|
|
198
|
+
if (hasOnlyExpression) {
|
|
199
|
+
// Fully dynamic - extract the expression code
|
|
200
|
+
const exprNode = styleAttr.value[0];
|
|
201
|
+
const expressionCode = source.substring(exprNode.start, exprNode.end);
|
|
202
|
+
return {
|
|
203
|
+
value: expressionCode,
|
|
204
|
+
start: exprNode.start,
|
|
205
|
+
end: exprNode.end
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Mixed content (both Text and ExpressionTag)
|
|
209
|
+
// Extract the full content including dynamic parts
|
|
210
|
+
const start = styleAttr.value[0].start;
|
|
211
|
+
const end = styleAttr.value[styleAttr.value.length - 1].end;
|
|
212
|
+
const fullContent = source.substring(start, end);
|
|
213
|
+
return {
|
|
214
|
+
value: fullContent,
|
|
215
|
+
start: styleAttr.start,
|
|
216
|
+
end: styleAttr.end
|
|
217
|
+
};
|
|
218
|
+
}
|
|
153
219
|
/**
|
|
154
220
|
* Find the <Head> component in Svelte 5 AST
|
|
155
221
|
* Returns the position where we should inject styles
|
|
@@ -167,7 +233,7 @@ export function findHeadComponent(source) {
|
|
|
167
233
|
}
|
|
168
234
|
return { found: false, insertPosition: null };
|
|
169
235
|
}
|
|
170
|
-
catch
|
|
236
|
+
catch {
|
|
171
237
|
return { found: false, insertPosition: null };
|
|
172
238
|
}
|
|
173
239
|
}
|
|
@@ -43,6 +43,27 @@ export interface ClassAttribute {
|
|
|
43
43
|
*/
|
|
44
44
|
isStatic: boolean;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Represents a style attribute found in the AST
|
|
48
|
+
*/
|
|
49
|
+
export interface StyleAttribute {
|
|
50
|
+
/**
|
|
51
|
+
* Raw style string (e.g., "background-color: red;")
|
|
52
|
+
*/
|
|
53
|
+
raw: string;
|
|
54
|
+
/**
|
|
55
|
+
* Start position in source code
|
|
56
|
+
*/
|
|
57
|
+
start: number;
|
|
58
|
+
/**
|
|
59
|
+
* End position in source code
|
|
60
|
+
*/
|
|
61
|
+
end: number;
|
|
62
|
+
/**
|
|
63
|
+
* Parent element/component name
|
|
64
|
+
*/
|
|
65
|
+
elementName: string;
|
|
66
|
+
}
|
|
46
67
|
/**
|
|
47
68
|
* Result of transforming Tailwind classes
|
|
48
69
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-svelte-email",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"author": "Anatole",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,13 +46,19 @@
|
|
|
46
46
|
".": {
|
|
47
47
|
"types": "./dist/index.d.ts",
|
|
48
48
|
"svelte": "./dist/index.js",
|
|
49
|
-
"
|
|
49
|
+
"import": "./dist/preprocessor/index.js",
|
|
50
|
+
"default": "./dist/preprocessor/index.js"
|
|
50
51
|
},
|
|
51
52
|
"./preprocessor": {
|
|
52
53
|
"types": "./dist/preprocessor/index.d.ts",
|
|
53
54
|
"import": "./dist/preprocessor/index.js",
|
|
54
55
|
"default": "./dist/preprocessor/index.js"
|
|
55
|
-
}
|
|
56
|
+
},
|
|
57
|
+
"./components/*": {
|
|
58
|
+
"types": "./dist/components/*.svelte.d.ts",
|
|
59
|
+
"svelte": "./dist/components/*.svelte"
|
|
60
|
+
},
|
|
61
|
+
"./package.json": "./package.json"
|
|
56
62
|
},
|
|
57
63
|
"description": "A Svelte 5 preprocessor that transforms Tailwind CSS classes in email components to inline styles with responsive media query support",
|
|
58
64
|
"files": [
|