payload-plugin-newsletter 0.16.10 → 0.17.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/dist/types.d.cts CHANGED
@@ -519,6 +519,13 @@ interface BroadcastCustomizations {
519
519
  fieldOverrides?: {
520
520
  content?: (defaultField: RichTextField) => RichTextField;
521
521
  };
522
+ /**
523
+ * Custom block email converter
524
+ * @param node - The block node from Lexical editor state
525
+ * @param mediaUrl - Base URL for media files
526
+ * @returns Promise<string> - The email-safe HTML for the block
527
+ */
528
+ customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>;
522
529
  }
523
530
  interface NewsletterPluginConfig {
524
531
  /**
package/dist/types.d.ts CHANGED
@@ -519,6 +519,13 @@ interface BroadcastCustomizations {
519
519
  fieldOverrides?: {
520
520
  content?: (defaultField: RichTextField) => RichTextField;
521
521
  };
522
+ /**
523
+ * Custom block email converter
524
+ * @param node - The block node from Lexical editor state
525
+ * @param mediaUrl - Base URL for media files
526
+ * @returns Promise<string> - The email-safe HTML for the block
527
+ */
528
+ customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>;
522
529
  }
523
530
  interface NewsletterPluginConfig {
524
531
  /**
package/dist/utils.cjs CHANGED
@@ -104,62 +104,73 @@ async function convertToEmailSafeHtml(editorState, options) {
104
104
  if (!editorState) {
105
105
  return "";
106
106
  }
107
- const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
107
+ const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
108
108
  const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
109
109
  if (options?.wrapInTemplate) {
110
110
  return wrapInEmailTemplate(sanitizedHtml, options.preheader);
111
111
  }
112
112
  return sanitizedHtml;
113
113
  }
114
- async function lexicalToEmailHtml(editorState, mediaUrl) {
114
+ async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
115
115
  const { root } = editorState;
116
116
  if (!root || !root.children) {
117
117
  return "";
118
118
  }
119
- const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
120
- return html;
119
+ const htmlParts = await Promise.all(
120
+ root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
121
+ );
122
+ return htmlParts.join("");
121
123
  }
122
- function convertNode(node, mediaUrl) {
124
+ async function convertNode(node, mediaUrl, customBlockConverter) {
123
125
  switch (node.type) {
124
126
  case "paragraph":
125
- return convertParagraph(node, mediaUrl);
127
+ return convertParagraph(node, mediaUrl, customBlockConverter);
126
128
  case "heading":
127
- return convertHeading(node, mediaUrl);
129
+ return convertHeading(node, mediaUrl, customBlockConverter);
128
130
  case "list":
129
- return convertList(node, mediaUrl);
131
+ return convertList(node, mediaUrl, customBlockConverter);
130
132
  case "listitem":
131
- return convertListItem(node, mediaUrl);
133
+ return convertListItem(node, mediaUrl, customBlockConverter);
132
134
  case "blockquote":
133
- return convertBlockquote(node, mediaUrl);
135
+ return convertBlockquote(node, mediaUrl, customBlockConverter);
134
136
  case "text":
135
137
  return convertText(node);
136
138
  case "link":
137
- return convertLink(node, mediaUrl);
139
+ return convertLink(node, mediaUrl, customBlockConverter);
138
140
  case "linebreak":
139
141
  return "<br>";
140
142
  case "upload":
141
143
  return convertUpload(node, mediaUrl);
142
144
  case "block":
143
- return convertBlock(node, mediaUrl);
145
+ return await convertBlock(node, mediaUrl, customBlockConverter);
144
146
  default:
145
147
  if (node.children) {
146
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
148
+ const childParts = await Promise.all(
149
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
150
+ );
151
+ return childParts.join("");
147
152
  }
148
153
  return "";
149
154
  }
150
155
  }
151
- function convertParagraph(node, mediaUrl) {
156
+ async function convertParagraph(node, mediaUrl, customBlockConverter) {
152
157
  const align = getAlignment(node.format);
153
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
158
+ const childParts = await Promise.all(
159
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
160
+ );
161
+ const children = childParts.join("");
154
162
  if (!children.trim()) {
155
163
  return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
156
164
  }
157
165
  return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
158
166
  }
159
- function convertHeading(node, mediaUrl) {
167
+ async function convertHeading(node, mediaUrl, customBlockConverter) {
160
168
  const tag = node.tag || "h1";
161
169
  const align = getAlignment(node.format);
162
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
170
+ const childParts = await Promise.all(
171
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
172
+ );
173
+ const children = childParts.join("");
163
174
  const styles = {
164
175
  h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
165
176
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
@@ -168,18 +179,27 @@ function convertHeading(node, mediaUrl) {
168
179
  const style = `${styles[tag] || styles.h3} text-align: ${align};`;
169
180
  return `<${tag} style="${style}">${children}</${tag}>`;
170
181
  }
171
- function convertList(node, mediaUrl) {
182
+ async function convertList(node, mediaUrl, customBlockConverter) {
172
183
  const tag = node.listType === "number" ? "ol" : "ul";
173
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
184
+ const childParts = await Promise.all(
185
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
186
+ );
187
+ const children = childParts.join("");
174
188
  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;";
175
189
  return `<${tag} style="${style}">${children}</${tag}>`;
176
190
  }
177
- function convertListItem(node, mediaUrl) {
178
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
191
+ async function convertListItem(node, mediaUrl, customBlockConverter) {
192
+ const childParts = await Promise.all(
193
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
194
+ );
195
+ const children = childParts.join("");
179
196
  return `<li style="margin: 0 0 8px 0;">${children}</li>`;
180
197
  }
181
- function convertBlockquote(node, mediaUrl) {
182
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
198
+ async function convertBlockquote(node, mediaUrl, customBlockConverter) {
199
+ const childParts = await Promise.all(
200
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
201
+ );
202
+ const children = childParts.join("");
183
203
  const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
184
204
  return `<blockquote style="${style}">${children}</blockquote>`;
185
205
  }
@@ -199,8 +219,11 @@ function convertText(node) {
199
219
  }
200
220
  return text;
201
221
  }
202
- function convertLink(node, mediaUrl) {
203
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
222
+ async function convertLink(node, mediaUrl, customBlockConverter) {
223
+ const childParts = await Promise.all(
224
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
225
+ );
226
+ const children = childParts.join("");
204
227
  const url = node.fields?.url || "#";
205
228
  const newTab = node.fields?.newTab ?? false;
206
229
  const targetAttr = newTab ? ' target="_blank"' : "";
@@ -231,8 +254,18 @@ function convertUpload(node, mediaUrl) {
231
254
  }
232
255
  return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
233
256
  }
234
- function convertBlock(node, mediaUrl) {
235
- const blockType = node.fields?.blockName;
257
+ async function convertBlock(node, mediaUrl, customBlockConverter) {
258
+ const blockType = node.fields?.blockName || node.blockName;
259
+ if (customBlockConverter) {
260
+ try {
261
+ const customHtml = await customBlockConverter(node, mediaUrl);
262
+ if (customHtml) {
263
+ return customHtml;
264
+ }
265
+ } catch (error) {
266
+ console.error(`Custom block converter error for ${blockType}:`, error);
267
+ }
268
+ }
236
269
  switch (blockType) {
237
270
  case "button":
238
271
  return convertButtonBlock(node.fields);
@@ -240,7 +273,10 @@ function convertBlock(node, mediaUrl) {
240
273
  return convertDividerBlock(node.fields);
241
274
  default:
242
275
  if (node.children) {
243
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
276
+ const childParts = await Promise.all(
277
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
278
+ );
279
+ return childParts.join("");
244
280
  }
245
281
  return "";
246
282
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/exports/utils.ts","../src/utils/emailSafeHtml.ts","../src/utils/validateEmailHtml.ts","../src/utils/getBroadcastConfig.ts","../src/utils/getResendConfig.ts"],"sourcesContent":["// Email utilities\nexport { convertToEmailSafeHtml, EMAIL_SAFE_CONFIG } from '../utils/emailSafeHtml'\nexport { validateEmailHtml } from '../utils/validateEmailHtml'\nexport type { ValidationResult } from '../utils/validateEmailHtml'\n\n// Configuration utilities\nexport { getBroadcastConfig } from '../utils/getBroadcastConfig'\nexport { getResendConfig } from '../utils/getResendConfig'","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 | undefined | null,\n options?: {\n wrapInTemplate?: boolean\n preheader?: string\n mediaUrl?: string // Base URL for media files\n }\n): Promise<string> {\n // Handle empty content\n if (!editorState) {\n return ''\n }\n \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;\">&nbsp;</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 '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#039;'\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}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, BroadcastProviderConfig } from '../types'\n\nexport async function getBroadcastConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<BroadcastProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'broadcast' && settings?.broadcastSettings) {\n return {\n apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || '',\n token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || '',\n replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.broadcast || null\n } catch (error) {\n req.payload.logger.error('Failed to get broadcast config from settings:', error)\n // Fall back to env var config on error\n return pluginConfig.providers?.broadcast || null\n }\n}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, ResendProviderConfig } from '../types'\n\nexport async function getResendConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<ResendProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'resend' && settings?.resendSettings) {\n return {\n apiKey: settings.resendSettings.apiKey || pluginConfig.providers?.resend?.apiKey || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.resend?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.resend?.fromName || '',\n audienceIds: settings.resendSettings.audienceIds || pluginConfig.providers?.resend?.audienceIds,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.resend || null\n } catch (error) {\n req.payload.logger.error('Failed to get resend config from settings:', error)\n // Fall back to env var config on error\n return pluginConfig.providers?.resend || null\n }\n}"],"mappings":"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","names":["DOMPurify"]}
1
+ {"version":3,"sources":["../src/exports/utils.ts","../src/utils/emailSafeHtml.ts","../src/utils/validateEmailHtml.ts","../src/utils/getBroadcastConfig.ts","../src/utils/getResendConfig.ts"],"sourcesContent":["// Email utilities\nexport { convertToEmailSafeHtml, EMAIL_SAFE_CONFIG } from '../utils/emailSafeHtml'\nexport { validateEmailHtml } from '../utils/validateEmailHtml'\nexport type { ValidationResult } from '../utils/validateEmailHtml'\n\n// Configuration utilities\nexport { getBroadcastConfig } from '../utils/getBroadcastConfig'\nexport { getResendConfig } from '../utils/getResendConfig'","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 | undefined | null,\n options?: {\n wrapInTemplate?: boolean\n preheader?: string\n mediaUrl?: string // Base URL for media files\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n }\n): Promise<string> {\n // Handle empty content\n if (!editorState) {\n return ''\n }\n \n // First, convert Lexical state to HTML using custom converters\n const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter)\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(\n editorState: SerializedEditorState, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const { root } = editorState\n \n if (!root || !root.children) {\n return ''\n }\n \n // Convert nodes asynchronously to support custom converters\n const htmlParts = await Promise.all(\n root.children.map((node: any) => convertNode(node, mediaUrl, customBlockConverter))\n )\n \n return htmlParts.join('')\n}\n\n/**\n * Convert individual Lexical nodes to email-safe HTML\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertNode(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n switch (node.type) {\n case 'paragraph':\n return convertParagraph(node, mediaUrl, customBlockConverter)\n case 'heading':\n return convertHeading(node, mediaUrl, customBlockConverter)\n case 'list':\n return convertList(node, mediaUrl, customBlockConverter)\n case 'listitem':\n return convertListItem(node, mediaUrl, customBlockConverter)\n case 'blockquote':\n return convertBlockquote(node, mediaUrl, customBlockConverter)\n case 'text':\n return convertText(node)\n case 'link':\n return convertLink(node, mediaUrl, customBlockConverter)\n case 'linebreak':\n return '<br>'\n case 'upload':\n return convertUpload(node, mediaUrl)\n case 'block':\n return await convertBlock(node, mediaUrl, customBlockConverter)\n default:\n // Unknown node type - convert children if any\n if (node.children) {\n const childParts = await Promise.all(\n node.children.map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n return childParts.join('')\n }\n return ''\n }\n}\n\n/**\n * Convert paragraph node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertParagraph(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const align = getAlignment(node.format)\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n \n if (!children.trim()) {\n return '<p style=\"margin: 0 0 16px 0; min-height: 1em;\">&nbsp;</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\nasync function convertHeading(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const tag = node.tag || 'h1'\n const align = getAlignment(node.format)\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.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\nasync function convertList(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const tag = node.listType === 'number' ? 'ol' : 'ul'\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.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\nasync function convertListItem(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.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\nasync function convertBlockquote(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.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\nasync function convertLink(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.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\nasync function convertBlock(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const blockType = node.fields?.blockName || node.blockName\n \n // First, check if there's a custom converter for this block\n if (customBlockConverter) {\n try {\n const customHtml = await customBlockConverter(node, mediaUrl)\n if (customHtml) {\n return customHtml\n }\n } catch (error) {\n console.error(`Custom block converter error for ${blockType}:`, error)\n // Fall through to default handling\n }\n }\n \n // Default handling for built-in blocks\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 const childParts = await Promise.all(\n node.children.map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n return childParts.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 '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#039;'\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}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, BroadcastProviderConfig } from '../types'\n\nexport async function getBroadcastConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<BroadcastProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'broadcast' && settings?.broadcastSettings) {\n return {\n apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || '',\n token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || '',\n replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.broadcast || null\n } catch (error) {\n req.payload.logger.error('Failed to get broadcast config from settings:', error)\n // Fall back to env var config on error\n return pluginConfig.providers?.broadcast || null\n }\n}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, ResendProviderConfig } from '../types'\n\nexport async function getResendConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<ResendProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'resend' && settings?.resendSettings) {\n return {\n apiKey: settings.resendSettings.apiKey || pluginConfig.providers?.resend?.apiKey || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.resend?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.resend?.fromName || '',\n audienceIds: settings.resendSettings.audienceIds || pluginConfig.providers?.resend?.audienceIds,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.resend || null\n } catch (error) {\n req.payload.logger.error('Failed to get resend config from settings:', error)\n // Fall back to env var config on error\n return pluginConfig.providers?.resend || null\n }\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kCAAsB;AAMf,IAAM,oBAAoB;AAAA,EAC/B,cAAc;AAAA,IACZ;AAAA,IAAK;AAAA,IAAM;AAAA,IAAU;AAAA,IAAK;AAAA,IAAM;AAAA,IAAK;AAAA,IAAK;AAAA,IAAU;AAAA,IAAK;AAAA,IACzD;AAAA,IAAK;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAc;AAAA,IACvD;AAAA,IAAO;AAAA,IAAO;AAAA,IAAS;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAS;AAAA,EACpD;AAAA,EACA,cAAc,CAAC,QAAQ,SAAS,UAAU,OAAO,SAAS,OAAO,OAAO,SAAS,UAAU,UAAU,eAAe,aAAa;AAAA,EACjI,gBAAgB;AAAA,IACd,KAAK;AAAA,MACH;AAAA,MAAS;AAAA,MAAoB;AAAA,MAAa;AAAA,MAC1C;AAAA,MAAc;AAAA,MAAmB;AAAA,MAAc;AAAA,MAC/C;AAAA,MAAc;AAAA,MAAgB;AAAA,MAAiB;AAAA,MAC/C;AAAA,MAAW;AAAA,MAAe;AAAA,MAAiB;AAAA,MAC3C;AAAA,MAAgB;AAAA,MAAe;AAAA,MAAe;AAAA,MAC9C;AAAA,MAAqB;AAAA,IACvB;AAAA,EACF;AAAA,EACA,aAAa,CAAC,UAAU,SAAS,UAAU,UAAU,SAAS,QAAQ,OAAO;AAAA,EAC7E,aAAa,CAAC,SAAS,MAAM,WAAW,UAAU,SAAS;AAC7D;AAKA,eAAsB,uBACpB,aACA,SAMiB;AAEjB,MAAI,CAAC,aAAa;AAChB,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM,mBAAmB,aAAa,SAAS,UAAU,SAAS,oBAAoB;AAGtG,QAAM,gBAAgB,4BAAAA,QAAU,SAAS,SAAS,iBAAiB;AAGnE,MAAI,SAAS,gBAAgB;AAC3B,WAAO,oBAAoB,eAAe,QAAQ,SAAS;AAAA,EAC7D;AAEA,SAAO;AACT;AAKA,eAAe,mBACb,aACA,UACA,sBACiB;AACjB,QAAM,EAAE,KAAK,IAAI;AAEjB,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,MAAM,QAAQ;AAAA,IAC9B,KAAK,SAAS,IAAI,CAAC,SAAc,YAAY,MAAM,UAAU,oBAAoB,CAAC;AAAA,EACpF;AAEA,SAAO,UAAU,KAAK,EAAE;AAC1B;AAMA,eAAe,YACb,MACA,UACA,sBACiB;AACjB,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,iBAAiB,MAAM,UAAU,oBAAoB;AAAA,IAC9D,KAAK;AACH,aAAO,eAAe,MAAM,UAAU,oBAAoB;AAAA,IAC5D,KAAK;AACH,aAAO,YAAY,MAAM,UAAU,oBAAoB;AAAA,IACzD,KAAK;AACH,aAAO,gBAAgB,MAAM,UAAU,oBAAoB;AAAA,IAC7D,KAAK;AACH,aAAO,kBAAkB,MAAM,UAAU,oBAAoB;AAAA,IAC/D,KAAK;AACH,aAAO,YAAY,IAAI;AAAA,IACzB,KAAK;AACH,aAAO,YAAY,MAAM,UAAU,oBAAoB;AAAA,IACzD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,cAAc,MAAM,QAAQ;AAAA,IACrC,KAAK;AACH,aAAO,MAAM,aAAa,MAAM,UAAU,oBAAoB;AAAA,IAChE;AAEE,UAAI,KAAK,UAAU;AACjB,cAAM,aAAa,MAAM,QAAQ;AAAA,UAC/B,KAAK,SAAS,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,QACtF;AACA,eAAO,WAAW,KAAK,EAAE;AAAA,MAC3B;AACA,aAAO;AAAA,EACX;AACF;AAMA,eAAe,iBACb,MACA,UACA,sBACiB;AACjB,QAAM,QAAQ,aAAa,KAAK,MAAM;AACtC,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AAEnC,MAAI,CAAC,SAAS,KAAK,GAAG;AACpB,WAAO;AAAA,EACT;AAEA,SAAO,6CAA6C,KAAK,MAAM,QAAQ;AACzE;AAMA,eAAe,eACb,MACA,UACA,sBACiB;AACjB,QAAM,MAAM,KAAK,OAAO;AACxB,QAAM,QAAQ,aAAa,KAAK,MAAM;AACtC,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AAEnC,QAAM,SAAiC;AAAA,IACrC,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,QAAM,QAAQ,GAAG,OAAO,GAAG,KAAK,OAAO,EAAE,gBAAgB,KAAK;AAE9D,SAAO,IAAI,GAAG,WAAW,KAAK,KAAK,QAAQ,KAAK,GAAG;AACrD;AAMA,eAAe,YACb,MACA,UACA,sBACiB;AACjB,QAAM,MAAM,KAAK,aAAa,WAAW,OAAO;AAChD,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AAEnC,QAAM,QAAQ,QAAQ,OAClB,mEACA;AAEJ,SAAO,IAAI,GAAG,WAAW,KAAK,KAAK,QAAQ,KAAK,GAAG;AACrD;AAMA,eAAe,gBACb,MACA,UACA,sBACiB;AACjB,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AACnC,SAAO,kCAAkC,QAAQ;AACnD;AAMA,eAAe,kBACb,MACA,UACA,sBACiB;AACjB,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AACnC,QAAM,QAAQ;AAEd,SAAO,sBAAsB,KAAK,KAAK,QAAQ;AACjD;AAMA,SAAS,YAAY,MAAmB;AACtC,MAAI,OAAO,WAAW,KAAK,QAAQ,EAAE;AAGrC,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,WAAW,IAAI;AAAA,EACxB;AACA,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,OAAO,IAAI;AAAA,EACpB;AACA,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,MAAM,IAAI;AAAA,EACnB;AACA,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,WAAW,IAAI;AAAA,EACxB;AAEA,SAAO;AACT;AAMA,eAAe,YACb,MACA,UACA,sBACiB;AACjB,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AACnC,QAAM,MAAM,KAAK,QAAQ,OAAO;AAChC,QAAM,SAAS,KAAK,QAAQ,UAAU;AAGtC,QAAM,aAAa,SAAS,qBAAqB;AACjD,QAAM,UAAU,SAAS,+BAA+B;AAExD,SAAO,YAAY,WAAW,GAAG,CAAC,IAAI,UAAU,GAAG,OAAO,wDAAwD,QAAQ;AAC5H;AAMA,SAAS,cAAc,MAAW,UAA2B;AAC3D,QAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ,QAAO;AAGpB,MAAI,MAAM;AACV,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM;AAAA,EACR,WAAW,OAAO,KAAK;AACrB,UAAM,OAAO;AAAA,EACf,WAAW,OAAO,YAAY,UAAU;AAEtC,UAAM,GAAG,QAAQ,IAAI,OAAO,QAAQ;AAAA,EACtC;AAEA,QAAM,MAAM,KAAK,QAAQ,WAAW,OAAO,OAAO;AAClD,QAAM,UAAU,KAAK,QAAQ,WAAW;AAGxC,QAAM,UAAU,aAAa,WAAW,GAAG,CAAC,UAAU,WAAW,GAAG,CAAC;AAErE,MAAI,SAAS;AACX,WAAO;AAAA;AAAA,UAED,OAAO;AAAA,6FAC4E,WAAW,OAAO,CAAC;AAAA;AAAA;AAAA,EAG9G;AAEA,SAAO,wDAAwD,OAAO;AACxE;AAMA,eAAe,aACb,MACA,UACA,sBACiB;AACjB,QAAM,YAAY,KAAK,QAAQ,aAAa,KAAK;AAGjD,MAAI,sBAAsB;AACxB,QAAI;AACF,YAAM,aAAa,MAAM,qBAAqB,MAAM,QAAQ;AAC5D,UAAI,YAAY;AACd,eAAO;AAAA,MACT;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,oCAAoC,SAAS,KAAK,KAAK;AAAA,IAEvE;AAAA,EACF;AAGA,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,aAAO,mBAAmB,KAAK,MAAM;AAAA,IACvC,KAAK;AACH,aAAO,oBAAoB,KAAK,MAAM;AAAA,IACxC;AAEE,UAAI,KAAK,UAAU;AACjB,cAAM,aAAa,MAAM,QAAQ;AAAA,UAC/B,KAAK,SAAS,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,QACtF;AACA,eAAO,WAAW,KAAK,EAAE;AAAA,MAC3B;AACA,aAAO;AAAA,EACX;AACF;AAMA,SAAS,mBAAmB,QAAqB;AAC/C,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,QAAQ,QAAQ,SAAS;AAE/B,QAAM,SAAiC;AAAA,IACrC,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AAEA,QAAM,cAAc,GAAG,OAAO,KAAK,KAAK,OAAO,OAAO;AAEtD,SAAO;AAAA;AAAA,iBAEQ,WAAW,GAAG,CAAC,sDAAsD,WAAW,KAAK,WAAW,IAAI,CAAC;AAAA;AAAA;AAGtH;AAMA,SAAS,oBAAoB,QAAqB;AAChD,QAAM,QAAQ,QAAQ,SAAS;AAE/B,QAAM,SAAiC;AAAA,IACrC,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AAEA,SAAO,cAAc,OAAO,KAAK,KAAK,OAAO,KAAK;AACpD;AAKA,SAAS,aAAa,QAAyB;AAC7C,MAAI,CAAC,OAAQ,QAAO;AAGpB,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AAEvB,SAAO;AACT;AAKA,SAAS,WAAW,MAAsB;AACxC,QAAM,MAA8B;AAAA,IAClC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAEA,SAAO,KAAK,QAAQ,YAAY,OAAK,IAAI,CAAC,CAAC;AAC7C;AAKA,SAAS,oBAAoB,SAAiB,WAA4B;AACxE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAiBL,YAAY,gEAAgE,WAAW,SAAS,CAAC,WAAW,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAOlG,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASvB;;;ACvbO,SAAS,kBAAkB,MAAgC;AAChE,QAAM,WAAqB,CAAC;AAC5B,QAAM,SAAmB,CAAC;AAG1B,QAAM,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE;AAGrC,MAAI,cAAc,QAAQ;AACxB,aAAS,KAAK,eAAe,KAAK,MAAM,cAAc,IAAI,CAAC,wDAAwD;AAAA,EACrH;AAGA,MAAI,KAAK,SAAS,WAAW,MAAM,KAAK,SAAS,oBAAoB,KAAK,KAAK,SAAS,iBAAiB,IAAI;AAC3G,WAAO,KAAK,mEAAmE;AAAA,EACjF;AAEA,MAAI,KAAK,SAAS,eAAe,KAAK,KAAK,SAAS,eAAe,GAAG;AACpE,WAAO,KAAK,kEAAkE;AAAA,EAChF;AAEA,MAAI,KAAK,SAAS,QAAQ,GAAG;AAC3B,aAAS,KAAK,iDAAiD;AAAA,EACjE;AAGA,QAAM,gBACJ,KAAK,SAAS,SAAS,KACvB,KAAK,SAAS,SAAS,KACvB,KAAK,SAAS,QAAQ,KACtB,KAAK,SAAS,aAAa;AAE7B,MAAI,eAAe;AACjB,WAAO,KAAK,4EAA4E;AAAA,EAC1F;AAGA,QAAM,oBAAoB,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,YAAY;AAC9E,MAAI,mBAAmB;AACrB,WAAO,KAAK,iEAAiE;AAAA,EAC/E;AAGA,MAAI,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,SAAS,GAAG;AACjF,WAAO,KAAK,6DAA6D;AAAA,EAC3E;AAGA,QAAM,kBAAkB;AAAA,IACtB;AAAA,IAAS;AAAA,IAAS;AAAA,IAAU;AAAA,IAAS;AAAA,IAAU;AAAA,IAAU;AAAA,EAC3D;AAEA,aAAW,OAAO,iBAAiB;AACjC,QAAI,KAAK,SAAS,IAAI,GAAG,EAAE,GAAG;AAC5B,aAAO,KAAK,IAAI,GAAG,mCAAmC;AAAA,IACxD;AAAA,EACF;AAGA,QAAM,cAAc,KAAK,MAAM,OAAO,KAAK,CAAC,GAAG;AAC/C,QAAM,aAAa,KAAK,MAAM,KAAK,KAAK,CAAC,GAAG;AAG5C,MAAI,aAAa,IAAI;AACnB,aAAS,KAAK,0BAA0B,UAAU,gCAAgC;AAAA,EACpF;AAGA,QAAM,oBAAoB,KAAK,MAAM,+BAA+B,KAAK,CAAC,GAAG;AAC7E,MAAI,mBAAmB,GAAG;AACxB,aAAS,KAAK,GAAG,gBAAgB,0DAA0D;AAAA,EAC7F;AAGA,QAAM,sBAAsB,KAAK,MAAM,gCAAgC,KAAK,CAAC,GAAG;AAChF,MAAI,qBAAqB,GAAG;AAC1B,aAAS,KAAK,GAAG,kBAAkB,4CAA4C;AAAA,EACjF;AAGA,MAAI,KAAK,SAAS,cAAc,KAAK,KAAK,SAAS,aAAa,GAAG;AACjE,aAAS,KAAK,uFAAuF;AAAA,EACvG;AAEA,MAAI,KAAK,SAAS,kBAAkB,GAAG;AACrC,aAAS,KAAK,kFAAkF;AAAA,EAClG;AAGA,MAAI,KAAK,MAAM,gBAAgB,GAAG;AAChC,aAAS,KAAK,qEAAqE;AAAA,EACrF;AAGA,MAAI,KAAK,MAAM,qBAAqB,GAAG;AACrC,WAAO,KAAK,0DAA0D;AAAA,EACxE;AAGA,QAAM,sBAAsB,KAAK,MAAM,kBAAkB,KAAK,CAAC;AAC/D,QAAM,YAAY,CAAC,mBAAmB,oBAAoB,wBAAwB,qBAAqB;AAEvG,aAAW,OAAO,qBAAqB;AACrC,UAAM,aAAa,IAAI,QAAQ,SAAS,EAAE,EAAE,KAAK;AACjD,QAAI,CAAC,UAAU,SAAS,UAAU,GAAG;AACnC,eAAS,KAAK,gCAAgC,GAAG,EAAE;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO,OAAO,WAAW;AAAA,IACzB;AAAA,IACA;AAAA,IACA,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;;;AC1IA,eAAsB,mBACpB,KACA,cACyC;AACzC,MAAI;AAEF,UAAM,WAAW,MAAM,IAAI,QAAQ,WAAW;AAAA,MAC5C,MAAM,aAAa,gBAAgB;AAAA,MACnC;AAAA,IACF,CAAC;AAGD,QAAI,UAAU,aAAa,eAAe,UAAU,mBAAmB;AACrE,aAAO;AAAA,QACL,QAAQ,SAAS,kBAAkB,UAAU,aAAa,WAAW,WAAW,UAAU;AAAA,QAC1F,OAAO,SAAS,kBAAkB,SAAS,aAAa,WAAW,WAAW,SAAS;AAAA,QACvF,aAAa,SAAS,eAAe,aAAa,WAAW,WAAW,eAAe;AAAA,QACvF,UAAU,SAAS,YAAY,aAAa,WAAW,WAAW,YAAY;AAAA,QAC9E,SAAS,SAAS,WAAW,aAAa,WAAW,WAAW;AAAA,MAClE;AAAA,IACF;AAGA,WAAO,aAAa,WAAW,aAAa;AAAA,EAC9C,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,MAAM,iDAAiD,KAAK;AAE/E,WAAO,aAAa,WAAW,aAAa;AAAA,EAC9C;AACF;;;AC7BA,eAAsB,gBACpB,KACA,cACsC;AACtC,MAAI;AAEF,UAAM,WAAW,MAAM,IAAI,QAAQ,WAAW;AAAA,MAC5C,MAAM,aAAa,gBAAgB;AAAA,MACnC;AAAA,IACF,CAAC;AAGD,QAAI,UAAU,aAAa,YAAY,UAAU,gBAAgB;AAC/D,aAAO;AAAA,QACL,QAAQ,SAAS,eAAe,UAAU,aAAa,WAAW,QAAQ,UAAU;AAAA,QACpF,aAAa,SAAS,eAAe,aAAa,WAAW,QAAQ,eAAe;AAAA,QACpF,UAAU,SAAS,YAAY,aAAa,WAAW,QAAQ,YAAY;AAAA,QAC3E,aAAa,SAAS,eAAe,eAAe,aAAa,WAAW,QAAQ;AAAA,MACtF;AAAA,IACF;AAGA,WAAO,aAAa,WAAW,UAAU;AAAA,EAC3C,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,MAAM,8CAA8C,KAAK;AAE5E,WAAO,aAAa,WAAW,UAAU;AAAA,EAC3C;AACF;","names":["DOMPurify"]}
package/dist/utils.d.cts CHANGED
@@ -21,6 +21,7 @@ declare function convertToEmailSafeHtml(editorState: SerializedEditorState | und
21
21
  wrapInTemplate?: boolean;
22
22
  preheader?: string;
23
23
  mediaUrl?: string;
24
+ customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>;
24
25
  }): Promise<string>;
25
26
 
26
27
  /**
package/dist/utils.d.ts CHANGED
@@ -21,6 +21,7 @@ declare function convertToEmailSafeHtml(editorState: SerializedEditorState | und
21
21
  wrapInTemplate?: boolean;
22
22
  preheader?: string;
23
23
  mediaUrl?: string;
24
+ customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>;
24
25
  }): Promise<string>;
25
26
 
26
27
  /**
package/dist/utils.js CHANGED
@@ -64,62 +64,73 @@ async function convertToEmailSafeHtml(editorState, options) {
64
64
  if (!editorState) {
65
65
  return "";
66
66
  }
67
- const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
67
+ const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
68
68
  const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
69
69
  if (options?.wrapInTemplate) {
70
70
  return wrapInEmailTemplate(sanitizedHtml, options.preheader);
71
71
  }
72
72
  return sanitizedHtml;
73
73
  }
74
- async function lexicalToEmailHtml(editorState, mediaUrl) {
74
+ async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
75
75
  const { root } = editorState;
76
76
  if (!root || !root.children) {
77
77
  return "";
78
78
  }
79
- const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
80
- return html;
79
+ const htmlParts = await Promise.all(
80
+ root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
81
+ );
82
+ return htmlParts.join("");
81
83
  }
82
- function convertNode(node, mediaUrl) {
84
+ async function convertNode(node, mediaUrl, customBlockConverter) {
83
85
  switch (node.type) {
84
86
  case "paragraph":
85
- return convertParagraph(node, mediaUrl);
87
+ return convertParagraph(node, mediaUrl, customBlockConverter);
86
88
  case "heading":
87
- return convertHeading(node, mediaUrl);
89
+ return convertHeading(node, mediaUrl, customBlockConverter);
88
90
  case "list":
89
- return convertList(node, mediaUrl);
91
+ return convertList(node, mediaUrl, customBlockConverter);
90
92
  case "listitem":
91
- return convertListItem(node, mediaUrl);
93
+ return convertListItem(node, mediaUrl, customBlockConverter);
92
94
  case "blockquote":
93
- return convertBlockquote(node, mediaUrl);
95
+ return convertBlockquote(node, mediaUrl, customBlockConverter);
94
96
  case "text":
95
97
  return convertText(node);
96
98
  case "link":
97
- return convertLink(node, mediaUrl);
99
+ return convertLink(node, mediaUrl, customBlockConverter);
98
100
  case "linebreak":
99
101
  return "<br>";
100
102
  case "upload":
101
103
  return convertUpload(node, mediaUrl);
102
104
  case "block":
103
- return convertBlock(node, mediaUrl);
105
+ return await convertBlock(node, mediaUrl, customBlockConverter);
104
106
  default:
105
107
  if (node.children) {
106
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
108
+ const childParts = await Promise.all(
109
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
110
+ );
111
+ return childParts.join("");
107
112
  }
108
113
  return "";
109
114
  }
110
115
  }
111
- function convertParagraph(node, mediaUrl) {
116
+ async function convertParagraph(node, mediaUrl, customBlockConverter) {
112
117
  const align = getAlignment(node.format);
113
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
118
+ const childParts = await Promise.all(
119
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
120
+ );
121
+ const children = childParts.join("");
114
122
  if (!children.trim()) {
115
123
  return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
116
124
  }
117
125
  return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
118
126
  }
119
- function convertHeading(node, mediaUrl) {
127
+ async function convertHeading(node, mediaUrl, customBlockConverter) {
120
128
  const tag = node.tag || "h1";
121
129
  const align = getAlignment(node.format);
122
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
130
+ const childParts = await Promise.all(
131
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
132
+ );
133
+ const children = childParts.join("");
123
134
  const styles = {
124
135
  h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
125
136
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
@@ -128,18 +139,27 @@ function convertHeading(node, mediaUrl) {
128
139
  const style = `${styles[tag] || styles.h3} text-align: ${align};`;
129
140
  return `<${tag} style="${style}">${children}</${tag}>`;
130
141
  }
131
- function convertList(node, mediaUrl) {
142
+ async function convertList(node, mediaUrl, customBlockConverter) {
132
143
  const tag = node.listType === "number" ? "ol" : "ul";
133
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
144
+ const childParts = await Promise.all(
145
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
146
+ );
147
+ const children = childParts.join("");
134
148
  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;";
135
149
  return `<${tag} style="${style}">${children}</${tag}>`;
136
150
  }
137
- function convertListItem(node, mediaUrl) {
138
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
151
+ async function convertListItem(node, mediaUrl, customBlockConverter) {
152
+ const childParts = await Promise.all(
153
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
154
+ );
155
+ const children = childParts.join("");
139
156
  return `<li style="margin: 0 0 8px 0;">${children}</li>`;
140
157
  }
141
- function convertBlockquote(node, mediaUrl) {
142
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
158
+ async function convertBlockquote(node, mediaUrl, customBlockConverter) {
159
+ const childParts = await Promise.all(
160
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
161
+ );
162
+ const children = childParts.join("");
143
163
  const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
144
164
  return `<blockquote style="${style}">${children}</blockquote>`;
145
165
  }
@@ -159,8 +179,11 @@ function convertText(node) {
159
179
  }
160
180
  return text;
161
181
  }
162
- function convertLink(node, mediaUrl) {
163
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
182
+ async function convertLink(node, mediaUrl, customBlockConverter) {
183
+ const childParts = await Promise.all(
184
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
185
+ );
186
+ const children = childParts.join("");
164
187
  const url = node.fields?.url || "#";
165
188
  const newTab = node.fields?.newTab ?? false;
166
189
  const targetAttr = newTab ? ' target="_blank"' : "";
@@ -191,8 +214,18 @@ function convertUpload(node, mediaUrl) {
191
214
  }
192
215
  return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
193
216
  }
194
- function convertBlock(node, mediaUrl) {
195
- const blockType = node.fields?.blockName;
217
+ async function convertBlock(node, mediaUrl, customBlockConverter) {
218
+ const blockType = node.fields?.blockName || node.blockName;
219
+ if (customBlockConverter) {
220
+ try {
221
+ const customHtml = await customBlockConverter(node, mediaUrl);
222
+ if (customHtml) {
223
+ return customHtml;
224
+ }
225
+ } catch (error) {
226
+ console.error(`Custom block converter error for ${blockType}:`, error);
227
+ }
228
+ }
196
229
  switch (blockType) {
197
230
  case "button":
198
231
  return convertButtonBlock(node.fields);
@@ -200,7 +233,10 @@ function convertBlock(node, mediaUrl) {
200
233
  return convertDividerBlock(node.fields);
201
234
  default:
202
235
  if (node.children) {
203
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
236
+ const childParts = await Promise.all(
237
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
238
+ );
239
+ return childParts.join("");
204
240
  }
205
241
  return "";
206
242
  }