Import CSS into Editor v4 Global Variables and Classes

PHP Code Snippet

/*Snippet Name: Import Global Classes
Description: Adds an admin panel for importing CSS into Elementor Editor v4 global classes and variables.
Compatibale with Elementor version 3.35 beta 4
Version: 2.0 
Author: Charles Emes and ChatGPT, and Imran Siddiq for inspiration
Modified to use get_post_meta and update_post_meta
*/

// Hook into admin menu to add settings page
add_action('admin_menu', 'importglobals_add_admin_menu');

function importglobals_add_admin_menu() {
    $hook = add_submenu_page(
        'options-general.php', // Parent menu: Settings
        'ImportGlobals',         // Page title
        'ImportGlobals',         // Menu title
        'manage_options',     // Capability required
        'importglobals',         // Menu slug
        'importglobals_render_page' // Callback function
    );
}

// Render the Import Globals admin page
function importglobals_render_page() {
    // Check user permissions
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'importglobals'));
    }
	// Add nonce for security
    wp_nonce_field('importglobals_action', 'importglobals_nonce');
	$message = '';
	$logOutput = '';
	$jsonOutput = '';
	$variablesJson = '';
	// get the post id that Elementor uses to store global classes and variables in wp_postmeta
	$postid = get_post_id_function();
	
	// check the  '_elementor_global_classes' row exists
	$exists = false;
	if (metadata_exists('post', $postid, '_elementor_global_classes')) {
		// do nothing;
	//	print_red('do nothing for classes');
	}
	else {  // insert it with the default of '{"items":[],"order":[]}'
		$default = '{"items":[],"order":[]}';
		update_post_meta($postid , '_elementor_global_classes', $default);
		$message = 'Inserted global classes row';
		print($message . "<br/>" );
		$message = '';
	}


	// check the  '_elementor_global_variables' row exists
    $exists = false;
	if (metadata_exists('post', $postid, '_elementor_global_variables')) {
		// do nothing;
		//print_red('do nothing for vars');
	}
	else {  // insert it with the default of ''
		update_post_meta($postid , '_elementor_global_variables', '');
		$message = 'Inserted global variables row';
		print($message . "<br/>");
		$message = '';
	}


  // Handles the POST action on form submit  
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $parser = new CSSParserToJson();
	$logger = new ImportGlobalsLogger ();

	 if (isset($_POST['generate_json'])) {
        $inputCSS = $_POST['css_input'] ?? '';
		$classSort = $_POST['clsorder'] ?? '';
        $jsonOutput = $parser->parse($inputCSS, $classSort, $logger);
		$logOutput =  currentLog($logger);
		//$logOutput = implode(',', currentLog($logger));
    }

	if (isset($_POST['generate_variables'])) {
   		$vargroup = '';
		if (isset($_POST['vargroup'])) {
    		$vargroup = $_POST['vargroup'];
		}  else {
			$vargroup = 'global-color-variable';
		}
        $variablesInput = $_POST['variables_input'] ?? '';
		$variablesSort = $_POST['varorder'] ?? '';
		$arr_json = array();
		$var_result =  get_post_meta($postid , '_elementor_global_variables', true);  //returns a json array of existing global variables or null if emtpy

		if ($var_result !== ''){
			$arr_json = json_decode($var_result, true);   
		}
		else { // otherwise pass an empty array
			$arr_json = array();
		}
        $variablesJson = $parser->parseVariables($variablesInput, $variablesSort, $vargroup, $arr_json );
    }
    if (isset($_POST['validate_classes'])) {
        $jsonOutput = $_POST['json_output'] ?? '';
        $jsonOutput  = stripslashes( $jsonOutput);
        $message = checkClasses($jsonOutput);
    }
	 
    if (isset($_POST['update_elementor'])) {
		$jsonOutput = $_POST['json_output'] ?? '';
		$jsonOutput  = stripslashes( $jsonOutput);
		if ($jsonOutput == ''){
			$message = "Nothing to do!";
		}
		else {
			$mode = '';
			if (isset($_POST['mode'])) {
				$mode = $_POST['mode'];
			}  else {
				$mode = 'append';
			}			
			if ($mode == 'overwrite') {
				$updated = update_post_meta($postid , '_elementor_global_classes',  $jsonOutput);
		 		$message = $updated !== false ? 'Elementor global classes updated. Go to Elementor->Tools->*CLEAR FILES AND DATA*' : 'Failed to update.';
			}
			else {   // we are appending 
				//$message = "mode: " . $mode;
				$str_json =  get_post_meta($postid , '_elementor_global_classes', true);  //returns a json array of existing global classes 
				$arr_json = json_decode($str_json, true);
				$classMap = json_decode($jsonOutput, true);
				$result = $parser->mergeClassMaps($arr_json, $classMap);
				$str_result = json_encode($result, JSON_UNESCAPED_SLASHES);
				$updated = update_post_meta($postid , '_elementor_global_classes',  $str_result);
		 		$message = $updated !== false ? 'Elementor global classes updated. Go to Elementor->Tools->*CLEAR FILES AND DATA*' : 'Failed to update.';
			}   // end of else append
        }  // end of else if ($jsonOutput == '')
    }
	 
    if (isset($_POST['validate_variables'])) {
        $variablesJson = $_POST['variables_output'] ?? '';
        $variablesJson  = stripslashes($variablesJson);       
        $message = checkVars($variablesJson) ;
    }
	
    if (isset($_POST['update_variables'])) {
        $variablesJson = $_POST['variables_output'] ?? '';
        $variablesJson  = stripslashes($variablesJson);
        $updated = update_post_meta($postid , '_elementor_global_variables', $variablesJson);
        $message = $updated !== false ? 'Elementor global variables updated.' : 'Failed to update.';
    }

    if (isset($_POST['reset'])) {
        $default = '{"items":[],"order":[]}';
		if (get_post_meta($postid , '_elementor_global_classes', true) != $default){
        	$updated = update_post_meta($postid , '_elementor_global_classes',  $default);
        	$message = $updated !== false ? 'Reset successful.' : 'Failed to reset.';
		}
		else {
			$message = 'Reset not necessary.';
		}
    }
    if (isset($_POST['resetvars'])) {
        $default = '';    // set meta_value to null
		if (get_post_meta($postid , '_elementor_global_variables', true) != $default){
        	$updated = update_post_meta($postid , '_elementor_global_variables', $default);
        	$message = $updated !== false ? 'Reset successful.' : 'Failed to reset.';
		}
		else {
			$message = 'Reset not necessary.';
		}
    }
}
//  ** Start of the form **//	
    ?><div style="width:100%;">
    <h1>CSS to Elementor Global Classes</h1>
