array( 'id', 'class', 'style', 'title' ),
'em' => array( 'id', 'class', 'style', 'title' ),
'u' => array( 'id', 'class', 'style', 'title' ),
's' => array( 'id', 'class', 'style', 'title' ),
'sup' => array( 'id', 'class', 'style', 'title' ),
'sub' => array( 'id', 'class', 'style', 'title' ),
'a' => array( 'id', 'class', 'style', 'title', 'href', 'target', 'rel' ),
'span' => array( 'id', 'class', 'style', 'title' ),
'img' => array( 'id', 'class', 'style', 'src', 'alt', 'title', 'width', 'height', 'loading', 'decoding' ),
'br' => array(),
);
private array $breakpoints = array();
public function __construct( ?array $breakpoints = null ) {
$this->breakpoints = $breakpoints ?? $this->get_default_breakpoints();
}
private function get_default_breakpoints(): array {
return array(
'mobile' => array( 'direction' => 'max', 'value' => 767 ),
'mobile_extra' => array( 'direction' => 'max', 'value' => 880 ),
'tablet' => array( 'direction' => 'max', 'value' => 1024 ),
'tablet_extra' => array( 'direction' => 'max', 'value' => 1200 ),
'laptop' => array( 'direction' => 'max', 'value' => 1366 ),
'widescreen' => array( 'direction' => 'min', 'value' => 2400 ),
);
}
public function convert( array $element ): array {
$native = $this->convert_with_elementor( $element );
if ( null !== $native ) {
return $native;
}
$styles = $this->collect_styles( $element );
$css_blocks = array_filter( array( $this->render_styles( $styles ), $this->render_legacy_styles( $element ) ) );
$css = implode( "\n", $css_blocks );
$html = $this->render_element( $element );
return array( 'html' => $html, 'css' => $css );
}
/**
* Use Elementor's own frontend renderer and control-aware CSS generator for
* registered legacy elements. This is the only reliable way to preserve the
* wrapper markup and widget selectors expected by Elementor's CSS rules.
* Atomic elements and unsupported widgets continue through the portable
* semantic converter below.
*/
private function convert_with_elementor( array $element ): ?array {
if ( ! class_exists( '\\Elementor\\Plugin' ) || ! class_exists( '\\Elementor\\Core\\Files\\CSS\\Post' ) ) {
return null;
}
$el_type = (string) ( $element['elType'] ?? '' );
$widget_type = (string) ( $element['widgetType'] ?? '' );
if ( str_starts_with( $el_type, 'e-' ) ) { return null; }
$allowed_elements = array( 'container', 'section', 'column' );
$allowed_widgets = array(
'heading', 'text-editor', 'button', 'image', 'icon', 'icon-list', 'image-box', 'icon-box',
'divider', 'spacer', 'video', 'image-gallery', 'image-carousel', 'counter', 'progress',
'testimonial', 'alert', 'rating', 'star-rating', 'social-icons', 'tabs', 'accordion',
'toggle', 'menu-anchor',
);
if ( ! $this->is_native_tree_supported( $element, $allowed_elements, $allowed_widgets ) ) {
return null;
}
try {
$render_element = $this->strip_custom_css_from_element( $element );
$instance = \Elementor\Plugin::$instance->elements_manager->create_element_instance( $render_element );
if ( ! $instance ) { return null; }
$disable_shortcode = static fn() => false;
add_filter( 'elementor/element/should_render_shortcode', $disable_shortcode, PHP_INT_MAX );
try {
ob_start();
$instance->print_element();
$html = (string) ob_get_clean();
} finally {
remove_filter( 'elementor/element/should_render_shortcode', $disable_shortcode, PHP_INT_MAX );
if ( ob_get_level() > 0 && ! isset( $html ) ) { ob_end_clean(); }
}
$css_file = new class( 0 ) extends \Elementor\Core\Files\CSS\Post {
public function get_element_unique_selector( \Elementor\Element_Base $element ) {
return '.elementor-element' . $element->get_unique_selector();
}
public function render_selected_element( \Elementor\Element_Base $element ): string {
$this->render_styles( $element );
return (string) $this->get_stylesheet();
}
};
$css = $css_file->render_selected_element( $instance );
return '' !== trim( $html ) ? array( 'html' => $html, 'css' => $css ) : null;
} catch ( \Throwable $e ) {
return null;
}
}
/**
* Remove AI-managed/raw custom CSS from a cloned tree before Elementor's
* native renderer sees it. The source element is intentionally untouched so
* stored user-authored settings and transaction snapshots remain intact.
*/
private function strip_custom_css_from_element( array $element ): array {
$copy = $element;
if ( isset( $copy['settings'] ) && is_array( $copy['settings'] ) ) {
unset( $copy['settings']['custom_css'] );
}
if ( isset( $copy['styles'] ) && is_array( $copy['styles'] ) ) {
foreach ( $copy['styles'] as $style_key => $style_def ) {
if ( ! is_array( $style_def ) || ! isset( $style_def['variants'] ) || ! is_array( $style_def['variants'] ) ) {
continue;
}
foreach ( $style_def['variants'] as $variant_key => $variant ) {
if ( ! is_array( $variant ) ) {
continue;
}
unset( $variant['custom_css'] );
$style_def['variants'][ $variant_key ] = $variant;
}
$copy['styles'][ $style_key ] = $style_def;
}
}
if ( isset( $copy['elements'] ) && is_array( $copy['elements'] ) ) {
$copy['elements'] = array_map(
fn( $child ) => is_array( $child ) ? $this->strip_custom_css_from_element( $child ) : $child,
$copy['elements']
);
}
return $copy;
}
private function is_native_tree_supported( array $element, array $allowed_elements, array $allowed_widgets ): bool {
$el_type = (string) ( $element['elType'] ?? '' );
$widget_type = (string) ( $element['widgetType'] ?? '' );
if ( ! in_array( $el_type, $allowed_elements, true ) && ( 'widget' !== $el_type || ! in_array( $widget_type, $allowed_widgets, true ) ) ) {
return false;
}
foreach ( (array) ( $element['elements'] ?? array() ) as $child ) {
if ( ! is_array( $child ) || ! $this->is_native_tree_supported( $child, $allowed_elements, $allowed_widgets ) ) { return false; }
}
return true;
}
/**
* Collect style definitions from the selected element and all descendants.
* Elementor stores atomic styles on each individual element rather than on
* the selected root, so rendering only the root styles produces incomplete
* CSS for containers with nested widgets.
*/
private function collect_styles( array $element ): array {
$styles = array();
foreach ( (array) ( $element['styles'] ?? array() ) as $key => $style_def ) {
if ( ! is_array( $style_def ) ) {
continue;
}
$style_id = $style_def['id'] ?? ( is_string( $key ) ? $key : '' );
if ( '' !== $style_id ) {
$styles[ $style_id ] = $style_def;
} else {
$styles[] = $style_def;
}
}
foreach ( (array) ( $element['elements'] ?? array() ) as $child ) {
if ( ! is_array( $child ) ) {
continue;
}
foreach ( $this->collect_styles( $child ) as $key => $style_def ) {
$styles[ $key ] = $style_def;
}
}
return $styles;
}
// ================================================================
// STYLE RENDERING
// ================================================================
private function render_styles( array $styles ): string {
if ( empty( $styles ) ) {
return '';
}
$blocks = array();
foreach ( $styles as $style_def ) {
$css = $this->render_style_definition( $style_def );
if ( '' !== $css ) {
$blocks[] = $css;
}
}
return implode( "\n", $blocks );
}
/**
* Legacy Elementor elements store presentation controls in settings rather
* than atomic style definitions. Convert the useful, active controls into
* rules scoped to the stable element ID class used by the rendered markup.
*/
private function render_legacy_styles( array $element ): string {
$blocks = array();
$id = $this->safe_class( $element['id'] ?? '' );
if ( '' !== $id && ! empty( $element['settings'] ) && is_array( $element['settings'] ) ) {
$settings = $this->flatten_settings( $element['settings'] );
$selector = '.elementor-element-' . $id;
foreach ( array( '' => null, '_tablet' => 'tablet', '_mobile' => 'mobile' ) as $suffix => $breakpoint ) {
$decls = $this->legacy_declarations( $settings, $suffix, false );
if ( $decls ) { $blocks[] = $this->legacy_rule( $selector, $decls, $breakpoint ); }
$hover = $this->legacy_declarations( $settings, $suffix, true );
if ( $hover ) { $blocks[] = $this->legacy_rule( $selector . ':hover', $hover, $breakpoint ); }
}
}
foreach ( (array) ( $element['elements'] ?? array() ) as $child ) {
if ( is_array( $child ) ) {
$child_css = $this->render_legacy_styles( $child );
if ( '' !== $child_css ) { $blocks[] = $child_css; }
}
}
return implode( "\n", array_filter( $blocks ) );
}
private function legacy_rule( string $selector, array $decls, ?string $breakpoint ): string {
$parts = array();
foreach ( $decls as $property => $value ) {
if ( '' !== $value && null !== $value ) { $parts[] = $property . ':' . $value; }
}
if ( ! $parts ) { return ''; }
$rule = $selector . ' { ' . implode( '; ', $parts ) . ' }';
if ( $breakpoint ) {
$media = $this->get_media_query( $breakpoint );
if ( $media ) { $rule = $media . ' { ' . $rule . ' }'; }
}
return $rule;
}
private function legacy_declarations( array $settings, string $suffix, bool $hover ): array {
$out = array();
$prefix = $hover ? 'hover_' : '';
$get = function ( string $key ) use ( $settings, $suffix, $prefix ) {
$infix = preg_replace( '/^([^_]+)/', '$1_hover', $key );
if ( str_starts_with( $key, 'box_shadow_' ) ) { $infix = 'box_shadow_hover_' . substr( $key, strlen( 'box_shadow_' ) ); }
$candidates = $prefix ? array( $infix . $suffix, $key . '_hover' . $suffix, $prefix . $key . $suffix, $key . $suffix . '_hover' ) : array( $key . $suffix );
foreach ( $candidates as $candidate ) {
if ( array_key_exists( $candidate, $settings ) ) { return $settings[ $candidate ]; }
}
return null;
};
$put = function ( string $property, $value ) use ( &$out ) {
if ( null !== $value && '' !== $value && ! is_array( $value ) ) { $out[ $property ] = (string) $value; }
};
if ( ! $hover ) {
$container_type = (string) ( $get( 'container_type' ) ?? ( $settings['container_type'] ?? '' ) );
if ( 'grid' === $container_type ) { $out['display'] = 'grid'; }
if ( 'flex' === $container_type ) { $out['display'] = 'flex'; }
foreach ( array(
'overflow' => 'overflow', 'position' => 'position', 'z_index' => 'z-index',
'align' => 'text-align', 'text_color' => 'color', 'object-fit' => 'object-fit', 'object-position' => 'object-position',
) as $setting => $property ) { $put( $property, $get( $setting ) ); }
if ( 'flex' === $container_type ) {
foreach ( array( 'flex_direction' => 'flex-direction', 'flex_justify_content' => 'justify-content', 'flex_align_items' => 'align-items', 'flex_align_content' => 'align-content', 'flex_wrap' => 'flex-wrap' ) as $setting => $property ) { $put( $property, $get( $setting ) ); }
}
if ( 'grid' === $container_type ) {
foreach ( array( 'grid_auto_flow' => 'grid-auto-flow', 'grid_justify_items' => 'justify-items', 'grid_align_items' => 'align-items', 'grid_justify_content' => 'justify-content', 'grid_align_content' => 'align-content' ) as $setting => $property ) { $put( $property, $get( $setting ) ); }
}
foreach ( array( 'width' => 'width', 'min_height' => 'min-height', 'height' => 'height', 'max_width' => 'max-width' ) as $setting => $property ) {
$value = $this->legacy_size( $get( $setting ) );
if ( '' !== $value ) { $out[ $property ] = $value; }
}
foreach ( array( 'margin' => 'margin', 'padding' => 'padding', 'border_radius' => 'border-radius' ) as $setting => $property ) {
$value = $this->legacy_dimensions( $get( $setting ) );
if ( '' !== $value ) { $out[ $property ] = $value; }
}
$gap = $this->legacy_gap( 'grid' === $container_type ? $get( 'grid_gaps' ) : $get( 'flex_gap' ) );
if ( '' !== $gap ) { $out['gap'] = $gap; }
if ( 'grid' === $container_type ) {
$columns = $this->legacy_size( $get( 'grid_columns_grid' ) );
$rows = $this->legacy_size( $get( 'grid_rows_grid' ) );
if ( $columns ) { $out['grid-template-columns'] = 'repeat(' . preg_replace( '/[^0-9.]/', '', $columns ) . ', 1fr)'; }
if ( $rows ) { $out['grid-template-rows'] = 'repeat(' . preg_replace( '/[^0-9.]/', '', $rows ) . ', 1fr)'; }
}
}
$background_key = 'background';
$background_on = $get( $background_key . '_background' );
if ( $background_on ) {
$color = $get( $background_key . '_color' );
if ( $color ) { $out['background-color'] = (string) $color; }
$image = $this->legacy_media_url( $get( $background_key . '_image' ) );
if ( $image ) { $out['background-image'] = 'url("' . esc_attr( $image ) . '")'; }
foreach ( array( 'position' => 'background-position', 'repeat' => 'background-repeat', 'size' => 'background-size', 'attachment' => 'background-attachment' ) as $setting => $property ) {
$put( $property, $get( $background_key . '_' . $setting ) );
}
}
$border_key = 'border';
$border_on = $get( $border_key . '_border' );
if ( $border_on ) {
$out['border-style'] = (string) $border_on;
$put( 'border-color', $get( $border_key . '_color' ) );
$width = $this->legacy_dimensions( $get( $border_key . '_width' ) );
if ( $width ) { $out['border-width'] = $width; }
}
$radius = $this->legacy_dimensions( $get( 'border_radius' ) );
if ( $radius ) { $out['border-radius'] = $radius; }
$shadow_on = $get( 'box_shadow_box_shadow_type' );
$shadow = $get( 'box_shadow_box_shadow' );
if ( $shadow_on && is_array( $shadow ) ) { $out['box-shadow'] = $this->legacy_shadow( $shadow, $get( 'box_shadow_box_shadow_position' ) ); }
$typography_on = $get( 'typography_typography' );
if ( $suffix && null === $typography_on ) { $typography_on = $settings['typography_typography'] ?? null; }
if ( ! $hover && ( 'custom' === $typography_on || $typography_on ) ) {
foreach ( array( 'font_family' => 'font-family', 'font_weight' => 'font-weight', 'font_style' => 'font-style', 'text_transform' => 'text-transform', 'text_decoration' => 'text-decoration' ) as $setting => $property ) { $put( $property, $get( 'typography_' . $setting ) ); }
foreach ( array( 'font_size' => 'font-size', 'line_height' => 'line-height', 'letter_spacing' => 'letter-spacing', 'word_spacing' => 'word-spacing' ) as $setting => $property ) {
$value = $this->legacy_size( $get( 'typography_' . $setting ) );
if ( $value ) { $out[ $property ] = $value; }
}
}
foreach ( array( 'button_background_color' => 'background-color', 'button_text_color' => 'color', 'title_color' => 'color', 'icon_color' => 'color', 'opacity' => 'opacity' ) as $setting => $property ) {
$value = $get( $setting );
if ( is_array( $value ) && 'opacity' === $setting ) { $value = $value['size'] ?? ''; }
$put( $property, $value );
}
return $out;
}
private function legacy_size( $value ): string {
if ( is_numeric( $value ) ) { return (string) $value; }
if ( ! is_array( $value ) || ! array_key_exists( 'size', $value ) || '' === $value['size'] || null === $value['size'] ) { return ''; }
$unit = (string) ( $value['unit'] ?? 'px' );
return (string) $value['size'] . ( 'custom' === $unit ? '' : $unit );
}
private function legacy_dimensions( $value ): string {
if ( ! is_array( $value ) ) { return ''; }
$unit = (string) ( $value['unit'] ?? 'px' );
$parts = array();
foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
if ( ! array_key_exists( $side, $value ) || '' === $value[ $side ] || null === $value[ $side ] ) { return ''; }
$parts[] = (string) $value[ $side ] . $unit;
}
return implode( ' ', $parts );
}
private function legacy_gap( $value ): string {
if ( ! is_array( $value ) ) { return ''; }
$unit = (string) ( $value['unit'] ?? 'px' );
$row = $value['row'] ?? $value['size'] ?? '';
$col = $value['column'] ?? $value['size'] ?? '';
if ( '' === $row || '' === $col ) { return ''; }
return (string) $row . $unit . ' ' . (string) $col . $unit;
}
private function legacy_shadow( array $value, $position ): string {
$parts = array( $value['horizontal'] ?? 0, $value['vertical'] ?? 0, $value['blur'] ?? 0, $value['spread'] ?? 0 );
$parts = array_map( fn( $part ) => (string) $part . 'px', $parts );
return ( 'inset' === trim( (string) $position ) ? 'inset ' : '' ) . implode( ' ', $parts ) . ' ' . (string) ( $value['color'] ?? 'transparent' );
}
private function legacy_media_url( $value ): string {
if ( is_string( $value ) ) { return $value; }
return is_array( $value ) && is_string( $value['url'] ?? null ) ? $value['url'] : '';
}
private function render_style_definition( array $style_def ): string {
$selector = $this->get_selector( $style_def );
if ( ! $selector ) {
return '';
}
$variants = $style_def['variants'] ?? array();
if ( empty( $variants ) ) {
return '';
}
$blocks = array();
foreach ( $variants as $variant ) {
$props = $variant['props'] ?? array();
$meta = $variant['meta'] ?? array();
$breakpoint = $meta['breakpoint'] ?? null;
$state = $meta['state'] ?? null;
if ( empty( $props ) ) {
continue;
}
$declarations = $this->resolve_props_to_css( $props );
if ( '' === $declarations ) {
continue;
}
$rule_sel = $this->apply_state_to_selector( $selector, $state );
$rule = $rule_sel . ' { ' . $declarations . ' }';
if ( $breakpoint ) {
$media = $this->get_media_query( $breakpoint );
if ( $media ) {
$rule = $media . ' { ' . $rule . ' }';
}
}
$blocks[] = $rule;
}
return implode( "\n", $blocks );
}
private function get_selector( array $style_def ): string {
$type = $style_def['type'] ?? '';
if ( 'class' !== $type ) {
return '';
}
$id = $style_def['cssName'] ?? $style_def['id'] ?? '';
if ( '' === $id ) {
return '';
}
return '.' . $id;
}
private function apply_state_to_selector( string $sel, ?string $state ): string {
if ( null === $state || '' === $state ) {
return $sel;
}
$pseudo = array( 'hover', 'active', 'focus', 'focus-visible', 'checked' );
if ( in_array( $state, $pseudo, true ) ) {
return $sel . ':' . $state;
}
if ( str_starts_with( $state, 'e--' ) ) {
return $sel . '.' . $state;
}
return $sel;
}
private function resolve_props_to_css( array $props ): string {
$decls = array();
foreach ( $props as $prop_name => $prop_value ) {
$css_v = $this->resolve_prop_value( $prop_value );
if ( null === $css_v || '' === $css_v ) {
continue;
}
if ( 'content' === $prop_name && 'normal' === $css_v ) {
continue;
}
$decls[] = $prop_name . ':' . $css_v;
}
return implode( '; ', $decls );
}
private function resolve_prop_value( $prop_value ): ?string {
if ( ! is_array( $prop_value ) || ! isset( $prop_value['$$type'] ) || ! array_key_exists( 'value', $prop_value ) ) {
return is_string( $prop_value ) ? $prop_value : null;
}
if ( ! empty( $prop_value['disabled'] ) ) {
return null;
}
$type = $prop_value['$$type'];
$value = $prop_value['value'];
switch ( $type ) {
case 'string': return (string) $value;
case 'size': return $this->resolve_size( $value );
case 'number': return (string) $value;
case 'color': return $this->resolve_color( $value );
case 'dimensions': return $this->resolve_dimensions( $value );
case 'opacity': return (string) ( (float) $value / 100 );
case 'background': return $this->resolve_background( $value );
case 'box-shadow': return $this->resolve_box_shadow( $value );
case 'flex': return $this->resolve_flex( $value );
case 'stroke': return $this->resolve_stroke( $value );
case 'border-radius': return $this->resolve_border_radius( $value );
case 'border-width': return $this->resolve_border_radius( $value );
case 'gap': return $this->resolve_gap( $value );
case 'classes':
case 'html-v3':
case 'link':
case 'image':
case 'svg-src':
case 'url':
case 'attributes':
return null;
default:
return is_scalar( $value ) ? (string) $value : null;
}
}
// --- Individual type resolvers ---
private function resolve_size( $value ): string {
if ( ! is_array( $value ) ) {
return (string) $value;
}
if ( isset( $value['$$type'] ) ) {
return (string) ( $this->resolve_prop_value( $value ) ?? '' );
}
$size = $value['size'] ?? 0;
$unit = $value['unit'] ?? 'px';
return 'custom' === $unit ? (string) $size : $size . $unit;
}
private function resolve_color( $value ): string {
if ( is_string( $value ) ) {
return $value;
}
if ( is_array( $value ) ) {
$r = (int) ( $value['r'] ?? 0 );
$g = (int) ( $value['g'] ?? 0 );
$b = (int) ( $value['b'] ?? 0 );
$a = isset( $value['a'] ) ? (float) $value['a'] : 1;
if ( $a < 1 ) {
$alpha = rtrim( rtrim( (string) $a, '0' ), '.' ) ?: '0';
return sprintf( 'rgba(%d,%d,%d,%s)', $r, $g, $b, $alpha );
}
return sprintf( '#%02x%02x%02x', $r, $g, $b );
}
return (string) $value;
}
private function resolve_dimensions( $value ): string {
if ( ! is_array( $value ) ) {
return (string) $value;
}
$t = isset( $value['block-start'] ) ? $this->resolve_size( $value['block-start'] ) : '0';
$r = isset( $value['inline-end'] ) ? $this->resolve_size( $value['inline-end'] ) : '0';
$b = isset( $value['block-end'] ) ? $this->resolve_size( $value['block-end'] ) : '0';
$l = isset( $value['inline-start'] ) ? $this->resolve_size( $value['inline-start'] ) : '0';
if ( $t === $r && $r === $b && $b === $l ) {
return $t;
}
if ( $t === $b && $r === $l ) {
return $t . ' ' . $r;
}
return "$t $r $b $l";
}
private function resolve_background( $value ): string {
if ( ! is_array( $value ) ) {
return (string) $value;
}
$parts = array();
if ( isset( $value['color'] ) ) {
$c = $this->resolve_prop_value( $value['color'] );
if ( $c ) { $parts[] = $c; }
}
if ( isset( $value['image']['src'] ) ) {
$parts[] = 'url(' . $value['image']['src'] . ')';
if ( isset( $value['image']['repeat'] ) ) { $parts[] = $value['image']['repeat']; }
if ( isset( $value['image']['size'] ) ) { $parts[] = is_string( $value['image']['size'] ) ? $value['image']['size'] : 'cover'; }
}
if ( isset( $value['gradient']['stops']['value'] ) ) {
$parts[] = $this->resolve_gradient( $value['gradient'] );
}
return implode( ' ', $parts );
}
private function resolve_gradient( $gradient ): string {
$stops_arr = $gradient['stops']['value'] ?? $gradient['stops'] ?? array();
$type = $gradient['type'] ?? 'linear';
$colors = array();
foreach ( $stops_arr as $stop ) {
$sv = $stop['value'] ?? $stop;
$cl = isset( $sv['color'] ) ? $this->resolve_prop_value( $sv['color'] ) : '';
$pos = isset( $sv['position'] ) ? $this->resolve_prop_value( $sv['position'] ) : null;
if ( $cl ) { $colors[] = $cl . ( $pos ? ' ' . $pos : '' ); }
}
if ( empty( $colors ) ) { return ''; }
$angle = isset( $gradient['angle'] ) ? $this->resolve_prop_value( $gradient['angle'] ) : '90deg';
return "$type-gradient($angle, " . implode( ', ', $colors ) . ')';
}
private function resolve_box_shadow( $value ): string {
if ( ! is_array( $value ) || empty( $value ) ) { return ''; }
$shadows = array();
foreach ( $value as $item ) {
$sv = $item['value'] ?? $item;
if ( ! is_array( $sv ) ) { continue; }
$inset = ! empty( $sv['inset'] ) ? 'inset ' : '';
$x = $this->sz_str( $sv['x'] ?? array( 'size' => 0, 'unit' => 'px' ) );
$y = $this->sz_str( $sv['y'] ?? array( 'size' => 0, 'unit' => 'px' ) );
$blur = $this->sz_str( $sv['blur'] ?? array( 'size' => 0, 'unit' => 'px' ) );
$spread = $this->sz_str( $sv['spread'] ?? array( 'size' => 0, 'unit' => 'px' ) );
$cl = isset( $sv['color'] ) ? $this->resolve_prop_value( $sv['color'] ) : 'transparent';
$shadows[] = "$inset$x $y $blur $spread $cl";
}
return implode( ', ', $shadows );
}
private function resolve_flex( $value ): string {
if ( ! is_array( $value ) ) { return (string) $value; }
$grow = $value['grow'] ?? 0;
$shrink = $value['shrink'] ?? 1;
$basis = isset( $value['basis'] ) ? $this->resolve_prop_value( $value['basis'] ) : 'auto';
return "$grow $shrink $basis";
}
private function resolve_stroke( $value ): string {
if ( ! is_array( $value ) ) { return ''; }
$size = $value['size'] ?? 1;
$unit = $value['unit'] ?? 'px';
$color = isset( $value['color'] ) ? $this->resolve_prop_value( $value['color'] ) : 'currentColor';
return "$size$unit $color";
}
private function resolve_border_radius( $value ): string {
if ( ! is_array( $value ) ) { return (string) $value; }
$ts = isset( $value['top']['start'] ) ? $this->resolve_prop_value( $value['top']['start'] ) : '0';
$te = isset( $value['top']['end'] ) ? $this->resolve_prop_value( $value['top']['end'] ) : $ts;
$be = isset( $value['bottom']['end'] ) ? $this->resolve_prop_value( $value['bottom']['end'] ) : $ts;
$bs = isset( $value['bottom']['start'] ) ? $this->resolve_prop_value( $value['bottom']['start'] ) : $te;
if ( $ts === $te && $te === $be && $be === $bs ) { return $ts; }
return "$ts $te $be $bs";
}
private function resolve_gap( $value ): string {
if ( ! is_array( $value ) ) { return ''; }
$col = isset( $value['column'] ) ? $this->resolve_prop_value( $value['column'] ) : '';
$row = isset( $value['row'] ) ? $this->resolve_prop_value( $value['row'] ) : '';
return $col === $row ? $col : "$row $col";
}
private function sz_str( $val ): string {
if ( is_array( $val ) && isset( $val['size'] ) ) {
return (string) ( $val['size'] ?? 0 ) . ( $val['unit'] ?? 'px' );
}
return (string) $val;
}
private function get_media_query( string $id ): string {
if ( ! isset( $this->breakpoints[ $id ] ) ) { return ''; }
$bp = $this->breakpoints[ $id ];
$bound = 'min' === ( $bp['direction'] ?? 'max' ) ? 'min-width' : 'max-width';
return '@media(' . $bound . ':' . ( $bp['value'] ?? 0 ) . 'px)';
}
// ================================================================
// HTML RENDERING
// ================================================================
private function render_element( array $element ): string {
$el_type = $element['elType'] ?? '';
$widget_type = $element['widgetType'] ?? '';
$settings = $this->flatten_settings( $element['settings'] ?? array() );
$children = $element['elements'] ?? array();
$atomic_containers = array( 'e-div-block', 'e-flexbox', 'e-grid' );
if ( 'widget' === $el_type || ( str_starts_with( $el_type, 'e-' ) && ! in_array( $el_type, $atomic_containers, true ) ) ) {
$widget_type = $widget_type ?: $el_type;
return $this->render_widget( $widget_type, $settings, $children, $element );
}
return $this->render_container( $el_type, $settings, $children, $element );
}
private function render_container( string $el_type, array $settings, array $children, array $raw ): string {
$tag = $this->safe_tag( $settings['tag'] ?? ( 'section' === $el_type ? 'section' : 'div' ), 'div' );
$attr = $this->build_class_attr( $settings, $raw )
. $this->build_id_attr( $settings )
. $this->build_custom_attrs( $settings )
. ( 'a' === $tag ? $this->link_attrs( $settings['link'] ?? array() ) : '' );
$inner = '';
foreach ( $children as $child ) {
$inner .= $this->render_element( $child );
}
return "<$tag$attr>$inner$tag>";
}
private function render_widget( string $type, array $settings, array $children, array $raw ): string {
switch ( $type ) {
case 'e-heading': return $this->render_heading( $settings, $raw );
case 'e-paragraph': return $this->render_paragraph( $settings, $raw );
case 'e-button': return $this->render_button( $settings, $raw );
case 'e-image': return $this->render_image( $settings, $raw );
case 'e-svg': return $this->render_svg( $settings, $raw );
case 'e-divider': return $this->render_divider( $settings, $raw );
case 'e-youtube':
case 'e-self-hosted-video': return $this->render_video( $type, $settings, $raw );
case 'e-tabs':
case 'e-tabs-menu':
case 'e-tabs-content-area': return $this->render_group( $type, $settings, $children, $raw );
case 'e-tab': return $this->render_atomic_tab( $settings, $children, $raw, true );
case 'e-tab-content': return $this->render_atomic_tab( $settings, $children, $raw, false );
case 'e-form': return $this->render_form( $settings, $children, $raw );
case 'e-form-error-message':
case 'e-form-success-message': return $this->render_message( $type, $settings, $children, $raw );
case 'heading': return $this->render_legacy_heading( $settings, $raw );
case 'text-editor': return $this->render_simple_content( 'div', $settings['editor'] ?? '', $settings, $raw );
case 'button': return $this->render_legacy_button( $settings, $raw );
case 'image': return $this->render_legacy_image( $settings, $raw );
case 'icon': return $this->render_icon_widget( $settings, $raw );
case 'icon-list': return $this->render_icon_list( $settings, $raw );
case 'image-box': return $this->render_media_box( $settings, $raw, true );
case 'icon-box': return $this->render_media_box( $settings, $raw, false );
case 'divider': return $this->render_divider( $settings, $raw );
case 'spacer': return $this->render_simple_content( 'div', '', $settings, $raw, 'elementor-spacer' );
case 'video': return $this->render_video( $type, $settings, $raw );
case 'image-gallery':
case 'image-carousel': return $this->render_gallery( $settings, $raw );
case 'counter': return $this->render_counter( $settings, $raw );
case 'progress': return $this->render_progress( $settings, $raw );
case 'testimonial': return $this->render_testimonial( $settings, $raw );
case 'alert': return $this->render_alert( $settings, $raw );
case 'rating':
case 'star-rating': return $this->render_rating( $settings, $raw );
case 'social-icons': return $this->render_social_icons( $settings, $raw );
case 'tabs': return $this->render_legacy_tabs( $settings, $raw, 'tabs' );
case 'accordion': return $this->render_legacy_tabs( $settings, $raw, 'accordion' );
case 'toggle': return $this->render_legacy_tabs( $settings, $raw, 'toggle' );
case 'menu-anchor': return '';
case 'html': return (string) ( $settings['html'] ?? '' );
default: return $this->render_unknown( $type, $settings, $children, $raw );
}
}
// --- Individual widget renderers ---
private function render_heading( array $settings, array $raw ): string {
$tag = $this->safe_tag( $settings['tag'] ?? 'h2', 'h2', array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ) );
$title = $this->extract_html_v3( $raw['settings']['title'] ?? null, true );
$link = $settings['link'] ?? array();
$attr = $this->build_class_attr( $settings, $raw ) . $this->build_id_attr( $settings );
$content = $title;
if ( ! empty( $link['href'] ) ) {
$lt = $link['tag'] ?? 'a';
$tg = isset( $link['target'] ) && '_blank' === $link['target'] ? ' target=_blank rel=noopener' : '';
$content = "<$lt href=\"" . esc_attr( $link['href'] ) . "\"$tg>$title$lt>";
}
return "<$tag$attr>$content$tag>";
}
private function render_paragraph( array $settings, array $raw ): string {
$tag = $this->safe_tag( $settings['tag'] ?? 'p', 'p', array( 'p', 'span' ) );
$text = $this->extract_html_v3( $raw['settings']['paragraph'] ?? null, true );
$link = $settings['link'] ?? array();
$attr = $this->build_class_attr( $settings, $raw ) . $this->build_id_attr( $settings );
$content = $text;
if ( ! empty( $link['href'] ) ) {
$lt = $link['tag'] ?? 'a';
$tg = isset( $link['target'] ) && '_blank' === $link['target'] ? ' target=_blank rel=noopener' : '';
$content = "<$lt href=\"" . esc_attr( $link['href'] ) . "\"$tg>$text$lt>";
}
return "<$tag$attr>$content$tag>";
}
private function render_button( array $settings, array $raw ): string {
$text = $this->extract_html_v3( $raw['settings']['text'] ?? null );
$link = $settings['link'] ?? array();
$attr = $this->build_class_attr( $settings, $raw ) . $this->build_id_attr( $settings );
if ( ! empty( $link['href'] ) ) {
$lt = $link['tag'] ?? 'a';
$tg = isset( $link['target'] ) && '_blank' === $link['target'] ? ' target=_blank rel=noopener' : '';
return "<$lt href=\"" . esc_attr( $link['href'] ) . "\"$attr$tg>$text$lt>";
}
return "";
}
private function render_image( array $settings, array $raw ): string {
$img_setting = $raw['settings']['image'] ?? null;
$img = $this->flatten_nested( $img_setting );
$link = $settings['link'] ?? array();
$attr = $this->build_class_attr( $settings, $raw ) . $this->build_id_attr( $settings );
$src = $img['src'] ?? '';
$alt = $img['alt'] ?? '';
$w = ! empty( $img['width'] ) ? ' width=' . (int) $img['width'] : '';
$h = ! empty( $img['height'] ) ? ' height=' . (int) $img['height'] : '';
$img_tag = "";
if ( ! empty( $link['href'] ) ) {
$lt = $link['tag'] ?? 'a';
$tg = isset( $link['target'] ) && '_blank' === $link['target'] ? ' target=_blank rel=noopener' : '';
return "<$lt href=\"" . esc_attr( $link['href'] ) . "\"$tg>$img_tag$lt>";
}
return $img_tag;
}
private function render_svg( array $settings, array $raw ): string {
$svg_setting = $raw['settings']['svg'] ?? null;
$svg_data = $this->flatten_svg( $svg_setting );
$link = $settings['link'] ?? array();
$attr = $this->build_class_attr( $settings, $raw ) . $this->build_id_attr( $settings );
$svg_url = $svg_data['url'] ?? '';
$svg_html = $svg_data['html'] ?? '';
if ( $svg_html ) {
$content = $svg_html;
} elseif ( str_starts_with( (string) $svg_url, 'data:image/svg+xml' ) ) {
$content = $this->decode_svg_uri( $svg_url );
} elseif ( '' !== $svg_url ) {
$content = '
';
} else {
$content = '';
}
if ( ! empty( $link['href'] ) ) {
$lt = $link['tag'] ?? 'a';
$tg = isset( $link['target'] ) && '_blank' === $link['target'] ? ' target=_blank rel=noopener' : '';
return "<$lt href=\"" . esc_attr( $link['href'] ) . "\"$attr$tg>$content$lt>";
}
return "
' . $desc . '
' : '' ); return '' . (string) ( $settings['testimonial_content'] ?? $settings['content'] ?? '' ) . ''; $body .= $src ? '