Created
September 6, 2025 15:51
-
-
Save celsowm/de30baeee693ac764b9232684ecf3330 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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