<p>Requires Elementor to be installed and Editor v4 active.</p>
<p>Backup your database before updating. Maximum of 100 classes.</p>
<p>Supports :hover :focus :active provided they immediately follow the class - no support for multiple classes like .toggle-icon .middle-bar{}.  
Supports properties with a single value - either a value like 10px or a variable like var(--space-s). 
Limited support for shorthand properties. Not supported: {padding: 0px 1px;} {border:solid 1px #CCCCCC;} Supported: box-shadow and background-image: linear-gradient(). 
No support for id classes like #mybtn, element classes like body or h1, h2, h3, pseudo classes like ::before ::after, @media queries or @container</p>

    <?php if (!empty($message)): ?>
        <div style="padding:10px; background-color: #fff3cd; border-left: 4px solid #ffeeba; margin-bottom: 15px;">
            <?php echo esc_html($message); ?>
        </div>
    <?php endif; ?>
 <form method="post">
	<div style="display: flex; width:98%;column-gap: 1%;padding-right: 1%;">
		<div style="display:inline-block;width:33%;">
			<h2>Variables Input (one variable per line):</h2>
			<div style="display: inline">
				<input type="radio" id="color" name="vargroup"  value="global-color-variable" checked>
				<label for="color" style="padding-right:20px;">Color</label>
				<input type="radio" id="font-size" name="vargroup"  value="global-font-variable" >
				<label for="font-size"  style="padding-right:20px;">Font family</label>
				<input type="radio" id="space" name="vargroup"  value="global-size-variable"  >
				<label for="space">Size</label>
			</div>
			<textarea name="variables_input" rows="10" style="width: 100%;"><?php echo esc_textarea($_POST['variables_input'] ?? ''); ?></textarea>
			<p><input type="submit" name="generate_variables" class="button button-primary" value="Generate Variables JSON">&nbsp;&nbsp;<input name="varorder" id="varorder" type=checkbox checked=checked><label for="varorder">Sort A-Z</label></p>

			<h2>Variables JSON Output:</h2>
			<textarea readonly name="variables_output" rows="10" style="width: 100%;"><?php echo esc_textarea($variablesJson); ?></textarea>
			<p>
				<input type="submit" name="validate_variables" class="button button-secondary" value="Validate Json Variables">
				<input type="submit" name="update_variables" class="button button-secondary" value="Update Elementor Global Variables">
				<input type="submit" name="resetvars" class="button button-danger" value="Reset Variables" onclick="return confirm('Are you sure you want to reset the global variables?')">
			</p>
		</div>
			<div style="display:inline-block;width:33%;">
				<h2>CSS classes Input</h2>
				<div style="display: inline">
					<input type="radio" id="overwrite" name="mode"  value="overwrite">
					<label for="overwrite" style="color:red;padding-right:20px;">Overwrite</label>
					<input type="radio" id="append" name="mode"  value="append" checked>
					<label for="append">Append</label>
				</div>				
				<textarea name="css_input" rows="10" style="width: 100%;"><?php echo esc_textarea($_POST['css_input'] ?? ''); ?></textarea>
				<p><input type="submit" name="generate_json" class="button button-primary" value="Generate JSON">&nbsp;&nbsp;<input name="clsorder" id="clsorder" type=checkbox checked=checked><label for="clsorder">Sort A-Z</label></p>

				<h2>Generated JSON Output:</h2>
				<textarea readonly name="json_output" rows="10" style="width: 100%;"><?php echo esc_textarea($jsonOutput); ?></textarea>
				<p>
					<input type="submit" name="validate_classes" class="button button-secondary" value="Validate Json Classes">
					<input type="submit" name="update_elementor" class="button button-secondary" value="Update Elementor Global Classes" onclick="return confirm('Are you sure you want to amend the global classes? Check if you selected Overwrite or Append.')"> 
					<input type="submit" name="reset" class="button button-danger" value="Reset" onclick="return confirm('Are you sure you want to reset the global classes? ')">
				</p>

			</div>
		<div style="display:inline-block;width:33%;">
			<h2>Log of Convert CSS -> JSON</h2>
			<span>&nbsp;</span>
			<textarea readonly name="log_output" rows="25" style="width: 100%;"><?php echo esc_textarea($logOutput); ?></textarea>
		</div>
		</div>
    </form>
</div>
<?php 
}
//  ** End of the form **//	

