diff --git a/apps/api/src/services/EmailService.ts b/apps/api/src/services/EmailService.ts index b5f3ebc..02a4a06 100644 --- a/apps/api/src/services/EmailService.ts +++ b/apps/api/src/services/EmailService.ts @@ -652,7 +652,7 @@ export class EmailService { /** * Detects if HTML contains custom patterns that indicate it was written in the HTML editor - * rather than the visual editor. Mirrors the same logic in EmailPreviewModal.tsx. + * rather than the visual editor. Mirrors the same logic in apps/web/src/lib/emailStyles.ts. */ private static detectCustomHtmlPatterns(html: string): boolean { if (!html || html.trim() === '') return false; @@ -692,7 +692,7 @@ export class EmailService { /** * Wraps visual editor content with a full HTML document and prose styles. - * Mirrors wrapEmailWithStyles() in EmailPreviewModal.tsx so sent emails + * Mirrors wrapEmailWithStyles() in apps/web/src/lib/emailStyles.ts so sent emails * match the preview modal exactly. */ private static wrapWithEmailStyles(htmlBody: string): string { diff --git a/apps/web/src/components/EmailEditor/EmailEditor.tsx b/apps/web/src/components/EmailEditor/EmailEditor.tsx index 4447200..2660bf2 100644 --- a/apps/web/src/components/EmailEditor/EmailEditor.tsx +++ b/apps/web/src/components/EmailEditor/EmailEditor.tsx @@ -31,6 +31,7 @@ import { } from '@plunk/ui'; import {Code2, Eye, Monitor, Smartphone, Tablet, Upload, X} from 'lucide-react'; import {network} from '../../lib/network'; +import {detectCustomHtmlPatterns, wrapEmailWithStyles} from '../../lib/emailStyles'; import 'tippy.js/dist/tippy.css'; interface EmailEditorProps { @@ -51,54 +52,6 @@ const commonVariables = [ {name: 'manageUrl', description: 'Manage link'}, ]; -const detectCustomHtmlPatterns = (html: string): boolean => { - if (!html || html.trim() === '') return false; - - // Check for common patterns that indicate custom HTML - const hasInlineStyles = /<[^>]+style\s*=\s*["'][^"']*["']/i.test(html); - - // Check for custom classes (exclude TipTap's generated classes) - const classMatches = html.matchAll(/class\s*=\s*["']([^"']*)["']/gi); - let hasCustomClasses = false; - for (const match of classMatches) { - const classValue = match[1]; - if (!classValue) continue; - // Split by whitespace to get individual classes - const classes = classValue.split(/\s+/).filter(c => c.length > 0); - // Check if any class is NOT in the allowed list - const allowedPrefixes = [ - 'prose', - 'variable-', - 'email-image', - 'ProseMirror', - 'resizable-image', - 'selected', - 'resize-handle', - ]; - const hasDisallowedClass = classes.some(cls => !allowedPrefixes.some(prefix => cls.startsWith(prefix))); - if (hasDisallowedClass) { - hasCustomClasses = true; - break; - } - } - - const hasCustomAttributes = /<[^>]+(?:data-|aria-|role=|id=)/i.test(html); - const hasComplexTables = /