2963 lines
112 KiB
PHP
2963 lines
112 KiB
PHP
<?php
|
|
|
|
namespace FluentCrm\App\Services;
|
|
/**
|
|
* Gutenberg Block Parser for Email
|
|
* Converts Gutenberg blocks to email-compatible HTML
|
|
*/
|
|
|
|
use FluentCrm\App\Services\BlockRender\BlockEditorHelper;
|
|
use FluentCrm\App\Services\BlockRender\CartProduct;
|
|
use FluentCrm\App\Services\BlockRender\CartProducts;
|
|
use FluentCrm\App\Services\BlockRender\WooProduct;
|
|
use FluentCrm\App\Services\BlockRender\WooProducts;
|
|
use FluentCrm\Framework\Support\Arr;
|
|
|
|
class GutenbergEmailParser
|
|
{
|
|
|
|
private $inlineStyles = [];
|
|
|
|
private $childCss = '';
|
|
|
|
private static $rssRenderCache = [];
|
|
|
|
private $autoPaddedElements = [
|
|
'core/heading',
|
|
'core/paragraph',
|
|
'core/list'
|
|
];
|
|
|
|
/**
|
|
* Parse Gutenberg blocks and convert to email HTML
|
|
*
|
|
* @param string $content The post content with Gutenberg blocks
|
|
* @return string Email-compatible HTML
|
|
*/
|
|
public function parse($content)
|
|
{
|
|
$this->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 = '<style type="text/css">';
|
|
|
|
foreach ($this->inlineStyles as $selector => $styles) {
|
|
$css .= $selector . ' { ';
|
|
foreach ($styles as $property => $value) {
|
|
$css .= $property . ': ' . $value . '; ';
|
|
}
|
|
$css .= '} ';
|
|
}
|
|
|
|
if ($this->childCss) {
|
|
$css .= $this->childCss;
|
|
}
|
|
|
|
$css .= '</style>';
|
|
|
|
return $css;
|
|
}
|
|
|
|
/**
|
|
* Manual block parser (fallback if parse_blocks not available)
|
|
*/
|
|
private function parseBlocksManually($content)
|
|
{
|
|
$blocks = [];
|
|
$pattern = '/<!--\s+wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+(\{.*?\})?\s+(\/)?-->/';
|
|
|
|
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 = '/<!--\s+\/wp:' . preg_quote($blockName, '/') . '\s+-->/';
|
|
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("<p style=\"text-align: center; padding: 20px; background: #f0f0f0;\"><a href=\"{$url}\" style=\"color: #0073aa; text-decoration: underline;\">{$linkText}</a></p>", $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 === '<p></p>') {
|
|
return '';
|
|
}
|
|
|
|
// Extract content if it's wrapped in <p> tags
|
|
if (preg_match('/<p[^>]*>(.*?)<\/p>/s', $content, $matches)) {
|
|
$innerContent = $matches[1];
|
|
} else {
|
|
$innerContent = $content;
|
|
}
|
|
|
|
$elementId = $attrs['elem_id'] ?? '';
|
|
|
|
return $this->wrapInTable("<p id=\"{$elementId}\">{$innerContent}</p>", $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][^>]*>(.*?)<\/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('/<h[1-6][^>]*class=["\']([^"\']*)["\'][^>]*>/s', $content, $matches)) {
|
|
$className = $matches[1];
|
|
}
|
|
|
|
$elemId = $attrs['elem_id'] ?? '';
|
|
|
|
return $this->wrapInTable("<h{$level} class='$className' id='$elemId'>{$innerContent}</h{$level}>", $attrs);
|
|
}
|
|
|
|
/**
|
|
* Render image block
|
|
*/
|
|
private function renderImage($attrs, $innerHTML = '')
|
|
{
|
|
|
|
// get the content between <figure> tags if exists, as sometimes image block can have that wrapper in innerHTML
|
|
if (preg_match('/<figure[^>]*>(.*?)<\/figure>/s', $innerHTML, $matches)) {
|
|
$innerHTML = $matches[1];
|
|
}
|
|
|
|
$id = $attrs['elem_id'] ?? '';
|
|
|
|
// add the id to the <img tag if exists in innerHTML
|
|
if ($id && preg_match('/<img[^>]*>/s', $innerHTML, $matches)) {
|
|
$imgTag = $matches[0];
|
|
// add id attribute to img tag
|
|
if (strpos($imgTag, 'id=') === false) {
|
|
$newImgTag = str_replace('<img', '<img id="' . $id . '"', $imgTag);
|
|
$innerHTML = str_replace($imgTag, $newImgTag, $innerHTML);
|
|
}
|
|
}
|
|
|
|
$align = Arr::get($attrs, 'align', 'left');
|
|
|
|
$class = 'fc_image';
|
|
|
|
$class .= ' fc_align_' . $align;
|
|
|
|
$html = '<div class="' . $class . '">' . $innerHTML . '</div>';
|
|
|
|
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}</{$tag}>", $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('/<li\b([^>]*)>/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 '<li' . $attrs . ' style="' . esc_attr($appendedStyles) . '">';
|
|
}, $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
|
|
? '<a href="' . $link . '"' . $linkAttrs . '>' . esc_html($title) . '</a>'
|
|
: 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 .= '<span class="wp-block-rss__item-publish-date" style="display:block;font-size:13px;color:#6b7280;">' .
|
|
esc_html(date_i18n(get_option('date_format'), $timestamp)) .
|
|
'</span>';
|
|
}
|
|
}
|
|
|
|
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 .= '<span class="wp-block-rss__item-author" style="display:block;font-size:13px;color:#6b7280;">' .
|
|
sprintf(
|
|
/* translators: %s: author name. */
|
|
esc_html__('by %s', 'fluent-crm'),
|
|
esc_html($authorName)
|
|
) .
|
|
'</span>';
|
|
}
|
|
}
|
|
}
|
|
|
|
$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 = '<div class="wp-block-rss__item-excerpt" style="margin-top:6px;">' .
|
|
esc_html(wp_trim_words($description, $excerptLength, ' [...]')) .
|
|
'</div>';
|
|
}
|
|
}
|
|
|
|
$listItems .= '<div class="wp-block-rss__item" style="margin-bottom:12px;">' .
|
|
'<div class="wp-block-rss__item-title">' . $titleHtml . '</div>' .
|
|
$metaHtml .
|
|
$excerptHtml .
|
|
'</div>';
|
|
}
|
|
|
|
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 = '<div id="' . $elementId . '" class="wp-block-rss" style="padding:0;margin:0;">' . $listItems . '</div>';
|
|
|
|
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 <cite> content if exists in $content
|
|
if (preg_match('/<cite[^>]*>(.*?)<\/cite>/s', $content, $matches)) {
|
|
$cite = '<div style="font-style: italic; font-size: 90%;">' . $matches[1] . '</div>';
|
|
$html .= $cite;
|
|
}
|
|
|
|
$id = $attrs['elem_id'] ?? '';
|
|
|
|
return $this->wrapInTable("<div id='$id' style=\"{$styles}\">{$html}</div>", $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
|
|
<table role="presentation" style="$tableStyles" align="$textAlign" class="fc_buttons_wrap" width="100%" cellspacing="0" cellpadding="0" border="0"><td id="$elementId" class="fc_buttons_inner">
|
|
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 .= '<div style="display:block;">' . $bttonHtml . '</div>';
|
|
} else {
|
|
$buttonsHtml .= $bttonHtml;
|
|
}
|
|
|
|
$this->collectInlineStyles($buttonElementId, $button['attrs'], 'core/button');
|
|
|
|
}
|
|
}
|
|
|
|
if (!$buttonsHtml) {
|
|
return '';
|
|
}
|
|
|
|
$innerHtml .= $buttonsHtml;
|
|
|
|
$innerHtml .= "</td></tr></table>";
|
|
|
|
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('/<a[^>]*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 "<a class='" . esc_attr($class) . "' id=\"" . esc_attr($elementId) . "\" style='" . esc_attr($styles) . "' href=\"" . $this->escapeButtonUrl($url) . "\">" . esc_html($text) . "</a>";
|
|
}
|
|
|
|
/**
|
|
* 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('/<a[^>]*href=["\']([^"\']+)["\'][^>]*>/i', $innerHTML, $urlMatch)) {
|
|
$buttonUrl = $urlMatch[1];
|
|
}
|
|
if (preg_match('/<a[^>]*>(.*?)<\/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('/<a[^>]*href=["\']([^"\']+)["\'][^>]*>/i', $buttonInnerHTML, $urlMatch)) {
|
|
$buttonUrl = $urlMatch[1];
|
|
}
|
|
|
|
if (preg_match('/<a[^>]*>(.*?)<\/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(
|
|
'<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>',
|
|
$buttonAttrs
|
|
);
|
|
|
|
if (!$buttonHtml) {
|
|
$buttonHtml = '<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>';
|
|
} 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('/<a[^>]*href=["\']([^"\']+)["\'][^>]*>/i', $innerHTML, $urlMatch)) {
|
|
$buttonUrl = $urlMatch[1];
|
|
}
|
|
if (preg_match('/<a[^>]*>(.*?)<\/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(
|
|
'<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>',
|
|
$buttonAttrs
|
|
);
|
|
|
|
if (!$buttonHtml) {
|
|
$buttonHtml = '<a href="' . esc_url($buttonUrl) . '">' . esc_html($buttonText) . '</a>';
|
|
} 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 <pre> tags if exists, as sometimes code block can have that wrapper in innerHTML
|
|
if (preg_match('/<pre[^>]*>(.*?)<\/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("<pre id=\"{$elementId}\" style=\"font-family: monospace; padding: 15px; overflow-x: auto; text-wrap: auto; width: 100%; border-radius: 4px;\">{$code}</pre>", $attrs);
|
|
|
|
}
|
|
|
|
private function renderPullQuote($innerHTML, $attrs)
|
|
{
|
|
$html = '';
|
|
// get the content between <pre> tags if exists, as sometimes code block can have that wrapper in innerHTML
|
|
if (preg_match('/<blockquote[^>]*>(.*?)<\/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('/<p[^>]*(?:style=["\'][^"\']*font-size\s*:|class=["\'][^"\']*has-[a-z0-9-]+-font-size)/i', $html);
|
|
$needsDefaultTitleFontSize = (!$hasBlockFontSize && !$hasParagraphFontSize);
|
|
|
|
$firstPApplied = false;
|
|
$html = preg_replace_callback('/<p([^>]*)>/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 '<p' . $attrs . '>';
|
|
}
|
|
|
|
$styleParts = [];
|
|
if ($needsDefaultTitleFontSize) {
|
|
$styleParts[] = 'font-size: 25px';
|
|
}
|
|
$styleParts[] = 'margin: 0 0 1em 0';
|
|
$styleParts[] = 'line-height: 1.6';
|
|
|
|
return '<p' . $attrs . ' style="' . implode('; ', $styleParts) . ';">';
|
|
}, $html, 1);
|
|
|
|
if (!$hasBlockFontSize && !preg_match('/<cite[^>]*(?:style=["\'][^"\']*font-size\s*:|class=["\'][^"\']*has-[a-z0-9-]+-font-size)/i', $html)) {
|
|
$html = preg_replace_callback('/<cite([^>]*)>/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 '<cite' . $attrs . '>';
|
|
}
|
|
|
|
return '<cite' . $attrs . ' style="font-size: 16px;">';
|
|
}, $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("<div class='fc_pull_qoute' id=\"{$elementId}\" style=\"$styles\">{$html}</div>", $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 = '<table class="' . $tableClass . '" role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="' . BlockEditorHelper::renderStyles($columnTableStyles) . '"><tr>';
|
|
|
|
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 .= '<td class="fc_column"' . $widthMarkup . ' style="' . BlockEditorHelper::renderStyles($styles) . '">';
|
|
|
|
$column['attrs']['elem_id'] = $elementId;
|
|
|
|
$columnsHtml .= $this->renderColumn($column['innerBlocks'], $column['attrs'], $column['innerHTML']);
|
|
$columnsHtml .= '</td>';
|
|
} else {
|
|
$columnsHtml .= '<td width="' . $columnWidth . '%" style="vertical-align: ' . $verticalAlignment . '; padding: 0 10px;">';
|
|
$columnsHtml .= $this->renderBlock($column, true);
|
|
$columnsHtml .= '</td>';
|
|
}
|
|
|
|
|
|
}
|
|
|
|
$columnsHtml .= '</tr></table>';
|
|
|
|
$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 = '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0; background-image: url(\'' . $url . '\'); background-size: cover; background-position: center; min-height: ' . $minHeightValue . ';">';
|
|
$html .= '<tr>';
|
|
$html .= '<td style="padding: 40px 20px; vertical-align: ' . $vAlignStyle . '; text-align: ' . $textAlign . '; background-color: rgba(0,0,0,' . $opacity . '); color: #ffffff; min-height: ' . $minHeightValue . ';">';
|
|
$html .= $innerContent;
|
|
$html .= '</td>';
|
|
$html .= '</tr>';
|
|
$html .= '</table>';
|
|
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 = "<div id='$escapedId' class='$escapedClass' style='border:none;text-align:center;line-height:1;color:$escapedSeparatorColor;font-size:24px;letter-spacing:10px;'>···</div>";
|
|
return $this->wrapInTable($separator, $attrs);
|
|
}
|
|
|
|
$lineWidth = $isWide ? '100%' : '100px';
|
|
$separator = "<hr id='$escapedId' class='$escapedClass' style='margin-left: auto;margin-right: auto;border:none;height:2px;line-height:2px;font-size:0;background-color:$escapedSeparatorColor;width:$lineWidth;' />";
|
|
|
|
return $this->wrapInTable($separator, $attrs);
|
|
}
|
|
|
|
/**
|
|
* Render spacer block
|
|
*/
|
|
private function renderSpacer($innerHtml, $attrs)
|
|
{
|
|
$id = $attrs['elem_id'] ?? '';
|
|
|
|
return $this->wrapInTable("<div id='$id'> </div>", $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 = "<div class='fc_group_content' style='" . BlockEditorHelper::renderStyles($innerStyles) . "'>{$content}</div>";
|
|
}
|
|
|
|
return $this->wrapInTable("<div id='$id' class='" . esc_attr(implode(' ', $groupClasses)) . "'>{$content}</div>", $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 = "<div class='fc_group_content' style='" . BlockEditorHelper::renderStyles($innerStyles) . "'>{$content}</div>";
|
|
}
|
|
|
|
return $this->wrapInTable("<div id='$id' class='" . esc_attr(implode(' ', $groupClasses)) . "'>{$content}</div>", $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 = '<table id="' . esc_attr($id) . '" class="fc_row" role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="' . BlockEditorHelper::renderStyles($tableStyles) . '"><tr>';
|
|
|
|
$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 .= '<td class="fc_row_item" align="' . esc_attr($cellAlign) . '" style="' . BlockEditorHelper::renderStyles($cellStyles) . '">';
|
|
$rowHtml .= $renderRowChild($innerBlock);
|
|
$rowHtml .= '</td>';
|
|
}
|
|
|
|
$rowHtml .= '</tr></table>';
|
|
} else {
|
|
$outerAlign = $justifyMode === 'center'
|
|
? 'center'
|
|
: ($justifyMode === 'right' ? $this->getDirectionalTextAlign('right') : $this->getDirectionalTextAlign('left'));
|
|
|
|
$innerTableStyles = [
|
|
'width' => 'auto',
|
|
'border-collapse' => 'separate',
|
|
'border-spacing' => '0'
|
|
];
|
|
|
|
$innerHtml = '<table id="' . esc_attr($id) . '" class="fc_row" role="presentation" cellspacing="0" cellpadding="0" border="0" style="' . BlockEditorHelper::renderStyles($innerTableStyles) . '"><tr>';
|
|
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 .= '<td class="fc_row_item" style="' . BlockEditorHelper::renderStyles($cellStyles) . '">';
|
|
$innerHtml .= $renderRowChild($innerBlock);
|
|
$innerHtml .= '</td>';
|
|
}
|
|
$innerHtml .= '</tr></table>';
|
|
|
|
$rowHtml = '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="' . esc_attr($outerAlign) . '">' . $innerHtml . '</td></tr></table>';
|
|
}
|
|
|
|
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\b([^>]*)>(.*?)<\/tbody>/is', function ($tbodyMatches) use ($stripeColor) {
|
|
$rowIndex = 0;
|
|
$tbodyContent = preg_replace_callback('/<tr\b([^>]*)>(.*?)<\/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 '<tr' . $rowMatches[1] . '>' . $rowContent . '</tr>';
|
|
}, $tbodyMatches[2]);
|
|
|
|
return '<tbody' . $tbodyMatches[1] . '>' . $tbodyContent . '</tbody>';
|
|
}, $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[^>]*>(.*?)<\/table>/s', $content, $matches)) {
|
|
$tableContent = $matches[1];
|
|
// Add styles to table cells
|
|
$tableContent = preg_replace_callback('/<td\b([^>]*)>/i', function ($matches) use ($cellStyles) {
|
|
return $this->appendInlineStyleToTag($matches[0], $cellStyles);
|
|
}, $tableContent);
|
|
$tableContent = preg_replace_callback('/<th\b([^>]*)>/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("<table id='$id' class='{$tableClass}' style=\"{$styles}\">{$tableContent}</table>", $attrs);
|
|
}
|
|
|
|
/**
|
|
* Render social links block
|
|
*/
|
|
private function renderSocialLinks($innerBlocks, $attrs, $innerHTML)
|
|
{
|
|
$socialHtml = '<div style="margin: 20px 0; text-align: center;">';
|
|
|
|
$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('/<a[^>]*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('/<li[^>]*class="[^"]*wp-social-link[^"]*"[^>]*>.*?<a[^>]*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 .= '<a href="' . htmlspecialchars($link['url']) . '" style="display: inline-block; margin: 0 5px; text-decoration: none;">';
|
|
$socialHtml .= $iconHtml;
|
|
$socialHtml .= '</a>';
|
|
}
|
|
}
|
|
|
|
$socialHtml .= '</div>';
|
|
|
|
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 '<span style="display: inline-block; width: 44px; height: 44px; background-color: ' . $bgColor . '; border-radius: 50%; text-align: center; padding: 10px; box-sizing: border-box;" title="' . htmlspecialchars($alt) . '">' . $iconContent . '</span>';
|
|
}
|
|
|
|
/**
|
|
* Get social media icon SVG or text representation
|
|
*/
|
|
private function getSocialIconSVG($service)
|
|
{
|
|
// Simple SVG icons as inline data
|
|
$icons = [
|
|
'facebook' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
|
|
'twitter' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
|
|
'linkedin' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>',
|
|
'instagram' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>',
|
|
'github' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>',
|
|
'wordpress' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11zm-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.607-3.582.607zM1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.212 12zm10.785-10.784C6.596 1.215 1.214 6.597 1.214 12s5.382 10.785 10.784 10.785S22.784 17.403 22.784 12 17.402 1.215 11.996 1.215z"/></svg>',
|
|
'amazon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M14.465 11.813c-1.75-1.297-4.056-2.017-6.121-2.017-2.766 0-5.264 1.032-7.153 2.742-.207.183-.371.322-.559.322-.188 0-.316-.128-.316-.316 0-.184.18-.404.484-.672C2.821 9.84 5.831 8.621 9.071 8.621c2.766 0 5.262 1.031 7.151 2.742.211.183.375.322.563.322.184 0 .312-.128.312-.316 0-.184-.176-.404-.484-.672zm3.438 1.227c-.133.027-.27.04-.402.04-.297 0-.559-.09-.805-.274-1.367-.992-3.164-1.547-4.93-1.547-2.465 0-4.742 1.051-6.562 2.871-.188.183-.316.304-.508.304-.188 0-.316-.12-.316-.308 0-.184.176-.402.488-.672 1.98-1.96 4.621-3.039 7.465-3.039 2.027 0 3.926.582 5.504 1.684.238.164.375.402.375.672 0 .238-.188.5-.309.524v-.003zm4.016-3.863c-.863 0-1.637.504-1.875 1.297-.231.77.13 1.617.895 1.895.691.25 1.457-.145 1.703-.836.227-.652-.012-1.391-.637-1.746-.18-.094-.387-.137-.586-.16-.164-.012-.329-.012-.5.039.004.004.004-.012.004-.016v-.012c.238-.348.54-.602.984-.707.43-.097.851-.016 1.262.16.199.082.379.211.578.32-.012.012-.027.027-.043.043-.125.133-.266.273-.398.402-.145.148-.273.297-.437.399-.035.023-.074.046-.113.05-.039.008-.102-.008-.125-.035-.098-.098-.207-.195-.309-.293-.543-.52-1.254-.742-2.055-.742-.012-.012-.012 0-.012.012zM24 17.887c-2.059 1.52-4.121 3.031-6.18 4.551-.078.059-.164.113-.258.16-.586.305-1.199.031-1.199-.559v-8.664c0-.129.047-.266.129-.383.078-.102.172-.195.277-.258 2.078-1.523 4.156-3.043 6.234-4.566.074-.054.156-.109.242-.152.375-.195.797-.094 1.016.258.094.145.117.297.117.457v8.765c0 .129-.039.262-.129.39h-.004z"/></svg>',
|
|
'link' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>'
|
|
];
|
|
|
|
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 <<<HTML
|
|
<table role="presentation" class="$tableClass"$tableWidthMarkup$tableAlignMarkup cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td id="$tdId" class="$tdClass">
|
|
{$content}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
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 <<<HTML
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<title>{$title}</title>
|
|
<style type="text/css">
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
color: #333333;
|
|
background-color: #f4f4f4;
|
|
}
|
|
table {
|
|
border-collapse: collapse;
|
|
}
|
|
img {
|
|
border: 0;
|
|
outline: none;
|
|
text-decoration: none;
|
|
-ms-interpolation-mode: bicubic;
|
|
}
|
|
a {
|
|
color: #0073aa;
|
|
}
|
|
@media only screen and (max-width: 600px) {
|
|
.email-container {
|
|
width: 100% !important;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body style="margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f4f4f4;">
|
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td align="center" style="padding: 20px 0;">
|
|
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: #ffffff;">
|
|
<tr>
|
|
<td style="padding: 40px 30px;">
|
|
{$parsedContent}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
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']);
|
|
*/
|