Import CSS into Editor v4 Global Classes

PHP Code Snippet

Read this post first for important information on usage.

Click Copy in the top right hand corner. Appears as ‘ImportGlobals’ menu item under Settings in WordPress Admin panel.

Editor v4 Limitations

  • Cannot create classes that contain #, space or dot
  • No support (yet) for elements like body, h1 etc
  • No support for ::before and ::after
  • There is a limit of 50 classes!!!!!!

Import Tool Notes

  • Import variables first
  • Avoid shorthand properties like border: solid 1px #cccccc; 
  • Use well formatted CSS – every property ends with ;
  • Use minified CSS for classes using https://minifycss.codeutility.io/
/*Snippet Name: Import Global Classes
Description: Adds an admin panel for importing CSS into Elementor Editor v4 global classes and variables.
Version: 1.0 - Core code as a snippet
Version: 1.1 - Reset added
Version: 1.2 - Simple JSON Validation added
Version: 1.3 - Added support for stroke, border radius and border width, for z-index and line-height and max 50 classes
Version: 1.4 - Added support for box-shadow like .myshadow{box-shadow:10px 10px 5px 12px var(--shadow-primary)}  or hsla or rgba
Version: 1.5 - Added support for linear gradient like .mygradient{background-image:linear-gradient(180deg, rgba(255, 0, 255, 0.5) 3%, rgba(0, 0, 0, 0.5) 90%)}  - rgb, rgba, hsl and hsla values supported, color variable not yet supported by editor.  MUST be background-image
Author: Charles Emes and ChatGPT
*/
// 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');
	
	global $wpdb;
	$message = '';
	$jsonOutput = '';
	$variablesJson = '';
	// get the post id that Elementor uses to store global classes and variables in wp_mostmeta
	 $postid = get_post_id_function();
	 if ($postid == null) {
		return;  // we have a problem
	 }
	// check the  '_elementor_global_classes' row exists
	$searchstr = '_elementor_global_classes';
	$exists = false;
	$exists = global_rows_exist_function( $searchstr);
	if (!$exists){
		return;
	}
	// check the  '_elementor_global_variables' row exists
	$searchstr = '_elementor_global_variables';
	$exists = global_rows_exist_function( $searchstr);
	if (!$exists){
		return ;
	}

  // Handles the POST action on form submit  
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $parser = new CSSParserToJson();
	global $wpdb;
    if (isset($_POST['generate_json'])) {
        $inputCSS = $_POST['css_input'] ?? '';
        $jsonOutput = $parser->parse($inputCSS);
    }

	if (isset($_POST['generate_variables'])) {
   		$vargroup = '';
		if (isset($_POST['vargroup'])) {
    		$vargroup = $_POST['vargroup'];
		}  else {
			$vargroup = 'global-color-variable';
		}
        $variablesInput = $_POST['variables_input'] ?? '';
		$arr_json =  $parser-> get_global_vars_function();  //returns a json array of existing global variables or null if emtpy
        $variablesJson = $parser->parseVariables($variablesInput, $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);
        $updated = $wpdb->update($wpdb->postmeta, ['meta_value' => $jsonOutput], ['meta_key' => '_elementor_global_classes', 'post_id' => $postid]);
        $message = $updated !== false ? 'Elementor global classes updated.' : 'Failed to update.';
    }
	 
    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 = $wpdb->update($wpdb->postmeta, ['meta_value' => $variablesJson], ['meta_key' => '_elementor_global_variables', 'post_id' => $postid]);
        $message = $updated !== false ? 'Elementor global variables updated.' : 'Failed to update.';
    }

    if (isset($_POST['reset'])) {
        $default = '{"items":[],"order":[]}';
        $updated = $wpdb->update($wpdb->postmeta, ['meta_value' => $default], ['meta_key' => '_elementor_global_classes', 'post_id' => $postid]);
        $message = $updated !== false ? 'Reset successful.' : 'Failed to reset.';
    }
    if (isset($_POST['resetvars'])) {
        $default = '';    // set meta_value to null
        $updated = $wpdb->update($wpdb->postmeta, ['meta_value' => $default], ['meta_key' => '_elementor_global_variables', 'post_id' => $postid]);
        $message = $updated !== false ? 'Reset successful.' : 'Failed to reset.';
    }
}
//  ** 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. Check SELECT * FROM wp_postmeta WHERE meta_key LIKE '_elementor_global%' returns 2 rows.</p>
<p>Backup your database before updating. Maximum of 50 classes. Overwrites any existing gobal classes.</p>
<p>Supports single class declarations and :hover :focus :active provided they immediately follow the class - no support for multiple classes like .toggle-icon .middle-bar{} </p>
<p>Supports properties with a single value - either a value like 10px or a variable like var(--space-s)</p>
<p>Limited support for shorthand properties. Not supported: {padding: 0px 1px;} {border:solid 1px #CCCCCC;} Supported: box-shadow and background-image: linear-gradient()</p>
<p>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>
<p>Before update, additional validation of the JSON should be done against the Schema which can be found <a target="_blank" href="https://xms-services.com/resources/">here</a></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:49%;">
			<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">Color</label>
				<input type="radio" id="font-size" name="vargroup"  value="global-font-variable" >
				<label for="font-size">Font-size</label>
				<input type="radio" id="space" name="vargroup"  value="global-space-variable"  >
				<label for="space">Spacing</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"></p>

			<h2>ColorVariables JSON Output:</h2>
			<textarea 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:49%;">
				<h2>Minified CSS Input:</h2>
				<span style="color:red">Overwrites any existing global classes</span>
				<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"></p>

				<h2>Generated JSON Output:</h2>
				<textarea 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">
					<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>
    </form>
</div>
<?php 
}
//  ** End of the form **//	

