better-svelte-email 0.3.6 → 1.0.0-beta.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.
Files changed (56) hide show
  1. package/dist/components/Body.svelte +7 -3
  2. package/dist/components/Button.svelte +10 -12
  3. package/dist/components/Button.svelte.d.ts +2 -2
  4. package/dist/index.d.ts +2 -6
  5. package/dist/index.js +3 -5
  6. package/dist/preprocessor/index.d.ts +19 -0
  7. package/dist/preprocessor/index.js +19 -0
  8. package/dist/preview/index.d.ts +32 -5
  9. package/dist/preview/index.js +63 -37
  10. package/dist/render/index.d.ts +66 -0
  11. package/dist/render/index.js +138 -0
  12. package/dist/render/utils/compatibility/sanitize-class-name.d.ts +7 -0
  13. package/dist/render/utils/compatibility/sanitize-class-name.js +35 -0
  14. package/dist/render/utils/css/extract-rules-per-class.d.ts +5 -0
  15. package/dist/render/utils/css/extract-rules-per-class.js +37 -0
  16. package/dist/render/utils/css/get-custom-properties.d.ts +8 -0
  17. package/dist/render/utils/css/get-custom-properties.js +37 -0
  18. package/dist/render/utils/css/is-rule-inlinable.d.ts +2 -0
  19. package/dist/render/utils/css/is-rule-inlinable.js +6 -0
  20. package/dist/render/utils/css/make-inline-styles-for.d.ts +3 -0
  21. package/dist/render/utils/css/make-inline-styles-for.js +57 -0
  22. package/dist/render/utils/css/resolve-all-css-variables.d.ts +8 -0
  23. package/dist/render/utils/css/resolve-all-css-variables.js +123 -0
  24. package/dist/render/utils/css/resolve-calc-expressions.d.ts +5 -0
  25. package/dist/render/utils/css/resolve-calc-expressions.js +126 -0
  26. package/dist/render/utils/css/sanitize-declarations.d.ts +15 -0
  27. package/dist/render/utils/css/sanitize-declarations.js +354 -0
  28. package/dist/render/utils/css/sanitize-non-inlinable-rules.d.ts +11 -0
  29. package/dist/render/utils/css/sanitize-non-inlinable-rules.js +33 -0
  30. package/dist/render/utils/css/sanitize-stylesheet.d.ts +2 -0
  31. package/dist/render/utils/css/sanitize-stylesheet.js +8 -0
  32. package/dist/render/utils/css/unwrap-value.d.ts +2 -0
  33. package/dist/render/utils/css/unwrap-value.js +6 -0
  34. package/dist/render/utils/html/is-valid-node.d.ts +2 -0
  35. package/dist/render/utils/html/is-valid-node.js +3 -0
  36. package/dist/render/utils/html/remove-attributes-functions.d.ts +2 -0
  37. package/dist/render/utils/html/remove-attributes-functions.js +10 -0
  38. package/dist/render/utils/html/walk.d.ts +15 -0
  39. package/dist/render/utils/html/walk.js +36 -0
  40. package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.d.ts +4 -0
  41. package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.js +61 -0
  42. package/dist/render/utils/tailwindcss/pixel-based-preset.d.ts +2 -0
  43. package/dist/render/utils/tailwindcss/pixel-based-preset.js +58 -0
  44. package/dist/render/utils/tailwindcss/setup-tailwind.d.ts +7 -0
  45. package/dist/render/utils/tailwindcss/setup-tailwind.js +67 -0
  46. package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.d.ts +2 -0
  47. package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.js +899 -0
  48. package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.d.ts +2 -0
  49. package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.js +396 -0
  50. package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.d.ts +2 -0
  51. package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.js +465 -0
  52. package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.d.ts +2 -0
  53. package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.js +4 -0
  54. package/dist/utils/index.d.ts +2 -1
  55. package/dist/utils/index.js +13 -10
  56. package/package.json +17 -2
@@ -1,15 +1,19 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
 
4
- let { children, style, ...restProps }: { children?: any } & HTMLAttributes<HTMLBodyElement> =
5
- $props();
4
+ let {
5
+ children,
6
+ style,
7
+ class: className,
8
+ ...restProps
9
+ }: { children?: any } & HTMLAttributes<HTMLBodyElement> = $props();
6
10
  </script>
