fontFamilyValue = $fontFamilyValue; $this->fontFamilyStyleList = $fontFamilyStyleList; } } class CssManager { private $cssString = ""; // Stores the cumulative CSS string to be injected private $cssInsertionTrack = array(); // Tracks which block IDs have had their CSS processed private $googleFonts = array(); // Google fonts track private $utils; // Instance of the Utils class for accessing utility methods // Constructor initializes hooks and the Utils instance public function __construct() { // Hook into WordPress to prepare CSS when rendering blocks and to inject CSS in the head section add_filter('render_block', [$this, 'prepareCss'], 10, 2); add_action('wp_enqueue_scripts', [$this, 'injectCss']); // Initialize the Utils instance $this->utils = new Utils(); } // Apply Target to all selectors in the css string private function applyTarget($css, $target = "") { $cssString = ""; $finalSelector = ""; if ($css) { $finalSelector .= "%%selector%%"; } if ($target) { $finalSelector .= " " . $target; } if ($css) { $cssString = str_replace('%%selector%%', $finalSelector, $css); } return $cssString; } private function applySpecifityToSelector($selector, $frontEndSpecifity = "", $blockName ) { if(!isset($blockName)) { return $selector; } $settings = $this->utils->getSettings(); $finalSpecifity = ""; if ($this->utils->isCoreBlock($blockName)) { $finalSpecifity = $settings['frontEndSpecifityCore']; } else { $finalSpecifity = $settings['frontEndSpecifityThirdParty']; } if(isset($finalSpecifity)) { return $finalSpecifity . " " . $selector; } return $selector; } private function applyClassName($css, $className, $blockName) { // We apply specifity first $finalSelector = $this->applySpecifityToSelector("%%selector%%", "", $blockName); // Replace %%selector%% with actual className $finalSelector = str_replace("%%selector%%", "." . $className, $finalSelector); $result = str_replace("%%selector%%", $finalSelector, $css); // We use final selector in the replace return $result; } // Wraps CSS in a media query private function wrapWithMediaQuery($css, $query) { return "@media only screen and " . $query . " {" . $css . "}"; } // Helper function to apply CSS and CSS editor values to a target or directly to device CSS private function applyControlCss($baseCss, $controlData, $target = null, $controlType = null) { $controlCss = $baseCss; $css = $controlData['css'] ?? ''; if (!empty($target) && $controlType !== "custom_css") { // If there's a target, apply the CSS to the target execluding custom_css control $controlCss .= $this->applyTarget($css, $target); } else { // If there's no target, append CSS directly $controlCss .= $css; } return $controlCss; } // Helper function to finalize CSS by combining rules, applying class names, and avoiding wrapping in media queries if not needed private function finalizeCss($baseCss, $targetsCss, $className, $mediaQuery = null, $blockName) { $finalCss = $baseCss . implode("", $targetsCss); // Combine device-specific CSS with targeted CSS rules // Check if finalCss is empty or contains only whitespace if (!trim($finalCss)) { return ""; // Return an empty string if there's no CSS to process } $finalCss = $this->combineCssRules($finalCss); // Combine CSS rules $finalCss = $this->applyClassName($finalCss, $className, $blockName); // Apply final class name with specificity // Wrap the CSS in a media query if provided, and finalCss is not empty if ($mediaQuery && trim($finalCss)) { $finalCss = $this->wrapWithMediaQuery($finalCss, $mediaQuery); } return $finalCss; } private function processDeviceControls($deviceControls, $blockName, $className, $mediaQuery = null, $mobileFirstMediaQuery = null) { $deviceCss = ""; $targetsCss = array(); $mobileFirstDeviceCss = ""; $mobileFirstTargetsCss = array(); foreach ($deviceControls as $controlSlug => $controlData) { $controlId = $controlData["controlId"] ?? null; // Extract control type from control slug $controlType = $this->utils->getControlTypeFromSlug($controlSlug); // Check if CSS should be generated for this control if ($this->utils->shouldGenerateCssForControl($blockName, $controlId)) { $target = $this->utils->getControlTarget($blockName, $controlId); if ($this->utils->isMobileFirstControl($controlType)) { // For mobile-first controls $mobileFirstDeviceCss = $this->applyControlCss($mobileFirstDeviceCss, $controlData, $target); if (!empty($target)) { $mobileFirstTargetsCss[] = $this->applyControlCss("", $controlData, $target); } } else { // For other controls $deviceCss = $this->applyControlCss($deviceCss, $controlData, $target, $controlType); if (!empty($target)) { $targetsCss[] = $this->applyControlCss("", $controlData, $target); } } // Track google fonts if (isset($controlData['data']['fontFamilyValue'])) { $fontFamilyStyle = $controlData['data']['fontFamilyStyle'] ?? null; $this->googleFonts = $this->addOrUpdateFont($this->googleFonts, $controlData['data']['fontFamilyValue'], $fontFamilyStyle); } } } // Finalize CSS for desktop-first and mobile-first $finalDeviceCss = $this->finalizeCss($deviceCss, $targetsCss, $className, $mediaQuery, $blockName); $finalMobileFirstDeviceCss = $this->finalizeCss($mobileFirstDeviceCss, $mobileFirstTargetsCss, $className, $mobileFirstMediaQuery, $blockName); // Combine desktop-first and mobile-first CSS return $finalDeviceCss . $finalMobileFirstDeviceCss; } // Main method that prepares the CSS for each block public function prepareCss($block_content, $block) { if (isset($block['attrs'][AttributesManager::ATTRIBUTE_KEY])) { // Getting attribute data $data = json_decode($block['attrs'][AttributesManager::ATTRIBUTE_KEY], true); // Check and process only if the block css ID haven't been processed yet if (isset($data['id']) && !in_array($data['id'], $this->cssInsertionTrack)) { // className $className = "abc-style-" . $data['id']; // Mark this block css ID as processed to avoid duplication $this->cssInsertionTrack[] = $data['id']; $settings = $this->utils->getSettings(); $finalCss = ""; // for mobile first controls $mobileFirstDesktopQuery = "(min-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']})"; $mobileFirstTabletQuery = "(min-width:{$settings['brpMobileVal']}{$settings['brpMobileUnit']}) and (max-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']})"; $mobileFirstMobileQuery = "(max-width:{$settings['brpMobileVal']}{$settings['brpMobileUnit']})"; // Define media queries based on settings $tabletQuery = "(max-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']})"; $mobileQuery = "(max-width:{$settings['brpMobileVal']}{$settings['brpMobileUnit']})"; // Process CSS for each device if (isset($data['screens'])) { if (isset($data['screens']['Desktop'])) { $finalCss .= $this->processDeviceControls($data['screens']['Desktop'], $block['blockName'], $className, "", $mobileFirstDesktopQuery); } if (isset($data['screens']['Tablet'])) { $finalCss .= $this->processDeviceControls($data['screens']['Tablet'], $block['blockName'], $className, $tabletQuery, $mobileFirstTabletQuery); } if (isset($data['screens']['Mobile'])) { $finalCss .= $this->processDeviceControls($data['screens']['Mobile'], $block['blockName'], $className, $mobileQuery, $mobileFirstMobileQuery); } } // Apply "forceRules" setting by replacing placeholder if ($this->utils->isCoreBlock($block["blockName"]) && $settings['forceRulesCore']) { $finalCss = str_replace("%%important%%", " !important", $finalCss); } else if (!$this->utils->isCoreBlock($block["blockName"]) && $settings['forceRulesThirdParty']) { $finalCss = str_replace("%%important%%", " !important", $finalCss); } else { $finalCss = str_replace("%%important%%", "", $finalCss); } // Append the final CSS to the class property for later injection $this->cssString .= $finalCss; } // Modify the block's content to include the generated CSS class (if necessary) if( isset($data['id']) && !empty( $block_content ) ) { // Use DOMDocument to append the class to the block's outermost element $doc = new \DOMDocument(); @$doc->loadHTML(mb_convert_encoding($block_content, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); $outerElement = $doc->documentElement; // We only apply the styles in front end if the block has a wrapper element if ($outerElement instanceof \DOMElement) { $existingClass = $outerElement->getAttribute('class'); if($existingClass) { $outerElement->setAttribute('class', $existingClass . ' abc-style-' . $data['id']); } else { $outerElement->setAttribute('class', $existingClass . 'abc-style-' . $data['id']); } $block_content = $doc->saveHTML($outerElement); } } } return $block_content; } // Injects the accumulated CSS into the page head public function injectCss() { // Handle Google Fonts import if there are any fonts to include if (count($this->googleFonts) > 0) { $googleFontsCss = $this->generateSingleCSSImportGoogleFonts($this->googleFonts); // Generate a version hash for caching $googleFontsVersion = md5($googleFontsCss); // Enqueue the Google Fonts CSS wp_register_style('abcon-google-fonts', false, [], $googleFontsVersion ); wp_enqueue_style('abcon-google-fonts'); wp_add_inline_style('abcon-google-fonts', $googleFontsCss); } // Check if there is any CSS to inject if (!empty($this->cssString)) { // Generate a version hash for caching $cssVersion = md5($this->cssString); // Enqueue the main CSS string wp_register_style('abcon-styles', false, [], $cssVersion); wp_enqueue_style('abcon-styles'); wp_add_inline_style('abcon-styles', $this->cssString); } } /** * Adds a new font family or updates an existing one within the global `googleFonts` array. * * @param array $googleFonts The array of FontObjects to add or update. * @param string $fontFamilyValue The name of the font family to add or update. * @param string|null $fontFamilyStyle Optional. The style to add to the font family's style list. * @return array The updated array of FontObjects. */ private function addOrUpdateFont($googleFonts, $fontFamilyValue, $fontFamilyStyle = null) { $found = false; foreach ($googleFonts as $font) { if ($font->fontFamilyValue === $fontFamilyValue) { $found = true; if ($fontFamilyStyle !== null && !in_array($fontFamilyStyle, $font->fontFamilyStyleList)) { $font->fontFamilyStyleList[] = $fontFamilyStyle; } break; } } if (!$found) { $newFontObject = new FontObject($fontFamilyValue, $fontFamilyStyle ? [$fontFamilyStyle] : []); $googleFonts[] = $newFontObject; } return $googleFonts; } /** * Generates a single CSS `@import` statement to import all specified fonts and their styles from Google Fonts. * * @param array $googleFonts An array of FontObjects where each object represents a font family. * @return string A string containing a single CSS `@import` statement that includes all specified fonts and their styles. */ private function generateSingleCSSImportGoogleFonts($googleFonts) { $allFonts = array_map(function($font) { $formattedStyles = array_map(function($style) { if (substr($style, -1) === "i") { return substr($style, 0, -1) . "italic"; } elseif ($style === "regular") { return "400"; } elseif ($style === "italic") { return "400italic"; } else { return $style; } }, $font->fontFamilyStyleList); $formattedStyles = implode(",", $formattedStyles); $fontFamily = str_replace(' ', '+', $font->fontFamilyValue); return $fontFamily . ($formattedStyles ? ':' . $formattedStyles : ''); }, $googleFonts); $allFonts = implode("|", $allFonts); return "@import url('https://fonts.googleapis.com/css?family={$allFonts}');"; } private function combineCssRules(string $input): string { $regex = '/(? $match) { $unmatchedBefore = substr($input, $lastIndex, $match[1] - $lastIndex); $unmatchedEnd .= $unmatchedBefore; $lastIndex = $match[1] + strlen($match[0]); if ($input[$match[1] - 1] === "{") { continue; } $selector = trim($matches[1][$i][0]); $cssRules = trim($matches[2][$i][0]); if (array_key_exists($selector, $rulesMap)) { // Only append the new CSS rule if it doesn't already exist for the selector if (strpos($rulesMap[$selector], $cssRules) === false) { $rulesMap[$selector] .= $cssRules; } } else { $rulesMap[$selector] = $cssRules; } } $unmatchedAfter = substr($input, $lastIndex); $unmatchedEnd .= $unmatchedAfter; $combinedCss = ""; foreach ($rulesMap as $selector => $rules) { $combinedCss .= $selector . "{" . $rules . "}"; } $combinedCss .= $unmatchedEnd; return $combinedCss; } }