This class parses a CSS file (or direct CSS input) and returns a multidimensional associative array with selectors set hierarchically to their parent selectors as they are written in the CSS file.
<?php
/**
* Class CssParser
*
* Parses CSS files and extracts relevant data hierarchically.
*/
class CssParser6 {
/**
* Flag indicating whether only color-related attributes should be parsed.
*
* @var bool
*/
private bool $colorsOnly = false;
/**
* Indicates whether to include media at-rules during CSS parsing.
*
* @var bool
*/
private bool $includeMediaQueries = false;
/**
* Parsed CSS data structure.
*
* @var array
*/
private array $parsedCss = [];
/**
* The key used to identify at-rules in the parsed CSS data structure.
*
* @var string
*/
private string $atRulesKey = '@';
/**
* The initial CSS passed into the parser
*
* @var string
*/
private string $css;
/**
* Sets the flag to indicate whether only color-related attributes should be parsed.
*
* This method sets the flag to indicate whether only color-related attributes
* should be parsed during CSS parsing. If the parameter $colorsOnly is set to true,
* only color-related attributes will be parsed; otherwise, all attributes will be parsed.
*
* @param bool $colorsOnly (optional) If set to true, only color-related attributes will be parsed. Defaults to false.
* @return self
*/
public function colorsOnly(bool $colorsOnly = false): self
{
$this->colorsOnly = $colorsOnly;
return $this;
}
/**
* Sets whether to include media at-rules during CSS parsing.
*
* @param bool $includeMediaQueries If true, media at-rules will be included during parsing;
* if false, they will be excluded.
* @return self
*/
public function includeMediaQueries(bool $includeMediaQueries = false): self
{
$this->includeMediaQueries = $includeMediaQueries;
return $this;
}
/**
* Retrieves the parsed CSS data.
*
* This method returns the parsed CSS data structure.
*
* @return array The parsed CSS data structure.
*/
public function parsed(): array
{
return $this->parsedCss;
}
/**
* Sets the CSS content from a file and cleans it.
*
* This method reads the contents of the CSS file specified by the given filename,
* cleans the CSS content by removing comments and @media blocks,
* and sets the cleaned CSS content to be parsed.
*
* @param string $filename The path to the CSS file.
* @throws \Exception If the file cannot be read or doesn't exist.
* @return self
*/
public function setCssFromFile(string $filename): self
{
if (!file_exists($filename)) {
throw new \Exception("File not found: $filename");
}
$css = file_get_contents($filename);
if ($css === false) {
throw new \Exception("Failed to read file: $filename");
}
$this->setCssFromSource($css);
return $this;
}
/**
* Sets the CSS content directly.
*
* This method takes CSS content provided directly and sets the CSS content to be parsed.
*
* @param string $source The CSS content from the source.
* @return self
*/
public function setCssFromSource(string $source): self
{
$this->css = $source;
return $this;
}
/**
* Parses CSS content and populates the parsed CSS array.
*
* This method uses a regular expression to match CSS rules and at-rules in the provided CSS content.
* It then delegates parsing to other methods in the class based on the matches found,
* and finally removes any empty arrays from the parsed CSS data structure.
*
* @return void
*/
public function parseCss(): void
{
preg_match_all('/@*[^;{}]+\s*{(?:[^{}]*|(?R))*}|@[^;{}]+;/', $this->clean($this->css), $matches);
$this->parseMatches($matches);
$this->removeEmptyArrays($this->parsedCss);
}
/**
* Cleans a CSS string by removing comments and @media blocks.
*
* This method removes CSS comments and optionally @media blocks from the provided CSS string.
* It then trims any leading or trailing whitespace and returns the cleaned CSS string.
*
* @param string $css The CSS string to be cleaned.
* @return string The cleaned CSS string.
*/
private function clean(string $css): string
{
$css = preg_replace('~/\*(?:(?!\*/).)*\*/~s', '', $css);
if($this->includeMediaQueries === false)
{
$css = preg_replace('~@media\b[^{]*({((?:[^{}]+|(?1))*)})~', '', $css);
}
return trim($css);
}
/**
* Parses a CSS attribute string into a key-value pair.
*
* This method takes a CSS attribute string, splits it into key and value parts,
* trims each part, and returns an associative array representing the attribute.
* If the attribute string is invalid or empty, an empty array is returned.
*
* @param string $attribute The CSS attribute string to be parsed.
* @return array The parsed key-value pair representing the attribute.
*/
private function setAttribute(string $attribute): array
{
$parts = explode(':', $attribute, 2);
return (count($parts) == 2) ? [trim($parts[0]) => trim($parts[1], " ;")] : [];
}
/**
* Parses matches from a CSS string and delegates parsing to appropriate methods.
*
* This method iterates over the matches array, determining whether each match
* corresponds to an at-rule or a regular CSS rule, and delegates the parsing
* accordingly to other methods in the class.
*
* @param array $matches An array containing matches from a CSS string.
* @return void
*/
private function parseMatches(array $matches): void
{
foreach ($matches[0] as $match) {
if (strpos($match, '@') !== false) {
$this->parseAtRules($match);
} else {
$this->parseRegular($match);
}
}
}
/**
* Parses a regular CSS rule, extracts selectors and attributes, and merges them into the parsed CSS structure.
*
* This method takes a regular CSS rule as input, splits it into selectors and attributes,
* and then processes each selector and its associated attributes. It handles multiple selectors separated by commas,
* splits selectors to extract nested keys, and merges attributes into corresponding levels of the parsed CSS structure.
*
* @param string $match The regular CSS rule to be parsed.
* @return void
*/
private function parseRegular(string $match): void
{
$parts = explode('{', $match, 2); // Limit to two parts to handle cases with multiple {
$selector = trim($parts[0]);
preg_match_all('/([\w-]+)\s*:\s*((?:[^;\'"\(\)]+|\((?:[^\(\)]++|(?2))*\))+);/', $parts[1], $matches);
$selectors = preg_split('/,(?![^()]*\))/', $selector);
$this->setSelectors($this->parsedCss, $selectors, $matches[0]);
}
/**
* Sets the structure of nested arrays based on given selectors and merges attributes into the final array.
*
* This method iterates over each selector in the provided list, splits them to extract nested keys,
* then sets up the structure of nested arrays in the $nestedArray based on the nested keys.
* Finally, it merges the attributes into each corresponding level of the final array.
*
* @param array &$nestedArray The reference to the nested array where the structure will be set and attributes merged.
* @param array $selectors An array containing the selectors to be processed.
* @param array $attributes An array containing the attributes to be merged into the final array.
* @return void
*/
private function setSelectors(array &$nestedArray, array $selectors, array $attributes): void
{
foreach ($selectors as $selector) {
$nestedKeys = preg_split('/\s+(?![^()]*\))/', trim($selector));
$this->setStructure($nestedArray, $nestedKeys, $attributes);
}
}
/**
* Sets the structure of nested arrays based on given keys and merges attributes into the final array.
*
* This method traverses the nested array structure based on the provided keys and ensures that each
* nested level exists. Once the structure is set up, it merges the attributes into the final array.
*
* @param array &$nestedArray The reference to the nested array where the structure will be set.
* @param array $nestedKeys An array containing the keys representing the nested structure.
* @param array $attributes An array containing the attributes to be merged into the final array.
* @return void
*/
private function setStructure(array &$nestedArray, array $nestedKeys, array $attributes): void
{
foreach ($nestedKeys as $key) {
if (!isset($nestedArray[$key])) {
$nestedArray[$key] = [];
}
$nestedArray = &$nestedArray[$key];
}
$this->mergeAttributes($nestedArray, $attributes);
}
/**
* Merges attributes into the nested array.
*
* This method merges the provided attributes into the nested array.
* If the 'colorsOnly' flag is set, it merges only color-related attributes.
*
* @param array &$nestedArray A reference to the nested array to merge attributes into.
* @param array $attributes The array of attributes to merge into the nested array.
* @return void
*/
private function mergeAttributes(array &$nestedArray, array $attributes): void
{
if ($this->colorsOnly) {
$this->setColors($nestedArray, $attributes);
return;
}
foreach ($attributes as $attribute) {
$attribute = trim($attribute);
if (!empty($attribute)) {
$parsedAttributes = $this->setAttribute($attribute);
if (!empty($parsedAttributes)) {
$nestedArray = array_merge($nestedArray, $parsedAttributes);
}
}
}
}
/**
* Sets color-related attributes in the nested array.
*
* This method filters the provided attributes to include only those related to colors
* (e.g., color, gradient, border, outline, shadow, fill, stroke). It then merges these
* color-related attributes into the nested array.
*
* @param array &$nestedArray A reference to the nested array to merge color attributes into.
* @param array $attributes The array of attributes to filter and merge into the nested array.
* @return void
*/
private function setColors(array &$nestedArray, array $attributes): void
{
$colorAttributes = $this->colorsOnlyFilter($attributes);
foreach ($colorAttributes as $colorAttribute) {
$colorAttribute = trim($colorAttribute);
if (!empty($colorAttribute)) {
$colorAttributesArray = $this->setAttribute($colorAttribute);
$nestedArray = array_merge($nestedArray, $colorAttributesArray);
}
}
}
/**
* Filters an array of CSS attributes to include only those related to colors, gradients,
* borders, outlines, shadows, fills, and strokes.
*
* This method applies a filter function to the array of attributes, retaining only those
* attributes that match specific patterns related to colors and related properties.
* Attributes containing certain exclusions, such as box-sizing, border-box, radius,
* 'none', or ': 0', are also excluded from the result.
*
* @param array $attributes An array of CSS attributes to be filtered.
* @return array The filtered array containing only attributes related to colors, gradients, borders, outlines, shadows, fills, and strokes.
*/
private function colorsOnlyFilter(array $attributes)
{
return array_filter($attributes, function ($attribute) {
return preg_match('/color|gradient|border|outline|shadow|fill|stroke/i', $attribute)
&& !preg_match('/^box-sizing|border-box|radius/i', $attribute)
&& !preg_match('/\bnone\b/i', $attribute)
&& !preg_match('/\b: 0$/i', trim($attribute));
});
}
/**
* Parses at-rules from the given match.
*
* This method extracts at-rules from the provided match string,
* then delegates the parsing of each at-rule to the appropriate method.
*
* @param string $match The string containing at-rules to be parsed.
* @return void
*/
private function parseAtRules(string $match): void
{
$atRule = trim($match);
$atRule = explode("@", $atRule);
foreach (array_slice($atRule, 1) as $rule) {
$rule = trim($rule);
preg_match('/^([^{}]+)\s*{?(.*)}?$/s', $rule, $ruleMatches);
if (isset($ruleMatches[2]) && !empty($ruleMatches[2])) {
$this->delegateAtRuleBlock($ruleMatches);
} else {
$this->parseAtRuleLine($rule);
}
}
}
/**
* Delegates the processing of an at-rule block based on its content.
*
* @param array $matches An array containing the matched at-rule name and content.
* The first element is the at-rule name, and the second element
* is the content of the at-rule block.
* @return void
*/
private function delegateAtRuleBlock(array $matches): void
{
$atRuleName = trim($matches[1]);
$atRuleContent = trim($matches[2]);
if(strpos($atRuleContent, '{'))
{
$this->prepareAtRuleBlock($atRuleName, $atRuleContent);
} else {
$this->parseAtRuleBlock($atRuleName, $atRuleContent);
}
}
/**
* Prepares an at-rule block for further processing by parsing the at-rule name
* and associating the content with the appropriate type and name in the parsed CSS data.
*
* @param string $atRuleName The name of the at-rule.
* @param string $atRuleContent The content of the at-rule block.
* @return void
*/
private function prepareAtRuleBlock(string $atRuleName, string $atRuleContent): void
{
list($type, $name) = preg_split('/\s+(?=\w+\s)*/', $atRuleName, 2, PREG_SPLIT_DELIM_CAPTURE);
$this->parsedCss[$this->atRulesKey][$type][$name] = $this->setAtRuleContent($atRuleContent);
}
/**
* Parses the content of an at-rule block and returns an array of parsed declarations.
*
* @param string $atRuleContent The content of the at-rule block.
* @return array An array containing the parsed declarations extracted from the at-rule content.
*/
private function setAtRuleContent(string $atRuleContent): array
{
$return = [];
$parts = explode('}', $atRuleContent);
foreach($parts as $part) {
if(!empty($part))
{
$this->splitAtRulePart($return, $part);
}
}
return $return;
}
/**
* Splits an at-rule part into its name and content and adds it to the return array.
*
* @param array &$return The array to which the parsed at-rule part will be added.
* @param string $part The at-rule part to be split and parsed.
* @return void
*/
private function splitAtRulePart(array &$return, string $part): void
{
$parts = preg_split('/\s*({|})\s*/', trim($part), -1, PREG_SPLIT_DELIM_CAPTURE);
if(isset($parts[0]) && isset($parts[2]))
{
$this->setAtRulePart($return, $parts);
}
}
/**
* Parses and merges attributes of an at-rule part into the return array, considering multiple selectors.
*
* This method parses the attributes of an at-rule part and merges them into the return array,
* considering multiple selectors separated by commas. It splits the selectors to extract nested keys,
* then sets the structure of nested arrays and merges attributes into each corresponding level.
*
* @param array &$return The array to which the parsed at-rule part attributes will be merged.
* @param array $parts An array containing the split parts of the at-rule content.
* The first element is the comma-separated list of selectors, and the second element
* is the content of the at-rule part.
* @return void
*/
private function setAtRulePart(array &$return, array $parts): void
{
$attributes = explode(';', $parts[2]);
$selectors = preg_split('/,(?![^()]*\))/', $parts[0]);
$this->setSelectors($return, $selectors, $attributes);
}
/**
* Parses an at-rule block from the given matches.
*
* This method extracts an at-rule block from the provided matches array,
* initializes an empty array for the at-rule in the parsed CSS,
* and iterates over the at-rule values, delegating their parsing to another method.
*
* @param array $matches The array containing matches for the at-rule block.
* @return void
*/
private function parseAtRuleBlock(string $atRuleName, string $atRuleContent): void
{
$atRuleValues = explode(';', $atRuleContent);
$this->parsedCss[$this->atRulesKey][$atRuleName] = [];
foreach ($atRuleValues as $value) {
$this->setAtRuleBlock($value, $atRuleName);
}
}
/**
* Sets attributes for an at-rule block in the parsed CSS.
*
* This method takes a value from the at-rule block, trims it, and parses it into attributes.
* If attributes are found, they are merged into the corresponding at-rule in the parsed CSS.
*
* @param string $value The value from the at-rule block to be parsed.
* @param string $atRuleName The name of the at-rule block in the parsed CSS.
* @return void
*/
private function setAtRuleBlock(string $value, string $atRuleName): void
{
$value = trim($value);
if (!empty($value)) {
$attributes = $this->setAttribute($value);
if (!empty($attributes)) {
$this->parsedCss[$this->atRulesKey][$atRuleName] = array_merge($this->parsedCss[$this->atRulesKey][$atRuleName], $attributes);
}
}
}
/**
* Parses a single-line at-rule and sets its key-value pair in the parsed CSS.
*
* This method takes a single-line at-rule, splits it into key and value parts,
* trims them, and sets the key-value pair in the parsed CSS.
*
* @param string $rule The single-line at-rule to be parsed.
* @return void
*/
private function parseAtRuleLine(string $rule): void
{
$atRuleKeyValue = array_map('trim', explode(' ', $rule, 2));
if (count($atRuleKeyValue) === 2) {
$this->parsedCss[$this->atRulesKey][$atRuleKeyValue[0]] = $atRuleKeyValue[1];
}
}
/**
* Recursively removes empty arrays from the given array.
*
* This method iterates over the elements of the given array recursively.
* If an element is an array, it calls itself recursively to remove empty arrays.
* If an array element is empty after removal, it unsets it from the parent array.
*
* @param array &$array The array from which to remove empty arrays (passed by reference).
* @return void
*/
private function removeEmptyArrays(array &$array): void
{
foreach ($array as $key => &$value) {
if (is_array($value)) {
$this->removeEmptyArrays($value);
if (empty($value)) {
unset($array[$key]);
}
}
}
}
}
This code snippet was published on It was last edited on