payload-plugin-newsletter 0.16.10 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/collections.cjs +73 -31
- package/dist/collections.cjs.map +1 -1
- package/dist/collections.js +73 -31
- package/dist/collections.js.map +1 -1
- package/dist/components.cjs +295 -275
- package/dist/components.cjs.map +1 -1
- package/dist/components.js +305 -285
- package/dist/components.js.map +1 -1
- package/dist/index.cjs +118 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +118 -33
- package/dist/index.js.map +1 -1
- package/dist/types.d.cts +7 -0
- package/dist/types.d.ts +7 -0
- package/dist/utils.cjs +64 -28
- 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 +64 -28
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
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
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
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;"> </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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/utils.cjs.map
CHANGED
|
@@ -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;\"> </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}","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;\"> </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 '&': '&',\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}","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"]}
|
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
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|
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;"> </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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|