import { CSSProperties, Fragment } from "react"; import type { JSONContent } from "@tiptap/core"; import { Text, Html, Head, Body, Font, Container, Link, Heading, Hr, Button, Img, Preview, Row, Column, render, Code, } from "jsx-email"; import { AllowedAlignments } from "./types"; interface NodeOptions { parent?: JSONContent; prev?: JSONContent; next?: JSONContent; } export interface ThemeOptions { colors?: { heading?: string; paragraph?: string; horizontal?: string; footer?: string; blockquoteBorder?: string; codeBackground?: string; codeText?: string; link?: string; }; fontSize?: { paragraph?: string; footer?: { size?: string; lineHeight?: string; }; }; } export interface RenderConfig { /** * The preview text is the snippet of text that is pulled into the inbox * preview of an email client, usually right after the subject line. * * Default: `undefined` */ preview?: string; /** * The theme object allows you to customize the colors and font sizes of the * rendered email. * * Default: * ```js * { * colors: { * heading: 'rgb(17, 24, 39)', * paragraph: 'rgb(55, 65, 81)', * horizontal: 'rgb(234, 234, 234)', * footer: 'rgb(100, 116, 139)', * }, * fontSize: { * paragraph: '15px', * footer: { * size: '14px', * lineHeight: '24px', * }, * }, * } * ``` * */ theme?: ThemeOptions; } const DEFAULT_THEME: ThemeOptions = { colors: { heading: "rgb(17, 24, 39)", paragraph: "rgb(55, 65, 81)", horizontal: "rgb(234, 234, 234)", footer: "rgb(100, 116, 139)", blockquoteBorder: "rgb(209, 213, 219)", codeBackground: "rgb(239, 239, 239)", codeText: "rgb(17, 24, 39)", link: "rgb(59, 130, 246)", }, fontSize: { paragraph: "15px", footer: { size: ".8rem", }, }, }; const CODE_FONT_FAMILY = 'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'; const allowedHeadings = ["h1", "h2", "h3"] as const; type AllowedHeadings = (typeof allowedHeadings)[number]; const headings: Record = { h1: { fontSize: "36px", lineHeight: "40px", fontWeight: 800, }, h2: { fontSize: "30px", lineHeight: "36px", fontWeight: 700, }, h3: { fontSize: "24px", lineHeight: "38px", fontWeight: 600, }, }; const allowedSpacers = ["sm", "md", "lg", "xl"] as const; export type AllowedSpacers = (typeof allowedSpacers)[number]; const spacers: Record = { sm: "8px", md: "16px", lg: "32px", xl: "64px", }; export interface MarkType { [key: string]: any; type: string; attrs?: Record | undefined; } const antialiased: CSSProperties = { WebkitFontSmoothing: "antialiased", MozOsxFontSmoothing: "grayscale", }; export function generateKey() { return Math.random().toString(36).substring(2, 8); } export type VariableFormatter = (options: { variable: string; fallback?: string; }) => string; const allowedLogoSizes = ["sm", "md", "lg"] as const; type AllowedLogoSizes = (typeof allowedLogoSizes)[number]; const logoSizes: Record = { sm: "40px", md: "48px", lg: "64px", }; type EmailRendererOption = { shouldReplaceVariableValues?: boolean; variableValues?: Record; linkValues?: Record; }; export class EmailRenderer { private config: RenderConfig = { theme: DEFAULT_THEME, }; private shouldReplaceVariableValues = false; private variableValues: Record = {}; private linkValues: Record = {}; constructor( private readonly email: JSONContent = { type: "doc", content: [] }, options: EmailRendererOption = {} ) { this.shouldReplaceVariableValues = options.shouldReplaceVariableValues || false; this.variableValues = options.variableValues || {}; this.linkValues = options.linkValues || {}; } private variableFormatter: VariableFormatter = ({ variable, fallback }) => { return fallback ? `{{${variable},fallback=${fallback}}}` : `{{${variable}}}`; }; public render(options: EmailRendererOption = {}) { this.shouldReplaceVariableValues = options.shouldReplaceVariableValues || false; this.variableValues = options.variableValues || {}; this.linkValues = options.linkValues || {}; const markup = this.markup(); return render(markup); } markup() { const nodes = this.email.content || []; const jsxNodes = nodes.map((node, index) => { const nodeOptions: NodeOptions = { prev: nodes[index - 1], next: nodes[index + 1], parent: node, }; const component = this.renderNode(node, nodeOptions); if (!component) { return null; } return {component}; }); const markup = (