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 CssParser {

    /**
     * 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
    {
        $attribute = str_replace('}', '', $attribute);
        $att = array_map("trim", explode(":", trim($attribute)));
        return count($att) === 2 ? [$att[0] => $att[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);
        $selector = trim($parts[0]);
        $attributes = explode(';', $parts[1]);
        $selectors = preg_split('/,(?![^()]*\))/', $selector);
        
        $this->setSelectors($this->parsedCss, $selectors, $attributes);
    }

    /**
     * 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)) {
                $attributes = $this->setAttribute($attribute);

                if (!empty($attributes)) {
                    $nestedArray = array_merge($nestedArray, $attributes);
                }
            }
        }
    }

    /**
     * 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

Contributing Authors

1

3 Comments

  • Votes
  • Oldest
  • Latest
BO
443 9
Commented
Updated

Fixed the media query parsing, it also takes the $colorsOnly flag into account.

Wrote the class for a personal project I'm working on which has to do with colors in a CSS file and thought I'd share the class here.

$parser = new CssParser();

$parser->setCssFromFile($file)->colorsOnly(true)->includeMediaQueries(true)->parseCss();

echo "<pre>";
print_r($parser->parsed());

colorsOnly and includeMediaQueries are optional configurations, both of which are false by default.

  • 1
    Happy to say I fixed the at-rules bug. It now parses @keyframes, @supports and all of the at-rules properly (to the best of my knowledge). Only thing is, it doesn't take the $colorsOnly into account when it comes to at-rules. — Bogey
  • 0
    To the best of my knowledge, everything is now fixed. — Bogey
add a comment
0
Commented
Updated

Did you write this completely yourself? Or did you borrow ideas from another? Mainly just curious, but great job using single responsibility with the different methods utilized here. Good job with the PHPDoc comments too!

Also, did you have some sample output of what you might expect to see for the $parser->parsed() result?

add a comment
0
BO
443 9
Commented
Updated
.col-1 {
    flex: 0 0 auto;
    width: 8.33333333%;
}

.col-2 {
    flex: 0 0 auto;
    width: 16.66666667%;
}

.col-3 {
    width: 25%;
    flex: 0 0 auto;
}

.col-4 {
    flex: 0 0 auto;
    width: 33.33333333%;
}

.col-5 {
    flex: 0 0 auto;
    width: 41.66666667%;
}

.col-6 {
    width: 50%;
    flex: 0 0 auto;
}

.col-7 {
    flex: 0 0 auto;
    width: 58.33333333%;
}

.col-8 {
    flex: 0 0 auto;
    width: 66.66666667%;
}

.col-9 {
    width: 75%;
    flex: 0 0 auto;
}

.col-10 {
    flex: 0 0 auto;
    width: 83.33333333%;
}

.col-11 {
    flex: 0 0 auto;
    width: 91.66666667%;
}

.col-12 {
    width: 100%;
    flex: 0 0 auto;
}

.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-10,
.col-11,
.col-12 {
    flex-grow: 0;
    box-sizing: border-box;
}

Would parse into

    [.col-1] => Array
        (
            [flex] => 0 0 auto
            [width] => 8.33333333%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-2] => Array
        (
            [flex] => 0 0 auto
            [width] => 16.66666667%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-3] => Array
        (
            [width] => 25%
            [flex] => 0 0 auto
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-4] => Array
        (
            [flex] => 0 0 auto
            [width] => 33.33333333%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-5] => Array
        (
            [flex] => 0 0 auto
            [width] => 41.66666667%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-6] => Array
        (
            [width] => 50%
            [flex] => 0 0 auto
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-7] => Array
        (
            [flex] => 0 0 auto
            [width] => 58.33333333%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-8] => Array
        (
            [flex] => 0 0 auto
            [width] => 66.66666667%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-9] => Array
        (
            [width] => 75%
            [flex] => 0 0 auto
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-10] => Array
        (
            [flex] => 0 0 auto
            [width] => 83.33333333%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-11] => Array
        (
            [flex] => 0 0 auto
            [width] => 91.66666667%
            [flex-grow] => 0
            [box-sizing] => border-box
        )

    [.col-12] => Array
        (
            [width] => 100%
            [flex] => 0 0 auto
            [flex-grow] => 0
            [box-sizing] => border-box
        )

The following sample CSS

.comments {
    display: flex;
    flex-direction: column;
}

.comments .comment {
    display: flex;
    margin-left: 5rem;
    margin-block: 0.5rem;
    flex-direction: column;
}

.comments .comment .comment-avatar {
    display: flex;
    padding: 0.25rem;
    flex-direction: column;
}

.comments .comment .comment-avatar img {
    width: 100px;
    height: 100px;
    border-radius: 25%;
}

.comments .comment .comment-body {
    display: flex;
    flex-direction: row;
    padding-block: 0.5rem;
    border-radius: 0.75rem;
}

.comments .comment .comment-body .comment-card {
    display: flex;
    flex: 1 1 100%;
    flex-direction: column;
}

.comments .comment .comment-body .comment-card .comment-update {
    display: none;
}

.comments .comment .comment-body .comment-card .comment-actions {
    width: 100%;
    display: flex;
    align-items: center;
    padding-inline: 0.5rem;
    box-sizing: border-box;
    justify-content: space-between;
}

.comments .comment .comment-body .comment-card .comment-actions .comment-voted {
    padding-inline: -2px;
    color: rgba(255, 153, 0, 0.8);
}

.comments .comment .comment-body .comment-card .comment-actions .left-actions {
    padding-block: 0.65rem;
    background-color: rgba(0, 111, 175, 0.2);
}

.comments .comment .comment-body .comment-card .comment-actions .left-actions p {
    margin: 0;
    padding-inline: 0.75rem;
}

.comments .comment .comment-body .comment-card .comment-actions .left-actions,
.comments .comment .comment-body .comment-card .comment-actions .right-actions {
    display: flex;
    align-items: center;
    border-radius: 0.75rem;
}

.comments .comment .comment-body .comment-card .comment-actions .left-actions ul {
    flex-direction: column;
}

.comments .comment .comment-body .comment-card .comment-actions .left-actions ul,
.comments .comment .comment-body .comment-card .comment-actions .right-actions ul {
    display: flex;
    list-style-type: none;
}

.comments .comment .comment-body .comment-card .comment-actions .left-actions li {
    padding-inline: 0.25rem;
}

.comments .comment .comment-body .comment-card .comment-actions .left-actions li a,
.comments .comment .comment-body .comment-card .comment-actions .right-actions li a,
.comments .comment .comment-body .comment-card .comment-actions .right-actions li a .btn-link {
    display: block;
    text-decoration: none;
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions ul li a,
.comments .comment .comment-body .comment-card .comment-actions .right-actions ul li a .btn-link {
    padding: 0.65rem;
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions ul {
    text-align: end;
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions ul.comment-reply {
    background-color: rgba(0, 111, 175, 0.2);
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions ul.toggleable {
    opacity: 0;
    visibility: hidden;
    transition: opacity 1s, visibility 1s;
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions ul.toggleable.visible {
    opacity: 1;
    visibility: visible;
    transition: opacity 1s, visibility 1s;
    background-color: rgba(0, 111, 175, 0.2);
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions ul.toggleable.visible li:hover{
    background-color: rgba(0, 63, 99, 0.75);
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions ul.toggleable.visible li:first-child{
    border-top-left-radius: 0.75rem;
    border-bottom-left-radius: 0.75rem;
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions .comment-toggle,
.comments .comment .comment-body .comment-card .comment-actions .right-actions .comment-toggle a {
    position: relative;
    padding-block: 0.55rem;
    padding-inline: 0.25rem;
    border-top-left-radius: 0.75rem;
    border-bottom-left-radius: 0.75rem;
    background-color: rgb(0, 70, 111);
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions .btn-link {
    line-height: 1.5em;
}

.comments .comment .comment-body .comment-card .comment-actions .right-actions .active,
.comments .comment .comment-body .comment-card .comment-actions .right-actions .active a {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    color: rgba(255, 153, 0, 0.7);
    background-color: rgb(0, 40, 63);
}

.comments .comment .comment-body .comment-card .comment-actions .comment-vote {
    padding-right: 0.75rem;
}

.comments .comment .comment-body .comment-card .comment-content {
    flex-grow: 1;
    padding: 0.25rem;
}

.comments .comment .comment-body .comment-card .comment-content .the-comment {
    display: block;
}

.comments .comment .comment-body .comment-card .post-info {
    margin-top: 0rem;
}

Is going to parse into if onlyColors are set to false

    [.comments] => Array
        (
            [display] => flex
            [flex-direction] => column
            [.comment] => Array
                (
                    [display] => flex
                    [margin-left] => 5rem
                    [margin-block] => 0.5rem
                    [flex-direction] => column
                    [.comment-avatar] => Array
                        (
                            [display] => flex
                            [padding] => 0.25rem
                            [flex-direction] => column
                            [img] => Array
                                (
                                    [width] => 100px
                                    [height] => 100px
                                    [border-radius] => 25%
                                )

                        )

                    [.comment-body] => Array
                        (
                            [display] => flex
                            [flex-direction] => row
                            [padding-block] => 0.5rem
                            [border-radius] => 0.75rem
                            [.comment-card] => Array
                                (
                                    [display] => flex
                                    [flex] => 1 1 100%
                                    [flex-direction] => column
                                    [.comment-update] => Array
                                        (
                                            [display] => none
                                        )

                                    [.comment-actions] => Array
                                        (
                                            [width] => 100%
                                            [display] => flex
                                            [align-items] => center
                                            [padding-inline] => 0.5rem
                                            [box-sizing] => border-box
                                            [justify-content] => space-between
                                            [.comment-voted] => Array
                                                (
                                                    [padding-inline] => -2px
                                                    [color] => rgba(255, 153, 0, 0.8)
                                                )

                                            [.left-actions] => Array
                                                (
                                                    [padding-block] => 0.65rem
                                                    [background-color] => rgba(0, 111, 175, 0.2)
                                                    [p] => Array
                                                        (
                                                            [margin] => 0
                                                            [padding-inline] => 0.75rem
                                                        )

                                                    [display] => flex
                                                    [align-items] => center
                                                    [border-radius] => 0.75rem
                                                    [ul] => Array
                                                        (
                                                            [flex-direction] => column
                                                            [display] => flex
                                                            [list-style-type] => none
                                                        )

                                                    [li] => Array
                                                        (
                                                            [padding-inline] => 0.25rem
                                                            [a] => Array
                                                                (
                                                                    [display] => block
                                                                    [text-decoration] => none
                                                                )

                                                        )

                                                )

                                            [.right-actions] => Array
                                                (
                                                    [display] => flex
                                                    [align-items] => center
                                                    [border-radius] => 0.75rem
                                                    [ul] => Array
                                                        (
                                                            [display] => flex
                                                            [list-style-type] => none
                                                            [li] => Array
                                                                (
                                                                    [a] => Array
                                                                        (
                                                                            [padding] => 0.65rem
                                                                            [.btn-link] => Array
                                                                                (
                                                                                    [padding] => 0.65rem
                                                                                )

                                                                        )

                                                                )

                                                            [text-align] => end
                                                        )

                                                    [li] => Array
                                                        (
                                                            [a] => Array
                                                                (
                                                                    [display] => block
                                                                    [text-decoration] => none
                                                                    [.btn-link] => Array
                                                                        (
                                                                            [display] => block
                                                                            [text-decoration] => none
                                                                        )

                                                                )

                                                        )

                                                    [ul.comment-reply] => Array
                                                        (
                                                            [background-color] => rgba(0, 111, 175, 0.2)
                                                        )

                                                    [ul.toggleable] => Array
                                                        (
                                                            [opacity] => 0
                                                            [visibility] => hidden
                                                            [transition] => opacity 1s, visibility 1s
                                                        )

                                                    [ul.toggleable.visible] => Array
                                                        (
                                                            [opacity] => 1
                                                            [visibility] => visible
                                                            [transition] => opacity 1s, visibility 1s
                                                            [background-color] => rgba(0, 111, 175, 0.2)
                                                            [li:hover] => Array
                                                                (
                                                                    [background-color] => rgba(0, 63, 99, 0.75)
                                                                )

                                                            [li:first-child] => Array
                                                                (
                                                                    [border-top-left-radius] => 0.75rem
                                                                    [border-bottom-left-radius] => 0.75rem
                                                                )

                                                        )

                                                    [.comment-toggle] => Array
                                                        (
                                                            [position] => relative
                                                            [padding-block] => 0.55rem
                                                            [padding-inline] => 0.25rem
                                                            [border-top-left-radius] => 0.75rem
                                                            [border-bottom-left-radius] => 0.75rem
                                                            [background-color] => rgb(0, 70, 111)
                                                            [a] => Array
                                                                (
                                                                    [position] => relative
                                                                    [padding-block] => 0.55rem
                                                                    [padding-inline] => 0.25rem
                                                                    [border-top-left-radius] => 0.75rem
                                                                    [border-bottom-left-radius] => 0.75rem
                                                                    [background-color] => rgb(0, 70, 111)
                                                                )

                                                        )

                                                    [.btn-link] => Array
                                                        (
                                                            [line-height] => 1.5em
                                                        )

                                                    [.active] => Array
                                                        (
                                                            [border-top-left-radius] => 0
                                                            [border-bottom-left-radius] => 0
                                                            [color] => rgba(255, 153, 0, 0.7)
                                                            [background-color] => rgb(0, 40, 63)
                                                            [a] => Array
                                                                (
                                                                    [border-top-left-radius] => 0
                                                                    [border-bottom-left-radius] => 0
                                                                    [color] => rgba(255, 153, 0, 0.7)
                                                                    [background-color] => rgb(0, 40, 63)
                                                                )

                                                        )

                                                )

                                            [.comment-vote] => Array
                                                (
                                                    [padding-right] => 0.75rem
                                                )

                                        )

                                    [.comment-content] => Array
                                        (
                                            [flex-grow] => 1
                                            [padding] => 0.25rem
                                            [.the-comment] => Array
                                                (
                                                    [display] => block
                                                )

                                        )

                                    [.post-info] => Array
                                        (
                                            [margin-top] => 0rem
                                        )

                                )

                        )

                )

        )

But if you enable onlyColors flag

    [.comments] => Array
        (
            [.comment] => Array
                (
                    [.comment-body] => Array
                        (
                            [.comment-card] => Array
                                (
                                    [.comment-actions] => Array
                                        (
                                            [.comment-voted] => Array
                                                (
                                                    [color] => rgba(255, 153, 0, 0.8)
                                                )

                                            [.left-actions] => Array
                                                (
                                                    [background-color] => rgba(0, 111, 175, 0.2)
                                                )

                                            [.right-actions] => Array
                                                (
                                                    [ul.comment-reply] => Array
                                                        (
                                                            [background-color] => rgba(0, 111, 175, 0.2)
                                                        )

                                                    [ul.toggleable.visible] => Array
                                                        (
                                                            [background-color] => rgba(0, 111, 175, 0.2)
                                                            [li:hover] => Array
                                                                (
                                                                    [background-color] => rgba(0, 63, 99, 0.75)
                                                                )

                                                        )

                                                    [.comment-toggle] => Array
                                                        (
                                                            [background-color] => rgb(0, 70, 111)
                                                            [a] => Array
                                                                (
                                                                    [background-color] => rgb(0, 70, 111)
                                                                )

                                                        )

                                                    [.active] => Array
                                                        (
                                                            [color] => rgba(255, 153, 0, 0.7)
                                                            [background-color] => rgb(0, 40, 63)
                                                            [a] => Array
                                                                (
                                                                    [color] => rgba(255, 153, 0, 0.7)
                                                                    [background-color] => rgb(0, 40, 63)
                                                                )

                                                        )

                                                )

                                        )

                                )

                        )

                )

        )

ChatGPT helped me, mostly with the regex stuff; but otherwise, my work.

add a comment
1