// utility function to echo messages in red
function print_red($message){
	print "<font color ='red'>" . $message . "<br/></font>";
}
// utility function to validate JSON variables
function checkVars($jsonInput){
	$validator = new SimpleJsonValidator();
	$validator->registerSchema('schema1', 'validateVariables');
	$errors = $validator->validate($jsonInput, 'schema1');
	if (empty($errors)) {
		return "Validation passed.\n";
	} else {
		return "Validation failed:\n" . implode("\n", $errors);
	}
}
// utility function to validate JSON classes
function checkClasses($jsonInput){
	$validator = new SimpleJsonValidator();
	$validator->registerSchema('schema2', 'validateClasses');
	$errors = $validator->validate($jsonInput, 'schema2');
	if (empty($errors)) {
    	return "Validation passed.\n";
	} else {
    	return "Validation failed:\n" . implode("\n", $errors);
	}
}
//  utility function to get the post id
function get_post_id_function(){
	$postid  = (int) get_option('elementor_active_kit'); 
	// Fallback if the above does not work
	if ($postid  <= 0) {
		$q = new WP_Query([
			'post_type'      => 'elementor_library', 
			'post_status'    => 'publish', 
			'posts_per_page' => 1, 
			'tax_query'      => [[ 
				'taxonomy' => 'elementor_library_type',  
				'field'    => 'slug',  
				'terms'    => ['kit'], 
				] ],
			'fields' => 'ids', 
			]);
		if (!empty($q->posts[0])) { 
			$postid  = (int) $q->posts[0];
		}
		else {   // no rows found
			$message = 'Unable to find row in posts table for Default Kit';
			print_red($message);
		}
	}
	return $postid ;
}

// main class to do the parsing of the CSS to JSON format  
class CSSParserToJson {

    public function parse(string $cssString, string $clsSort , ImportGlobalsLogger $logger ) {
		
		$logger->log('Begin parse of CSS string length ' . strlen($cssString) );
		$cssString = preg_replace("/\s+/", "", $cssString);  // removes all the whitespace - CR and space
        preg_match_all('/\.([a-zA-Z0-9_-]+)(:hover|:active|:focus)?\{([^}]*)\}/', $cssString, $matches, PREG_SET_ORDER);
        $classMap = [];
        $lastBaseClass = null;
		$classcount = 0;
        foreach ($matches as $match) {
            $classname = $match[1];
			$logger->log('Class name ' . $classname);
            $pseudo = $match[2] ?? null;
            $rules = $match[3];
            $state = $pseudo ? ltrim($pseudo, ':') : null;

            if ($state === null) {
                $lastBaseClass = $classname;
                $id = 'g-' . $this->generate_unique_hex_string(7);
				$logger->log('  global class id ' . $id );
                $classMap[$classname] = [
                    'id' => $id,
                    'type' => 'class',
                    'label' => $classname,
                    'variants' => []
                ];
            }

            $baseClass = $state ? $lastBaseClass : $classname;
            if (!isset($classMap[$baseClass])) continue;
			
			if (!empty($rules)) {   			// if there are no properties then we get an empty variants[] array
			
				$rawProps = [];
				foreach (explode(';', $rules) as $rule) {
					if (trim($rule) === '') continue;
					$property = substr($rule,0,strpos($rule,":"));
					$logger->log('  property = ' . $property );
					if ($this->unsupportedProperties($property)){
						$logger->log('*** Unsupported property = ' . $property . ' ignored ***' );					
					}
					else {
					list($propName, $propValue) = array_map('trim', explode(':', $rule, 2));
					if ($propName=='top'){ $propName = 'inset-block-start';}
					if ($propName=='bottom'){ $propName = 'inset-block-end';}
					if ($propName=='left'){ $propName = 'inset-inline-start';}
					if ($propName=='right'){ $propName = 'inset-inline-end';}
					if ($propValue=='left'){ $propValue = 'start';}
					if ($propValue=='right'){ $propValue = 'end';}
					$rawProps[$propName] = $propValue;
					}
				}  // end of for each property

				$props = $this->normalizeProps($rawProps, $logger);

				$props = $this->rename_array_key($props, 'background-image', 'background'); 

				$classMap[$baseClass]['variants'][] = [
					'meta' => [
						'breakpoint' => 'desktop',
						'state' => $state
					],
					'props' => $props, 
					'custom_css' => null
				];
			
			} // end of if (!empty($rules)) 
			
			//increment class counter
			$classcount  = $classcount  + 1;
			if ($classcount==100) {
				print_red('Maximum class count of 100 reached.') ;
				$logger->log('****Maximum class count of 100 reached. Processing aborted.');
				break;
			}
		}  // end of for each class
		if ($clsSort == "on") {
        	usort($classMap, fn($a, $b) => strcmp($a['label'], $b['label']));   // sort A - Z
		}

        $result = ['items' => [], 'order' => []];
        foreach ($classMap as $item) {
            $result['items'][$item['id']] = $item;
            $result['order'][] = $item['id'];
        }
		$logger->log('End - CSS classes parsed ' . $classcount);
        return json_encode($result, JSON_UNESCAPED_SLASHES);
    }
  
	private function rename_array_key(array $array, string $oldKey, string $newKey): array {
		$newArray = [];
		foreach ($array as $key => $value) {
			// Replace the key if it matches
			if ($key === $oldKey) {
				$key = $newKey;
			}

			// Recursively handle nested arrays
// 			if (is_array($value)) {
// 				$value = $this->rename_array_key($value, $oldKey, $newKey);
// 			}

			$newArray[$key] = $value;
		}
		return $newArray;
	}
	
