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"; } 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"; } return "<$tag$attr>$content"; } 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"; } return "<$tag$attr>$content"; } 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"; } return "$text"; } 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"; } 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"; } return "$content"; } private function render_simple_content( string $tag, $content, array $settings, array $raw, string $extra_class = '' ): string { $tag = $this->safe_tag( $tag, 'div' ); $attr = $this->build_common_attrs( $settings, $raw, $extra_class ); return "<$tag$attr>" . ( is_scalar( $content ) ? (string) $content : '' ) . ""; } private function render_group( string $type, array $settings, array $children, array $raw ): string { $tags = array( 'e-tabs' => 'div', 'e-tabs-menu' => 'nav', 'e-tabs-content-area' => 'div' ); return '<' . $tags[ $type ] . $this->build_common_attrs( $settings, $raw, $type ) . '>' . $this->render_children( $children ) . ''; } private function render_atomic_tab( array $settings, array $children, array $raw, bool $is_label ): string { $content = $this->first_content( $settings, array( 'label', 'title', 'text', 'content' ) ); $content .= $this->render_children( $children ); $tag = $is_label ? 'button' : 'section'; return '<' . $tag . $this->build_common_attrs( $settings, $raw, $is_label ? 'e-tab' : 'e-tab-content' ) . '>' . $content . ''; } private function render_form( array $settings, array $children, array $raw ): string { $inner = $this->render_children( $children ); $fields = $this->get_repeater( $settings, array( 'fields', 'form_fields' ) ); foreach ( $fields as $index => $field ) { $type = strtolower( (string) ( $field['type'] ?? $field['field_type'] ?? 'text' ) ); $input_types = array( 'text', 'email', 'tel', 'number', 'url', 'date', 'time', 'checkbox', 'radio', 'file', 'hidden', 'password' ); $type = in_array( $type, $input_types, true ) ? $type : 'text'; $name = $field['name'] ?? $field['custom_id'] ?? 'field_' . $index; $label = $field['label'] ?? ''; $placeholder = $field['placeholder'] ?? ''; $required = ! empty( $field['required'] ) && 'no' !== $field['required'] ? ' required' : ''; $inner .= ''; } if ( '' === $inner ) { $inner = $this->first_content( $settings, array( 'content', 'text' ) ); } return 'build_common_attrs( $settings, $raw, 'e-form' ) . '>' . $inner . ''; } private function render_message( string $type, array $settings, array $children, array $raw ): string { $content = $this->first_content( $settings, array( 'message', 'text', 'content' ) ) . $this->render_children( $children ); $role = 'e-form-error-message' === $type ? ' role="alert"' : ' role="status"'; return 'build_common_attrs( $settings, $raw, $type ) . $role . '>' . $content . ''; } private function render_legacy_heading( array $settings, array $raw ): string { $tag = $this->safe_tag( $settings['header_size'] ?? $settings['size'] ?? 'h2', 'h2', array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'p' ) ); $content = (string) ( $settings['title'] ?? '' ); return $this->wrap_link( "<$tag" . $this->build_common_attrs( $settings, $raw ) . ">$content", $settings['link'] ?? array() ); } private function render_legacy_button( array $settings, array $raw ): string { $content = $this->render_icon( $settings['selected_icon'] ?? $settings['icon'] ?? array() ) . (string) ( $settings['text'] ?? '' ); $attr = $this->build_common_attrs( $settings, $raw, 'elementor-button' ); $link = $this->normalise_link( $settings['link'] ?? array() ); return ! empty( $link['href'] ) ? 'link_attrs( $link ) . '>' . $content . '' : '' . $content . ''; } private function render_legacy_image( array $settings, array $raw ): string { $image = $this->normalise_media( $settings['image'] ?? array() ); $src = $image['url'] ?? $image['src'] ?? ''; if ( '' === $src ) { return ''; } $alt = $image['alt'] ?? $settings['alt'] ?? $settings['caption'] ?? ''; $caption = (string) ( $settings['caption'] ?? '' ); $html = 'build_common_attrs( $settings, $raw ) . '>' . esc_attr( $alt ) . ''; $html .= '' !== $caption ? '
' . $caption . '
' : ''; $html .= ''; return $this->wrap_link( $html, $settings['link'] ?? array() ); } private function render_icon_widget( array $settings, array $raw ): string { $icon = $this->render_icon( $settings['selected_icon'] ?? $settings['icon'] ?? array() ); return $this->wrap_link( 'build_common_attrs( $settings, $raw, 'elementor-icon' ) . '>' . $icon . '', $settings['link'] ?? array() ); } private function render_icon_list( array $settings, array $raw ): string { $items = $this->get_repeater( $settings, array( 'icon_list', 'items' ) ); $inner = ''; foreach ( $items as $item ) { $content = $this->render_icon( $item['selected_icon'] ?? $item['icon'] ?? array() ) . '' . (string) ( $item['text'] ?? $item['title'] ?? '' ) . ''; $inner .= '
  • ' . $this->wrap_link( $content, $item['link'] ?? array() ) . '
  • '; } return 'build_common_attrs( $settings, $raw, 'elementor-icon-list') . '>' . $inner . ''; } private function render_media_box( array $settings, array $raw, bool $image_box ): string { $media = $image_box ? $this->render_legacy_image( array( 'image' => $settings['image'] ?? array(), 'link' => $settings['link'] ?? array() ), array() ) : $this->render_icon( $settings['selected_icon'] ?? $settings['icon'] ?? array() ); $title = (string) ( $settings['title_text'] ?? $settings['title'] ?? '' ); $desc = (string) ( $settings['description_text'] ?? $settings['description'] ?? '' ); $body = $media . ( '' !== $title ? '

    ' . $this->wrap_link( $title, $settings['link'] ?? array() ) . '

    ' : '' ) . ( '' !== $desc ? '

    ' . $desc . '

    ' : '' ); return 'build_common_attrs( $settings, $raw, $image_box ? 'elementor-image-box' : 'elementor-icon-box' ) . '>' . $body . ''; } private function render_divider( array $settings, array $raw ): string { $content = $this->render_icon( $settings['selected_icon'] ?? $settings['icon'] ?? array() ) . (string) ( $settings['text'] ?? '' ); return 'build_common_attrs( $settings, $raw, 'elementor-divider' ) . '>
    ' . $content . ''; } private function render_video( string $type, array $settings, array $raw ): string { $url = $this->first_url( $settings, array( 'source', 'video', 'src', 'video_url', 'youtube_url', 'vimeo_url', 'hosted_url', 'external_url' ) ); if ( '' === $url ) { return $this->render_unknown( $type, $settings, array(), $raw ); } $attr = $this->build_common_attrs( $settings, $raw, 'elementor-video' ); if ( str_contains( $type, 'youtube' ) || str_contains( $url, 'youtu' ) || str_contains( $url, 'vimeo' ) ) { return ''; } $poster = $this->first_url( $settings, array( 'poster', 'image_overlay' ) ); return ''; } private function render_gallery( array $settings, array $raw ): string { $items = $this->get_repeater( $settings, array( 'gallery', 'carousel', 'slides', 'images' ) ); $inner = ''; foreach ( $items as $item ) { $image = $this->normalise_media( $item ); $src = $image['url'] ?? $image['src'] ?? ''; if ( $src ) { $inner .= '
    ' . esc_attr( $image['alt'] ?? $image['caption'] ?? '' ) . '
    '; } } return 'build_common_attrs( $settings, $raw, 'elementor-gallery' ) . '>' . $inner . ''; } private function render_counter( array $settings, array $raw ): string { $value = (string) ( $settings['ending_number'] ?? $settings['starting_number'] ?? '0' ); $body = '' . (string) ( $settings['prefix'] ?? '' ) . $value . (string) ( $settings['suffix'] ?? '' ) . ''; $body .= ! empty( $settings['title'] ) ? '

    ' . (string) $settings['title'] . '

    ' : ''; return 'build_common_attrs( $settings, $raw, 'elementor-counter' ) . '>' . $body . ''; } private function render_progress( array $settings, array $raw ): string { $percent = $settings['percent'] ?? 0; $percent = is_array( $percent ) ? ( $percent['size'] ?? 0 ) : $percent; return 'build_common_attrs( $settings, $raw, 'elementor-progress' ) . '>' . (string) ( $settings['title'] ?? '' ) . '' . esc_html( $percent ) . '%'; } private function render_testimonial( array $settings, array $raw ): string { $image = $this->normalise_media( $settings['testimonial_image'] ?? $settings['image'] ?? array() ); $src = $image['url'] ?? ''; $body = '
    ' . (string) ( $settings['testimonial_content'] ?? $settings['content'] ?? '' ) . '
    '; $body .= $src ? '' . esc_attr( $settings['testimonial_name'] ?? '' ) . '' : ''; $body .= '' . (string) ( $settings['testimonial_name'] ?? $settings['name'] ?? '' ) . ( ! empty( $settings['testimonial_job'] ) ? ', ' . (string) $settings['testimonial_job'] : '' ) . ''; return 'build_common_attrs( $settings, $raw, 'elementor-testimonial' ) . '>' . $body . ''; } private function render_alert( array $settings, array $raw ): string { return 'build_common_attrs( $settings, $raw, 'elementor-alert' ) . ' role="alert">' . (string) ( $settings['alert_title'] ?? $settings['title'] ?? '' ) . '
    ' . (string) ( $settings['alert_description'] ?? $settings['description'] ?? '' ) . '
    '; } private function render_rating( array $settings, array $raw ): string { $value = $settings['rating_value'] ?? $settings['rating'] ?? 0; $max = $settings['rating_scale'] ?? 5; $value = is_array( $value ) ? ( $value['size'] ?? 0 ) : $value; $max = is_array( $max ) ? ( $max['size'] ?? 5 ) : $max; return 'build_common_attrs( $settings, $raw, 'elementor-rating' ) . ' aria-label="' . esc_attr( "$value out of $max" ) . '">' . (string) ( $settings['title'] ?? '' ) . ' ' . esc_html( $value ) . '/' . esc_html( $max ) . ''; } private function render_social_icons( array $settings, array $raw ): string { $items = $this->get_repeater( $settings, array( 'social_icon_list', 'social_icons', 'icons' ) ); $inner = ''; foreach ( $items as $item ) { $label = (string) ( $item['social'] ?? $item['label'] ?? 'Social link' ); $icon = $this->render_icon( $item['social_icon'] ?? $item['selected_icon'] ?? array() ); $inner .= $this->wrap_link( $icon . '' . esc_html( $label ) . '', $item['link'] ?? array() ); } return 'build_common_attrs( $settings, $raw, 'elementor-social-icons' ) . '>' . $inner . ''; } private function render_legacy_tabs( array $settings, array $raw, string $type ): string { $items = $this->get_repeater( $settings, array( 'tabs', 'items' ) ); $inner = ''; foreach ( $items as $item ) { $title = (string) ( $item['tab_title'] ?? $item['title'] ?? '' ); $content = (string) ( $item['tab_content'] ?? $item['content'] ?? '' ); $inner .= 'tabs' === $type ? '

    ' . $title . '

    ' . $content . '
    ' : '
    ' . $title . '
    ' . $content . '
    '; } return 'build_common_attrs( $settings, $raw, 'elementor-' . $type ) . '>' . $inner . ''; } private function decode_svg_uri( string $uri ): string { if ( preg_match( '/data:image\/svg\+xml;base64,(.+)/i', $uri, $m ) ) { $decoded = base64_decode( $m[1], true ); if ( $decoded && str_contains( $decoded, ''; } private function render_unknown( string $type, array $settings, array $children, array $raw ): string { $inner = $this->render_children( $children ); foreach ( array( 'title', 'text', 'paragraph', 'description', 'content', 'editor' ) as $key ) { if ( isset( $settings[ $key ] ) && is_scalar( $settings[ $key ] ) ) { $inner .= (string) $settings[ $key ]; } } foreach ( array( 'image', 'media', 'thumbnail' ) as $key ) { if ( empty( $settings[ $key ] ) ) { continue; } $media = $this->normalise_media( $settings[ $key ] ); $src = $media['url'] ?? $media['src'] ?? ''; if ( $src ) { $inner .= '' . esc_attr( $media['alt'] ?? '' ) . ''; } } return 'build_common_attrs( $settings, $raw, 'elementor-widget-' . $this->safe_class( $type ?: 'unknown' ) ) . ' data-elementor-widget="' . esc_attr( $type ?: 'unknown' ) . '">' . $inner . ''; } // ================================================================ // HELPERS // ================================================================ private function flatten_settings( array $settings ): array { $flat = array(); foreach ( $settings as $k => $v ) { $flat[ $k ] = $this->deep_unwrap( $v ); } return $flat; } private function deep_unwrap( $value ) { if ( ! is_array( $value ) ) { return $value; } if ( isset( $value['$$type'] ) && array_key_exists( 'value', $value ) ) { return $this->deep_unwrap( $value['value'] ); } $result = array(); foreach ( $value as $key => $item ) { $result[ $key ] = $this->deep_unwrap( $item ); } return $result; } private function unwrap( $envelope ) { if ( ! is_array( $envelope ) || ! isset( $envelope['$$type'] ) || ! array_key_exists( 'value', $envelope ) ) { return $envelope; } $value = $envelope['value']; if ( 'link' === $envelope['$$type'] && is_array( $value ) ) { $flat = array(); foreach ( $value as $lk => $lv ) { $flat[ $lk ] = $this->unwrap( $lv ); } return $flat; } if ( in_array( $envelope['$$type'], array( 'image', 'svg-src' ), true ) && is_array( $value ) ) { $flat = array(); foreach ( $value as $ik => $iv ) { $flat[ $ik ] = $this->unwrap( $iv ); } return $flat; } return $value; } private function extract_html_v3( $setting, bool $normalise_inline = false ): string { $value = $setting; // Elementor normally wraps html-v3 in a typed envelope, while API // responses and older saved data can provide the already-flattened value. if ( is_array( $value ) && isset( $value['$$type'] ) && array_key_exists( 'value', $value ) ) { $value = $value['value']; } elseif ( is_array( $value ) && isset( $value['value'] ) && is_array( $value['value'] ) && ( isset( $value['value']['$$type'] ) || array_key_exists( 'content', $value['value'] ) || array_key_exists( 'children', $value['value'] ) ) ) { $value = $value['value']; } if ( ! is_array( $value ) ) { return $this->maybe_normalise_inline_html( (string) $value, $normalise_inline ); } // Content is authoritative. The child index is metadata for Elementor's // editor and must not replace the ordered HTML string when both exist. if ( array_key_exists( 'content', $value ) ) { $content = $this->unwrap_html_v3_value( $value['content'] ); if ( null !== $content ) { return $this->maybe_normalise_inline_html( (string) $content, $normalise_inline ); } } if ( array_key_exists( 'value', $value ) && ! is_array( $value['value'] ) && ! array_key_exists( 'children', $value ) ) { return $this->maybe_normalise_inline_html( (string) $value['value'], $normalise_inline ); } $parts = array(); foreach ( (array) ( $value['children'] ?? array() ) as $child ) { if ( ! is_array( $child ) ) { continue; } $content = array_key_exists( 'content', $child ) ? $this->unwrap_html_v3_value( $child['content'] ) : $this->unwrap_html_v3_value( $child['value'] ?? null ); if ( null !== $content ) { $parts[] = (string) $content; } } return $this->maybe_normalise_inline_html( implode( '', $parts ), $normalise_inline ); } private function maybe_normalise_inline_html( string $html, bool $normalise_inline ): string { return $normalise_inline ? $this->normalise_inline_html( $html ) : $html; } private function unwrap_html_v3_value( $value ): ?string { if ( is_array( $value ) && array_key_exists( 'value', $value ) && ( isset( $value['$$type'] ) || 1 === count( $value ) ) ) { $value = $value['value']; } return is_scalar( $value ) || null === $value ? ( null === $value ? null : (string) $value ) : null; } /** * Keep only the inline HTML contract used by the Atomic text widgets. * Entity-encoded tags are restored one tag at a time so text such as * &lt;script&gt; is never broadly decoded into executable markup. */ private function normalise_inline_html( string $html ): string { $html = preg_replace_callback( '/<(\\/)?\\s*(strong|em|u|s|sup|sub|br|a|span|img)\\b(.*?)>/is', function ( array $matches ): string { return $this->render_inline_tag( strtolower( $matches[2] ), ! empty( $matches[1] ), $matches[3] ); }, $html ) ?? $html; return preg_replace_callback( '/<\\/?([a-z][a-z0-9]*)\\b([^>]*)>/i', function ( array $matches ): string { $tag = strtolower( $matches[1] ); if ( ! in_array( $tag, self::INLINE_HTML_TAGS, true ) ) { return ''; } return $this->render_inline_tag( $tag, '/' === $matches[0][1], $matches[2] ); }, $html ) ?? $html; } private function render_inline_tag( string $tag, bool $closing, string $raw_attrs ): string { if ( $closing ) { return in_array( $tag, array( 'br', 'img' ), true ) ? '' : ''; } if ( 'br' === $tag ) { return '
    '; } return '<' . $tag . $this->sanitize_inline_attributes( $raw_attrs, $tag ) . '>'; } private function sanitize_inline_attributes( string $raw_attrs, string $tag ): string { $raw_attrs = html_entity_decode( $raw_attrs, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); if ( ! preg_match_all( '/([A-Za-z][A-Za-z0-9_-]*)\\s*=\\s*(["\\x27])(.*?)\\2/s', $raw_attrs, $matches, PREG_SET_ORDER ) ) { return ''; } $output = ''; foreach ( $matches as $match ) { $name = strtolower( $match[1] ); if ( ! in_array( $name, self::INLINE_HTML_ATTRIBUTES[ $tag ] ?? array(), true ) ) { continue; } $value = html_entity_decode( $match[3], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); switch ( $name ) { case 'id': $value = preg_match( '/^[A-Za-z0-9_-]+$/', $value ) ? $value : ''; break; case 'class': $classes = preg_split( '/\\s+/', trim( $value ) ) ?: array(); $classes = array_filter( array_map( array( $this, 'safe_class' ), $classes ) ); $value = implode( ' ', array_unique( $classes ) ); break; case 'style': $value = $this->sanitize_inline_style( $value ); break; case 'href': case 'src': $value = $this->sanitize_inline_url( $value ); break; case 'target': $value = in_array( $value, array( '_blank', '_self', '_parent', '_top' ), true ) ? $value : ''; break; default: $value = $this->sanitize_inline_text( $value ); break; } if ( '' !== $value ) { $output .= ' ' . $name . '="' . esc_attr( $value ) . '"'; } } return $output; } private function sanitize_inline_style( string $value ): string { $value = function_exists( 'safecss_filter_attr' ) ? safecss_filter_attr( $value ) : $value; return preg_match( '/(?:expression|javascript\\s*:|vbscript\\s*:|url\\s*\\(\\s*["\\x27]?javascript)/i', $value ) ? '' : $value; } private function sanitize_inline_url( string $value ): string { $value = trim( $value ); if ( preg_match( '/^\\s*(?:javascript|vbscript):/i', $value ) ) { return ''; } return function_exists( 'esc_url' ) ? (string) esc_url( $value ) : $value; } private function sanitize_inline_text( string $value ): string { return function_exists( 'sanitize_text_field' ) ? (string) sanitize_text_field( $value ) : preg_replace( '/[\\x00-\\x1F\\x7F]/', '', $value ); } private function flatten_nested( $setting ): array { if ( ! is_array( $setting ) ) { return array(); } $value = $setting['value'] ?? $setting; if ( ! is_array( $value ) ) { return array(); } $flat = array(); foreach ( $value as $k => $v ) { $flat[ $k ] = is_array( $v ) && isset( $v['$$type'] ) ? ( $v['value'] ?? $v ) : $v; } return $flat; } private function flatten_svg( $setting ): array { if ( ! is_array( $setting ) ) { return array(); } $value = $setting['value'] ?? $setting; if ( ! is_array( $value ) ) { return array(); } $flat = array(); foreach ( $value as $k => $v ) { if ( is_array( $v ) && isset( $v['$$type'] ) && 'url' === $v['$$type'] ) { $flat[ $k ] = $v['value'] ?? ''; } else { $flat[ $k ] = $v; } } return $flat; } private function build_class_attr( array $settings, array $raw ): string { $classes = array(); if ( ! empty( $raw['id'] ) ) { $classes[] = 'elementor-element-' . $this->safe_class( $raw['id'] ); } $styles = $raw['styles'] ?? array(); foreach ( $styles as $sd ) { $id = $sd['id'] ?? ''; if ( '' !== $id && 'class' === ( $sd['type'] ?? '' ) ) { $classes[] = $id; } } if ( ! empty( $settings['_css_classes'] ) ) { $classes = array_merge( $classes, preg_split( '/\s+/', (string) $settings['_css_classes'] ) ?: array() ); } foreach ( (array) ( $settings['classes'] ?? array() ) as $class ) { if ( is_string( $class ) ) { $classes[] = $class; } if ( is_array( $class ) && is_string( $class['value'] ?? null ) ) { $classes[] = $class['value']; } } $classes = array_values( array_unique( array_filter( array_map( array( $this, 'safe_class' ), $classes ) ) ) ); if ( empty( $classes ) ) { return ''; } return ' class="' . esc_attr( implode( ' ', $classes ) ) . '"'; } private function build_common_attrs( array $settings, array $raw, string $extra_class = '' ): string { $classes = array(); if ( ! empty( $raw['id'] ) ) { $classes[] = 'elementor-element-' . $this->safe_class( $raw['id'] ); } foreach ( (array) ( $raw['styles'] ?? array() ) as $style ) { if ( is_array( $style ) && 'class' === ( $style['type'] ?? '' ) && ! empty( $style['id'] ) ) { $classes[] = $style['id']; } } if ( ! empty( $settings['_css_classes'] ) ) { $classes = array_merge( $classes, preg_split( '/\s+/', (string) $settings['_css_classes'] ) ?: array() ); } if ( ! empty( $settings['classes'] ) ) { $setting_classes = is_array( $settings['classes'] ) ? $settings['classes'] : preg_split( '/\s+/', (string) $settings['classes'] ); foreach ( $setting_classes ?: array() as $class ) { if ( is_string( $class ) ) { $classes[] = $class; } if ( is_array( $class ) && is_string( $class['value'] ?? null ) ) { $classes[] = $class['value']; } } } if ( $extra_class ) { $classes[] = $extra_class; } $classes = array_values( array_unique( array_filter( array_map( array( $this, 'safe_class' ), $classes ) ) ) ); return ( $classes ? ' class="' . esc_attr( implode( ' ', $classes ) ) . '"' : '' ) . $this->build_id_attr( $settings ) . $this->build_custom_attrs( $settings ); } private function build_id_attr( array $settings ): string { return empty( $settings['_cssid'] ) ? '' : ' id="' . esc_attr( $settings['_cssid'] ) . '"'; } private function build_link_attrs( array $settings ): string { $link = $settings['link'] ?? array(); if ( empty( $link['href'] ) ) { return ''; } $attrs = ' href="' . esc_attr( $link['href'] ) . '"'; if ( isset( $link['target'] ) && '_blank' === $link['target'] ) { $attrs .= ' target="_blank" rel="noopener"'; } return $attrs; } private function build_custom_attrs( array $settings ): string { $attrs = $settings['_attributes'] ?? $settings['attributes'] ?? array(); if ( is_string( $attrs ) ) { $parsed = array(); foreach ( preg_split( '/\r?\n/', $attrs ) ?: array() as $line ) { $parts = explode( '|', $line, 2 ); if ( 2 === count( $parts ) ) { $parsed[ trim( $parts[0] ) ] = trim( $parts[1] ); } } $attrs = $parsed; } if ( ! is_array( $attrs ) ) { return ''; } $html = ''; $allowed = array( 'title', 'role', 'aria-label', 'aria-hidden', 'aria-controls', 'aria-describedby', 'tabindex' ); foreach ( $attrs as $name => $value ) { $name = strtolower( (string) $name ); if ( ! in_array( $name, $allowed, true ) && ! preg_match( '/^data-[a-z0-9_.:-]+$/', $name ) ) { continue; } if ( ! is_scalar( $value ) ) { continue; } $html .= ' ' . $name . '="' . esc_attr( (string) $value ) . '"'; } return $html; } private function safe_tag( $tag, string $fallback = 'div', ?array $allowed = null ): string { $allowed = $allowed ?? array( 'div', 'section', 'article', 'main', 'aside', 'header', 'footer', 'nav', 'figure', 'figcaption', 'ul', 'ol', 'li', 'p', 'span', 'a', 'button', 'form', 'label', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ); $tag = strtolower( (string) $tag ); return in_array( $tag, $allowed, true ) ? $tag : $fallback; } private function safe_class( $class ): string { return preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $class ) ?: ''; } private function render_children( array $children ): string { $html = ''; foreach ( $children as $child ) { if ( is_array( $child ) ) { $html .= $this->render_element( $child ); } } return $html; } private function get_repeater( array $settings, array $keys ): array { foreach ( $keys as $key ) { if ( isset( $settings[ $key ] ) && is_array( $settings[ $key ] ) ) { return array_values( array_filter( $settings[ $key ], 'is_array' ) ); } } return array(); } private function normalise_media( $media ): array { $media = $this->deep_unwrap( $media ); if ( is_string( $media ) ) { return array( 'url' => $media ); } if ( ! is_array( $media ) ) { return array(); } if ( empty( $media['url'] ) && ! empty( $media['src'] ) && is_string( $media['src'] ) ) { $media['url'] = $media['src']; } return $media; } private function normalise_link( $link ): array { $link = $this->deep_unwrap( $link ); if ( is_string( $link ) ) { return array( 'href' => $link ); } if ( ! is_array( $link ) ) { return array(); } if ( empty( $link['href'] ) && ! empty( $link['url'] ) ) { $link['href'] = $link['url']; } return $link; } private function link_attrs( $link ): string { $link = $this->normalise_link( $link ); if ( empty( $link['href'] ) ) { return ''; } $attrs = ' href="' . esc_attr( $link['href'] ) . '"'; if ( '_blank' === ( $link['target'] ?? '' ) || ! empty( $link['is_external'] ) ) { $attrs .= ' target="_blank" rel="noopener noreferrer"'; } if ( ! empty( $link['nofollow'] ) ) { $attrs .= ' rel="nofollow"'; } return $attrs; } private function wrap_link( string $html, $link ): string { $attrs = $this->link_attrs( $link ); return $attrs ? '' . $html . '' : $html; } private function render_icon( $icon ): string { $icon = $this->deep_unwrap( $icon ); if ( is_string( $icon ) ) { return ''; } if ( ! is_array( $icon ) ) { return ''; } $value = $icon['value'] ?? $icon['library'] ?? ''; if ( is_string( $value ) && str_starts_with( $value, '