better-svelte-email 0.3.6 → 1.0.0-beta.1
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 +1 -0
- package/dist/components/Body.svelte +7 -3
- package/dist/components/Button.svelte +10 -12
- package/dist/components/Button.svelte.d.ts +2 -2
- package/dist/index.d.ts +2 -6
- package/dist/index.js +3 -5
- package/dist/preprocessor/index.d.ts +19 -0
- package/dist/preprocessor/index.js +19 -0
- package/dist/preview/EmailPreview.svelte +539 -196
- package/dist/preview/EmailPreview.svelte.d.ts +11 -1
- package/dist/preview/EmailTreeNode.svelte +255 -0
- package/dist/preview/EmailTreeNode.svelte.d.ts +10 -0
- package/dist/preview/Favicon.svelte +106 -0
- package/dist/preview/Favicon.svelte.d.ts +5 -0
- package/dist/preview/email-tree.d.ts +13 -0
- package/dist/preview/email-tree.js +61 -0
- package/dist/preview/index.d.ts +35 -5
- package/dist/preview/index.js +87 -37
- package/dist/preview/theme.css +42 -0
- package/dist/render/index.d.ts +66 -0
- package/dist/render/index.js +138 -0
- package/dist/render/utils/compatibility/sanitize-class-name.d.ts +7 -0
- package/dist/render/utils/compatibility/sanitize-class-name.js +35 -0
- package/dist/render/utils/css/extract-rules-per-class.d.ts +5 -0
- package/dist/render/utils/css/extract-rules-per-class.js +37 -0
- package/dist/render/utils/css/get-custom-properties.d.ts +8 -0
- package/dist/render/utils/css/get-custom-properties.js +37 -0
- package/dist/render/utils/css/is-rule-inlinable.d.ts +2 -0
- package/dist/render/utils/css/is-rule-inlinable.js +6 -0
- package/dist/render/utils/css/make-inline-styles-for.d.ts +3 -0
- package/dist/render/utils/css/make-inline-styles-for.js +57 -0
- package/dist/render/utils/css/resolve-all-css-variables.d.ts +8 -0
- package/dist/render/utils/css/resolve-all-css-variables.js +123 -0
- package/dist/render/utils/css/resolve-calc-expressions.d.ts +5 -0
- package/dist/render/utils/css/resolve-calc-expressions.js +126 -0
- package/dist/render/utils/css/sanitize-declarations.d.ts +15 -0
- package/dist/render/utils/css/sanitize-declarations.js +354 -0
- package/dist/render/utils/css/sanitize-non-inlinable-rules.d.ts +11 -0
- package/dist/render/utils/css/sanitize-non-inlinable-rules.js +33 -0
- package/dist/render/utils/css/sanitize-stylesheet.d.ts +2 -0
- package/dist/render/utils/css/sanitize-stylesheet.js +8 -0
- package/dist/render/utils/css/unwrap-value.d.ts +2 -0
- package/dist/render/utils/css/unwrap-value.js +6 -0
- package/dist/render/utils/html/is-valid-node.d.ts +2 -0
- package/dist/render/utils/html/is-valid-node.js +3 -0
- package/dist/render/utils/html/remove-attributes-functions.d.ts +2 -0
- package/dist/render/utils/html/remove-attributes-functions.js +10 -0
- package/dist/render/utils/html/walk.d.ts +15 -0
- package/dist/render/utils/html/walk.js +36 -0
- package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.d.ts +4 -0
- package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.js +61 -0
- package/dist/render/utils/tailwindcss/pixel-based-preset.d.ts +2 -0
- package/dist/render/utils/tailwindcss/pixel-based-preset.js +58 -0
- package/dist/render/utils/tailwindcss/setup-tailwind.d.ts +7 -0
- package/dist/render/utils/tailwindcss/setup-tailwind.js +67 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.js +899 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.js +396 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.js +465 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.js +4 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +13 -10
- package/package.json +39 -20
package/dist/preview/index.js
CHANGED
|
@@ -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
|
*
|
|
@@ -63,53 +63,91 @@ export const getEmailComponent = async (emailPath, file) => {
|
|
|
63
63
|
throw new Error(`Failed to import email component '${fileName}'. Make sure the file exists and includes the <Head /> component.\nOriginal error: ${err}`);
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
+
const getEmailSource = async (emailPath, file) => {
|
|
67
|
+
const normalizedEmailPath = emailPath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
68
|
+
const normalizedFile = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
69
|
+
const candidates = new Set();
|
|
70
|
+
const relativeEmailPath = normalizedEmailPath.replace(/^\/+/, '');
|
|
71
|
+
if (normalizedEmailPath) {
|
|
72
|
+
candidates.add(path.resolve(process.cwd(), relativeEmailPath, `${normalizedFile}.svelte`));
|
|
73
|
+
candidates.add(path.resolve(process.cwd(), normalizedEmailPath, `${normalizedFile}.svelte`));
|
|
74
|
+
candidates.add(path.resolve(normalizedEmailPath, `${normalizedFile}.svelte`));
|
|
75
|
+
}
|
|
76
|
+
candidates.add(path.resolve(process.cwd(), `${normalizedFile}.svelte`));
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
try {
|
|
79
|
+
return await fs.promises.readFile(candidate, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// continue to next candidate
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.warn(`Source file not found for ${normalizedFile} in ${normalizedEmailPath}`);
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
66
88
|
/**
|
|
67
89
|
* SvelteKit form action to render an email component.
|
|
68
90
|
* Use this with the Preview component to render email templates on demand.
|
|
69
91
|
*
|
|
92
|
+
* @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
|
|
93
|
+
*
|
|
70
94
|
* @example
|
|
71
95
|
* ```ts
|
|
72
96
|
* // +page.server.ts
|
|
73
97
|
* import { createEmail } from 'better-svelte-email/preview';
|
|
98
|
+
* import Renderer from 'better-svelte-email/render';
|
|
74
99
|
*
|
|
75
|
-
*
|
|
100
|
+
* const renderer = new Renderer({
|
|
101
|
+
* theme: {
|
|
102
|
+
* extend: {
|
|
103
|
+
* colors: {
|
|
104
|
+
* brand: '#FF3E00'
|
|
105
|
+
* }
|
|
106
|
+
* }
|
|
107
|
+
* }
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* export const actions = createEmail(renderer);
|
|
76
111
|
* ```
|
|
77
112
|
*/
|
|
78
|
-
export const createEmail = {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
export const createEmail = (renderer = new Renderer()) => {
|
|
114
|
+
return {
|
|
115
|
+
'create-email': async (event) => {
|
|
116
|
+
try {
|
|
117
|
+
const data = await event.request.formData();
|
|
118
|
+
const file = data.get('file');
|
|
119
|
+
const emailPath = data.get('path');
|
|
120
|
+
if (!file || !emailPath) {
|
|
121
|
+
return {
|
|
122
|
+
status: 400,
|
|
123
|
+
body: { error: 'Missing file or path parameter' }
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const emailComponent = await getEmailComponent(emailPath, file);
|
|
127
|
+
const source = await getEmailSource(emailPath, file);
|
|
128
|
+
// Render the component to HTML
|
|
129
|
+
const html = await renderer.render(emailComponent);
|
|
130
|
+
// Remove all HTML comments from the body before formatting
|
|
131
|
+
const formattedHtml = await prettier.format(html, {
|
|
132
|
+
parser: 'html',
|
|
133
|
+
plugins: [parserHtml]
|
|
134
|
+
});
|
|
85
135
|
return {
|
|
86
|
-
|
|
87
|
-
|
|
136
|
+
body: formattedHtml,
|
|
137
|
+
source
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error('Error rendering email:', error);
|
|
142
|
+
return {
|
|
143
|
+
status: 500,
|
|
144
|
+
error: {
|
|
145
|
+
message: error instanceof Error ? error.message : 'Failed to render email'
|
|
146
|
+
}
|
|
88
147
|
};
|
|
89
148
|
}
|
|
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
149
|
}
|
|
112
|
-
}
|
|
150
|
+
};
|
|
113
151
|
};
|
|
114
152
|
const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKey) => {
|
|
115
153
|
// stringify api key to comment out temp
|
|
@@ -128,19 +166,31 @@ const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKe
|
|
|
128
166
|
*
|
|
129
167
|
* @param options.resendApiKey - Your Resend API key (keep this server-side only)
|
|
130
168
|
* @param options.customSendEmailFunction - Optional custom function to send emails
|
|
169
|
+
* @param options.renderer - Optional renderer to use for rendering the email component (use this if you want to use a custom tailwind config)
|
|
131
170
|
*
|
|
132
171
|
* @example
|
|
133
172
|
* ```ts
|
|
134
173
|
* // In +page.server.ts
|
|
135
174
|
* import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
|
|
175
|
+
* import Renderer from 'better-svelte-email/render';
|
|
176
|
+
*
|
|
177
|
+
* const renderer = new Renderer({
|
|
178
|
+
* theme: {
|
|
179
|
+
* extend: {
|
|
180
|
+
* colors: {
|
|
181
|
+
* brand: '#FF3E00'
|
|
182
|
+
* }
|
|
183
|
+
* }
|
|
184
|
+
* }
|
|
185
|
+
* });
|
|
136
186
|
*
|
|
137
187
|
* export const actions = {
|
|
138
|
-
* ...createEmail,
|
|
139
|
-
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
|
|
188
|
+
* ...createEmail(renderer),
|
|
189
|
+
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY, renderer })
|
|
140
190
|
* };
|
|
141
191
|
* ```
|
|
142
192
|
*/
|
|
143
|
-
export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
|
|
193
|
+
export const sendEmail = ({ customSendEmailFunction, resendApiKey, renderer = new Renderer() } = {}) => {
|
|
144
194
|
return {
|
|
145
195
|
'send-email': async (event) => {
|
|
146
196
|
const data = await event.request.formData();
|
|
@@ -157,7 +207,7 @@ export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
|
|
|
157
207
|
from: 'svelte-email-tailwind <onboarding@resend.dev>',
|
|
158
208
|
to: `${data.get('to')}`,
|
|
159
209
|
subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
|
|
160
|
-
html:
|
|
210
|
+
html: await renderer.render(emailComponent)
|
|
161
211
|
};
|
|
162
212
|
let sent = { success: false, error: null };
|
|
163
213
|
if (!customSendEmailFunction && resendApiKey) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--background: oklch(98.5% 0.001 106.423);
|
|
3
|
+
--foreground: oklch(21.6% 0.006 56.043);
|
|
4
|
+
--card: oklch(1 0 0);
|
|
5
|
+
--card-foreground: oklch(0.147 0.004 49.25);
|
|
6
|
+
--popover: oklch(1 0 0);
|
|
7
|
+
--popover-foreground: oklch(0.147 0.004 49.25);
|
|
8
|
+
--primary: oklch(0.216 0.006 56.043);
|
|
9
|
+
--primary-foreground: oklch(0.985 0.001 106.423);
|
|
10
|
+
--secondary: oklch(0.958 0.003 48.717);
|
|
11
|
+
--secondary-foreground: oklch(0.454 0.01 67.558);
|
|
12
|
+
--muted: oklch(0.9483 0.0061 67.75);
|
|
13
|
+
--muted-foreground: oklch(0.4761 0.021783 55.8952);
|
|
14
|
+
--accent: oklch(0.97 0.001 106.424);
|
|
15
|
+
--accent-foreground: oklch(0.216 0.006 56.043);
|
|
16
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
17
|
+
--border: oklch(0.923 0.003 48.717);
|
|
18
|
+
--input: oklch(0.923 0.003 48.717);
|
|
19
|
+
--ring: oklch(0.709 0.01 56.259);
|
|
20
|
+
--svelte: #f73b01;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dark {
|
|
24
|
+
--background: oklch(14.7% 0.004 49.25);
|
|
25
|
+
--foreground: oklch(97% 0.001 106.424);
|
|
26
|
+
--card: oklch(0.216 0.006 56.043);
|
|
27
|
+
--card-foreground: oklch(0.985 0.001 106.423);
|
|
28
|
+
--popover: oklch(0.216 0.006 56.043);
|
|
29
|
+
--popover-foreground: oklch(0.985 0.001 106.423);
|
|
30
|
+
--primary: oklch(0.923 0.003 48.717);
|
|
31
|
+
--primary-foreground: oklch(0.216 0.006 56.043);
|
|
32
|
+
--secondary: oklch(0.216 0.006 56.043);
|
|
33
|
+
--secondary-foreground: oklch(0.709 0.01 56.259);
|
|
34
|
+
--muted: oklch(0.268 0.007 34.298);
|
|
35
|
+
--muted-foreground: oklch(0.7348 0.0326 67.28);
|
|
36
|
+
--accent: oklch(0.268 0.007 34.298);
|
|
37
|
+
--accent-foreground: oklch(0.985 0.001 106.423);
|
|
38
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
39
|
+
--border: oklch(1 0 0 / 10%);
|
|
40
|
+
--input: oklch(1 0 0 / 15%);
|
|
41
|
+
--ring: oklch(0.553 0.013 58.071);
|
|
42
|
+
}
|
|
@@ -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,37 @@
|
|
|
1
|
+
import { string, walk } from 'css-tree';
|
|
2
|
+
import { isRuleInlinable } from './is-rule-inlinable.js';
|
|
3
|
+
export function extractRulesPerClass(root, classes) {
|
|
4
|
+
const classSet = new Set(classes);
|
|
5
|
+
const inlinableRules = new Map();
|
|
6
|
+
const nonInlinableRules = new Map();
|
|
7
|
+
walk(root, {
|
|
8
|
+
visit: 'Rule',
|
|
9
|
+
enter(rule) {
|
|
10
|
+
const selectorClasses = [];
|
|
11
|
+
walk(rule, {
|
|
12
|
+
visit: 'ClassSelector',
|
|
13
|
+
enter(classSelector) {
|
|
14
|
+
selectorClasses.push(string.decode(classSelector.name));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
if (isRuleInlinable(rule)) {
|
|
18
|
+
for (const className of selectorClasses) {
|
|
19
|
+
if (classSet.has(className)) {
|
|
20
|
+
inlinableRules.set(className, rule);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
for (const className of selectorClasses) {
|
|
26
|
+
if (classSet.has(className)) {
|
|
27
|
+
nonInlinableRules.set(className, rule);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
inlinable: inlinableRules,
|
|
35
|
+
nonInlinable: nonInlinableRules
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CssNode, type Declaration } from 'css-tree';
|
|
2
|
+
export interface CustomProperty {
|
|
3
|
+
syntax?: Declaration;
|
|
4
|
+
inherits?: Declaration;
|
|
5
|
+
initialValue?: Declaration;
|
|
6
|
+
}
|
|
7
|
+
export type CustomProperties = Map<string, CustomProperty>;
|
|
8
|
+
export declare function getCustomProperties(node: CssNode): Map<string, CustomProperty>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { generate, walk } from 'css-tree';
|
|
2
|
+
export function getCustomProperties(node) {
|
|
3
|
+
const customProperties = new Map();
|
|
4
|
+
walk(node, {
|
|
5
|
+
visit: 'Atrule',
|
|
6
|
+
enter(atrule) {
|
|
7
|
+
if (atrule.name === 'property' && atrule.prelude) {
|
|
8
|
+
const prelude = generate(atrule.prelude);
|
|
9
|
+
if (prelude.startsWith('--')) {
|
|
10
|
+
let syntax;
|
|
11
|
+
let inherits;
|
|
12
|
+
let initialValue;
|
|
13
|
+
walk(atrule, {
|
|
14
|
+
visit: 'Declaration',
|
|
15
|
+
enter(declaration) {
|
|
16
|
+
if (declaration.property === 'syntax') {
|
|
17
|
+
syntax = declaration;
|
|
18
|
+
}
|
|
19
|
+
if (declaration.property === 'inherits') {
|
|
20
|
+
inherits = declaration;
|
|
21
|
+
}
|
|
22
|
+
if (declaration.property === 'initial-value') {
|
|
23
|
+
initialValue = declaration;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
customProperties.set(prelude, {
|
|
28
|
+
syntax,
|
|
29
|
+
inherits,
|
|
30
|
+
initialValue
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return customProperties;
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { find } from 'css-tree';
|
|
2
|
+
export function isRuleInlinable(rule) {
|
|
3
|
+
const hasAtRuleInside = find(rule, (node) => node.type === 'Atrule') !== null;
|
|
4
|
+
const hasPseudoSelector = find(rule, (node) => node.type === 'PseudoClassSelector' || node.type === 'PseudoElementSelector') !== null;
|
|
5
|
+
return !hasAtRuleInside && !hasPseudoSelector;
|
|
6
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { generate, walk } from 'css-tree';
|
|
2
|
+
import { unwrapValue } from './unwrap-value.js';
|
|
3
|
+
export function makeInlineStylesFor(inlinableRules, customProperties) {
|
|
4
|
+
let styles = '';
|
|
5
|
+
const localVariableDeclarations = new Map();
|
|
6
|
+
for (const rule of inlinableRules) {
|
|
7
|
+
walk(rule, {
|
|
8
|
+
visit: 'Declaration',
|
|
9
|
+
enter(declaration) {
|
|
10
|
+
if (declaration.property.startsWith('--')) {
|
|
11
|
+
localVariableDeclarations.set(declaration.property, declaration);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
for (const rule of inlinableRules) {
|
|
17
|
+
walk(rule, {
|
|
18
|
+
visit: 'Function',
|
|
19
|
+
enter(func, funcParentListItem) {
|
|
20
|
+
if (func.name === 'var') {
|
|
21
|
+
let variableName;
|
|
22
|
+
walk(func, {
|
|
23
|
+
visit: 'Identifier',
|
|
24
|
+
enter(identifier) {
|
|
25
|
+
variableName = identifier.name;
|
|
26
|
+
return this.break;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (variableName) {
|
|
30
|
+
const definition = localVariableDeclarations.get(variableName);
|
|
31
|
+
if (definition) {
|
|
32
|
+
funcParentListItem.data = unwrapValue(definition.value);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// For most variables tailwindcss defines, they also define a custom
|
|
36
|
+
// property for them with an initial value that we can inline here
|
|
37
|
+
const customProperty = customProperties.get(variableName);
|
|
38
|
+
if (customProperty?.initialValue) {
|
|
39
|
+
funcParentListItem.data = unwrapValue(customProperty.initialValue.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
walk(rule, {
|
|
47
|
+
visit: 'Declaration',
|
|
48
|
+
enter(declaration) {
|
|
49
|
+
if (declaration.property.startsWith('--')) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
styles += `${declaration.property}: ${generate(declaration.value)} ${declaration.important ? '!important' : ''};`;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return styles;
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CssNode, type Declaration } from 'css-tree';
|
|
2
|
+
export interface VariableDefinition {
|
|
3
|
+
declaration: Declaration;
|
|
4
|
+
path: CssNode[];
|
|
5
|
+
variableName: string;
|
|
6
|
+
definition: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveAllCssVariables(node: CssNode): void;
|