    private function inferType($prop, $value) {
		$size_properties = ['font-size', 'padding', 'margin', 'width', 'left', 'right', 'top', 'bottom', 'spacing', 'height', 'gap', 'radius', 'opacity', 'inset', 'inline', 'block'];
		$size_property_match = false;
		foreach ($size_properties as $keyword) {
    		if (str_contains($prop, $keyword)) {
        		$size_property_match = true;
       		break;
    		}
		}
		if ($size_property_match) {
			if (preg_match('/^var\(--[a-zA-Z0-9_-]+\)$/', $value)) {  //it might be a size variable
				$variableId = $this->getElementorVariableIdFromLabel($value); 
				if ($variableId) {	return 'global-size-variable'; } else {return 'size';}
			 }
			else {
				return 'size';
			}			
        }
		if (str_contains($prop, 'background-color')) {
			if (preg_match('/^var\(--[a-zA-Z0-9_-]+\)$/', $value)) {  //its a color variable
				 return 'global-color-variable';
			 }
			else {
				return 'background';
			}
		} 
		if (str_contains($prop, 'background-image')) return 'background';
		if (str_contains($prop, 'stroke')) return 'stroke';
        if (str_contains($prop, 'box-shadow')) return 'box-shadow';
        if ($prop=='z-index' || $prop=='column-count') return 'number';
		
        if (str_contains($prop, 'color')) {
			 if (preg_match('/^var\(--[a-zA-Z0-9_-]+\)$/', $value)) {  //its a color variable
				 return 'global-color-variable';
			 }
			else {
				return 'color';
			}			
		}
		if (str_contains($prop, 'font-family')) {
			 if (preg_match('/^var\(--[a-zA-Z0-9_-]+\)$/', $value)) {  //its a font variable
				 return 'global-font-variable';
			 }
			else {
				return 'string';
			}			
		}
        return 'string'; 		// catch all for all the other CSS properties
    }

    private function normalizeProps($rawProps, ImportGlobalsLogger $logger ) {
        $output = [];
        $logicalGroups = [
            'padding' => [
                'props' => [
                    'padding-top' => 'block-start',
                    'padding-bottom' => 'block-end',
                    'padding-left' => 'inline-start',
                    'padding-right' => 'inline-end'
                ],
                '$$type' => 'dimensions'
            ],
            'margin' => [
                'props' => [
                    'margin-top' => 'block-start',
                    'margin-bottom' => 'block-end',
                    'margin-left' => 'inline-start',
                    'margin-right' => 'inline-end'
                ],
                '$$type' => 'dimensions'
            ],
			'border-radius' => [
                'props' => [
                    'border-radius-top' => 'start-start',
                    'border-radius-bottom' => 'start-end',
                    'border-radius-left' => 'end-start',
                    'border-radius-right' => 'end-end'
                ],
                '$$type' => 'border-radius'
            ],
			'border-width' => [
                'props' => [
                    'border-width-top' => 'block-start',
                    'border-width-bottom' => 'block-end',
                    'border-width-left' => 'inline-start',
                    'border-width-right' => 'inline-end'
                ],
                '$$type' => 'border-width'
            ],
			'gap' => [
                'props' => [
                    'column-gap' => 'column',
                    'row-gap' => 'row',
                ],
                '$$type' => 'layout-direction'
            ],
            'stroke' => [
                'props' => [
                    'stroke' => 'color' ,
                    'stroke-width' => 'width'
                ],
                '$$type' => 'stroke'
            ],
	   		'background' => [
                'props' => [
                    'background-color' => 'color'
                ],
                '$$type' => 'background'
            ]
        ];

        foreach ($logicalGroups as $groupKey => $config) {
            $valueObj = [];
            foreach ($config['props'] as $cssProp => $logicalKey) {
                if (isset($rawProps[$cssProp])) {
                    $raw = trim($rawProps[$cssProp]);
                    unset($rawProps[$cssProp]);

					if (in_array($raw, ['auto', 'inherit', 'initial'])) {
        				return [
            				'size' => '',
           					'unit' => $raw
        				];
    				}
                    if (preg_match('/^(\d+(?:\.\d+)?)(px|em|rem|%)$/', $raw, $m)) { 
						$logger->log('  ' . $logicalKey . ', $$type = size,  value = ' . $raw . ' (527)');
                        $valueObj[$logicalKey] = ['$$type' => 'size', 'value' => ['size' => floatval($m[1]), 'unit' => $m[2]]];
                    } 
					if (preg_match('/^var\(--[a-zA-Z0-9-]+\)$/', $raw)) { 
						//check to see if its a GLOBAL variable
					    $variableid = $this->getElementorVariableIdFromLabel($raw);
						if ($variableid !== ''){
							$typeVar = $this->inferType($cssProp, $raw);
							$logger->log('  $typeVar ' . $typeVar);	
							$valueObj[$logicalKey] = ['$$type' => $typeVar, 'value' => $variableid];
							$logger->log('  ' . $logicalKey . ', $$type = ' . $typeVar . ', value = ' . $raw . ', var id = ' . $variableid . ' (537)');			
						}
						else { // its not a global variable so assign it anyway as custom
							$logger->log('  ' . $logicalKey . ', $$type = size,  variable = ' . $raw  . ' (540)');
							$valueObj[$logicalKey] = ['$$type' => 'size', 'value' => ['size' => $raw, 'unit' => 'custom']];
							}
                    } 
					elseif  (preg_match('/^(#|hsl|hsla|rgb|rgba)/', $raw)){    //stroke is a color
                        $logger->log('  ' . $logicalKey . ', $$type = color, value = ' . $raw  . ' (545)' );
						$valueObj[$logicalKey] = ['$$type' => 'color', 'value' => $raw];
                    }
					elseif ($raw == 0) {  //minify removes the unit when value is 0 
                        $logger->log('  ' . $logicalKey . ', $$type = size,  variable = ' . $raw  . ' (549)');
						$valueObj[$logicalKey] = ['$$type' => 'size', 'value' => ['size' => 0, 'unit' => 'px']];
                    } 
					 else {  
                        $valueObj[$logicalKey] = null;
                    }
                } else {  // (isset($rawProps[$cssProp]) is false
					$valueObj[$logicalKey] = null;
                }
            }  
            if (!empty(array_filter($valueObj, fn($v) => $v !== null))) {
                $output[$groupKey] = ['$$type' => $config['$$type'], 'value' => $valueObj];
            }
        }

        foreach ($rawProps as $name => $value) {
			$logger->log('  $$type ' . $this->inferType($name, $value) . ' (565)');
            $output[$name] = ['$$type' => $this->inferType($name, $value), 'value' => $this->normalizeValue($value,$logger, $name)];
        }
        return $output;
    }

