'Arial, Helvetica, sans-serif', 'fontSize' => '16px', 'lineHeight' => '1.5', 'backgroundColor' => '#f4f4f4', 'contentWidth' => '600', 'primaryColor' => '#0073aa', 'textColor' => '#333333', 'linkColor' => '#0073aa', ); /** * Current global styles. * * @var array */ private array $globalStyles = array(); /** * Block Registry for Pro checks. * * @var BlockRegistry|null */ private ?BlockRegistry $blockRegistry = null; /** * Generate email HTML from blocks. * * @param array $blocks Array of block data. * @param array $globalStyles Optional. Global styles. * @return string Generated HTML. */ public function generate( array $blocks, array $globalStyles = array() ): string { $this->globalStyles = wp_parse_args( $globalStyles, $this->defaultStyles ); $this->blockRegistry = new BlockRegistry(); $contentWidth = (int) $this->globalStyles['contentWidth']; $backgroundColor = esc_attr( $this->globalStyles['backgroundColor'] ); $fontFamily = esc_attr( $this->globalStyles['fontFamily'] ); $textColor = esc_attr( $this->globalStyles['textColor'] ); $blocksHtml = $this->renderBlocks( $blocks ); return <<
{$blocksHtml}
HTML; } /** * Render all blocks. * * @param array $blocks Array of blocks. * @param string $parentBgColor Parent background color for inheritance. * @return string Rendered HTML. */ private function renderBlocks( array $blocks, string $parentBgColor = '' ): string { $html = ''; foreach ( $blocks as $block ) { $html .= $this->renderBlock( $block, $parentBgColor ); } return $html; } /** * Render a single block. * * @param array $block Block data. * @param string $parentBgColor Parent background color for inheritance. * @return string Rendered HTML. */ private function renderBlock( array $block, string $parentBgColor = '' ): string { $type = $block['type'] ?? ''; $attributes = $block['attributes'] ?? array(); $children = $block['children'] ?? array(); $customCss = $attributes['customCss'] ?? ''; $html = $this->renderBlockContent( $type, $attributes, $children, $parentBgColor ); // Inject custom CSS as inline styles (Pro only) if ( ! empty( $customCss ) && $this->blockRegistry && $this->blockRegistry->isProActive() ) { $html = $this->injectCustomCss( $html, $customCss ); } return $html; } /** * Render block content by type. * * @param string $type Block type. * @param array $attributes Block attributes. * @param array $children Child blocks. * @param string $parentBgColor Parent background color for inheritance. * @return string Rendered HTML. */ private function renderBlockContent( string $type, array $attributes, array $children, string $parentBgColor = '' ): string { switch ( $type ) { case 'email-wrapper': return $this->renderEmailWrapper( $attributes, $children ); case 'row': return $this->renderRow( $attributes, $children, $parentBgColor ); case 'columns-1': case 'columns-2': case 'columns-2-sidebar': case 'columns-3': return $this->renderColumns( $type, $attributes, $children, $parentBgColor ); case 'header': return $this->renderHeader( $attributes ); case 'heading': return $this->renderHeading( $attributes ); case 'text': return $this->renderText( $attributes ); case 'button': return $this->renderButton( $attributes ); case 'image': return $this->renderImage( $attributes ); case 'divider': return $this->renderDivider( $attributes ); case 'spacer': return $this->renderSpacer( $attributes, $parentBgColor ); case 'social-icons': return $this->renderSocialIcons( $attributes ); case 'footer': return $this->renderFooter( $attributes ); case 'placeholder-confirm-link': return $this->renderPlaceholder( '[doubleoptinlink]', $attributes ); case 'placeholder-optout-link': return $this->renderPlaceholder( '[doubleoptoutlink]', $attributes ); case 'placeholder-date': return $this->renderPlaceholder( '[doubleoptin_form_date]', $attributes ); case 'placeholder-time': return $this->renderPlaceholder( '[doubleoptin_form_time]', $attributes ); case 'placeholder-url': return $this->renderPlaceholder( '[doubleoptin_form_url]', $attributes ); case 'placeholder-custom': $fieldName = $attributes['fieldName'] ?? 'field_name'; return $this->renderPlaceholder( "[{$fieldName}]", $attributes ); case 'conditional-content': return $this->renderConditionalContent( $attributes, $children, $parentBgColor ); default: return ''; } } /** * Render email wrapper. */ private function renderEmailWrapper( array $attrs, array $children ): string { $bgColor = esc_attr( $attrs['backgroundColor'] ?? '#ffffff' ); $padding = esc_attr( $attrs['padding'] ?? '20px' ); $innerHtml = $this->renderBlocks( $children, $bgColor ); return << {$innerHtml}
HTML; } /** * Render row. */ private function renderRow( array $attrs, array $children, string $parentBgColor = '' ): string { $bgColor = esc_attr( $attrs['backgroundColor'] ?? 'transparent' ); $padding = esc_attr( $attrs['padding'] ?? '0' ); // Pass row's own bg color if set, otherwise inherit parent's $childBgColor = ( $bgColor !== 'transparent' && ! empty( $bgColor ) ) ? $bgColor : $parentBgColor; $innerHtml = $this->renderBlocks( $children, $childBgColor ); return << {$innerHtml}
HTML; } /** * Render columns. */ private function renderColumns( string $type, array $attrs, array $children, string $parentBgColor = '' ): string { $bgColor = esc_attr( $attrs['backgroundColor'] ?? 'transparent' ); // Determine column widths - use custom columnWidths if set $customWidths = $attrs['columnWidths'] ?? null; if ( is_array( $customWidths ) && ! empty( $customWidths ) ) { $widths = array_map( 'intval', $customWidths ); } else { switch ( $type ) { case 'columns-1': $widths = array( 100 ); break; case 'columns-2': $widths = array( 50, 50 ); break; case 'columns-2-sidebar': $widths = ( $attrs['layout'] ?? '70-30' ) === '70-30' ? array( 70, 30 ) : array( 30, 70 ); break; case 'columns-3': $widths = array( 33, 34, 33 ); break; default: $widths = array( 100 ); } } $contentWidth = (int) $this->globalStyles['contentWidth']; $childBgColor = ( $bgColor !== 'transparent' && ! empty( $bgColor ) ) ? $bgColor : $parentBgColor; $columnsHtml = ''; foreach ( $children as $index => $child ) { $width = $widths[ $index ] ?? $widths[0]; $pixelWidth = (int) ( $contentWidth * $width / 100 ); $childHtml = $this->renderBlock( $child, $childBgColor ); $columnsHtml .= <<
{$childHtml}
HTML; } return << {$columnsHtml} HTML; } /** * Render header block. */ private function renderHeader( array $attrs ): string { $bgColor = esc_attr( $attrs['backgroundColor'] ?? $this->globalStyles['primaryColor'] ); $padding = esc_attr( $attrs['padding'] ?? '30px 20px' ); $logoUrl = esc_url( $attrs['logoUrl'] ?? '' ); $logoAlt = esc_attr( $attrs['logoAlt'] ?? get_bloginfo( 'name' ) ); $logoWidth = (int) ( $attrs['logoWidth'] ?? 200 ); $align = esc_attr( $attrs['align'] ?? 'center' ); $logoHtml = ''; if ( $logoUrl ) { $logoHtml = "\"{$logoAlt}\""; } else { $siteTitle = esc_html( get_bloginfo( 'name' ) ); $logoHtml = "

{$siteTitle}

"; } return << {$logoHtml} HTML; } /** * Render heading block. */ private function renderHeading( array $attrs ): string { $level = (int) ( $attrs['level'] ?? 2 ); $level = max( 1, min( 6, $level ) ); $tag = "h{$level}"; $text = wp_kses_post( $attrs['text'] ?? '' ); $color = esc_attr( $attrs['color'] ?? $this->globalStyles['textColor'] ); $align = esc_attr( $attrs['align'] ?? 'left' ); $fontSize = esc_attr( $attrs['fontSize'] ?? $this->getHeadingFontSize( $level ) ); $padding = esc_attr( $attrs['padding'] ?? '10px 0' ); return << <{$tag} style="margin: 0; color: {$color}; text-align: {$align}; font-size: {$fontSize}; font-weight: bold;">{$text} HTML; } /** * Get default font size for heading level. */ private function getHeadingFontSize( int $level ): string { $sizes = array( 1 => '32px', 2 => '28px', 3 => '24px', 4 => '20px', 5 => '18px', 6 => '16px', ); return $sizes[ $level ] ?? '16px'; } /** * Render text block. */ private function renderText( array $attrs ): string { $content = wp_kses_post( $attrs['content'] ?? '' ); $color = esc_attr( $attrs['color'] ?? $this->globalStyles['textColor'] ); $fontSize = esc_attr( $attrs['fontSize'] ?? $this->globalStyles['fontSize'] ); $lineHeight = esc_attr( $attrs['lineHeight'] ?? $this->globalStyles['lineHeight'] ); $align = esc_attr( $attrs['align'] ?? 'left' ); $padding = esc_attr( $attrs['padding'] ?? '10px 0' ); $fontFamily = esc_attr( $this->globalStyles['fontFamily'] ); return <<
{$content}
HTML; } /** * Render button block. */ private function renderButton( array $attrs ): string { $text = esc_html( $attrs['text'] ?? __( 'Click Here', 'double-opt-in' ) ); $rawUrl = $attrs['url'] ?? '#'; // Check if URL is a placeholder (contains [...]) - don't escape it to avoid adding http:// prefix // Placeholders will be replaced later with actual URLs that already have proper protocols if ( preg_match( '/\[.+\]/', $rawUrl ) ) { $url = $rawUrl; } else { $url = esc_url( $rawUrl ); } $bgColor = esc_attr( $attrs['backgroundColor'] ?? $this->globalStyles['primaryColor'] ); $textColor = esc_attr( $attrs['textColor'] ?? '#ffffff' ); $borderRadius = (int) ( $attrs['borderRadius'] ?? 4 ); $fontSize = esc_attr( $attrs['fontSize'] ?? '16px' ); $paddingV = (int) ( $attrs['paddingV'] ?? 12 ); $paddingH = (int) ( $attrs['paddingH'] ?? 24 ); $align = esc_attr( $attrs['align'] ?? 'center' ); $padding = esc_attr( $attrs['padding'] ?? '10px 0' ); $width = esc_attr( $attrs['width'] ?? 'auto' ); // Build width styles $tableWidth = ( $width !== 'auto' ) ? ' width="100%"' : ''; $widthStyle = ( $width !== 'auto' ) ? "width: {$width}; box-sizing: border-box; text-align: center;" : ''; return <<
{$text}
HTML; } /** * Render image block. */ private function renderImage( array $attrs ): string { $url = esc_url( $attrs['url'] ?? '' ); $alt = esc_attr( $attrs['alt'] ?? '' ); $width = esc_attr( $attrs['width'] ?? '100%' ); $rawLink = $attrs['link'] ?? ''; // Check if link is a placeholder (contains [...]) - don't escape it to avoid adding http:// prefix if ( ! empty( $rawLink ) && preg_match( '/\[.+\]/', $rawLink ) ) { $link = $rawLink; } else { $link = esc_url( $rawLink ); } $align = esc_attr( $attrs['align'] ?? 'center' ); $padding = esc_attr( $attrs['padding'] ?? '10px 0' ); if ( empty( $url ) ) { return ''; } $imgHtml = "\"{$alt}\""; if ( $link ) { $imgHtml = "{$imgHtml}"; } return << {$imgHtml} HTML; } /** * Render divider block. */ private function renderDivider( array $attrs ): string { $color = esc_attr( $attrs['color'] ?? '#dddddd' ); $thickness = (int) ( $attrs['thickness'] ?? 1 ); $style = esc_attr( $attrs['style'] ?? 'solid' ); $padding = esc_attr( $attrs['padding'] ?? '20px 0' ); return <<
HTML; } /** * Render spacer block. */ private function renderSpacer( array $attrs, string $parentBgColor = '' ): string { $height = (int) ( $attrs['height'] ?? 20 ); $bgColor = ! empty( $attrs['backgroundColor'] ) && $attrs['backgroundColor'] !== 'transparent' ? esc_attr( $attrs['backgroundColor'] ) : ( ! empty( $parentBgColor ) ? esc_attr( $parentBgColor ) : 'transparent' ); return <<   HTML; } /** * Render social icons block. */ private function renderSocialIcons( array $attrs ): string { $icons = $attrs['icons'] ?? array(); $iconSize = (int) ( $attrs['iconSize'] ?? 32 ); $align = esc_attr( $attrs['align'] ?? 'center' ); $padding = esc_attr( $attrs['padding'] ?? '20px 0' ); $spacing = (int) ( $attrs['spacing'] ?? 10 ); $iconColors = array( 'facebook' => '#1877f2', 'twitter' => '#1da1f2', 'instagram' => '#e4405f', 'linkedin' => '#0a66c2', 'youtube' => '#ff0000', ); $iconsHtml = ''; foreach ( $icons as $icon ) { $network = esc_attr( $icon['network'] ?? '' ); $url = esc_url( $icon['url'] ?? '#' ); $color = esc_attr( $iconColors[ $network ] ?? '#666666' ); $label = ucfirst( $network ); // Simple text-based icons for email compatibility $iconsHtml .= << {$label[0]} HTML; } if ( empty( $iconsHtml ) ) { return ''; } return << {$iconsHtml}
HTML; } /** * Render footer block. */ private function renderFooter( array $attrs ): string { $bgColor = esc_attr( $attrs['backgroundColor'] ?? '#f4f4f4' ); $textColor = esc_attr( $attrs['textColor'] ?? '#666666' ); $content = wp_kses_post( $attrs['content'] ?? '' ); $padding = esc_attr( $attrs['padding'] ?? '30px 20px' ); $align = esc_attr( $attrs['align'] ?? 'center' ); $fontSize = esc_attr( $attrs['fontSize'] ?? '12px' ); if ( empty( $content ) ) { $year = date( 'Y' ); $siteName = esc_html( get_bloginfo( 'name' ) ); $content = "© {$year} {$siteName}. All rights reserved."; } return <<
{$content}
HTML; } /** * Inject custom CSS as inline styles on the outermost HTML element. * * @param string $html The block HTML. * @param string $customCss CSS properties to inject (e.g., "background: #fff; border: 1px solid #ccc;"). * @return string HTML with custom CSS injected. */ private function injectCustomCss( string $html, string $customCss ): string { $sanitizedCss = esc_attr( trim( $customCss ) ); if ( empty( $sanitizedCss ) ) { return $html; } // Wrap in a div with inline styles (email-safe approach) return '
' . $html . '
'; } /** * Render conditional content block. * * @param array $attrs Block attributes. * @param array $children Child blocks. * @return string Rendered HTML with condition markers. */ private function renderConditionalContent( array $attrs, array $children, string $parentBgColor = '' ): string { $field = esc_attr( $attrs['field'] ?? '' ); $operator = esc_attr( $attrs['operator'] ?? 'not_empty' ); $value = esc_attr( $attrs['value'] ?? '' ); $padding = esc_attr( $attrs['padding'] ?? '0' ); $condition = wp_json_encode( array( 'field' => $field, 'operator' => $operator, 'value' => $value, ) ); $innerHtml = $this->renderBlocks( $children, $parentBgColor ); return '
' . $innerHtml . '
'; } /** * Render placeholder block. */ private function renderPlaceholder( string $placeholder, array $attrs ): string { $display = esc_attr( $attrs['display'] ?? 'inline' ); $padding = esc_attr( $attrs['padding'] ?? '0' ); if ( $display === 'button' ) { // Render as a styled button placeholder $bgColor = esc_attr( $attrs['backgroundColor'] ?? $this->globalStyles['primaryColor'] ); $textColor = esc_attr( $attrs['textColor'] ?? '#ffffff' ); $text = esc_html( $attrs['buttonText'] ?? __( 'Confirm', 'double-opt-in' ) ); $align = esc_attr( $attrs['align'] ?? 'center' ); return <<
{$text}
HTML; } // Inline display return $placeholder; } }