'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 <<
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 <<
|
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 <<
|
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 .= <<
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 = "
";
} 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}{$tag}>
|
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 <<
|
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 = "
";
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 <<
|
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 <<
|
HTML;
}
// Inline display
return $placeholder;
}
}