	private function normalizeValue($value, ImportGlobalsLogger $logger, $propName = '') {
    $value = trim($value, '"');

    // Detect variable and resolve its ID
    if (preg_match('/^var\(--[a-zA-Z0-9_-]+\)$/', $value)) {
  			// applies for color, font and size variables
            $variableId = $this->getElementorVariableIdFromLabel($value);
			$logger->log('  ' . $propName . ', variable = ' . $variableId . ' (578)');
            if ($variableId) {
				return $variableId;
            }

		$logger->log('  ' . $propName . ', no global size variable called ' . $value  . ' (583)');
        return [
            'size' => $value,
            'unit' => 'custom'
        ];
    }

    // Regular color like #00ff00 or hsla()
    if (str_contains($propName, 'color') && preg_match('/^(#|hsl|hsla|rgb|rgba)/', $value)) {
		$logger->log('  ' . $propName . ', value = ' . $value  . ' (592)');
        return $value;
    }

    // Size value
    if (preg_match('/^(\d+(?:\.\d+)?)(px|em|rem|%)$/', $value, $m)) {
		$logger->log('  ' . $propName . ', size = ' . $value  . ' (598)');
        return [
            'size' => floatval($m[1]),
            'unit' => $m[2]
        ];
    }
	if ($propName == 'box-shadow') {
		$boxvalues = (explode(" ",$value));
		if (count($boxvalues) >= 5){
			$hOffset = $this->get_size_values_function($boxvalues[0]);
			$vOffset = $this->get_size_values_function($boxvalues[1]);
			$blur = $this->get_size_values_function($boxvalues[2]);
			$spread = $this->get_size_values_function($boxvalues[3]);
			$color = $this->get_color_function($value);  // it will be exploded if using rgba or hsla
			$logger->log('  hOffset = ' . $boxvalues[0] . ', vOffset = ' . $boxvalues[1] . ', blur = ' . $boxvalues[2] . ', spread = ' . $boxvalues[3] . ', color = ' . $boxvalues[3] . ', position = null (612)'  );
		return [		
				['$$type' => 'shadow',
				'value' => [
					'hOffset' => $hOffset,
					'vOffset' => $vOffset,
					'blur' => $blur,
					'spread' => $spread,
					'color' => $color, 
					'position' => null
					]
				 ]
        	];
		}
		else {  //count($boxvalues) 
			$logger->log('** To few parameters in box-shadow - requires at least 5 - needs fixing (627)');
			print_red('To few parameters in box-shadow - requires at least 5.');
		}
	}	
	 if ($propName == 'background-image') {
		 if (str_contains($value, 'linear')){
			$gradient_type =  ['$$type' => 'string', 'value' => 'linear'];	
			if (preg_match('/\b(0|[1-9]\d?|[12]\d\d|3[0-5]\d|360)deg\b/', $value, $degs)) {
    			 $degrees  =  ['$$type' => 'number', 'value' => $degs[1]];	 //  $degs[1]; Output: 180
				 $logger->log('  linear-gradient, degrees = '. $degs[1]  . ' (636)');
			}
			 else {
				 $logger->log('**Deg value not found in background-image property, needs fixing (639)');
				 print_red('Deg value not found in background-image property');
				 return;
			 }

			 if (preg_match_all('/(?:rgb|rgba|hsl|hsla)\([^)]*\)\s+(100|[1-9]?\d)%/', $value, $matches)){
				 $stop = $matches[1];
			 }
			 else {
				 $logger->log('**Stop % not found in background-image property, needs fixing (648)');
				 print_red('Stop % not found in background-image property');
				 return;
			 }
			// if (preg_match_all('/(?:rgba?|hsla?)\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)|var\(--[a-zA-Z0-9_-]+\)/', $value, $rgbs)) {
			// if (preg_match_all('/rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)/', $value, $rgbs)) {
			if (preg_match_all('/(?:hsla?|rgba?)\(\s*[^)]+\)/', $value, $rgbs)) {	
			     $colortype1 = 'color';
				 $colortype2 = 'color';
				// $stopcolor = $rgbs[0];
				 $stopcolor1 = $rgbs[0][0];
				 $stopcolor2 = $rgbs[0][1];
				 // repeat for $stopcolor2
				 if (str_contains($stopcolor1, 'var')){
					 // replace with color variable id
					 // not supported by editor yet
					 // $colortype1 = 'global-color-variable';
					 // $stopcolor1 = $this->getElementorVariableIdFromLabel($stopcolor1)				 
				 }
				$logger->log('  color1 = ' . $stopcolor1 . ', offset1 = ' . $stop[0] .  ', color2 = ' . $stopcolor2 . ', offset2 = ' . $stop[1]  . ' (648)' );
				 $arrstop1 = [
						 '$$type' => 'color-stop' , 
						 'value'=> [ 
							 'color' => [
								 '$$type' => $colortype1, 
								 'value' => $stopcolor1
							 	],
							 'offset' => [
								 '$$type' => 'number', 
								 'value' => $stop[0]			 
						 		]
					 		]
						 ];
				 $arrstop2 = [ 
						 '$$type' => 'color-stop' , 
						 'value'=> [ 
							 'color' => [
								 '$$type' => $colortype2, 
								 'value' => $stopcolor2
							 	],
							 'offset' => [
								 '$$type' => 'number', 
								 'value' => $stop[1]			 
						 		]
					 		]];
			 }
			 else {
				 $logger->log('**Color values not found in background-image property, needs fixing (695)');
				 print_red('Color values not found in background-image property');
				 return;
			 }

			 return [		
				 'background-overlay' => [
					'$$type'=> 'background-overlay',
					 'value' => [[		
						'$$type' => 'background-gradient-overlay',
						'value' => [
							'type' => $gradient_type,
							'angle' => $degrees,
							'stops' => 	['$$type' => 'gradient-color-stop',  'value' => [$arrstop1,  $arrstop2] ]
							]
						 ]]				 
					]
			  	
        	];
		 }
		else {
			$logger->log('**Linear gradient not found in background-image property, needs fixing (716)');
			print_red('Linear gradient not found in background-image property');
			return;
		}
	 }
	 if (in_array($value, ['auto', 'inherit', 'initial'])) {
        return [
            'size' => '',
            'unit' => $value
        ];
    }
    if ($value == 0){   // minify removes the px so put it back
		        return [
            'size' => 0,
            'unit' => 'px'
        ];
    }
	// fix for properties that just have numbers
	if (($propName == 'line-height') || ($propName == 'z-index') || ($propName == 'column-count') || ($propName == 'grid-column-start') || ($propName == 'grid-column-end') || ($propName == 'grid-row-start') || ($propName == 'grid-row-end'))   {
		if (strval($value) == strval(floatval($value))){
        	return   floatval($value);
		}
    }
    return $value;
}
	
