<?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';
}
}