@emailens/engine 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/LICENSE +21 -0
- package/README.md +285 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +3359 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3359 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/clients.ts
|
|
19
|
+
var EMAIL_CLIENTS = [
|
|
20
|
+
{
|
|
21
|
+
id: "gmail-web",
|
|
22
|
+
name: "Gmail",
|
|
23
|
+
category: "webmail",
|
|
24
|
+
engine: "Gmail Web",
|
|
25
|
+
darkModeSupport: true,
|
|
26
|
+
icon: "mail"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "gmail-android",
|
|
30
|
+
name: "Gmail Android",
|
|
31
|
+
category: "mobile",
|
|
32
|
+
engine: "Gmail Mobile",
|
|
33
|
+
darkModeSupport: true,
|
|
34
|
+
icon: "smartphone"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "gmail-ios",
|
|
38
|
+
name: "Gmail iOS",
|
|
39
|
+
category: "mobile",
|
|
40
|
+
engine: "Gmail Mobile",
|
|
41
|
+
darkModeSupport: true,
|
|
42
|
+
icon: "smartphone"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "outlook-web",
|
|
46
|
+
name: "Outlook 365",
|
|
47
|
+
category: "webmail",
|
|
48
|
+
engine: "Outlook Web",
|
|
49
|
+
darkModeSupport: true,
|
|
50
|
+
icon: "mail"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "outlook-windows",
|
|
54
|
+
name: "Outlook Windows",
|
|
55
|
+
category: "desktop",
|
|
56
|
+
engine: "Microsoft Word",
|
|
57
|
+
darkModeSupport: false,
|
|
58
|
+
icon: "monitor"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "apple-mail-macos",
|
|
62
|
+
name: "Apple Mail",
|
|
63
|
+
category: "desktop",
|
|
64
|
+
engine: "WebKit",
|
|
65
|
+
darkModeSupport: true,
|
|
66
|
+
icon: "monitor"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "apple-mail-ios",
|
|
70
|
+
name: "Apple Mail iOS",
|
|
71
|
+
category: "mobile",
|
|
72
|
+
engine: "WebKit",
|
|
73
|
+
darkModeSupport: true,
|
|
74
|
+
icon: "smartphone"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "yahoo-mail",
|
|
78
|
+
name: "Yahoo Mail",
|
|
79
|
+
category: "webmail",
|
|
80
|
+
engine: "Yahoo",
|
|
81
|
+
darkModeSupport: true,
|
|
82
|
+
icon: "mail"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "samsung-mail",
|
|
86
|
+
name: "Samsung Mail",
|
|
87
|
+
category: "mobile",
|
|
88
|
+
engine: "Samsung",
|
|
89
|
+
darkModeSupport: true,
|
|
90
|
+
icon: "smartphone"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "thunderbird",
|
|
94
|
+
name: "Thunderbird",
|
|
95
|
+
category: "desktop",
|
|
96
|
+
engine: "Gecko",
|
|
97
|
+
darkModeSupport: false,
|
|
98
|
+
icon: "monitor"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "hey-mail",
|
|
102
|
+
name: "HEY Mail",
|
|
103
|
+
category: "webmail",
|
|
104
|
+
engine: "WebKit",
|
|
105
|
+
darkModeSupport: true,
|
|
106
|
+
icon: "mail"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "superhuman",
|
|
110
|
+
name: "Superhuman",
|
|
111
|
+
category: "desktop",
|
|
112
|
+
engine: "Blink",
|
|
113
|
+
darkModeSupport: true,
|
|
114
|
+
icon: "monitor"
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
function getClient(id) {
|
|
118
|
+
return EMAIL_CLIENTS.find((c) => c.id === id);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/transform.ts
|
|
122
|
+
import * as cheerio from "cheerio";
|
|
123
|
+
import * as csstree from "css-tree";
|
|
124
|
+
|
|
125
|
+
// src/rules/css-support.ts
|
|
126
|
+
var CSS_SUPPORT = {
|
|
127
|
+
// --- Layout ---
|
|
128
|
+
"display": {
|
|
129
|
+
"gmail-web": "supported",
|
|
130
|
+
// block, inline, inline-block, none all work
|
|
131
|
+
"gmail-android": "partial",
|
|
132
|
+
// basic values only
|
|
133
|
+
"gmail-ios": "partial",
|
|
134
|
+
"outlook-web": "supported",
|
|
135
|
+
"outlook-windows": "partial",
|
|
136
|
+
// limited values
|
|
137
|
+
"apple-mail-macos": "supported",
|
|
138
|
+
"apple-mail-ios": "supported",
|
|
139
|
+
"yahoo-mail": "supported",
|
|
140
|
+
"samsung-mail": "supported",
|
|
141
|
+
"thunderbird": "supported",
|
|
142
|
+
"hey-mail": "supported",
|
|
143
|
+
"superhuman": "supported"
|
|
144
|
+
},
|
|
145
|
+
"display:flex": {
|
|
146
|
+
"gmail-web": "supported",
|
|
147
|
+
// caniemail: y
|
|
148
|
+
"gmail-android": "partial",
|
|
149
|
+
// caniemail: a #1 (non-Google accounts)
|
|
150
|
+
"gmail-ios": "partial",
|
|
151
|
+
// caniemail: a #1
|
|
152
|
+
"outlook-web": "supported",
|
|
153
|
+
// caniemail: y
|
|
154
|
+
"outlook-windows": "unsupported",
|
|
155
|
+
"apple-mail-macos": "supported",
|
|
156
|
+
"apple-mail-ios": "supported",
|
|
157
|
+
"yahoo-mail": "supported",
|
|
158
|
+
// caniemail: y #2 (no inline-flex)
|
|
159
|
+
"samsung-mail": "supported",
|
|
160
|
+
// caniemail: y
|
|
161
|
+
"thunderbird": "supported",
|
|
162
|
+
"hey-mail": "supported",
|
|
163
|
+
"superhuman": "supported"
|
|
164
|
+
},
|
|
165
|
+
"display:grid": {
|
|
166
|
+
"gmail-web": "unsupported",
|
|
167
|
+
"gmail-android": "unsupported",
|
|
168
|
+
"gmail-ios": "unsupported",
|
|
169
|
+
"outlook-web": "unsupported",
|
|
170
|
+
"outlook-windows": "unsupported",
|
|
171
|
+
"apple-mail-macos": "supported",
|
|
172
|
+
"apple-mail-ios": "supported",
|
|
173
|
+
"yahoo-mail": "unsupported",
|
|
174
|
+
"samsung-mail": "unsupported",
|
|
175
|
+
"thunderbird": "supported",
|
|
176
|
+
"hey-mail": "supported",
|
|
177
|
+
"superhuman": "supported"
|
|
178
|
+
},
|
|
179
|
+
"float": {
|
|
180
|
+
"gmail-web": "supported",
|
|
181
|
+
// caniemail: y
|
|
182
|
+
"gmail-android": "partial",
|
|
183
|
+
// caniemail: a #2 (no logical values)
|
|
184
|
+
"gmail-ios": "partial",
|
|
185
|
+
// caniemail: a #2
|
|
186
|
+
"outlook-web": "supported",
|
|
187
|
+
"outlook-windows": "unsupported",
|
|
188
|
+
// caniemail: n #1
|
|
189
|
+
"apple-mail-macos": "supported",
|
|
190
|
+
"apple-mail-ios": "supported",
|
|
191
|
+
"yahoo-mail": "partial",
|
|
192
|
+
// caniemail: a #2 (no logical values)
|
|
193
|
+
"samsung-mail": "supported",
|
|
194
|
+
"thunderbird": "supported",
|
|
195
|
+
"hey-mail": "supported",
|
|
196
|
+
"superhuman": "supported"
|
|
197
|
+
},
|
|
198
|
+
"position": {
|
|
199
|
+
"gmail-web": "unsupported",
|
|
200
|
+
"gmail-android": "unsupported",
|
|
201
|
+
"gmail-ios": "unsupported",
|
|
202
|
+
"outlook-web": "partial",
|
|
203
|
+
// caniemail: a #2 (sticky only)
|
|
204
|
+
"outlook-windows": "unsupported",
|
|
205
|
+
"apple-mail-macos": "partial",
|
|
206
|
+
// caniemail: a #1 (no sticky/fixed)
|
|
207
|
+
"apple-mail-ios": "partial",
|
|
208
|
+
// caniemail: a #1
|
|
209
|
+
"yahoo-mail": "partial",
|
|
210
|
+
// caniemail: a #3 (relative only)
|
|
211
|
+
"samsung-mail": "partial",
|
|
212
|
+
// caniemail: a #1
|
|
213
|
+
"thunderbird": "supported",
|
|
214
|
+
"hey-mail": "partial",
|
|
215
|
+
// relative only; HEY strips fixed/sticky positioning
|
|
216
|
+
"superhuman": "partial"
|
|
217
|
+
// relative/absolute supported; fixed/sticky stripped
|
|
218
|
+
},
|
|
219
|
+
// --- Box Model ---
|
|
220
|
+
"margin": {
|
|
221
|
+
"gmail-web": "partial",
|
|
222
|
+
// no negative margins
|
|
223
|
+
"gmail-android": "partial",
|
|
224
|
+
"gmail-ios": "partial",
|
|
225
|
+
"outlook-web": "supported",
|
|
226
|
+
"outlook-windows": "partial",
|
|
227
|
+
// no auto margins
|
|
228
|
+
"apple-mail-macos": "supported",
|
|
229
|
+
"apple-mail-ios": "supported",
|
|
230
|
+
"yahoo-mail": "supported",
|
|
231
|
+
"samsung-mail": "supported",
|
|
232
|
+
"thunderbird": "supported",
|
|
233
|
+
"hey-mail": "supported",
|
|
234
|
+
"superhuman": "supported"
|
|
235
|
+
},
|
|
236
|
+
"padding": {
|
|
237
|
+
"gmail-web": "supported",
|
|
238
|
+
"gmail-android": "supported",
|
|
239
|
+
"gmail-ios": "supported",
|
|
240
|
+
"outlook-web": "supported",
|
|
241
|
+
"outlook-windows": "partial",
|
|
242
|
+
// not on <p>, <div>
|
|
243
|
+
"apple-mail-macos": "supported",
|
|
244
|
+
"apple-mail-ios": "supported",
|
|
245
|
+
"yahoo-mail": "supported",
|
|
246
|
+
"samsung-mail": "supported",
|
|
247
|
+
"thunderbird": "supported",
|
|
248
|
+
"hey-mail": "supported",
|
|
249
|
+
"superhuman": "supported"
|
|
250
|
+
},
|
|
251
|
+
"width": {
|
|
252
|
+
"gmail-web": "supported",
|
|
253
|
+
"gmail-android": "supported",
|
|
254
|
+
"gmail-ios": "supported",
|
|
255
|
+
"outlook-web": "supported",
|
|
256
|
+
"outlook-windows": "supported",
|
|
257
|
+
"apple-mail-macos": "supported",
|
|
258
|
+
"apple-mail-ios": "supported",
|
|
259
|
+
"yahoo-mail": "supported",
|
|
260
|
+
"samsung-mail": "supported",
|
|
261
|
+
"thunderbird": "supported",
|
|
262
|
+
"hey-mail": "supported",
|
|
263
|
+
"superhuman": "supported"
|
|
264
|
+
},
|
|
265
|
+
"max-width": {
|
|
266
|
+
"gmail-web": "supported",
|
|
267
|
+
"gmail-android": "supported",
|
|
268
|
+
"gmail-ios": "supported",
|
|
269
|
+
"outlook-web": "supported",
|
|
270
|
+
"outlook-windows": "partial",
|
|
271
|
+
// caniemail: a #1 (only on <table> elements)
|
|
272
|
+
"apple-mail-macos": "supported",
|
|
273
|
+
"apple-mail-ios": "supported",
|
|
274
|
+
"yahoo-mail": "supported",
|
|
275
|
+
"samsung-mail": "supported",
|
|
276
|
+
"thunderbird": "supported",
|
|
277
|
+
"hey-mail": "supported",
|
|
278
|
+
"superhuman": "supported"
|
|
279
|
+
},
|
|
280
|
+
"height": {
|
|
281
|
+
"gmail-web": "supported",
|
|
282
|
+
"gmail-android": "supported",
|
|
283
|
+
"gmail-ios": "supported",
|
|
284
|
+
"outlook-web": "supported",
|
|
285
|
+
"outlook-windows": "supported",
|
|
286
|
+
"apple-mail-macos": "supported",
|
|
287
|
+
"apple-mail-ios": "supported",
|
|
288
|
+
"yahoo-mail": "supported",
|
|
289
|
+
"samsung-mail": "supported",
|
|
290
|
+
"thunderbird": "supported",
|
|
291
|
+
"hey-mail": "supported",
|
|
292
|
+
"superhuman": "supported"
|
|
293
|
+
},
|
|
294
|
+
"box-sizing": {
|
|
295
|
+
"gmail-web": "unsupported",
|
|
296
|
+
"gmail-android": "unsupported",
|
|
297
|
+
"gmail-ios": "unsupported",
|
|
298
|
+
"outlook-web": "supported",
|
|
299
|
+
"outlook-windows": "unsupported",
|
|
300
|
+
"apple-mail-macos": "supported",
|
|
301
|
+
"apple-mail-ios": "supported",
|
|
302
|
+
"yahoo-mail": "supported",
|
|
303
|
+
"samsung-mail": "supported",
|
|
304
|
+
"thunderbird": "supported",
|
|
305
|
+
"hey-mail": "supported",
|
|
306
|
+
"superhuman": "supported"
|
|
307
|
+
},
|
|
308
|
+
// --- Typography ---
|
|
309
|
+
"font-family": {
|
|
310
|
+
"gmail-web": "supported",
|
|
311
|
+
"gmail-android": "supported",
|
|
312
|
+
"gmail-ios": "supported",
|
|
313
|
+
"outlook-web": "supported",
|
|
314
|
+
"outlook-windows": "supported",
|
|
315
|
+
"apple-mail-macos": "supported",
|
|
316
|
+
"apple-mail-ios": "supported",
|
|
317
|
+
"yahoo-mail": "supported",
|
|
318
|
+
"samsung-mail": "supported",
|
|
319
|
+
"thunderbird": "supported",
|
|
320
|
+
"hey-mail": "supported",
|
|
321
|
+
"superhuman": "supported"
|
|
322
|
+
},
|
|
323
|
+
"font-size": {
|
|
324
|
+
"gmail-web": "supported",
|
|
325
|
+
"gmail-android": "supported",
|
|
326
|
+
"gmail-ios": "partial",
|
|
327
|
+
// may auto-resize small text
|
|
328
|
+
"outlook-web": "supported",
|
|
329
|
+
"outlook-windows": "supported",
|
|
330
|
+
"apple-mail-macos": "supported",
|
|
331
|
+
"apple-mail-ios": "partial",
|
|
332
|
+
"yahoo-mail": "supported",
|
|
333
|
+
"samsung-mail": "supported",
|
|
334
|
+
"thunderbird": "supported",
|
|
335
|
+
"hey-mail": "supported",
|
|
336
|
+
"superhuman": "supported"
|
|
337
|
+
},
|
|
338
|
+
"font-weight": {
|
|
339
|
+
"gmail-web": "supported",
|
|
340
|
+
"gmail-android": "supported",
|
|
341
|
+
"gmail-ios": "supported",
|
|
342
|
+
"outlook-web": "supported",
|
|
343
|
+
"outlook-windows": "supported",
|
|
344
|
+
"apple-mail-macos": "supported",
|
|
345
|
+
"apple-mail-ios": "supported",
|
|
346
|
+
"yahoo-mail": "supported",
|
|
347
|
+
"samsung-mail": "supported",
|
|
348
|
+
"thunderbird": "supported",
|
|
349
|
+
"hey-mail": "supported",
|
|
350
|
+
"superhuman": "supported"
|
|
351
|
+
},
|
|
352
|
+
"line-height": {
|
|
353
|
+
"gmail-web": "supported",
|
|
354
|
+
"gmail-android": "supported",
|
|
355
|
+
"gmail-ios": "supported",
|
|
356
|
+
"outlook-web": "supported",
|
|
357
|
+
"outlook-windows": "partial",
|
|
358
|
+
// ignores on some elements
|
|
359
|
+
"apple-mail-macos": "supported",
|
|
360
|
+
"apple-mail-ios": "supported",
|
|
361
|
+
"yahoo-mail": "supported",
|
|
362
|
+
"samsung-mail": "supported",
|
|
363
|
+
"thunderbird": "supported",
|
|
364
|
+
"hey-mail": "supported",
|
|
365
|
+
"superhuman": "supported"
|
|
366
|
+
},
|
|
367
|
+
"letter-spacing": {
|
|
368
|
+
"gmail-web": "supported",
|
|
369
|
+
"gmail-android": "supported",
|
|
370
|
+
"gmail-ios": "supported",
|
|
371
|
+
"outlook-web": "supported",
|
|
372
|
+
"outlook-windows": "partial",
|
|
373
|
+
"apple-mail-macos": "supported",
|
|
374
|
+
"apple-mail-ios": "supported",
|
|
375
|
+
"yahoo-mail": "supported",
|
|
376
|
+
"samsung-mail": "supported",
|
|
377
|
+
"thunderbird": "supported",
|
|
378
|
+
"hey-mail": "supported",
|
|
379
|
+
"superhuman": "supported"
|
|
380
|
+
},
|
|
381
|
+
"text-align": {
|
|
382
|
+
"gmail-web": "supported",
|
|
383
|
+
"gmail-android": "supported",
|
|
384
|
+
"gmail-ios": "supported",
|
|
385
|
+
"outlook-web": "supported",
|
|
386
|
+
"outlook-windows": "supported",
|
|
387
|
+
"apple-mail-macos": "supported",
|
|
388
|
+
"apple-mail-ios": "supported",
|
|
389
|
+
"yahoo-mail": "supported",
|
|
390
|
+
"samsung-mail": "supported",
|
|
391
|
+
"thunderbird": "supported",
|
|
392
|
+
"hey-mail": "supported",
|
|
393
|
+
"superhuman": "supported"
|
|
394
|
+
},
|
|
395
|
+
"text-decoration": {
|
|
396
|
+
"gmail-web": "supported",
|
|
397
|
+
"gmail-android": "supported",
|
|
398
|
+
"gmail-ios": "supported",
|
|
399
|
+
"outlook-web": "supported",
|
|
400
|
+
"outlook-windows": "supported",
|
|
401
|
+
"apple-mail-macos": "supported",
|
|
402
|
+
"apple-mail-ios": "supported",
|
|
403
|
+
"yahoo-mail": "supported",
|
|
404
|
+
"samsung-mail": "supported",
|
|
405
|
+
"thunderbird": "supported",
|
|
406
|
+
"hey-mail": "supported",
|
|
407
|
+
"superhuman": "supported"
|
|
408
|
+
},
|
|
409
|
+
"text-transform": {
|
|
410
|
+
"gmail-web": "supported",
|
|
411
|
+
"gmail-android": "supported",
|
|
412
|
+
"gmail-ios": "supported",
|
|
413
|
+
"outlook-web": "supported",
|
|
414
|
+
"outlook-windows": "supported",
|
|
415
|
+
"apple-mail-macos": "supported",
|
|
416
|
+
"apple-mail-ios": "supported",
|
|
417
|
+
"yahoo-mail": "supported",
|
|
418
|
+
"samsung-mail": "supported",
|
|
419
|
+
"thunderbird": "supported",
|
|
420
|
+
"hey-mail": "supported",
|
|
421
|
+
"superhuman": "supported"
|
|
422
|
+
},
|
|
423
|
+
"@font-face": {
|
|
424
|
+
"gmail-web": "unsupported",
|
|
425
|
+
"gmail-android": "unsupported",
|
|
426
|
+
"gmail-ios": "unsupported",
|
|
427
|
+
"outlook-web": "unsupported",
|
|
428
|
+
"outlook-windows": "partial",
|
|
429
|
+
// caniemail: a #4 (declaration kept, remote fonts ignored)
|
|
430
|
+
"apple-mail-macos": "supported",
|
|
431
|
+
"apple-mail-ios": "supported",
|
|
432
|
+
"yahoo-mail": "unsupported",
|
|
433
|
+
"samsung-mail": "supported",
|
|
434
|
+
// caniemail: y #8
|
|
435
|
+
"thunderbird": "supported",
|
|
436
|
+
"hey-mail": "supported",
|
|
437
|
+
// HEY uses WebKit — web fonts work
|
|
438
|
+
"superhuman": "supported"
|
|
439
|
+
// Superhuman uses Blink — web fonts work
|
|
440
|
+
},
|
|
441
|
+
// --- Colors & Backgrounds ---
|
|
442
|
+
"color": {
|
|
443
|
+
"gmail-web": "supported",
|
|
444
|
+
"gmail-android": "supported",
|
|
445
|
+
"gmail-ios": "supported",
|
|
446
|
+
"outlook-web": "supported",
|
|
447
|
+
"outlook-windows": "supported",
|
|
448
|
+
"apple-mail-macos": "supported",
|
|
449
|
+
"apple-mail-ios": "supported",
|
|
450
|
+
"yahoo-mail": "supported",
|
|
451
|
+
"samsung-mail": "supported",
|
|
452
|
+
"thunderbird": "supported",
|
|
453
|
+
"hey-mail": "supported",
|
|
454
|
+
"superhuman": "supported"
|
|
455
|
+
},
|
|
456
|
+
"background-color": {
|
|
457
|
+
"gmail-web": "supported",
|
|
458
|
+
"gmail-android": "supported",
|
|
459
|
+
"gmail-ios": "supported",
|
|
460
|
+
"outlook-web": "supported",
|
|
461
|
+
"outlook-windows": "supported",
|
|
462
|
+
"apple-mail-macos": "supported",
|
|
463
|
+
"apple-mail-ios": "supported",
|
|
464
|
+
"yahoo-mail": "supported",
|
|
465
|
+
"samsung-mail": "supported",
|
|
466
|
+
"thunderbird": "supported",
|
|
467
|
+
"hey-mail": "supported",
|
|
468
|
+
"superhuman": "supported"
|
|
469
|
+
},
|
|
470
|
+
"background-image": {
|
|
471
|
+
"gmail-web": "supported",
|
|
472
|
+
// caniemail: y (restored 2023-08)
|
|
473
|
+
"gmail-android": "supported",
|
|
474
|
+
// caniemail: y
|
|
475
|
+
"gmail-ios": "supported",
|
|
476
|
+
// caniemail: y
|
|
477
|
+
"outlook-web": "supported",
|
|
478
|
+
// caniemail: y
|
|
479
|
+
"outlook-windows": "partial",
|
|
480
|
+
// caniemail: n #5 (VML workaround)
|
|
481
|
+
"apple-mail-macos": "supported",
|
|
482
|
+
"apple-mail-ios": "supported",
|
|
483
|
+
"yahoo-mail": "partial",
|
|
484
|
+
// caniemail: a #3 (no multiple values)
|
|
485
|
+
"samsung-mail": "supported",
|
|
486
|
+
// caniemail: y
|
|
487
|
+
"thunderbird": "supported",
|
|
488
|
+
"hey-mail": "supported",
|
|
489
|
+
"superhuman": "supported"
|
|
490
|
+
},
|
|
491
|
+
"background": {
|
|
492
|
+
"gmail-web": "partial",
|
|
493
|
+
// color only, no shorthand with images
|
|
494
|
+
"gmail-android": "partial",
|
|
495
|
+
"gmail-ios": "partial",
|
|
496
|
+
"outlook-web": "partial",
|
|
497
|
+
"outlook-windows": "partial",
|
|
498
|
+
"apple-mail-macos": "supported",
|
|
499
|
+
"apple-mail-ios": "supported",
|
|
500
|
+
"yahoo-mail": "partial",
|
|
501
|
+
"samsung-mail": "partial",
|
|
502
|
+
"thunderbird": "supported",
|
|
503
|
+
"hey-mail": "supported",
|
|
504
|
+
"superhuman": "supported"
|
|
505
|
+
},
|
|
506
|
+
"linear-gradient": {
|
|
507
|
+
"gmail-web": "unsupported",
|
|
508
|
+
"gmail-android": "unsupported",
|
|
509
|
+
"gmail-ios": "unsupported",
|
|
510
|
+
"outlook-web": "unsupported",
|
|
511
|
+
"outlook-windows": "unsupported",
|
|
512
|
+
"apple-mail-macos": "supported",
|
|
513
|
+
"apple-mail-ios": "supported",
|
|
514
|
+
"yahoo-mail": "unsupported",
|
|
515
|
+
"samsung-mail": "unsupported",
|
|
516
|
+
"thunderbird": "supported",
|
|
517
|
+
"hey-mail": "supported",
|
|
518
|
+
"superhuman": "supported"
|
|
519
|
+
},
|
|
520
|
+
// --- Borders ---
|
|
521
|
+
"border": {
|
|
522
|
+
"gmail-web": "supported",
|
|
523
|
+
"gmail-android": "supported",
|
|
524
|
+
"gmail-ios": "supported",
|
|
525
|
+
"outlook-web": "supported",
|
|
526
|
+
"outlook-windows": "partial",
|
|
527
|
+
// no shorthand on some elements
|
|
528
|
+
"apple-mail-macos": "supported",
|
|
529
|
+
"apple-mail-ios": "supported",
|
|
530
|
+
"yahoo-mail": "supported",
|
|
531
|
+
"samsung-mail": "supported",
|
|
532
|
+
"thunderbird": "supported",
|
|
533
|
+
"hey-mail": "supported",
|
|
534
|
+
"superhuman": "supported"
|
|
535
|
+
},
|
|
536
|
+
"border-radius": {
|
|
537
|
+
"gmail-web": "supported",
|
|
538
|
+
"gmail-android": "supported",
|
|
539
|
+
"gmail-ios": "supported",
|
|
540
|
+
"outlook-web": "supported",
|
|
541
|
+
"outlook-windows": "unsupported",
|
|
542
|
+
"apple-mail-macos": "supported",
|
|
543
|
+
"apple-mail-ios": "supported",
|
|
544
|
+
"yahoo-mail": "partial",
|
|
545
|
+
// caniemail: a #2 (no elliptical slash notation)
|
|
546
|
+
"samsung-mail": "supported",
|
|
547
|
+
"thunderbird": "supported",
|
|
548
|
+
"hey-mail": "supported",
|
|
549
|
+
"superhuman": "supported"
|
|
550
|
+
},
|
|
551
|
+
"border-collapse": {
|
|
552
|
+
"gmail-web": "supported",
|
|
553
|
+
"gmail-android": "supported",
|
|
554
|
+
"gmail-ios": "supported",
|
|
555
|
+
"outlook-web": "supported",
|
|
556
|
+
"outlook-windows": "supported",
|
|
557
|
+
"apple-mail-macos": "supported",
|
|
558
|
+
"apple-mail-ios": "supported",
|
|
559
|
+
"yahoo-mail": "supported",
|
|
560
|
+
"samsung-mail": "supported",
|
|
561
|
+
"thunderbird": "supported",
|
|
562
|
+
"hey-mail": "supported",
|
|
563
|
+
"superhuman": "supported"
|
|
564
|
+
},
|
|
565
|
+
"box-shadow": {
|
|
566
|
+
"gmail-web": "unsupported",
|
|
567
|
+
"gmail-android": "partial",
|
|
568
|
+
// caniemail: a #1 (non-Google accounts)
|
|
569
|
+
"gmail-ios": "partial",
|
|
570
|
+
// caniemail: a #1
|
|
571
|
+
"outlook-web": "supported",
|
|
572
|
+
// caniemail: y (since 2023-12)
|
|
573
|
+
"outlook-windows": "unsupported",
|
|
574
|
+
"apple-mail-macos": "supported",
|
|
575
|
+
"apple-mail-ios": "supported",
|
|
576
|
+
"yahoo-mail": "unsupported",
|
|
577
|
+
"samsung-mail": "supported",
|
|
578
|
+
// caniemail: y
|
|
579
|
+
"thunderbird": "supported",
|
|
580
|
+
"hey-mail": "supported",
|
|
581
|
+
"superhuman": "supported"
|
|
582
|
+
},
|
|
583
|
+
// --- Transforms & Animation ---
|
|
584
|
+
"transform": {
|
|
585
|
+
"gmail-web": "unsupported",
|
|
586
|
+
"gmail-android": "unsupported",
|
|
587
|
+
"gmail-ios": "unsupported",
|
|
588
|
+
"outlook-web": "unsupported",
|
|
589
|
+
"outlook-windows": "unsupported",
|
|
590
|
+
"apple-mail-macos": "supported",
|
|
591
|
+
"apple-mail-ios": "supported",
|
|
592
|
+
"yahoo-mail": "unsupported",
|
|
593
|
+
"samsung-mail": "unsupported",
|
|
594
|
+
"thunderbird": "supported",
|
|
595
|
+
"hey-mail": "unsupported",
|
|
596
|
+
// HEY strips transform for security
|
|
597
|
+
"superhuman": "partial"
|
|
598
|
+
// Superhuman (Blink) allows some transforms
|
|
599
|
+
},
|
|
600
|
+
"animation": {
|
|
601
|
+
"gmail-web": "unsupported",
|
|
602
|
+
"gmail-android": "unsupported",
|
|
603
|
+
"gmail-ios": "unsupported",
|
|
604
|
+
"outlook-web": "unsupported",
|
|
605
|
+
"outlook-windows": "unsupported",
|
|
606
|
+
"apple-mail-macos": "supported",
|
|
607
|
+
"apple-mail-ios": "supported",
|
|
608
|
+
"yahoo-mail": "unsupported",
|
|
609
|
+
"samsung-mail": "unsupported",
|
|
610
|
+
"thunderbird": "unsupported",
|
|
611
|
+
"hey-mail": "unsupported",
|
|
612
|
+
"superhuman": "partial"
|
|
613
|
+
// Superhuman allows CSS animations but they may be disabled by user settings
|
|
614
|
+
},
|
|
615
|
+
"transition": {
|
|
616
|
+
"gmail-web": "unsupported",
|
|
617
|
+
"gmail-android": "unsupported",
|
|
618
|
+
"gmail-ios": "unsupported",
|
|
619
|
+
"outlook-web": "unsupported",
|
|
620
|
+
"outlook-windows": "unsupported",
|
|
621
|
+
"apple-mail-macos": "supported",
|
|
622
|
+
"apple-mail-ios": "supported",
|
|
623
|
+
"yahoo-mail": "unsupported",
|
|
624
|
+
"samsung-mail": "unsupported",
|
|
625
|
+
"thunderbird": "unsupported",
|
|
626
|
+
"hey-mail": "unsupported",
|
|
627
|
+
"superhuman": "unsupported"
|
|
628
|
+
},
|
|
629
|
+
// --- Media & Responsive ---
|
|
630
|
+
"@media": {
|
|
631
|
+
"gmail-web": "partial",
|
|
632
|
+
// caniemail: a #7 (no height-based, no nested)
|
|
633
|
+
"gmail-android": "partial",
|
|
634
|
+
// caniemail: a #6 #7
|
|
635
|
+
"gmail-ios": "partial",
|
|
636
|
+
// caniemail: a #6 #7
|
|
637
|
+
"outlook-web": "partial",
|
|
638
|
+
// caniemail: a #1 #10 (no nested)
|
|
639
|
+
"outlook-windows": "unsupported",
|
|
640
|
+
"apple-mail-macos": "supported",
|
|
641
|
+
"apple-mail-ios": "supported",
|
|
642
|
+
"yahoo-mail": "partial",
|
|
643
|
+
// caniemail: a #2
|
|
644
|
+
"samsung-mail": "partial",
|
|
645
|
+
// caniemail: a #9
|
|
646
|
+
"thunderbird": "supported",
|
|
647
|
+
"hey-mail": "supported",
|
|
648
|
+
// HEY is WebKit-based, full @media support
|
|
649
|
+
"superhuman": "supported"
|
|
650
|
+
// Superhuman is Blink-based, full @media support
|
|
651
|
+
},
|
|
652
|
+
// --- HTML Elements ---
|
|
653
|
+
"<style>": {
|
|
654
|
+
"gmail-web": "partial",
|
|
655
|
+
// caniemail: a #1 #6 (head only, 16KB limit)
|
|
656
|
+
"gmail-android": "partial",
|
|
657
|
+
// caniemail: a #1 (head only)
|
|
658
|
+
"gmail-ios": "partial",
|
|
659
|
+
// caniemail: a #1 #2
|
|
660
|
+
"outlook-web": "supported",
|
|
661
|
+
"outlook-windows": "partial",
|
|
662
|
+
// caniemail: a #4 (must declare before use)
|
|
663
|
+
"apple-mail-macos": "supported",
|
|
664
|
+
"apple-mail-ios": "supported",
|
|
665
|
+
"yahoo-mail": "supported",
|
|
666
|
+
"samsung-mail": "supported",
|
|
667
|
+
"thunderbird": "supported",
|
|
668
|
+
"hey-mail": "supported",
|
|
669
|
+
"superhuman": "supported"
|
|
670
|
+
},
|
|
671
|
+
"<link>": {
|
|
672
|
+
"gmail-web": "unsupported",
|
|
673
|
+
"gmail-android": "unsupported",
|
|
674
|
+
"gmail-ios": "unsupported",
|
|
675
|
+
"outlook-web": "unsupported",
|
|
676
|
+
"outlook-windows": "unsupported",
|
|
677
|
+
"apple-mail-macos": "supported",
|
|
678
|
+
"apple-mail-ios": "supported",
|
|
679
|
+
"yahoo-mail": "unsupported",
|
|
680
|
+
"samsung-mail": "unsupported",
|
|
681
|
+
"thunderbird": "unsupported",
|
|
682
|
+
"hey-mail": "unsupported",
|
|
683
|
+
// HEY strips external stylesheets
|
|
684
|
+
"superhuman": "unsupported"
|
|
685
|
+
// Superhuman strips external stylesheets
|
|
686
|
+
},
|
|
687
|
+
"<video>": {
|
|
688
|
+
"gmail-web": "unsupported",
|
|
689
|
+
"gmail-android": "unsupported",
|
|
690
|
+
"gmail-ios": "unsupported",
|
|
691
|
+
"outlook-web": "unsupported",
|
|
692
|
+
"outlook-windows": "unsupported",
|
|
693
|
+
"apple-mail-macos": "supported",
|
|
694
|
+
"apple-mail-ios": "partial",
|
|
695
|
+
"yahoo-mail": "unsupported",
|
|
696
|
+
"samsung-mail": "unsupported",
|
|
697
|
+
"thunderbird": "unsupported",
|
|
698
|
+
"hey-mail": "unsupported",
|
|
699
|
+
"superhuman": "unsupported"
|
|
700
|
+
},
|
|
701
|
+
"<svg>": {
|
|
702
|
+
"gmail-web": "unsupported",
|
|
703
|
+
"gmail-android": "unsupported",
|
|
704
|
+
"gmail-ios": "unsupported",
|
|
705
|
+
"outlook-web": "partial",
|
|
706
|
+
"outlook-windows": "unsupported",
|
|
707
|
+
"apple-mail-macos": "supported",
|
|
708
|
+
"apple-mail-ios": "supported",
|
|
709
|
+
"yahoo-mail": "unsupported",
|
|
710
|
+
"samsung-mail": "partial",
|
|
711
|
+
"thunderbird": "supported",
|
|
712
|
+
"hey-mail": "partial",
|
|
713
|
+
// HEY allows SVG but strips some attributes
|
|
714
|
+
"superhuman": "supported"
|
|
715
|
+
// Superhuman (Blink) renders SVG well
|
|
716
|
+
},
|
|
717
|
+
"<form>": {
|
|
718
|
+
"gmail-web": "unsupported",
|
|
719
|
+
"gmail-android": "unsupported",
|
|
720
|
+
"gmail-ios": "unsupported",
|
|
721
|
+
"outlook-web": "unsupported",
|
|
722
|
+
"outlook-windows": "unsupported",
|
|
723
|
+
"apple-mail-macos": "supported",
|
|
724
|
+
"apple-mail-ios": "supported",
|
|
725
|
+
"yahoo-mail": "unsupported",
|
|
726
|
+
"samsung-mail": "unsupported",
|
|
727
|
+
"thunderbird": "supported",
|
|
728
|
+
"hey-mail": "unsupported",
|
|
729
|
+
// HEY strips forms for security
|
|
730
|
+
"superhuman": "unsupported"
|
|
731
|
+
// Superhuman strips forms for security
|
|
732
|
+
},
|
|
733
|
+
// --- Misc ---
|
|
734
|
+
"opacity": {
|
|
735
|
+
"gmail-web": "unsupported",
|
|
736
|
+
"gmail-android": "unsupported",
|
|
737
|
+
"gmail-ios": "unsupported",
|
|
738
|
+
"outlook-web": "supported",
|
|
739
|
+
"outlook-windows": "unsupported",
|
|
740
|
+
"apple-mail-macos": "supported",
|
|
741
|
+
"apple-mail-ios": "supported",
|
|
742
|
+
"yahoo-mail": "supported",
|
|
743
|
+
"samsung-mail": "supported",
|
|
744
|
+
"thunderbird": "supported",
|
|
745
|
+
"hey-mail": "supported",
|
|
746
|
+
"superhuman": "supported"
|
|
747
|
+
},
|
|
748
|
+
"overflow": {
|
|
749
|
+
"gmail-web": "unsupported",
|
|
750
|
+
"gmail-android": "unsupported",
|
|
751
|
+
"gmail-ios": "unsupported",
|
|
752
|
+
"outlook-web": "supported",
|
|
753
|
+
"outlook-windows": "unsupported",
|
|
754
|
+
"apple-mail-macos": "supported",
|
|
755
|
+
"apple-mail-ios": "supported",
|
|
756
|
+
"yahoo-mail": "supported",
|
|
757
|
+
"samsung-mail": "partial",
|
|
758
|
+
"thunderbird": "supported",
|
|
759
|
+
"hey-mail": "supported",
|
|
760
|
+
"superhuman": "supported"
|
|
761
|
+
},
|
|
762
|
+
"visibility": {
|
|
763
|
+
"gmail-web": "unsupported",
|
|
764
|
+
"gmail-android": "unsupported",
|
|
765
|
+
"gmail-ios": "unsupported",
|
|
766
|
+
"outlook-web": "supported",
|
|
767
|
+
"outlook-windows": "unsupported",
|
|
768
|
+
"apple-mail-macos": "supported",
|
|
769
|
+
"apple-mail-ios": "supported",
|
|
770
|
+
"yahoo-mail": "supported",
|
|
771
|
+
"samsung-mail": "supported",
|
|
772
|
+
"thunderbird": "supported",
|
|
773
|
+
"hey-mail": "supported",
|
|
774
|
+
"superhuman": "supported"
|
|
775
|
+
},
|
|
776
|
+
"gap": {
|
|
777
|
+
"gmail-web": "unsupported",
|
|
778
|
+
"gmail-android": "unsupported",
|
|
779
|
+
"gmail-ios": "unsupported",
|
|
780
|
+
"outlook-web": "unsupported",
|
|
781
|
+
"outlook-windows": "unsupported",
|
|
782
|
+
"apple-mail-macos": "supported",
|
|
783
|
+
"apple-mail-ios": "supported",
|
|
784
|
+
"yahoo-mail": "unsupported",
|
|
785
|
+
"samsung-mail": "unsupported",
|
|
786
|
+
"thunderbird": "supported",
|
|
787
|
+
"hey-mail": "supported",
|
|
788
|
+
"superhuman": "supported"
|
|
789
|
+
},
|
|
790
|
+
"object-fit": {
|
|
791
|
+
"gmail-web": "unsupported",
|
|
792
|
+
"gmail-android": "unsupported",
|
|
793
|
+
"gmail-ios": "unsupported",
|
|
794
|
+
"outlook-web": "unsupported",
|
|
795
|
+
"outlook-windows": "unsupported",
|
|
796
|
+
"apple-mail-macos": "supported",
|
|
797
|
+
"apple-mail-ios": "supported",
|
|
798
|
+
"yahoo-mail": "unsupported",
|
|
799
|
+
"samsung-mail": "unsupported",
|
|
800
|
+
"thunderbird": "supported",
|
|
801
|
+
"hey-mail": "supported",
|
|
802
|
+
"superhuman": "supported"
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
var GMAIL_STRIPPED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
806
|
+
"position",
|
|
807
|
+
"overflow",
|
|
808
|
+
"visibility",
|
|
809
|
+
"opacity",
|
|
810
|
+
"box-shadow",
|
|
811
|
+
"text-shadow",
|
|
812
|
+
"transform",
|
|
813
|
+
"animation",
|
|
814
|
+
"transition",
|
|
815
|
+
"box-sizing",
|
|
816
|
+
"object-fit",
|
|
817
|
+
"gap"
|
|
818
|
+
]);
|
|
819
|
+
var OUTLOOK_WORD_UNSUPPORTED = /* @__PURE__ */ new Set([
|
|
820
|
+
"border-radius",
|
|
821
|
+
"box-shadow",
|
|
822
|
+
"text-shadow",
|
|
823
|
+
"max-width",
|
|
824
|
+
"max-height",
|
|
825
|
+
"float",
|
|
826
|
+
"position",
|
|
827
|
+
"display",
|
|
828
|
+
"overflow",
|
|
829
|
+
"opacity",
|
|
830
|
+
"transform",
|
|
831
|
+
"animation",
|
|
832
|
+
"transition",
|
|
833
|
+
"background-size",
|
|
834
|
+
"background-position",
|
|
835
|
+
"box-sizing",
|
|
836
|
+
"object-fit",
|
|
837
|
+
"gap"
|
|
838
|
+
]);
|
|
839
|
+
|
|
840
|
+
// src/fix-snippets.ts
|
|
841
|
+
var FIX_DATABASE = {
|
|
842
|
+
// ── border-radius (Outlook VML fallback) ──────────────────────────────
|
|
843
|
+
"border-radius::outlook": {
|
|
844
|
+
language: "html",
|
|
845
|
+
description: "Use VML to render rounded buttons in Outlook",
|
|
846
|
+
before: `<a href="https://example.com"
|
|
847
|
+
style="background-color: #6d28d9; color: #fff;
|
|
848
|
+
padding: 12px 32px; border-radius: 6px;
|
|
849
|
+
text-decoration: none; display: inline-block;">
|
|
850
|
+
Click Here
|
|
851
|
+
</a>`,
|
|
852
|
+
after: `<!--[if mso]>
|
|
853
|
+
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
854
|
+
href="https://example.com"
|
|
855
|
+
style="height:44px; v-text-anchor:middle; width:200px;"
|
|
856
|
+
arcsize="14%" strokecolor="#6d28d9" fillcolor="#6d28d9">
|
|
857
|
+
<w:anchorlock/>
|
|
858
|
+
<center style="color:#fff; font-family:Arial,sans-serif;
|
|
859
|
+
font-size:14px; font-weight:bold;">Click Here</center>
|
|
860
|
+
</v:roundrect>
|
|
861
|
+
<![endif]-->
|
|
862
|
+
<!--[if !mso]><!-->
|
|
863
|
+
<a href="https://example.com"
|
|
864
|
+
style="background-color: #6d28d9; color: #fff;
|
|
865
|
+
padding: 12px 32px; border-radius: 6px;
|
|
866
|
+
text-decoration: none; display: inline-block;">
|
|
867
|
+
Click Here
|
|
868
|
+
</a>
|
|
869
|
+
<!--<![endif]-->`
|
|
870
|
+
},
|
|
871
|
+
// ── background-image (Outlook VML) ────────────────────────────────────
|
|
872
|
+
"background-image::outlook": {
|
|
873
|
+
language: "html",
|
|
874
|
+
description: "Use VML for background images in Outlook",
|
|
875
|
+
before: `<td style="background-image: url('hero.jpg');
|
|
876
|
+
background-size: cover; padding: 40px;">
|
|
877
|
+
<h1 style="color: #fff;">Hello World</h1>
|
|
878
|
+
</td>`,
|
|
879
|
+
after: `<!--[if gte mso 9]>
|
|
880
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true"
|
|
881
|
+
stroke="false" style="width:600px; height:300px;">
|
|
882
|
+
<v:fill type="frame" src="hero.jpg" />
|
|
883
|
+
<v:textbox inset="0,0,0,0">
|
|
884
|
+
<![endif]-->
|
|
885
|
+
<td style="background-image: url('hero.jpg');
|
|
886
|
+
background-size: cover; padding: 40px;">
|
|
887
|
+
<h1 style="color: #fff;">Hello World</h1>
|
|
888
|
+
</td>
|
|
889
|
+
<!--[if gte mso 9]>
|
|
890
|
+
</v:textbox>
|
|
891
|
+
</v:rect>
|
|
892
|
+
<![endif]-->`
|
|
893
|
+
},
|
|
894
|
+
// ── display:flex → table layout ───────────────────────────────────────
|
|
895
|
+
"display:flex::outlook": {
|
|
896
|
+
language: "html",
|
|
897
|
+
description: "Use table layout as fallback for flexbox in Outlook",
|
|
898
|
+
before: `<div style="display: flex; gap: 16px;">
|
|
899
|
+
<div style="flex: 1;">Column 1</div>
|
|
900
|
+
<div style="flex: 1;">Column 2</div>
|
|
901
|
+
</div>`,
|
|
902
|
+
after: `<!--[if mso]>
|
|
903
|
+
<table role="presentation" width="100%" cellpadding="0"
|
|
904
|
+
cellspacing="0" border="0"><tr>
|
|
905
|
+
<td width="50%" valign="top">Column 1</td>
|
|
906
|
+
<td width="50%" valign="top">Column 2</td>
|
|
907
|
+
</tr></table>
|
|
908
|
+
<![endif]-->
|
|
909
|
+
<!--[if !mso]><!-->
|
|
910
|
+
<div style="display: flex; gap: 16px;">
|
|
911
|
+
<div style="flex: 1;">Column 1</div>
|
|
912
|
+
<div style="flex: 1;">Column 2</div>
|
|
913
|
+
</div>
|
|
914
|
+
<!--<![endif]-->`
|
|
915
|
+
},
|
|
916
|
+
// ── display:grid → table layout ───────────────────────────────────────
|
|
917
|
+
"display:grid": {
|
|
918
|
+
language: "html",
|
|
919
|
+
description: "Replace CSS Grid with table layout for email compatibility",
|
|
920
|
+
before: `<div style="display: grid;
|
|
921
|
+
grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
922
|
+
<div>Item 1</div>
|
|
923
|
+
<div>Item 2</div>
|
|
924
|
+
</div>`,
|
|
925
|
+
after: `<table role="presentation" width="100%" cellpadding="0"
|
|
926
|
+
cellspacing="0" border="0">
|
|
927
|
+
<tr>
|
|
928
|
+
<td width="50%" style="padding: 8px;" valign="top">
|
|
929
|
+
Item 1
|
|
930
|
+
</td>
|
|
931
|
+
<td width="50%" style="padding: 8px;" valign="top">
|
|
932
|
+
Item 2
|
|
933
|
+
</td>
|
|
934
|
+
</tr>
|
|
935
|
+
</table>`
|
|
936
|
+
},
|
|
937
|
+
// ── linear-gradient fallback ──────────────────────────────────────────
|
|
938
|
+
"linear-gradient": {
|
|
939
|
+
language: "html",
|
|
940
|
+
description: "Add solid color fallback for gradient backgrounds",
|
|
941
|
+
before: `<td style="background: linear-gradient(135deg, #667eea, #764ba2);
|
|
942
|
+
padding: 40px; color: #fff;">
|
|
943
|
+
Content here
|
|
944
|
+
</td>`,
|
|
945
|
+
after: `<!-- Always declare a solid background-color before the gradient.
|
|
946
|
+
Clients that strip gradients will show the fallback color. -->
|
|
947
|
+
<td style="background-color: #667eea;
|
|
948
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
949
|
+
padding: 40px; color: #fff;">
|
|
950
|
+
Content here
|
|
951
|
+
</td>`
|
|
952
|
+
},
|
|
953
|
+
"linear-gradient::outlook": {
|
|
954
|
+
language: "html",
|
|
955
|
+
description: "Use VML to render gradients in Outlook",
|
|
956
|
+
before: `<td style="background: linear-gradient(135deg, #667eea, #764ba2);
|
|
957
|
+
padding: 40px; color: #fff;">
|
|
958
|
+
Content here
|
|
959
|
+
</td>`,
|
|
960
|
+
after: `<!--[if gte mso 9]>
|
|
961
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true"
|
|
962
|
+
stroke="false" style="width:600px;">
|
|
963
|
+
<v:fill type="gradient" color="#667eea" color2="#764ba2"
|
|
964
|
+
angle="135" />
|
|
965
|
+
<v:textbox inset="0,0,0,0">
|
|
966
|
+
<![endif]-->
|
|
967
|
+
<td style="background-color: #667eea;
|
|
968
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
969
|
+
padding: 40px; color: #fff;">
|
|
970
|
+
Content here
|
|
971
|
+
</td>
|
|
972
|
+
<!--[if gte mso 9]>
|
|
973
|
+
</v:textbox>
|
|
974
|
+
</v:rect>
|
|
975
|
+
<![endif]-->`
|
|
976
|
+
},
|
|
977
|
+
// ── <style> stripped by Gmail ──────────────────────────────────────────
|
|
978
|
+
"<style>::gmail": {
|
|
979
|
+
language: "html",
|
|
980
|
+
description: "Inline CSS for Gmail compatibility",
|
|
981
|
+
before: `<style>
|
|
982
|
+
.header { background-color: #6d28d9; padding: 32px; }
|
|
983
|
+
.title { color: #fff; font-size: 24px; }
|
|
984
|
+
</style>
|
|
985
|
+
<div class="header">
|
|
986
|
+
<h1 class="title">Hello</h1>
|
|
987
|
+
</div>`,
|
|
988
|
+
after: `<div style="background-color: #6d28d9; padding: 32px;">
|
|
989
|
+
<h1 style="color: #fff; font-size: 24px;
|
|
990
|
+
font-family: Arial, sans-serif; margin: 0;">
|
|
991
|
+
Hello
|
|
992
|
+
</h1>
|
|
993
|
+
</div>`
|
|
994
|
+
},
|
|
995
|
+
// ── <svg> replaced with image ─────────────────────────────────────────
|
|
996
|
+
"<svg>": {
|
|
997
|
+
language: "html",
|
|
998
|
+
description: "Convert inline SVG to an image for email compatibility",
|
|
999
|
+
before: `<svg width="24" height="24" viewBox="0 0 24 24">
|
|
1000
|
+
<path d="M12 2L2 7l10 5 10-5-10-5z" fill="#6d28d9"/>
|
|
1001
|
+
</svg>`,
|
|
1002
|
+
after: `<img src="https://example.com/icon.png"
|
|
1003
|
+
width="24" height="24" alt="Icon"
|
|
1004
|
+
style="display: block; border: 0;" />`
|
|
1005
|
+
},
|
|
1006
|
+
// ── <form> → link to web form ─────────────────────────────────────────
|
|
1007
|
+
"<form>": {
|
|
1008
|
+
language: "html",
|
|
1009
|
+
description: "Replace embedded form with a link to a hosted form",
|
|
1010
|
+
before: `<form action="/subscribe" method="POST">
|
|
1011
|
+
<input type="email" placeholder="Email" />
|
|
1012
|
+
<button type="submit">Subscribe</button>
|
|
1013
|
+
</form>`,
|
|
1014
|
+
after: `<table role="presentation" cellpadding="0" cellspacing="0">
|
|
1015
|
+
<tr>
|
|
1016
|
+
<td style="background-color: #6d28d9; border-radius: 6px;
|
|
1017
|
+
padding: 12px 32px;">
|
|
1018
|
+
<a href="https://example.com/subscribe"
|
|
1019
|
+
style="color: #fff; text-decoration: none;
|
|
1020
|
+
font-family: Arial, sans-serif;
|
|
1021
|
+
font-weight: bold;">
|
|
1022
|
+
Subscribe Now
|
|
1023
|
+
</a>
|
|
1024
|
+
</td>
|
|
1025
|
+
</tr>
|
|
1026
|
+
</table>`
|
|
1027
|
+
},
|
|
1028
|
+
// ── <video> → GIF + play button ───────────────────────────────────────
|
|
1029
|
+
"<video>": {
|
|
1030
|
+
language: "html",
|
|
1031
|
+
description: "Replace video with animated GIF or linked thumbnail",
|
|
1032
|
+
before: `<video width="600" autoplay muted>
|
|
1033
|
+
<source src="demo.mp4" type="video/mp4">
|
|
1034
|
+
</video>`,
|
|
1035
|
+
after: `<a href="https://example.com/watch" target="_blank">
|
|
1036
|
+
<img src="https://example.com/video-thumb.gif"
|
|
1037
|
+
width="600" alt="Watch the video"
|
|
1038
|
+
style="display: block; border: 0; max-width: 100%;" />
|
|
1039
|
+
</a>`
|
|
1040
|
+
},
|
|
1041
|
+
// ── @font-face → web-safe stack ───────────────────────────────────────
|
|
1042
|
+
"@font-face": {
|
|
1043
|
+
language: "css",
|
|
1044
|
+
description: "Add web-safe font fallback stack",
|
|
1045
|
+
before: `@font-face {
|
|
1046
|
+
font-family: 'CustomFont';
|
|
1047
|
+
src: url('custom.woff2') format('woff2');
|
|
1048
|
+
}
|
|
1049
|
+
h1 { font-family: 'CustomFont'; }`,
|
|
1050
|
+
after: `/* Keep @font-face for clients that support it */
|
|
1051
|
+
@font-face {
|
|
1052
|
+
font-family: 'CustomFont';
|
|
1053
|
+
src: url('custom.woff2') format('woff2');
|
|
1054
|
+
}
|
|
1055
|
+
h1 {
|
|
1056
|
+
font-family: 'CustomFont', Arial, Helvetica, sans-serif;
|
|
1057
|
+
}`
|
|
1058
|
+
},
|
|
1059
|
+
// ── @media → mobile-first layout ──────────────────────────────────────
|
|
1060
|
+
"@media": {
|
|
1061
|
+
language: "html",
|
|
1062
|
+
description: "Design mobile-first for clients without media query support",
|
|
1063
|
+
before: `<table width="800">
|
|
1064
|
+
<tr>
|
|
1065
|
+
<td width="400">Left Column</td>
|
|
1066
|
+
<td width="400">Right Column</td>
|
|
1067
|
+
</tr>
|
|
1068
|
+
</table>
|
|
1069
|
+
<style>
|
|
1070
|
+
@media (max-width: 600px) {
|
|
1071
|
+
table { width: 100% !important; }
|
|
1072
|
+
td { display: block !important; width: 100% !important; }
|
|
1073
|
+
}
|
|
1074
|
+
</style>`,
|
|
1075
|
+
after: `<!-- Single-column layout that works without @media -->
|
|
1076
|
+
<table role="presentation" width="100%"
|
|
1077
|
+
style="max-width: 600px;" cellpadding="0"
|
|
1078
|
+
cellspacing="0" border="0">
|
|
1079
|
+
<tr>
|
|
1080
|
+
<td style="padding: 16px;">Left Column</td>
|
|
1081
|
+
</tr>
|
|
1082
|
+
<tr>
|
|
1083
|
+
<td style="padding: 16px;">Right Column</td>
|
|
1084
|
+
</tr>
|
|
1085
|
+
</table>`
|
|
1086
|
+
},
|
|
1087
|
+
// ── box-shadow → border alternative ───────────────────────────────────
|
|
1088
|
+
"box-shadow": {
|
|
1089
|
+
language: "css",
|
|
1090
|
+
description: "Use border as a fallback for box-shadow",
|
|
1091
|
+
before: `.card {
|
|
1092
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
1093
|
+
}`,
|
|
1094
|
+
after: `.card {
|
|
1095
|
+
border: 1px solid #e0e0e0;
|
|
1096
|
+
/* box-shadow as progressive enhancement */
|
|
1097
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
1098
|
+
}`
|
|
1099
|
+
},
|
|
1100
|
+
// ── max-width (Outlook) → fixed width table ───────────────────────────
|
|
1101
|
+
"max-width::outlook": {
|
|
1102
|
+
language: "html",
|
|
1103
|
+
description: "Use a fixed-width table wrapper for Outlook",
|
|
1104
|
+
before: `<div style="max-width: 600px; margin: 0 auto;">
|
|
1105
|
+
Content here
|
|
1106
|
+
</div>`,
|
|
1107
|
+
after: `<!--[if mso]>
|
|
1108
|
+
<table role="presentation" width="600" cellpadding="0"
|
|
1109
|
+
cellspacing="0" border="0" align="center"><tr><td>
|
|
1110
|
+
<![endif]-->
|
|
1111
|
+
<div style="max-width: 600px; margin: 0 auto;">
|
|
1112
|
+
Content here
|
|
1113
|
+
</div>
|
|
1114
|
+
<!--[if mso]>
|
|
1115
|
+
</td></tr></table>
|
|
1116
|
+
<![endif]-->`
|
|
1117
|
+
},
|
|
1118
|
+
// ── float (Outlook) → table align ─────────────────────────────────────
|
|
1119
|
+
"float::outlook": {
|
|
1120
|
+
language: "html",
|
|
1121
|
+
description: "Use table align attribute instead of CSS float",
|
|
1122
|
+
before: `<img src="photo.jpg" style="float: left;
|
|
1123
|
+
margin-right: 16px;" width="200" />
|
|
1124
|
+
<p>Text wraps around the image.</p>`,
|
|
1125
|
+
after: `<table role="presentation" cellpadding="0"
|
|
1126
|
+
cellspacing="0" border="0">
|
|
1127
|
+
<tr>
|
|
1128
|
+
<td width="200" valign="top" style="padding-right: 16px;">
|
|
1129
|
+
<img src="photo.jpg" width="200"
|
|
1130
|
+
style="display: block; border: 0;" />
|
|
1131
|
+
</td>
|
|
1132
|
+
<td valign="top">
|
|
1133
|
+
<p>Text next to the image.</p>
|
|
1134
|
+
</td>
|
|
1135
|
+
</tr>
|
|
1136
|
+
</table>`
|
|
1137
|
+
},
|
|
1138
|
+
// ── gap → padding on children ─────────────────────────────────────────
|
|
1139
|
+
"gap": {
|
|
1140
|
+
language: "html",
|
|
1141
|
+
description: "Use padding or margin on child elements instead of gap",
|
|
1142
|
+
before: `<div style="display: flex; gap: 16px;">
|
|
1143
|
+
<div>Item 1</div>
|
|
1144
|
+
<div>Item 2</div>
|
|
1145
|
+
<div>Item 3</div>
|
|
1146
|
+
</div>`,
|
|
1147
|
+
after: `<table role="presentation" cellpadding="0"
|
|
1148
|
+
cellspacing="0" border="0">
|
|
1149
|
+
<tr>
|
|
1150
|
+
<td style="padding-right: 16px;">Item 1</td>
|
|
1151
|
+
<td style="padding-right: 16px;">Item 2</td>
|
|
1152
|
+
<td>Item 3</td>
|
|
1153
|
+
</tr>
|
|
1154
|
+
</table>`
|
|
1155
|
+
},
|
|
1156
|
+
// ── opacity → solid colors ────────────────────────────────────────────
|
|
1157
|
+
"opacity": {
|
|
1158
|
+
language: "css",
|
|
1159
|
+
description: "Replace opacity with solid color equivalents",
|
|
1160
|
+
before: `.overlay {
|
|
1161
|
+
background-color: #000;
|
|
1162
|
+
opacity: 0.5;
|
|
1163
|
+
}`,
|
|
1164
|
+
after: `.overlay {
|
|
1165
|
+
/* Use a semi-transparent color instead of opacity */
|
|
1166
|
+
background-color: #808080;
|
|
1167
|
+
/* Or for modern clients: rgba(0, 0, 0, 0.5) */
|
|
1168
|
+
}`
|
|
1169
|
+
},
|
|
1170
|
+
// ── position → table layout ───────────────────────────────────────────
|
|
1171
|
+
"position": {
|
|
1172
|
+
language: "html",
|
|
1173
|
+
description: "Use table-based positioning instead of CSS position",
|
|
1174
|
+
before: `<div style="position: relative;">
|
|
1175
|
+
<div style="position: absolute; top: 0; right: 0;">
|
|
1176
|
+
Badge
|
|
1177
|
+
</div>
|
|
1178
|
+
<p>Content</p>
|
|
1179
|
+
</div>`,
|
|
1180
|
+
after: `<table role="presentation" width="100%" cellpadding="0"
|
|
1181
|
+
cellspacing="0" border="0">
|
|
1182
|
+
<tr>
|
|
1183
|
+
<td valign="top">Content</td>
|
|
1184
|
+
<td width="80" valign="top" align="right">Badge</td>
|
|
1185
|
+
</tr>
|
|
1186
|
+
</table>`
|
|
1187
|
+
},
|
|
1188
|
+
// ── box-sizing → nested padding ───────────────────────────────────────
|
|
1189
|
+
"box-sizing": {
|
|
1190
|
+
language: "html",
|
|
1191
|
+
description: "Account for padding in width manually (no box-sizing)",
|
|
1192
|
+
before: `<div style="width: 300px; padding: 20px;
|
|
1193
|
+
box-sizing: border-box;">
|
|
1194
|
+
Content \u2014 total width stays 300px
|
|
1195
|
+
</div>`,
|
|
1196
|
+
after: `<!-- Set width to content-width (300 - 40 = 260px) -->
|
|
1197
|
+
<div style="width: 300px;">
|
|
1198
|
+
<div style="padding: 20px;">
|
|
1199
|
+
Content \u2014 padding on inner element
|
|
1200
|
+
</div>
|
|
1201
|
+
</div>`
|
|
1202
|
+
},
|
|
1203
|
+
// ── <link> → inline styles ────────────────────────────────────────────
|
|
1204
|
+
"<link>": {
|
|
1205
|
+
language: "html",
|
|
1206
|
+
description: "Inline all CSS instead of using external stylesheets",
|
|
1207
|
+
before: `<head>
|
|
1208
|
+
<link rel="stylesheet" href="styles.css" />
|
|
1209
|
+
</head>`,
|
|
1210
|
+
after: `<head>
|
|
1211
|
+
<style>
|
|
1212
|
+
/* Paste your CSS here, or use a build tool like
|
|
1213
|
+
juice/inline-css to inline automatically */
|
|
1214
|
+
.container { max-width: 600px; margin: 0 auto; }
|
|
1215
|
+
.header { background-color: #6d28d9; padding: 32px; }
|
|
1216
|
+
</style>
|
|
1217
|
+
</head>`
|
|
1218
|
+
},
|
|
1219
|
+
// ── dark-mode (Apple Mail) ────────────────────────────────────────────
|
|
1220
|
+
"dark-mode::apple": {
|
|
1221
|
+
language: "css",
|
|
1222
|
+
description: "Add prefers-color-scheme dark mode styles for Apple Mail",
|
|
1223
|
+
before: `<style>
|
|
1224
|
+
.header { background-color: #6d28d9; }
|
|
1225
|
+
.content { background-color: #ffffff; color: #333; }
|
|
1226
|
+
</style>`,
|
|
1227
|
+
after: `<style>
|
|
1228
|
+
.header { background-color: #6d28d9; }
|
|
1229
|
+
.content { background-color: #ffffff; color: #333; }
|
|
1230
|
+
|
|
1231
|
+
@media (prefers-color-scheme: dark) {
|
|
1232
|
+
.content {
|
|
1233
|
+
background-color: #1a1a2e !important;
|
|
1234
|
+
color: #e0e0e0 !important;
|
|
1235
|
+
}
|
|
1236
|
+
/* Force images to stay visible */
|
|
1237
|
+
img { opacity: 1 !important; }
|
|
1238
|
+
}
|
|
1239
|
+
</style>`
|
|
1240
|
+
},
|
|
1241
|
+
// ── object-fit → width/height attributes ──────────────────────────────
|
|
1242
|
+
"object-fit": {
|
|
1243
|
+
language: "html",
|
|
1244
|
+
description: "Use width/height attributes instead of object-fit",
|
|
1245
|
+
before: `<img src="photo.jpg" style="width: 300px; height: 200px;
|
|
1246
|
+
object-fit: cover;" />`,
|
|
1247
|
+
after: `<!-- Crop/resize image server-side to exact dimensions -->
|
|
1248
|
+
<img src="photo-300x200.jpg" width="300" height="200"
|
|
1249
|
+
alt="Photo" style="display: block; border: 0;" />`
|
|
1250
|
+
},
|
|
1251
|
+
// ── transform (not supported) ─────────────────────────────────────────
|
|
1252
|
+
"transform": {
|
|
1253
|
+
language: "html",
|
|
1254
|
+
description: "Pre-render transformed states as images or use table layout",
|
|
1255
|
+
before: `<div style="transform: rotate(45deg);">
|
|
1256
|
+
Rotated content
|
|
1257
|
+
</div>`,
|
|
1258
|
+
after: `<!-- Pre-render rotated content as an image -->
|
|
1259
|
+
<img src="rotated-content.png" width="200" height="200"
|
|
1260
|
+
alt="Rotated content"
|
|
1261
|
+
style="display: block; border: 0;" />`
|
|
1262
|
+
},
|
|
1263
|
+
// ── animation → animated GIF ──────────────────────────────────────────
|
|
1264
|
+
"animation": {
|
|
1265
|
+
language: "html",
|
|
1266
|
+
description: "Replace CSS animation with an animated GIF",
|
|
1267
|
+
before: `<style>
|
|
1268
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
1269
|
+
.badge { animation: pulse 2s infinite; }
|
|
1270
|
+
</style>
|
|
1271
|
+
<span class="badge">New!</span>`,
|
|
1272
|
+
after: `<!-- Use an animated GIF for the effect -->
|
|
1273
|
+
<img src="https://example.com/badge-animated.gif"
|
|
1274
|
+
width="60" height="24" alt="New!"
|
|
1275
|
+
style="display: inline-block; border: 0;" />`
|
|
1276
|
+
},
|
|
1277
|
+
// ── transition (not supported) ────────────────────────────────────────
|
|
1278
|
+
"transition": {
|
|
1279
|
+
language: "css",
|
|
1280
|
+
description: "Transitions don't work in email \u2014 style the default state well",
|
|
1281
|
+
before: `.button {
|
|
1282
|
+
background-color: #6d28d9;
|
|
1283
|
+
transition: background-color 0.2s;
|
|
1284
|
+
}
|
|
1285
|
+
.button:hover {
|
|
1286
|
+
background-color: #5b21b6;
|
|
1287
|
+
}`,
|
|
1288
|
+
after: `.button {
|
|
1289
|
+
/* Use the most visually appealing state as default.
|
|
1290
|
+
:hover is only supported in a few clients. */
|
|
1291
|
+
background-color: #6d28d9;
|
|
1292
|
+
color: #ffffff;
|
|
1293
|
+
text-decoration: none;
|
|
1294
|
+
font-weight: bold;
|
|
1295
|
+
}`
|
|
1296
|
+
},
|
|
1297
|
+
// ── background-size (Outlook) → VML ───────────────────────────────────
|
|
1298
|
+
"background-size": {
|
|
1299
|
+
language: "html",
|
|
1300
|
+
description: "Outlook ignores background-size \u2014 use VML or sized images",
|
|
1301
|
+
before: `<td style="background: url('bg.jpg') center/cover no-repeat;">
|
|
1302
|
+
Content
|
|
1303
|
+
</td>`,
|
|
1304
|
+
after: `<!--[if gte mso 9]>
|
|
1305
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true"
|
|
1306
|
+
stroke="false" style="width:600px; height:400px;">
|
|
1307
|
+
<v:fill type="frame" src="bg.jpg" />
|
|
1308
|
+
<v:textbox inset="0,0,0,0">
|
|
1309
|
+
<![endif]-->
|
|
1310
|
+
<td style="background: url('bg.jpg') center/cover no-repeat;
|
|
1311
|
+
background-color: #333;">
|
|
1312
|
+
Content
|
|
1313
|
+
</td>
|
|
1314
|
+
<!--[if gte mso 9]>
|
|
1315
|
+
</v:textbox>
|
|
1316
|
+
</v:rect>
|
|
1317
|
+
<![endif]-->`
|
|
1318
|
+
},
|
|
1319
|
+
// ── overflow (Gmail strips it) ────────────────────────────────────────
|
|
1320
|
+
"overflow": {
|
|
1321
|
+
language: "html",
|
|
1322
|
+
description: "Content will always be visible \u2014 design for full content display",
|
|
1323
|
+
before: `<div style="max-height: 200px; overflow: hidden;">
|
|
1324
|
+
Long content that gets clipped...
|
|
1325
|
+
</div>`,
|
|
1326
|
+
after: `<!-- Show full content, or truncate server-side -->
|
|
1327
|
+
<div style="max-height: 200px;">
|
|
1328
|
+
Shortened content that fits...
|
|
1329
|
+
<a href="https://example.com/full">Read more</a>
|
|
1330
|
+
</div>`
|
|
1331
|
+
},
|
|
1332
|
+
// ── visibility (Gmail strips it) ──────────────────────────────────────
|
|
1333
|
+
"visibility": {
|
|
1334
|
+
language: "html",
|
|
1335
|
+
description: "Use conditional comments or remove hidden content",
|
|
1336
|
+
before: `<div style="visibility: hidden;">
|
|
1337
|
+
Hidden content for screen readers
|
|
1338
|
+
</div>`,
|
|
1339
|
+
after: `<!-- For screen readers, use font-size: 0 trick -->
|
|
1340
|
+
<div style="font-size: 0; max-height: 0; overflow: hidden;
|
|
1341
|
+
mso-hide: all;" aria-hidden="true">
|
|
1342
|
+
Preheader text
|
|
1343
|
+
</div>`
|
|
1344
|
+
},
|
|
1345
|
+
// ── REACT EMAIL (jsx) framework-specific fixes ────────────────────────────
|
|
1346
|
+
"display:flex::outlook::jsx": {
|
|
1347
|
+
language: "jsx",
|
|
1348
|
+
description: "Use React Email Row + Column components instead of flexbox (Outlook-safe)",
|
|
1349
|
+
before: `<div style={{ display: "flex", gap: "16px" }}>
|
|
1350
|
+
<div style={{ flex: 1 }}>Column 1</div>
|
|
1351
|
+
<div style={{ flex: 1 }}>Column 2</div>
|
|
1352
|
+
</div>`,
|
|
1353
|
+
after: `import { Row, Column } from "@react-email/components";
|
|
1354
|
+
|
|
1355
|
+
<Row>
|
|
1356
|
+
<Column style={{ width: "50%", verticalAlign: "top" }}>
|
|
1357
|
+
Column 1
|
|
1358
|
+
</Column>
|
|
1359
|
+
<Column style={{ width: "50%", verticalAlign: "top" }}>
|
|
1360
|
+
Column 2
|
|
1361
|
+
</Column>
|
|
1362
|
+
</Row>`
|
|
1363
|
+
},
|
|
1364
|
+
"display:grid::jsx": {
|
|
1365
|
+
language: "jsx",
|
|
1366
|
+
description: "Replace CSS Grid with React Email Row + Column components",
|
|
1367
|
+
before: `<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "16px" }}>
|
|
1368
|
+
<div>Item 1</div>
|
|
1369
|
+
<div>Item 2</div>
|
|
1370
|
+
</div>`,
|
|
1371
|
+
after: `import { Row, Column } from "@react-email/components";
|
|
1372
|
+
|
|
1373
|
+
<Row>
|
|
1374
|
+
<Column style={{ width: "50%", padding: "8px", verticalAlign: "top" }}>
|
|
1375
|
+
Item 1
|
|
1376
|
+
</Column>
|
|
1377
|
+
<Column style={{ width: "50%", padding: "8px", verticalAlign: "top" }}>
|
|
1378
|
+
Item 2
|
|
1379
|
+
</Column>
|
|
1380
|
+
</Row>`
|
|
1381
|
+
},
|
|
1382
|
+
"max-width::outlook::jsx": {
|
|
1383
|
+
language: "jsx",
|
|
1384
|
+
description: "Use React Email Container component for Outlook-safe max-width centering",
|
|
1385
|
+
before: `<div style={{ maxWidth: "600px", margin: "0 auto" }}>
|
|
1386
|
+
Content here
|
|
1387
|
+
</div>`,
|
|
1388
|
+
after: `import { Container } from "@react-email/components";
|
|
1389
|
+
|
|
1390
|
+
<Container style={{ maxWidth: "600px" }}>
|
|
1391
|
+
Content here
|
|
1392
|
+
</Container>`
|
|
1393
|
+
},
|
|
1394
|
+
"@font-face::jsx": {
|
|
1395
|
+
language: "jsx",
|
|
1396
|
+
description: "Use React Email Font component instead of @font-face in <style>",
|
|
1397
|
+
before: `import { Head } from "@react-email/components";
|
|
1398
|
+
|
|
1399
|
+
<Head>
|
|
1400
|
+
<style>{\`
|
|
1401
|
+
@font-face {
|
|
1402
|
+
font-family: 'CustomFont';
|
|
1403
|
+
src: url('custom.woff2') format('woff2');
|
|
1404
|
+
}
|
|
1405
|
+
\`}</style>
|
|
1406
|
+
</Head>
|
|
1407
|
+
<h1 style={{ fontFamily: "'CustomFont'" }}>Hello</h1>`,
|
|
1408
|
+
after: `import { Head, Font } from "@react-email/components";
|
|
1409
|
+
|
|
1410
|
+
<Head>
|
|
1411
|
+
<Font
|
|
1412
|
+
fontFamily="CustomFont"
|
|
1413
|
+
fallbackFontFamily="Arial"
|
|
1414
|
+
webFont={{ url: "https://example.com/custom.woff2", format: "woff2" }}
|
|
1415
|
+
fontWeight={400}
|
|
1416
|
+
fontStyle="normal"
|
|
1417
|
+
/>
|
|
1418
|
+
</Head>
|
|
1419
|
+
<h1 style={{ fontFamily: "'CustomFont', Arial, sans-serif" }}>Hello</h1>`
|
|
1420
|
+
},
|
|
1421
|
+
"<svg>::jsx": {
|
|
1422
|
+
language: "jsx",
|
|
1423
|
+
description: "Replace inline SVG with React Email Img component",
|
|
1424
|
+
before: `<svg width="24" height="24" viewBox="0 0 24 24">
|
|
1425
|
+
<path d="M12 2L2 7l10 5 10-5-10-5z" fill="#6d28d9"/>
|
|
1426
|
+
</svg>`,
|
|
1427
|
+
after: `import { Img } from "@react-email/components";
|
|
1428
|
+
|
|
1429
|
+
<Img
|
|
1430
|
+
src="https://example.com/icon.png"
|
|
1431
|
+
width={24}
|
|
1432
|
+
height={24}
|
|
1433
|
+
alt="Icon"
|
|
1434
|
+
style={{ display: "block", border: "0" }}
|
|
1435
|
+
/>`
|
|
1436
|
+
},
|
|
1437
|
+
"<video>::jsx": {
|
|
1438
|
+
language: "jsx",
|
|
1439
|
+
description: "Replace video with a linked thumbnail using React Email Img + Link",
|
|
1440
|
+
before: `<video width="600" autoPlay muted>
|
|
1441
|
+
<source src="demo.mp4" type="video/mp4" />
|
|
1442
|
+
</video>`,
|
|
1443
|
+
after: `import { Img, Link } from "@react-email/components";
|
|
1444
|
+
|
|
1445
|
+
<Link href="https://example.com/watch">
|
|
1446
|
+
<Img
|
|
1447
|
+
src="https://example.com/video-thumb.gif"
|
|
1448
|
+
width={600}
|
|
1449
|
+
alt="Watch the video"
|
|
1450
|
+
style={{ display: "block", border: "0", maxWidth: "100%" }}
|
|
1451
|
+
/>
|
|
1452
|
+
</Link>`
|
|
1453
|
+
},
|
|
1454
|
+
"border-radius::outlook::jsx": {
|
|
1455
|
+
language: "jsx",
|
|
1456
|
+
description: "Render rounded buttons with VML via JSX dangerouslySetInnerHTML (Outlook workaround)",
|
|
1457
|
+
before: `<a
|
|
1458
|
+
href="https://example.com"
|
|
1459
|
+
style={{ backgroundColor: "#6d28d9", color: "#fff",
|
|
1460
|
+
padding: "12px 32px", borderRadius: "6px",
|
|
1461
|
+
textDecoration: "none", display: "inline-block" }}>
|
|
1462
|
+
Click Here
|
|
1463
|
+
</a>`,
|
|
1464
|
+
after: `{/* Use dangerouslySetInnerHTML to inject VML for Outlook rounded corners */}
|
|
1465
|
+
<div
|
|
1466
|
+
dangerouslySetInnerHTML={{
|
|
1467
|
+
__html: \`
|
|
1468
|
+
<!--[if mso]>
|
|
1469
|
+
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
1470
|
+
href="https://example.com"
|
|
1471
|
+
style="height:44px; v-text-anchor:middle; width:200px;"
|
|
1472
|
+
arcsize="14%" strokecolor="#6d28d9" fillcolor="#6d28d9">
|
|
1473
|
+
<w:anchorlock/>
|
|
1474
|
+
<center style="color:#fff; font-family:Arial,sans-serif;
|
|
1475
|
+
font-size:14px; font-weight:bold;">Click Here</center>
|
|
1476
|
+
</v:roundrect>
|
|
1477
|
+
<![endif]-->
|
|
1478
|
+
<!--[if !mso]><!-->
|
|
1479
|
+
<a href="https://example.com"
|
|
1480
|
+
style="background-color:#6d28d9; color:#fff; padding:12px 32px;
|
|
1481
|
+
border-radius:6px; text-decoration:none; display:inline-block;">
|
|
1482
|
+
Click Here
|
|
1483
|
+
</a>
|
|
1484
|
+
<!--<![endif]-->
|
|
1485
|
+
\`,
|
|
1486
|
+
}}
|
|
1487
|
+
/>`
|
|
1488
|
+
},
|
|
1489
|
+
"gap::jsx": {
|
|
1490
|
+
language: "jsx",
|
|
1491
|
+
description: "Use padding style prop on Column instead of gap (email-safe spacing)",
|
|
1492
|
+
before: `<Row style={{ gap: "16px" }}>
|
|
1493
|
+
<Column>Item 1</Column>
|
|
1494
|
+
<Column>Item 2</Column>
|
|
1495
|
+
<Column>Item 3</Column>
|
|
1496
|
+
</Row>`,
|
|
1497
|
+
after: `import { Row, Column } from "@react-email/components";
|
|
1498
|
+
|
|
1499
|
+
{/* Use padding on Column \u2014 gap is not supported in email clients */}
|
|
1500
|
+
<Row>
|
|
1501
|
+
<Column style={{ paddingRight: "16px" }}>Item 1</Column>
|
|
1502
|
+
<Column style={{ paddingRight: "16px" }}>Item 2</Column>
|
|
1503
|
+
<Column>Item 3</Column>
|
|
1504
|
+
</Row>`
|
|
1505
|
+
},
|
|
1506
|
+
"<style>::gmail::jsx": {
|
|
1507
|
+
language: "jsx",
|
|
1508
|
+
description: "React Email inlines styles via style props \u2014 manual <style> blocks won't survive Gmail",
|
|
1509
|
+
before: `import { Head } from "@react-email/components";
|
|
1510
|
+
|
|
1511
|
+
<Head>
|
|
1512
|
+
<style>{\`
|
|
1513
|
+
.header { background-color: #6d28d9; padding: 32px; }
|
|
1514
|
+
.title { color: #fff; font-size: 24px; }
|
|
1515
|
+
\`}</style>
|
|
1516
|
+
</Head>
|
|
1517
|
+
<div className="header">
|
|
1518
|
+
<h1 className="title">Hello</h1>
|
|
1519
|
+
</div>`,
|
|
1520
|
+
after: `{/* Gmail strips <style> blocks. Move styles to inline style props: */}
|
|
1521
|
+
<div style={{ backgroundColor: "#6d28d9", padding: "32px" }}>
|
|
1522
|
+
<h1 style={{ color: "#fff", fontSize: "24px", margin: 0 }}>Hello</h1>
|
|
1523
|
+
</div>`
|
|
1524
|
+
},
|
|
1525
|
+
"<link>::jsx": {
|
|
1526
|
+
language: "jsx",
|
|
1527
|
+
description: "Use React Email Head component instead of <link> for stylesheet references",
|
|
1528
|
+
before: `import { Head } from "@react-email/components";
|
|
1529
|
+
|
|
1530
|
+
<Head>
|
|
1531
|
+
<link rel="stylesheet" href="styles.css" />
|
|
1532
|
+
</Head>`,
|
|
1533
|
+
after: `import { Head } from "@react-email/components";
|
|
1534
|
+
|
|
1535
|
+
{/* External stylesheets are stripped by most email clients.
|
|
1536
|
+
Place styles inline via style props, or use Head for font imports only. */}
|
|
1537
|
+
<Head>
|
|
1538
|
+
{/* Inline your CSS here or use Font component for web fonts */}
|
|
1539
|
+
</Head>`
|
|
1540
|
+
},
|
|
1541
|
+
// ── MJML framework-specific fixes ─────────────────────────────────────────
|
|
1542
|
+
"@font-face::mjml": {
|
|
1543
|
+
language: "mjml",
|
|
1544
|
+
description: "Use mj-font in mj-head instead of @font-face",
|
|
1545
|
+
before: `<mj-style>
|
|
1546
|
+
@font-face {
|
|
1547
|
+
font-family: 'CustomFont';
|
|
1548
|
+
src: url('https://example.com/custom.woff2') format('woff2');
|
|
1549
|
+
}
|
|
1550
|
+
</mj-style>`,
|
|
1551
|
+
after: `<mjml>
|
|
1552
|
+
<mj-head>
|
|
1553
|
+
<mj-font name="CustomFont"
|
|
1554
|
+
href="https://fonts.googleapis.com/css2?family=CustomFont" />
|
|
1555
|
+
<mj-attributes>
|
|
1556
|
+
<mj-all font-family="CustomFont, Arial, Helvetica, sans-serif" />
|
|
1557
|
+
</mj-attributes>
|
|
1558
|
+
</mj-head>
|
|
1559
|
+
</mjml>`
|
|
1560
|
+
},
|
|
1561
|
+
"<style>::gmail::mjml": {
|
|
1562
|
+
language: "mjml",
|
|
1563
|
+
description: "Use mj-style inline='inline' to force style inlining for Gmail",
|
|
1564
|
+
before: `<mj-head>
|
|
1565
|
+
<mj-style>
|
|
1566
|
+
.custom { color: #6d28d9; }
|
|
1567
|
+
</mj-style>
|
|
1568
|
+
</mj-head>`,
|
|
1569
|
+
after: `<mj-head>
|
|
1570
|
+
<!-- Use inline="inline" to force MJML to inline these styles.
|
|
1571
|
+
Class-based styles in a plain mj-style block will be stripped by Gmail. -->
|
|
1572
|
+
<mj-style inline="inline">
|
|
1573
|
+
.custom { color: #6d28d9; }
|
|
1574
|
+
</mj-style>
|
|
1575
|
+
</mj-head>`
|
|
1576
|
+
},
|
|
1577
|
+
"border-radius::outlook::mjml": {
|
|
1578
|
+
language: "mjml",
|
|
1579
|
+
description: "MJML limitation: border-radius is unsupported in Outlook \u2014 MJML does not generate VML",
|
|
1580
|
+
before: `<mj-button border-radius="6px" background-color="#6d28d9">
|
|
1581
|
+
Click Here
|
|
1582
|
+
</mj-button>`,
|
|
1583
|
+
after: `<!-- Known MJML limitation: MJML does not generate VML for rounded corners.
|
|
1584
|
+
Options: accept flat corners, use mj-raw for VML, or set border-radius="0". -->
|
|
1585
|
+
<mj-button border-radius="0" background-color="#6d28d9">
|
|
1586
|
+
Click Here
|
|
1587
|
+
</mj-button>`
|
|
1588
|
+
},
|
|
1589
|
+
"background-image::outlook::mjml": {
|
|
1590
|
+
language: "mjml",
|
|
1591
|
+
description: "Use mj-section background-url for Outlook-compatible background images",
|
|
1592
|
+
before: `<mj-section>
|
|
1593
|
+
<mj-column>
|
|
1594
|
+
<mj-image src="hero.jpg" />
|
|
1595
|
+
</mj-column>
|
|
1596
|
+
</mj-section>`,
|
|
1597
|
+
after: `<!-- MJML generates VML-compatible markup automatically via background-url on mj-section. -->
|
|
1598
|
+
<mj-section background-url="https://example.com/hero.jpg"
|
|
1599
|
+
background-size="cover"
|
|
1600
|
+
background-repeat="no-repeat"
|
|
1601
|
+
background-color="#333333">
|
|
1602
|
+
<mj-column>
|
|
1603
|
+
<mj-text color="#ffffff">Your content here</mj-text>
|
|
1604
|
+
</mj-column>
|
|
1605
|
+
</mj-section>`
|
|
1606
|
+
},
|
|
1607
|
+
"display:flex::mjml": {
|
|
1608
|
+
language: "mjml",
|
|
1609
|
+
description: "Replace flexbox (from mj-raw or inline styles) with mj-section and mj-column",
|
|
1610
|
+
before: `<mj-raw>
|
|
1611
|
+
<div style="display: flex; gap: 16px;">
|
|
1612
|
+
<div style="flex: 1;">Column 1</div>
|
|
1613
|
+
<div style="flex: 1;">Column 2</div>
|
|
1614
|
+
</div>
|
|
1615
|
+
</mj-raw>`,
|
|
1616
|
+
after: `<!-- Flexbox in MJML is not Outlook-compatible.
|
|
1617
|
+
Use mj-section and mj-column \u2014 MJML compiles these to table-based layouts. -->
|
|
1618
|
+
<mj-section>
|
|
1619
|
+
<mj-column width="50%">
|
|
1620
|
+
<mj-text>Column 1</mj-text>
|
|
1621
|
+
</mj-column>
|
|
1622
|
+
<mj-column width="50%">
|
|
1623
|
+
<mj-text>Column 2</mj-text>
|
|
1624
|
+
</mj-column>
|
|
1625
|
+
</mj-section>`
|
|
1626
|
+
},
|
|
1627
|
+
"@media::mjml": {
|
|
1628
|
+
language: "mjml",
|
|
1629
|
+
description: "Use MJML responsive attributes and breakpoints instead of hand-written @media",
|
|
1630
|
+
before: `<mj-style>
|
|
1631
|
+
@media (max-width: 600px) {
|
|
1632
|
+
.mobile-stack { display: block !important; width: 100% !important; }
|
|
1633
|
+
}
|
|
1634
|
+
</mj-style>`,
|
|
1635
|
+
after: `<!-- MJML generates responsive @media queries automatically.
|
|
1636
|
+
Use mj-breakpoint and mj-column widths to control responsive behavior. -->
|
|
1637
|
+
<mj-head>
|
|
1638
|
+
<mj-breakpoint width="600px" />
|
|
1639
|
+
</mj-head>
|
|
1640
|
+
<mj-body>
|
|
1641
|
+
<mj-section>
|
|
1642
|
+
<mj-column width="50%"><mj-text>Left</mj-text></mj-column>
|
|
1643
|
+
<mj-column width="50%"><mj-text>Right</mj-text></mj-column>
|
|
1644
|
+
</mj-section>
|
|
1645
|
+
</mj-body>`
|
|
1646
|
+
},
|
|
1647
|
+
// ── MAIZZLE framework-specific fixes ──────────────────────────────────────
|
|
1648
|
+
"display:flex::outlook::maizzle": {
|
|
1649
|
+
language: "maizzle",
|
|
1650
|
+
description: "Replace Tailwind flex classes with HTML table + MSO conditional comments",
|
|
1651
|
+
before: `<div class="flex gap-4">
|
|
1652
|
+
<div class="flex-1">Column 1</div>
|
|
1653
|
+
<div class="flex-1">Column 2</div>
|
|
1654
|
+
</div>`,
|
|
1655
|
+
after: `<!--[if mso]>
|
|
1656
|
+
<table role="presentation" width="100%" cellpadding="0"
|
|
1657
|
+
cellspacing="0" border="0"><tr>
|
|
1658
|
+
<td class="w-1/2" valign="top">Column 1</td>
|
|
1659
|
+
<td class="w-1/2" valign="top">Column 2</td>
|
|
1660
|
+
</tr></table>
|
|
1661
|
+
<![endif]-->
|
|
1662
|
+
<!--[if !mso]><!-->
|
|
1663
|
+
<div class="flex gap-4">
|
|
1664
|
+
<div class="flex-1">Column 1</div>
|
|
1665
|
+
<div class="flex-1">Column 2</div>
|
|
1666
|
+
</div>
|
|
1667
|
+
<!--<![endif]-->`
|
|
1668
|
+
},
|
|
1669
|
+
"@font-face::maizzle": {
|
|
1670
|
+
language: "maizzle",
|
|
1671
|
+
description: 'Add fonts via the googleFonts key in config.js \u2014 Maizzle injects the Google Fonts link tag automatically. Set googleFonts: "Inter:ital,wght@0,400;0,700" in your environment config, then reference the font family in your template.',
|
|
1672
|
+
before: `<style>
|
|
1673
|
+
@font-face {
|
|
1674
|
+
font-family: 'Inter';
|
|
1675
|
+
src: url('https://fonts.gstatic.com/...') format('woff2');
|
|
1676
|
+
}
|
|
1677
|
+
</style>`,
|
|
1678
|
+
after: `<!-- config.js: googleFonts: "Inter:ital,wght@0,400;0,700" -->
|
|
1679
|
+
<p class="font-['Inter',Arial,sans-serif]">Hello</p>`
|
|
1680
|
+
},
|
|
1681
|
+
"<style>::gmail::maizzle": {
|
|
1682
|
+
language: "maizzle",
|
|
1683
|
+
description: "Maizzle automatically inlines CSS via juice during build (inlineCSS: true in config.js). Manual <style> blocks bypass juice and will be stripped by Gmail \u2014 prefer Tailwind utility classes instead.",
|
|
1684
|
+
before: `<style>
|
|
1685
|
+
.custom { color: #6d28d9; }
|
|
1686
|
+
</style>
|
|
1687
|
+
<div class="custom">Hello</div>`,
|
|
1688
|
+
after: `<!-- Prefer Tailwind classes \u2014 Maizzle inlines them automatically during build -->
|
|
1689
|
+
<div class="text-[#6d28d9]">Hello</div>`
|
|
1690
|
+
},
|
|
1691
|
+
"max-width::outlook::maizzle": {
|
|
1692
|
+
language: "maizzle",
|
|
1693
|
+
description: "Wrap max-width containers with MSO conditional table for Outlook",
|
|
1694
|
+
before: `<div class="max-w-[600px] mx-auto">
|
|
1695
|
+
Content here
|
|
1696
|
+
</div>`,
|
|
1697
|
+
after: `<!--[if mso]>
|
|
1698
|
+
<table role="presentation" width="600" cellpadding="0"
|
|
1699
|
+
cellspacing="0" border="0" align="center"><tr><td>
|
|
1700
|
+
<![endif]-->
|
|
1701
|
+
<div class="max-w-[600px] mx-auto">
|
|
1702
|
+
Content here
|
|
1703
|
+
</div>
|
|
1704
|
+
<!--[if mso]>
|
|
1705
|
+
</td></tr></table>
|
|
1706
|
+
<![endif]-->`
|
|
1707
|
+
},
|
|
1708
|
+
"gap::maizzle": {
|
|
1709
|
+
language: "maizzle",
|
|
1710
|
+
description: "Use padding Tailwind classes on child elements instead of gap",
|
|
1711
|
+
before: `<div class="flex gap-4">
|
|
1712
|
+
<div>Item 1</div>
|
|
1713
|
+
<div>Item 2</div>
|
|
1714
|
+
<div>Item 3</div>
|
|
1715
|
+
</div>`,
|
|
1716
|
+
after: `<!-- gap is not supported in Outlook or many email clients.
|
|
1717
|
+
Use padding classes on child elements instead. -->
|
|
1718
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
|
1719
|
+
<tr>
|
|
1720
|
+
<td class="pr-4">Item 1</td>
|
|
1721
|
+
<td class="pr-4">Item 2</td>
|
|
1722
|
+
<td>Item 3</td>
|
|
1723
|
+
</tr>
|
|
1724
|
+
</table>`
|
|
1725
|
+
},
|
|
1726
|
+
// ── JSX fixes for remaining common properties ─────────────────────────────
|
|
1727
|
+
"<form>::jsx": {
|
|
1728
|
+
language: "jsx",
|
|
1729
|
+
description: "Replace embedded form with a React Email Button linking to a hosted form",
|
|
1730
|
+
before: `<form action="/subscribe" method="POST">
|
|
1731
|
+
<input type="email" placeholder="Email" />
|
|
1732
|
+
<button type="submit">Subscribe</button>
|
|
1733
|
+
</form>`,
|
|
1734
|
+
after: `import { Button } from "@react-email/components";
|
|
1735
|
+
|
|
1736
|
+
<Button
|
|
1737
|
+
href="https://example.com/subscribe"
|
|
1738
|
+
style={{
|
|
1739
|
+
backgroundColor: "#6d28d9",
|
|
1740
|
+
color: "#fff",
|
|
1741
|
+
padding: "12px 32px",
|
|
1742
|
+
borderRadius: "6px",
|
|
1743
|
+
textDecoration: "none",
|
|
1744
|
+
fontWeight: "bold",
|
|
1745
|
+
}}
|
|
1746
|
+
>
|
|
1747
|
+
Subscribe Now
|
|
1748
|
+
</Button>`
|
|
1749
|
+
},
|
|
1750
|
+
"@media::jsx": {
|
|
1751
|
+
language: "jsx",
|
|
1752
|
+
description: "Design mobile-first \u2014 @media queries are stripped by many clients",
|
|
1753
|
+
before: `import { Head } from "@react-email/components";
|
|
1754
|
+
|
|
1755
|
+
<Head>
|
|
1756
|
+
<style>{\`
|
|
1757
|
+
@media (max-width: 600px) {
|
|
1758
|
+
.cols { display: block !important; }
|
|
1759
|
+
.col { width: 100% !important; }
|
|
1760
|
+
}
|
|
1761
|
+
\`}</style>
|
|
1762
|
+
</Head>
|
|
1763
|
+
<Row>
|
|
1764
|
+
<Column className="col" style={{ width: "50%" }}>Left</Column>
|
|
1765
|
+
<Column className="col" style={{ width: "50%" }}>Right</Column>
|
|
1766
|
+
</Row>`,
|
|
1767
|
+
after: `import { Container, Section, Text } from "@react-email/components";
|
|
1768
|
+
|
|
1769
|
+
{/* Single-column stacked layout works without @media.
|
|
1770
|
+
Stack content vertically so it reads well on all clients. */}
|
|
1771
|
+
<Container style={{ maxWidth: "600px" }}>
|
|
1772
|
+
<Section style={{ padding: "16px" }}>
|
|
1773
|
+
<Text>Left</Text>
|
|
1774
|
+
</Section>
|
|
1775
|
+
<Section style={{ padding: "16px" }}>
|
|
1776
|
+
<Text>Right</Text>
|
|
1777
|
+
</Section>
|
|
1778
|
+
</Container>`
|
|
1779
|
+
},
|
|
1780
|
+
"position::jsx": {
|
|
1781
|
+
language: "jsx",
|
|
1782
|
+
description: "Use React Email Row and Column for layout instead of CSS position",
|
|
1783
|
+
before: `<div style={{ position: "relative" }}>
|
|
1784
|
+
<div style={{ position: "absolute", top: 0, right: 0 }}>
|
|
1785
|
+
Badge
|
|
1786
|
+
</div>
|
|
1787
|
+
<p>Content</p>
|
|
1788
|
+
</div>`,
|
|
1789
|
+
after: `import { Row, Column, Text } from "@react-email/components";
|
|
1790
|
+
|
|
1791
|
+
<Row>
|
|
1792
|
+
<Column style={{ verticalAlign: "top" }}>
|
|
1793
|
+
<Text>Content</Text>
|
|
1794
|
+
</Column>
|
|
1795
|
+
<Column style={{ width: "80px", verticalAlign: "top", textAlign: "right" }}>
|
|
1796
|
+
<Text>Badge</Text>
|
|
1797
|
+
</Column>
|
|
1798
|
+
</Row>`
|
|
1799
|
+
},
|
|
1800
|
+
"float::jsx": {
|
|
1801
|
+
language: "jsx",
|
|
1802
|
+
description: "Use React Email Row and Column instead of float for side-by-side layout",
|
|
1803
|
+
before: `<img src="photo.jpg" style={{ float: "left", marginRight: "16px" }} width={200} />
|
|
1804
|
+
<p>Text wraps around the image.</p>`,
|
|
1805
|
+
after: `import { Row, Column, Img, Text } from "@react-email/components";
|
|
1806
|
+
|
|
1807
|
+
<Row>
|
|
1808
|
+
<Column style={{ width: "200px", paddingRight: "16px", verticalAlign: "top" }}>
|
|
1809
|
+
<Img src="photo.jpg" width={200} style={{ display: "block", border: "0" }} />
|
|
1810
|
+
</Column>
|
|
1811
|
+
<Column style={{ verticalAlign: "top" }}>
|
|
1812
|
+
<Text>Text next to the image.</Text>
|
|
1813
|
+
</Column>
|
|
1814
|
+
</Row>`
|
|
1815
|
+
},
|
|
1816
|
+
"background-image::outlook::jsx": {
|
|
1817
|
+
language: "jsx",
|
|
1818
|
+
description: "Use VML for Outlook background images in JSX via dangerouslySetInnerHTML",
|
|
1819
|
+
before: `<td style={{ backgroundImage: "url('hero.jpg')",
|
|
1820
|
+
backgroundSize: "cover", padding: "40px" }}>
|
|
1821
|
+
<h1 style={{ color: "#fff" }}>Hello World</h1>
|
|
1822
|
+
</td>`,
|
|
1823
|
+
after: `<div
|
|
1824
|
+
dangerouslySetInnerHTML={{
|
|
1825
|
+
__html: \`
|
|
1826
|
+
<!--[if gte mso 9]>
|
|
1827
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true"
|
|
1828
|
+
stroke="false" style="width:600px; height:300px;">
|
|
1829
|
+
<v:fill type="frame" src="hero.jpg" />
|
|
1830
|
+
<v:textbox inset="0,0,0,0">
|
|
1831
|
+
<![endif]-->
|
|
1832
|
+
<div style="background-image:url('hero.jpg'); background-size:cover; padding:40px;">
|
|
1833
|
+
<h1 style="color:#fff;">Hello World</h1>
|
|
1834
|
+
</div>
|
|
1835
|
+
<!--[if gte mso 9]>
|
|
1836
|
+
</v:textbox>
|
|
1837
|
+
</v:rect>
|
|
1838
|
+
<![endif]-->
|
|
1839
|
+
\`,
|
|
1840
|
+
}}
|
|
1841
|
+
/>`
|
|
1842
|
+
},
|
|
1843
|
+
"opacity::jsx": {
|
|
1844
|
+
language: "jsx",
|
|
1845
|
+
description: "Use solid colors instead of opacity in style objects",
|
|
1846
|
+
before: `<div style={{
|
|
1847
|
+
backgroundColor: "#000",
|
|
1848
|
+
opacity: 0.5,
|
|
1849
|
+
}}>
|
|
1850
|
+
Overlay content
|
|
1851
|
+
</div>`,
|
|
1852
|
+
after: `<div style={{
|
|
1853
|
+
/* Use a pre-mixed solid color instead of opacity */
|
|
1854
|
+
backgroundColor: "#808080",
|
|
1855
|
+
}}>
|
|
1856
|
+
Overlay content
|
|
1857
|
+
</div>`
|
|
1858
|
+
},
|
|
1859
|
+
"box-shadow::jsx": {
|
|
1860
|
+
language: "jsx",
|
|
1861
|
+
description: "Use border as a fallback for boxShadow in style objects",
|
|
1862
|
+
before: `<div style={{
|
|
1863
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
|
|
1864
|
+
padding: "24px",
|
|
1865
|
+
}}>
|
|
1866
|
+
Card content
|
|
1867
|
+
</div>`,
|
|
1868
|
+
after: `import { Section } from "@react-email/components";
|
|
1869
|
+
|
|
1870
|
+
<Section style={{
|
|
1871
|
+
border: "1px solid #e0e0e0",
|
|
1872
|
+
padding: "24px",
|
|
1873
|
+
}}>
|
|
1874
|
+
Card content
|
|
1875
|
+
</Section>`
|
|
1876
|
+
},
|
|
1877
|
+
"linear-gradient::jsx": {
|
|
1878
|
+
language: "jsx",
|
|
1879
|
+
description: "Add a solid backgroundColor fallback before the gradient",
|
|
1880
|
+
before: `<div style={{
|
|
1881
|
+
background: "linear-gradient(135deg, #667eea, #764ba2)",
|
|
1882
|
+
padding: "40px",
|
|
1883
|
+
color: "#fff",
|
|
1884
|
+
}}>
|
|
1885
|
+
Content here
|
|
1886
|
+
</div>`,
|
|
1887
|
+
after: `<div style={{
|
|
1888
|
+
/* Solid fallback for clients that strip gradients */
|
|
1889
|
+
backgroundColor: "#667eea",
|
|
1890
|
+
background: "linear-gradient(135deg, #667eea, #764ba2)",
|
|
1891
|
+
padding: "40px",
|
|
1892
|
+
color: "#fff",
|
|
1893
|
+
}}>
|
|
1894
|
+
Content here
|
|
1895
|
+
</div>`
|
|
1896
|
+
},
|
|
1897
|
+
"transform::jsx": {
|
|
1898
|
+
language: "jsx",
|
|
1899
|
+
description: "Pre-render transformed content as an image using React Email Img",
|
|
1900
|
+
before: `<div style={{ transform: "rotate(45deg)" }}>
|
|
1901
|
+
Rotated content
|
|
1902
|
+
</div>`,
|
|
1903
|
+
after: `import { Img } from "@react-email/components";
|
|
1904
|
+
|
|
1905
|
+
{/* CSS transforms are not supported in email \u2014 pre-render as an image */}
|
|
1906
|
+
<Img
|
|
1907
|
+
src="https://example.com/rotated-content.png"
|
|
1908
|
+
width={200}
|
|
1909
|
+
height={200}
|
|
1910
|
+
alt="Rotated content"
|
|
1911
|
+
style={{ display: "block", border: "0" }}
|
|
1912
|
+
/>`
|
|
1913
|
+
},
|
|
1914
|
+
"animation::jsx": {
|
|
1915
|
+
language: "jsx",
|
|
1916
|
+
description: "Replace CSS animation with a React Email Img using an animated GIF",
|
|
1917
|
+
before: `<span style={{ animation: "pulse 2s infinite" }}>New!</span>`,
|
|
1918
|
+
after: `import { Img } from "@react-email/components";
|
|
1919
|
+
|
|
1920
|
+
{/* CSS animations are not supported \u2014 use an animated GIF */}
|
|
1921
|
+
<Img
|
|
1922
|
+
src="https://example.com/badge-animated.gif"
|
|
1923
|
+
width={60}
|
|
1924
|
+
height={24}
|
|
1925
|
+
alt="New!"
|
|
1926
|
+
style={{ display: "inline-block", border: "0" }}
|
|
1927
|
+
/>`
|
|
1928
|
+
},
|
|
1929
|
+
"transition::jsx": {
|
|
1930
|
+
language: "jsx",
|
|
1931
|
+
description: "Transitions don't work in email \u2014 style the default state well",
|
|
1932
|
+
before: `<a
|
|
1933
|
+
href="#"
|
|
1934
|
+
style={{
|
|
1935
|
+
backgroundColor: "#6d28d9",
|
|
1936
|
+
color: "#fff",
|
|
1937
|
+
transition: "background-color 0.2s",
|
|
1938
|
+
}}
|
|
1939
|
+
>
|
|
1940
|
+
Click
|
|
1941
|
+
</a>`,
|
|
1942
|
+
after: `import { Button } from "@react-email/components";
|
|
1943
|
+
|
|
1944
|
+
{/* Transitions are not supported \u2014 style the default state: */}
|
|
1945
|
+
<Button
|
|
1946
|
+
href="https://example.com"
|
|
1947
|
+
style={{
|
|
1948
|
+
backgroundColor: "#6d28d9",
|
|
1949
|
+
color: "#fff",
|
|
1950
|
+
textDecoration: "none",
|
|
1951
|
+
fontWeight: "bold",
|
|
1952
|
+
padding: "12px 32px",
|
|
1953
|
+
}}
|
|
1954
|
+
>
|
|
1955
|
+
Click
|
|
1956
|
+
</Button>`
|
|
1957
|
+
},
|
|
1958
|
+
"overflow::jsx": {
|
|
1959
|
+
language: "jsx",
|
|
1960
|
+
description: "Content will always be visible \u2014 design for full content display",
|
|
1961
|
+
before: `<div style={{ maxHeight: "200px", overflow: "hidden" }}>
|
|
1962
|
+
Long content that gets clipped...
|
|
1963
|
+
</div>`,
|
|
1964
|
+
after: `import { Link, Text } from "@react-email/components";
|
|
1965
|
+
|
|
1966
|
+
{/* overflow:hidden is stripped \u2014 show full content or truncate server-side */}
|
|
1967
|
+
<div>
|
|
1968
|
+
<Text>Shortened content that fits...</Text>
|
|
1969
|
+
<Link href="https://example.com/full">Read more</Link>
|
|
1970
|
+
</div>`
|
|
1971
|
+
},
|
|
1972
|
+
"visibility::jsx": {
|
|
1973
|
+
language: "jsx",
|
|
1974
|
+
description: "Use font-size/max-height trick instead of visibility:hidden",
|
|
1975
|
+
before: `<div style={{ visibility: "hidden" }}>
|
|
1976
|
+
Hidden preheader text
|
|
1977
|
+
</div>`,
|
|
1978
|
+
after: `{/* visibility:hidden is stripped by most clients \u2014 use the preheader trick.
|
|
1979
|
+
msoHide is non-standard but needed to hide content in Outlook. */}
|
|
1980
|
+
<div
|
|
1981
|
+
style={{
|
|
1982
|
+
fontSize: "0px",
|
|
1983
|
+
lineHeight: "0px",
|
|
1984
|
+
maxHeight: "0px",
|
|
1985
|
+
overflow: "hidden",
|
|
1986
|
+
display: "none",
|
|
1987
|
+
msoHide: "all",
|
|
1988
|
+
} as React.CSSProperties}
|
|
1989
|
+
aria-hidden="true"
|
|
1990
|
+
>
|
|
1991
|
+
Preheader text
|
|
1992
|
+
</div>`
|
|
1993
|
+
},
|
|
1994
|
+
"object-fit::jsx": {
|
|
1995
|
+
language: "jsx",
|
|
1996
|
+
description: "Use React Email Img with explicit width/height instead of object-fit",
|
|
1997
|
+
before: `<img
|
|
1998
|
+
src="photo.jpg"
|
|
1999
|
+
style={{ width: "300px", height: "200px", objectFit: "cover" }}
|
|
2000
|
+
/>`,
|
|
2001
|
+
after: `import { Img } from "@react-email/components";
|
|
2002
|
+
|
|
2003
|
+
{/* Crop/resize image server-side to exact dimensions */}
|
|
2004
|
+
<Img
|
|
2005
|
+
src="https://example.com/photo-300x200.jpg"
|
|
2006
|
+
width={300}
|
|
2007
|
+
height={200}
|
|
2008
|
+
alt="Photo"
|
|
2009
|
+
style={{ display: "block", border: "0" }}
|
|
2010
|
+
/>`
|
|
2011
|
+
},
|
|
2012
|
+
"background-size::jsx": {
|
|
2013
|
+
language: "jsx",
|
|
2014
|
+
description: "Outlook ignores background-size \u2014 use sized images instead",
|
|
2015
|
+
before: `<div style={{
|
|
2016
|
+
background: "url('bg.jpg') center/cover no-repeat",
|
|
2017
|
+
}}>
|
|
2018
|
+
Content
|
|
2019
|
+
</div>`,
|
|
2020
|
+
after: `import { Img } from "@react-email/components";
|
|
2021
|
+
|
|
2022
|
+
{/* background-size is not supported in most email clients.
|
|
2023
|
+
Use a full-width image instead of a background: */}
|
|
2024
|
+
<Img
|
|
2025
|
+
src="https://example.com/bg-600x400.jpg"
|
|
2026
|
+
width={600}
|
|
2027
|
+
alt=""
|
|
2028
|
+
style={{ display: "block", width: "100%", border: "0" }}
|
|
2029
|
+
/>`
|
|
2030
|
+
},
|
|
2031
|
+
"box-sizing::jsx": {
|
|
2032
|
+
language: "jsx",
|
|
2033
|
+
description: "Account for padding in width manually (no box-sizing support)",
|
|
2034
|
+
before: `<div style={{
|
|
2035
|
+
width: "300px",
|
|
2036
|
+
padding: "20px",
|
|
2037
|
+
boxSizing: "border-box",
|
|
2038
|
+
}}>
|
|
2039
|
+
Content \u2014 total width stays 300px
|
|
2040
|
+
</div>`,
|
|
2041
|
+
after: `{/* Set outer width, use inner element for padding */}
|
|
2042
|
+
<div style={{ width: "300px" }}>
|
|
2043
|
+
<div style={{ padding: "20px" }}>
|
|
2044
|
+
Content \u2014 padding on inner element
|
|
2045
|
+
</div>
|
|
2046
|
+
</div>`
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
function getCodeFix(property, clientId, framework) {
|
|
2050
|
+
const clientPrefix = getClientPrefix(clientId);
|
|
2051
|
+
if (framework && clientPrefix) {
|
|
2052
|
+
const tier1 = FIX_DATABASE[`${property}::${clientPrefix}::${framework}`];
|
|
2053
|
+
if (tier1) return tier1;
|
|
2054
|
+
}
|
|
2055
|
+
if (framework) {
|
|
2056
|
+
const tier2 = FIX_DATABASE[`${property}::${framework}`];
|
|
2057
|
+
if (tier2) return tier2;
|
|
2058
|
+
}
|
|
2059
|
+
if (clientPrefix) {
|
|
2060
|
+
const tier3 = FIX_DATABASE[`${property}::${clientPrefix}`];
|
|
2061
|
+
if (tier3) return tier3;
|
|
2062
|
+
}
|
|
2063
|
+
return FIX_DATABASE[property];
|
|
2064
|
+
}
|
|
2065
|
+
function isCodeFixGenericFallback(property, clientId, framework) {
|
|
2066
|
+
if (!framework) return false;
|
|
2067
|
+
const clientPrefix = getClientPrefix(clientId);
|
|
2068
|
+
if (clientPrefix && FIX_DATABASE[`${property}::${clientPrefix}::${framework}`]) return false;
|
|
2069
|
+
if (FIX_DATABASE[`${property}::${framework}`]) return false;
|
|
2070
|
+
return true;
|
|
2071
|
+
}
|
|
2072
|
+
function getClientPrefix(clientId) {
|
|
2073
|
+
if (clientId.startsWith("outlook-windows")) return "outlook";
|
|
2074
|
+
if (clientId.startsWith("outlook")) return null;
|
|
2075
|
+
if (clientId.startsWith("gmail")) return "gmail";
|
|
2076
|
+
if (clientId.startsWith("apple-mail")) return "apple";
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
var SUGGESTION_DATABASE = {
|
|
2080
|
+
// ── <style> ───────────────────────────────────────────────────────────
|
|
2081
|
+
"<style>": "Use a CSS inliner tool (like juice) to move styles to inline attributes.",
|
|
2082
|
+
"<style>:partial": "Use inline styles as the primary approach, with <style> in <head> as progressive enhancement.",
|
|
2083
|
+
"<style>::jsx": "Move styles to inline style props \u2014 React Email components accept style objects directly.",
|
|
2084
|
+
"<style>:partial::jsx": "Use inline style props on React Email components. Reserve <style> in <Head> for progressive enhancement only.",
|
|
2085
|
+
"<style>::mjml": 'Use mj-style inline="inline" to force MJML to inline styles for Gmail compatibility.',
|
|
2086
|
+
"<style>:partial::mjml": 'Use mj-style inline="inline" for critical styles; plain mj-style for progressive enhancement.',
|
|
2087
|
+
"<style>::maizzle": "Prefer Tailwind utility classes \u2014 Maizzle inlines CSS via juice during build (inlineCSS: true in config.js).",
|
|
2088
|
+
"<style>:partial::maizzle": "Use Tailwind utility classes for critical styles. Maizzle automatically inlines them at build time.",
|
|
2089
|
+
// ── <link> ────────────────────────────────────────────────────────────
|
|
2090
|
+
"<link>": "Inline all CSS directly in the HTML.",
|
|
2091
|
+
"<link>::jsx": "Use the React Email <Head> component for font imports; place all other styles inline via style props.",
|
|
2092
|
+
"<link>::mjml": "MJML does not support external stylesheets. Use mj-style or inline attributes.",
|
|
2093
|
+
"<link>::maizzle": "External stylesheets are stripped. Use Tailwind CSS classes \u2014 Maizzle inlines them at build time.",
|
|
2094
|
+
// ── <svg> ─────────────────────────────────────────────────────────────
|
|
2095
|
+
"<svg>": "Convert SVGs to PNG/JPG images.",
|
|
2096
|
+
"<svg>::jsx": "Replace inline SVG with the React Email <Img> component pointing to a hosted PNG.",
|
|
2097
|
+
"<svg>::mjml": "Replace inline SVG with an mj-image component pointing to a hosted PNG.",
|
|
2098
|
+
"<svg>::maizzle": "Replace inline SVG with an <img> tag pointing to a hosted PNG.",
|
|
2099
|
+
// ── <video> ───────────────────────────────────────────────────────────
|
|
2100
|
+
"<video>": "Use an animated GIF or a static image with a play button linking to the video.",
|
|
2101
|
+
"<video>::jsx": "Replace <video> with a React Email <Link> wrapping an <Img> thumbnail.",
|
|
2102
|
+
"<video>::mjml": "Replace <video> with an mj-image linking to a video thumbnail.",
|
|
2103
|
+
"<video>::maizzle": "Replace <video> with a linked image thumbnail.",
|
|
2104
|
+
// ── <form> ────────────────────────────────────────────────────────────
|
|
2105
|
+
"<form>": "Use links to a web form instead of embedding forms in email.",
|
|
2106
|
+
"<form>::jsx": "Replace the form with a React Email <Button> or <Link> component pointing to a hosted form page.",
|
|
2107
|
+
"<form>::mjml": "Replace the form with an mj-button linking to a hosted form page.",
|
|
2108
|
+
"<form>::maizzle": "Replace the form with a CTA link/button pointing to a hosted form page.",
|
|
2109
|
+
// ── @font-face ────────────────────────────────────────────────────────
|
|
2110
|
+
"@font-face": "Always include a web-safe font stack as fallback (e.g., Arial, Helvetica, sans-serif).",
|
|
2111
|
+
"@font-face::jsx": "Use the React Email <Font> component in <Head> with a fallbackFontFamily prop.",
|
|
2112
|
+
"@font-face::mjml": "Use mj-font in mj-head instead of @font-face in mj-style.",
|
|
2113
|
+
"@font-face::maizzle": "Use the googleFonts key in config.js \u2014 Maizzle injects the Google Fonts link tag automatically.",
|
|
2114
|
+
// ── @media ────────────────────────────────────────────────────────────
|
|
2115
|
+
"@media": "Design emails mobile-first with a single-column layout that works without media queries.",
|
|
2116
|
+
"@media::jsx": "Use a single-column layout with React Email <Container> and <Section>. Avoid relying on @media queries.",
|
|
2117
|
+
"@media::mjml": "MJML generates responsive @media queries automatically. Use mj-breakpoint and mj-column widths.",
|
|
2118
|
+
"@media::maizzle": "Use Tailwind responsive utility classes and Maizzle's breakpoints config instead of hand-written @media.",
|
|
2119
|
+
// ── display:flex ──────────────────────────────────────────────────────
|
|
2120
|
+
"display:flex": "Use <table> layouts for email client compatibility.",
|
|
2121
|
+
"display:flex::outlook": "Use <table> layouts with <!--[if mso]> conditional comments for Outlook's Word engine.",
|
|
2122
|
+
"display:flex::jsx": "Use React Email <Row> and <Column> components instead of flexbox.",
|
|
2123
|
+
"display:flex::mjml": "Use mj-section and mj-column \u2014 MJML compiles these to table-based layouts.",
|
|
2124
|
+
"display:flex::maizzle": "Replace Tailwind flex classes with HTML table + MSO conditional comments for Outlook.",
|
|
2125
|
+
// ── display:grid ──────────────────────────────────────────────────────
|
|
2126
|
+
"display:grid": "Replace CSS Grid with table layout for email compatibility.",
|
|
2127
|
+
"display:grid::jsx": "Use React Email <Row> and <Column> components instead of CSS Grid.",
|
|
2128
|
+
"display:grid::mjml": "Use mj-section and mj-column for grid-like layouts.",
|
|
2129
|
+
"display:grid::maizzle": "Replace Tailwind grid classes with HTML table layout for email compatibility.",
|
|
2130
|
+
// ── linear-gradient ───────────────────────────────────────────────────
|
|
2131
|
+
"linear-gradient": "Add a solid background-color fallback before the gradient.",
|
|
2132
|
+
"linear-gradient::jsx": "Add a solid backgroundColor style prop as fallback before the gradient.",
|
|
2133
|
+
"linear-gradient::mjml": "Add a background-color attribute on mj-section as a fallback.",
|
|
2134
|
+
"linear-gradient::maizzle": "Add a bg-[color] Tailwind class as a fallback before the gradient.",
|
|
2135
|
+
// ── box-shadow ────────────────────────────────────────────────────────
|
|
2136
|
+
"box-shadow": "Use border styling as an alternative to box-shadow.",
|
|
2137
|
+
"box-shadow::jsx": "Use a border style prop as an alternative to boxShadow.",
|
|
2138
|
+
"box-shadow::mjml": "Use a border attribute on mj-section or mj-column as an alternative.",
|
|
2139
|
+
"box-shadow::maizzle": "Use Tailwind border classes as an alternative to shadow classes.",
|
|
2140
|
+
// ── border-radius ─────────────────────────────────────────────────────
|
|
2141
|
+
"border-radius": "Use VML for rounded corners in Outlook, or accept square corners.",
|
|
2142
|
+
"border-radius::outlook": "Use VML (Vector Markup Language) for rounded buttons in Outlook.",
|
|
2143
|
+
"border-radius::jsx": "Outlook ignores borderRadius. Use dangerouslySetInnerHTML with VML for rounded buttons if needed.",
|
|
2144
|
+
"border-radius::mjml": 'MJML does not generate VML \u2014 border-radius will not render in Outlook. Set border-radius="0" or accept flat corners.',
|
|
2145
|
+
"border-radius::maizzle": "Outlook ignores border-radius. Accept flat corners or use MSO conditional VML.",
|
|
2146
|
+
// ── max-width ─────────────────────────────────────────────────────────
|
|
2147
|
+
"max-width": "Use a fixed-width table wrapper for maximum compatibility.",
|
|
2148
|
+
"max-width::outlook": "Use a fixed width on table cells instead of max-width.",
|
|
2149
|
+
"max-width::jsx": "Use the React Email <Container> component which handles max-width across clients.",
|
|
2150
|
+
"max-width::mjml": "Set the width attribute on mj-body or mj-section for maximum compatibility.",
|
|
2151
|
+
"max-width::maizzle": "Wrap max-w containers with MSO conditional table for Outlook.",
|
|
2152
|
+
// ── gap ───────────────────────────────────────────────────────────────
|
|
2153
|
+
"gap": "Use padding/margin on child elements instead of gap.",
|
|
2154
|
+
"gap::outlook": "Use cellpadding/cellspacing on tables, or padding on cells.",
|
|
2155
|
+
"gap::jsx": "Use padding style props on <Column> components instead of gap.",
|
|
2156
|
+
"gap::mjml": "Use padding attribute on mj-column or mj-text for spacing.",
|
|
2157
|
+
"gap::maizzle": "Use Tailwind padding classes on child elements instead of gap.",
|
|
2158
|
+
// ── float ─────────────────────────────────────────────────────────────
|
|
2159
|
+
"float": "Use table cells with align attribute for side-by-side content.",
|
|
2160
|
+
"float::outlook": 'Use table cells with align="left" or align="right".',
|
|
2161
|
+
"float::jsx": "Use React Email <Row> and <Column> components for side-by-side layout.",
|
|
2162
|
+
"float::mjml": "Use mj-section with multiple mj-column elements for side-by-side layout.",
|
|
2163
|
+
"float::maizzle": "Use HTML tables for side-by-side layout instead of Tailwind float classes.",
|
|
2164
|
+
// ── background-image ──────────────────────────────────────────────────
|
|
2165
|
+
"background-image": "Use VML for background images in clients that require it.",
|
|
2166
|
+
"background-image::outlook": "Use <!--[if gte mso 9]> with <v:background> VML for Outlook background images.",
|
|
2167
|
+
"background-image::jsx": "Use VML via dangerouslySetInnerHTML for Outlook background images.",
|
|
2168
|
+
"background-image::mjml": "Use background-url attribute on mj-section \u2014 MJML generates VML automatically.",
|
|
2169
|
+
"background-image::maizzle": "Use MSO conditional VML for Outlook background images.",
|
|
2170
|
+
// ── position ──────────────────────────────────────────────────────────
|
|
2171
|
+
"position": "Use table-based positioning instead of CSS position.",
|
|
2172
|
+
"position::jsx": "Use React Email <Row> and <Column> components for positioning.",
|
|
2173
|
+
"position::mjml": "Use mj-section and mj-column for layout positioning.",
|
|
2174
|
+
"position::maizzle": "Use HTML table layout instead of Tailwind position classes.",
|
|
2175
|
+
// ── opacity ───────────────────────────────────────────────────────────
|
|
2176
|
+
"opacity": "Use solid colors instead of opacity.",
|
|
2177
|
+
"opacity::jsx": "Use solid colors. Opacity is not supported in many email clients.",
|
|
2178
|
+
"opacity::mjml": "Use solid colors. Most email clients don't support opacity.",
|
|
2179
|
+
"opacity::maizzle": "Use solid Tailwind color classes instead of opacity.",
|
|
2180
|
+
// ── Additional properties covered by transform helpers ────────────────
|
|
2181
|
+
"overflow": "Content will always be visible. Design accordingly.",
|
|
2182
|
+
"visibility": "Remove the element or use display:none as an alternative.",
|
|
2183
|
+
"transform": "CSS transforms are not supported in email. Pre-render the effect as an image.",
|
|
2184
|
+
"animation": "CSS animations are not supported. Use animated GIFs instead.",
|
|
2185
|
+
"transition": "CSS transitions are not supported in email.",
|
|
2186
|
+
"box-sizing": "Account for padding in your width calculations (use padding on a nested element).",
|
|
2187
|
+
"object-fit": "Use width/height attributes on <img> directly.",
|
|
2188
|
+
"max-height": "Use fixed height instead.",
|
|
2189
|
+
"background-size": "Not supported in many clients. Set image dimensions directly.",
|
|
2190
|
+
"background-position": "Not supported in many clients. Use VML for positioning.",
|
|
2191
|
+
"display": "Use tables for layout in email clients."
|
|
2192
|
+
};
|
|
2193
|
+
function getSuggestion(property, clientId, framework) {
|
|
2194
|
+
const clientPrefix = getClientPrefix(clientId);
|
|
2195
|
+
if (framework && clientPrefix) {
|
|
2196
|
+
const tier1 = SUGGESTION_DATABASE[`${property}::${clientPrefix}::${framework}`];
|
|
2197
|
+
if (tier1) return { text: tier1, isGenericFallback: false };
|
|
2198
|
+
}
|
|
2199
|
+
if (framework) {
|
|
2200
|
+
const tier2 = SUGGESTION_DATABASE[`${property}::${framework}`];
|
|
2201
|
+
if (tier2) return { text: tier2, isGenericFallback: false };
|
|
2202
|
+
}
|
|
2203
|
+
if (clientPrefix) {
|
|
2204
|
+
const tier3 = SUGGESTION_DATABASE[`${property}::${clientPrefix}`];
|
|
2205
|
+
if (tier3) return { text: tier3, isGenericFallback: !!framework };
|
|
2206
|
+
}
|
|
2207
|
+
const tier4 = SUGGESTION_DATABASE[property];
|
|
2208
|
+
if (tier4) return { text: tier4, isGenericFallback: !!framework };
|
|
2209
|
+
return {
|
|
2210
|
+
text: `"${property}" is not supported in this email client.`,
|
|
2211
|
+
isGenericFallback: !!framework
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// src/style-utils.ts
|
|
2216
|
+
function splitStyleDeclarations(style) {
|
|
2217
|
+
const parts = [];
|
|
2218
|
+
let current = "";
|
|
2219
|
+
let inSingleQuote = false;
|
|
2220
|
+
let inDoubleQuote = false;
|
|
2221
|
+
let parenDepth = 0;
|
|
2222
|
+
for (let i = 0; i < style.length; i++) {
|
|
2223
|
+
const ch = style[i];
|
|
2224
|
+
if (ch === "'" && !inDoubleQuote) {
|
|
2225
|
+
inSingleQuote = !inSingleQuote;
|
|
2226
|
+
} else if (ch === '"' && !inSingleQuote) {
|
|
2227
|
+
inDoubleQuote = !inDoubleQuote;
|
|
2228
|
+
} else if (ch === "(" && !inSingleQuote && !inDoubleQuote) {
|
|
2229
|
+
parenDepth++;
|
|
2230
|
+
} else if (ch === ")" && !inSingleQuote && !inDoubleQuote) {
|
|
2231
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
2232
|
+
}
|
|
2233
|
+
if (ch === ";" && !inSingleQuote && !inDoubleQuote && parenDepth === 0) {
|
|
2234
|
+
if (current.trim()) {
|
|
2235
|
+
parts.push(current.trim());
|
|
2236
|
+
}
|
|
2237
|
+
current = "";
|
|
2238
|
+
} else {
|
|
2239
|
+
current += ch;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (current.trim()) {
|
|
2243
|
+
parts.push(current.trim());
|
|
2244
|
+
}
|
|
2245
|
+
return parts;
|
|
2246
|
+
}
|
|
2247
|
+
function parseStyleProperties(style) {
|
|
2248
|
+
const props = [];
|
|
2249
|
+
const parts = splitStyleDeclarations(style);
|
|
2250
|
+
for (const part of parts) {
|
|
2251
|
+
const colonIndex = part.indexOf(":");
|
|
2252
|
+
if (colonIndex === -1) continue;
|
|
2253
|
+
const prop = part.slice(0, colonIndex).trim().toLowerCase();
|
|
2254
|
+
if (prop) props.push(prop);
|
|
2255
|
+
}
|
|
2256
|
+
return props;
|
|
2257
|
+
}
|
|
2258
|
+
function getStyleValue(style, property) {
|
|
2259
|
+
const parts = splitStyleDeclarations(style);
|
|
2260
|
+
for (const part of parts) {
|
|
2261
|
+
const colonIndex = part.indexOf(":");
|
|
2262
|
+
if (colonIndex === -1) continue;
|
|
2263
|
+
const prop = part.slice(0, colonIndex).trim().toLowerCase();
|
|
2264
|
+
if (prop === property) {
|
|
2265
|
+
return part.slice(colonIndex + 1).trim();
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
function parseInlineStyle(style) {
|
|
2271
|
+
const map = /* @__PURE__ */ new Map();
|
|
2272
|
+
const declarations = splitStyleDeclarations(style);
|
|
2273
|
+
for (const decl of declarations) {
|
|
2274
|
+
const colonIndex = decl.indexOf(":");
|
|
2275
|
+
if (colonIndex === -1) continue;
|
|
2276
|
+
const prop = decl.slice(0, colonIndex).trim().toLowerCase();
|
|
2277
|
+
const value = decl.slice(colonIndex + 1).trim();
|
|
2278
|
+
if (prop && value) {
|
|
2279
|
+
map.set(prop, value);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
return map;
|
|
2283
|
+
}
|
|
2284
|
+
function serializeStyle(map) {
|
|
2285
|
+
const parts = [];
|
|
2286
|
+
map.forEach((value, prop) => {
|
|
2287
|
+
parts.push(`${prop}: ${value}`);
|
|
2288
|
+
});
|
|
2289
|
+
return parts.join("; ");
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// src/transform.ts
|
|
2293
|
+
function inlineStyles($) {
|
|
2294
|
+
const styleBlocks = [];
|
|
2295
|
+
$("style").each((_, el) => {
|
|
2296
|
+
styleBlocks.push($(el).text());
|
|
2297
|
+
});
|
|
2298
|
+
if (styleBlocks.length === 0) return;
|
|
2299
|
+
for (const block of styleBlocks) {
|
|
2300
|
+
let ast;
|
|
2301
|
+
try {
|
|
2302
|
+
ast = csstree.parse(block, { parseCustomProperty: true });
|
|
2303
|
+
} catch (e) {
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
csstree.walk(ast, {
|
|
2307
|
+
visit: "Rule",
|
|
2308
|
+
enter(node) {
|
|
2309
|
+
if (node.type !== "Rule" || node.prelude.type !== "SelectorList") return;
|
|
2310
|
+
const declarations = csstree.generate(node.block);
|
|
2311
|
+
const declText = declarations.slice(1, -1).trim();
|
|
2312
|
+
if (!declText) return;
|
|
2313
|
+
const selectorText = csstree.generate(node.prelude);
|
|
2314
|
+
if (selectorText.includes(":hover") || selectorText.includes(":focus") || selectorText.includes(":active") || selectorText.includes("::")) {
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
try {
|
|
2318
|
+
$(selectorText).each((_, el) => {
|
|
2319
|
+
const existing = $(el).attr("style") || "";
|
|
2320
|
+
$(el).attr("style", existing ? `${existing}; ${declText}` : declText);
|
|
2321
|
+
});
|
|
2322
|
+
} catch (e) {
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function makeWarning(base, prop, clientId, framework) {
|
|
2329
|
+
const sug = getSuggestion(prop, clientId, framework);
|
|
2330
|
+
const fix = getCodeFix(prop, clientId, framework);
|
|
2331
|
+
const isFallback = framework && ((sug == null ? void 0 : sug.isGenericFallback) || fix && isCodeFixGenericFallback(prop, clientId, framework));
|
|
2332
|
+
return __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, base), sug ? { suggestion: sug.text } : {}), fix ? { fix } : {}), isFallback ? { fixIsGenericFallback: true } : {});
|
|
2333
|
+
}
|
|
2334
|
+
function transformGmail(html, clientId, framework) {
|
|
2335
|
+
const $ = cheerio.load(html);
|
|
2336
|
+
const warnings = [];
|
|
2337
|
+
let hasAtFontFace = false;
|
|
2338
|
+
$("style").each((_, el) => {
|
|
2339
|
+
try {
|
|
2340
|
+
const ast = csstree.parse($(el).text());
|
|
2341
|
+
csstree.walk(ast, {
|
|
2342
|
+
enter(node) {
|
|
2343
|
+
if (node.type === "Atrule" && node.name === "font-face") {
|
|
2344
|
+
hasAtFontFace = true;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
} catch (e) {
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
if (hasAtFontFace) {
|
|
2352
|
+
warnings.push(makeWarning({
|
|
2353
|
+
severity: "warning",
|
|
2354
|
+
client: clientId,
|
|
2355
|
+
property: "@font-face",
|
|
2356
|
+
message: "Gmail does not support custom web fonts."
|
|
2357
|
+
}, "@font-face", clientId, framework));
|
|
2358
|
+
}
|
|
2359
|
+
inlineStyles($);
|
|
2360
|
+
$("style").remove();
|
|
2361
|
+
$("link[rel='stylesheet']").remove();
|
|
2362
|
+
$("*").contents().filter(function() {
|
|
2363
|
+
return this.type === "comment";
|
|
2364
|
+
}).each(function() {
|
|
2365
|
+
const commentText = this.data || "";
|
|
2366
|
+
if (commentText.includes("<style") || commentText.includes("[if mso]") || commentText.includes("[if gte mso")) {
|
|
2367
|
+
$(this).remove();
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
const styleSug = getSuggestion("<style>:partial", clientId, framework);
|
|
2371
|
+
warnings.push({
|
|
2372
|
+
severity: "info",
|
|
2373
|
+
client: clientId,
|
|
2374
|
+
property: "<style>",
|
|
2375
|
+
message: "Gmail partially supports <style> blocks (head only, 16KB limit). Inlining recommended for safety.",
|
|
2376
|
+
suggestion: styleSug.text
|
|
2377
|
+
});
|
|
2378
|
+
$("[style]").each((_, el) => {
|
|
2379
|
+
const style = $(el).attr("style") || "";
|
|
2380
|
+
const props = parseInlineStyle(style);
|
|
2381
|
+
const removed = [];
|
|
2382
|
+
props.forEach((value, prop) => {
|
|
2383
|
+
if (GMAIL_STRIPPED_PROPERTIES.has(prop)) {
|
|
2384
|
+
removed.push(prop);
|
|
2385
|
+
props.delete(prop);
|
|
2386
|
+
}
|
|
2387
|
+
if (prop === "display" && value.includes("grid")) {
|
|
2388
|
+
removed.push(prop);
|
|
2389
|
+
props.delete(prop);
|
|
2390
|
+
}
|
|
2391
|
+
if (prop === "background" && (value.includes("linear-gradient") || value.includes("radial-gradient"))) {
|
|
2392
|
+
removed.push(prop);
|
|
2393
|
+
props.delete(prop);
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
if (removed.length > 0) {
|
|
2397
|
+
$(el).attr("style", serializeStyle(props));
|
|
2398
|
+
for (const prop of removed) {
|
|
2399
|
+
warnings.push(makeWarning({
|
|
2400
|
+
severity: "warning",
|
|
2401
|
+
client: clientId,
|
|
2402
|
+
property: prop,
|
|
2403
|
+
message: `Gmail strips "${prop}" from inline styles.`
|
|
2404
|
+
}, prop, clientId, framework));
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
if ($("svg").length > 0) {
|
|
2409
|
+
warnings.push(makeWarning({
|
|
2410
|
+
severity: "error",
|
|
2411
|
+
client: clientId,
|
|
2412
|
+
property: "<svg>",
|
|
2413
|
+
message: "Gmail does not support inline SVG elements."
|
|
2414
|
+
}, "<svg>", clientId, framework));
|
|
2415
|
+
$("svg").each((_, el) => {
|
|
2416
|
+
$(el).replaceWith('<img alt="[SVG not supported]" />');
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
if ($("form").length > 0) {
|
|
2420
|
+
warnings.push(makeWarning({
|
|
2421
|
+
severity: "error",
|
|
2422
|
+
client: clientId,
|
|
2423
|
+
property: "<form>",
|
|
2424
|
+
message: "Gmail strips all form elements."
|
|
2425
|
+
}, "<form>", clientId, framework));
|
|
2426
|
+
$("form").each((_, el) => {
|
|
2427
|
+
$(el).replaceWith($(el).html() || "");
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
return { clientId, html: $.html(), warnings };
|
|
2431
|
+
}
|
|
2432
|
+
function transformOutlookWindows(html, clientId, framework) {
|
|
2433
|
+
const $ = cheerio.load(html);
|
|
2434
|
+
const warnings = [];
|
|
2435
|
+
$("[style]").each((_, el) => {
|
|
2436
|
+
const style = $(el).attr("style") || "";
|
|
2437
|
+
const props = parseInlineStyle(style);
|
|
2438
|
+
const removed = [];
|
|
2439
|
+
props.forEach((value, prop) => {
|
|
2440
|
+
if (OUTLOOK_WORD_UNSUPPORTED.has(prop)) {
|
|
2441
|
+
removed.push(prop);
|
|
2442
|
+
props.delete(prop);
|
|
2443
|
+
}
|
|
2444
|
+
if ((prop === "background" || prop === "background-image") && (value.includes("linear-gradient") || value.includes("radial-gradient"))) {
|
|
2445
|
+
removed.push(prop);
|
|
2446
|
+
props.delete(prop);
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
if (removed.length > 0) {
|
|
2450
|
+
$(el).attr("style", serializeStyle(props));
|
|
2451
|
+
for (const prop of removed) {
|
|
2452
|
+
warnings.push(makeWarning({
|
|
2453
|
+
severity: "warning",
|
|
2454
|
+
client: clientId,
|
|
2455
|
+
property: prop,
|
|
2456
|
+
message: `Outlook Windows (Word engine) does not support "${prop}".`
|
|
2457
|
+
}, prop, clientId, framework));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
const borderRadiusElements = $("[style*='border-radius']");
|
|
2462
|
+
if (borderRadiusElements.length > 0) {
|
|
2463
|
+
warnings.push(makeWarning({
|
|
2464
|
+
severity: "warning",
|
|
2465
|
+
client: clientId,
|
|
2466
|
+
property: "border-radius",
|
|
2467
|
+
message: "Outlook Windows ignores border-radius. Buttons and containers will have sharp corners."
|
|
2468
|
+
}, "border-radius", clientId, framework));
|
|
2469
|
+
}
|
|
2470
|
+
const maxWidthElements = $("[style*='max-width']");
|
|
2471
|
+
if (maxWidthElements.length > 0) {
|
|
2472
|
+
warnings.push(makeWarning({
|
|
2473
|
+
severity: "warning",
|
|
2474
|
+
client: clientId,
|
|
2475
|
+
property: "max-width",
|
|
2476
|
+
message: "Outlook Windows ignores max-width."
|
|
2477
|
+
}, "max-width", clientId, framework));
|
|
2478
|
+
}
|
|
2479
|
+
const hasDivLayout = $("div[style*='display']").length > 0 || $("div[style*='flex']").length > 0 || $("div[style*='grid']").length > 0;
|
|
2480
|
+
if (hasDivLayout) {
|
|
2481
|
+
warnings.push(makeWarning({
|
|
2482
|
+
severity: "error",
|
|
2483
|
+
client: clientId,
|
|
2484
|
+
property: "display:flex",
|
|
2485
|
+
message: "Outlook Windows uses Microsoft Word for rendering. Flexbox and Grid layouts will break."
|
|
2486
|
+
}, "display:flex", clientId, framework));
|
|
2487
|
+
}
|
|
2488
|
+
if ($("[style*='background-image']").length > 0 || $("[style*='background:']").filter(
|
|
2489
|
+
(_, el) => ($(el).attr("style") || "").includes("url(")
|
|
2490
|
+
).length > 0) {
|
|
2491
|
+
warnings.push(makeWarning({
|
|
2492
|
+
severity: "warning",
|
|
2493
|
+
client: clientId,
|
|
2494
|
+
property: "background-image",
|
|
2495
|
+
message: "Outlook Windows requires VML for background images."
|
|
2496
|
+
}, "background-image", clientId, framework));
|
|
2497
|
+
}
|
|
2498
|
+
return { clientId, html: $.html(), warnings };
|
|
2499
|
+
}
|
|
2500
|
+
function transformOutlookWeb(html, clientId, framework) {
|
|
2501
|
+
const $ = cheerio.load(html);
|
|
2502
|
+
const warnings = [];
|
|
2503
|
+
$("[style]").each((_, el) => {
|
|
2504
|
+
const style = $(el).attr("style") || "";
|
|
2505
|
+
const props = parseInlineStyle(style);
|
|
2506
|
+
const removed = [];
|
|
2507
|
+
const outlookWebUnsupported = /* @__PURE__ */ new Set([
|
|
2508
|
+
"position",
|
|
2509
|
+
"transform",
|
|
2510
|
+
"animation",
|
|
2511
|
+
"transition"
|
|
2512
|
+
]);
|
|
2513
|
+
props.forEach((_2, prop) => {
|
|
2514
|
+
if (outlookWebUnsupported.has(prop)) {
|
|
2515
|
+
removed.push(prop);
|
|
2516
|
+
props.delete(prop);
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
if (removed.length > 0) {
|
|
2520
|
+
$(el).attr("style", serializeStyle(props));
|
|
2521
|
+
for (const prop of removed) {
|
|
2522
|
+
warnings.push(makeWarning({
|
|
2523
|
+
severity: "warning",
|
|
2524
|
+
client: clientId,
|
|
2525
|
+
property: prop,
|
|
2526
|
+
message: `Outlook 365 Web does not support "${prop}".`
|
|
2527
|
+
}, prop, clientId, framework));
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
return { clientId, html: $.html(), warnings };
|
|
2532
|
+
}
|
|
2533
|
+
function transformAppleMail(html, clientId, _framework) {
|
|
2534
|
+
const warnings = [];
|
|
2535
|
+
const $ = cheerio.load(html);
|
|
2536
|
+
const imgsWithTransparentBg = $("img").filter((_, el) => {
|
|
2537
|
+
const src = $(el).attr("src") || "";
|
|
2538
|
+
return src.endsWith(".png") || src.endsWith(".svg");
|
|
2539
|
+
});
|
|
2540
|
+
if (imgsWithTransparentBg.length > 0) {
|
|
2541
|
+
warnings.push({
|
|
2542
|
+
severity: "info",
|
|
2543
|
+
client: clientId,
|
|
2544
|
+
property: "dark-mode",
|
|
2545
|
+
message: "PNG/SVG images with transparent backgrounds may become invisible in Apple Mail dark mode.",
|
|
2546
|
+
suggestion: "Add a white background or padding around images, or use dark-mode-friendly image variants."
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
return { clientId, html: $.html(), warnings };
|
|
2550
|
+
}
|
|
2551
|
+
function transformYahooMail(html, clientId, framework) {
|
|
2552
|
+
const $ = cheerio.load(html);
|
|
2553
|
+
const warnings = [];
|
|
2554
|
+
warnings.push({
|
|
2555
|
+
severity: "info",
|
|
2556
|
+
client: clientId,
|
|
2557
|
+
property: "class",
|
|
2558
|
+
message: "Yahoo Mail rewrites CSS class names with a prefix. Class-based selectors in <style> blocks will still work but the names change."
|
|
2559
|
+
});
|
|
2560
|
+
if ($("[style*='background']").filter(
|
|
2561
|
+
(_, el) => ($(el).attr("style") || "").includes("url(")
|
|
2562
|
+
).length > 0) {
|
|
2563
|
+
warnings.push(makeWarning({
|
|
2564
|
+
severity: "warning",
|
|
2565
|
+
client: clientId,
|
|
2566
|
+
property: "background-image",
|
|
2567
|
+
message: "Yahoo Mail has inconsistent support for CSS background images."
|
|
2568
|
+
}, "background-image", clientId, framework));
|
|
2569
|
+
}
|
|
2570
|
+
const yahooStripped = /* @__PURE__ */ new Set([
|
|
2571
|
+
"position",
|
|
2572
|
+
"box-shadow",
|
|
2573
|
+
"transform",
|
|
2574
|
+
"animation",
|
|
2575
|
+
"transition",
|
|
2576
|
+
"opacity"
|
|
2577
|
+
]);
|
|
2578
|
+
$("[style]").each((_, el) => {
|
|
2579
|
+
const style = $(el).attr("style") || "";
|
|
2580
|
+
const props = parseInlineStyle(style);
|
|
2581
|
+
const removed = [];
|
|
2582
|
+
props.forEach((_2, prop) => {
|
|
2583
|
+
if (yahooStripped.has(prop)) {
|
|
2584
|
+
removed.push(prop);
|
|
2585
|
+
props.delete(prop);
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
if (removed.length > 0) {
|
|
2589
|
+
$(el).attr("style", serializeStyle(props));
|
|
2590
|
+
for (const prop of removed) {
|
|
2591
|
+
warnings.push(makeWarning({
|
|
2592
|
+
severity: "warning",
|
|
2593
|
+
client: clientId,
|
|
2594
|
+
property: prop,
|
|
2595
|
+
message: `Yahoo Mail strips "${prop}" from styles.`
|
|
2596
|
+
}, prop, clientId, framework));
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
return { clientId, html: $.html(), warnings };
|
|
2601
|
+
}
|
|
2602
|
+
function transformSamsungMail(html, clientId, framework) {
|
|
2603
|
+
const $ = cheerio.load(html);
|
|
2604
|
+
const warnings = [];
|
|
2605
|
+
const samsungPartial = /* @__PURE__ */ new Set([
|
|
2606
|
+
"box-shadow",
|
|
2607
|
+
"transform",
|
|
2608
|
+
"animation",
|
|
2609
|
+
"transition",
|
|
2610
|
+
"opacity"
|
|
2611
|
+
]);
|
|
2612
|
+
$("[style]").each((_, el) => {
|
|
2613
|
+
const style = $(el).attr("style") || "";
|
|
2614
|
+
const props = parseInlineStyle(style);
|
|
2615
|
+
props.forEach((_2, prop) => {
|
|
2616
|
+
if (samsungPartial.has(prop)) {
|
|
2617
|
+
warnings.push(makeWarning({
|
|
2618
|
+
severity: "info",
|
|
2619
|
+
client: clientId,
|
|
2620
|
+
property: prop,
|
|
2621
|
+
message: `Samsung Mail has limited support for "${prop}".`
|
|
2622
|
+
}, prop, clientId, framework));
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
});
|
|
2626
|
+
return { clientId, html: $.html(), warnings };
|
|
2627
|
+
}
|
|
2628
|
+
function transformThunderbird(html, clientId, _framework) {
|
|
2629
|
+
const $ = cheerio.load(html);
|
|
2630
|
+
const warnings = [];
|
|
2631
|
+
let hasAnimationOrTransition = false;
|
|
2632
|
+
$("[style]").each((_, el) => {
|
|
2633
|
+
const style = $(el).attr("style") || "";
|
|
2634
|
+
const props = parseInlineStyle(style);
|
|
2635
|
+
props.forEach((_2, prop) => {
|
|
2636
|
+
if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
|
|
2637
|
+
hasAnimationOrTransition = true;
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
if (!hasAnimationOrTransition) {
|
|
2642
|
+
$("style").each((_, el) => {
|
|
2643
|
+
try {
|
|
2644
|
+
const ast = csstree.parse($(el).text());
|
|
2645
|
+
csstree.walk(ast, {
|
|
2646
|
+
enter(node) {
|
|
2647
|
+
if (node.type === "Declaration") {
|
|
2648
|
+
const prop = node.property.toLowerCase();
|
|
2649
|
+
if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
|
|
2650
|
+
hasAnimationOrTransition = true;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
} catch (e) {
|
|
2656
|
+
}
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
if (hasAnimationOrTransition) {
|
|
2660
|
+
warnings.push({
|
|
2661
|
+
severity: "info",
|
|
2662
|
+
client: clientId,
|
|
2663
|
+
property: "animation",
|
|
2664
|
+
message: "Thunderbird does not support CSS animations or transitions."
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
return { clientId, html: $.html(), warnings };
|
|
2668
|
+
}
|
|
2669
|
+
function transformHeyMail(html, clientId, framework) {
|
|
2670
|
+
const $ = cheerio.load(html);
|
|
2671
|
+
const warnings = [];
|
|
2672
|
+
const heyStripped = /* @__PURE__ */ new Set([
|
|
2673
|
+
"transform",
|
|
2674
|
+
"animation",
|
|
2675
|
+
"transition"
|
|
2676
|
+
]);
|
|
2677
|
+
$("[style]").each((_, el) => {
|
|
2678
|
+
const style = $(el).attr("style") || "";
|
|
2679
|
+
const props = parseInlineStyle(style);
|
|
2680
|
+
const removed = [];
|
|
2681
|
+
props.forEach((value, prop) => {
|
|
2682
|
+
if (heyStripped.has(prop)) {
|
|
2683
|
+
removed.push(prop);
|
|
2684
|
+
props.delete(prop);
|
|
2685
|
+
}
|
|
2686
|
+
if (prop === "position" && (value.includes("fixed") || value.includes("sticky"))) {
|
|
2687
|
+
removed.push(prop);
|
|
2688
|
+
props.delete(prop);
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
if (removed.length > 0) {
|
|
2692
|
+
$(el).attr("style", serializeStyle(props));
|
|
2693
|
+
for (const prop of removed) {
|
|
2694
|
+
warnings.push(makeWarning({
|
|
2695
|
+
severity: "warning",
|
|
2696
|
+
client: clientId,
|
|
2697
|
+
property: prop,
|
|
2698
|
+
message: `HEY Mail strips "${prop}" for security and rendering consistency.`
|
|
2699
|
+
}, prop, clientId, framework));
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
if ($("form").length > 0) {
|
|
2704
|
+
warnings.push(makeWarning({
|
|
2705
|
+
severity: "error",
|
|
2706
|
+
client: clientId,
|
|
2707
|
+
property: "<form>",
|
|
2708
|
+
message: "HEY Mail removes form elements for security."
|
|
2709
|
+
}, "<form>", clientId, framework));
|
|
2710
|
+
$("form").each((_, el) => {
|
|
2711
|
+
$(el).replaceWith($(el).html() || "");
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
if ($("link[rel='stylesheet']").length > 0) {
|
|
2715
|
+
warnings.push(makeWarning({
|
|
2716
|
+
severity: "error",
|
|
2717
|
+
client: clientId,
|
|
2718
|
+
property: "<link>",
|
|
2719
|
+
message: "HEY Mail does not load external stylesheets."
|
|
2720
|
+
}, "<link>", clientId, framework));
|
|
2721
|
+
$("link[rel='stylesheet']").remove();
|
|
2722
|
+
}
|
|
2723
|
+
if (!html.includes("prefers-color-scheme")) {
|
|
2724
|
+
warnings.push({
|
|
2725
|
+
severity: "info",
|
|
2726
|
+
client: clientId,
|
|
2727
|
+
property: "dark-mode",
|
|
2728
|
+
message: "HEY Mail supports @media (prefers-color-scheme: dark). Consider adding dark mode styles.",
|
|
2729
|
+
suggestion: "Add a @media (prefers-color-scheme: dark) block to optimize for HEY's audience."
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
return { clientId, html: $.html(), warnings };
|
|
2733
|
+
}
|
|
2734
|
+
function transformSuperhuman(html, clientId, framework) {
|
|
2735
|
+
const $ = cheerio.load(html);
|
|
2736
|
+
const warnings = [];
|
|
2737
|
+
if ($("form").length > 0) {
|
|
2738
|
+
warnings.push(makeWarning({
|
|
2739
|
+
severity: "error",
|
|
2740
|
+
client: clientId,
|
|
2741
|
+
property: "<form>",
|
|
2742
|
+
message: "Superhuman removes form elements."
|
|
2743
|
+
}, "<form>", clientId, framework));
|
|
2744
|
+
$("form").each((_, el) => {
|
|
2745
|
+
$(el).replaceWith($(el).html() || "");
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
if ($("link[rel='stylesheet']").length > 0) {
|
|
2749
|
+
warnings.push(makeWarning({
|
|
2750
|
+
severity: "error",
|
|
2751
|
+
client: clientId,
|
|
2752
|
+
property: "<link>",
|
|
2753
|
+
message: "Superhuman does not load external stylesheets."
|
|
2754
|
+
}, "<link>", clientId, framework));
|
|
2755
|
+
$("link[rel='stylesheet']").remove();
|
|
2756
|
+
}
|
|
2757
|
+
let hasAnimation = false;
|
|
2758
|
+
$("[style]").each((_, el) => {
|
|
2759
|
+
const style = $(el).attr("style") || "";
|
|
2760
|
+
const props = parseInlineStyle(style);
|
|
2761
|
+
props.forEach((_2, prop) => {
|
|
2762
|
+
if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
|
|
2763
|
+
hasAnimation = true;
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
});
|
|
2767
|
+
if (!hasAnimation) {
|
|
2768
|
+
$("style").each((_, el) => {
|
|
2769
|
+
try {
|
|
2770
|
+
const ast = csstree.parse($(el).text());
|
|
2771
|
+
csstree.walk(ast, {
|
|
2772
|
+
enter(node) {
|
|
2773
|
+
if (node.type === "Declaration") {
|
|
2774
|
+
const prop = node.property.toLowerCase();
|
|
2775
|
+
if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
|
|
2776
|
+
hasAnimation = true;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
} catch (e) {
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
if (hasAnimation) {
|
|
2786
|
+
warnings.push({
|
|
2787
|
+
severity: "info",
|
|
2788
|
+
client: clientId,
|
|
2789
|
+
property: "animation",
|
|
2790
|
+
message: "Superhuman may honor OS-level 'reduce motion' preferences, disabling animations.",
|
|
2791
|
+
suggestion: "Use @media (prefers-reduced-motion: reduce) to provide static fallbacks."
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
warnings.push({
|
|
2795
|
+
severity: "info",
|
|
2796
|
+
client: clientId,
|
|
2797
|
+
property: "<style>",
|
|
2798
|
+
message: "Superhuman uses Chromium rendering with excellent CSS support. Flexbox, Grid, CSS variables, and modern properties all work."
|
|
2799
|
+
});
|
|
2800
|
+
return { clientId, html: $.html(), warnings };
|
|
2801
|
+
}
|
|
2802
|
+
var TRANSFORMERS = {
|
|
2803
|
+
"gmail-web": transformGmail,
|
|
2804
|
+
"gmail-android": transformGmail,
|
|
2805
|
+
"gmail-ios": transformGmail,
|
|
2806
|
+
"outlook-web": transformOutlookWeb,
|
|
2807
|
+
"outlook-windows": transformOutlookWindows,
|
|
2808
|
+
"apple-mail-macos": transformAppleMail,
|
|
2809
|
+
"apple-mail-ios": transformAppleMail,
|
|
2810
|
+
"yahoo-mail": transformYahooMail,
|
|
2811
|
+
"samsung-mail": transformSamsungMail,
|
|
2812
|
+
"thunderbird": transformThunderbird,
|
|
2813
|
+
"hey-mail": transformHeyMail,
|
|
2814
|
+
"superhuman": transformSuperhuman
|
|
2815
|
+
};
|
|
2816
|
+
function transformForClient(html, clientId, framework) {
|
|
2817
|
+
if (!html || !html.trim()) {
|
|
2818
|
+
return { clientId, html: html || "", warnings: [] };
|
|
2819
|
+
}
|
|
2820
|
+
const transformer = TRANSFORMERS[clientId];
|
|
2821
|
+
if (!transformer) {
|
|
2822
|
+
return {
|
|
2823
|
+
clientId,
|
|
2824
|
+
html,
|
|
2825
|
+
warnings: [
|
|
2826
|
+
{
|
|
2827
|
+
severity: "info",
|
|
2828
|
+
client: clientId,
|
|
2829
|
+
property: "unknown",
|
|
2830
|
+
message: `No transformation rules available for client "${clientId}".`
|
|
2831
|
+
}
|
|
2832
|
+
]
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
return transformer(html, clientId, framework);
|
|
2836
|
+
}
|
|
2837
|
+
function transformForAllClients(html, framework) {
|
|
2838
|
+
return Object.keys(TRANSFORMERS).map(
|
|
2839
|
+
(clientId) => transformForClient(html, clientId, framework)
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// src/analyze.ts
|
|
2844
|
+
import * as cheerio2 from "cheerio";
|
|
2845
|
+
import * as csstree2 from "css-tree";
|
|
2846
|
+
function analyzeEmail(html, framework) {
|
|
2847
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2848
|
+
if (!html || !html.trim()) {
|
|
2849
|
+
return [];
|
|
2850
|
+
}
|
|
2851
|
+
const $ = cheerio2.load(html);
|
|
2852
|
+
const warnings = [];
|
|
2853
|
+
const seenWarnings = /* @__PURE__ */ new Set();
|
|
2854
|
+
function addWarning(w) {
|
|
2855
|
+
const key = `${w.client}:${w.property}:${w.severity}`;
|
|
2856
|
+
if (!seenWarnings.has(key)) {
|
|
2857
|
+
seenWarnings.add(key);
|
|
2858
|
+
warnings.push(w);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
if ($("style").length > 0) {
|
|
2862
|
+
for (const client of EMAIL_CLIENTS) {
|
|
2863
|
+
const support = (_a = CSS_SUPPORT["<style>"]) == null ? void 0 : _a[client.id];
|
|
2864
|
+
if (support === "unsupported") {
|
|
2865
|
+
const sug = getSuggestion("<style>", client.id, framework);
|
|
2866
|
+
const fix = getCodeFix("<style>", client.id, framework);
|
|
2867
|
+
addWarning(__spreadValues({
|
|
2868
|
+
severity: "error",
|
|
2869
|
+
client: client.id,
|
|
2870
|
+
property: "<style>",
|
|
2871
|
+
message: `${client.name} strips <style> blocks. Styles must be inlined.`,
|
|
2872
|
+
suggestion: sug.text,
|
|
2873
|
+
fix
|
|
2874
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
2875
|
+
} else if (support === "partial") {
|
|
2876
|
+
const sug = getSuggestion("<style>:partial", client.id, framework);
|
|
2877
|
+
const fix = getCodeFix("<style>", client.id, framework);
|
|
2878
|
+
addWarning(__spreadValues({
|
|
2879
|
+
severity: "warning",
|
|
2880
|
+
client: client.id,
|
|
2881
|
+
property: "<style>",
|
|
2882
|
+
message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
|
|
2883
|
+
suggestion: sug.text,
|
|
2884
|
+
fix
|
|
2885
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
if ($("link[rel='stylesheet']").length > 0) {
|
|
2890
|
+
for (const client of EMAIL_CLIENTS) {
|
|
2891
|
+
const support = (_b = CSS_SUPPORT["<link>"]) == null ? void 0 : _b[client.id];
|
|
2892
|
+
if (support === "unsupported") {
|
|
2893
|
+
const sug = getSuggestion("<link>", client.id, framework);
|
|
2894
|
+
const fix = getCodeFix("<link>", client.id, framework);
|
|
2895
|
+
addWarning(__spreadValues({
|
|
2896
|
+
severity: "error",
|
|
2897
|
+
client: client.id,
|
|
2898
|
+
property: "<link>",
|
|
2899
|
+
message: `${client.name} does not support external stylesheets.`,
|
|
2900
|
+
suggestion: sug.text,
|
|
2901
|
+
fix
|
|
2902
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<link>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
if ($("svg").length > 0) {
|
|
2907
|
+
for (const client of EMAIL_CLIENTS) {
|
|
2908
|
+
const support = (_c = CSS_SUPPORT["<svg>"]) == null ? void 0 : _c[client.id];
|
|
2909
|
+
if (support === "unsupported") {
|
|
2910
|
+
const sug = getSuggestion("<svg>", client.id, framework);
|
|
2911
|
+
const fix = getCodeFix("<svg>", client.id, framework);
|
|
2912
|
+
addWarning(__spreadValues({
|
|
2913
|
+
severity: "error",
|
|
2914
|
+
client: client.id,
|
|
2915
|
+
property: "<svg>",
|
|
2916
|
+
message: `${client.name} does not support inline SVG.`,
|
|
2917
|
+
suggestion: sug.text,
|
|
2918
|
+
fix
|
|
2919
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<svg>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
if ($("video").length > 0) {
|
|
2924
|
+
for (const client of EMAIL_CLIENTS) {
|
|
2925
|
+
const support = (_d = CSS_SUPPORT["<video>"]) == null ? void 0 : _d[client.id];
|
|
2926
|
+
if (support === "unsupported") {
|
|
2927
|
+
const sug = getSuggestion("<video>", client.id, framework);
|
|
2928
|
+
const fix = getCodeFix("<video>", client.id, framework);
|
|
2929
|
+
addWarning(__spreadValues({
|
|
2930
|
+
severity: "warning",
|
|
2931
|
+
client: client.id,
|
|
2932
|
+
property: "<video>",
|
|
2933
|
+
message: `${client.name} does not support <video> elements.`,
|
|
2934
|
+
suggestion: sug.text,
|
|
2935
|
+
fix
|
|
2936
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<video>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
if ($("form").length > 0 || $("input").length > 0 || $("button[type='submit']").length > 0) {
|
|
2941
|
+
for (const client of EMAIL_CLIENTS) {
|
|
2942
|
+
const support = (_e = CSS_SUPPORT["<form>"]) == null ? void 0 : _e[client.id];
|
|
2943
|
+
if (support === "unsupported") {
|
|
2944
|
+
const sug = getSuggestion("<form>", client.id, framework);
|
|
2945
|
+
const fix = getCodeFix("<form>", client.id, framework);
|
|
2946
|
+
addWarning(__spreadValues({
|
|
2947
|
+
severity: "error",
|
|
2948
|
+
client: client.id,
|
|
2949
|
+
property: "<form>",
|
|
2950
|
+
message: `${client.name} strips form elements.`,
|
|
2951
|
+
suggestion: sug.text,
|
|
2952
|
+
fix
|
|
2953
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<form>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
const parsedAtRules = /* @__PURE__ */ new Set();
|
|
2958
|
+
const parsedProperties = /* @__PURE__ */ new Set();
|
|
2959
|
+
$("style").each((_, el) => {
|
|
2960
|
+
const cssText = $(el).text();
|
|
2961
|
+
try {
|
|
2962
|
+
const ast = csstree2.parse(cssText, { parseCustomProperty: true });
|
|
2963
|
+
csstree2.walk(ast, {
|
|
2964
|
+
enter(node) {
|
|
2965
|
+
if (node.type === "Atrule") {
|
|
2966
|
+
parsedAtRules.add(`@${node.name}`);
|
|
2967
|
+
}
|
|
2968
|
+
if (node.type === "Declaration") {
|
|
2969
|
+
parsedProperties.add(node.property.toLowerCase());
|
|
2970
|
+
if (node.property.toLowerCase() === "display") {
|
|
2971
|
+
const value = csstree2.generate(node.value);
|
|
2972
|
+
if (value.includes("flex")) parsedProperties.add("display:flex");
|
|
2973
|
+
if (value.includes("grid")) parsedProperties.add("display:grid");
|
|
2974
|
+
}
|
|
2975
|
+
const valueStr = csstree2.generate(node.value);
|
|
2976
|
+
if (valueStr.includes("linear-gradient") || valueStr.includes("radial-gradient")) {
|
|
2977
|
+
parsedProperties.add("linear-gradient");
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
} catch (e) {
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
if (parsedAtRules.has("@font-face")) {
|
|
2986
|
+
for (const client of EMAIL_CLIENTS) {
|
|
2987
|
+
const support = (_f = CSS_SUPPORT["@font-face"]) == null ? void 0 : _f[client.id];
|
|
2988
|
+
if (support === "unsupported") {
|
|
2989
|
+
const sug = getSuggestion("@font-face", client.id, framework);
|
|
2990
|
+
const fix = getCodeFix("@font-face", client.id, framework);
|
|
2991
|
+
addWarning(__spreadValues({
|
|
2992
|
+
severity: "warning",
|
|
2993
|
+
client: client.id,
|
|
2994
|
+
property: "@font-face",
|
|
2995
|
+
message: `${client.name} does not support web fonts (@font-face).`,
|
|
2996
|
+
suggestion: sug.text,
|
|
2997
|
+
fix
|
|
2998
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("@font-face", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
if (parsedAtRules.has("@media")) {
|
|
3003
|
+
for (const client of EMAIL_CLIENTS) {
|
|
3004
|
+
const support = (_g = CSS_SUPPORT["@media"]) == null ? void 0 : _g[client.id];
|
|
3005
|
+
if (support === "unsupported") {
|
|
3006
|
+
const sug = getSuggestion("@media", client.id, framework);
|
|
3007
|
+
const fix = getCodeFix("@media", client.id, framework);
|
|
3008
|
+
addWarning(__spreadValues({
|
|
3009
|
+
severity: "warning",
|
|
3010
|
+
client: client.id,
|
|
3011
|
+
property: "@media",
|
|
3012
|
+
message: `${client.name} does not support @media queries.`,
|
|
3013
|
+
suggestion: sug.text,
|
|
3014
|
+
fix
|
|
3015
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("@media", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
|
|
3020
|
+
(k) => !k.startsWith("<") && !k.startsWith("@")
|
|
3021
|
+
);
|
|
3022
|
+
$("[style]").each((_, el) => {
|
|
3023
|
+
const style = $(el).attr("style") || "";
|
|
3024
|
+
const props = parseStyleProperties(style);
|
|
3025
|
+
for (const prop of props) {
|
|
3026
|
+
if (prop === "display") {
|
|
3027
|
+
const value2 = getStyleValue(style, "display");
|
|
3028
|
+
if (value2 == null ? void 0 : value2.includes("flex")) {
|
|
3029
|
+
checkPropertySupport("display:flex", addWarning, framework);
|
|
3030
|
+
} else if (value2 == null ? void 0 : value2.includes("grid")) {
|
|
3031
|
+
checkPropertySupport("display:grid", addWarning, framework);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
if (cssPropertiesToCheck.includes(prop)) {
|
|
3035
|
+
checkPropertySupport(prop, addWarning, framework);
|
|
3036
|
+
}
|
|
3037
|
+
const value = getStyleValue(style, prop);
|
|
3038
|
+
if (value && (value.includes("linear-gradient") || value.includes("radial-gradient"))) {
|
|
3039
|
+
checkPropertySupport("linear-gradient", addWarning, framework);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
for (const prop of parsedProperties) {
|
|
3044
|
+
if (prop.includes(":")) continue;
|
|
3045
|
+
if (!cssPropertiesToCheck.includes(prop)) continue;
|
|
3046
|
+
for (const client of EMAIL_CLIENTS) {
|
|
3047
|
+
const support = (_h = CSS_SUPPORT[prop]) == null ? void 0 : _h[client.id];
|
|
3048
|
+
if (support === "unsupported") {
|
|
3049
|
+
addWarning({
|
|
3050
|
+
severity: "warning",
|
|
3051
|
+
client: client.id,
|
|
3052
|
+
property: prop,
|
|
3053
|
+
message: `${client.name} does not support "${prop}" in <style> blocks.`
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
for (const compound of ["display:flex", "display:grid", "linear-gradient"]) {
|
|
3059
|
+
if (parsedProperties.has(compound)) {
|
|
3060
|
+
checkPropertySupport(compound, addWarning, framework);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
3064
|
+
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
3065
|
+
return warnings;
|
|
3066
|
+
}
|
|
3067
|
+
function checkPropertySupport(prop, addWarning, framework) {
|
|
3068
|
+
const supportData = CSS_SUPPORT[prop];
|
|
3069
|
+
if (!supportData) return;
|
|
3070
|
+
for (const client of EMAIL_CLIENTS) {
|
|
3071
|
+
const support = supportData[client.id] || "unknown";
|
|
3072
|
+
if (support === "unsupported") {
|
|
3073
|
+
const sug = getSuggestion(prop, client.id, framework);
|
|
3074
|
+
const fix = getCodeFix(prop, client.id, framework);
|
|
3075
|
+
addWarning(__spreadValues({
|
|
3076
|
+
severity: "warning",
|
|
3077
|
+
client: client.id,
|
|
3078
|
+
property: prop,
|
|
3079
|
+
message: `${client.name} does not support "${prop}".`,
|
|
3080
|
+
suggestion: sug.text,
|
|
3081
|
+
fix
|
|
3082
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
3083
|
+
} else if (support === "partial") {
|
|
3084
|
+
const sug = getSuggestion(prop, client.id, framework);
|
|
3085
|
+
const fix = getCodeFix(prop, client.id, framework);
|
|
3086
|
+
addWarning(__spreadValues({
|
|
3087
|
+
severity: "info",
|
|
3088
|
+
client: client.id,
|
|
3089
|
+
property: prop,
|
|
3090
|
+
message: `${client.name} has partial support for "${prop}".`,
|
|
3091
|
+
suggestion: sug.text,
|
|
3092
|
+
fix
|
|
3093
|
+
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
function generateCompatibilityScore(warnings) {
|
|
3098
|
+
const result = {};
|
|
3099
|
+
for (const client of EMAIL_CLIENTS) {
|
|
3100
|
+
const clientWarnings = warnings.filter((w) => w.client === client.id);
|
|
3101
|
+
const errors = clientWarnings.filter((w) => w.severity === "error").length;
|
|
3102
|
+
const warns = clientWarnings.filter((w) => w.severity === "warning").length;
|
|
3103
|
+
const info = clientWarnings.filter((w) => w.severity === "info").length;
|
|
3104
|
+
const score = Math.max(0, Math.min(100, 100 - errors * 15 - warns * 5 - info * 1));
|
|
3105
|
+
result[client.id] = { score, errors, warnings: warns, info };
|
|
3106
|
+
}
|
|
3107
|
+
return result;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// src/dark-mode.ts
|
|
3111
|
+
import * as cheerio3 from "cheerio";
|
|
3112
|
+
function simulateDarkMode(html, clientId) {
|
|
3113
|
+
if (!html || !html.trim()) {
|
|
3114
|
+
return { html: html || "", warnings: [] };
|
|
3115
|
+
}
|
|
3116
|
+
const $ = cheerio3.load(html);
|
|
3117
|
+
const warnings = [];
|
|
3118
|
+
$("img").each((_, el) => {
|
|
3119
|
+
const src = $(el).attr("src") || "";
|
|
3120
|
+
if (src.endsWith(".png") || src.endsWith(".svg") || src.endsWith(".webp")) {
|
|
3121
|
+
warnings.push({
|
|
3122
|
+
severity: "warning",
|
|
3123
|
+
client: clientId,
|
|
3124
|
+
property: "dark-mode",
|
|
3125
|
+
message: "Image with potentially transparent background may disappear in dark mode.",
|
|
3126
|
+
suggestion: "Add a background-color to the parent element, or use a non-transparent image format."
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
});
|
|
3130
|
+
switch (clientId) {
|
|
3131
|
+
case "gmail-web":
|
|
3132
|
+
case "gmail-android":
|
|
3133
|
+
case "gmail-ios":
|
|
3134
|
+
applyColorInversion($, clientId === "gmail-android" ? "full" : "partial");
|
|
3135
|
+
break;
|
|
3136
|
+
case "outlook-web":
|
|
3137
|
+
applyColorInversion($, "partial");
|
|
3138
|
+
break;
|
|
3139
|
+
case "apple-mail-macos":
|
|
3140
|
+
case "apple-mail-ios":
|
|
3141
|
+
applyColorInversion($, "partial");
|
|
3142
|
+
if (!html.includes("prefers-color-scheme")) {
|
|
3143
|
+
warnings.push({
|
|
3144
|
+
severity: "info",
|
|
3145
|
+
client: clientId,
|
|
3146
|
+
property: "dark-mode",
|
|
3147
|
+
message: "Apple Mail supports @media (prefers-color-scheme: dark). Consider adding dark mode styles.",
|
|
3148
|
+
suggestion: "Add a @media (prefers-color-scheme: dark) block with inverted colors for the best dark mode experience."
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
break;
|
|
3152
|
+
case "yahoo-mail":
|
|
3153
|
+
applyColorInversion($, "partial");
|
|
3154
|
+
break;
|
|
3155
|
+
case "samsung-mail":
|
|
3156
|
+
applyColorInversion($, "full");
|
|
3157
|
+
break;
|
|
3158
|
+
case "hey-mail":
|
|
3159
|
+
applyColorInversion($, "partial");
|
|
3160
|
+
if (!html.includes("prefers-color-scheme")) {
|
|
3161
|
+
warnings.push({
|
|
3162
|
+
severity: "info",
|
|
3163
|
+
client: clientId,
|
|
3164
|
+
property: "dark-mode",
|
|
3165
|
+
message: "HEY Mail supports @media (prefers-color-scheme: dark). Add dark mode styles for the best experience.",
|
|
3166
|
+
suggestion: "Add a @media (prefers-color-scheme: dark) block with inverted colors."
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
break;
|
|
3170
|
+
case "superhuman":
|
|
3171
|
+
applyColorInversion($, "partial");
|
|
3172
|
+
if (!html.includes("prefers-color-scheme")) {
|
|
3173
|
+
warnings.push({
|
|
3174
|
+
severity: "info",
|
|
3175
|
+
client: clientId,
|
|
3176
|
+
property: "dark-mode",
|
|
3177
|
+
message: "Superhuman respects @media (prefers-color-scheme: dark). Many Superhuman users run in dark mode.",
|
|
3178
|
+
suggestion: "Add @media (prefers-color-scheme: dark) styles \u2014 Superhuman's power-user audience often prefers dark mode."
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
break;
|
|
3182
|
+
case "outlook-windows":
|
|
3183
|
+
case "thunderbird":
|
|
3184
|
+
break;
|
|
3185
|
+
}
|
|
3186
|
+
$("body").css("background-color", "#1a1a1a");
|
|
3187
|
+
$("body").css("color", "#e0e0e0");
|
|
3188
|
+
return { html: $.html(), warnings };
|
|
3189
|
+
}
|
|
3190
|
+
function applyColorInversion($, mode) {
|
|
3191
|
+
$("[style]").each((_, el) => {
|
|
3192
|
+
const style = $(el).attr("style") || "";
|
|
3193
|
+
if (mode === "full") {
|
|
3194
|
+
const updated = style.replace(/background-color:\s*(#fff|#ffffff|white|#fafafa|#f5f5f5|#f0f0f0|#fefefe)/gi, "background-color: #1a1a1a").replace(/background:\s*(#fff|#ffffff|white|#fafafa|#f5f5f5|#f0f0f0|#fefefe)/gi, "background: #1a1a1a").replace(/color:\s*(#000|#000000|black|#111|#222|#333)/gi, "color: #e0e0e0").replace(/color:\s*(#fff|#ffffff|white)/gi, "color: #e0e0e0").replace(
|
|
3195
|
+
/border(?:-[a-z]+)?:\s*[^;]*(?:#000|#111|#222|#333|black)/gi,
|
|
3196
|
+
(match) => match.replace(/#000|#111|#222|#333|black/gi, "#555")
|
|
3197
|
+
);
|
|
3198
|
+
$(el).attr("style", updated);
|
|
3199
|
+
} else {
|
|
3200
|
+
const updated = style.replace(/background-color:\s*(#fff|#ffffff|white)/gi, "background-color: #2d2d2d").replace(/background:\s*(#fff|#ffffff|white)/gi, "background: #2d2d2d").replace(/color:\s*(#000|#000000|black)/gi, "color: #d4d4d4");
|
|
3201
|
+
$(el).attr("style", updated);
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
$("[bgcolor]").each((_, el) => {
|
|
3205
|
+
const bgcolor = ($(el).attr("bgcolor") || "").toLowerCase();
|
|
3206
|
+
if (bgcolor === "#ffffff" || bgcolor === "#fff" || bgcolor === "white" || bgcolor === "#fafafa" || bgcolor === "#f5f5f5") {
|
|
3207
|
+
$(el).attr("bgcolor", mode === "full" ? "#1a1a1a" : "#2d2d2d");
|
|
3208
|
+
}
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
// src/diff.ts
|
|
3213
|
+
function diffResults(before, after) {
|
|
3214
|
+
var _a, _b, _c, _d;
|
|
3215
|
+
const results = [];
|
|
3216
|
+
for (const client of EMAIL_CLIENTS) {
|
|
3217
|
+
const scoreBefore = (_b = (_a = before.scores[client.id]) == null ? void 0 : _a.score) != null ? _b : 100;
|
|
3218
|
+
const scoreAfter = (_d = (_c = after.scores[client.id]) == null ? void 0 : _c.score) != null ? _d : 100;
|
|
3219
|
+
const beforeWarnings = before.warnings.filter((w) => w.client === client.id);
|
|
3220
|
+
const afterWarnings = after.warnings.filter((w) => w.client === client.id);
|
|
3221
|
+
const beforeKeys = new Set(beforeWarnings.map(warningKey));
|
|
3222
|
+
const afterKeys = new Set(afterWarnings.map(warningKey));
|
|
3223
|
+
const fixed = beforeWarnings.filter((w) => !afterKeys.has(warningKey(w)));
|
|
3224
|
+
const introduced = afterWarnings.filter((w) => !beforeKeys.has(warningKey(w)));
|
|
3225
|
+
const unchanged = afterWarnings.filter((w) => beforeKeys.has(warningKey(w)));
|
|
3226
|
+
results.push({
|
|
3227
|
+
clientId: client.id,
|
|
3228
|
+
scoreBefore,
|
|
3229
|
+
scoreAfter,
|
|
3230
|
+
scoreDelta: scoreAfter - scoreBefore,
|
|
3231
|
+
fixed,
|
|
3232
|
+
introduced,
|
|
3233
|
+
unchanged
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
return results;
|
|
3237
|
+
}
|
|
3238
|
+
function warningKey(w) {
|
|
3239
|
+
return `${w.property}:${w.severity}`;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/export-prompt.ts
|
|
3243
|
+
function generateFixPrompt(options) {
|
|
3244
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
3245
|
+
const {
|
|
3246
|
+
originalHtml,
|
|
3247
|
+
warnings,
|
|
3248
|
+
scores,
|
|
3249
|
+
scope,
|
|
3250
|
+
selectedClientId,
|
|
3251
|
+
format = "html"
|
|
3252
|
+
} = options;
|
|
3253
|
+
const filteredWarnings = scope === "current" && selectedClientId ? warnings.filter((w) => w.client === selectedClientId) : warnings;
|
|
3254
|
+
const filteredScores = scope === "current" && selectedClientId ? { [selectedClientId]: scores[selectedClientId] } : scores;
|
|
3255
|
+
const clientCount = Object.keys(filteredScores).length;
|
|
3256
|
+
const errorCount = filteredWarnings.filter(
|
|
3257
|
+
(w) => w.severity === "error"
|
|
3258
|
+
).length;
|
|
3259
|
+
const warnCount = filteredWarnings.filter(
|
|
3260
|
+
(w) => w.severity === "warning"
|
|
3261
|
+
).length;
|
|
3262
|
+
const infoCount = filteredWarnings.filter(
|
|
3263
|
+
(w) => w.severity === "info"
|
|
3264
|
+
).length;
|
|
3265
|
+
const clientLabel = scope === "current" && selectedClientId ? (_b = (_a = getClient(selectedClientId)) == null ? void 0 : _a.name) != null ? _b : selectedClientId : `${clientCount} email clients`;
|
|
3266
|
+
const sections = [];
|
|
3267
|
+
sections.push(
|
|
3268
|
+
`# Email Compatibility Fix Request
|
|
3269
|
+
|
|
3270
|
+
- **Format:** ${format.toUpperCase()}
|
|
3271
|
+
- **Scope:** ${clientLabel}
|
|
3272
|
+
- **Issues found:** ${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warnCount} warning${warnCount !== 1 ? "s" : ""}, ${infoCount} info`
|
|
3273
|
+
);
|
|
3274
|
+
sections.push(
|
|
3275
|
+
`## Original Email Code
|
|
3276
|
+
|
|
3277
|
+
\`\`\`${format}
|
|
3278
|
+
${originalHtml}
|
|
3279
|
+
\`\`\``
|
|
3280
|
+
);
|
|
3281
|
+
const scoreEntries = Object.entries(filteredScores).filter(
|
|
3282
|
+
([, v]) => v != null
|
|
3283
|
+
);
|
|
3284
|
+
if (scoreEntries.length > 0) {
|
|
3285
|
+
let table = `## Compatibility Scores
|
|
3286
|
+
|
|
3287
|
+
`;
|
|
3288
|
+
table += `| Client | Score | Errors | Warnings | Info |
|
|
3289
|
+
`;
|
|
3290
|
+
table += `|--------|------:|-------:|---------:|-----:|
|
|
3291
|
+
`;
|
|
3292
|
+
for (const [clientId, data] of scoreEntries) {
|
|
3293
|
+
const name = (_d = (_c = getClient(clientId)) == null ? void 0 : _c.name) != null ? _d : clientId;
|
|
3294
|
+
table += `| ${name} | ${data.score} | ${data.errors} | ${data.warnings} | ${data.info} |
|
|
3295
|
+
`;
|
|
3296
|
+
}
|
|
3297
|
+
sections.push(table.trimEnd());
|
|
3298
|
+
}
|
|
3299
|
+
if (filteredWarnings.length > 0) {
|
|
3300
|
+
let issueSection = `## Detected Issues
|
|
3301
|
+
`;
|
|
3302
|
+
const groups = [
|
|
3303
|
+
["Errors", filteredWarnings.filter((w) => w.severity === "error")],
|
|
3304
|
+
["Warnings", filteredWarnings.filter((w) => w.severity === "warning")],
|
|
3305
|
+
["Info", filteredWarnings.filter((w) => w.severity === "info")]
|
|
3306
|
+
];
|
|
3307
|
+
for (const [label, group] of groups) {
|
|
3308
|
+
if (group.length === 0) continue;
|
|
3309
|
+
issueSection += `
|
|
3310
|
+
### ${label}
|
|
3311
|
+
`;
|
|
3312
|
+
for (const w of group) {
|
|
3313
|
+
const clientName = (_f = (_e = getClient(w.client)) == null ? void 0 : _e.name) != null ? _f : w.client;
|
|
3314
|
+
issueSection += `
|
|
3315
|
+
- **${w.property}** (${clientName}): ${w.message}`;
|
|
3316
|
+
if (w.suggestion) {
|
|
3317
|
+
issueSection += `
|
|
3318
|
+
- Suggestion: ${w.suggestion}`;
|
|
3319
|
+
}
|
|
3320
|
+
if (w.fix) {
|
|
3321
|
+
const fixLabel = w.fixIsGenericFallback && format !== "html" ? `Fix (generic HTML \u2014 adapt to ${format.toUpperCase()} syntax)` : "Fix";
|
|
3322
|
+
issueSection += `
|
|
3323
|
+
- ${fixLabel}:`;
|
|
3324
|
+
issueSection += `
|
|
3325
|
+
- Before: \`${w.fix.before}\``;
|
|
3326
|
+
issueSection += `
|
|
3327
|
+
- After: \`${w.fix.after}\``;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
sections.push(issueSection);
|
|
3332
|
+
}
|
|
3333
|
+
const formatInstructions = {
|
|
3334
|
+
jsx: `Apply all the fixes listed above to the original email code. Return complete fixed JSX code using @react-email/components. Use Row, Column, Container, Font, Img, Head, and Link components from @react-email/components wherever the fix suggests them. Keep all style values as camelCase JavaScript object properties (e.g. { backgroundColor: "#fff" }). Ensure the result is compatible with ${clientLabel}. Do not remove any content \u2014 only modify the JSX structure and style props needed to fix the issues.`,
|
|
3335
|
+
mjml: `Apply all the fixes listed above to the original email code. Return complete fixed MJML markup. Use MJML-native elements (mj-section, mj-column, mj-text, mj-button, mj-font, mj-style, mj-raw) as indicated in the fixes. Ensure the result is valid MJML that compiles without errors. Ensure the result is compatible with ${clientLabel}. Do not remove any content \u2014 only modify the MJML structure and attributes needed to fix the issues.`,
|
|
3336
|
+
maizzle: `Apply all the fixes listed above to the original email code. Return the complete fixed Maizzle template. Use Tailwind CSS utility classes and Maizzle config settings as indicated in the fixes. Add MSO conditional comment table wrappers where needed for Outlook compatibility. Ensure the result is compatible with ${clientLabel}. Do not remove any content \u2014 only modify the Tailwind classes and HTML structure needed to fix the issues.`,
|
|
3337
|
+
html: `Apply all the fixes listed above to the original email code. Return the complete fixed HTML code. Ensure the result is compatible with ${clientLabel}. Do not remove any content \u2014 only modify the CSS and HTML attributes needed to fix the issues.`
|
|
3338
|
+
};
|
|
3339
|
+
sections.push(
|
|
3340
|
+
`## Instructions
|
|
3341
|
+
|
|
3342
|
+
` + ((_g = formatInstructions[format]) != null ? _g : formatInstructions.html)
|
|
3343
|
+
);
|
|
3344
|
+
return sections.join("\n\n");
|
|
3345
|
+
}
|
|
3346
|
+
export {
|
|
3347
|
+
EMAIL_CLIENTS,
|
|
3348
|
+
analyzeEmail,
|
|
3349
|
+
diffResults,
|
|
3350
|
+
generateCompatibilityScore,
|
|
3351
|
+
generateFixPrompt,
|
|
3352
|
+
getClient,
|
|
3353
|
+
getCodeFix,
|
|
3354
|
+
getSuggestion,
|
|
3355
|
+
simulateDarkMode,
|
|
3356
|
+
transformForAllClients,
|
|
3357
|
+
transformForClient
|
|
3358
|
+
};
|
|
3359
|
+
//# sourceMappingURL=index.js.map
|