	private function get_size_values_function($value) {
		    // Size value
		if (preg_match('/^(\d+(?:\.\d+)?)(px|em|rem|%)$/', $value, $m)) {
			return [
				'$$type' => 'size',
					'value' => [
								'size' => floatval($m[1]),
								'unit' => $m[2]
								]
			];
		}
		if (preg_match('/^var\(--[a-zA-Z0-9_-]+\)$/', $value)) {
				// not yet implemented for global size vars
		}
	}

	private function get_color_function($value){
		$color = '';
		$variable = '';
		 // Regex patterns for hex, rgba, and hsla
    	$patterns = [
        // Hex (#RGB, #RRGGBB, #RRGGBBAA)
        '/#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/',
        
        // RGBA (rgba(255,255,255,1))
        '/rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0?\.\d+)\s*\)/i',
        
        // HSLA (hsla(120, 100%, 50%, 0.5))
        '/hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*(0|1|0?\.\d+)\s*\)/i'
    	];

    	foreach ($patterns as $pattern) {
       	 if (preg_match($pattern, $value, $matches)) {
          	 $color  = $matches[0]; // Return the first match
       	 	}
    	}
		if ($color == '')  { // if that was no match look for a variable 
			if (preg_match('/var\(\s*--[a-zA-Z0-9_-]+\s*\)/', $value, $matches)) {
				 $variable  = $matches[0]; // Return the first match
				}
		}
		
		if ($color != '')  {
        	return [
          	  '$$type' => 'color',
          	  'value' => $color 
       		 ];
    	} /// end check for rgb
		 if ($variable != '') {
            $variableId = $this->getElementorVariableIdFromLabel($variable);
            if ($variableId) {
                 return [
                     '$$type' => 'global-color-variable',
                     'value' => $variableId
                ];
            }
		 }  // end check for color var
	}
	
	// merge two arrays 
	public function mergeClassMaps(array $arr_json, array $classMap): array {
    // Ensure required keys exist
    $arr_json['items'] = $arr_json['items'] ?? [];
    $arr_json['order'] = $arr_json['order'] ?? [];

    if (!isset($classMap['items'], $classMap['order'])) {
        return $arr_json;
    }

    // Merge items (classMap items are appended)
    foreach ($classMap['items'] as $id => $item) {
        $arr_json['items'][$id] = $item;
    }

    // Prepend new order IDs (keeping existing ones)
    $arr_json['order'] = array_values(array_unique(
        array_merge($classMap['order'], $arr_json['order'])
    ));

    return $arr_json;
	}

	
	private function unsupportedProperties($searchstr) {
	$unsupported = array("text-decoration-line", "text-decoration-color","transition", "transform", "scroll-margin", "scroll-padding");
	if (in_array($searchstr, $unsupported)){
		return true;
	}
	else {
		return false;
	}
}
	