// 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(){
	global $wpdb;
	$results = $wpdb->get_results("SELECT ID FROM {$wpdb->prefix}posts WHERE post_title = 'Default Kit'");
	$number_of_results = count($results);
	if ($number_of_results > 1){
		$message = 'More than one row found in posts table for Default Kit';
		echo $message  .  '<br/>';
		return;
	}
	if ($number_of_results == 0){
		$message = 'Unable to find row in posts table for Default Kit';
		echo $message  .  '<br/>';
		return;
	}
	if ($number_of_results == 1){
		$postid  = $results[0]->ID;
		return $postid ;
	}
}
//  utility function to see if globals rows exist in table
function global_rows_exist_function($searchstr){
	global $wpdb;
	$exists = false;

	$results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key = '" . $searchstr . "'");
	$number_of_results = count($results);
	if ($number_of_results > 1){
		$message = 'More than one row found in post_meta table for ' . $searchstr;
		echo $message  .  '<br/>';
		return;
	}
	if ($number_of_results == 0){
		$message = 'Unable to find row in post_meta table for ' . $searchstr;
		echo $message  .  '<br/>';
		return;
	}
	if ($number_of_results == 1){
		$exists = true;
		return $exists ;
	}
}
// main class to do the parsing of the CSS to JSON format  
class CSSParserToJson {

    public function parse($cssString) {
        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];
            $pseudo = $match[2] ?? null;
            $rules = $match[3];
            $state = $pseudo ? ltrim($pseudo, ':') : null;

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

            $baseClass = $state ? $lastBaseClass : $classname;
            if (!isset($classMap[$baseClass])) continue;

            $rawProps = [];
            foreach (explode(';', $rules) as $rule) {
                if (trim($rule) === '') continue;
				$property = substr($rule,0,strpos($rule,":"));
				
				if ($this->unsupportedProperties($property)){
					echo  'Unsupported ' . $property;
				}
                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';}
                $rawProps[$propName] = $propValue;
            }

            $props = $this->normalizeProps($rawProps);
			
			$props = $this->rename_array_key($props, 'background-image', 'background'); 
			
