payload-plugin-newsletter 0.9.2 → 0.11.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/CHANGELOG.md +75 -0
- package/README.md +11 -5
- package/dist/components.cjs +107 -33
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +0 -2
- package/dist/components.d.ts +0 -2
- package/dist/components.js +107 -33
- package/dist/components.js.map +1 -1
- package/dist/fields.cjs +109 -9
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.js +113 -9
- package/dist/fields.js.map +1 -1
- package/dist/index.cjs +559 -932
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +562 -931
- package/dist/index.js.map +1 -1
- package/dist/types.cjs +0 -2
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +3 -83
- package/dist/types.d.ts +3 -83
- package/dist/types.js +0 -2
- package/dist/types.js.map +1 -1
- package/dist/utils.cjs +104 -26
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +104 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/utils.cjs
CHANGED
|
@@ -58,9 +58,17 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
58
58
|
"ol",
|
|
59
59
|
"li",
|
|
60
60
|
"blockquote",
|
|
61
|
-
"hr"
|
|
61
|
+
"hr",
|
|
62
|
+
"img",
|
|
63
|
+
"div",
|
|
64
|
+
"table",
|
|
65
|
+
"tr",
|
|
66
|
+
"td",
|
|
67
|
+
"th",
|
|
68
|
+
"tbody",
|
|
69
|
+
"thead"
|
|
62
70
|
],
|
|
63
|
-
ALLOWED_ATTR: ["href", "style", "target", "rel", "align"],
|
|
71
|
+
ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
|
|
64
72
|
ALLOWED_STYLES: {
|
|
65
73
|
"*": [
|
|
66
74
|
"color",
|
|
@@ -91,58 +99,62 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
91
99
|
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
|
|
92
100
|
};
|
|
93
101
|
async function convertToEmailSafeHtml(editorState, options) {
|
|
94
|
-
const rawHtml = await lexicalToEmailHtml(editorState);
|
|
102
|
+
const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
|
|
95
103
|
const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
|
|
96
104
|
if (options?.wrapInTemplate) {
|
|
97
105
|
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
|
|
98
106
|
}
|
|
99
107
|
return sanitizedHtml;
|
|
100
108
|
}
|
|
101
|
-
async function lexicalToEmailHtml(editorState) {
|
|
109
|
+
async function lexicalToEmailHtml(editorState, mediaUrl) {
|
|
102
110
|
const { root } = editorState;
|
|
103
111
|
if (!root || !root.children) {
|
|
104
112
|
return "";
|
|
105
113
|
}
|
|
106
|
-
const html = root.children.map((node) => convertNode(node)).join("");
|
|
114
|
+
const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
|
|
107
115
|
return html;
|
|
108
116
|
}
|
|
109
|
-
function convertNode(node) {
|
|
117
|
+
function convertNode(node, mediaUrl) {
|
|
110
118
|
switch (node.type) {
|
|
111
119
|
case "paragraph":
|
|
112
|
-
return convertParagraph(node);
|
|
120
|
+
return convertParagraph(node, mediaUrl);
|
|
113
121
|
case "heading":
|
|
114
|
-
return convertHeading(node);
|
|
122
|
+
return convertHeading(node, mediaUrl);
|
|
115
123
|
case "list":
|
|
116
|
-
return convertList(node);
|
|
124
|
+
return convertList(node, mediaUrl);
|
|
117
125
|
case "listitem":
|
|
118
|
-
return convertListItem(node);
|
|
126
|
+
return convertListItem(node, mediaUrl);
|
|
119
127
|
case "blockquote":
|
|
120
|
-
return convertBlockquote(node);
|
|
128
|
+
return convertBlockquote(node, mediaUrl);
|
|
121
129
|
case "text":
|
|
122
130
|
return convertText(node);
|
|
123
131
|
case "link":
|
|
124
|
-
return convertLink(node);
|
|
132
|
+
return convertLink(node, mediaUrl);
|
|
125
133
|
case "linebreak":
|
|
126
134
|
return "<br>";
|
|
135
|
+
case "upload":
|
|
136
|
+
return convertUpload(node, mediaUrl);
|
|
137
|
+
case "block":
|
|
138
|
+
return convertBlock(node, mediaUrl);
|
|
127
139
|
default:
|
|
128
140
|
if (node.children) {
|
|
129
|
-
return node.children.map(convertNode).join("");
|
|
141
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
130
142
|
}
|
|
131
143
|
return "";
|
|
132
144
|
}
|
|
133
145
|
}
|
|
134
|
-
function convertParagraph(node) {
|
|
146
|
+
function convertParagraph(node, mediaUrl) {
|
|
135
147
|
const align = getAlignment(node.format);
|
|
136
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
148
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
137
149
|
if (!children.trim()) {
|
|
138
150
|
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
|
|
139
151
|
}
|
|
140
152
|
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
|
|
141
153
|
}
|
|
142
|
-
function convertHeading(node) {
|
|
154
|
+
function convertHeading(node, mediaUrl) {
|
|
143
155
|
const tag = node.tag || "h1";
|
|
144
156
|
const align = getAlignment(node.format);
|
|
145
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
157
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
146
158
|
const styles = {
|
|
147
159
|
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
|
|
148
160
|
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
|
|
@@ -151,18 +163,18 @@ function convertHeading(node) {
|
|
|
151
163
|
const style = `${styles[tag] || styles.h3} text-align: ${align};`;
|
|
152
164
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
153
165
|
}
|
|
154
|
-
function convertList(node) {
|
|
166
|
+
function convertList(node, mediaUrl) {
|
|
155
167
|
const tag = node.listType === "number" ? "ol" : "ul";
|
|
156
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
168
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
157
169
|
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
|
|
158
170
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
159
171
|
}
|
|
160
|
-
function convertListItem(node) {
|
|
161
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
172
|
+
function convertListItem(node, mediaUrl) {
|
|
173
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
162
174
|
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
|
|
163
175
|
}
|
|
164
|
-
function convertBlockquote(node) {
|
|
165
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
176
|
+
function convertBlockquote(node, mediaUrl) {
|
|
177
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
166
178
|
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
|
|
167
179
|
return `<blockquote style="${style}">${children}</blockquote>`;
|
|
168
180
|
}
|
|
@@ -182,10 +194,76 @@ function convertText(node) {
|
|
|
182
194
|
}
|
|
183
195
|
return text;
|
|
184
196
|
}
|
|
185
|
-
function convertLink(node) {
|
|
186
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
197
|
+
function convertLink(node, mediaUrl) {
|
|
198
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
187
199
|
const url = node.fields?.url || "#";
|
|
188
|
-
|
|
200
|
+
const newTab = node.fields?.newTab ?? false;
|
|
201
|
+
const targetAttr = newTab ? ' target="_blank"' : "";
|
|
202
|
+
const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
|
|
203
|
+
return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
|
|
204
|
+
}
|
|
205
|
+
function convertUpload(node, mediaUrl) {
|
|
206
|
+
const upload = node.value;
|
|
207
|
+
if (!upload) return "";
|
|
208
|
+
let src = "";
|
|
209
|
+
if (typeof upload === "string") {
|
|
210
|
+
src = upload;
|
|
211
|
+
} else if (upload.url) {
|
|
212
|
+
src = upload.url;
|
|
213
|
+
} else if (upload.filename && mediaUrl) {
|
|
214
|
+
src = `${mediaUrl}/${upload.filename}`;
|
|
215
|
+
}
|
|
216
|
+
const alt = node.fields?.altText || upload.alt || "";
|
|
217
|
+
const caption = node.fields?.caption || "";
|
|
218
|
+
const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
|
|
219
|
+
if (caption) {
|
|
220
|
+
return `
|
|
221
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
222
|
+
${imgHtml}
|
|
223
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
|
|
224
|
+
</div>
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
|
|
228
|
+
}
|
|
229
|
+
function convertBlock(node, mediaUrl) {
|
|
230
|
+
const blockType = node.fields?.blockName;
|
|
231
|
+
switch (blockType) {
|
|
232
|
+
case "button":
|
|
233
|
+
return convertButtonBlock(node.fields);
|
|
234
|
+
case "divider":
|
|
235
|
+
return convertDividerBlock(node.fields);
|
|
236
|
+
default:
|
|
237
|
+
if (node.children) {
|
|
238
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
239
|
+
}
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function convertButtonBlock(fields) {
|
|
244
|
+
const text = fields?.text || "Click here";
|
|
245
|
+
const url = fields?.url || "#";
|
|
246
|
+
const style = fields?.style || "primary";
|
|
247
|
+
const styles = {
|
|
248
|
+
primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
|
|
249
|
+
secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
|
|
250
|
+
outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
|
|
251
|
+
};
|
|
252
|
+
const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
|
|
253
|
+
return `
|
|
254
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
255
|
+
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
|
|
256
|
+
</div>
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
function convertDividerBlock(fields) {
|
|
260
|
+
const style = fields?.style || "solid";
|
|
261
|
+
const styles = {
|
|
262
|
+
solid: "border-top: 1px solid #e5e7eb;",
|
|
263
|
+
dashed: "border-top: 1px dashed #e5e7eb;",
|
|
264
|
+
dotted: "border-top: 1px dotted #e5e7eb;"
|
|
265
|
+
};
|
|
266
|
+
return `<hr style="${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
|
|
189
267
|
}
|
|
190
268
|
function getAlignment(format) {
|
|
191
269
|
if (!format) return "left";
|
package/dist/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/exports/utils.ts","../src/utils/emailSafeHtml.ts","../src/utils/validateEmailHtml.ts"],"sourcesContent":["// Email utilities\nexport { convertToEmailSafeHtml, EMAIL_SAFE_CONFIG } from '../utils/emailSafeHtml'\nexport { validateEmailHtml } from '../utils/validateEmailHtml'\nexport type { ValidationResult } from '../utils/validateEmailHtml'","import DOMPurify from 'isomorphic-dompurify'\nimport type { SerializedEditorState } from 'lexical'\n\n/**\n * DOMPurify configuration for email-safe HTML\n */\nexport const EMAIL_SAFE_CONFIG = {\n ALLOWED_TAGS: [\n 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'strike', 's', 'span',\n 'a', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote', 'hr'\n ],\n ALLOWED_ATTR: ['href', 'style', 'target', 'rel', 'align'],\n ALLOWED_STYLES: {\n '*': [\n 'color', 'background-color', 'font-size', 'font-weight',\n 'font-style', 'text-decoration', 'text-align', 'margin',\n 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',\n 'padding', 'padding-top', 'padding-right', 'padding-bottom', \n 'padding-left', 'line-height', 'border-left', 'border-left-width',\n 'border-left-style', 'border-left-color'\n ],\n },\n FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input'],\n FORBID_ATTR: ['class', 'id', 'onclick', 'onload', 'onerror'],\n}\n\n/**\n * Converts Lexical editor state to email-safe HTML\n */\nexport async function convertToEmailSafeHtml(\n editorState: SerializedEditorState,\n options?: {\n wrapInTemplate?: boolean\n preheader?: string\n }\n): Promise<string> {\n // First, convert Lexical state to HTML using custom converters\n const rawHtml = await lexicalToEmailHtml(editorState)\n \n // Sanitize the HTML\n const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG)\n \n // Optionally wrap in email template\n if (options?.wrapInTemplate) {\n return wrapInEmailTemplate(sanitizedHtml, options.preheader)\n }\n \n return sanitizedHtml\n}\n\n/**\n * Custom Lexical to HTML converter for email\n */\nasync function lexicalToEmailHtml(editorState: SerializedEditorState): Promise<string> {\n const { root } = editorState\n \n if (!root || !root.children) {\n return ''\n }\n \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const html = root.children.map((node: any) => convertNode(node)).join('')\n return html\n}\n\n/**\n * Convert individual Lexical nodes to email-safe HTML\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertNode(node: any): string {\n switch (node.type) {\n case 'paragraph':\n return convertParagraph(node)\n case 'heading':\n return convertHeading(node)\n case 'list':\n return convertList(node)\n case 'listitem':\n return convertListItem(node)\n case 'blockquote':\n return convertBlockquote(node)\n case 'text':\n return convertText(node)\n case 'link':\n return convertLink(node)\n case 'linebreak':\n return '<br>'\n default:\n // Unknown node type - convert children if any\n if (node.children) {\n return node.children.map(convertNode).join('')\n }\n return ''\n }\n}\n\n/**\n * Convert paragraph node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertParagraph(node: any): string {\n const align = getAlignment(node.format)\n const children = node.children?.map(convertNode).join('') || ''\n \n if (!children.trim()) {\n return '<p style=\"margin: 0 0 16px 0; min-height: 1em;\"> </p>'\n }\n \n return `<p style=\"margin: 0 0 16px 0; text-align: ${align};\">${children}</p>`\n}\n\n/**\n * Convert heading node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertHeading(node: any): string {\n const tag = node.tag || 'h1'\n const align = getAlignment(node.format)\n const children = node.children?.map(convertNode).join('') || ''\n \n const styles: Record<string, string> = {\n h1: 'font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;',\n h2: 'font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;',\n h3: 'font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;',\n }\n \n const style = `${styles[tag] || styles.h3} text-align: ${align};`\n \n return `<${tag} style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertList(node: any): string {\n const tag = node.listType === 'number' ? 'ol' : 'ul'\n const children = node.children?.map(convertNode).join('') || ''\n \n const style = tag === 'ul' \n ? 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;'\n : 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;'\n \n return `<${tag} style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list item node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertListItem(node: any): string {\n const children = node.children?.map(convertNode).join('') || ''\n return `<li style=\"margin: 0 0 8px 0;\">${children}</li>`\n}\n\n/**\n * Convert blockquote node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertBlockquote(node: any): string {\n const children = node.children?.map(convertNode).join('') || ''\n const style = 'margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;'\n \n return `<blockquote style=\"${style}\">${children}</blockquote>`\n}\n\n/**\n * Convert text node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertText(node: any): string {\n let text = escapeHtml(node.text || '')\n \n // Apply formatting\n if (node.format & 1) { // Bold\n text = `<strong>${text}</strong>`\n }\n if (node.format & 2) { // Italic\n text = `<em>${text}</em>`\n }\n if (node.format & 8) { // Underline\n text = `<u>${text}</u>`\n }\n if (node.format & 4) { // Strikethrough\n text = `<strike>${text}</strike>`\n }\n \n return text\n}\n\n/**\n * Convert link node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertLink(node: any): string {\n const children = node.children?.map(convertNode).join('') || ''\n const url = node.fields?.url || '#'\n \n // Ensure links open in new tab and have security attributes\n return `<a href=\"${escapeHtml(url)}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: #2563eb; text-decoration: underline;\">${children}</a>`\n}\n\n/**\n * Get text alignment from format number\n */\nfunction getAlignment(format?: number): string {\n if (!format) return 'left'\n \n // Lexical alignment format values\n if (format & 2) return 'center'\n if (format & 3) return 'right'\n if (format & 4) return 'justify'\n \n return 'left'\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n }\n \n return text.replace(/[&<>\"']/g, m => map[m])\n}\n\n/**\n * Wrap content in a basic email template\n */\nfunction wrapInEmailTemplate(content: string, preheader?: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Email</title>\n <!--[if mso]>\n <noscript>\n <xml>\n <o:OfficeDocumentSettings>\n <o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml>\n </noscript>\n <![endif]-->\n</head>\n<body style=\"margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; background-color: #f3f4f6;\">\n ${preheader ? `<div style=\"display: none; max-height: 0; overflow: hidden;\">${escapeHtml(preheader)}</div>` : ''}\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"margin: 0; padding: 0;\">\n <tr>\n <td align=\"center\" style=\"padding: 20px 0;\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;\">\n <tr>\n <td style=\"padding: 40px 30px;\">\n ${content}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Extract personalization tags from content\n */\nexport function extractPersonalizationTags(html: string): string[] {\n const regex = /\\{\\{([^}]+)\\}\\}/g\n const tags: string[] = []\n let match\n \n while ((match = regex.exec(html)) !== null) {\n tags.push(match[1].trim())\n }\n \n return [...new Set(tags)]\n}\n\n/**\n * Replace personalization tags with sample data\n */\nexport function replacePersonalizationTags(\n html: string, \n sampleData: Record<string, string>\n): string {\n return html.replace(/\\{\\{([^}]+)\\}\\}/g, (match, tag) => {\n const trimmedTag = tag.trim()\n return sampleData[trimmedTag] || match\n })\n}","/**\n * Email HTML validation utilities\n */\n\nexport interface ValidationResult {\n valid: boolean\n warnings: string[]\n errors: string[]\n stats: {\n sizeInBytes: number\n imageCount: number\n linkCount: number\n hasExternalStyles: boolean\n hasJavaScript: boolean\n }\n}\n\n/**\n * Validate HTML for email compatibility\n */\nexport function validateEmailHtml(html: string): ValidationResult {\n const warnings: string[] = []\n const errors: string[] = []\n \n // Calculate size\n const sizeInBytes = new Blob([html]).size\n \n // Check size limits\n if (sizeInBytes > 102400) { // 100KB\n warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`)\n }\n \n // Check for problematic CSS\n if (html.includes('position:') && (html.includes('position: absolute') || html.includes('position: fixed'))) {\n errors.push('Absolute/fixed positioning is not supported in most email clients')\n }\n \n if (html.includes('display: flex') || html.includes('display: grid')) {\n errors.push('Flexbox and Grid layouts are not supported in many email clients')\n }\n \n if (html.includes('@media')) {\n warnings.push('Media queries may not work in all email clients')\n }\n \n // Check for JavaScript\n const hasJavaScript = \n html.includes('<script') || \n html.includes('onclick') || \n html.includes('onload') ||\n html.includes('javascript:')\n \n if (hasJavaScript) {\n errors.push('JavaScript is not supported in email and will be stripped by email clients')\n }\n \n // Check for external styles\n const hasExternalStyles = html.includes('<link') && html.includes('stylesheet')\n if (hasExternalStyles) {\n errors.push('External stylesheets are not supported - use inline styles only')\n }\n \n // Check for forms\n if (html.includes('<form') || html.includes('<input') || html.includes('<button')) {\n errors.push('Forms and form elements are not reliably supported in email')\n }\n \n // Check for unsupported tags\n const unsupportedTags = [\n 'video', 'audio', 'iframe', 'embed', 'object', 'canvas', 'svg'\n ]\n \n for (const tag of unsupportedTags) {\n if (html.includes(`<${tag}`)) {\n errors.push(`<${tag}> tags are not supported in email`)\n }\n }\n \n // Count images and links\n const imageCount = (html.match(/<img/g) || []).length\n const linkCount = (html.match(/<a/g) || []).length\n \n // Check image usage\n if (imageCount > 20) {\n warnings.push(`High number of images (${imageCount}) may affect email performance`)\n }\n \n // Check for missing alt text\n const imagesWithoutAlt = (html.match(/<img(?![^>]*\\balt\\s*=)[^>]*>/g) || []).length\n if (imagesWithoutAlt > 0) {\n warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`)\n }\n \n // Check for proper link attributes\n const linksWithoutTarget = (html.match(/<a(?![^>]*\\btarget\\s*=)[^>]*>/g) || []).length\n if (linksWithoutTarget > 0) {\n warnings.push(`${linksWithoutTarget} link(s) missing target=\"_blank\" attribute`)\n }\n \n // Check for CSS property usage\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.push('margin: auto is not supported in Outlook - use align=\"center\" or tables for centering')\n }\n \n if (html.includes('background-image')) {\n warnings.push('Background images are not reliably supported - consider using <img> tags instead')\n }\n \n // Check for rem/em units\n if (html.match(/\\d+\\s*(rem|em)/)) {\n warnings.push('rem/em units may render inconsistently - use px for reliable sizing')\n }\n \n // Check for negative margins\n if (html.match(/margin[^:]*:\\s*-\\d+/)) {\n errors.push('Negative margins are not supported in many email clients')\n }\n \n // Validate personalization tags\n const personalizationTags = html.match(/\\{\\{([^}]+)\\}\\}/g) || []\n const validTags = ['subscriber.name', 'subscriber.email', 'subscriber.firstName', 'subscriber.lastName']\n \n for (const tag of personalizationTags) {\n const tagContent = tag.replace(/[{}]/g, '').trim()\n if (!validTags.includes(tagContent)) {\n warnings.push(`Unknown personalization tag: ${tag}`)\n }\n }\n \n return {\n valid: errors.length === 0,\n warnings,\n errors,\n stats: {\n sizeInBytes,\n imageCount,\n linkCount,\n hasExternalStyles,\n hasJavaScript,\n }\n }\n}\n\n/**\n * Get email client compatibility warnings for specific HTML\n */\nexport function getClientCompatibilityWarnings(html: string): Record<string, string[]> {\n const warnings: Record<string, string[]> = {\n gmail: [],\n outlook: [],\n appleMail: [],\n mobile: [],\n }\n \n // Gmail specific\n if (html.includes('<style')) {\n warnings.gmail.push('Gmail may strip <style> tags in some contexts')\n }\n \n // Outlook specific\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.outlook.push('Outlook does not support margin: auto')\n }\n \n if (html.includes('padding') && html.includes('<p')) {\n warnings.outlook.push('Outlook may not respect padding on <p> tags')\n }\n \n if (html.includes('background-image')) {\n warnings.outlook.push('Outlook has limited background image support')\n }\n \n // Mobile specific\n const hasSmallText = html.match(/font-size:\\s*(\\d+)px/g)?.some(match => {\n const size = parseInt(match.match(/\\d+/)?.[0] || '16')\n return size < 14\n })\n \n if (hasSmallText) {\n warnings.mobile.push('Text smaller than 14px may be hard to read on mobile')\n }\n \n const hasSmallLinks = html.match(/<a[^>]*>[^<]{1,3}<\\/a>/g)\n if (hasSmallLinks) {\n warnings.mobile.push('Short link text may be hard to tap on mobile devices')\n }\n \n return warnings\n}\n\n/**\n * Suggest fixes for common email HTML issues\n */\nexport function suggestFixes(html: string): string[] {\n const suggestions: string[] = []\n \n if (html.includes('display: flex')) {\n suggestions.push('Replace flexbox with table-based layouts for better email client support')\n }\n \n if (html.includes('position: absolute')) {\n suggestions.push('Use table cells or margins instead of absolute positioning')\n }\n \n if (html.match(/\\d+rem/) || html.match(/\\d+em/)) {\n suggestions.push('Convert rem/em units to px for consistent rendering')\n }\n \n if (!html.includes('<!DOCTYPE')) {\n suggestions.push('Add <!DOCTYPE html> declaration for better rendering')\n }\n \n if (!html.includes('charset')) {\n suggestions.push('Add <meta charset=\"UTF-8\"> for proper character encoding')\n }\n \n return suggestions\n}"],"mappings":"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","names":["DOMPurify"]}
|
|
1
|
+
{"version":3,"sources":["../src/exports/utils.ts","../src/utils/emailSafeHtml.ts","../src/utils/validateEmailHtml.ts"],"sourcesContent":["// Email utilities\nexport { convertToEmailSafeHtml, EMAIL_SAFE_CONFIG } from '../utils/emailSafeHtml'\nexport { validateEmailHtml } from '../utils/validateEmailHtml'\nexport type { ValidationResult } from '../utils/validateEmailHtml'","import DOMPurify from 'isomorphic-dompurify'\nimport type { SerializedEditorState } from 'lexical'\n\n/**\n * DOMPurify configuration for email-safe HTML\n */\nexport const EMAIL_SAFE_CONFIG = {\n ALLOWED_TAGS: [\n 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'strike', 's', 'span',\n 'a', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote', 'hr',\n 'img', 'div', 'table', 'tr', 'td', 'th', 'tbody', 'thead'\n ],\n ALLOWED_ATTR: ['href', 'style', 'target', 'rel', 'align', 'src', 'alt', 'width', 'height', 'border', 'cellpadding', 'cellspacing'],\n ALLOWED_STYLES: {\n '*': [\n 'color', 'background-color', 'font-size', 'font-weight',\n 'font-style', 'text-decoration', 'text-align', 'margin',\n 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',\n 'padding', 'padding-top', 'padding-right', 'padding-bottom', \n 'padding-left', 'line-height', 'border-left', 'border-left-width',\n 'border-left-style', 'border-left-color'\n ],\n },\n FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input'],\n FORBID_ATTR: ['class', 'id', 'onclick', 'onload', 'onerror'],\n}\n\n/**\n * Converts Lexical editor state to email-safe HTML\n */\nexport async function convertToEmailSafeHtml(\n editorState: SerializedEditorState,\n options?: {\n wrapInTemplate?: boolean\n preheader?: string\n mediaUrl?: string // Base URL for media files\n }\n): Promise<string> {\n // First, convert Lexical state to HTML using custom converters\n const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl)\n \n // Sanitize the HTML\n const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG)\n \n // Optionally wrap in email template\n if (options?.wrapInTemplate) {\n return wrapInEmailTemplate(sanitizedHtml, options.preheader)\n }\n \n return sanitizedHtml\n}\n\n/**\n * Custom Lexical to HTML converter for email\n */\nasync function lexicalToEmailHtml(editorState: SerializedEditorState, mediaUrl?: string): Promise<string> {\n const { root } = editorState\n \n if (!root || !root.children) {\n return ''\n }\n \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const html = root.children.map((node: any) => convertNode(node, mediaUrl)).join('')\n return html\n}\n\n/**\n * Convert individual Lexical nodes to email-safe HTML\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertNode(node: any, mediaUrl?: string): string {\n switch (node.type) {\n case 'paragraph':\n return convertParagraph(node, mediaUrl)\n case 'heading':\n return convertHeading(node, mediaUrl)\n case 'list':\n return convertList(node, mediaUrl)\n case 'listitem':\n return convertListItem(node, mediaUrl)\n case 'blockquote':\n return convertBlockquote(node, mediaUrl)\n case 'text':\n return convertText(node)\n case 'link':\n return convertLink(node, mediaUrl)\n case 'linebreak':\n return '<br>'\n case 'upload':\n return convertUpload(node, mediaUrl)\n case 'block':\n return convertBlock(node, mediaUrl)\n default:\n // Unknown node type - convert children if any\n if (node.children) {\n return node.children.map((child: any) => convertNode(child, mediaUrl)).join('')\n }\n return ''\n }\n}\n\n/**\n * Convert paragraph node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertParagraph(node: any, mediaUrl?: string): string {\n const align = getAlignment(node.format)\n const children = node.children?.map((child: any) => convertNode(child, mediaUrl)).join('') || ''\n \n if (!children.trim()) {\n return '<p style=\"margin: 0 0 16px 0; min-height: 1em;\"> </p>'\n }\n \n return `<p style=\"margin: 0 0 16px 0; text-align: ${align};\">${children}</p>`\n}\n\n/**\n * Convert heading node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertHeading(node: any, mediaUrl?: string): string {\n const tag = node.tag || 'h1'\n const align = getAlignment(node.format)\n const children = node.children?.map((child: any) => convertNode(child, mediaUrl)).join('') || ''\n \n const styles: Record<string, string> = {\n h1: 'font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;',\n h2: 'font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;',\n h3: 'font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;',\n }\n \n const style = `${styles[tag] || styles.h3} text-align: ${align};`\n \n return `<${tag} style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertList(node: any, mediaUrl?: string): string {\n const tag = node.listType === 'number' ? 'ol' : 'ul'\n const children = node.children?.map((child: any) => convertNode(child, mediaUrl)).join('') || ''\n \n const style = tag === 'ul' \n ? 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;'\n : 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;'\n \n return `<${tag} style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list item node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertListItem(node: any, mediaUrl?: string): string {\n const children = node.children?.map((child: any) => convertNode(child, mediaUrl)).join('') || ''\n return `<li style=\"margin: 0 0 8px 0;\">${children}</li>`\n}\n\n/**\n * Convert blockquote node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertBlockquote(node: any, mediaUrl?: string): string {\n const children = node.children?.map((child: any) => convertNode(child, mediaUrl)).join('') || ''\n const style = 'margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;'\n \n return `<blockquote style=\"${style}\">${children}</blockquote>`\n}\n\n/**\n * Convert text node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertText(node: any): string {\n let text = escapeHtml(node.text || '')\n \n // Apply formatting\n if (node.format & 1) { // Bold\n text = `<strong>${text}</strong>`\n }\n if (node.format & 2) { // Italic\n text = `<em>${text}</em>`\n }\n if (node.format & 8) { // Underline\n text = `<u>${text}</u>`\n }\n if (node.format & 4) { // Strikethrough\n text = `<strike>${text}</strike>`\n }\n \n return text\n}\n\n/**\n * Convert link node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertLink(node: any, mediaUrl?: string): string {\n const children = node.children?.map((child: any) => convertNode(child, mediaUrl)).join('') || ''\n const url = node.fields?.url || '#'\n const newTab = node.fields?.newTab ?? false\n \n // Add target and rel attributes based on newTab setting\n const targetAttr = newTab ? ' target=\"_blank\"' : ''\n const relAttr = newTab ? ' rel=\"noopener noreferrer\"' : ''\n \n return `<a href=\"${escapeHtml(url)}\"${targetAttr}${relAttr} style=\"color: #2563eb; text-decoration: underline;\">${children}</a>`\n}\n\n/**\n * Convert upload (image) node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertUpload(node: any, mediaUrl?: string): string {\n const upload = node.value\n if (!upload) return ''\n \n // Get image URL - handle both direct URL and media object\n let src = ''\n if (typeof upload === 'string') {\n src = upload\n } else if (upload.url) {\n src = upload.url\n } else if (upload.filename && mediaUrl) {\n // Construct URL from media URL and filename\n src = `${mediaUrl}/${upload.filename}`\n }\n \n const alt = node.fields?.altText || upload.alt || ''\n const caption = node.fields?.caption || ''\n \n // Email-safe image with max-width for responsiveness\n const imgHtml = `<img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(alt)}\" style=\"max-width: 100%; height: auto; display: block; margin: 0 auto;\" />`\n \n if (caption) {\n return `\n <div style=\"margin: 0 0 16px 0; text-align: center;\">\n ${imgHtml}\n <p style=\"margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;\">${escapeHtml(caption)}</p>\n </div>\n `\n }\n \n return `<div style=\"margin: 0 0 16px 0; text-align: center;\">${imgHtml}</div>`\n}\n\n/**\n * Convert custom block node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertBlock(node: any, mediaUrl?: string): string {\n const blockType = node.fields?.blockName\n \n switch (blockType) {\n case 'button':\n return convertButtonBlock(node.fields)\n case 'divider':\n return convertDividerBlock(node.fields)\n default:\n // Unknown block type - try to convert children\n if (node.children) {\n return node.children.map((child: any) => convertNode(child, mediaUrl)).join('')\n }\n return ''\n }\n}\n\n/**\n * Convert button block\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertButtonBlock(fields: any): string {\n const text = fields?.text || 'Click here'\n const url = fields?.url || '#'\n const style = fields?.style || 'primary'\n \n const styles: Record<string, string> = {\n primary: 'background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;',\n secondary: 'background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;',\n outline: 'background-color: transparent; color: #2563eb; border: 2px solid #2563eb;',\n }\n \n const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`\n \n return `\n <div style=\"margin: 0 0 16px 0; text-align: center;\">\n <a href=\"${escapeHtml(url)}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"${buttonStyle}\">${escapeHtml(text)}</a>\n </div>\n `\n}\n\n/**\n * Convert divider block\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertDividerBlock(fields: any): string {\n const style = fields?.style || 'solid'\n \n const styles: Record<string, string> = {\n solid: 'border-top: 1px solid #e5e7eb;',\n dashed: 'border-top: 1px dashed #e5e7eb;',\n dotted: 'border-top: 1px dotted #e5e7eb;',\n }\n \n return `<hr style=\"${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;\" />`\n}\n\n/**\n * Get text alignment from format number\n */\nfunction getAlignment(format?: number): string {\n if (!format) return 'left'\n \n // Lexical alignment format values\n if (format & 2) return 'center'\n if (format & 3) return 'right'\n if (format & 4) return 'justify'\n \n return 'left'\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n }\n \n return text.replace(/[&<>\"']/g, m => map[m])\n}\n\n/**\n * Wrap content in a basic email template\n */\nfunction wrapInEmailTemplate(content: string, preheader?: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Email</title>\n <!--[if mso]>\n <noscript>\n <xml>\n <o:OfficeDocumentSettings>\n <o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml>\n </noscript>\n <![endif]-->\n</head>\n<body style=\"margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; background-color: #f3f4f6;\">\n ${preheader ? `<div style=\"display: none; max-height: 0; overflow: hidden;\">${escapeHtml(preheader)}</div>` : ''}\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"margin: 0; padding: 0;\">\n <tr>\n <td align=\"center\" style=\"padding: 20px 0;\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\" style=\"margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;\">\n <tr>\n <td style=\"padding: 40px 30px;\">\n ${content}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Extract personalization tags from content\n */\nexport function extractPersonalizationTags(html: string): string[] {\n const regex = /\\{\\{([^}]+)\\}\\}/g\n const tags: string[] = []\n let match\n \n while ((match = regex.exec(html)) !== null) {\n tags.push(match[1].trim())\n }\n \n return [...new Set(tags)]\n}\n\n/**\n * Replace personalization tags with sample data\n */\nexport function replacePersonalizationTags(\n html: string, \n sampleData: Record<string, string>\n): string {\n return html.replace(/\\{\\{([^}]+)\\}\\}/g, (match, tag) => {\n const trimmedTag = tag.trim()\n return sampleData[trimmedTag] || match\n })\n}","/**\n * Email HTML validation utilities\n */\n\nexport interface ValidationResult {\n valid: boolean\n warnings: string[]\n errors: string[]\n stats: {\n sizeInBytes: number\n imageCount: number\n linkCount: number\n hasExternalStyles: boolean\n hasJavaScript: boolean\n }\n}\n\n/**\n * Validate HTML for email compatibility\n */\nexport function validateEmailHtml(html: string): ValidationResult {\n const warnings: string[] = []\n const errors: string[] = []\n \n // Calculate size\n const sizeInBytes = new Blob([html]).size\n \n // Check size limits\n if (sizeInBytes > 102400) { // 100KB\n warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`)\n }\n \n // Check for problematic CSS\n if (html.includes('position:') && (html.includes('position: absolute') || html.includes('position: fixed'))) {\n errors.push('Absolute/fixed positioning is not supported in most email clients')\n }\n \n if (html.includes('display: flex') || html.includes('display: grid')) {\n errors.push('Flexbox and Grid layouts are not supported in many email clients')\n }\n \n if (html.includes('@media')) {\n warnings.push('Media queries may not work in all email clients')\n }\n \n // Check for JavaScript\n const hasJavaScript = \n html.includes('<script') || \n html.includes('onclick') || \n html.includes('onload') ||\n html.includes('javascript:')\n \n if (hasJavaScript) {\n errors.push('JavaScript is not supported in email and will be stripped by email clients')\n }\n \n // Check for external styles\n const hasExternalStyles = html.includes('<link') && html.includes('stylesheet')\n if (hasExternalStyles) {\n errors.push('External stylesheets are not supported - use inline styles only')\n }\n \n // Check for forms\n if (html.includes('<form') || html.includes('<input') || html.includes('<button')) {\n errors.push('Forms and form elements are not reliably supported in email')\n }\n \n // Check for unsupported tags\n const unsupportedTags = [\n 'video', 'audio', 'iframe', 'embed', 'object', 'canvas', 'svg'\n ]\n \n for (const tag of unsupportedTags) {\n if (html.includes(`<${tag}`)) {\n errors.push(`<${tag}> tags are not supported in email`)\n }\n }\n \n // Count images and links\n const imageCount = (html.match(/<img/g) || []).length\n const linkCount = (html.match(/<a/g) || []).length\n \n // Check image usage\n if (imageCount > 20) {\n warnings.push(`High number of images (${imageCount}) may affect email performance`)\n }\n \n // Check for missing alt text\n const imagesWithoutAlt = (html.match(/<img(?![^>]*\\balt\\s*=)[^>]*>/g) || []).length\n if (imagesWithoutAlt > 0) {\n warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`)\n }\n \n // Check for proper link attributes\n const linksWithoutTarget = (html.match(/<a(?![^>]*\\btarget\\s*=)[^>]*>/g) || []).length\n if (linksWithoutTarget > 0) {\n warnings.push(`${linksWithoutTarget} link(s) missing target=\"_blank\" attribute`)\n }\n \n // Check for CSS property usage\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.push('margin: auto is not supported in Outlook - use align=\"center\" or tables for centering')\n }\n \n if (html.includes('background-image')) {\n warnings.push('Background images are not reliably supported - consider using <img> tags instead')\n }\n \n // Check for rem/em units\n if (html.match(/\\d+\\s*(rem|em)/)) {\n warnings.push('rem/em units may render inconsistently - use px for reliable sizing')\n }\n \n // Check for negative margins\n if (html.match(/margin[^:]*:\\s*-\\d+/)) {\n errors.push('Negative margins are not supported in many email clients')\n }\n \n // Validate personalization tags\n const personalizationTags = html.match(/\\{\\{([^}]+)\\}\\}/g) || []\n const validTags = ['subscriber.name', 'subscriber.email', 'subscriber.firstName', 'subscriber.lastName']\n \n for (const tag of personalizationTags) {\n const tagContent = tag.replace(/[{}]/g, '').trim()\n if (!validTags.includes(tagContent)) {\n warnings.push(`Unknown personalization tag: ${tag}`)\n }\n }\n \n return {\n valid: errors.length === 0,\n warnings,\n errors,\n stats: {\n sizeInBytes,\n imageCount,\n linkCount,\n hasExternalStyles,\n hasJavaScript,\n }\n }\n}\n\n/**\n * Get email client compatibility warnings for specific HTML\n */\nexport function getClientCompatibilityWarnings(html: string): Record<string, string[]> {\n const warnings: Record<string, string[]> = {\n gmail: [],\n outlook: [],\n appleMail: [],\n mobile: [],\n }\n \n // Gmail specific\n if (html.includes('<style')) {\n warnings.gmail.push('Gmail may strip <style> tags in some contexts')\n }\n \n // Outlook specific\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.outlook.push('Outlook does not support margin: auto')\n }\n \n if (html.includes('padding') && html.includes('<p')) {\n warnings.outlook.push('Outlook may not respect padding on <p> tags')\n }\n \n if (html.includes('background-image')) {\n warnings.outlook.push('Outlook has limited background image support')\n }\n \n // Mobile specific\n const hasSmallText = html.match(/font-size:\\s*(\\d+)px/g)?.some(match => {\n const size = parseInt(match.match(/\\d+/)?.[0] || '16')\n return size < 14\n })\n \n if (hasSmallText) {\n warnings.mobile.push('Text smaller than 14px may be hard to read on mobile')\n }\n \n const hasSmallLinks = html.match(/<a[^>]*>[^<]{1,3}<\\/a>/g)\n if (hasSmallLinks) {\n warnings.mobile.push('Short link text may be hard to tap on mobile devices')\n }\n \n return warnings\n}\n\n/**\n * Suggest fixes for common email HTML issues\n */\nexport function suggestFixes(html: string): string[] {\n const suggestions: string[] = []\n \n if (html.includes('display: flex')) {\n suggestions.push('Replace flexbox with table-based layouts for better email client support')\n }\n \n if (html.includes('position: absolute')) {\n suggestions.push('Use table cells or margins instead of absolute positioning')\n }\n \n if (html.match(/\\d+rem/) || html.match(/\\d+em/)) {\n suggestions.push('Convert rem/em units to px for consistent rendering')\n }\n \n if (!html.includes('<!DOCTYPE')) {\n suggestions.push('Add <!DOCTYPE html> declaration for better rendering')\n }\n \n if (!html.includes('charset')) {\n suggestions.push('Add <meta charset=\"UTF-8\"> for proper character encoding')\n }\n \n return suggestions\n}"],"mappings":"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","names":["DOMPurify"]}
|
package/dist/utils.d.cts
CHANGED
package/dist/utils.d.ts
CHANGED
package/dist/utils.js
CHANGED
|
@@ -20,9 +20,17 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
20
20
|
"ol",
|
|
21
21
|
"li",
|
|
22
22
|
"blockquote",
|
|
23
|
-
"hr"
|
|
23
|
+
"hr",
|
|
24
|
+
"img",
|
|
25
|
+
"div",
|
|
26
|
+
"table",
|
|
27
|
+
"tr",
|
|
28
|
+
"td",
|
|
29
|
+
"th",
|
|
30
|
+
"tbody",
|
|
31
|
+
"thead"
|
|
24
32
|
],
|
|
25
|
-
ALLOWED_ATTR: ["href", "style", "target", "rel", "align"],
|
|
33
|
+
ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
|
|
26
34
|
ALLOWED_STYLES: {
|
|
27
35
|
"*": [
|
|
28
36
|
"color",
|
|
@@ -53,58 +61,62 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
53
61
|
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
|
|
54
62
|
};
|
|
55
63
|
async function convertToEmailSafeHtml(editorState, options) {
|
|
56
|
-
const rawHtml = await lexicalToEmailHtml(editorState);
|
|
64
|
+
const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
|
|
57
65
|
const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
|
|
58
66
|
if (options?.wrapInTemplate) {
|
|
59
67
|
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
|
|
60
68
|
}
|
|
61
69
|
return sanitizedHtml;
|
|
62
70
|
}
|
|
63
|
-
async function lexicalToEmailHtml(editorState) {
|
|
71
|
+
async function lexicalToEmailHtml(editorState, mediaUrl) {
|
|
64
72
|
const { root } = editorState;
|
|
65
73
|
if (!root || !root.children) {
|
|
66
74
|
return "";
|
|
67
75
|
}
|
|
68
|
-
const html = root.children.map((node) => convertNode(node)).join("");
|
|
76
|
+
const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
|
|
69
77
|
return html;
|
|
70
78
|
}
|
|
71
|
-
function convertNode(node) {
|
|
79
|
+
function convertNode(node, mediaUrl) {
|
|
72
80
|
switch (node.type) {
|
|
73
81
|
case "paragraph":
|
|
74
|
-
return convertParagraph(node);
|
|
82
|
+
return convertParagraph(node, mediaUrl);
|
|
75
83
|
case "heading":
|
|
76
|
-
return convertHeading(node);
|
|
84
|
+
return convertHeading(node, mediaUrl);
|
|
77
85
|
case "list":
|
|
78
|
-
return convertList(node);
|
|
86
|
+
return convertList(node, mediaUrl);
|
|
79
87
|
case "listitem":
|
|
80
|
-
return convertListItem(node);
|
|
88
|
+
return convertListItem(node, mediaUrl);
|
|
81
89
|
case "blockquote":
|
|
82
|
-
return convertBlockquote(node);
|
|
90
|
+
return convertBlockquote(node, mediaUrl);
|
|
83
91
|
case "text":
|
|
84
92
|
return convertText(node);
|
|
85
93
|
case "link":
|
|
86
|
-
return convertLink(node);
|
|
94
|
+
return convertLink(node, mediaUrl);
|
|
87
95
|
case "linebreak":
|
|
88
96
|
return "<br>";
|
|
97
|
+
case "upload":
|
|
98
|
+
return convertUpload(node, mediaUrl);
|
|
99
|
+
case "block":
|
|
100
|
+
return convertBlock(node, mediaUrl);
|
|
89
101
|
default:
|
|
90
102
|
if (node.children) {
|
|
91
|
-
return node.children.map(convertNode).join("");
|
|
103
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
92
104
|
}
|
|
93
105
|
return "";
|
|
94
106
|
}
|
|
95
107
|
}
|
|
96
|
-
function convertParagraph(node) {
|
|
108
|
+
function convertParagraph(node, mediaUrl) {
|
|
97
109
|
const align = getAlignment(node.format);
|
|
98
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
110
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
99
111
|
if (!children.trim()) {
|
|
100
112
|
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
|
|
101
113
|
}
|
|
102
114
|
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
|
|
103
115
|
}
|
|
104
|
-
function convertHeading(node) {
|
|
116
|
+
function convertHeading(node, mediaUrl) {
|
|
105
117
|
const tag = node.tag || "h1";
|
|
106
118
|
const align = getAlignment(node.format);
|
|
107
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
119
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
108
120
|
const styles = {
|
|
109
121
|
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
|
|
110
122
|
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
|
|
@@ -113,18 +125,18 @@ function convertHeading(node) {
|
|
|
113
125
|
const style = `${styles[tag] || styles.h3} text-align: ${align};`;
|
|
114
126
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
115
127
|
}
|
|
116
|
-
function convertList(node) {
|
|
128
|
+
function convertList(node, mediaUrl) {
|
|
117
129
|
const tag = node.listType === "number" ? "ol" : "ul";
|
|
118
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
130
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
119
131
|
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
|
|
120
132
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
121
133
|
}
|
|
122
|
-
function convertListItem(node) {
|
|
123
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
134
|
+
function convertListItem(node, mediaUrl) {
|
|
135
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
124
136
|
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
|
|
125
137
|
}
|
|
126
|
-
function convertBlockquote(node) {
|
|
127
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
138
|
+
function convertBlockquote(node, mediaUrl) {
|
|
139
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
128
140
|
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
|
|
129
141
|
return `<blockquote style="${style}">${children}</blockquote>`;
|
|
130
142
|
}
|
|
@@ -144,10 +156,76 @@ function convertText(node) {
|
|
|
144
156
|
}
|
|
145
157
|
return text;
|
|
146
158
|
}
|
|
147
|
-
function convertLink(node) {
|
|
148
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
159
|
+
function convertLink(node, mediaUrl) {
|
|
160
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
149
161
|
const url = node.fields?.url || "#";
|
|
150
|
-
|
|
162
|
+
const newTab = node.fields?.newTab ?? false;
|
|
163
|
+
const targetAttr = newTab ? ' target="_blank"' : "";
|
|
164
|
+
const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
|
|
165
|
+
return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
|
|
166
|
+
}
|
|
167
|
+
function convertUpload(node, mediaUrl) {
|
|
168
|
+
const upload = node.value;
|
|
169
|
+
if (!upload) return "";
|
|
170
|
+
let src = "";
|
|
171
|
+
if (typeof upload === "string") {
|
|
172
|
+
src = upload;
|
|
173
|
+
} else if (upload.url) {
|
|
174
|
+
src = upload.url;
|
|
175
|
+
} else if (upload.filename && mediaUrl) {
|
|
176
|
+
src = `${mediaUrl}/${upload.filename}`;
|
|
177
|
+
}
|
|
178
|
+
const alt = node.fields?.altText || upload.alt || "";
|
|
179
|
+
const caption = node.fields?.caption || "";
|
|
180
|
+
const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
|
|
181
|
+
if (caption) {
|
|
182
|
+
return `
|
|
183
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
184
|
+
${imgHtml}
|
|
185
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
|
|
186
|
+
</div>
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
|
|
190
|
+
}
|
|
191
|
+
function convertBlock(node, mediaUrl) {
|
|
192
|
+
const blockType = node.fields?.blockName;
|
|
193
|
+
switch (blockType) {
|
|
194
|
+
case "button":
|
|
195
|
+
return convertButtonBlock(node.fields);
|
|
196
|
+
case "divider":
|
|
197
|
+
return convertDividerBlock(node.fields);
|
|
198
|
+
default:
|
|
199
|
+
if (node.children) {
|
|
200
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
201
|
+
}
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function convertButtonBlock(fields) {
|
|
206
|
+
const text = fields?.text || "Click here";
|
|
207
|
+
const url = fields?.url || "#";
|
|
208
|
+
const style = fields?.style || "primary";
|
|
209
|
+
const styles = {
|
|
210
|
+
primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
|
|
211
|
+
secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
|
|
212
|
+
outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
|
|
213
|
+
};
|
|
214
|
+
const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
|
|
215
|
+
return `
|
|
216
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
217
|
+
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
|
|
218
|
+
</div>
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
function convertDividerBlock(fields) {
|
|
222
|
+
const style = fields?.style || "solid";
|
|
223
|
+
const styles = {
|
|
224
|
+
solid: "border-top: 1px solid #e5e7eb;",
|
|
225
|
+
dashed: "border-top: 1px dashed #e5e7eb;",
|
|
226
|
+
dotted: "border-top: 1px dotted #e5e7eb;"
|
|
227
|
+
};
|
|
228
|
+
return `<hr style="${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
|
|
151
229
|
}
|
|
152
230
|
function getAlignment(format) {
|
|
153
231
|
if (!format) return "left";
|