{"version":3,"file":"index.mjs","names":[],"sources":["../src/utils/paste-sanitizer.ts","../src/core/create-paste-handler.ts","../src/email-editor/email-editor.tsx"],"sourcesContent":["/**\n * Sanitizes pasted HTML.\n * - From editor (has node-* classes): pass through as-is\n * - From external: strip all styles/classes, keep only semantic HTML\n */\n\n/**\n * Detects content from the Resend editor by checking for node-* class names.\n */\nconst EDITOR_CLASS_PATTERN = /class=\"[^\"]*node-/;\n\n/**\n * Attributes to preserve on specific elements for EXTERNAL content.\n * Only functional attributes - NO style or class.\n */\nconst PRESERVED_ATTRIBUTES: Record<string, string[]> = {\n  a: ['href', 'target', 'rel'],\n  img: ['src', 'alt', 'width', 'height'],\n  td: ['colspan', 'rowspan'],\n  th: ['colspan', 'rowspan', 'scope'],\n  table: ['border', 'cellpadding', 'cellspacing'],\n  '*': ['id'],\n};\n\nfunction isFromEditor(html: string): boolean {\n  return EDITOR_CLASS_PATTERN.test(html);\n}\n\nexport function sanitizePastedHtml(html: string): string {\n  if (isFromEditor(html)) {\n    return html;\n  }\n\n  const parser = new DOMParser();\n  const doc = parser.parseFromString(html, 'text/html');\n\n  sanitizeNode(doc.body);\n\n  return doc.body.innerHTML;\n}\n\nfunction sanitizeNode(node: Node): void {\n  if (node.nodeType === Node.ELEMENT_NODE) {\n    const el = node as HTMLElement;\n    sanitizeElement(el);\n  }\n\n  for (const child of Array.from(node.childNodes)) {\n    sanitizeNode(child);\n  }\n}\n\nfunction sanitizeElement(el: HTMLElement): void {\n  const tagName = el.tagName.toLowerCase();\n\n  const allowedForTag = PRESERVED_ATTRIBUTES[tagName] || [];\n  const allowedGlobal = PRESERVED_ATTRIBUTES['*'] || [];\n  const allowed = new Set([...allowedForTag, ...allowedGlobal]);\n\n  const attributesToRemove: string[] = [];\n\n  for (const attr of Array.from(el.attributes)) {\n    if (attr.name.startsWith('data-')) {\n      attributesToRemove.push(attr.name);\n      continue;\n    }\n\n    if (!allowed.has(attr.name)) {\n      attributesToRemove.push(attr.name);\n    }\n  }\n\n  for (const attr of attributesToRemove) {\n    el.removeAttribute(attr);\n  }\n}\n","import type { Extensions } from '@tiptap/core';\nimport { generateJSON } from '@tiptap/html';\nimport type { Slice } from '@tiptap/pm/model';\nimport type { EditorView } from '@tiptap/pm/view';\nimport { sanitizePastedHtml } from '../utils/paste-sanitizer';\n\nexport type PasteHandler = (\n  payload: string | File,\n  view: EditorView,\n) => boolean;\n\nexport function createPasteHandler({\n  onPaste,\n  extensions,\n}: {\n  onPaste?: PasteHandler;\n  extensions: Extensions;\n}) {\n  return (view: EditorView, event: ClipboardEvent, slice: Slice): boolean => {\n    const text = event.clipboardData?.getData('text/plain');\n\n    if (text && onPaste?.(text, view)) {\n      event.preventDefault();\n\n      return true;\n    }\n\n    if (event.clipboardData?.files?.[0]) {\n      const file = event.clipboardData.files[0];\n      if (onPaste?.(file, view)) {\n        event.preventDefault();\n\n        return true;\n      }\n    }\n\n    if (slice.content.childCount === 1) {\n      return false;\n    }\n\n    if (event.clipboardData?.getData?.('text/html')) {\n      event.preventDefault();\n      const html = event.clipboardData.getData('text/html');\n\n      const sanitizedHtml = sanitizePastedHtml(html);\n\n      const jsonContent = generateJSON(sanitizedHtml, extensions);\n      const node = view.state.schema.nodeFromJSON(jsonContent);\n\n      const transaction = view.state.tr.replaceSelectionWith(node, false);\n      view.dispatch(transaction);\n\n      return true;\n    }\n    return false;\n  };\n}\n","import type { Content, Editor, Extensions, JSONContent } from '@tiptap/core';\nimport {\n  EditorProvider,\n  type UseEditorOptions,\n  useCurrentEditor,\n} from '@tiptap/react';\nimport {\n  forwardRef,\n  type ReactNode,\n  type Ref,\n  useEffect,\n  useImperativeHandle,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n} from 'react';\nimport { createPasteHandler } from '../core/create-paste-handler';\nimport { composeReactEmail } from '../core/serializer/compose-react-email';\nimport { StarterKit } from '../extensions';\nimport { EmailTheming } from '../plugins/email-theming/extension';\nimport type { EditorThemeInput } from '../plugins/email-theming/types';\nimport { createImageExtension } from '../plugins/image/extension';\nimport { BubbleMenu } from '../ui/bubble-menu';\nimport { SlashCommandRoot } from '../ui/slash-command/root';\nimport '../ui/themes/default.css';\nimport { Placeholder } from '@tiptap/extension-placeholder';\n\nexport interface EmailEditorRef {\n  getEmail: () => Promise<{ html: string; text: string }>;\n  getEmailHTML: () => Promise<string>;\n  getEmailText: () => Promise<string>;\n  getJSON: () => JSONContent;\n  editor: Editor | null;\n}\n\nexport interface EmailEditorProps {\n  content?: Content;\n  onUpdate?: (ref: EmailEditorRef) => void;\n  onReady?: (ref: EmailEditorRef) => void;\n  theme?: EditorThemeInput;\n  editable?: boolean;\n  placeholder?: string;\n  bubbleMenu?: {\n    hideWhenActiveNodes?: string[];\n    hideWhenActiveMarks?: string[];\n  };\n  extensions?: Extensions;\n  onUploadImage?: (file: File) => Promise<{ url: string }>;\n  className?: string;\n  children?: ReactNode;\n}\n\nfunction buildRef(editor: Editor | null): EmailEditorRef {\n  return {\n    getEmail: async () => {\n      if (!editor) return { html: '', text: '' };\n      return composeReactEmail({ editor });\n    },\n    getEmailHTML: async () => {\n      if (!editor) return '';\n      const result = await composeReactEmail({ editor });\n      return result.html;\n    },\n    getEmailText: async () => {\n      if (!editor) return '';\n      const result = await composeReactEmail({ editor });\n      return result.text;\n    },\n    getJSON: () => editor?.getJSON() ?? { type: 'doc', content: [] },\n    editor,\n  };\n}\n\nfunction RefBridge({\n  editorRef,\n  onUpdateRef,\n}: {\n  editorRef: Ref<EmailEditorRef>;\n  onUpdateRef: React.RefObject<((ref: EmailEditorRef) => void) | undefined>;\n}) {\n  const { editor } = useCurrentEditor();\n\n  const emailEditorRef = useMemo(() => buildRef(editor), [editor]);\n\n  useImperativeHandle(editorRef, () => emailEditorRef, [emailEditorRef]);\n\n  useEffect(() => {\n    if (!editor) return;\n\n    const handler = () => {\n      onUpdateRef.current?.(emailEditorRef);\n    };\n\n    editor.on('update', handler);\n    return () => {\n      editor.off('update', handler);\n    };\n  }, [editor, emailEditorRef, onUpdateRef]);\n\n  return null;\n}\n\nfunction EmailEditorReadyBridge({\n  onReadyRef,\n}: {\n  onReadyRef: React.RefObject<((ref: EmailEditorRef) => void) | undefined>;\n}) {\n  const { editor } = useCurrentEditor();\n\n  useLayoutEffect(() => {\n    if (!editor) return;\n    onReadyRef.current?.(buildRef(editor));\n  }, [editor, onReadyRef]);\n\n  return null;\n}\n\nexport const EmailEditor = forwardRef<EmailEditorRef, EmailEditorProps>(\n  (\n    {\n      content,\n      onUpdate,\n      onReady,\n      theme = 'basic',\n      editable = true,\n      placeholder,\n      bubbleMenu,\n      extensions: extensionsProp,\n      onUploadImage,\n      className,\n      children,\n    },\n    ref,\n  ) => {\n    const onUpdateRef = useRef(onUpdate);\n    onUpdateRef.current = onUpdate;\n\n    const onReadyRef = useRef(onReady);\n    onReadyRef.current = onReady;\n\n    const imageExtension = useMemo(() => {\n      if (!onUploadImage) return null;\n      return createImageExtension({ uploadImage: onUploadImage });\n    }, [onUploadImage]);\n\n    const extensions = useMemo(() => {\n      const base = extensionsProp ?? [\n        StarterKit.configure(),\n        Placeholder.configure({\n          placeholder:\n            placeholder ??\n            (({ node }) => {\n              // TODO: this heading placeholder is not working,\n              // in part because styles are only targetting paragraphs,\n              // but in part because of the way the content is rendered\n              if (node.type.name === 'heading') {\n                return `Heading ${node.attrs.level}`;\n              }\n              return \"Press '/' for commands\";\n            }),\n          includeChildren: true,\n        }),\n        EmailTheming.configure({ theme }),\n      ];\n\n      return imageExtension ? [...base, imageExtension] : base;\n    }, [extensionsProp, theme, placeholder, imageExtension]);\n\n    const editorProps: UseEditorOptions['editorProps'] = useMemo(\n      () => ({\n        handlePaste: createPasteHandler({\n          extensions,\n        }),\n      }),\n      [extensions],\n    );\n\n    return (\n      <EditorProvider\n        key={typeof theme === 'string' ? theme : JSON.stringify(theme)}\n        extensions={extensions}\n        content={content}\n        editable={editable}\n        immediatelyRender={false}\n        editorProps={editorProps}\n        editorContainerProps={{ className }}\n      >\n        <RefBridge editorRef={ref} onUpdateRef={onUpdateRef} />\n        <EmailEditorReadyBridge onReadyRef={onReadyRef} />\n        <BubbleMenu\n          hideWhenActiveNodes={\n            bubbleMenu?.hideWhenActiveNodes ?? ['button', 'horizontalRule']\n          }\n          hideWhenActiveMarks={bubbleMenu?.hideWhenActiveMarks ?? ['link']}\n        />\n        <BubbleMenu.LinkDefault />\n        <BubbleMenu.ButtonDefault />\n        <BubbleMenu.ImageDefault />\n        <SlashCommandRoot />\n        {children}\n      </EditorProvider>\n    );\n  },\n);\n\nEmailEditor.displayName = 'EmailEditor';\n"],"mappings":"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"}