inlineStyles = []; $this->childCss = ''; // Parse blocks using WordPress function if available if (function_exists('parse_blocks')) { $blocks = parse_blocks($content); } else { // Fallback: use custom parser $blocks = $this->parseBlocksManually($content); } $blockHtmls = $this->renderBlocks($blocks); $css = $this->generateInlineStyles(); $css = BlockEditorHelper::replaceStyleSlugsWithValues($css); return $css . $blockHtmls; } private function collectInlineStyles($id, $attrs, $blockName = '') { if (!$attrs) { return; } $idSelector = '#' . $id; $styleAttr = []; // Handling the variable css attribuytes if ($fontSize = Arr::get($attrs, 'fontSize')) { $styleAttr['font-size'] = $this->resolveFontSizeValue($fontSize); } // Some blocks (notably list) may only store preset font size as className. if (empty($styleAttr['font-size']) && ($className = Arr::get($attrs, 'className'))) { if (preg_match('/has-([a-z0-9-]+)-font-size/i', (string)$className, $matches)) { $styleAttr['font-size'] = $this->resolveFontSizeValue($matches[1]); } } if ($height = Arr::get($attrs, 'height')) { $styleAttr['height'] = $height; } if ($textColor = Arr::get($attrs, 'textColor')) { $styleAttr['color'] = 'var(--fcom--color--' . $textColor . ')'; } if ($backgroundColor = Arr::get($attrs, 'backgroundColor')) { $colorValue = 'var(--fcom--color--' . $backgroundColor . ')'; if ($blockName === 'core/separator') { $styleAttr['color'] = $colorValue; } else { $styleAttr['background-color'] = $colorValue; } } $textAlign = Arr::get($attrs, 'align'); if ($blockName === 'core/button') { $textAlign = Arr::get($attrs, 'textAlign'); } if ($textAlign) { $styleAttr['text-align'] = $this->getDirectionalTextAlign($textAlign); } $className = (string)Arr::get($attrs, 'className', ''); if (preg_match('/has-([a-z0-9-]+)-font-family/i', $className, $fontFamilyMatch)) { $resolvedFontFamily = $this->resolveFontFamilyValue($fontFamilyMatch[1]); if ($resolvedFontFamily) { $styleAttr['font-family'] = $resolvedFontFamily; } } if (empty($styleAttr['font-family']) && ($fontFamily = Arr::get($attrs, 'fontFamily'))) { $resolvedFontFamily = $this->resolveFontFamilyValue($fontFamily); if ($resolvedFontFamily) { $styleAttr['font-family'] = $resolvedFontFamily; } } $style = Arr::get($attrs, 'style', []); $childCss = ''; $hadPadding = false; if ($style && is_array($style)) { if ($linkColor = Arr::get($style, 'elements.link.color.text')) { $childCss .= $idSelector . ' a { color: ' . $this->transformToCssVar($linkColor) . '; }'; } if ($color = Arr::get($style, 'color.text')) { $styleAttr['color'] = $color; } if ($bgColor = Arr::get($style, 'color.background')) { if ($blockName === 'core/separator') { $styleAttr['color'] = $bgColor; } else { $styleAttr['background-color'] = $bgColor; } } if ($paddings = Arr::get($style, 'spacing.padding', [])) { foreach ($paddings as $paddingType => $padding) { if (!$padding) { continue; } $hadPadding = true; $styleAttr['padding-' . $paddingType] = $this->transformToCssVar($padding); } } if ($margins = Arr::get($style, 'spacing.margin', [])) { foreach ($margins as $marginType => $margin) { if (!$margin) { continue; } $styleAttr['margin-' . $marginType] = $this->transformToCssVar($margin); } } if ($borderConfig = Arr::get($style, 'border', [])) { $width = Arr::get($borderConfig, 'width', '0px'); $color = Arr::get($borderConfig, 'color', ''); if (!$color) { if ($borderColor = Arr::get($attrs, 'borderColor', '')) { $color = 'var(--fcom--color--' . $borderColor . ')'; } } $borderStyle = Arr::get($borderConfig, 'style', 'solid'); if ($width || $color) { $styleAttr['border'] = trim($width . ' ' . $borderStyle . ' ' . $color); } foreach (['top', 'right', 'bottom', 'left'] as $borderSide) { $sideConfig = Arr::get($borderConfig, $borderSide, []); if (!$sideConfig || !is_array($sideConfig)) { continue; } $sideWidth = $this->normalizeCssSize(Arr::get($sideConfig, 'width', '')); $sideStyle = Arr::get($sideConfig, 'style', 'solid'); $sideColor = Arr::get($sideConfig, 'color', ''); if ($sideColor && strpos($sideColor, 'var:') === 0) { $sideColor = $this->transformToCssVar($sideColor); } elseif ($sideColor && strpos($sideColor, '#') !== 0 && strpos($sideColor, 'rgb') !== 0 && strpos($sideColor, 'hsl') !== 0 && strpos($sideColor, 'var(') !== 0) { $sideColor = 'var(--fcom--color--' . $sideColor . ')'; } if (!$sideWidth && !$sideColor) { continue; } $styleAttr['border-' . $borderSide] = trim($sideWidth . ' ' . $sideStyle . ' ' . $sideColor); } $borderRadius = Arr::get($borderConfig, 'radius', []); if (is_string($borderRadius) || is_numeric($borderRadius)) { $styleAttr['border-radius'] = $this->normalizeCssSize($borderRadius); } elseif ($borderRadius) { $radiusTypeMap = [ 'topLeft' => 'top-left', 'topRight' => 'top-right', 'bottomLeft' => 'bottom-left', 'bottomRight' => 'bottom-right' ]; foreach ($borderRadius as $radiusType => $radius) { if (!$radius || !isset($radiusTypeMap[$radiusType])) { continue; } $styleAttr['border-' . $radiusTypeMap[$radiusType] . '-radius'] = $radius; } } } if ($minHeight = Arr::get($style, 'dimensions.minHeight')) { $styleAttr['min-height'] = $this->normalizeCssSize($minHeight); } $typoGraphis = Arr::get($style, 'typography', []); foreach ($typoGraphis as $typographyType => $typoValue) { if (!$typoValue || !is_string($typoValue)) { continue; } $cssProperty = $this->camelCaseToKebabCase($typographyType); // Normalize font-size slugs/tokens to a valid CSS size. if ($cssProperty === 'font-size') { if (strpos($typoValue, 'var:') === 0) { $typoValue = $this->transformToCssVar($typoValue); } $typoValue = $this->resolveFontSizeValue($typoValue); } else if ($cssProperty === 'font-family') { $typoValue = $this->resolveFontFamilyValue($typoValue); } else if (strpos($typoValue, 'var:') === 0) { // Convert Gutenberg var:preset|... tokens to CSS vars. $typoValue = $this->transformToCssVar($typoValue); } $styleAttr[$cssProperty] = $typoValue; } } if ($blockName === 'core/list') { if (!empty($styleAttr['font-size'])) { $size = $styleAttr['font-size']; $childCss .= $idSelector . ' li, ' . $idSelector . ' li p { font-size: ' . $size . '; }'; } if (!empty($styleAttr['line-height'])) { $lineHeight = $styleAttr['line-height']; $childCss .= $idSelector . ' li, ' . $idSelector . ' li p { line-height: ' . $lineHeight . '; }'; } } else if ($blockName === 'core/button') { $className = (string)Arr::get($attrs, 'className', ''); $isOutline = strpos($className, 'is-style-outline') !== false; // Keep button text centered by default unless explicitly set. if (empty($styleAttr['text-align'])) { $styleAttr['text-align'] = 'center'; } if (empty($styleAttr['line-height'])) { $styleAttr['line-height'] = '1.5'; } if ($isOutline) { $backgroundColor = Arr::get($styleAttr, 'background-color'); $hasExplicitBackground = !empty($backgroundColor); if (!$hasExplicitBackground) { $styleAttr['background-color'] = 'transparent'; } if (empty($styleAttr['border'])) { $borderWidth = Arr::get($attrs, 'style.border.width', '2px'); $borderStyle = Arr::get($attrs, 'style.border.style', 'solid'); $borderColor = Arr::get($attrs, 'style.border.color', 'currentColor'); if (is_numeric($borderWidth)) { $borderWidth .= 'px'; } if (is_string($borderColor) && strpos($borderColor, 'var:') === 0) { $borderColor = $this->transformToCssVar($borderColor); } $styleAttr['border'] = trim($borderWidth . ' ' . $borderStyle . ' ' . $borderColor); } } } else if ($blockName === 'core/buttons') { $justifyContent = Arr::get($attrs, 'layout.justifyContent', 'left'); $alignMap = [ 'left' => 'left', 'center' => 'center', 'right' => 'right', 'space-between' => 'start', 'space-around' => 'center', 'space-evenly' => 'center' ]; if (isset($alignMap[(string)$justifyContent])) { $styleAttr['text-align'] = $this->getDirectionalTextAlign($alignMap[(string)$justifyContent]); } else if (empty($styleAttr['text-align'])) { $styleAttr['text-align'] = $this->getDirectionalTextAlign('left'); } } if (!empty($styleAttr['background-color']) && !$hadPadding && in_array($blockName, $this->autoPaddedElements)) { $styleAttr['padding'] = '20px'; } $this->inlineStyles[$idSelector] = $styleAttr; $this->childCss .= $childCss . ' '; } private function setDefaultBlockStyles($id, $styles = []) { $idSelector = '#' . $id; if (!isset($this->inlineStyles[$idSelector])) { $this->inlineStyles[$idSelector] = $styles; return; } foreach ($styles as $property => $value) { if (!isset($this->inlineStyles[$idSelector][$property])) { $this->inlineStyles[$idSelector][$property] = $value; } } return; } private function getDirectionalTextAlign($align, $default = 'left') { $align = strtolower(trim((string)$align)); if ($align === '') { $align = strtolower((string)$default); } if ($align === 'start') { $align = 'left'; } else if ($align === 'end') { $align = 'right'; } if (!in_array($align, ['left', 'center', 'right', 'justify'], true)) { $align = strtolower((string)$default); } if (fluentcrm_is_rtl()) { if ($align === 'left') { return 'right'; } if ($align === 'right') { return 'left'; } } return $align; } private function camelCaseToKebabCase($string) { return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $string)); } private function transformToCssVar($cssProperty) { if (strpos($cssProperty, 'var:') === 0) { $parts = explode('|', substr($cssProperty, 4)); if (count($parts) === 3) { return 'var(--wp--' . str_replace('|', '--', $parts[0] . '--' . $parts[1] . '--' . $parts[2]) . ')'; } } return $cssProperty; } private function normalizeCssSize($value) { if ($value === null || $value === '') { return ''; } $value = trim((string)$value); if (strpos($value, 'var:') === 0) { return $this->transformToCssVar($value); } return $value; } private function normalizeHtmlWidthAttribute($width) { $width = trim((string)$width); if ($width === '') { return ''; } if (substr($width, -2) === 'px') { return preg_replace('/[^0-9.]/', '', $width); } return $width; } private function generateInlineStyles() { $css = ''; return $css; } /** * Manual block parser (fallback if parse_blocks not available) */ private function parseBlocksManually($content) { $blocks = []; $pattern = '//'; preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE); $lastOffset = 0; foreach ($matches[0] as $index => $match) { $blockName = ($matches[1][$index][0] ?? '') . $matches[2][$index][0]; $attrs = $matches[3][$index][0] ?? '{}'; $isSelfClosing = !empty($matches[4][$index][0]); $blockStart = $match[1] + strlen($match[0]); // Find closing tag if not self-closing if (!$isSelfClosing) { $closingPattern = '//'; if (preg_match($closingPattern, $content, $closeMatch, PREG_OFFSET_CAPTURE, $blockStart)) { $innerHTML = substr($content, $blockStart, $closeMatch[0][1] - $blockStart); $lastOffset = $closeMatch[0][1] + strlen($closeMatch[0][0]); } else { $innerHTML = ''; } } else { $innerHTML = ''; } $blocks[] = [ 'blockName' => $blockName, 'attrs' => json_decode($attrs, true) ?? [], 'innerHTML' => trim($innerHTML), 'innerBlocks' => [] ]; } return $blocks; } /** * Render blocks to email HTML */ private function renderBlocks($blocks, $nested = false) { $html = ''; foreach ($blocks as $block) { if (empty($block['blockName'])) { // Classic content or unrecognized block if (!empty($block['innerHTML'])) { $html .= $this->wrapInTable($block['innerHTML']); } continue; } $html .= $this->renderBlock($block, $nested); } return $html; } /** * Render individual block */ private function renderBlock($block, $isNested = false) { $isInvisible = isset($block['attrs']['metadata']['blockVisibility']) && $block['attrs']['metadata']['blockVisibility'] === false; if ($isInvisible) { return ''; } $blockName = $block['blockName']; $attrs = $block['attrs'] ?? []; $innerHTML = $block['innerHTML'] ?? ''; $innerBlocks = $block['innerBlocks'] ?? []; // Per-block conditional visibility (conditional-content has its own handler) if ($blockName !== 'fluent-crm/conditional-content') { if (!$this->checkBlockConditionVisibility($attrs)) { return ''; } } // create an unique element ID for blocks that don't have one, to help with styling if needed $elementId = uniqid('block-', false); $this->collectInlineStyles($elementId, $attrs, $blockName); $attrs['elem_id'] = $elementId; $attrs['is_root'] = !$isNested; // For blocks with innerContent array, reconstruct innerHTML if (empty($innerHTML) && !empty($block['innerContent'])) { $innerHTML = implode('', array_filter($block['innerContent'], 'is_string')); } // Handle different block types switch ($blockName) { case 'core/block': return $this->renderSyncedPattern($attrs, $isNested); case 'core/paragraph': // done return $this->renderParagraph($innerHTML, $attrs); case 'core/heading': // done return $this->renderHeading($innerHTML, $attrs); case 'core/image': // done return $this->renderImage($attrs, $innerHTML); case 'core/list': // done return $this->renderList($innerHTML, $innerBlocks, $attrs); case 'core/list-item': // done return $this->renderListItem($innerHTML, $attrs); case 'core/quote': // done return $this->renderQuote($innerHTML, $innerBlocks, $attrs); case 'core/button': // done return $this->renderButton($innerHTML, $attrs); case 'core/buttons': // done return $this->renderButtons($innerBlocks, $attrs); case 'core/columns': // done return $this->renderColumns($innerBlocks, $attrs); case 'core/column': // done return $this->renderColumn($innerBlocks, $attrs, $innerHTML); case 'core/separator': // partially done return $this->renderSeparator($innerHTML, $attrs); case 'core/spacer': // done return $this->renderSpacer($innerHTML, $attrs); case 'core/group': // done return $this->renderGroup($innerBlocks, $attrs, $innerHTML); case 'core/row': return $this->renderRow($innerBlocks, $attrs, $innerHTML); case 'core/table': // done return $this->renderTable($innerHTML, $attrs); case 'core/rss': // done return $this->renderRss($attrs); case 'fluentcrm/woo-product': case 'fluent-crm/woo-product': return $this->renderWooProductBlock($block, $attrs, $innerHTML); case 'fluent-crm/cart-product': return $this->renderCartProductBlock($block, $attrs, $innerHTML); case 'fluent-crm/latest-posts': // partially done return $this->renderLatestPostsBlock($block, $attrs); case 'fluent-crm/woo-products': // partially done return $this->renderProductsBlock($block, $attrs, $blockName); case 'fluent-crm/cart-products': // partially done return $this->renderCartProductsBlock($block, $attrs); case 'fluent-crm/conditional-content': // done case 'fluentcrm/conditional-group': // done return $this->renderConditionalGroupBlock($innerBlocks, $attrs, $innerHTML); case 'core/freeform': // done case 'core/html': // done // Classic editor content - render as-is with email-safe wrapper return $this->wrapInTable($innerHTML, $attrs); case 'core/preformatted': // done case 'core/code': // done case 'core/verse': // done return $this->renderCodeBlock($innerHTML, $attrs); case 'core/pullquote': // done return $this->renderPullQuote($innerHTML, $attrs); case 'core/embed': case 'core/video': case 'core/audio': // For video/audio embeds in email, show a linked thumbnail or text $url = $attrs['url'] ?? ''; if (!empty($url)) { $linkText = __('Click here to view media content', 'fluent-crm'); return $this->wrapInTable("
", $attrs); } return ''; default: // Fallback for unrecognized blocks if (!empty($innerHTML)) { $attrs['td_id'] = $attrs['elem_id'] ?? ''; return $this->wrapInTable($innerHTML, $attrs); } if (!empty($innerBlocks)) { return $this->renderBlocks($innerBlocks, true); } return ''; } } /** * Render paragraph block */ private function renderParagraph($content, $attrs) { $content = trim($content); // Skip empty paragraphs if (empty($content) || $content === '') { return ''; } // Extract content if it's wrapped intags if (preg_match('/
]*>(.*?)<\/p>/s', $content, $matches)) { $innerContent = $matches[1]; } else { $innerContent = $content; } $elementId = $attrs['elem_id'] ?? ''; return $this->wrapInTable("
{$innerContent}
", $attrs); } /** * Render heading block */ private function renderHeading($content, $attrs) { $level = $attrs['level'] ?? 2; // Extract content if it's wrapped in heading tags if (preg_match('/ tags if exists, as sometimes code block can have that wrapper in innerHTML
if (preg_match('/]*>(.*?)<\/pre>/s', $innerHTML, $matches)) {
$code = $matches[1];
}
if (!$code) {
return '';
}
$elementId = $attrs['elem_id'] ?? '';
if (!isset($this->inlineStyles['#' . $elementId]['background-color'])) {
$this->inlineStyles['#' . $elementId]['background-color'] = '#e5e7eb';
}
return $this->wrapInTable("{$code}", $attrs);
}
private function renderPullQuote($innerHTML, $attrs)
{
$html = '';
// get the content between tags if exists, as sometimes code block can have that wrapper in innerHTML
if (preg_match('/]*>(.*?)<\/blockquote>/s', $innerHTML, $matches)) {
$html = $matches[1];
}
if (!$html) {
return '';
}
$elementId = $attrs['elem_id'] ?? '';
$elementSelector = $elementId ? ('#' . $elementId) : '';
$existingStyles = $elementSelector ? ($this->inlineStyles[$elementSelector] ?? []) : [];
$hasBlockFontSize = !empty(Arr::get($existingStyles, 'font-size')) ||
!empty(Arr::get($attrs, 'fontSize')) ||
!empty(Arr::get($attrs, 'style.typography.fontSize'));
// Pullquote defaults for title paragraph:
// - Always ensure margin + line-height when missing.
// - Add 25px font-size only when no font-size is configured.
$hasParagraphFontSize = preg_match('/]*(?:style=["\'][^"\']*font-size\s*:|class=["\'][^"\']*has-[a-z0-9-]+-font-size)/i', $html);
$needsDefaultTitleFontSize = (!$hasBlockFontSize && !$hasParagraphFontSize);
$firstPApplied = false;
$html = preg_replace_callback('/
]*)>/i', function ($matches) use (&$firstPApplied, $needsDefaultTitleFontSize) {
if ($firstPApplied) {
return $matches[0];
}
$firstPApplied = true;
$attrs = $matches[1];
if (preg_match('/style=(["\'])(.*?)\1/i', $attrs, $styleMatch)) {
$quote = $styleMatch[1];
$styleValue = rtrim($styleMatch[2], ';');
if ($needsDefaultTitleFontSize && !preg_match('/font-size\s*:/i', $styleValue)) {
$styleValue .= '; font-size: 25px';
}
if (!preg_match('/(?:^|;)\s*margin\s*:/i', $styleValue)) {
$styleValue .= '; margin: 0 0 1em 0';
}
if (!preg_match('/line-height\s*:/i', $styleValue)) {
$styleValue .= '; line-height: 1.6';
}
$styleValue = trim($styleValue, " ;") . ';';
$attrs = preg_replace('/style=(["\'])(.*?)\1/i', 'style=' . $quote . $styleValue . $quote, $attrs, 1);
return '
';
}
$styleParts = [];
if ($needsDefaultTitleFontSize) {
$styleParts[] = 'font-size: 25px';
}
$styleParts[] = 'margin: 0 0 1em 0';
$styleParts[] = 'line-height: 1.6';
return '
';
}, $html, 1);
if (!$hasBlockFontSize && !preg_match('/]*(?:style=["\'][^"\']*font-size\s*:|class=["\'][^"\']*has-[a-z0-9-]+-font-size)/i', $html)) {
$html = preg_replace_callback('/]*)>/i', function ($matches) {
$attrs = $matches[1];
if (preg_match('/style=(["\'])(.*?)\1/i', $attrs, $styleMatch)) {
$quote = $styleMatch[1];
$styleValue = rtrim($styleMatch[2], ';') . '; font-size: 16px;';
$attrs = preg_replace('/style=(["\'])(.*?)\1/i', 'style=' . $quote . $styleValue . $quote, $attrs, 1);
return '';
}
return '';
}, $html, 1);
}
$styles = 'width: 100%;';
$defaultStyles = [
'padding-left' => '20px',
'padding-right' => '20px',
'padding-bottom' => '20px',
'padding-top' => '20px',
'margin-bottom' => '20px',
'text-align' => $this->getDirectionalTextAlign(Arr::get($attrs, 'textAlign', 'left'))
];
if ($elementId) {
$hasCustomBorder = isset($existingStyles['border'])
|| isset($existingStyles['border-left'])
|| isset($existingStyles['border-right'])
|| isset($existingStyles['border-top'])
|| isset($existingStyles['border-bottom']);
if (!$hasCustomBorder) {
$defaultStyles['border-left'] = '4px solid #e5e7eb';
$defaultStyles['border-right'] = '4px solid #e5e7eb';
$defaultStyles['border-bottom'] = '4px solid #e5e7eb';
$defaultStyles['border-top'] = '4px solid #e5e7eb';
}
$this->setDefaultBlockStyles($elementId, $defaultStyles);
}
return $this->wrapInTable("{$html}", $attrs);
}
/**
* Check per-block conditional visibility attributes.
* Returns true if the block should be shown, false if hidden.
*/
private function checkBlockConditionVisibility($attrs)
{
$conditionType = isset($attrs['fcrmConditionType']) ? $attrs['fcrmConditionType'] : '';
$conditionType = $this->normalizeConditionalCheckType($conditionType);
if (empty($conditionType)) {
return true;
}
$subscriber = BlockParserHelper::getSubscriber();
if (!$subscriber) {
$subscriber = apply_filters('fluent_crm/get_current_block_condition_subscriber', $subscriber);
}
if (!$subscriber) {
return true;
}
$tagIds = isset($attrs['fcrmTagIds']) && is_array($attrs['fcrmTagIds']) ? $attrs['fcrmTagIds'] : [];
if (empty($tagIds)) {
return true;
}
$tagMatched = $subscriber->hasAnyTagId($tagIds);
if ($conditionType === 'show_if_tag_exist') {
return $tagMatched;
}
if ($conditionType === 'show_if_tag_not_exist') {
return !$tagMatched;
}
return true;
}
/**
* Keep backward compatibility with legacy conditional values.
*/
private function normalizeConditionalCheckType($checkType)
{
$checkType = trim((string)$checkType);
if ($checkType === '') {
return '';
}
$map = [
// v2 legacy values
'show_if_tag_exists' => 'show_if_tag_exist',
'show_if_tag_not_exists' => 'show_if_tag_not_exist',
];
return Arr::get($map, $checkType, $checkType);
}
/**
* Render columns block
*/
private function renderColumns($innerBlocks, $attrs)
{
if (empty($innerBlocks)) {
return '';
}
$hasBlockGap = isset($attrs['style']['spacing']['blockGap']['left']);
$blockGap = 20;
if ($hasBlockGap) {
$blockGap = (int)str_replace('px', '', $attrs['style']['spacing']['blockGap']['left']);
}
$id = $attrs['elem_id'] ?? '';
$verticalAlignment = Arr::get($attrs, 'verticalAlignment', 'top');
$columnCount = count($innerBlocks);
$columnWidth = floor(100 / $columnCount);
$hasExplicitColumnWidths = false;
foreach ($innerBlocks as $innerBlock) {
if (!empty(Arr::get($innerBlock, 'attrs.width'))) {
$hasExplicitColumnWidths = true;
break;
}
}
$isMobileStackable = Arr::get($attrs, 'isStackedOnMobile', true);
$tableClass = 'fc_columns';
if ($isMobileStackable) {
$tableClass .= ' fc_columns_stack_mobile';
}
$columnTableStyles = [
'width' => '100%',
'border-collapse' => 'collapse',
'border-spacing' => '0'
];
if ($margins = Arr::get($attrs, 'style.spacing.margin', [])) {
foreach ($margins as $marginType => $marginValue) {
if (!$marginValue) {
continue;
}
$columnTableStyles['margin-' . $marginType] = $this->normalizeCssSize($marginValue);
}
}
$columnsHtml = '';
foreach ($innerBlocks as $index => $column) {
$blockName = $column['blockName'] ?? '';
if ($blockName === 'core/column') {
$elementId = uniqid('block-', false);
$this->collectInlineStyles($elementId, $column['attrs'] ?? [], 'core/column');
$columnVerticalAlignment = Arr::get($column, 'attrs.verticalAlignment', $verticalAlignment);
$styles = [
'vertical-align' => $columnVerticalAlignment,
];
$columnWidthValue = $this->normalizeCssSize(Arr::get($column, 'attrs.width', ''));
$widthAttr = '';
if ($columnWidthValue) {
$styles['width'] = $columnWidthValue;
if (substr($columnWidthValue, -2) === 'px') {
$styles['max-width'] = $columnWidthValue;
}
$widthAttr = $this->normalizeHtmlWidthAttribute($columnWidthValue);
} elseif (!$hasExplicitColumnWidths) {
$styles['width'] = $columnWidth . '%';
$widthAttr = $columnWidth . '%';
}
$padding = $blockGap / 2;
$styles['padding-left'] = $padding . 'px';
$styles['padding-right'] = $padding . 'px';
$widthMarkup = $widthAttr ? ' width="' . esc_attr($widthAttr) . '"' : '';
$columnsHtml .= '';
$column['attrs']['elem_id'] = $elementId;
$columnsHtml .= $this->renderColumn($column['innerBlocks'], $column['attrs'], $column['innerHTML']);
$columnsHtml .= ' ';
} else {
$columnsHtml .= '';
$columnsHtml .= $this->renderBlock($column, true);
$columnsHtml .= ' ';
}
}
$columnsHtml .= '
';
$attrs['td_id'] = $id;
return $this->wrapInTable($columnsHtml, $attrs);
}
/**
* Render column block
*/
private function renderColumn($innerBlocks, $attrs, $innerHTML)
{
$html = '';
if (!empty($innerBlocks)) {
$html = $this->renderBlocks($innerBlocks, true);
} elseif (!empty($innerHTML)) {
$html = $innerHTML;
}
$attrs['td_id'] = $attrs['elem_id'] ?? '';
return $this->wrapInTable($html, $attrs);
}
/**
* Render cover block
*/
private function renderCover($innerBlocks, $attrs, $innerHTML)
{
$url = $attrs['url'] ?? '';
$dimRatio = $attrs['dimRatio'] ?? 50;
$overlayColor = $attrs['overlayColor'] ?? '';
$style = $attrs['style'] ?? [];
$contentPosition = $attrs['contentPosition'] ?? 'center center';
$minHeight = $attrs['minHeight'] ?? '';
$minHeightUnit = $attrs['minHeightUnit'] ?? 'px';
// Extract image URL from innerHTML if not in attrs
if (empty($url) && !empty($innerHTML)) {
if (preg_match('/src=["\']([^"\']+)["\']/', $innerHTML, $matches)) {
$url = $matches[1];
}
}
$opacity = $dimRatio / 100;
// Parse content position (e.g., "top center", "center center", "bottom left")
$verticalAlign = 'center';
$textAlign = 'center';
if (!empty($contentPosition)) {
$positions = explode(' ', $contentPosition);
if (count($positions) >= 2) {
$verticalAlign = $positions[0]; // top, center, bottom
$textAlign = $positions[1]; // left, center, right
} elseif (count($positions) === 1) {
$verticalAlign = $positions[0];
}
}
$textAlign = $this->getDirectionalTextAlign($textAlign, 'center');
// Map vertical alignment to table cell vertical-align
$vAlignStyle = $verticalAlign === 'top' ? 'top' : ($verticalAlign === 'bottom' ? 'bottom' : 'middle');
// Determine minimum height
$minHeightValue = $minHeight ? $minHeight . $minHeightUnit : '300px';
// Render inner content
$innerContent = '';
if (!empty($innerBlocks)) {
foreach ($innerBlocks as $block) {
$innerContent .= $this->renderBlock($block);
}
}
// For email, create a table-based layout with background image
if ($url) {
$html = '';
$html .= '';
$html .= '';
$html .= $innerContent;
$html .= ' ';
$html .= ' ';
$html .= '
';
return $html;
}
return $this->wrapInTable($innerContent);
}
/**
* Render separator block
*/
private function renderSeparator($innerHTML, $attrs)
{
$class = 'fc_separator';
if ($className = Arr::get($attrs, 'className')) {
$class .= ' ' . $className;
}
$id = $attrs['elem_id'] ?? '';
$separatorColor = Arr::get($attrs, 'style.color.background', '');
if (!$separatorColor) {
$separatorColor = Arr::get($attrs, 'backgroundColor', '');
}
if ($separatorColor && strpos($separatorColor, '#') !== 0 && strpos($separatorColor, 'rgb') !== 0 && strpos($separatorColor, 'hsl') !== 0 && strpos($separatorColor, 'var(') !== 0 && strpos($separatorColor, 'var:') !== 0) {
$separatorColor = 'var(--fcom--color--' . $separatorColor . ')';
}
if ($separatorColor && strpos($separatorColor, 'var:') === 0) {
$separatorColor = $this->transformToCssVar($separatorColor);
}
if ($separatorColor && strpos($separatorColor, 'var(') === 0) {
$separatorColor = BlockEditorHelper::replaceStyleSlugsWithValues($separatorColor);
}
if (!$separatorColor) {
$separatorColor = '#d1d5db';
}
$escapedId = esc_attr($id);
$escapedClass = esc_attr($class);
$escapedSeparatorColor = esc_attr($separatorColor);
$isDots = strpos($class, 'is-style-dots') !== false;
$hasWideAlignmentClass = strpos($class, 'alignwide') !== false || strpos($class, 'alignfull') !== false;
$isWide = strpos($class, 'is-style-wide') !== false || $hasWideAlignmentClass || in_array(Arr::get($attrs, 'align', ''), ['wide', 'full'], true);
if ($isDots) {
$separator = "···";
return $this->wrapInTable($separator, $attrs);
}
$lineWidth = $isWide ? '100%' : '100px';
$separator = "
";
return $this->wrapInTable($separator, $attrs);
}
/**
* Render spacer block
*/
private function renderSpacer($innerHtml, $attrs)
{
$id = $attrs['elem_id'] ?? '';
return $this->wrapInTable(" ", $attrs);
}
/**
* Render group block
*/
private function renderGroup($innerBlocks, $attrs, $innerHTML)
{
$layoutType = strtolower((string)Arr::get($attrs, 'layout.type', ''));
if ($layoutType === 'flex') {
return $this->renderRow($innerBlocks, $attrs, $innerHTML);
}
if (!empty($innerBlocks)) {
$content = $this->renderBlocks($innerBlocks, true);
} elseif (!empty($innerHTML)) {
$content = $innerHTML;
} else {
return '';
}
$id = $attrs['elem_id'] ?? '';
$disableBottomSpacing = !empty($attrs['fcrmDisableBottomSpacing']);
$groupClasses = ['fc_group'];
if (!empty($attrs['is_root'])) {
$groupClasses[] = 'fc_group_root';
}
if ($disableBottomSpacing) {
$groupClasses[] = 'fcrm-no-bottom-spacing';
}
$contentSize = $this->normalizeCssSize(Arr::get($attrs, 'layout.contentSize', ''));
if ($contentSize) {
$innerStyles = [
'max-width' => $contentSize,
'margin-left' => 'auto',
'margin-right' => 'auto'
];
$content = "{$content}";
}
return $this->wrapInTable("{$content}", $attrs);
}
/**
* Render row block.
*/
private function renderRow($innerBlocks, $attrs, $innerHTML)
{
if (empty($innerBlocks)) {
if (!empty($innerHTML)) {
return $this->wrapInTable($innerHTML, $attrs);
}
return '';
}
$layout = Arr::get($attrs, 'layout', []);
$orientation = strtolower((string)Arr::get($layout, 'orientation', 'horizontal'));
$className = strtolower((string)Arr::get($attrs, 'className', ''));
if (strpos($className, 'is-vertical') !== false) {
$orientation = 'vertical';
}
// A vertical row behaves like stacked content in email clients.
if ($orientation === 'vertical') {
$content = !empty($innerBlocks) ? $this->renderBlocks($innerBlocks, true) : $innerHTML;
if (!$content) {
return '';
}
$id = $attrs['elem_id'] ?? '';
$disableBottomSpacing = !empty($attrs['fcrmDisableBottomSpacing']);
$groupClasses = ['fc_group'];
if (!empty($attrs['is_root'])) {
$groupClasses[] = 'fc_group_root';
}
if ($disableBottomSpacing) {
$groupClasses[] = 'fcrm-no-bottom-spacing';
}
$contentSize = $this->normalizeCssSize(Arr::get($attrs, 'layout.contentSize', ''));
if ($contentSize) {
$innerStyles = [
'max-width' => $contentSize,
'margin-left' => 'auto',
'margin-right' => 'auto'
];
$content = "{$content}";
}
return $this->wrapInTable("{$content}", $attrs);
}
$justifyContent = strtolower((string)Arr::get($layout, 'justifyContent', 'left'));
$justifyMode = 'left';
if (in_array($justifyContent, ['right', 'end'], true)) {
$justifyMode = 'right';
} elseif ($justifyContent === 'center') {
$justifyMode = 'center';
} elseif ($justifyContent === 'space-between') {
$justifyMode = 'space-between';
}
$rowGap = Arr::get($attrs, 'style.spacing.blockGap', '');
if (is_array($rowGap)) {
$rowGap = Arr::get($rowGap, 'left', Arr::get($rowGap, 'horizontal', ''));
}
$rowGap = $this->normalizeCssSize($rowGap);
if (!$rowGap) {
$rowGap = '20px';
}
$id = $attrs['elem_id'] ?? '';
$blockCount = count($innerBlocks);
$renderRowChild = function ($innerBlock) {
$childBlock = $innerBlock;
$childAttrs = (array)Arr::get($childBlock, 'attrs', []);
// Row children must remain content-width for predictable alignment.
$childAttrs['fcrmTableWidth'] = 'auto';
$childBlock['attrs'] = $childAttrs;
return $this->renderBlock($childBlock, true);
};
if ($justifyMode === 'space-between') {
$tableStyles = [
'width' => '100%',
'border-collapse' => 'separate',
'border-spacing' => '0'
];
$rowHtml = '';
$startAlign = $this->getDirectionalTextAlign('left');
$endAlign = $this->getDirectionalTextAlign('right');
foreach ($innerBlocks as $index => $innerBlock) {
$cellStyles = [
'vertical-align' => 'top'
];
$cellAlign = 'center';
if ($index === 0) {
$cellAlign = $startAlign;
} elseif ($index === ($blockCount - 1)) {
$cellAlign = $endAlign;
}
$rowHtml .= '';
$rowHtml .= $renderRowChild($innerBlock);
$rowHtml .= ' ';
}
$rowHtml .= '
';
} else {
$outerAlign = $justifyMode === 'center'
? 'center'
: ($justifyMode === 'right' ? $this->getDirectionalTextAlign('right') : $this->getDirectionalTextAlign('left'));
$innerTableStyles = [
'width' => 'auto',
'border-collapse' => 'separate',
'border-spacing' => '0'
];
$innerHtml = '';
foreach ($innerBlocks as $index => $innerBlock) {
$cellStyles = [
'vertical-align' => 'top'
];
if ($index < ($blockCount - 1)) {
if (fluentcrm_is_rtl()) {
$cellStyles['padding-left'] = $rowGap;
} else {
$cellStyles['padding-right'] = $rowGap;
}
}
$innerHtml .= '';
$innerHtml .= $renderRowChild($innerBlock);
$innerHtml .= ' ';
}
$innerHtml .= '
';
$rowHtml = '' . $innerHtml . '
';
}
unset($attrs['elem_id']);
return $this->wrapInTable($rowHtml, $attrs);
}
/**
* Append inline CSS to an opening HTML tag without dropping existing styles.
*/
private function appendInlineStyleToTag($tag, $style)
{
if (preg_match('/\sstyle=(["\'])(.*?)\1/i', $tag, $matches)) {
$quote = $matches[1];
$existingStyle = rtrim(trim($matches[2]), ';');
$newStyle = $existingStyle ? $existingStyle . '; ' . $style : $style;
// Use str_replace (not preg_replace) so existing style content cannot be
// misread as a regex backreference in the replacement string.
return str_replace($matches[0], ' style=' . $quote . $newStyle . $quote, $tag);
}
return preg_replace('/>$/', ' style="' . $style . '">', $tag, 1);
}
/**
* Apply email-safe stripe backgrounds to table body rows.
*/
private function applyTableStripeStyles($tableContent, $stripeColor = '#f0f0f0')
{
return preg_replace_callback('/]*)>(.*?)<\/tbody>/is', function ($tbodyMatches) use ($stripeColor) {
$rowIndex = 0;
$tbodyContent = preg_replace_callback('/]*)>(.*?)<\/tr>/is', function ($rowMatches) use (&$rowIndex, $stripeColor) {
$rowIndex++;
if ($rowIndex % 2 === 0) {
return $rowMatches[0];
}
$rowContent = preg_replace_callback('/<(td|th)\b([^>]*)>/i', function ($cellMatches) use ($stripeColor) {
$tag = $this->appendInlineStyleToTag($cellMatches[0], 'background-color: ' . $stripeColor . ';');
if (stripos($tag, ' bgcolor=') === false) {
$tag = preg_replace('/>$/', ' bgcolor="' . $stripeColor . '">', $tag, 1);
}
return $tag;
}, $rowMatches[2]);
return ' ' . $rowContent . ' ';
}, $tbodyMatches[2]);
return '' . $tbodyContent . '';
}, $tableContent);
}
/**
* Render table block
*/
private function renderTable($content, $attrs)
{
$borderConfig = Arr::get($attrs, 'style.border', []);
$hasCustomBorder = !empty(Arr::get($borderConfig, 'width')) ||
!empty(Arr::get($borderConfig, 'style')) ||
!empty(Arr::get($borderConfig, 'color')) ||
!empty(Arr::get($attrs, 'borderColor'));
$className = (string)Arr::get($attrs, 'className', '');
$isStriped = strpos($className, 'is-style-stripes') !== false;
if ($hasCustomBorder) {
$borderWidth = Arr::get($borderConfig, 'width', '1px');
$borderStyle = Arr::get($borderConfig, 'style', 'solid');
$borderColor = BlockEditorHelper::getBorderColor($attrs);
} else {
$borderWidth = '1px';
$borderStyle = 'solid';
$borderColor = '#6b7280';
}
$borderCss = "border: {$borderWidth} {$borderStyle} {$borderColor};";
$styles = "width: 100%; border-collapse: collapse;";
$cellStyles = "padding: 6px 10px;" . $borderCss;
if (!isset($attrs['style']['spacing']['margin']['bottom'])) {
$styles .= " margin-bottom: 20px;";
}
// Extract table content
if (preg_match('/]*>(.*?)<\/table>/s', $content, $matches)) {
$tableContent = $matches[1];
// Add styles to table cells
$tableContent = preg_replace_callback('/]*)>/i', function ($matches) use ($cellStyles) {
return $this->appendInlineStyleToTag($matches[0], $cellStyles);
}, $tableContent);
$tableContent = preg_replace_callback('/ ]*)>/i', function ($matches) use ($cellStyles) {
return $this->appendInlineStyleToTag($matches[0], $cellStyles . ' font-weight: bold;');
}, $tableContent);
if ($isStriped) {
$tableContent = $this->applyTableStripeStyles($tableContent);
}
} else {
return '';
}
$id = $attrs['elem_id'] ?? '';
$tableClass = 'fc_table';
if ($isStriped) {
$tableClass .= ' fc_table_striped';
}
return $this->wrapInTable("{$tableContent}
", $attrs);
}
/**
* Render social links block
*/
private function renderSocialLinks($innerBlocks, $attrs, $innerHTML)
{
$socialHtml = '';
$socialLinks = [];
// First, try to get from innerBlocks (preferred method)
if (!empty($innerBlocks)) {
foreach ($innerBlocks as $block) {
if ($block['blockName'] === 'core/social-link') {
$blockAttrs = $block['attrs'] ?? [];
$blockInner = $block['innerHTML'] ?? '';
// Reconstruct innerHTML from innerContent if available
if (empty($blockInner) && !empty($block['innerContent'])) {
$blockInner = implode('', array_filter($block['innerContent'], 'is_string'));
}
$url = $blockAttrs['url'] ?? '';
$service = $blockAttrs['service'] ?? 'link';
$label = $blockAttrs['label'] ?? '';
// If URL is empty, try to extract from innerHTML
if (empty($url) && !empty($blockInner)) {
if (preg_match('/]*href=["\']([^"\']*)["\']/', $blockInner, $urlMatch)) {
$url = $urlMatch[1];
}
}
// Extract label from aria-label or innerHTML
if (empty($label) && !empty($blockInner)) {
if (preg_match('/aria-label=["\']([^"\']*)["\']/', $blockInner, $labelMatch)) {
$label = $labelMatch[1];
}
}
// Determine service from URL if not set
if ($service === 'link' && !empty($url)) {
if (strpos($url, 'wordpress.org') !== false || strpos($url, 'wordpress.com') !== false) {
$service = 'wordpress';
} elseif (strpos($url, 'facebook.com') !== false) {
$service = 'facebook';
} elseif (strpos($url, 'github.com') !== false) {
$service = 'github';
} elseif (strpos($url, 'twitter.com') !== false || strpos($url, 'x.com') !== false) {
$service = 'twitter';
} elseif (strpos($url, 'linkedin.com') !== false) {
$service = 'linkedin';
} elseif (strpos($url, 'instagram.com') !== false) {
$service = 'instagram';
} elseif (strpos($url, 'amazon.com') !== false) {
$service = 'amazon';
}
}
if (!empty($url)) {
$socialLinks[] = [
'url' => $url,
'service' => $service,
'label' => $label
];
}
}
}
}
// Fallback: Parse social links from innerHTML
if (empty($socialLinks) && !empty($innerHTML)) {
preg_match_all('/]*class="[^"]*wp-social-link[^"]*"[^>]*>.*?]*href=["\']([^"\']*)["\'][^>]*(?:aria-label=["\']([^"\']*)["\'])?[^>]*>.*?<\/a>.*?<\/li>/s', $innerHTML, $matches);
if (!empty($matches[1])) {
foreach ($matches[0] as $index => $match) {
$url = $matches[1][$index];
$label = $matches[2][$index] ?? '';
// Determine service from class or URL
$service = 'link';
if (strpos($match, 'wp-social-link-wordpress') !== false || strpos($url, 'wordpress.org') !== false || strpos($url, 'wordpress.com') !== false) {
$service = 'wordpress';
} elseif (strpos($match, 'wp-social-link-facebook') !== false || strpos($url, 'facebook.com') !== false) {
$service = 'facebook';
} elseif (strpos($match, 'wp-social-link-github') !== false || strpos($url, 'github.com') !== false) {
$service = 'github';
} elseif (strpos($match, 'wp-social-link-twitter') !== false || strpos($url, 'twitter.com') !== false || strpos($url, 'x.com') !== false) {
$service = 'twitter';
} elseif (strpos($match, 'wp-social-link-linkedin') !== false || strpos($url, 'linkedin.com') !== false) {
$service = 'linkedin';
} elseif (strpos($match, 'wp-social-link-instagram') !== false || strpos($url, 'instagram.com') !== false) {
$service = 'instagram';
} elseif (strpos($match, 'wp-social-link-amazon') !== false || strpos($url, 'amazon.com') !== false) {
$service = 'amazon';
}
$socialLinks[] = [
'url' => $url,
'service' => $service,
'label' => $label
];
}
}
}
// Render social links using image icons or better text fallbacks
if (!empty($socialLinks)) {
foreach ($socialLinks as $link) {
$iconHtml = $this->getSocialIconHtml($link['service'], $link['label']);
$socialHtml .= '';
$socialHtml .= $iconHtml;
$socialHtml .= '';
}
}
$socialHtml .= ' ';
return $this->wrapInTable($socialHtml);
}
/**
* Get social media icon HTML (with better styling)
*/
private function getSocialIconHtml($service, $label = '')
{
// Get background color for each service
$colors = [
'facebook' => '#1877f2',
'twitter' => '#1da1f2',
'linkedin' => '#0077b5',
'instagram' => '#e4405f',
'github' => '#181717',
'wordpress' => '#21759b',
'amazon' => '#ff9900',
'link' => '#0073aa'
];
$bgColor = $colors[$service] ?? '#0073aa';
$alt = $label ?: ucfirst($service);
// Use service-specific icon rendering
$iconContent = $this->getSocialIconSVG($service);
return '' . $iconContent . '';
}
/**
* Get social media icon SVG or text representation
*/
private function getSocialIconSVG($service)
{
// Simple SVG icons as inline data
$icons = [
'facebook' => '',
'twitter' => '',
'linkedin' => '',
'instagram' => '',
'github' => '',
'wordpress' => '',
'amazon' => '',
'link' => ''
];
return $icons[$service] ?? $icons['link'];
}
/**
* Get color from WordPress color slug
*/
private function getColorFromSlug($slug)
{
// Debug: Uncomment to see what colors your theme provides
// $this->debugThemeColors();
// First, try to get from FluentCRM Helper (which reads theme.json and editor-color-palette)
static $colorMap = null;
if ($colorMap === null) {
$colorMap = [];
// Get theme colors using the same method as AdminMenu.php
if (class_exists('\FluentCrm\App\Services\Helper')) {
$themeColors = \FluentCrm\App\Services\Helper::getThemeColorPalette();
if (!empty($themeColors)) {
foreach ($themeColors as $colorData) {
if (isset($colorData['slug']) && isset($colorData['color'])) {
$colorMap[$colorData['slug']] = $colorData['color'];
}
}
}
// Also get theme preferences
$themePref = \FluentCrm\App\Services\Helper::getThemePrefScheme();
if (!empty($themePref['colors'])) {
foreach ($themePref['colors'] as $colorData) {
if (isset($colorData['slug']) && isset($colorData['color'])) {
$colorMap[$colorData['slug']] = $colorData['color'];
}
}
}
}
}
// Check our color map first
if (isset($colorMap[$slug])) {
return $colorMap[$slug];
}
// Try to get theme color from WordPress theme.json or global settings
if (function_exists('wp_get_global_settings')) {
$settings = wp_get_global_settings();
if (!empty($settings['color']['palette']['theme'])) {
foreach ($settings['color']['palette']['theme'] as $color) {
if (isset($color['slug']) && $color['slug'] === $slug && !empty($color['color'])) {
return $color['color'];
}
}
}
}
// Try WP_Theme_JSON for block themes
if (class_exists('WP_Theme_JSON_Resolver')) {
$theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
if ($theme_json) {
$settings = $theme_json->get_settings();
if (!empty($settings['color']['palette'])) {
foreach ($settings['color']['palette'] as $palette) {
if (isset($palette['slug']) && $palette['slug'] === $slug && !empty($palette['color'])) {
return $palette['color'];
}
}
}
}
}
// Fallback: Common WordPress and popular theme colors
$colors = [
// Theme palette colors (adjust these based on your active theme)
// Twenty Twenty-Three defaults
'theme-palette-color-1' => '#000000', // Base/Black
'theme-palette-color-2' => '#6f42c1', // Purple
'theme-palette-color-3' => '#007cba', // Blue
'theme-palette-color-4' => '#16a085', // Teal
'theme-palette-color-5' => '#e74c3c', // Red
'theme-palette-color-6' => '#f39c12', // Orange
'theme-palette-color-7' => '#ffffff', // White
'theme-palette-color-8' => '#f5f5f5', // Light Gray
'theme-palette-color-9' => '#cccccc', // Gray
// Standard WordPress colors
'black' => '#000000',
'white' => '#ffffff',
'primary' => '#0073aa',
'secondary' => '#23282d',
'tertiary' => '#F0F0F1',
// Common named colors
'red' => '#e74c3c',
'blue' => '#3498db',
'green' => '#2ecc71',
'yellow' => '#f1c40f',
'orange' => '#e67e22',
'purple' => '#9b59b6',
'cyan' => '#1abc9c',
'vivid-red' => '#cf2e2e',
'vivid-orange' => '#ff6900',
'vivid-cyan-blue' => '#0693e3',
'vivid-green-cyan' => '#00d084',
'vivid-purple' => '#9b51e0',
'luminous-vivid-amber' => '#fcb900',
'luminous-vivid-orange' => '#ff6900',
'light-green-cyan' => '#7bdcb5',
'pale-pink' => '#f78da7',
'pale-cyan-blue' => '#8ed1fc',
];
return $colors[$slug] ?? '#0073aa';
}
/**
* Get font size from WordPress font size slug
*/
private function getFontSizeFromSlug($slug)
{
static $fontSizeMap = null;
if ($fontSizeMap === null) {
$fontSizeMap = [];
// Try wp_get_global_settings (WordPress 5.9+)
if (function_exists('wp_get_global_settings')) {
$settings = wp_get_global_settings();
if (!empty($settings['typography']['fontSizes'])) {
foreach ($settings['typography']['fontSizes'] as $size) {
if (isset($size['slug']) && isset($size['size'])) {
$fontSizeMap[$size['slug']] = $size['size'];
}
}
}
}
// Try WP_Theme_JSON for block themes
if (empty($fontSizeMap) && class_exists('WP_Theme_JSON_Resolver')) {
$theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
if ($theme_json) {
$settings = $theme_json->get_settings();
if (!empty($settings['typography']['fontSizes'])) {
foreach ($settings['typography']['fontSizes'] as $size) {
if (isset($size['slug']) && isset($size['size'])) {
$fontSizeMap[$size['slug']] = $size['size'];
}
}
}
}
}
// Fallback presets
if (empty($fontSizeMap)) {
$fontSizeMap = [
'small' => '14px',
'medium' => '18px',
'large' => '20px',
'x-large' => '28px',
'extra-small' => '12px',
'extra-large' => '32px',
'huge' => '42px'
];
}
}
return $fontSizeMap[$slug] ?? null;
}
/**
* Get font family from WordPress font family slug or return as-is
*/
private function getFontFamilyFromSlug($fontFamily)
{
// If it looks like a font stack (contains comma), return as-is
if (strpos($fontFamily, ',') !== false) {
return $fontFamily;
}
static $fontFamilyMap = null;
if ($fontFamilyMap === null) {
$fontFamilyMap = [];
// Try wp_get_global_settings (WordPress 5.9+)
if (function_exists('wp_get_global_settings')) {
$settings = wp_get_global_settings();
if (!empty($settings['typography']['fontFamilies'])) {
foreach ($settings['typography']['fontFamilies'] as $family) {
if (isset($family['slug']) && isset($family['fontFamily'])) {
$fontFamilyMap[$family['slug']] = $family['fontFamily'];
}
}
}
}
// Try WP_Theme_JSON for block themes
if (empty($fontFamilyMap) && class_exists('WP_Theme_JSON_Resolver')) {
$theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
if ($theme_json) {
$settings = $theme_json->get_settings();
if (!empty($settings['typography']['fontFamilies'])) {
foreach ($settings['typography']['fontFamilies'] as $family) {
if (isset($family['slug']) && isset($family['fontFamily'])) {
$fontFamilyMap[$family['slug']] = $family['fontFamily'];
}
}
}
}
}
// Common font family presets
$fontFamilyMap = array_merge([
'system-ui' => '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
'system' => '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
'arial' => 'Arial, Helvetica, sans-serif',
'helvetica' => '"Helvetica Neue", Helvetica, Arial, sans-serif',
'times' => '"Times New Roman", Times, serif',
'times-new-roman' => '"Times New Roman", Times, serif',
'georgia' => 'Georgia, serif',
'courier' => '"Courier New", Courier, monospace',
'courier-new' => '"Courier New", Courier, monospace',
'verdana' => 'Verdana, Geneva, sans-serif',
'tahoma' => 'Tahoma, Geneva, sans-serif',
'trebuchet' => '"Trebuchet MS", Helvetica, sans-serif',
'trebuchet-ms' => '"Trebuchet MS", Helvetica, sans-serif',
'palatino' => '"Palatino Linotype", "Book Antiqua", Palatino, serif',
'garamond' => 'Garamond, serif',
'impact' => 'Impact, Charcoal, sans-serif',
'comic-sans' => '"Comic Sans MS", cursive, sans-serif',
'comic-sans-ms' => '"Comic Sans MS", cursive, sans-serif',
'monospace' => 'Monaco, "Lucida Console", Courier, monospace'
], $fontFamilyMap);
}
// Return mapped font family or original value
return $fontFamilyMap[$fontFamily] ?? $fontFamily;
}
/**
* Resolve Gutenberg font-family values to email-safe values.
*/
private function resolveFontFamilyValue($fontFamilyValue)
{
$value = trim((string)$fontFamilyValue);
if ($value === '') {
return '';
}
if (strpos($value, 'var:preset|font-family|') === 0) {
return $this->getFontFamilyFromSlug(substr($value, strlen('var:preset|font-family|')));
}
if (preg_match('/^var\\(--wp--preset--font-family--([a-z0-9-]+)\\)$/i', $value, $matches)) {
return $this->getFontFamilyFromSlug($matches[1]);
}
if (strpos($value, 'var(') === 0) {
return $value;
}
return $this->getFontFamilyFromSlug($value);
}
/**
* Debug helper: Log all theme colors (for development only)
* Uncomment the call in getColorFromSlug() to use
*/
private function debugThemeColors()
{
static $logged = false;
if ($logged) return;
error_log('=== THEME COLORS DEBUG ===');
// Check wp_get_global_settings
if (function_exists('wp_get_global_settings')) {
$settings = wp_get_global_settings();
error_log('Global Settings Colors: ' . print_r($settings['color'] ?? 'none', true));
}
// Check WP_Theme_JSON_Resolver
if (class_exists('WP_Theme_JSON_Resolver')) {
$theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
if ($theme_json) {
$settings = $theme_json->get_settings();
error_log('Theme JSON Colors: ' . print_r($settings['color']['palette'] ?? 'none', true));
}
}
$logged = true;
}
/**
* Wrap content in email-safe table structure
*/
private function wrapInTable($content, $atts = [])
{
if (empty(trim($content))) {
return '';
}
$atts = (array)$atts;
$align = Arr::get($atts, 'align', '');
$tableClass = 'la-default';
if ($align) {
$tableClass = 'la-' . esc_attr($align);
}
$tableAlign = Arr::get($atts, 'fcrmTableAlign', '');
if (!$tableAlign && in_array($align, ['left', 'center', 'right'], true)) {
$tableAlign = $this->getDirectionalTextAlign($align);
}
$tableAlignMarkup = $tableAlign ? ' align="' . esc_attr($tableAlign) . '"' : '';
$tableWidth = trim((string)Arr::get($atts, 'fcrmTableWidth', '100%'));
$tableWidthMarkup = '';
if ($tableWidth !== '' && strtolower($tableWidth) !== 'auto') {
$tableWidthMarkup = ' width="' . esc_attr($tableWidth) . '"';
}
$tdClass = 'la-column';
if (!empty($atts['is_root'])) {
$tdClass = 'la-root-column';
}
$tdId = Arr::get($atts, 'td_id', '');
return <<
{$content}
HTML;
}
private function resolveFontSizeValue($slugOrSize)
{
$value = trim((string)$slugOrSize);
if ($value === '') {
return '';
}
// If a WP preset CSS var is passed, extract its slug for proper resolution.
if (preg_match('/^var\\(--wp--preset--font-size--([a-z0-9-]+)\\)$/i', $value, $matches)) {
$value = $matches[1];
} elseif (strpos($value, 'var(') === 0) {
// Keep unknown CSS var expressions untouched.
return $value;
}
// Already an explicit CSS size.
if (preg_match('/^-?\d+(\.\d+)?(px|em|rem|%|pt|vh|vw)$/i', $value)) {
return $value;
}
// Custom defaults in block editor helper (fc-small, fc-medium, etc).
$fontPresets = BlockEditorHelper::getDefaultPreset('font-size');
foreach ((array)$fontPresets as $preset) {
if (!is_array($preset)) {
continue;
}
if (Arr::get($preset, 'slug') === $value && Arr::get($preset, 'size')) {
return Arr::get($preset, 'size');
}
}
// Theme/WordPress presets (small, medium, large, x-large, etc).
if (class_exists('\FluentCrm\App\Services\Helper')) {
$themeFontSizes = Helper::getThemeFontSizes();
foreach ((array)$themeFontSizes as $preset) {
if (!is_array($preset)) {
continue;
}
if (Arr::get($preset, 'slug') !== $value || !Arr::get($preset, 'size')) {
continue;
}
$size = Arr::get($preset, 'size');
if (is_numeric($size)) {
return $size . 'px';
}
return (string)$size;
}
}
// Fallback to WP CSS variable naming.
return 'var(--wp--preset--font-size--' . $value . ')';
}
/**
* Generate complete email HTML with wrapper
*/
public function generateEmailHtml($content, $title = '')
{
$parsedContent = $this->parse($content);
return <<
{$title}
{$parsedContent}
HTML;
}
}
// Usage Example:
/*
$parser = new GutenbergEmailParser();
// Option 1: Parse blocks only (returns HTML fragment)
$post_content = get_post_field('post_content', $post_id);
$emailHtml = $parser->parse($post_content);
// Option 2: Generate complete email HTML with wrapper
$completeEmail = $parser->generateEmailHtml($post_content, 'Email Title');
// Send email
wp_mail($to, $subject, $completeEmail, ['Content-Type: text/html; charset=UTF-8']);
*/