Skip to content

Instantly share code, notes, and snippets.

@celsowm
Created September 6, 2025 15:51
Show Gist options
  • Save celsowm/de30baeee693ac764b9232684ecf3330 to your computer and use it in GitHub Desktop.
Save celsowm/de30baeee693ac764b9232684ecf3330 to your computer and use it in GitHub Desktop.
<?php
declare(strict_types=1);
class CssParser
{
/**
* Parseia uma string CSS e retorna uma estrutura hierárquica de regras
*/
public function parse(string $css): array
{
// Remove comentários CSS
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
if ($css === null) {
throw new \RuntimeException('Falha ao processar comentários CSS');
}
// Normaliza quebras de linha
$css = str_replace(["\r\n", "\r"], "\n", $css);
return $this->parseRules($css);
}
/**
* Processa regras CSS recursivamente
*/
private function parseRules(string $css, string $parentSelector = ''): array
{
$rules = [];
$currentBlock = '';
$braceLevel = 0;
$inString = false;
$stringChar = '';
$inComment = false;
$lastChar = '';
// Processa caractere por caractere
$length = strlen($css);
for ($i = 0; $i < $length; $i++) {
$char = $css[$i];
// Verifica contexto de comentário
if (!$inString && !$inComment && $lastChar === '/' && $char === '*') {
$inComment = true;
$currentBlock = substr($currentBlock, 0, -1); // Remove o '/'
$lastChar = '';
continue;
}
if ($inComment && $lastChar === '*' && $char === '/') {
$inComment = false;
$lastChar = '';
continue;
}
if ($inComment) {
$lastChar = $char;
continue;
}
// Verifica contexto de string
if (!$inString && in_array($char, ['"', "'"], true)) {
$inString = true;
$stringChar = $char;
} elseif ($inString && $char === $stringChar && $lastChar !== '\\') {
$inString = false;
}
// Controle de blocos
if (!$inString && !$inComment) {
if ($char === '{') {
$braceLevel++;
} elseif ($char === '}') {
$braceLevel--;
}
}
$currentBlock .= $char;
$lastChar = $char;
// Final de uma regra válida
if ($braceLevel === 0 && trim($currentBlock) !== '') {
$this->processRuleBlock($currentBlock, $rules, $parentSelector);
$currentBlock = '';
}
}
return $rules;
}
/**
* Processa um bloco de regra CSS
*/
private function processRuleBlock(string $block, array &$rules, string $parentSelector): void
{
$block = trim($block);
if ($block === '') {
return;
}
// Trata media queries e outros at-rules
if (preg_match('/^@([a-z-]+)\s*(?:\(([^)]+)\))?\s*\{/', $block, $atRuleMatch)) {
$atRuleType = $atRuleMatch[1];
$atRuleParams = $atRuleMatch[2] ?? '';
// Remove a parte do at-rule para obter o conteúdo
$contentStart = strpos($block, '{') + 1;
$contentEnd = $this->findClosingBrace($block, $contentStart - 1);
if ($contentEnd !== false) {
$atRuleContent = substr($block, $contentStart, $contentEnd - $contentStart);
$nestedRules = $this->parseRules($atRuleContent, $parentSelector);
$rules[] = [
'type' => 'at-rule',
'name' => $atRuleType,
'params' => $atRuleParams,
'rules' => $nestedRules
];
}
return;
}
// Trata regras normais
if (preg_match('/^(.+?)\s*\{(.*)\}\s*$/s', $block, $ruleMatch)) {
$selectorString = trim($ruleMatch[1]);
$declarationBlock = $ruleMatch[2];
// Processa seletores
$selectors = $this->processSelectors($selectorString, $parentSelector);
// Processa declarações
$declarations = $this->parseDeclarations($declarationBlock);
// Adiciona regra
$rules[] = [
'type' => 'rule',
'selectors' => $selectors,
'declarations' => $declarations
];
}
}
/**
* Processa seletores com suporte a aninhamento
*/
private function processSelectors(string $selectorString, string $parentSelector): array
{
$selectors = [];
// Separa múltiplos seletores
$rawSelectors = $this->splitSelectors($selectorString);
foreach ($rawSelectors as $rawSelector) {
$rawSelector = trim($rawSelector);
if ($parentSelector !== '' && preg_match('/&/', $rawSelector)) {
// Substitui & pelo seletor pai
$processed = str_replace('&', $parentSelector, $rawSelector);
$selectors[] = trim($processed);
} elseif ($parentSelector !== '') {
// Aninhamento simples (pai > filho)
$selectors[] = trim($parentSelector . ' ' . $rawSelector);
} else {
$selectors[] = $rawSelector;
}
}
return $selectors;
}
/**
* Separa seletores múltiplos com tratamento inteligente
*/
private function splitSelectors(string $selectorString): array
{
$selectors = [];
$current = '';
$inParens = 0;
$inString = false;
$stringChar = '';
for ($i = 0, $len = strlen($selectorString); $i < $len; $i++) {
$char = $selectorString[$i];
// Trata contexto de string
if (!$inString && in_array($char, ['"', "'"], true)) {
$inString = true;
$stringChar = $char;
} elseif ($inString && $char === $stringChar && $selectorString[$i - 1] !== '\\') {
$inString = false;
}
// Trata parênteses (importantes para pseudo-classes)
if (!$inString) {
if ($char === '(') {
$inParens++;
} elseif ($char === ')') {
$inParens--;
}
}
// Verifica se é um separador de seletores (vírgula)
if (!$inString && $inParens === 0 && $char === ',') {
$selectors[] = $current;
$current = '';
continue;
}
$current .= $char;
}
if (trim($current) !== '') {
$selectors[] = $current;
}
return $selectors;
}
/**
* Encontra a chave de fechamento correspondente
*/
private function findClosingBrace(string $css, int $startIndex): int|false
{
$braceLevel = 0;
$inString = false;
$stringChar = '';
for ($i = $startIndex, $len = strlen($css); $i < $len; $i++) {
$char = $css[$i];
if (!$inString && in_array($char, ['"', "'"], true)) {
$inString = true;
$stringChar = $char;
} elseif ($inString && $char === $stringChar && $css[$i - 1] !== '\\') {
$inString = false;
}
if (!$inString) {
if ($char === '{') {
$braceLevel++;
} elseif ($char === '}') {
$braceLevel--;
if ($braceLevel < 0) {
return $i;
}
}
}
}
return false;
}
/**
* Parseia declarações CSS
*/
public function parseDeclarations(string $declarationBlock): array
{
$declarations = [];
$properties = $this->splitDeclarations($declarationBlock);
foreach ($properties as $property) {
$property = trim($property);
if ($property === '') {
continue;
}
if (strpos($property, ':') !== false) {
[$name, $value] = explode(':', $property, 2);
$name = trim($name);
$value = trim($value);
// Remove ponto e vírgula final se existir
if (str_ends_with($value, ';')) {
$value = rtrim($value, ';');
}
$declarations[$name] = $value;
}
}
return $declarations;
}
/**
* Separa declarações com tratamento inteligente
*/
private function splitDeclarations(string $declarationBlock): array
{
$declarations = [];
$current = '';
$inParens = 0;
$inString = false;
$stringChar = '';
for ($i = 0, $len = strlen($declarationBlock); $i < $len; $i++) {
$char = $declarationBlock[$i];
// Trata contexto de string
if (!$inString && in_array($char, ['"', "'"], true)) {
$inString = true;
$stringChar = $char;
} elseif ($inString && $char === $stringChar && $declarationBlock[$i - 1] !== '\\') {
$inString = false;
}
// Trata parênteses (importantes para funções CSS)
if (!$inString) {
if ($char === '(') {
$inParens++;
} elseif ($char === ')') {
$inParens--;
}
}
// Verifica se é um separador de declarações (ponto e vírgula)
if (!$inString && $inParens === 0 && $char === ';') {
$declarations[] = $current;
$current = '';
continue;
}
$current .= $char;
}
if (trim($current) !== '') {
$declarations[] = $current;
}
return $declarations;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment