<?php
/**
 * Elementor JSON emitter — converts logical rows into Elementor template format.
 * Also includes the template validator.
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class FTE_Elementor_Emitter {

    private int $section_width;

    public function __construct( int $section_width = 1140 ) {
        $this->section_width = $section_width;
    }

    // -----------------------------------------------------------------------
    // Public API
    // -----------------------------------------------------------------------

    /**
     * Emit a complete Elementor template from logical rows.
     *
     * @param array[] $rows   Logical row arrays from FTE_Converter.
     * @param string  $title  Template title.
     * @return array          Elementor template structure.
     */
    public function emit_template( array $rows, string $title = 'Imported from Figma' ): array {
        $content = [];
        foreach ( $rows as $row ) {
            $content[] = $this->emit_section( $row, false );
        }

        return [
            'content'       => $content,
            'page_settings' => [],
            'version'       => '0.4',
            'title'         => $title,
            'type'          => 'page',
        ];
    }

    /**
     * Validate an Elementor template for structural correctness.
     *
     * @param array $template
     * @return array[] Validation errors [ [ 'path' => ..., 'message' => ... ], ... ]
     */
    public function validate( array $template ): array {
        $errors   = [];
        $seen_ids = [];

        foreach ( $template['content'] as $i => $node ) {
            if ( ( $node['elType'] ?? '' ) !== 'section' ) {
                $errors[] = [ 'path' => "content[$i]", 'message' => 'Top-level must be section' ];
            }
            $this->validate_node( $node, "content[$i]", false, $seen_ids, $errors );
        }

        return $errors;
    }

    // -----------------------------------------------------------------------
    // Section emission
    // -----------------------------------------------------------------------

    private function emit_section( array $row, bool $is_inner ): array {
        $columns = array_map(
            fn( $col ) => $this->emit_column( $col, $is_inner ),
            $row['children']
        );

        $settings = [
            'layout'    => $is_inner ? 'full_width' : 'boxed',
            'gap'       => $row['settings']['gap'] ?? 'default',
            'structure' => $this->structure_code( count( $row['children'] ) ),
        ];

        if ( ! $is_inner ) {
            $settings['content_width'] = [ 'size' => $this->section_width, 'unit' => 'px' ];
        }

        if ( ! empty( $row['settings']['backgroundColor'] ) ) {
            $settings['background_background'] = 'classic';
            $settings['background_color']      = $row['settings']['backgroundColor'];
        }

        $has_padding = false;
        foreach ( [ 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight' ] as $key ) {
            if ( ! empty( $row['settings'][ $key ] ) ) $has_padding = true;
        }
        if ( $has_padding ) {
            $settings['padding'] = [
                'top'      => (string) ( $row['settings']['paddingTop'] ?? 0 ),
                'right'    => (string) ( $row['settings']['paddingRight'] ?? 0 ),
                'bottom'   => (string) ( $row['settings']['paddingBottom'] ?? 0 ),
                'left'     => (string) ( $row['settings']['paddingLeft'] ?? 0 ),
                'unit'     => 'px',
                'isLinked' => false,
            ];
        }

        $cp = $row['settings']['contentPosition'] ?? 'top';
        if ( $cp !== 'top' ) {
            $settings['content_position'] = $cp === 'center' ? 'middle' : $cp;
        }

        $section = [
            'id'       => $this->generate_id(),
            'elType'   => 'section',
            'settings' => $settings,
            'elements' => $columns,
        ];

        if ( $is_inner ) {
            $section['isInner'] = true;
        }

        return $section;
    }

    // -----------------------------------------------------------------------
    // Column emission
    // -----------------------------------------------------------------------

    private function emit_column( array $col, bool $parent_is_inner ): array {
        $elements = [];

        foreach ( $col['children'] as $child ) {
            $child_type = $child['type'] ?? '';

            if ( $child_type === 'row' ) {
                if ( $parent_is_inner ) {
                    // Can't nest inner inside inner — flatten
                    array_push( $elements, ...$this->flatten_row_to_widgets( $child ) );
                } else {
                    $elements[] = $this->emit_section( $child, true );
                }
            } elseif ( $child_type === 'widget' ) {
                $elements[] = $this->emit_widget( $child );
            } elseif ( $child_type === 'rasterized' ) {
                $elements[] = $this->emit_rasterized( $child );
            }
        }

        $settings = [
            '_column_size' => $col['widthPct'],
            '_inline_size' => $col['widthPct'],
        ];

        if ( ! empty( $col['settings']['backgroundColor'] ) ) {
            $settings['background_background'] = 'classic';
            $settings['background_color']      = $col['settings']['backgroundColor'];
        }

        $has_padding = false;
        foreach ( [ 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight' ] as $key ) {
            if ( ! empty( $col['settings'][ $key ] ) ) $has_padding = true;
        }
        if ( $has_padding ) {
            $settings['padding'] = [
                'top'      => (string) ( $col['settings']['paddingTop'] ?? 0 ),
                'right'    => (string) ( $col['settings']['paddingRight'] ?? 0 ),
                'bottom'   => (string) ( $col['settings']['paddingBottom'] ?? 0 ),
                'left'     => (string) ( $col['settings']['paddingLeft'] ?? 0 ),
                'unit'     => 'px',
                'isLinked' => false,
            ];
        }

        $va = $col['settings']['verticalAlign'] ?? 'top';
        if ( $va !== 'top' ) {
            $settings['vertical_align'] = $va;
        }

        return [
            'id'       => $this->generate_id(),
            'elType'   => 'column',
            'settings' => $settings,
            'elements' => $elements,
        ];
    }

    // -----------------------------------------------------------------------
    // Widget emission
    // -----------------------------------------------------------------------

    private function emit_widget( array $widget ): array {
        return [
            'id'         => $this->generate_id(),
            'elType'     => 'widget',
            'widgetType' => $widget['widgetType'],
            'settings'   => $widget['settings'],
            'elements'   => [],
        ];
    }

    private function emit_rasterized( array $node ): array {
        return [
            'id'         => $this->generate_id(),
            'elType'     => 'widget',
            'widgetType' => 'image',
            'settings'   => [
                'image'                  => [
                    'url' => '__FIGMA_IMAGE_REF__:' . $node['figmaNodeId'],
                    'id'  => '',
                ],
                '_requires_image_export' => true,
            ],
            'elements'   => [],
        ];
    }

    private function flatten_row_to_widgets( array $row ): array {
        $result = [];
        foreach ( $row['children'] as $col ) {
            foreach ( $col['children'] as $child ) {
                $t = $child['type'] ?? '';
                if ( $t === 'widget' )        $result[] = $this->emit_widget( $child );
                elseif ( $t === 'rasterized' ) $result[] = $this->emit_rasterized( $child );
                elseif ( $t === 'row' )        array_push( $result, ...$this->flatten_row_to_widgets( $child ) );
            }
        }
        return $result;
    }

    // -----------------------------------------------------------------------
    // Validation
    // -----------------------------------------------------------------------

    private function validate_node( array $node, string $path, bool $inside_inner, array &$seen, array &$errors ): void {
        $id = $node['id'] ?? '';
        if ( isset( $seen[ $id ] ) ) {
            $errors[] = [ 'path' => $path, 'message' => "Duplicate ID: $id" ];
        }
        $seen[ $id ] = true;

        $el_type = $node['elType'] ?? '';

        if ( $el_type === 'section' ) {
            if ( ! empty( $node['isInner'] ) && $inside_inner ) {
                $errors[] = [ 'path' => $path, 'message' => 'Inner Section inside Inner Section (forbidden)' ];
            }
            foreach ( $node['elements'] as $i => $child ) {
                if ( ( $child['elType'] ?? '' ) !== 'column' ) {
                    $errors[] = [ 'path' => "$path.el[$i]", 'message' => 'Section child must be column' ];
                }
            }
            $widths = array_map( fn( $e ) => $e['settings']['_column_size'] ?? 0, $node['elements'] );
            $sum    = array_sum( $widths );
            if ( ! empty( $widths ) && ( $sum < 98 || $sum > 102 ) ) {
                $errors[] = [ 'path' => $path, 'message' => "Column widths sum to $sum%, expected ~100%" ];
            }
            $next_inner = ! empty( $node['isInner'] ) || $inside_inner;
            foreach ( $node['elements'] as $i => $child ) {
                $this->validate_node( $child, "$path.el[$i]", $next_inner, $seen, $errors );
            }
        }

        if ( $el_type === 'column' ) {
            foreach ( $node['elements'] as $i => $child ) {
                $ct = $child['elType'] ?? '';
                if ( $ct !== 'widget' && ! ( $ct === 'section' && ! empty( $child['isInner'] ) ) ) {
                    $errors[] = [ 'path' => "$path.el[$i]", 'message' => "Column child must be widget or inner section, got $ct" ];
                }
                $this->validate_node( $child, "$path.el[$i]", $inside_inner, $seen, $errors );
            }
        }

        if ( $el_type === 'widget' && ! empty( $node['elements'] ) ) {
            $errors[] = [ 'path' => $path, 'message' => 'Widget should have no children' ];
        }
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    private function generate_id(): string {
        return bin2hex( random_bytes( 4 ) ); // 8 hex chars
    }

    private function structure_code( int $num_columns ): string {
        if ( $num_columns <= 1 ) return '10';
        return $num_columns . '0';
    }
}

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *


Notice: ob_end_flush(): Failed to send buffer of zlib output compression (1) in /home/greenspacecoil/public_html/wp-includes/functions.php on line 5493

Notice: ob_end_flush(): Failed to send buffer of zlib output compression (1) in /home/greenspacecoil/public_html/wp-includes/functions.php on line 5493