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("

{$linkText}

", $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 in

tags 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('/]*>(.*?)<\/h[1-6]>/s', $content, $matches)) { $innerContent = $matches[1]; } else { $innerContent = $content; } // find the existing classnames from $content and preserve them in the new heading tag $className = ''; if (preg_match('/]*class=["\']([^"\']*)["\'][^>]*>/s', $content, $matches)) { $className = $matches[1]; } $elemId = $attrs['elem_id'] ?? ''; return $this->wrapInTable("{$innerContent}", $attrs); } /** * Render image block */ private function renderImage($attrs, $innerHTML = '') { // get the content between
tags if exists, as sometimes image block can have that wrapper in innerHTML if (preg_match('/]*>(.*?)<\/figure>/s', $innerHTML, $matches)) { $innerHTML = $matches[1]; } $id = $attrs['elem_id'] ?? ''; // add the id to the ]*>/s', $innerHTML, $matches)) { $imgTag = $matches[0]; // add id attribute to img tag if (strpos($imgTag, 'id=') === false) { $newImgTag = str_replace('' . $innerHTML . ''; return $this->wrapInTable($html, $attrs); } /** * Render list block */ private function renderList($content, $innerBlocks, $attrs) { $ordered = $attrs['ordered'] ?? false; $tag = $ordered ? 'ol' : 'ul'; $id = $attrs['elem_id'] ?? ''; $classes = ['fc_list_item']; if ($attrsClass = trim((string)Arr::get($attrs, 'className', ''))) { $classes = array_merge($classes, preg_split('/\s+/', $attrsClass)); } if (preg_match('/<' . $tag . '[^>]*class=["\']([^"\']+)["\']/i', $content, $matches)) { $classes = array_merge($classes, preg_split('/\s+/', trim((string)$matches[1]))); } $classes = array_filter(array_unique($classes)); $classAttr = implode(' ', $classes); $fontSizeValue = ''; if ($fontSize = Arr::get($attrs, 'fontSize')) { $fontSizeValue = $this->resolveFontSizeValue($fontSize); } elseif (preg_match('/has-([a-z0-9-]+)-font-size/i', $classAttr, $matches)) { $fontSizeValue = $this->resolveFontSizeValue($matches[1]); } $lineHeightValue = Arr::get($attrs, 'style.typography.lineHeight', ''); if (is_string($lineHeightValue) && strpos($lineHeightValue, 'var:') === 0) { $lineHeightValue = $this->transformToCssVar($lineHeightValue); } if (!is_string($lineHeightValue)) { $lineHeightValue = ''; } $listItems = ''; // If we have innerBlocks, render them if (!empty($innerBlocks)) { $listItems = ''; foreach ($innerBlocks as $block) { if ($block['blockName'] === 'core/list-item') { $listItems .= $this->renderListItem($block['innerHTML'], $block['attrs'] ?? []); } } } if (!$listItems) { return ''; } $inlineStyle = ''; if ($fontSizeValue) { $inlineStyle .= 'font-size:' . $fontSizeValue . ';'; if ($id) { $this->childCss .= '#' . $id . ' li, #' . $id . ' li p { font-size: ' . $fontSizeValue . '; } '; } } if ($lineHeightValue) { $inlineStyle .= 'line-height:' . $lineHeightValue . ';'; if ($id) { $this->childCss .= '#' . $id . ' li, #' . $id . ' li p { line-height: ' . $lineHeightValue . '; } '; } } $styleAttr = $inlineStyle ? " style=\"{$inlineStyle}\"" : ''; return $this->wrapInTable("<{$tag} class='{$classAttr}' id='$id'{$styleAttr}>{$listItems}", $attrs); } /** * Render list item */ private function renderListItem($content, $attrs) { if (!$this->checkBlockConditionVisibility($attrs)) { return ''; } $fontSizeValue = ''; $fontFamilyValue = ''; // Explicit typography style on list item takes highest priority. $styleFontSize = Arr::get($attrs, 'style.typography.fontSize', ''); if (is_string($styleFontSize) && $styleFontSize !== '') { if (strpos($styleFontSize, 'var:') === 0) { $styleFontSize = $this->transformToCssVar($styleFontSize); } $fontSizeValue = $this->resolveFontSizeValue($styleFontSize); } // Preset slug from attrs (e.g. fc-small). if (!$fontSizeValue && ($fontSize = Arr::get($attrs, 'fontSize'))) { $fontSizeValue = $this->resolveFontSizeValue($fontSize); } // Fallback: detect preset class directly from li markup. if (!$fontSizeValue && is_string($content) && preg_match('/has-([a-z0-9-]+)-font-size/i', $content, $matches)) { $fontSizeValue = $this->resolveFontSizeValue($matches[1]); } // Explicit typography style on list item takes highest priority. $styleFontFamily = Arr::get($attrs, 'style.typography.fontFamily', ''); if (is_string($styleFontFamily) && $styleFontFamily !== '') { $fontFamilyValue = $this->resolveFontFamilyValue($styleFontFamily); } // Preset slug or raw stack from attrs. if (!$fontFamilyValue && ($fontFamily = Arr::get($attrs, 'fontFamily'))) { $fontFamilyValue = $this->resolveFontFamilyValue($fontFamily); } // Fallback: detect font-family preset class from li markup. if (!$fontFamilyValue && is_string($content) && preg_match('/has-([a-z0-9-]+)-font-family/i', $content, $matches)) { $fontFamilyValue = $this->resolveFontFamilyValue($matches[1]); } if (!$fontSizeValue && !$fontFamilyValue) { return $content; } // Ensure list item typography is inline for email clients. $content = preg_replace_callback('/]*)>/i', function ($matches) use ($fontSizeValue, $fontFamilyValue) { $attrs = $matches[1]; $styleParts = []; if ($fontSizeValue) { $styleParts[] = 'font-size:' . $fontSizeValue; } if ($fontFamilyValue) { $styleParts[] = 'font-family:' . $fontFamilyValue; } $appendedStyles = implode(';', $styleParts); if (preg_match('/\sstyle=(["\'])(.*?)\1/i', $attrs, $styleMatch)) { $existingStyle = rtrim(trim($styleMatch[2]), ';'); $updatedStyle = $existingStyle . ';' . $appendedStyles; return str_replace($styleMatch[0], ' style="' . esc_attr($updatedStyle) . '"', $matches[0]); } return ''; }, $content, 1); return $content; } /** * Render the core RSS block with email-safe markup. * * @param array $attrs Block attributes. * @return string */ private function renderRss($attrs) { $feedUrl = esc_url_raw((string)Arr::get($attrs, 'feedURL', '')); if (!$feedUrl || !$this->isSafeRssFeedUrl($feedUrl)) { return ''; } $rssCacheKey = md5(wp_json_encode([ 'feed_url' => $feedUrl, 'items_to_show' => (int)Arr::get($attrs, 'itemsToShow', 5), 'display_date' => !empty($attrs['displayDate']), 'display_author' => !empty($attrs['displayAuthor']), 'display_excerpt' => !empty($attrs['displayExcerpt']), 'excerpt_length' => (int)Arr::get($attrs, 'excerptLength', 55), 'open_new_tab' => !empty($attrs['openInNewTab']), 'rel' => (string)Arr::get($attrs, 'rel', '') ])); if (isset(self::$rssRenderCache[$rssCacheKey])) { return $this->wrapRssHtmlWithCurrentBlock(self::$rssRenderCache[$rssCacheKey], $attrs); } if (!function_exists('fetch_feed') && defined('ABSPATH') && defined('WPINC')) { require_once ABSPATH . WPINC . '/feed.php'; } if (!function_exists('fetch_feed')) { return ''; } $rssRequestArgsFilter = function ($requestArgs, $url) use ($feedUrl) { if ($url === $feedUrl) { $requestArgs['timeout'] = 5; $requestArgs['redirection'] = 3; $requestArgs['reject_unsafe_urls'] = true; } return $requestArgs; }; add_filter('http_request_args', $rssRequestArgsFilter, 10, 2); $rss = fetch_feed($feedUrl); remove_filter('http_request_args', $rssRequestArgsFilter, 10); if (is_wp_error($rss) || !$rss || !method_exists($rss, 'get_item_quantity')) { return ''; } $itemsToShow = max(1, min(20, (int)Arr::get($attrs, 'itemsToShow', 5))); $quantity = $rss->get_item_quantity($itemsToShow); if (!$quantity) { return ''; } $items = $rss->get_items(0, $quantity); if (!$items) { return ''; } $listItems = ''; $displayDate = !empty($attrs['displayDate']); $displayAuthor = !empty($attrs['displayAuthor']); $displayExcerpt = !empty($attrs['displayExcerpt']); $excerptLength = max(1, (int)Arr::get($attrs, 'excerptLength', 55)); $openInNewTab = !empty($attrs['openInNewTab']); $rel = trim((string)Arr::get($attrs, 'rel', '')); $linkAttrs = ''; if ($openInNewTab) { $linkAttrs .= ' target="_blank"'; } if ($rel !== '') { $linkAttrs .= ' rel="' . esc_attr($rel) . '"'; } foreach ($items as $item) { $title = trim(wp_strip_all_tags(html_entity_decode((string)$item->get_title(), ENT_QUOTES, get_option('blog_charset')))); if ($title === '') { $title = __('(no title)', 'fluent-crm'); } $link = esc_url((string)$item->get_link()); $titleHtml = $link ? '' . esc_html($title) . '' : esc_html($title); $metaHtml = ''; if ($displayDate) { $timestamp = $item->get_date('U'); if ($timestamp) { $gmtOffset = get_option('gmt_offset'); $timestamp += (int)((float)$gmtOffset * HOUR_IN_SECONDS); $metaHtml .= '' . esc_html(date_i18n(get_option('date_format'), $timestamp)) . ''; } } if ($displayAuthor) { $author = $item->get_author(); if (is_object($author) && method_exists($author, 'get_name')) { $authorName = trim(wp_strip_all_tags((string)$author->get_name())); if ($authorName !== '') { $metaHtml .= '' . sprintf( /* translators: %s: author name. */ esc_html__('by %s', 'fluent-crm'), esc_html($authorName) ) . ''; } } } $excerptHtml = ''; if ($displayExcerpt) { $description = html_entity_decode((string)$item->get_description(), ENT_QUOTES, get_option('blog_charset')); $description = trim(wp_strip_all_tags($description)); if ($description !== '') { $excerptHtml = '
' . esc_html(wp_trim_words($description, $excerptLength, ' [...]')) . '
'; } } $listItems .= '
' . '
' . $titleHtml . '
' . $metaHtml . $excerptHtml . '
'; } if (!$listItems) { return ''; } self::$rssRenderCache[$rssCacheKey] = $listItems; return $this->wrapRssHtmlWithCurrentBlock($listItems, $attrs); } /** * Validate RSS feed URLs before the server fetches remote content. * * @param string $url Feed URL. * @return bool */ private function isSafeRssFeedUrl($url) { $parsed = wp_parse_url($url); if (!$parsed || empty($parsed['host'])) { return false; } $scheme = strtolower(isset($parsed['scheme']) ? $parsed['scheme'] : ''); if (!in_array($scheme, ['http', 'https'], true)) { return false; } $host = trim($parsed['host'], '[]'); if (filter_var($host, FILTER_VALIDATE_IP)) { $ip = $host; } else { $ip = gethostbyname($host); if ($ip === $host && !filter_var($ip, FILTER_VALIDATE_IP)) { return false; } } $isSafe = (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); /** * Allow site owners to enforce custom RSS feed source policies. * * @param bool $isSafe Whether the feed URL resolves to a public IP. * @param string $url Feed URL. * @param string $host Parsed URL host. * @param string $ip Resolved host IP. */ return (bool)apply_filters('fluent_crm/rss_block_is_safe_feed_url', $isSafe, $url, $host, $ip); } /** * Wrap cached RSS list items with the current block id and table attributes. * * @param string $listItems Rendered RSS item markup. * @param array $attrs Block attributes. * @return string */ private function wrapRssHtmlWithCurrentBlock($listItems, $attrs) { $elementId = esc_attr(Arr::get($attrs, 'elem_id', '')); $html = '
' . $listItems . '
'; return $this->wrapInTable($html, $attrs); } /** * Render quote block */ private function renderQuote($content, $innerBlocks, $attrs) { $borderColor = BlockEditorHelper::getBorderColor($attrs, '#4f46e5'); $quoteSide = fluentcrm_is_rtl() ? 'right' : 'left'; $styles = "border-{$quoteSide}: 4px solid " . $borderColor . '; padding-' . $quoteSide . ': 20px;'; if (!isset($attrs['style']['spacing']['margin']['bottom'])) { $styles .= ' margin-bottom: 20px;'; } $html = ''; if ($innerBlocks) { $html = $this->renderBlocks($innerBlocks, true); } if (!$html) { return ''; } // get content if exists in $content if (preg_match('/]*>(.*?)<\/cite>/s', $content, $matches)) { $cite = '
' . $matches[1] . '
'; $html .= $cite; } $id = $attrs['elem_id'] ?? ''; return $this->wrapInTable("
{$html}
", $attrs); } /** * Render buttons container */ private function renderButtons($innerBlocks, $attrs) { $layout = $attrs['layout'] ?? []; $justifyContent = $layout['justifyContent'] ?? 'left'; $orientation = strtolower((string)($layout['orientation'] ?? 'horizontal')); $isVertical = ($orientation === 'vertical'); $alignMap = [ 'left' => 'left', 'center' => 'center', 'right' => 'right', 'space-between' => 'start', // For email, align to start in the active direction 'space-around' => 'center', // For email, we'll center and let spacing ]; $textAlign = $this->getDirectionalTextAlign($alignMap[$justifyContent] ?? 'left'); $tableStyles = "width: 100%; border-collapse: collapse; text-align: {$textAlign};"; $elementId = $attrs['elem_id'] ?? ''; unset($attrs['elem_id']); $innerHtml = << HTML; $buttonsHtml = ''; foreach ($innerBlocks as $button) { if ($button['blockName'] === 'core/button') { $buttonElementId = uniqid('block-', false); $button['attrs']['elem_id'] = $buttonElementId; $bttonHtml = $this->renderButton($button['innerHTML'], $button['attrs'] ?? []); if (!$bttonHtml) { continue; } if ($isVertical) { $buttonsHtml .= '
' . $bttonHtml . '
'; } else { $buttonsHtml .= $bttonHtml; } $this->collectInlineStyles($buttonElementId, $button['attrs'], 'core/button'); } } if (!$buttonsHtml) { return ''; } $innerHtml .= $buttonsHtml; $innerHtml .= ""; return $this->wrapInTable($innerHtml, $attrs); } /** * Render button block */ private function renderButton($content, $attrs) { if (!$this->checkBlockConditionVisibility($attrs)) { return ''; } // Extract URL and text from content $url = '#'; $text = ''; if (!empty($content)) { // Try to extract from anchor tag if (preg_match('/]*href=["\']([^"\']*)["\'][^>]*>(.*?)<\/a>/s', $content, $matches)) { $url = $matches[1]; $rawText = $matches[2]; // Remove all HTML tags but keep the text $text = trim(preg_replace('/<[^>]*>/', '', $rawText)); } } // Fallback to attrs if extraction failed if ($url === '#' && !empty($attrs['url'])) { $url = $attrs['url']; } if (!empty($attrs['text'])) { $text = $attrs['text']; } if (!$text) { return ''; } $elementId = $attrs['elem_id'] ?? ''; $styles = ''; if ($width = Arr::get($attrs, 'width')) { $styles .= "min-width: {$width}%;"; } $class = 'fc_button'; $className = (string)Arr::get($attrs, 'className', ''); if ($className) { $class .= ' ' . $className; } return "escapeButtonUrl($url) . "\">" . esc_html($text) . ""; } /** * Escape button URLs without stripping smartcodes before the parser phase. * * Campaign smartcodes are parsed after Gutenberg blocks are rendered, so running * esc_url() on a smartcode URL here removes the curly braces and makes the later * parser miss it. Keep only smartcode-bearing safe-protocol URLs intact with * esc_attr(); static URLs and unsafe protocols still go through esc_url(). */ private function escapeButtonUrl($url) { $url = (string)$url; if (preg_match('/^http:\/\/(\{\{[^{}\r\n]+}}|##[^#\r\n]+##)$/i', trim($url), $matches)) { $url = $matches[1]; } if (!preg_match('/(\{\{[^{}\r\n]+}}|##[^#\r\n]+##)/', $url)) { return esc_url($url); } if (!preg_match('/^([a-z][a-z0-9+.-]*):/i', ltrim($url), $matches)) { return esc_attr($url); } $protocol = strtolower($matches[1]); if (in_array($protocol, ['http', 'https', 'mailto', 'tel'], true)) { return esc_attr($url); } return esc_url($url); } /** * Render legacy Woo single product block. */ private function renderWooProductBlock($block, $attrs, $innerHTML) { if (!defined('WC_PLUGIN_FILE')) { return ''; } $buttonText = !empty($attrs['buttonText']) ? $attrs['buttonText'] : __('Buy Now', 'fluent-crm'); $buttonUrl = '#'; if (!empty($attrs['productId']) && function_exists('wc_get_product')) { $product = wc_get_product((int)$attrs['productId']); if ($product && method_exists($product, 'get_permalink')) { $buttonUrl = $product->get_permalink(); } } if (!empty($innerHTML)) { if (preg_match('/]*href=["\']([^"\']+)["\'][^>]*>/i', $innerHTML, $urlMatch)) { $buttonUrl = $urlMatch[1]; } if (preg_match('/]*>(.*?)<\/a>/is', $innerHTML, $textMatch)) { $parsedText = trim(wp_strip_all_tags($textMatch[1])); if ($parsedText) { $buttonText = $parsedText; } } } $buttonBlock = $this->getFirstButtonBlock($block); $buttonAttrs = Arr::get($buttonBlock, 'attrs', []); $buttonInnerHTML = Arr::get($buttonBlock, 'innerHTML', ''); if (!empty($buttonInnerHTML)) { if (preg_match('/]*href=["\']([^"\']+)["\'][^>]*>/i', $buttonInnerHTML, $urlMatch)) { $buttonUrl = $urlMatch[1]; } if (preg_match('/]*>(.*?)<\/a>/is', $buttonInnerHTML, $textMatch)) { $parsedText = trim(wp_strip_all_tags($textMatch[1])); if ($parsedText) { $buttonText = $parsedText; } } } if (!empty($buttonAttrs['url'])) { $buttonUrl = $buttonAttrs['url']; } if (!empty($buttonAttrs['text'])) { $buttonText = $buttonAttrs['text']; } $buttonElementId = uniqid('block-', false); $buttonAttrs['elem_id'] = $buttonElementId; $buttonHtml = $this->renderButton( '' . esc_html($buttonText) . '', $buttonAttrs ); if (!$buttonHtml) { $buttonHtml = '' . esc_html($buttonText) . ''; } else { $this->collectInlineStyles($buttonElementId, $buttonAttrs, 'core/button'); } $html = WooProduct::renderProduct($buttonHtml, [ 'blockName' => 'fluentcrm/woo-product', 'attrs' => $attrs ]); if (!$html) { return ''; } $attrs['td_id'] = $attrs['elem_id'] ?? ''; return $this->wrapInTable($html, $attrs); } /** * Find the first nested Gutenberg button block inside product blocks. */ private function getFirstButtonBlock($block) { $innerBlocks = Arr::get($block, 'innerBlocks', []); foreach ($innerBlocks as $innerBlock) { if (Arr::get($innerBlock, 'blockName') === 'core/button') { return $innerBlock; } $buttonBlock = $this->getFirstButtonBlock($innerBlock); if ($buttonBlock) { return $buttonBlock; } } return []; } /** * Render FluentCart single product block. */ private function renderCartProductBlock($block, $attrs, $innerHTML) { if (!defined('FLUENTCART_VERSION')) { return ''; } $buttonText = !empty($attrs['buttonText']) ? $attrs['buttonText'] : __('Buy Now', 'fluent-crm'); $buttonUrl = '#'; if (!empty($attrs['productId'])) { $productPermalink = get_permalink((int)$attrs['productId']); if ($productPermalink) { $buttonUrl = $productPermalink; } } if (!empty($innerHTML)) { if (preg_match('/]*href=["\']([^"\']+)["\'][^>]*>/i', $innerHTML, $urlMatch)) { $buttonUrl = $urlMatch[1]; } if (preg_match('/]*>(.*?)<\/a>/is', $innerHTML, $textMatch)) { $parsedText = trim(wp_strip_all_tags($textMatch[1])); if ($parsedText) { $buttonText = $parsedText; } } } $buttonAttrs = $this->getFirstButtonBlockAttrs($block); $buttonElementId = uniqid('block-', false); $buttonAttrs['elem_id'] = $buttonElementId; $buttonHtml = $this->renderButton( '' . esc_html($buttonText) . '', $buttonAttrs ); if (!$buttonHtml) { $buttonHtml = '' . esc_html($buttonText) . ''; } else { $this->collectInlineStyles($buttonElementId, $buttonAttrs, 'core/button'); } $html = CartProduct::renderProduct($buttonHtml, [ 'blockName' => 'fluent-crm/cart-product', 'attrs' => $attrs ]); if (!$html) { return ''; } $attrs['td_id'] = $attrs['elem_id'] ?? ''; return $this->wrapInTable($html, $attrs); } /** * Render legacy latest posts block from FluentCampaign (if available). */ private function renderLatestPostsBlock($block, $attrs) { if (!class_exists('\FluentCampaign\App\Services\PostParser\LatestPost')) { return ''; } try { $html = \FluentCampaign\App\Services\PostParser\LatestPost::renderPosts('', [ 'blockName' => 'fluent-crm/latest-posts', 'attrs' => $attrs, 'innerHTML' => isset($block['innerHTML']) ? $block['innerHTML'] : '' ]); $attrs['elem_id'] = $attrs['elem_id'] ?? ''; return $this->wrapInTable($html, $attrs); } catch (\Throwable $e) { return ''; } } /** * Render product listing blocks via dedicated renderer. */ private function renderProductsBlock($block, $attrs, $blockName) { $html = WooProducts::renderProducts('', [ 'blockName' => $blockName, 'attrs' => $attrs, 'innerHTML' => isset($block['innerHTML']) ? $block['innerHTML'] : '' ]); if (!$html) { return ''; } $attrs['td_id'] = $attrs['elem_id'] ?? ''; return $this->wrapInTable($html, $attrs); } private function renderCartProductsBlock($block, $attrs) { $html = CartProducts::renderProducts('', [ 'blockName' => 'fluent-crm/cart-products', 'attrs' => $attrs, 'innerHTML' => isset($block['innerHTML']) ? $block['innerHTML'] : '' ]); if (!$html) { return ''; } $attrs['td_id'] = $attrs['elem_id'] ?? ''; return $this->wrapInTable($html, $attrs); } /** * Render conditional group block based on subscriber tags. */ private function renderConditionalGroupBlock($innerBlocks, $attrs, $innerHTML) { $content = ''; if (!empty($innerBlocks)) { $content = $this->renderBlocks($innerBlocks, true); } elseif (!empty($innerHTML)) { $content = $innerHTML; } if (!$content) { return ''; } $content = $this->wrapInTable($content, $attrs); $subscriber = BlockParserHelper::getSubscriber(); if (!$subscriber) { $subscriber = apply_filters('fluent_crm/get_current_block_condition_subscriber', $subscriber); } // Keep content visible in preview/test contexts when subscriber is unknown. if (!$subscriber) { return $content; } $tagIds = isset($attrs['tag_ids']) && is_array($attrs['tag_ids']) ? $attrs['tag_ids'] : []; if (!$tagIds) { return ''; } $checkType = $this->normalizeConditionalCheckType(isset($attrs['condition_type']) ? $attrs['condition_type'] : 'show_if_tag_exist'); $tagMatched = method_exists($subscriber, 'hasAnyTagId') ? $subscriber->hasAnyTagId($tagIds) : false; if ($checkType === 'show_if_tag_exist') { return $tagMatched ? $content : ''; } if ($checkType === 'show_if_tag_not_exist') { return $tagMatched ? '' : $content; } return ''; } /** * Render a synced pattern (core/block) by looking up its content from fc_meta * and recursively rendering the contained blocks. */ private static $syncedPatternCache = []; private function renderSyncedPattern($attrs, $nested = false) { $ref = isset($attrs['ref']) ? (int) $attrs['ref'] : 0; if (!$ref) { return ''; } if (!isset(self::$syncedPatternCache[$ref])) { $pattern = \FluentCrm\App\Models\Meta::where('object_type', 'email_pattern') ->where('id', $ref) ->first(); self::$syncedPatternCache[$ref] = ($pattern && !empty($pattern->value['content'])) ? $pattern->value['content'] : ''; } $content = self::$syncedPatternCache[$ref]; if (!$content) { return ''; } $blocks = parse_blocks($content); if (empty($blocks)) { return ''; } return $this->renderBlocks($blocks, $nested); } private function renderCodeBlock($innerHTML, $attrs) { $code = ''; // get the content between
 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 .= ''; } else { $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 .= ''; $html .= '
'; $html .= $innerContent; $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 .= ''; } 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 .= ''; $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}
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']); */