7
11
 
8
12
  <body {...restProps}>
9
13
  <table align="center" width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
10
14
  <tbody>
11
15
  <tr>
12
- <td {style}>
16
+ <td {style} class={className}>
13
17
  {@render children?.()}
14
18
  </td>
15
19
  </tr>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { styleToString, pxToPt, combineStyles } from '../utils/index.js';
3
- import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { HTMLAnchorAttributes } from 'svelte/elements';
4
4
 
5
5
  let {
6
6
  href = '#',
@@ -16,7 +16,7 @@
16
16
  pX?: number;
17
17
  pY?: number;
18
18
  children: any;
19
- } & HTMLAttributes<HTMLAnchorElement> = $props();
19
+ } & HTMLAnchorAttributes = $props();
20
20
 
21
21
  const y = pY * 2;
22
22
  const textRaise = pxToPt(y.toString());
@@ -43,17 +43,15 @@
43
43
  </script>
44
44
 
45
45
  <a {...restProps} {href} {target} style={combineStyles(buttonStyle, style)}>
46
- {#if pX}
47
- <span>
48
- {@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%;mso-text-raise:${textRaise}" hidden>&nbsp;</i><![endif]-->`}
49
- </span>
50
- {/if}
46
+ <span>
47
+ {@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%;mso-text-raise:${textRaise}" hidden>&nbsp;</i><![endif]-->`}
48
+ </span>
49
+
51
50
  <span style={buttonTextStyle}>
52
51
  {@render children?.()}
53
52
  </span>
54
- {#if pX}
55
- <span>
56
- {@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%" hidden>&nbsp;</i><![endif]-->`}
57
- </span>
58
- {/if}
53
+
54
+ <span style="display: none;">
55
+ {@html `<!--[if mso]><i style="letter-spacing: ${pX}px;mso-font-width:-100%" hidden>&nbsp;</i><![endif]-->`}
56
+ </span>
59
57
  </a>
@@ -1,11 +1,11 @@
1
- import type { HTMLAttributes } from 'svelte/elements';
1
+ import type { HTMLAnchorAttributes } from 'svelte/elements';
2
2
  type $$ComponentProps = {
3
3
  href?: string;
4
4
  target?: string;
5
5
  pX?: number;
6
6
  pY?: number;
7
7
  children: any;
8
- } & HTMLAttributes<HTMLAnchorElement>;
8
+ } & HTMLAnchorAttributes;
9
9
  declare const Button: import("svelte").Component<$$ComponentProps, {}, "">;
10
10
  type Button = ReturnType<typeof Button>;
11
11
  export default Button;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  export * from './components/index.js';
2
2
  export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
3
3
  export type { PreprocessorOptions, ComponentTransform } from './preprocessor/index.js';
4
- export type { ClassAttribute, TransformResult, MediaQueryStyle } from './preprocessor/types.js';
5
- export type { TailwindConfig } from 'tw-to-css';
6
- export { parseAttributes as parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
7
- export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
8
- export { injectMediaQueries } from './preprocessor/head-injector.js';
9
- export { styleToString, pxToPt, combineStyles, withMargin, renderAsPlainText } from './utils/index.js';
4
+ export { default as Renderer, type TailwindConfig, type RenderOptions } from './render/index.js';
5
+ export * from './utils/index.js';
package/dist/index.js CHANGED
@@ -2,9 +2,7 @@
2
2
  export * from './components/index.js';
3
3
  // Export the preprocessor
4
4
  export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
5
- // Export individual functions for advanced usage
6
- export { parseAttributes as parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
7
- export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
8
- export { injectMediaQueries } from './preprocessor/head-injector.js';
5
+ // Export renderer
6
+ export { default as Renderer } from './render/index.js';
9
7
  // Export utilities
10
- export { styleToString, pxToPt, combineStyles, withMargin, renderAsPlainText } from './utils/index.js';
8
+ export * from './utils/index.js';
@@ -3,8 +3,11 @@ import type { PreprocessorOptions, ComponentTransform } from './types.js';
3
3
  /**
4
4
  * Svelte 5 preprocessor for transforming Tailwind classes in email components
5
5
  *
6
+ * @deprecated The preprocessor approach is deprecated. Use the `Renderer` class instead for better performance and flexibility.
7
+ *
6
8
  * @example
7
9
  * ```javascript
10
+ * // Old (deprecated):
8
11
  * // svelte.config.js
9
12
  * import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
10
13
  *
@@ -17,6 +20,22 @@ import type { PreprocessorOptions, ComponentTransform } from './types.js';
17
20
  * })
18
21
  * ]
19
22
  * };
23
+ *
24
+ * // New (recommended):
25
+ * import Renderer from 'better-svelte-email/renderer';
26
+ * import EmailComponent from './email.svelte';
27
+ *
28
+ * const renderer = new Renderer({
29
+ * theme: {
30
+ * extend: {
31
+ * colors: { brand: '#FF3E00' }
32
+ * }
33
+ * }
34
+ * });
35
+ *
36
+ * const html = await renderer.render(EmailComponent, {
37
+ * props: { name: 'John' }
38
+ * });
20
39
  * ```
21
40
  *
22
41
  * Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
@@ -6,8 +6,11 @@ import path from 'path';
6
6
  /**
7
7
  * Svelte 5 preprocessor for transforming Tailwind classes in email components
8
8
  *
9
+ * @deprecated The preprocessor approach is deprecated. Use the `Renderer` class instead for better performance and flexibility.
10
+ *
9
11
  * @example
10
12
  * ```javascript
13
+ * // Old (deprecated):
11
14
  * // svelte.config.js
12
15
  * import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
13
16
  *
@@ -20,6 +23,22 @@ import path from 'path';
20
23
  * })
21
24
  * ]
22
25
  * };
26
+ *
27
+ * // New (recommended):
28
+ * import Renderer from 'better-svelte-email/renderer';
29
+ * import EmailComponent from './email.svelte';
30
+ *
31
+ * const renderer = new Renderer({
32
+ * theme: {
33
+ * extend: {
34
+ * colors: { brand: '#FF3E00' }
35
+ * }
36
+ * }
37
+ * });
38
+ *
39
+ * const html = await renderer.render(EmailComponent, {
40
+ * props: { name: 'John' }
41
+ * });
23
42
  * ```
24
43
  *
25
44
  * Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
@@ -1,4 +1,5 @@
1
1
  import type { RequestEvent } from '@sveltejs/kit';
2
+ import Renderer from '../render/index.js';
2
3
  /**
3
4
  * Import all Svelte email components file paths.
4
5
  * Create a list containing all Svelte email component file names.
@@ -39,15 +40,28 @@ export declare const getEmailComponent: (emailPath: string, file: string) => Pro
39
40
  * SvelteKit form action to render an email component.
40
41
  * Use this with the Preview component to render email templates on demand.
41
42
  *
43
+ * @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
44
+ *
42
45
  * @example
43
46
  * ```ts
44
47
  * // +page.server.ts
45
48
  * import { createEmail } from 'better-svelte-email/preview';
49
+ * import Renderer from 'better-svelte-email/render';
50
+ *
51
+ * const renderer = new Renderer({
52
+ * theme: {
53
+ * extend: {
54
+ * colors: {
55
+ * brand: '#FF3E00'
56
+ * }
57
+ * }
58
+ * }
59
+ * });
46
60
  *
47
- * export const actions = createEmail;
61
+ * export const actions = createEmail(renderer);
48
62
  * ```
49
63
  */
50
- export declare const createEmail: {
64
+ export declare const createEmail: (renderer?: Renderer) => {
51
65
  'create-email': (event: RequestEvent) => Promise<{
52
66
  status: number;
53
67
  body: {
@@ -80,20 +94,33 @@ export declare const SendEmailFunction: ({ from, to, subject, html }: {
80
94
  *
81
95
  * @param options.resendApiKey - Your Resend API key (keep this server-side only)
82
96
  * @param options.customSendEmailFunction - Optional custom function to send emails
97
+ * @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
83
98
  *
84
99
  * @example
85
100
  * ```ts
86
101
  * // In +page.server.ts
87
102
  * import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
103
+ * import Renderer from 'better-svelte-email/render';
104
+ *
105
+ * const renderer = new Renderer({
106
+ * theme: {
107
+ * extend: {
108
+ * colors: {
109
+ * brand: '#FF3E00'
110
+ * }
111
+ * }
112
+ * }
113
+ * });
88
114
  *
89
115
  * export const actions = {
90
- * ...createEmail,
91
- * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
116
+ * ...createEmail(renderer),
117
+ * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY, renderer })
92
118
  * };
93
119
  * ```
94
120
  */
95
- export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }?: {
121
+ export declare const sendEmail: ({ customSendEmailFunction, resendApiKey, renderer }?: {
96
122
  customSendEmailFunction?: typeof SendEmailFunction;
123
+ renderer?: Renderer;
97
124
  resendApiKey?: string;
98
125
  }) => {
99
126
  'send-email': (event: RequestEvent) => Promise<{
@@ -1,9 +1,9 @@
1
1
  import { Resend } from 'resend';
2
2
  import fs from 'fs';
3
- import { render } from 'svelte/server';
4
3
  import path from 'path';
5
4
  import prettier from 'prettier/standalone';
6
5
  import parserHtml from 'prettier/parser-html';
6
+ import Renderer from '../render/index.js';
7
7
  /**
8
8
  * Get a list of all email component files in the specified directory.
9
9
  *
@@ -67,49 +67,63 @@ export const getEmailComponent = async (emailPath, file) => {
67
67
  * SvelteKit form action to render an email component.
68
68
  * Use this with the Preview component to render email templates on demand.
69
69
  *
70
+ * @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
71
+ *
70
72
  * @example
71
73
  * ```ts
72
74
  * // +page.server.ts
73
75
  * import { createEmail } from 'better-svelte-email/preview';
76
+ * import Renderer from 'better-svelte-email/render';
77
+ *
78
+ * const renderer = new Renderer({
79
+ * theme: {
80
+ * extend: {
81
+ * colors: {
82
+ * brand: '#FF3E00'
83
+ * }
84
+ * }
85
+ * }
86
+ * });
74
87
  *
75
- * export const actions = createEmail;
88
+ * export const actions = createEmail(renderer);
76
89
  * ```
77
90
  */
78
- export const createEmail = {
79
- 'create-email': async (event) => {
80
- try {
81
- const data = await event.request.formData();
82
- const file = data.get('file');
83
- const emailPath = data.get('path');
84
- if (!file || !emailPath) {
91
+ export const createEmail = (renderer = new Renderer()) => {
92
+ return {
93
+ 'create-email': async (event) => {
94
+ try {
95
+ const data = await event.request.formData();
96
+ const file = data.get('file');
97
+ const emailPath = data.get('path');
98
+ if (!file || !emailPath) {
99
+ return {
100
+ status: 400,
101
+ body: { error: 'Missing file or path parameter' }
102
+ };
103
+ }
104
+ const emailComponent = await getEmailComponent(emailPath, file);
105
+ // Render the component to HTML
106
+ const html = await renderer.render(emailComponent);
107
+ // Remove all HTML comments from the body before formatting
108
+ const formattedHtml = await prettier.format(html, {
109
+ parser: 'html',
110
+ plugins: [parserHtml]
111
+ });
85
112
  return {
86
- status: 400,
87
- body: { error: 'Missing file or path parameter' }
113
+ body: formattedHtml
114
+ };
115
+ }
116
+ catch (error) {
117
+ console.error('Error rendering email:', error);
118
+ return {
119
+ status: 500,
120
+ error: {
121
+ message: error instanceof Error ? error.message : 'Failed to render email'
122
+ }
88
123
  };
89
124
  }
90
- const emailComponent = await getEmailComponent(emailPath, file);
91
- // Render the component to HTML
92
- const { body } = render(emailComponent);
93
- // Remove all HTML comments from the body before formatting
94
- const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
95
- const formattedBody = await prettier.format(bodyWithoutComments, {
96
- parser: 'html',
97
- plugins: [parserHtml]
98
- });
99
- return {
100
- body: formattedBody
101
- };
102
- }
103
- catch (error) {
104
- console.error('Error rendering email:', error);
105
- return {
106
- status: 500,
107
- error: {
108
- message: error instanceof Error ? error.message : 'Failed to render email'
109
- }
110
- };
111
125
  }
112
- }
126
+ };
113
127
  };
114
128
  const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKey) => {
115
129
  // stringify api key to comment out temp
@@ -128,19 +142,31 @@ const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKe
128
142
  *
129
143
  * @param options.resendApiKey - Your Resend API key (keep this server-side only)
130
144
  * @param options.customSendEmailFunction - Optional custom function to send emails
145
+ * @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
131
146
  *
132
147
  * @example
133
148
  * ```ts
134
149
  * // In +page.server.ts
135
150
  * import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
151
+ * import Renderer from 'better-svelte-email/render';
152
+ *
153
+ * const renderer = new Renderer({
154
+ * theme: {
155
+ * extend: {
156
+ * colors: {
157
+ * brand: '#FF3E00'
158
+ * }
159
+ * }
160
+ * }
161
+ * });
136
162
  *
137
163
  * export const actions = {
138
- * ...createEmail,
139
- * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
164
+ * ...createEmail(renderer),
165
+ * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY, renderer })
140
166
  * };
141
167
  * ```
142
168
  */
143
- export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
169
+ export const sendEmail = ({ customSendEmailFunction, resendApiKey, renderer = new Renderer() } = {}) => {
144
170
  return {
145
171
  'send-email': async (event) => {
146
172
  const data = await event.request.formData();
@@ -157,7 +183,7 @@ export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
157
183
  from: 'svelte-email-tailwind <onboarding@resend.dev>',
158
184
  to: `${data.get('to')}`,
159
185
  subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
160
- html: (await render(emailComponent)).body
186
+ html: await renderer.render(emailComponent)
161
187
  };
162
188
  let sent = { success: false, error: null };
163
189
  if (!customSendEmailFunction && resendApiKey) {
@@ -0,0 +1,66 @@
1
+ import { type DefaultTreeAdapterTypes } from 'parse5';
2
+ import type { Config } from 'tailwindcss';
3
+ export type TailwindConfig = Omit<Config, 'content'>;
4
+ export type { DefaultTreeAdapterTypes as AST };
5
+ /**
6
+ * Options for rendering a Svelte component
7
+ */
8
+ export type RenderOptions = {
9
+ props?: Omit<Record<string, any>, '$$slots' | '$$events'> | undefined;
10
+ context?: Map<any, any>;
11
+ idPrefix?: string;
12
+ };
13
+ /**
14
+ * Email renderer that converts Svelte components to email-safe HTML with inlined Tailwind styles.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import Renderer from 'better-svelte-email/renderer';
19
+ * import EmailComponent from './email.svelte';
20
+ *
21
+ * const renderer = new Renderer({
22
+ * theme: {
23
+ * extend: {
24
+ * colors: {
25
+ * brand: '#FF3E00'
26
+ * }
27
+ * }
28
+ * }
29
+ * });
30
+ *
31
+ * const html = await renderer.render(EmailComponent, {
32
+ * props: { name: 'John' }
33
+ * });
34
+ * ```
35
+ */
36
+ export default class Renderer {
37
+ private tailwindConfig;
38
+ constructor(tailwindConfig?: TailwindConfig);
39
+ /**
40
+ * Renders a Svelte component to email-safe HTML with inlined Tailwind CSS.
41
+ *
42
+ * Automatically:
43
+ * - Converts Tailwind classes to inline styles
44
+ * - Injects media queries into `<head>` for responsive classes
45
+ * - Replaces DOCTYPE with XHTML 1.0 Transitional
46
+ * - Removes comments and Svelte artifacts
47
+ *
48
+ * @param component - The Svelte component to render
49
+ * @param options - Render options including props, context, and idPrefix
50
+ * @returns Email-safe HTML string
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const html = await renderer.render(EmailComponent, {
55
+ * props: { username: 'john_doe', resetUrl: 'https://...' }
56
+ * });
57
+ * ```
58
+ */
59
+ render: (component: any, options?: RenderOptions | undefined) => Promise<string>;
60
+ }
61
+ /**
62
+ * Render HTML as plain text
63
+ * @param markup - HTML string
64
+ * @returns Plain text string
65
+ */
66
+ export declare const toPlainText: (markup: string) => string;
@@ -0,0 +1,138 @@
1
+ import { render as svelteRender } from 'svelte/server';
2
+ import { parse, serialize } from 'parse5';
3
+ import { walk } from './utils/html/walk.js';
4
+ import { setupTailwind } from './utils/tailwindcss/setup-tailwind.js';
5
+ import { sanitizeStyleSheet } from './utils/css/sanitize-stylesheet.js';
6
+ import { extractRulesPerClass } from './utils/css/extract-rules-per-class.js';
7
+ import { getCustomProperties } from './utils/css/get-custom-properties.js';
8
+ import { generate, List } from 'css-tree';
9
+ import { sanitizeNonInlinableRules } from './utils/css/sanitize-non-inlinable-rules.js';
10
+ import { addInlinedStylesToElement } from './utils/tailwindcss/add-inlined-styles-to-element.js';
11
+ import { isValidNode } from './utils/html/is-valid-node.js';
12
+ import { removeAttributesFunctions } from './utils/html/remove-attributes-functions.js';
13
+ import { convert } from 'html-to-text';
14
+ /**
15
+ * Email renderer that converts Svelte components to email-safe HTML with inlined Tailwind styles.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import Renderer from 'better-svelte-email/renderer';
20
+ * import EmailComponent from './email.svelte';
21
+ *
22
+ * const renderer = new Renderer({
23
+ * theme: {
24
+ * extend: {
25
+ * colors: {
26
+ * brand: '#FF3E00'
27
+ * }
28
+ * }
29
+ * }
30
+ * });
31
+ *
32
+ * const html = await renderer.render(EmailComponent, {
33
+ * props: { name: 'John' }
34
+ * });
35
+ * ```
36
+ */
37
+ export default class Renderer {
38
+ tailwindConfig;
39
+ constructor(tailwindConfig = {}) {
40
+ this.tailwindConfig = tailwindConfig;
41
+ }
42
+ /**
43
+ * Renders a Svelte component to email-safe HTML with inlined Tailwind CSS.
44
+ *
45
+ * Automatically:
46
+ * - Converts Tailwind classes to inline styles
47
+ * - Injects media queries into `<head>` for responsive classes
48
+ * - Replaces DOCTYPE with XHTML 1.0 Transitional
49
+ * - Removes comments and Svelte artifacts
50
+ *
51
+ * @param component - The Svelte component to render
52
+ * @param options - Render options including props, context, and idPrefix
53
+ * @returns Email-safe HTML string
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const html = await renderer.render(EmailComponent, {
58
+ * props: { username: 'john_doe', resetUrl: 'https://...' }
59
+ * });
60
+ * ```
61
+ */
62
+ render = async (component, options) => {
63
+ const { body } = svelteRender(component, options);
64
+ let ast = parse(body);
65
+ ast = removeAttributesFunctions(ast);
66
+ let classesUsed = [];
67
+ const tailwindSetup = await setupTailwind(this.tailwindConfig);
68
+ walk(ast, (node) => {
69
+ if (isValidNode(node)) {
70
+ const classAttr = node.attrs?.find((attr) => attr.name === 'class');
71
+ if (classAttr && classAttr.value) {
72
+ const classes = classAttr.value.split(/\s+/).filter(Boolean);
73
+ classesUsed = [...classesUsed, ...classes];
74
+ tailwindSetup.addUtilities(classes);
75
+ }
76
+ }
77
+ return node;
78
+ });
79
+ const styleSheet = tailwindSetup.getStyleSheet();
80
+ sanitizeStyleSheet(styleSheet);
81
+ const { inlinable: inlinableRules, nonInlinable: nonInlinableRules } = extractRulesPerClass(styleSheet, classesUsed);
82
+ const customProperties = getCustomProperties(styleSheet);
83
+ const nonInlineStyles = {
84
+ type: 'StyleSheet',
85
+ children: new List().fromArray(Array.from(nonInlinableRules.values()))
86
+ };
87
+ sanitizeNonInlinableRules(nonInlineStyles);
88
+ const hasNonInlineStylesToApply = nonInlinableRules.size > 0;
89
+ let appliedNonInlineStyles = false;
90
+ let hasHead = false;
91
+ const unknownClasses = [];
92
+ ast = walk(ast, (node) => {
93
+ if (isValidNode(node)) {
94
+ const elementWithInlinedStyles = addInlinedStylesToElement(node, inlinableRules, nonInlinableRules, customProperties, unknownClasses);
95
+ if (node.nodeName === 'head') {
96
+ hasHead = true;
97
+ }
98
+ return elementWithInlinedStyles;
99
+ }
100
+ return node;
101
+ });
102
+ let serialized = serialize(ast);
103
+ if (unknownClasses.length > 0) {
104
+ console.warn(`[better-svelte-email] You are using the following classes that were not recognized: ${unknownClasses.join(' ')}.`);
105
+ }
106
+ if (hasHead && hasNonInlineStylesToApply) {
107
+ appliedNonInlineStyles = true;
108
+ serialized = serialized.replace('<head>', '<head>' + '<style>' + generate(nonInlineStyles) + '</style>');
109
+ }
110
+ if (hasNonInlineStylesToApply && !appliedNonInlineStyles) {
111
+ throw new Error(`You are trying to use the following Tailwind classes that cannot be inlined: ${Array.from(nonInlinableRules.keys()).join(' ')}.
112
+ For the media queries to work properly on rendering, they need to be added into a <style> tag inside of a <head> tag,
113
+ the render function tried finding a <head> element but just wasn't able to find it.
114
+
115
+ Make sure that you have a <head> element at any depth.
116
+ This can also be our <Head> component.
117
+
118
+ If you do already have a <head> element at some depth,
119
+ please file a bug https://github.com/Konixy/better-svelte-email/issues/new?assignees=&labels=bug&projects=.`);
120
+ }
121
+ // Replace various DOCTYPE formats with XHTML 1.0 Transitional
122
+ serialized = serialized.replace(/<!DOCTYPE\s+html[^>]*>/i, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">');
123
+ return serialized;
124
+ };
125
+ }
126
+ /**
127
+ * Render HTML as plain text
128
+ * @param markup - HTML string
129
+ * @returns Plain text string
130
+ */
131
+ export const toPlainText = (markup) => {
132
+ return convert(markup, {
133
+ selectors: [
134
+ { selector: 'img', format: 'skip' },
135
+ { selector: '#__better-svelte-email-preview', format: 'skip' }
136
+ ]
137
+ });
138
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Replaces special characters to avoid problems on email clients.
3
+ *
4
+ * @param className - This should not come with any escaped charcters, it should come the same
5
+ * as is on the `className` attribute on React elements.
6
+ */
7
+ export declare function sanitizeClassName(className: string): string;
@@ -0,0 +1,35 @@
1
+ const digitToNameMap = {
2
+ '0': 'zero',
3
+ '1': 'one',
4
+ '2': 'two',
5
+ '3': 'three',
6
+ '4': 'four',
7
+ '5': 'five',
8
+ '6': 'six',
9
+ '7': 'seven',
10
+ '8': 'eight',
11
+ '9': 'nine'
12
+ };
13
+ /**
14
+ * Replaces special characters to avoid problems on email clients.
15
+ *
16
+ * @param className - This should not come with any escaped charcters, it should come the same
17
+ * as is on the `className` attribute on React elements.
18
+ */
19
+ export function sanitizeClassName(className) {
20
+ return className
21
+ .replaceAll('+', 'plus')
22
+ .replaceAll('[', '')
23
+ .replaceAll('%', 'pc')
24
+ .replaceAll(']', '')
25
+ .replaceAll('(', '')
26
+ .replaceAll(')', '')
27
+ .replaceAll('!', 'imprtnt')
28
+ .replaceAll('>', 'gt')
29
+ .replaceAll('<', 'lt')
30
+ .replaceAll('=', 'eq')
31
+ .replace(/^[0-9]/, (digit) => {
32
+ return digitToNameMap[digit];
33
+ })
34
+ .replace(/[^a-zA-Z0-9\-_]/g, '_');
35
+ }
@@ -0,0 +1,5 @@
1
+ import { type CssNode, type Rule } from 'css-tree';
2
+ export declare function extractRulesPerClass(root: CssNode, classes: string[]): {
3
+ inlinable: Map<string, Rule>;
4
+ nonInlinable: Map<string, Rule>;
5
+ };