	private function getElementorVariableIdFromLabel($varString) {
    if (!preg_match('/var\(--([a-zA-Z0-9_-]+)\)/', $varString, $matches)) {
        return null;
    }

    $label = $matches[1];
	$postid = get_post_id_function();
    $json =get_post_meta($postid , '_elementor_global_variables', true);

    if (!$json) return null;

    $data = json_decode($json, true);
    if (!isset($data['data'])) return null;

    foreach ($data['data'] as $id => $variable) {
        if ($variable['label'] === $label) {
            return $id;
        }
    }

    return null;
}
	
    private function generate_unique_hex_string($length) {
        $byte_length = ceil($length / 2);
        $random_bytes = random_bytes($byte_length);
        return substr(bin2hex($random_bytes), 0, $length);
    }
	
    public function parseVariables($input, $sort, $vartype, array &$data) {
        $lines = explode("\n", $input);
		if ($sort == "on") {
			sort($lines);  //sort A-Z
		}
		$maxOrder = 0;
		if (!empty($data)){
			foreach ($data['data'] as $item) {
				if (isset($item['order']) && $item['order'] > $maxOrder) {
					$maxOrder = $item['order'];
				}
			}
		}
		$varcount = $maxOrder + 1;  // if its zero set to 1, or set it to next highest number
        foreach ($lines as $line) {
            $line = trim($line);
            if (preg_match('/^--([a-zA-Z0-9-_]+)\s*:\s*([^;]+);?$/', $line, $matches)) {
                $label = $matches[1];
                $value = trim($matches[2]);
//				if (str_length($value) == 4){ // minify shortened #cccccc to #ccc
//					$value = $value . substr($value,1); // reinstate the 6 character hex code
//				}
                $id = 'e-gv-' . $this->generate_unique_hex_string(7);
				if ($vartype =='global-size-variable'){
					$data['data'][$id] = [
                	'type' => $vartype,
                	'label' => $label,
                	'value' => [
								'$$type' => 'size',
								'value' => [
									'size' => $value,
									'unit' => 'custom'
								]
							] , 
						'order' => $varcount
            			];
				}
				else {   //its either a color or font global variable
					$data['data'][$id] = [
                	'type' => $vartype,
                	'label' => $label,
                	'value' => $value,
					'order' => $varcount	
            	];
				}
            }
			$varcount = $varcount + 1;
        }
		 // Add watermark and version if not already set
		if (!isset($data['watermark'])) {
			$data['watermark'] = 1;
		}

		if (!isset($data['version'])) {
			$data['version'] = 1;
		}
		return json_encode($data, JSON_UNESCAPED_SLASHES);
    } // end public function
	
} // end of class CSSParserToJSON


// class to validate the JSON output
class SimpleJsonValidator {
    /**
     * Holds registered schema validators.
     * @var array<string, callable>
     */
    private array $schemas = [];

    /**
     * Register a named schema with its validation function
     */
    public function registerSchema(string $name, callable $validator): void    {
        $this->schemas[$name] = $validator;
    }

    /*** Validate a JSON string against a named schema
     *
     * @param string $jsonString The JSON string to validate
     * @param string $schemaName The name of the schema to use
     * @return array An array of error messages (empty if valid)
     */
    public function validate(string $jsonString, string $schemaName): array
    {
        if (!isset($this->schemas[$schemaName])) {
            throw new InvalidArgumentException("Schema '$schemaName' is not registered.");
        }

        $data = json_decode($jsonString, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return ['Invalid JSON: ' . json_last_error_msg()];
        }

        // Call the validator function with decoded data
        return ($this->schemas[$schemaName])($data);
    }
}  // end of Class

function validateVariables(array $data): array {
    $errors = [];

    foreach (['data', 'watermark', 'version'] as $key) {
        if (!isset($data[$key])) {
            $errors[] = "Missing required field: $key";
        }
    }

    if (isset($data['data']) && is_array($data['data'])) {
        foreach ($data['data'] as $key => $entry) {
            if (!preg_match('/^e-gv-[a-f0-9]{7}$/', $key)) {    //   /^g-[a-f0-9]{7}$/
                $errors[] = "Invalid key format: $key (must match e-gv-XXXXXXX)";
            }

            foreach (['type', 'label', 'value'] as $field) {
                if (empty($entry[$field])) {
                    $errors[] = "Missing '$field' in $key";
                }
            }

            if (isset($entry['type']) && !in_array($entry['type'], ['global-font-variable', 'global-color-variable', 'global-size-variable'])) {
                $errors[] = "Invalid type '{$entry['type']}' in $key";
            }
        }
    }

    return $errors;
}