            $classMap[$baseClass]['variants'][] = [
                'meta' => [
                    'breakpoint' => 'desktop',
                    'state' => $state
                ],
                'props' => $props
            ];
			//increment class counter
			$classcount  = $classcount  + 1;
			if ($classcount==50) {
				echo 'Maximum class count of 50 reached. ' ;
				break;
			}
		}  // end of for each class

        usort($classMap, fn($a, $b) => strcmp($a['label'], $b['label']));

        $result = ['items' => [], 'order' => []];
        foreach ($classMap as $item) {
            $result['items'][$item['id']] = $item;
            $result['order'][] = $item['id'];
        }

        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) {
            return 'size';
        }
		if (str_contains($prop, 'background-color')) 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';
			}
			
		}
        return 'string';
    }

    private function normalizeProps($rawProps) {
        $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'
            ],
            '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)) {   
                        $valueObj[$logicalKey] = ['$$type' => 'size', 'value' => ['size' => floatval($m[1]), 'unit' => $m[2]]];
                    } 
					elseif (preg_match('/^var\(--[a-zA-Z0-9-]+\)$/', $raw)) { 
                    if (str_contains( $cssProp, 'color')){
						$valueObj[$logicalKey] = ['$$type' => 'global-color-variable', 'value' => $this->getElementorVariableIdFromLabel($raw)];
						}
					else {
                       	$valueObj[$logicalKey] = ['$$type' => 'size', 'value' => ['size' => $raw, 'unit' => 'custom']];
						}
                    } 
					elseif  (preg_match('/^(#|hsl|hsla|rgb|rgba)/', $raw)){    //stroke is a color
                       $valueObj[$logicalKey] = ['$$type' => 'color', 'value' => $raw];
                    }
					elseif ($raw == 0) {  //minify removes the unit when value is 0 
                        $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) {
            $output[$name] = ['$$type' => $this->inferType($name, $value), 'value' => $this->normalizeValue($value, $name)];
        }
        return $output;
    }

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

    // Detect color variable and resolve its ID
    if (preg_match('/^var\(--[a-zA-Z0-9_-]+\)$/', $value)) {
        if (str_contains($propName, 'color')) {
            $variableId = $this->getElementorVariableIdFromLabel($value);
            if ($variableId) {
//                 return [
//                     '$$type' => 'global-color-variable',
//                     'value' => $variableId
//                 ];
                return $variableId;
            }
        }

        return [
            'size' => $value,
            'unit' => 'custom'
        ];
    }

    // Regular color like #00ff00 or hsla()
    if (str_contains($propName, 'color') && preg_match('/^(#|hsl|hsla|rgb|rgba)/', $value)) {
        return [
            '$$type' => 'color',
            'value' => $value
        ];
    }

    // Size value
    if (preg_match('/^(\d+(?:\.\d+)?)(px|em|rem|%)$/', $value, $m)) {
        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
		return [		
				['$$type' => 'shadow',
				'value' => [
					'hOffset' => $hOffset,
					'vOffset' => $vOffset,
					'blur' => $blur,
					'spread' => $spread,
					'color' => $color, 
					'position' => null
					]
				 ]
        	];
		}
	}	
	 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
			}
			 else {
				 echo '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 {
				 echo '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)				 
				 }
				 $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 {
				 echo '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 {
			echo '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
	}
		
	// needs to be public function
	public function get_global_vars_function(){
		global $wpdb;
		$arr_json = array(); 
		$results = $wpdb->get_results("SELECT meta_value FROM {$wpdb->prefix}postmeta WHERE meta_key = '_elementor_global_variables'");
		$number_of_results = count($results);
		if ($number_of_results == 1){
			if ($results[0]->meta_value == null){
			//	return a null array
			}
			else {
				$arr_json = json_decode($results[0]->meta_value,true);
			}
		}
		return $arr_json ;
	}
	
	private function unsupportedProperties($searchstr) {
	$unsupported = array("text-decoration-line", "text-decoration-color", "text-align", "scroll-margin", "scroll-padding");
	if (in_array($searchstr, $unsupported)){
		return true;
	}
	else {
		return false;
	}
}
	
	private function getElementorVariableIdFromLabel($varString) {
	global $wpdb;
    if (!preg_match('/var\(--([a-zA-Z0-9_-]+)\)/', $varString, $matches)) {
        return null;
    }

    $label = $matches[1];

    $json = $wpdb->get_var("
        SELECT meta_value 
        FROM {$wpdb->postmeta} 
        WHERE meta_key = '_elementor_global_variables' 
        LIMIT 1
    ");

    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 'g-' . substr(bin2hex($random_bytes), 0, $length);
    }
	
    public function parseVariables($input, $vartype, array &$data) {
        $lines = explode("\n", $input);

        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_starts_with($value, 'hsl(')) {
                    $value = str_replace('hsl(', 'hsla(', $value);
                    $value = str_replace(')', ', 1)', $value);
                } elseif (preg_match('/^#[a-fA-F0-9]{6}$/', $value)) {
                    $value .= 'ff';
                }

                $id = 'e-gv-' . substr(bin2hex(random_bytes(4)), 0, 7);

				$data['data'][$id] = [
                'type' => $vartype,
                'label' => $label,
                'value' => $value
            ];
            }
        }
		 // 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


	
}
// 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'])) {
                $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', '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;
}

JSON Schema for Elementor Global Classes

Use this tool to validate JSON against the schema

Warning: Schema is based on  Elementor Version 3.31.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",
            "dimensions",
            "shadow", 
            "border-width",
            "border-radius",
            "number",
            "stroke"
          ]
        },
        "value": {
          "anyOf": [
            {
              "type": "object",
              "properties": {
                "size": { "type": ["number", "string"] },
                "unit": {
                  "type": "string",
                  "enum": ["px", "em", "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.31.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"]
            },
            "label": {
              "type": "string"
            },
            "value": {
              "type": "string"
            }
          },
          "required": ["type", "label", "value"],
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    "watermark": {
      "type": "integer"
    },
    "version": {
      "type": "integer"
    }
  },
  "required": ["data", "watermark", "version"],
  "additionalProperties": false
}