function validateClasses(array $data): array {
    $errors = [];

    $validTypes = [
        'size', 'color', 'string', 'number', 'box-shadow', 'dimensions', 'background', 'global-font-variable', 'global-color-variable', 'global-size-variable', 'border-radius', 'border-width', 'stroke'
    ];

    $validUnits = ['px', 'em', 'rem', '%', 'vw', 'vh', 'auto', 'initial', 'inherit', 'custom'];

    if (!isset($data['items']) || !is_array($data['items'])) {
        $errors[] = "'items' must be a valid object";
        return $errors;
    }

    if (!isset($data['order']) || !is_array($data['order'])) {
        $errors[] = "'order' must be an array";
    }

    foreach ($data['items'] as $id => $item) {
        if (!preg_match('/^g-[a-f0-9]{7}$/', $id)) {
            $errors[] = "Invalid item ID format: $id (must match g-XXXXXXX)";
        }

        foreach (['id', 'type', 'label'] as $field) {
            if (!isset($item[$field])) {
                $errors[] = "Missing '$field' in item $id";
            }
        }

        if (isset($item['variants']) && is_array($item['variants'])) {
            foreach ($item['variants'] as $variantIndex => $variant) {
                $variantPath = "$id.variant[$variantIndex]";

                if (!isset($variant['meta']) || !isset($variant['props'])) {
                    $errors[] = "Missing 'meta' or 'props' in $variantPath";
                    continue;
                }

                foreach ($variant['props'] as $propName => $prop) {
                    $propPath = "$variantPath.props.$propName";

                    if (!isset($prop['$$type'])) {
                        $errors[] = "Missing '\$\$type' in $propPath";
                    } elseif (!in_array($prop['$$type'], $validTypes)) {
                        $errors[] = "Invalid '\$\$type' '{$prop['$$type']}' in $propPath";
                    }

                    // Check for "unit" inside value
                    if (isset($prop['value']) && is_array($prop['value'])) {
                        // Handle both: single size object or nested objects (like background -> color -> value)
                        $unitPaths = extractUnits($prop['value']);
                        foreach ($unitPaths as $unitPath => $unitValue) {
                            if (!in_array($unitValue, $validUnits)) {
                                $errors[] = "Invalid unit '$unitValue' at $propPath.value.$unitPath";
                            }
                        }
                    }
                }
            }
        }
    }

    return $errors;
}

function extractUnits(array $value, string $path = ''): array {
    $units = [];

    foreach ($value as $key => $val) {
        $currentPath = $path === '' ? $key : "$path.$key";

        if ($key === 'unit' && is_string($val)) {
            $units[$currentPath] = $val;
        } elseif (is_array($val)) {
            $units += extractUnits($val, $currentPath);
        }
    }

    return $units;
}

class ImportGlobalsLogger {
	public $logs = [];
	public function log(string $message){
		 $logEntry = [
            'message' => $message
        ];
		$this->logs[] = $logEntry;
	}
}

function currentLog(ImportGlobalsLogger $obj) {
	$output = '';
	$arr = $obj->logs;
	foreach($arr as $item){
		$output .= $item['message'] ."\n";
	}
	return $output;
   }

JSON Schema for Elementor Global Classes

Use this tool to validate JSON against the schema

Warning: Schema is based on  Elementor Version 3.35.0-beta1 but likely to change as Editor v4 develops.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "items": {
      "type": "object",
      "patternProperties": {
        "^g-[a-fA-F0-9]{7}$": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "pattern": "^g-[a-fA-F0-9]{7}$"
            },
            "type": { "type": "string" },
            "label": { "type": "string" },
            "variants": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "meta": {
                    "type": "object",
                    "properties": {
                      "breakpoint": { "type": "string" },
                      "state": { "type": ["string", "null"] }
                    },
                    "required": ["breakpoint"]
                  },
                  "props": {
                    "type": "object",
                    "additionalProperties": {
                      "$ref": "#/definitions/PropDefinition"
                    }
                  }
                },
                "required": ["meta", "props"]
              }
            }
          },
          "required": ["id", "type", "label", "variants"]
        }
      }
    },
    "order": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^g-[a-fA-F0-9]{7}$"
      }
    }
  },
  "required": ["items", "order"],
  "definitions": {
    "PropDefinition": {
      "type": "object",
      "properties": {
        "$$type": {
          "type": "string",
          "enum": [
            "size",
            "color",
            "string",
            "box-shadow",
            "background",
            "global-color-variable",
            "global-font-variable",
            "global-size-variable",
            "dimensions",
            "shadow", 
            "border-width",
            "border-radius",
            "number",
            "stroke"
          ]
        },
        "value": {
          "anyOf": [
            {
              "type": "object",
              "properties": {
                "size": { "type": ["number", "string"] },
                "unit": {
                  "type": "string",
                  "enum": ["px", "em", "ch", "rem", "%", "vw", "vh", "custom"]
                }
              },
              "required": ["size", "unit"],
              "additionalProperties": true
            },
            {
              "type": "object",
              "additionalProperties": true
            },
            { "type": "string" },
            { "type": "number" },
            { "type": "array" }
          ]
        }
      },
      "required": ["$$type", "value"]
    }
  }
}

JSON Schema for Elementor Global Variables

Use this tool to validate JSON against the schema

Warning: Schema is based on  Elementor Version 3.35.0-beta1 but likely to change as Editor v4 develops.

{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "type": "object",
  "properties": {
    "data": {
      "type": "object",
      "patternProperties": {
        "^e-gv-[a-fA-F0-9]{7}$": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "enum": ["global-font-variable", "global-color-variable", "global-size-variable"]
            },
            "label": {
              "type": "string"
            },
           "value": {
              "type": ["object", "string"],
              "properties": {
                "$$type": {
                  "type": "string"
                },
                "value": {
                  "type": "object",
                  "properties": {
                    "size": {
                      "type": "string"
                    },
                    "unit": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "size",
                    "unit"
                  ]
                }
              },
              "required": [
                "$$type",
                "value"
              ]
            },
            "order": {
              "type": "number"
            }
          },
          "required": ["type", "label", "value", "order"],
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    "watermark": {
      "type": "integer"
    },
    "version": {
      "type": "integer"
    }
  },
  "required": ["data", "watermark", "version"],
  "additionalProperties": false
}