8889841csrc/Rule/Rule.php000066600000023660150513463720007703 0ustar00 */ private $aIeHack; /** * @var int */ protected $iLineNo; /** * @var int */ protected $iColNo; /** * @var array */ protected $aComments; /** * @param string $sRule * @param int $iLineNo * @param int $iColNo */ public function __construct($sRule, $iLineNo = 0, $iColNo = 0) { $this->sRule = $sRule; $this->mValue = null; $this->bIsImportant = false; $this->aIeHack = []; $this->iLineNo = $iLineNo; $this->iColNo = $iColNo; $this->aComments = []; } /** * @return Rule * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $aComments = $oParserState->consumeWhiteSpace(); $oRule = new Rule( $oParserState->parseIdentifier(!$oParserState->comes("--")), $oParserState->currentLine(), $oParserState->currentColumn() ); $oRule->setComments($aComments); $oRule->addComments($oParserState->consumeWhiteSpace()); $oParserState->consume(':'); $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule())); $oRule->setValue($oValue); if ($oParserState->getSettings()->bLenientParsing) { while ($oParserState->comes('\\')) { $oParserState->consume('\\'); $oRule->addIeHack($oParserState->consume()); $oParserState->consumeWhiteSpace(); } } $oParserState->consumeWhiteSpace(); if ($oParserState->comes('!')) { $oParserState->consume('!'); $oParserState->consumeWhiteSpace(); $oParserState->consume('important'); $oRule->setIsImportant(true); } $oParserState->consumeWhiteSpace(); while ($oParserState->comes(';')) { $oParserState->consume(';'); } $oParserState->consumeWhiteSpace(); return $oRule; } /** * @param string $sRule * * @return array */ private static function listDelimiterForRule($sRule) { if (preg_match('/^font($|-)/', $sRule)) { return [',', '/', ' ']; } return [',', ' ', '/']; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * @return int */ public function getColNo() { return $this->iColNo; } /** * @param int $iLine * @param int $iColumn * * @return void */ public function setPosition($iLine, $iColumn) { $this->iColNo = $iColumn; $this->iLineNo = $iLine; } /** * @param string $sRule * * @return void */ public function setRule($sRule) { $this->sRule = $sRule; } /** * @return string */ public function getRule() { return $this->sRule; } /** * @return RuleValueList|null */ public function getValue() { return $this->mValue; } /** * @param RuleValueList|null $mValue * * @return void */ public function setValue($mValue) { $this->mValue = $mValue; } /** * @param array> $aSpaceSeparatedValues * * @return RuleValueList * * @deprecated will be removed in version 9.0 * Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. * Use `setValue()` instead and wrap the value inside a RuleValueList if necessary. */ public function setValues(array $aSpaceSeparatedValues) { $oSpaceSeparatedList = null; if (count($aSpaceSeparatedValues) > 1) { $oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo); } foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) { $oCommaSeparatedList = null; if (count($aCommaSeparatedValues) > 1) { $oCommaSeparatedList = new RuleValueList(',', $this->iLineNo); } foreach ($aCommaSeparatedValues as $mValue) { if (!$oSpaceSeparatedList && !$oCommaSeparatedList) { $this->mValue = $mValue; return $mValue; } if ($oCommaSeparatedList) { $oCommaSeparatedList->addListComponent($mValue); } else { $oSpaceSeparatedList->addListComponent($mValue); } } if (!$oSpaceSeparatedList) { $this->mValue = $oCommaSeparatedList; return $oCommaSeparatedList; } else { $oSpaceSeparatedList->addListComponent($oCommaSeparatedList); } } $this->mValue = $oSpaceSeparatedList; return $oSpaceSeparatedList; } /** * @return array> * * @deprecated will be removed in version 9.0 * Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. * Use `getValue()` instead and check for the existence of a (nested set of) ValueList object(s). */ public function getValues() { if (!$this->mValue instanceof RuleValueList) { return [[$this->mValue]]; } if ($this->mValue->getListSeparator() === ',') { return [$this->mValue->getListComponents()]; } $aResult = []; foreach ($this->mValue->getListComponents() as $mValue) { if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { $aResult[] = [$mValue]; continue; } if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { $aResult[] = []; } foreach ($mValue->getListComponents() as $mValue) { $aResult[count($aResult) - 1][] = $mValue; } } return $aResult; } /** * Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type. * Otherwise, the existing value will be wrapped by one. * * @param RuleValueList|array $mValue * @param string $sType * * @return void */ public function addValue($mValue, $sType = ' ') { if (!is_array($mValue)) { $mValue = [$mValue]; } if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { $mCurrentValue = $this->mValue; $this->mValue = new RuleValueList($sType, $this->iLineNo); if ($mCurrentValue) { $this->mValue->addListComponent($mCurrentValue); } } foreach ($mValue as $mValueItem) { $this->mValue->addListComponent($mValueItem); } } /** * @param int $iModifier * * @return void */ public function addIeHack($iModifier) { $this->aIeHack[] = $iModifier; } /** * @param array $aModifiers * * @return void */ public function setIeHack(array $aModifiers) { $this->aIeHack = $aModifiers; } /** * @return array */ public function getIeHack() { return $this->aIeHack; } /** * @param bool $bIsImportant * * @return void */ public function setIsImportant($bIsImportant) { $this->bIsImportant = $bIsImportant; } /** * @return bool */ public function getIsImportant() { return $this->bIsImportant; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; if ($this->mValue instanceof Value) { //Can also be a ValueList $sResult .= $this->mValue->render($oOutputFormat); } else { $sResult .= $this->mValue; } if (!empty($this->aIeHack)) { $sResult .= ' \\' . implode('\\', $this->aIeHack); } if ($this->bIsImportant) { $sResult .= ' !important'; } $sResult .= ';'; return $sResult; } /** * @param array $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array */ public function getComments() { return $this->aComments; } /** * @param array $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } src/Comment/Commentable.php000066600000000704150513463720011707 0ustar00 $aComments * * @return void */ public function addComments(array $aComments); /** * @return array */ public function getComments(); /** * @param array $aComments * * @return void */ public function setComments(array $aComments); } src/Comment/Comment.php000066600000002215150513463720011062 0ustar00sComment = $sComment; $this->iLineNo = $iLineNo; } /** * @return string */ public function getComment() { return $this->sComment; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * @param string $sComment * * @return void */ public function setComment($sComment) { $this->sComment = $sComment; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return '/*' . $this->sComment . '*/'; } } src/OutputFormat.php000066600000017233150513463720010535 0ustar00set('Space*Rules', "\n");`) */ public $sSpaceAfterRuleName = ' '; /** * @var string */ public $sSpaceBeforeRules = ''; /** * @var string */ public $sSpaceAfterRules = ''; /** * @var string */ public $sSpaceBetweenRules = ''; /** * @var string */ public $sSpaceBeforeBlocks = ''; /** * @var string */ public $sSpaceAfterBlocks = ''; /** * @var string */ public $sSpaceBetweenBlocks = "\n"; /** * Content injected in and around at-rule blocks. * * @var string */ public $sBeforeAtRuleBlock = ''; /** * @var string */ public $sAfterAtRuleBlock = ''; /** * This is what’s printed before and after the comma if a declaration block contains multiple selectors. * * @var string */ public $sSpaceBeforeSelectorSeparator = ''; /** * @var string */ public $sSpaceAfterSelectorSeparator = ' '; /** * This is what’s printed after the comma of value lists * * @var string */ public $sSpaceBeforeListArgumentSeparator = ''; /** * @var string */ public $sSpaceAfterListArgumentSeparator = ''; /** * @var string */ public $sSpaceBeforeOpeningBrace = ' '; /** * Content injected in and around declaration blocks. * * @var string */ public $sBeforeDeclarationBlock = ''; /** * @var string */ public $sAfterDeclarationBlockSelectors = ''; /** * @var string */ public $sAfterDeclarationBlock = ''; /** * Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. * * @var string */ public $sIndentation = "\t"; /** * Output exceptions. * * @var bool */ public $bIgnoreExceptions = false; /** * @var OutputFormatter|null */ private $oFormatter = null; /** * @var OutputFormat|null */ private $oNextLevelFormat = null; /** * @var int */ private $iIndentationLevel = 0; public function __construct() { } /** * @param string $sName * * @return string|null */ public function get($sName) { $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; foreach ($aVarPrefixes as $sPrefix) { $sFieldName = $sPrefix . ucfirst($sName); if (isset($this->$sFieldName)) { return $this->$sFieldName; } } return null; } /** * @param array|string $aNames * @param mixed $mValue * * @return self|false */ public function set($aNames, $mValue) { $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; if (is_string($aNames) && strpos($aNames, '*') !== false) { $aNames = [ str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames), ]; } elseif (!is_array($aNames)) { $aNames = [$aNames]; } foreach ($aVarPrefixes as $sPrefix) { $bDidReplace = false; foreach ($aNames as $sName) { $sFieldName = $sPrefix . ucfirst($sName); if (isset($this->$sFieldName)) { $this->$sFieldName = $mValue; $bDidReplace = true; } } if ($bDidReplace) { return $this; } } // Break the chain so the user knows this option is invalid return false; } /** * @param string $sMethodName * @param array $aArguments * * @return mixed * * @throws \Exception */ public function __call($sMethodName, array $aArguments) { if (strpos($sMethodName, 'set') === 0) { return $this->set(substr($sMethodName, 3), $aArguments[0]); } elseif (strpos($sMethodName, 'get') === 0) { return $this->get(substr($sMethodName, 3)); } elseif (method_exists(OutputFormatter::class, $sMethodName)) { return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments); } else { throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName); } } /** * @param int $iNumber * * @return self */ public function indentWithTabs($iNumber = 1) { return $this->setIndentation(str_repeat("\t", $iNumber)); } /** * @param int $iNumber * * @return self */ public function indentWithSpaces($iNumber = 2) { return $this->setIndentation(str_repeat(" ", $iNumber)); } /** * @return OutputFormat */ public function nextLevel() { if ($this->oNextLevelFormat === null) { $this->oNextLevelFormat = clone $this; $this->oNextLevelFormat->iIndentationLevel++; $this->oNextLevelFormat->oFormatter = null; } return $this->oNextLevelFormat; } /** * @return void */ public function beLenient() { $this->bIgnoreExceptions = true; } /** * @return OutputFormatter */ public function getFormatter() { if ($this->oFormatter === null) { $this->oFormatter = new OutputFormatter($this); } return $this->oFormatter; } /** * @return int */ public function level() { return $this->iIndentationLevel; } /** * Creates an instance of this class without any particular formatting settings. * * @return self */ public static function create() { return new OutputFormat(); } /** * Creates an instance of this class with a preset for compact formatting. * * @return self */ public static function createCompact() { $format = self::create(); $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('') ->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); return $format; } /** * Creates an instance of this class with a preset for pretty formatting. * * @return self */ public static function createPretty() { $format = self::create(); $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n") ->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']); return $format; } } src/Value/Color.php000066600000013230150513463720010207 0ustar00 $aColor * @param int $iLineNo */ public function __construct(array $aColor, $iLineNo = 0) { parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); } /** * @return Color|CSSFunction * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $aColor = []; if ($oParserState->comes('#')) { $oParserState->consume('#'); $sValue = $oParserState->parseIdentifier(false); if ($oParserState->strlen($sValue) === 3) { $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; } elseif ($oParserState->strlen($sValue) === 4) { $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3]; } if ($oParserState->strlen($sValue) === 8) { $aColor = [ 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), 'a' => new Size( round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine() ), ]; } else { $aColor = [ 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), ]; } } else { $sColorMode = $oParserState->parseIdentifier(true); $oParserState->consumeWhiteSpace(); $oParserState->consume('('); $bContainsVar = false; $iLength = $oParserState->strlen($sColorMode); for ($i = 0; $i < $iLength; ++$i) { $oParserState->consumeWhiteSpace(); if ($oParserState->comes('var')) { $aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState); $bContainsVar = true; } else { $aColor[$sColorMode[$i]] = Size::parse($oParserState, true); } if ($bContainsVar && $oParserState->comes(')')) { // With a var argument the function can have fewer arguments break; } $oParserState->consumeWhiteSpace(); if ($i < ($iLength - 1)) { $oParserState->consume(','); } } $oParserState->consume(')'); if ($bContainsVar) { return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine()); } } return new Color($aColor, $oParserState->currentLine()); } /** * @param float $fVal * @param float $fFromMin * @param float $fFromMax * @param float $fToMin * @param float $fToMax * * @return float */ private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) { $fFromRange = $fFromMax - $fFromMin; $fToRange = $fToMax - $fToMin; $fMultiplier = $fToRange / $fFromRange; $fNewVal = $fVal - $fFromMin; $fNewVal *= $fMultiplier; return $fNewVal + $fToMin; } /** * @return array */ public function getColor() { return $this->aComponents; } /** * @param array $aColor * * @return void */ public function setColor(array $aColor) { $this->setName(implode('', array_keys($aColor))); $this->aComponents = $aColor; } /** * @return string */ public function getColorDescription() { return $this->getName(); } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { // Shorthand RGB color values if ($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') { $sResult = sprintf( '%02x%02x%02x', $this->aComponents['r']->getSize(), $this->aComponents['g']->getSize(), $this->aComponents['b']->getSize() ); return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); } return parent::render($oOutputFormat); } } src/Value/CSSString.php000066600000005247150513463720010761 0ustar00sString = $sString; parent::__construct($iLineNo); } /** * @return CSSString * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $sBegin = $oParserState->peek(); $sQuote = null; if ($sBegin === "'") { $sQuote = "'"; } elseif ($sBegin === '"') { $sQuote = '"'; } if ($sQuote !== null) { $oParserState->consume($sQuote); } $sResult = ""; $sContent = null; if ($sQuote === null) { // Unquoted strings end in whitespace or with braces, brackets, parentheses while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { $sResult .= $oParserState->parseCharacter(false); } } else { while (!$oParserState->comes($sQuote)) { $sContent = $oParserState->parseCharacter(false); if ($sContent === null) { throw new SourceException( "Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine() ); } $sResult .= $sContent; } $oParserState->consume($sQuote); } return new CSSString($sResult, $oParserState->currentLine()); } /** * @param string $sString * * @return void */ public function setString($sString) { $this->sString = $sString; } /** * @return string */ public function getString() { return $this->sString; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sString = addslashes($this->sString); $sString = str_replace("\n", '\A', $sString); return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); } } src/Value/CalcFunction.php000066600000006520150513463720011505 0ustar00consumeUntil('(', false, true)); $oCalcList = new CalcRuleValueList($oParserState->currentLine()); $oList = new RuleValueList(',', $oParserState->currentLine()); $iNestingLevel = 0; $iLastComponentType = null; while (!$oParserState->comes(')') || $iNestingLevel > 0) { $oParserState->consumeWhiteSpace(); if ($oParserState->comes('(')) { $iNestingLevel++; $oCalcList->addListComponent($oParserState->consume(1)); $oParserState->consumeWhiteSpace(); continue; } elseif ($oParserState->comes(')')) { $iNestingLevel--; $oCalcList->addListComponent($oParserState->consume(1)); $oParserState->consumeWhiteSpace(); continue; } if ($iLastComponentType != CalcFunction::T_OPERAND) { $oVal = Value::parsePrimitiveValue($oParserState); $oCalcList->addListComponent($oVal); $iLastComponentType = CalcFunction::T_OPERAND; } else { if (in_array($oParserState->peek(), $aOperators)) { if (($oParserState->comes('-') || $oParserState->comes('+'))) { if ( $oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ ')) ) { throw new UnexpectedTokenException( " {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine() ); } } $oCalcList->addListComponent($oParserState->consume(1)); $iLastComponentType = CalcFunction::T_OPERATOR; } else { throw new UnexpectedTokenException( sprintf( 'Next token was expected to be an operand of type %s. Instead "%s" was found.', implode(', ', $aOperators), $oVal ), '', 'custom', $oParserState->currentLine() ); } } $oParserState->consumeWhiteSpace(); } $oList->addListComponent($oCalcList); $oParserState->consume(')'); return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); } } src/Value/Value.php000066600000016041150513463720010210 0ustar00iLineNo = $iLineNo; } /** * @param array $aListDelimiters * * @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string * * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) { /** @var array $aStack */ $aStack = []; $oParserState->consumeWhiteSpace(); //Build a list of delimiters and parsed values while ( !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\')) ) { if (count($aStack) > 0) { $bFoundDelimiter = false; foreach ($aListDelimiters as $sDelimiter) { if ($oParserState->comes($sDelimiter)) { array_push($aStack, $oParserState->consume($sDelimiter)); $oParserState->consumeWhiteSpace(); $bFoundDelimiter = true; break; } } if (!$bFoundDelimiter) { //Whitespace was the list delimiter array_push($aStack, ' '); } } array_push($aStack, self::parsePrimitiveValue($oParserState)); $oParserState->consumeWhiteSpace(); } // Convert the list to list objects foreach ($aListDelimiters as $sDelimiter) { if (count($aStack) === 1) { return $aStack[0]; } $iStartPosition = null; while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { $iLength = 2; //Number of elements to be joined for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) { if ($sDelimiter !== $aStack[$i]) { break; } } $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) { $oList->addListComponent($aStack[$i]); } array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]); } } if (!isset($aStack[0])) { throw new UnexpectedTokenException( " {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine() ); } return $aStack[0]; } /** * @param bool $bIgnoreCase * * @return CSSFunction|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) { $sResult = $oParserState->parseIdentifier($bIgnoreCase); if ($oParserState->comes('(')) { $oParserState->consume('('); $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); $oParserState->consume(')'); } return $sResult; } /** * @return CSSFunction|CSSString|LineName|Size|URL|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * @throws SourceException */ public static function parsePrimitiveValue(ParserState $oParserState) { $oValue = null; $oParserState->consumeWhiteSpace(); if ( is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1))) ) { $oValue = Size::parse($oParserState); } elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { $oValue = Color::parse($oParserState); } elseif ($oParserState->comes('url', true)) { $oValue = URL::parse($oParserState); } elseif ( $oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true) ) { $oValue = CalcFunction::parse($oParserState); } elseif ($oParserState->comes("'") || $oParserState->comes('"')) { $oValue = CSSString::parse($oParserState); } elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { $oValue = self::parseMicrosoftFilter($oParserState); } elseif ($oParserState->comes("[")) { $oValue = LineName::parse($oParserState); } elseif ($oParserState->comes("U+")) { $oValue = self::parseUnicodeRangeValue($oParserState); } else { $oValue = self::parseIdentifierOrFunction($oParserState); } $oParserState->consumeWhiteSpace(); return $oValue; } /** * @return CSSFunction * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseMicrosoftFilter(ParserState $oParserState) { $sFunction = $oParserState->consumeUntil('(', false, true); $aArguments = Value::parseValue($oParserState, [',', '=']); return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); } /** * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseUnicodeRangeValue(ParserState $oParserState) { $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits $sRange = ""; $oParserState->consume("U+"); do { if ($oParserState->comes('-')) { $iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them } $sRange .= $oParserState->consume(1); } while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); return "U+{$sRange}"; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } } src/Value/ValueList.php000066600000004536150513463720011052 0ustar00 */ protected $aComponents; /** * @var string */ protected $sSeparator; /** * phpcs:ignore Generic.Files.LineLength * @param array|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents * @param string $sSeparator * @param int $iLineNo */ public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) { parent::__construct($iLineNo); if (!is_array($aComponents)) { $aComponents = [$aComponents]; } $this->aComponents = $aComponents; $this->sSeparator = $sSeparator; } /** * @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent * * @return void */ public function addListComponent($mComponent) { $this->aComponents[] = $mComponent; } /** * @return array */ public function getListComponents() { return $this->aComponents; } /** * @param array $aComponents * * @return void */ public function setListComponents(array $aComponents) { $this->aComponents = $aComponents; } /** * @return string */ public function getListSeparator() { return $this->sSeparator; } /** * @param string $sSeparator * * @return void */ public function setListSeparator($sSeparator) { $this->sSeparator = $sSeparator; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return $oOutputFormat->implode( $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents ); } } src/Value/RuleValueList.php000066600000000443150513463720011673 0ustar00implode(' ', $this->aComponents); } } src/Value/Size.php000066600000012400150513463720010041 0ustar00 */ const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem']; /** * @var array */ const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; /** * @var array */ const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turns', 'Hz', 'kHz']; /** * @var array>|null */ private static $SIZE_UNITS = null; /** * @var float */ private $fSize; /** * @var string|null */ private $sUnit; /** * @var bool */ private $bIsColorComponent; /** * @param float|int|string $fSize * @param string|null $sUnit * @param bool $bIsColorComponent * @param int $iLineNo */ public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) { parent::__construct($iLineNo); $this->fSize = (float)$fSize; $this->sUnit = $sUnit; $this->bIsColorComponent = $bIsColorComponent; } /** * @param bool $bIsColorComponent * * @return Size * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState, $bIsColorComponent = false) { $sSize = ''; if ($oParserState->comes('-')) { $sSize .= $oParserState->consume('-'); } while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) { if ($oParserState->comes('.')) { $sSize .= $oParserState->consume('.'); } else { $sSize .= $oParserState->consume(1); } } $sUnit = null; $aSizeUnits = self::getSizeUnits(); foreach ($aSizeUnits as $iLength => &$aValues) { $sKey = strtolower($oParserState->peek($iLength)); if (array_key_exists($sKey, $aValues)) { if (($sUnit = $aValues[$sKey]) !== null) { $oParserState->consume($iLength); break; } } } return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); } /** * @return array> */ private static function getSizeUnits() { if (!is_array(self::$SIZE_UNITS)) { self::$SIZE_UNITS = []; foreach (array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) { $iSize = strlen($val); if (!isset(self::$SIZE_UNITS[$iSize])) { self::$SIZE_UNITS[$iSize] = []; } self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; } krsort(self::$SIZE_UNITS, SORT_NUMERIC); } return self::$SIZE_UNITS; } /** * @param string $sUnit * * @return void */ public function setUnit($sUnit) { $this->sUnit = $sUnit; } /** * @return string|null */ public function getUnit() { return $this->sUnit; } /** * @param float|int|string $fSize */ public function setSize($fSize) { $this->fSize = (float)$fSize; } /** * @return float */ public function getSize() { return $this->fSize; } /** * @return bool */ public function isColorComponent() { return $this->bIsColorComponent; } /** * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). * * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. */ public function isSize() { if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) { return false; } return !$this->isColorComponent(); } /** * @return bool */ public function isRelative() { if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) { return true; } if ($this->sUnit === null && $this->fSize != 0) { return true; } return false; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $l = localeconv(); $sPoint = preg_quote($l['decimal_point'], '/'); $sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize) ? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : $this->fSize; return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize) . ($this->sUnit === null ? '' : $this->sUnit); } } src/Value/CSSFunction.php000066600000003066150513463720011275 0ustar00 $aArguments * @param string $sSeparator * @param int $iLineNo */ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) { if ($aArguments instanceof RuleValueList) { $sSeparator = $aArguments->getListSeparator(); $aArguments = $aArguments->getListComponents(); } $this->sName = $sName; $this->iLineNo = $iLineNo; parent::__construct($aArguments, $sSeparator, $iLineNo); } /** * @return string */ public function getName() { return $this->sName; } /** * @param string $sName * * @return void */ public function setName($sName) { $this->sName = $sName; } /** * @return array */ public function getArguments() { return $this->aComponents; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $aArguments = parent::render($oOutputFormat); return "{$this->sName}({$aArguments})"; } } src/Value/PrimitiveValue.php000066600000000344150513463720012100 0ustar00 $aComponents * @param int $iLineNo */ public function __construct(array $aComponents = [], $iLineNo = 0) { parent::__construct($aComponents, ' ', $iLineNo); } /** * @return LineName * * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ public static function parse(ParserState $oParserState) { $oParserState->consume('['); $oParserState->consumeWhiteSpace(); $aNames = []; do { if ($oParserState->getSettings()->bLenientParsing) { try { $aNames[] = $oParserState->parseIdentifier(); } catch (UnexpectedTokenException $e) { if (!$oParserState->comes(']')) { throw $e; } } } else { $aNames[] = $oParserState->parseIdentifier(); } $oParserState->consumeWhiteSpace(); } while (!$oParserState->comes(']')); $oParserState->consume(']'); return new LineName($aNames, $oParserState->currentLine()); } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return '[' . parent::render(OutputFormat::createCompact()) . ']'; } } src/Value/URL.php000066600000003415150513463720007577 0ustar00oURL = $oURL; } /** * @return URL * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $bUseUrl = $oParserState->comes('url', true); if ($bUseUrl) { $oParserState->consume('url'); $oParserState->consumeWhiteSpace(); $oParserState->consume('('); } $oParserState->consumeWhiteSpace(); $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); if ($bUseUrl) { $oParserState->consumeWhiteSpace(); $oParserState->consume(')'); } return $oResult; } /** * @return void */ public function setURL(CSSString $oURL) { $this->oURL = $oURL; } /** * @return CSSString */ public function getURL() { return $this->oURL; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return "url({$this->oURL->render($oOutputFormat)})"; } } src/Parsing/UnexpectedEOFException.php000066600000000421150513463720013773 0ustar00iLineNo = $iLineNo; if (!empty($iLineNo)) { $sMessage .= " [line no: $iLineNo]"; } parent::__construct($sMessage); } /** * @return int */ public function getLineNo() { return $this->iLineNo; } } src/Parsing/OutputException.php000066600000000546150513463720012645 0ustar00 */ private $aText; /** * @var int */ private $iCurrentPosition; /** * @var string */ private $sCharset; /** * @var int */ private $iLength; /** * @var int */ private $iLineNo; /** * @param string $sText * @param int $iLineNo */ public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) { $this->oParserSettings = $oParserSettings; $this->sText = $sText; $this->iCurrentPosition = 0; $this->iLineNo = $iLineNo; $this->setCharset($this->oParserSettings->sDefaultCharset); } /** * @param string $sCharset * * @return void */ public function setCharset($sCharset) { $this->sCharset = $sCharset; $this->aText = $this->strsplit($this->sText); if (is_array($this->aText)) { $this->iLength = count($this->aText); } } /** * @return string */ public function getCharset() { return $this->sCharset; } /** * @return int */ public function currentLine() { return $this->iLineNo; } /** * @return int */ public function currentColumn() { return $this->iCurrentPosition; } /** * @return Settings */ public function getSettings() { return $this->oParserSettings; } /** * @param bool $bIgnoreCase * * @return string * * @throws UnexpectedTokenException */ public function parseIdentifier($bIgnoreCase = true) { $sResult = $this->parseCharacter(true); if ($sResult === null) { throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); } $sCharacter = null; while (($sCharacter = $this->parseCharacter(true)) !== null) { if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) { $sResult .= $sCharacter; } else { $sResult .= '\\' . $sCharacter; } } if ($bIgnoreCase) { $sResult = $this->strtolower($sResult); } return $sResult; } /** * @param bool $bIsForIdentifier * * @return string|null * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function parseCharacter($bIsForIdentifier) { if ($this->peek() === '\\') { if ( $bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9')) ) { // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. return null; } $this->consume('\\'); if ($this->comes('\n') || $this->comes('\r')) { return ''; } if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { return $this->consume(1); } $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); if ($this->strlen($sUnicode) < 6) { // Consume whitespace after incomplete unicode escape if (preg_match('/\\s/isSu', $this->peek())) { if ($this->comes('\r\n')) { $this->consume(2); } else { $this->consume(1); } } } $iUnicode = intval($sUnicode, 16); $sUtf32 = ""; for ($i = 0; $i < 4; ++$i) { $sUtf32 .= chr($iUnicode & 0xff); $iUnicode = $iUnicode >> 8; } return iconv('utf-32le', $this->sCharset, $sUtf32); } if ($bIsForIdentifier) { $peek = ord($this->peek()); // Ranges: a-z A-Z 0-9 - _ if ( ($peek >= 97 && $peek <= 122) || ($peek >= 65 && $peek <= 90) || ($peek >= 48 && $peek <= 57) || ($peek === 45) || ($peek === 95) || ($peek > 0xa1) ) { return $this->consume(1); } } else { return $this->consume(1); } return null; } /** * @return array|void * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consumeWhiteSpace() { $comments = []; do { while (preg_match('/\\s/isSu', $this->peek()) === 1) { $this->consume(1); } if ($this->oParserSettings->bLenientParsing) { try { $oComment = $this->consumeComment(); } catch (UnexpectedEOFException $e) { $this->iCurrentPosition = $this->iLength; return; } } else { $oComment = $this->consumeComment(); } if ($oComment !== false) { $comments[] = $oComment; } } while ($oComment !== false); return $comments; } /** * @param string $sString * @param bool $bCaseInsensitive * * @return bool */ public function comes($sString, $bCaseInsensitive = false) { $sPeek = $this->peek(strlen($sString)); return ($sPeek == '') ? false : $this->streql($sPeek, $sString, $bCaseInsensitive); } /** * @param int $iLength * @param int $iOffset * * @return string */ public function peek($iLength = 1, $iOffset = 0) { $iOffset += $this->iCurrentPosition; if ($iOffset >= $this->iLength) { return ''; } return $this->substr($iOffset, $iLength); } /** * @param int $mValue * * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consume($mValue = 1) { if (is_string($mValue)) { $iLineCount = substr_count($mValue, "\n"); $iLength = $this->strlen($mValue); if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) { throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo); } $this->iLineNo += $iLineCount; $this->iCurrentPosition += $this->strlen($mValue); return $mValue; } else { if ($this->iCurrentPosition + $mValue > $this->iLength) { throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo); } $sResult = $this->substr($this->iCurrentPosition, $mValue); $iLineCount = substr_count($sResult, "\n"); $this->iLineNo += $iLineCount; $this->iCurrentPosition += $mValue; return $sResult; } } /** * @param string $mExpression * @param int|null $iMaxLength * * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consumeExpression($mExpression, $iMaxLength = null) { $aMatches = null; $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft(); if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) { return $this->consume($aMatches[0][0]); } throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); } /** * @return Comment|false */ public function consumeComment() { $mComment = false; if ($this->comes('/*')) { $iLineNo = $this->iLineNo; $this->consume(1); $mComment = ''; while (($char = $this->consume(1)) !== '') { $mComment .= $char; if ($this->comes('*/')) { $this->consume(2); break; } } } if ($mComment !== false) { // We skip the * which was included in the comment. return new Comment(substr($mComment, 1), $iLineNo); } return $mComment; } /** * @return bool */ public function isEnd() { return $this->iCurrentPosition >= $this->iLength; } /** * @param array|string $aEnd * @param string $bIncludeEnd * @param string $consumeEnd * @param array $comments * * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []) { $aEnd = is_array($aEnd) ? $aEnd : [$aEnd]; $out = ''; $start = $this->iCurrentPosition; while (!$this->isEnd()) { $char = $this->consume(1); if (in_array($char, $aEnd)) { if ($bIncludeEnd) { $out .= $char; } elseif (!$consumeEnd) { $this->iCurrentPosition -= $this->strlen($char); } return $out; } $out .= $char; if ($comment = $this->consumeComment()) { $comments[] = $comment; } } if (in_array(self::EOF, $aEnd)) { return $out; } $this->iCurrentPosition = $start; throw new UnexpectedEOFException( 'One of ("' . implode('","', $aEnd) . '")', $this->peek(5), 'search', $this->iLineNo ); } /** * @return string */ private function inputLeft() { return $this->substr($this->iCurrentPosition, -1); } /** * @param string $sString1 * @param string $sString2 * @param bool $bCaseInsensitive * * @return bool */ public function streql($sString1, $sString2, $bCaseInsensitive = true) { if ($bCaseInsensitive) { return $this->strtolower($sString1) === $this->strtolower($sString2); } else { return $sString1 === $sString2; } } /** * @param int $iAmount * * @return void */ public function backtrack($iAmount) { $this->iCurrentPosition -= $iAmount; } /** * @param string $sString * * @return int */ public function strlen($sString) { if ($this->oParserSettings->bMultibyteSupport) { return mb_strlen($sString, $this->sCharset); } else { return strlen($sString); } } /** * @param int $iStart * @param int $iLength * * @return string */ private function substr($iStart, $iLength) { if ($iLength < 0) { $iLength = $this->iLength - $iStart + $iLength; } if ($iStart + $iLength > $this->iLength) { $iLength = $this->iLength - $iStart; } $sResult = ''; while ($iLength > 0) { $sResult .= $this->aText[$iStart]; $iStart++; $iLength--; } return $sResult; } /** * @param string $sString * * @return string */ private function strtolower($sString) { if ($this->oParserSettings->bMultibyteSupport) { return mb_strtolower($sString, $this->sCharset); } else { return strtolower($sString); } } /** * @param string $sString * * @return array */ private function strsplit($sString) { if ($this->oParserSettings->bMultibyteSupport) { if ($this->streql($this->sCharset, 'utf-8')) { return preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY); } else { $iLength = mb_strlen($sString, $this->sCharset); $aResult = []; for ($i = 0; $i < $iLength; ++$i) { $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); } return $aResult; } } else { if ($sString === '') { return []; } else { return str_split($sString); } } } /** * @param string $sString * @param string $sNeedle * @param int $iOffset * * @return int|false */ private function strpos($sString, $sNeedle, $iOffset) { if ($this->oParserSettings->bMultibyteSupport) { return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); } else { return strpos($sString, $sNeedle, $iOffset); } } } src/Parsing/UnexpectedTokenException.php000066600000002704150513463720014450 0ustar00sExpected = $sExpected; $this->sFound = $sFound; $this->sMatchType = $sMatchType; $sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”."; if ($this->sMatchType === 'search') { $sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”."; } elseif ($this->sMatchType === 'count') { $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; } elseif ($this->sMatchType === 'identifier') { $sMessage = "Identifier expected. Got “{$sFound}”"; } elseif ($this->sMatchType === 'custom') { $sMessage = trim("$sExpected $sFound"); } parent::__construct($sMessage, $iLineNo); } } src/Settings.php000066600000003647150513463720007670 0ustar00bMultibyteSupport = extension_loaded('mbstring'); } /** * @return self new instance */ public static function create() { return new Settings(); } /** * @param bool $bMultibyteSupport * * @return self fluent interface */ public function withMultibyteSupport($bMultibyteSupport = true) { $this->bMultibyteSupport = $bMultibyteSupport; return $this; } /** * @param string $sDefaultCharset * * @return self fluent interface */ public function withDefaultCharset($sDefaultCharset) { $this->sDefaultCharset = $sDefaultCharset; return $this; } /** * @param bool $bLenientParsing * * @return self fluent interface */ public function withLenientParsing($bLenientParsing = true) { $this->bLenientParsing = $bLenientParsing; return $this; } /** * @return self fluent interface */ public function beStrict() { return $this->withLenientParsing(false); } } src/CSSList/Document.php000066600000011211150513463720011114 0ustar00currentLine()); CSSList::parseList($oParserState, $oDocument); return $oDocument; } /** * Gets all `DeclarationBlock` objects recursively. * * @return array */ public function getAllDeclarationBlocks() { /** @var array $aResult */ $aResult = []; $this->allDeclarationBlocks($aResult); return $aResult; } /** * Gets all `DeclarationBlock` objects recursively. * * @return array * * @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead */ public function getAllSelectors() { return $this->getAllDeclarationBlocks(); } /** * Returns all `RuleSet` objects found recursively in the tree. * * @return array */ public function getAllRuleSets() { /** @var array $aResult */ $aResult = []; $this->allRuleSets($aResult); return $aResult; } /** * Returns all `Value` objects found recursively in the tree. * * @param CSSList|RuleSet|string $mElement * the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). * If a string is given, it is used as rule name filter. * @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. * * @return array * * @see RuleSet->getRules() */ public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { $sSearchString = null; if ($mElement === null) { $mElement = $this; } elseif (is_string($mElement)) { $sSearchString = $mElement; $mElement = $this; } /** @var array $aResult */ $aResult = []; $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); return $aResult; } /** * Returns all `Selector` objects found recursively in the tree. * * Note that this does not yield the full `DeclarationBlock` that the selector belongs to * (and, currently, there is no way to get to that). * * @param string|null $sSpecificitySearch * An optional filter by specificity. * May contain a comparison operator and a number or just a number (defaults to "=="). * * @return array * @example `getSelectorsBySpecificity('>= 100')` * */ public function getSelectorsBySpecificity($sSpecificitySearch = null) { /** @var array $aResult */ $aResult = []; $this->allSelectors($aResult, $sSpecificitySearch); return $aResult; } /** * Expands all shorthand properties to their long value. * * @return void */ public function expandShorthands() { foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->expandShorthands(); } } /** * Create shorthands properties whenever possible. * * @return void */ public function createShorthands() { foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->createShorthands(); } } /** * Overrides `render()` to make format argument optional. * * @param OutputFormat|null $oOutputFormat * * @return string */ public function render(OutputFormat $oOutputFormat = null) { if ($oOutputFormat === null) { $oOutputFormat = new OutputFormat(); } return parent::render($oOutputFormat); } /** * @return bool */ public function isRootList() { return true; } } src/CSSList/KeyFrame.php000066600000003616150513463720011053 0ustar00vendorKeyFrame = null; $this->animationName = null; } /** * @param string $vendorKeyFrame */ public function setVendorKeyFrame($vendorKeyFrame) { $this->vendorKeyFrame = $vendorKeyFrame; } /** * @return string|null */ public function getVendorKeyFrame() { return $this->vendorKeyFrame; } /** * @param string $animationName */ public function setAnimationName($animationName) { $this->animationName = $animationName; } /** * @return string|null */ public function getAnimationName() { return $this->animationName; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= parent::render($oOutputFormat); $sResult .= '}'; return $sResult; } /** * @return bool */ public function isRootList() { return false; } /** * @return string|null */ public function atRuleName() { return $this->vendorKeyFrame; } /** * @return string|null */ public function atRuleArgs() { return $this->animationName; } } src/CSSList/CSSList.php000066600000036416150513463720010640 0ustar00 */ protected $aComments; /** * @var array */ protected $aContents; /** * @var int */ protected $iLineNo; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { $this->aComments = []; $this->aContents = []; $this->iLineNo = $iLineNo; } /** * @return void * * @throws UnexpectedTokenException * @throws SourceException */ public static function parseList(ParserState $oParserState, CSSList $oList) { $bIsRoot = $oList instanceof Document; if (is_string($oParserState)) { $oParserState = new ParserState($oParserState, Settings::create()); } $bLenientParsing = $oParserState->getSettings()->bLenientParsing; while (!$oParserState->isEnd()) { $comments = $oParserState->consumeWhiteSpace(); $oListItem = null; if ($bLenientParsing) { try { $oListItem = self::parseListItem($oParserState, $oList); } catch (UnexpectedTokenException $e) { $oListItem = false; } } else { $oListItem = self::parseListItem($oParserState, $oList); } if ($oListItem === null) { // List parsing finished return; } if ($oListItem) { $oListItem->setComments($comments); $oList->append($oListItem); } $oParserState->consumeWhiteSpace(); } if (!$bIsRoot && !$bLenientParsing) { throw new SourceException("Unexpected end of document", $oParserState->currentLine()); } } /** * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseListItem(ParserState $oParserState, CSSList $oList) { $bIsRoot = $oList instanceof Document; if ($oParserState->comes('@')) { $oAtRule = self::parseAtRule($oParserState); if ($oAtRule instanceof Charset) { if (!$bIsRoot) { throw new UnexpectedTokenException( '@charset may only occur in root document', '', 'custom', $oParserState->currentLine() ); } if (count($oList->getContents()) > 0) { throw new UnexpectedTokenException( '@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine() ); } $oParserState->setCharset($oAtRule->getCharset()->getString()); } return $oAtRule; } elseif ($oParserState->comes('}')) { if (!$oParserState->getSettings()->bLenientParsing) { throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine()); } else { if ($bIsRoot) { if ($oParserState->getSettings()->bLenientParsing) { return DeclarationBlock::parse($oParserState); } else { throw new SourceException("Unopened {", $oParserState->currentLine()); } } else { return null; } } } else { return DeclarationBlock::parse($oParserState, $oList); } } /** * @param ParserState $oParserState * * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null * * @throws SourceException * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ private static function parseAtRule(ParserState $oParserState) { $oParserState->consume('@'); $sIdentifier = $oParserState->parseIdentifier(); $iIdentifierLineNum = $oParserState->currentLine(); $oParserState->consumeWhiteSpace(); if ($sIdentifier === 'import') { $oLocation = URL::parse($oParserState); $oParserState->consumeWhiteSpace(); $sMediaQuery = null; if (!$oParserState->comes(';')) { $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF])); } $oParserState->consumeUntil([';', ParserState::EOF], true, true); return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); } elseif ($sIdentifier === 'charset') { $sCharset = CSSString::parse($oParserState); $oParserState->consumeWhiteSpace(); $oParserState->consumeUntil([';', ParserState::EOF], true, true); return new Charset($sCharset, $iIdentifierLineNum); } elseif (self::identifierIs($sIdentifier, 'keyframes')) { $oResult = new KeyFrame($iIdentifierLineNum); $oResult->setVendorKeyFrame($sIdentifier); $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); CSSList::parseList($oParserState, $oResult); if ($oParserState->comes('}')) { $oParserState->consume('}'); } return $oResult; } elseif ($sIdentifier === 'namespace') { $sPrefix = null; $mUrl = Value::parsePrimitiveValue($oParserState); if (!$oParserState->comes(';')) { $sPrefix = $mUrl; $mUrl = Value::parsePrimitiveValue($oParserState); } $oParserState->consumeUntil([';', ParserState::EOF], true, true); if ($sPrefix !== null && !is_string($sPrefix)) { throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); } if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { throw new UnexpectedTokenException( 'Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum ); } return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); } else { // Unknown other at rule (font-face or such) $sArgs = trim($oParserState->consumeUntil('{', false, true)); if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) { if ($oParserState->getSettings()->bLenientParsing) { return null; } else { throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); } } $bUseRuleSet = true; foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { if (self::identifierIs($sIdentifier, $sBlockRuleName)) { $bUseRuleSet = false; break; } } if ($bUseRuleSet) { $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); RuleSet::parseRuleSet($oParserState, $oAtRule); } else { $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); CSSList::parseList($oParserState, $oAtRule); if ($oParserState->comes('}')) { $oParserState->consume('}'); } } return $oAtRule; } } /** * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. * We need to check for these versions too. * * @param string $sIdentifier * @param string $sMatch * * @return bool */ private static function identifierIs($sIdentifier, $sMatch) { return (strcasecmp($sIdentifier, $sMatch) === 0) ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * Prepends an item to the list of contents. * * @param RuleSet|CSSList|Import|Charset $oItem * * @return void */ public function prepend($oItem) { array_unshift($this->aContents, $oItem); } /** * Appends an item to tje list of contents. * * @param RuleSet|CSSList|Import|Charset $oItem * * @return void */ public function append($oItem) { $this->aContents[] = $oItem; } /** * Splices the list of contents. * * @param int $iOffset * @param int $iLength * @param array $mReplacement * * @return void */ public function splice($iOffset, $iLength = null, $mReplacement = null) { array_splice($this->aContents, $iOffset, $iLength, $mReplacement); } /** * Removes an item from the CSS list. * * @param RuleSet|Import|Charset|CSSList $oItemToRemove * May be a RuleSet (most likely a DeclarationBlock), a Import, * a Charset or another CSSList (most likely a MediaQuery) * * @return bool whether the item was removed */ public function remove($oItemToRemove) { $iKey = array_search($oItemToRemove, $this->aContents, true); if ($iKey !== false) { unset($this->aContents[$iKey]); return true; } return false; } /** * Replaces an item from the CSS list. * * @param RuleSet|Import|Charset|CSSList $oOldItem * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset` * or another `CSSList` (most likely a `MediaQuery`) * * @return bool */ public function replace($oOldItem, $mNewItem) { $iKey = array_search($oOldItem, $this->aContents, true); if ($iKey !== false) { if (is_array($mNewItem)) { array_splice($this->aContents, $iKey, 1, $mNewItem); } else { array_splice($this->aContents, $iKey, 1, [$mNewItem]); } return true; } return false; } /** * @param array $aContents */ public function setContents(array $aContents) { $this->aContents = []; foreach ($aContents as $content) { $this->append($content); } } /** * Removes a declaration block from the CSS list if it matches all given selectors. * * @param DeclarationBlock|array|string $mSelector the selectors to match * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks * * @return void */ public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { if ($mSelector instanceof DeclarationBlock) { $mSelector = $mSelector->getSelectors(); } if (!is_array($mSelector)) { $mSelector = explode(',', $mSelector); } foreach ($mSelector as $iKey => &$mSel) { if (!($mSel instanceof Selector)) { if (!Selector::isValid($mSel)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSel, "custom" ); } $mSel = new Selector($mSel); } } foreach ($this->aContents as $iKey => $mItem) { if (!($mItem instanceof DeclarationBlock)) { continue; } if ($mItem->getSelectors() == $mSelector) { unset($this->aContents[$iKey]); if (!$bRemoveAll) { return; } } } } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sResult = ''; $bIsFirst = true; $oNextLevel = $oOutputFormat; if (!$this->isRootList()) { $oNextLevel = $oOutputFormat->nextLevel(); } foreach ($this->aContents as $oContent) { $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) { return $oContent->render($oNextLevel); }); if ($sRendered === null) { continue; } if ($bIsFirst) { $bIsFirst = false; $sResult .= $oNextLevel->spaceBeforeBlocks(); } else { $sResult .= $oNextLevel->spaceBetweenBlocks(); } $sResult .= $sRendered; } if (!$bIsFirst) { // Had some output $sResult .= $oOutputFormat->spaceAfterBlocks(); } return $sResult; } /** * Return true if the list can not be further outdented. Only important when rendering. * * @return bool */ abstract public function isRootList(); /** * @return array */ public function getContents() { return $this->aContents; } /** * @param array $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array */ public function getComments() { return $this->aComments; } /** * @param array $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } src/CSSList/AtRuleBlockList.php000066600000003172150513463720012350 0ustar00sType = $sType; $this->sArgs = $sArgs; } /** * @return string */ public function atRuleName() { return $this->sType; } /** * @return string */ public function atRuleArgs() { return $this->sArgs; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sArgs = $this->sArgs; if ($sArgs) { $sArgs = ' ' . $sArgs; } $sResult = $oOutputFormat->sBeforeAtRuleBlock; $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= parent::render($oOutputFormat); $sResult .= '}'; $sResult .= $oOutputFormat->sAfterAtRuleBlock; return $sResult; } /** * @return bool */ public function isRootList() { return false; } } src/CSSList/CSSBlockList.php000066600000012164150513463720011605 0ustar00 $aResult * * @return void */ protected function allDeclarationBlocks(array &$aResult) { foreach ($this->aContents as $mContent) { if ($mContent instanceof DeclarationBlock) { $aResult[] = $mContent; } elseif ($mContent instanceof CSSBlockList) { $mContent->allDeclarationBlocks($aResult); } } } /** * @param array $aResult * * @return void */ protected function allRuleSets(array &$aResult) { foreach ($this->aContents as $mContent) { if ($mContent instanceof RuleSet) { $aResult[] = $mContent; } elseif ($mContent instanceof CSSBlockList) { $mContent->allRuleSets($aResult); } } } /** * @param CSSList|Rule|RuleSet|Value $oElement * @param array $aResult * @param string|null $sSearchString * @param bool $bSearchInFunctionArguments * * @return void */ protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { if ($oElement instanceof CSSBlockList) { foreach ($oElement->getContents() as $oContent) { $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); } } elseif ($oElement instanceof RuleSet) { foreach ($oElement->getRules($sSearchString) as $oRule) { $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); } } elseif ($oElement instanceof Rule) { $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); } elseif ($oElement instanceof ValueList) { if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { foreach ($oElement->getListComponents() as $mComponent) { $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); } } } else { // Non-List `Value` or `CSSString` (CSS identifier) $aResult[] = $oElement; } } /** * @param array $aResult * @param string|null $sSpecificitySearch * * @return void */ protected function allSelectors(array &$aResult, $sSpecificitySearch = null) { /** @var array $aDeclarationBlocks */ $aDeclarationBlocks = []; $this->allDeclarationBlocks($aDeclarationBlocks); foreach ($aDeclarationBlocks as $oBlock) { foreach ($oBlock->getSelectors() as $oSelector) { if ($sSpecificitySearch === null) { $aResult[] = $oSelector; } else { $sComparator = '==='; $aSpecificitySearch = explode(' ', $sSpecificitySearch); $iTargetSpecificity = $aSpecificitySearch[0]; if (count($aSpecificitySearch) > 1) { $sComparator = $aSpecificitySearch[0]; $iTargetSpecificity = $aSpecificitySearch[1]; } $iTargetSpecificity = (int)$iTargetSpecificity; $iSelectorSpecificity = $oSelector->getSpecificity(); $bMatches = false; switch ($sComparator) { case '<=': $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; break; case '<': $bMatches = $iSelectorSpecificity < $iTargetSpecificity; break; case '>=': $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; break; case '>': $bMatches = $iSelectorSpecificity > $iTargetSpecificity; break; default: $bMatches = $iSelectorSpecificity === $iTargetSpecificity; break; } if ($bMatches) { $aResult[] = $oSelector; } } } } } } src/RuleSet/DeclarationBlock.php000066600000071606150513463720012653 0ustar00 */ private $aSelectors; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); $this->aSelectors = []; } /** * @param CSSList|null $oList * * @return DeclarationBlock|false * * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ public static function parse(ParserState $oParserState, $oList = null) { $aComments = []; $oResult = new DeclarationBlock($oParserState->currentLine()); try { $aSelectorParts = []; $sStringWrapperChar = false; do { $aSelectorParts[] = $oParserState->consume(1) . $oParserState->consumeUntil(['{', '}', '\'', '"'], false, false, $aComments); if (in_array($oParserState->peek(), ['\'', '"']) && substr(end($aSelectorParts), -1) != "\\") { if ($sStringWrapperChar === false) { $sStringWrapperChar = $oParserState->peek(); } elseif ($sStringWrapperChar == $oParserState->peek()) { $sStringWrapperChar = false; } } } while (!in_array($oParserState->peek(), ['{', '}']) || $sStringWrapperChar !== false); $oResult->setSelectors(implode('', $aSelectorParts), $oList); if ($oParserState->comes('{')) { $oParserState->consume(1); } } catch (UnexpectedTokenException $e) { if ($oParserState->getSettings()->bLenientParsing) { if (!$oParserState->comes('}')) { $oParserState->consumeUntil('}', false, true); } return false; } else { throw $e; } } $oResult->setComments($aComments); RuleSet::parseRuleSet($oParserState, $oResult); return $oResult; } /** * @param array|string $mSelector * @param CSSList|null $oList * * @throws UnexpectedTokenException */ public function setSelectors($mSelector, $oList = null) { if (is_array($mSelector)) { $this->aSelectors = $mSelector; } else { $this->aSelectors = explode(',', $mSelector); } foreach ($this->aSelectors as $iKey => $mSelector) { if (!($mSelector instanceof Selector)) { if ($oList === null || !($oList instanceof KeyFrame)) { if (!Selector::isValid($mSelector)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom" ); } $this->aSelectors[$iKey] = new Selector($mSelector); } else { if (!KeyframeSelector::isValid($mSelector)) { throw new UnexpectedTokenException( "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom" ); } $this->aSelectors[$iKey] = new KeyframeSelector($mSelector); } } } } /** * Remove one of the selectors of the block. * * @param Selector|string $mSelector * * @return bool */ public function removeSelector($mSelector) { if ($mSelector instanceof Selector) { $mSelector = $mSelector->getSelector(); } foreach ($this->aSelectors as $iKey => $oSelector) { if ($oSelector->getSelector() === $mSelector) { unset($this->aSelectors[$iKey]); return true; } } return false; } /** * @return array * * @deprecated will be removed in version 9.0; use `getSelectors()` instead */ public function getSelector() { return $this->getSelectors(); } /** * @param Selector|string $mSelector * @param CSSList|null $oList * * @return void * * @deprecated will be removed in version 9.0; use `setSelectors()` instead */ public function setSelector($mSelector, $oList = null) { $this->setSelectors($mSelector, $oList); } /** * @return array */ public function getSelectors() { return $this->aSelectors; } /** * Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts. * * @return void */ public function expandShorthands() { // border must be expanded before dimensions $this->expandBorderShorthand(); $this->expandDimensionsShorthand(); $this->expandFontShorthand(); $this->expandBackgroundShorthand(); $this->expandListStyleShorthand(); } /** * Creates shorthand declarations (e.g. `margin` or `font`) whenever possible. * * @return void */ public function createShorthands() { $this->createBackgroundShorthand(); $this->createDimensionsShorthand(); // border must be shortened after dimensions $this->createBorderShorthand(); $this->createFontShorthand(); $this->createListStyleShorthand(); } /** * Splits shorthand border declarations (e.g. `border: 1px red;`). * * Additional splitting happens in expandDimensionsShorthand. * * Multiple borders are not yet supported as of 3. * * @return void */ public function expandBorderShorthand() { $aBorderRules = [ 'border', 'border-left', 'border-right', 'border-top', 'border-bottom', ]; $aBorderSizes = [ 'thin', 'medium', 'thick', ]; $aRules = $this->getRulesAssoc(); foreach ($aBorderRules as $sBorderRule) { if (!isset($aRules[$sBorderRule])) { continue; } $oRule = $aRules[$sBorderRule]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } foreach ($aValues as $mValue) { if ($mValue instanceof Value) { $mNewValue = clone $mValue; } else { $mNewValue = $mValue; } if ($mValue instanceof Size) { $sNewRuleName = $sBorderRule . "-width"; } elseif ($mValue instanceof Color) { $sNewRuleName = $sBorderRule . "-color"; } else { if (in_array($mValue, $aBorderSizes)) { $sNewRuleName = $sBorderRule . "-width"; } else { $sNewRuleName = $sBorderRule . "-style"; } } $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue([$mNewValue]); $this->addRule($oNewRule); } $this->removeRule($sBorderRule); } } /** * Splits shorthand dimensional declarations (e.g. `margin: 0px auto;`) * into their constituent parts. * * Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`. * * @return void */ public function expandDimensionsShorthand() { $aExpansions = [ 'margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width', ]; $aRules = $this->getRulesAssoc(); foreach ($aExpansions as $sProperty => $sExpanded) { if (!isset($aRules[$sProperty])) { continue; } $oRule = $aRules[$sProperty]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } $top = $right = $bottom = $left = null; switch (count($aValues)) { case 1: $top = $right = $bottom = $left = $aValues[0]; break; case 2: $top = $bottom = $aValues[0]; $left = $right = $aValues[1]; break; case 3: $top = $aValues[0]; $left = $right = $aValues[1]; $bottom = $aValues[2]; break; case 4: $top = $aValues[0]; $right = $aValues[1]; $bottom = $aValues[2]; $left = $aValues[3]; break; } foreach (['top', 'right', 'bottom', 'left'] as $sPosition) { $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue(${$sPosition}); $this->addRule($oNewRule); } $this->removeRule($sProperty); } } /** * Converts shorthand font declarations * (e.g. `font: 300 italic 11px/14px verdana, helvetica, sans-serif;`) * into their constituent parts. * * @return void */ public function expandFontShorthand() { $aRules = $this->getRulesAssoc(); if (!isset($aRules['font'])) { return; } $oRule = $aRules['font']; // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand $aFontProperties = [ 'font-style' => 'normal', 'font-variant' => 'normal', 'font-weight' => 'normal', 'font-size' => 'normal', 'line-height' => 'normal', ]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } foreach ($aValues as $mValue) { if (!$mValue instanceof Value) { $mValue = mb_strtolower($mValue); } if (in_array($mValue, ['normal', 'inherit'])) { foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) { if (!isset($aFontProperties[$sProperty])) { $aFontProperties[$sProperty] = $mValue; } } } elseif (in_array($mValue, ['italic', 'oblique'])) { $aFontProperties['font-style'] = $mValue; } elseif ($mValue == 'small-caps') { $aFontProperties['font-variant'] = $mValue; } elseif ( in_array($mValue, ['bold', 'bolder', 'lighter']) || ($mValue instanceof Size && in_array($mValue->getSize(), range(100, 900, 100))) ) { $aFontProperties['font-weight'] = $mValue; } elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { list($oSize, $oHeight) = $mValue->getListComponents(); $aFontProperties['font-size'] = $oSize; $aFontProperties['line-height'] = $oHeight; } elseif ($mValue instanceof Size && $mValue->getUnit() !== null) { $aFontProperties['font-size'] = $mValue; } else { $aFontProperties['font-family'] = $mValue; } } foreach ($aFontProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->addValue($mValue); $oNewRule->setIsImportant($oRule->getIsImportant()); $this->addRule($oNewRule); } $this->removeRule('font'); } /** * Converts shorthand background declarations * (e.g. `background: url("chess.png") gray 50% repeat fixed;`) * into their constituent parts. * * @see http://www.w3.org/TR/21/colors.html#propdef-background * * @return void */ public function expandBackgroundShorthand() { $aRules = $this->getRulesAssoc(); if (!isset($aRules['background'])) { return; } $oRule = $aRules['background']; $aBgProperties = [ 'background-color' => ['transparent'], 'background-image' => ['none'], 'background-repeat' => ['repeat'], 'background-attachment' => ['scroll'], 'background-position' => [ new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo), ], ]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } if (count($aValues) == 1 && $aValues[0] == 'inherit') { foreach ($aBgProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->addValue('inherit'); $oNewRule->setIsImportant($oRule->getIsImportant()); $this->addRule($oNewRule); } $this->removeRule('background'); return; } $iNumBgPos = 0; foreach ($aValues as $mValue) { if (!$mValue instanceof Value) { $mValue = mb_strtolower($mValue); } if ($mValue instanceof URL) { $aBgProperties['background-image'] = $mValue; } elseif ($mValue instanceof Color) { $aBgProperties['background-color'] = $mValue; } elseif (in_array($mValue, ['scroll', 'fixed'])) { $aBgProperties['background-attachment'] = $mValue; } elseif (in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'])) { $aBgProperties['background-repeat'] = $mValue; } elseif ( in_array($mValue, ['left', 'center', 'right', 'top', 'bottom']) || $mValue instanceof Size ) { if ($iNumBgPos == 0) { $aBgProperties['background-position'][0] = $mValue; $aBgProperties['background-position'][1] = 'center'; } else { $aBgProperties['background-position'][$iNumBgPos] = $mValue; } $iNumBgPos++; } } foreach ($aBgProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue($mValue); $this->addRule($oNewRule); } $this->removeRule('background'); } /** * @return void */ public function expandListStyleShorthand() { $aListProperties = [ 'list-style-type' => 'disc', 'list-style-position' => 'outside', 'list-style-image' => 'none', ]; $aListStyleTypes = [ 'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana', ]; $aListStylePositions = [ 'inside', 'outside', ]; $aRules = $this->getRulesAssoc(); if (!isset($aRules['list-style'])) { return; } $oRule = $aRules['list-style']; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } if (count($aValues) == 1 && $aValues[0] == 'inherit') { foreach ($aListProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->addValue('inherit'); $oNewRule->setIsImportant($oRule->getIsImportant()); $this->addRule($oNewRule); } $this->removeRule('list-style'); return; } foreach ($aValues as $mValue) { if (!$mValue instanceof Value) { $mValue = mb_strtolower($mValue); } if ($mValue instanceof Url) { $aListProperties['list-style-image'] = $mValue; } elseif (in_array($mValue, $aListStyleTypes)) { $aListProperties['list-style-types'] = $mValue; } elseif (in_array($mValue, $aListStylePositions)) { $aListProperties['list-style-position'] = $mValue; } } foreach ($aListProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue($mValue); $this->addRule($oNewRule); } $this->removeRule('list-style'); } /** * @param array $aProperties * @param string $sShorthand * * @return void */ public function createShorthandProperties(array $aProperties, $sShorthand) { $aRules = $this->getRulesAssoc(); $aNewValues = []; foreach ($aProperties as $sProperty) { if (!isset($aRules[$sProperty])) { continue; } $oRule = $aRules[$sProperty]; if (!$oRule->getIsImportant()) { $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } foreach ($aValues as $mValue) { $aNewValues[] = $mValue; } $this->removeRule($sProperty); } } if (count($aNewValues)) { $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo()); foreach ($aNewValues as $mValue) { $oNewRule->addValue($mValue); } $this->addRule($oNewRule); } } /** * @return void */ public function createBackgroundShorthand() { $aProperties = [ 'background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment', ]; $this->createShorthandProperties($aProperties, 'background'); } /** * @return void */ public function createListStyleShorthand() { $aProperties = [ 'list-style-type', 'list-style-position', 'list-style-image', ]; $this->createShorthandProperties($aProperties, 'list-style'); } /** * Combines `border-color`, `border-style` and `border-width` into `border`. * * Should be run after `create_dimensions_shorthand`! * * @return void */ public function createBorderShorthand() { $aProperties = [ 'border-width', 'border-style', 'border-color', ]; $this->createShorthandProperties($aProperties, 'border'); } /** * Looks for long format CSS dimensional properties * (margin, padding, border-color, border-style and border-width) * and converts them into shorthand CSS properties. * * @return void */ public function createDimensionsShorthand() { $aPositions = ['top', 'right', 'bottom', 'left']; $aExpansions = [ 'margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width', ]; $aRules = $this->getRulesAssoc(); foreach ($aExpansions as $sProperty => $sExpanded) { $aFoldable = []; foreach ($aRules as $sRuleName => $oRule) { foreach ($aPositions as $sPosition) { if ($sRuleName == sprintf($sExpanded, $sPosition)) { $aFoldable[$sRuleName] = $oRule; } } } // All four dimensions must be present if (count($aFoldable) == 4) { $aValues = []; foreach ($aPositions as $sPosition) { $oRule = $aRules[sprintf($sExpanded, $sPosition)]; $mRuleValue = $oRule->getValue(); $aRuleValues = []; if (!$mRuleValue instanceof RuleValueList) { $aRuleValues[] = $mRuleValue; } else { $aRuleValues = $mRuleValue->getListComponents(); } $aValues[$sPosition] = $aRuleValues; } $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); if ((string)$aValues['left'][0] == (string)$aValues['right'][0]) { if ((string)$aValues['top'][0] == (string)$aValues['bottom'][0]) { if ((string)$aValues['top'][0] == (string)$aValues['left'][0]) { // All 4 sides are equal $oNewRule->addValue($aValues['top']); } else { // Top and bottom are equal, left and right are equal $oNewRule->addValue($aValues['top']); $oNewRule->addValue($aValues['left']); } } else { // Only left and right are equal $oNewRule->addValue($aValues['top']); $oNewRule->addValue($aValues['left']); $oNewRule->addValue($aValues['bottom']); } } else { // No sides are equal $oNewRule->addValue($aValues['top']); $oNewRule->addValue($aValues['left']); $oNewRule->addValue($aValues['bottom']); $oNewRule->addValue($aValues['right']); } $this->addRule($oNewRule); foreach ($aPositions as $sPosition) { $this->removeRule(sprintf($sExpanded, $sPosition)); } } } } /** * Looks for long format CSS font properties (e.g. `font-weight`) and * tries to convert them into a shorthand CSS `font` property. * * At least `font-size` AND `font-family` must be present in order to create a shorthand declaration. * * @return void */ public function createFontShorthand() { $aFontProperties = [ 'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family', ]; $aRules = $this->getRulesAssoc(); if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) { return; } $oOldRule = isset($aRules['font-size']) ? $aRules['font-size'] : $aRules['font-family']; $oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo()); unset($oOldRule); foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) { if (isset($aRules[$sProperty])) { $oRule = $aRules[$sProperty]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } if ($aValues[0] !== 'normal') { $oNewRule->addValue($aValues[0]); } } } // Get the font-size value $oRule = $aRules['font-size']; $mRuleValue = $oRule->getValue(); $aFSValues = []; if (!$mRuleValue instanceof RuleValueList) { $aFSValues[] = $mRuleValue; } else { $aFSValues = $mRuleValue->getListComponents(); } // But wait to know if we have line-height to add it if (isset($aRules['line-height'])) { $oRule = $aRules['line-height']; $mRuleValue = $oRule->getValue(); $aLHValues = []; if (!$mRuleValue instanceof RuleValueList) { $aLHValues[] = $mRuleValue; } else { $aLHValues = $mRuleValue->getListComponents(); } if ($aLHValues[0] !== 'normal') { $val = new RuleValueList('/', $this->iLineNo); $val->addListComponent($aFSValues[0]); $val->addListComponent($aLHValues[0]); $oNewRule->addValue($val); } } else { $oNewRule->addValue($aFSValues[0]); } $oRule = $aRules['font-family']; $mRuleValue = $oRule->getValue(); $aFFValues = []; if (!$mRuleValue instanceof RuleValueList) { $aFFValues[] = $mRuleValue; } else { $aFFValues = $mRuleValue->getListComponents(); } $oFFValue = new RuleValueList(',', $this->iLineNo); $oFFValue->setListComponents($aFFValues); $oNewRule->addValue($oFFValue); $this->addRule($oNewRule); foreach ($aFontProperties as $sProperty) { $this->removeRule($sProperty); } } /** * @return string * * @throws OutputException */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string * * @throws OutputException */ public function render(OutputFormat $oOutputFormat) { if (count($this->aSelectors) === 0) { // If all the selectors have been removed, this declaration block becomes invalid throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo); } $sResult = $oOutputFormat->sBeforeDeclarationBlock; $sResult .= $oOutputFormat->implode( $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors ); $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; $sResult .= parent::render($oOutputFormat); $sResult .= '}'; $sResult .= $oOutputFormat->sAfterDeclarationBlock; return $sResult; } } src/RuleSet/RuleSet.php000066600000025324150513463720011032 0ustar00 */ private $aRules; /** * @var int */ protected $iLineNo; /** * @var array */ protected $aComments; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { $this->aRules = []; $this->iLineNo = $iLineNo; $this->aComments = []; } /** * @return void * * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) { while ($oParserState->comes(';')) { $oParserState->consume(';'); } while (!$oParserState->comes('}')) { $oRule = null; if ($oParserState->getSettings()->bLenientParsing) { try { $oRule = Rule::parse($oParserState); } catch (UnexpectedTokenException $e) { try { $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true); // We need to “unfind” the matches to the end of the ruleSet as this will be matched later if ($oParserState->streql(substr($sConsume, -1), '}')) { $oParserState->backtrack(1); } else { while ($oParserState->comes(';')) { $oParserState->consume(';'); } } } catch (UnexpectedTokenException $e) { // We’ve reached the end of the document. Just close the RuleSet. return; } } } else { $oRule = Rule::parse($oParserState); } if ($oRule) { $oRuleSet->addRule($oRule); } } $oParserState->consume('}'); } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * @param Rule|null $oSibling * * @return void */ public function addRule(Rule $oRule, Rule $oSibling = null) { $sRule = $oRule->getRule(); if (!isset($this->aRules[$sRule])) { $this->aRules[$sRule] = []; } $iPosition = count($this->aRules[$sRule]); if ($oSibling !== null) { $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); if ($iSiblingPos !== false) { $iPosition = $iSiblingPos; $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1); } } if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) { //this node is added manually, give it the next best line $rules = $this->getRules(); $pos = count($rules); if ($pos > 0) { $last = $rules[$pos - 1]; $oRule->setPosition($last->getLineNo() + 1, 0); } } array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]); } /** * Returns all rules matching the given rule name * * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array(). * * @example $oRuleSet->getRules('font-') * //returns an array of all rules either beginning with font- or matching font. * * @param Rule|string|null $mRule * Pattern to search for. If null, returns all rules. * If the pattern ends with a dash, all rules starting with the pattern are returned * as well as one matching the pattern with the dash excluded. * Passing a Rule behaves like calling `getRules($mRule->getRule())`. * * @return array */ public function getRules($mRule = null) { if ($mRule instanceof Rule) { $mRule = $mRule->getRule(); } /** @var array $aResult */ $aResult = []; foreach ($this->aRules as $sName => $aRules) { // Either no search rule is given or the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule. if ( !$mRule || $sName === $mRule || ( strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)) ) ) { $aResult = array_merge($aResult, $aRules); } } usort($aResult, function (Rule $first, Rule $second) { if ($first->getLineNo() === $second->getLineNo()) { return $first->getColNo() - $second->getColNo(); } return $first->getLineNo() - $second->getLineNo(); }); return $aResult; } /** * Overrides all the rules of this set. * * @param array $aRules The rules to override with. * * @return void */ public function setRules(array $aRules) { $this->aRules = []; foreach ($aRules as $rule) { $this->addRule($rule); } } /** * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name * as keys. This method exists mainly for backwards-compatibility and is really only partially useful. * * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. * * @param Rule|string|null $mRule $mRule * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, * all rules starting with the pattern are returned as well as one matching the pattern with the dash * excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`. * * @return array */ public function getRulesAssoc($mRule = null) { /** @var array $aResult */ $aResult = []; foreach ($this->getRules($mRule) as $oRule) { $aResult[$oRule->getRule()] = $oRule; } return $aResult; } /** * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts. * * If given a Rule, it will only remove this particular rule (by identity). * If given a name, it will remove all rules by that name. * * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`. * * @param Rule|string|null $mRule * pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, * all rules starting with the pattern are removed as well as one matching the pattern with the dash * excluded. Passing a Rule behaves matches by identity. * * @return void */ public function removeRule($mRule) { if ($mRule instanceof Rule) { $sRule = $mRule->getRule(); if (!isset($this->aRules[$sRule])) { return; } foreach ($this->aRules[$sRule] as $iKey => $oRule) { if ($oRule === $mRule) { unset($this->aRules[$sRule][$iKey]); } } } else { foreach ($this->aRules as $sName => $aRules) { // Either no search rule is given or the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule or equals it // (without the trailing dash). if ( !$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))) ) { unset($this->aRules[$sName]); } } } } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sResult = ''; $bIsFirst = true; foreach ($this->aRules as $aRules) { foreach ($aRules as $oRule) { $sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) { return $oRule->render($oOutputFormat->nextLevel()); }); if ($sRendered === null) { continue; } if ($bIsFirst) { $bIsFirst = false; $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules(); } else { $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules(); } $sResult .= $sRendered; } } if (!$bIsFirst) { // Had some output $sResult .= $oOutputFormat->spaceAfterRules(); } return $oOutputFormat->removeLastSemicolon($sResult); } /** * @param array $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array */ public function getComments() { return $this->aComments; } /** * @param array $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } src/RuleSet/AtRuleSet.php000066600000002620150513463720011311 0ustar00sType = $sType; $this->sArgs = $sArgs; } /** * @return string */ public function atRuleName() { return $this->sType; } /** * @return string */ public function atRuleArgs() { return $this->sArgs; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sArgs = $this->sArgs; if ($sArgs) { $sArgs = ' ' . $sArgs; } $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= parent::render($oOutputFormat); $sResult .= '}'; return $sResult; } } src/Property/CSSNamespace.php000066600000005247150513463720012157 0ustar00 */ protected $aComments; /** * @param string $mUrl * @param string|null $sPrefix * @param int $iLineNo */ public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) { $this->mUrl = $mUrl; $this->sPrefix = $sPrefix; $this->iLineNo = $iLineNo; $this->aComments = []; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ') . $this->mUrl->render($oOutputFormat) . ';'; } /** * @return string */ public function getUrl() { return $this->mUrl; } /** * @return string|null */ public function getPrefix() { return $this->sPrefix; } /** * @param string $mUrl * * @return void */ public function setUrl($mUrl) { $this->mUrl = $mUrl; } /** * @param string $sPrefix * * @return void */ public function setPrefix($sPrefix) { $this->sPrefix = $sPrefix; } /** * @return string */ public function atRuleName() { return 'namespace'; } /** * @return array */ public function atRuleArgs() { $aResult = [$this->mUrl]; if ($this->sPrefix) { array_unshift($aResult, $this->sPrefix); } return $aResult; } /** * @param array $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array */ public function getComments() { return $this->aComments; } /** * @param array $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } src/Property/Selector.php000066600000006553150513463720011473 0ustar00\~]+)[\w]+ # elements | \:{1,2}( # pseudo-elements after|before|first-letter|first-line|selection )) /ix'; /** * regexp for specificity calculations * * @var string */ const SELECTOR_VALIDATION_RX = '/ ^( (?: [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters (?:\\\\.)? # a single escaped character (?:([\'"]).*?(?setSelector($sSelector); if ($bCalculateSpecificity) { $this->getSpecificity(); } } /** * @return string */ public function getSelector() { return $this->sSelector; } /** * @param string $sSelector * * @return void */ public function setSelector($sSelector) { $this->sSelector = trim($sSelector); $this->iSpecificity = null; } /** * @return string */ public function __toString() { return $this->getSelector(); } /** * @return int */ public function getSpecificity() { if ($this->iSpecificity === null) { $a = 0; /// @todo should exclude \# as well as "#" $aMatches = null; $b = substr_count($this->sSelector, '#'); $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; } return $this->iSpecificity; } } src/Property/Charset.php000066600000004440150513463720011275 0ustar00 */ protected $aComments; /** * @param string $sCharset * @param int $iLineNo */ public function __construct($sCharset, $iLineNo = 0) { $this->sCharset = $sCharset; $this->iLineNo = $iLineNo; $this->aComments = []; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * @param string $sCharset * * @return void */ public function setCharset($sCharset) { $this->sCharset = $sCharset; } /** * @return string */ public function getCharset() { return $this->sCharset; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return "@charset {$this->sCharset->render($oOutputFormat)};"; } /** * @return string */ public function atRuleName() { return 'charset'; } /** * @return string */ public function atRuleArgs() { return $this->sCharset; } /** * @param array $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array */ public function getComments() { return $this->aComments; } /** * @param array $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } src/Property/Import.php000066600000004760150513463720011163 0ustar00 */ protected $aComments; /** * @param URL $oLocation * @param string $sMediaQuery * @param int $iLineNo */ public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) { $this->oLocation = $oLocation; $this->sMediaQuery = $sMediaQuery; $this->iLineNo = $iLineNo; $this->aComments = []; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * @param URL $oLocation * * @return void */ public function setLocation($oLocation) { $this->oLocation = $oLocation; } /** * @return URL */ public function getLocation() { return $this->oLocation; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return "@import " . $this->oLocation->render($oOutputFormat) . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';'; } /** * @return string */ public function atRuleName() { return 'import'; } /** * @return array */ public function atRuleArgs() { $aResult = [$this->oLocation]; if ($this->sMediaQuery) { array_push($aResult, $this->sMediaQuery); } return $aResult; } /** * @param array $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array */ public function getComments() { return $this->aComments; } /** * @param array $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } src/Property/AtRule.php000066600000001441150513463720011076 0ustar00]* # any sequence of valid unescaped characters (?:\\\\.)? # a single escaped character (?:([\'"]).*?(?oFormat = $oFormat; } /** * @param string $sName * @param string|null $sType * * @return string */ public function space($sName, $sType = null) { $sSpaceString = $this->oFormat->get("Space$sName"); // If $sSpaceString is an array, we have multiple values configured // depending on the type of object the space applies to if (is_array($sSpaceString)) { if ($sType !== null && isset($sSpaceString[$sType])) { $sSpaceString = $sSpaceString[$sType]; } else { $sSpaceString = reset($sSpaceString); } } return $this->prepareSpace($sSpaceString); } /** * @return string */ public function spaceAfterRuleName() { return $this->space('AfterRuleName'); } /** * @return string */ public function spaceBeforeRules() { return $this->space('BeforeRules'); } /** * @return string */ public function spaceAfterRules() { return $this->space('AfterRules'); } /** * @return string */ public function spaceBetweenRules() { return $this->space('BetweenRules'); } /** * @return string */ public function spaceBeforeBlocks() { return $this->space('BeforeBlocks'); } /** * @return string */ public function spaceAfterBlocks() { return $this->space('AfterBlocks'); } /** * @return string */ public function spaceBetweenBlocks() { return $this->space('BetweenBlocks'); } /** * @return string */ public function spaceBeforeSelectorSeparator() { return $this->space('BeforeSelectorSeparator'); } /** * @return string */ public function spaceAfterSelectorSeparator() { return $this->space('AfterSelectorSeparator'); } /** * @param string $sSeparator * * @return string */ public function spaceBeforeListArgumentSeparator($sSeparator) { return $this->space('BeforeListArgumentSeparator', $sSeparator); } /** * @param string $sSeparator * * @return string */ public function spaceAfterListArgumentSeparator($sSeparator) { return $this->space('AfterListArgumentSeparator', $sSeparator); } /** * @return string */ public function spaceBeforeOpeningBrace() { return $this->space('BeforeOpeningBrace'); } /** * Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting. * * @param string $cCode the name of the function to call * * @return string|null */ public function safely($cCode) { if ($this->oFormat->get('IgnoreExceptions')) { // If output exceptions are ignored, run the code with exception guards try { return $cCode(); } catch (OutputException $e) { return null; } // Do nothing } else { // Run the code as-is return $cCode(); } } /** * Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`. * * @param string $sSeparator * @param array $aValues * @param bool $bIncreaseLevel * * @return string */ public function implode($sSeparator, array $aValues, $bIncreaseLevel = false) { $sResult = ''; $oFormat = $this->oFormat; if ($bIncreaseLevel) { $oFormat = $oFormat->nextLevel(); } $bIsFirst = true; foreach ($aValues as $mValue) { if ($bIsFirst) { $bIsFirst = false; } else { $sResult .= $sSeparator; } if ($mValue instanceof Renderable) { $sResult .= $mValue->render($oFormat); } else { $sResult .= $mValue; } } return $sResult; } /** * @param string $sString * * @return string */ public function removeLastSemicolon($sString) { if ($this->oFormat->get('SemicolonAfterLastRule')) { return $sString; } $sString = explode(';', $sString); if (count($sString) < 2) { return $sString[0]; } $sLast = array_pop($sString); $sNextToLast = array_pop($sString); array_push($sString, $sNextToLast . $sLast); return implode(';', $sString); } /** * @param string $sSpaceString * * @return string */ private function prepareSpace($sSpaceString) { return str_replace("\n", "\n" . $this->indent(), $sSpaceString); } /** * @return string */ private function indent() { return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); } } src/Parser.php000066600000002457150513463720007322 0ustar00oParserState = new ParserState($sText, $oParserSettings, $iLineNo); } /** * @param string $sCharset * * @return void */ public function setCharset($sCharset) { $this->oParserState->setCharset($sCharset); } /** * @return void */ public function getCharset() { // Note: The `return` statement is missing here. This is a bug that needs to be fixed. $this->oParserState->getCharset(); } /** * @return Document * * @throws SourceException */ public function parse() { return Document::parse($this->oParserState); } } src/Renderable.php000066600000000450150513463720010120 0ustar00parse(); ``` The resulting CSS document structure can be manipulated prior to being output. ### Options #### Charset The charset option is used only if no `@charset` declaration is found in the CSS file. UTF-8 is the default, so you won’t have to create a settings object at all if you don’t intend to change that. ```php $settings = \Sabberworm\CSS\Settings::create() ->withDefaultCharset('windows-1252'); $parser = new \Sabberworm\CSS\Parser($css, $settings); ``` #### Strict parsing To have the parser choke on invalid rules, supply a thusly configured `\Sabberworm\CSS\Settings` object: ```php $parser = new \Sabberworm\CSS\Parser( file_get_contents('somefile.css'), \Sabberworm\CSS\Settings::create()->beStrict() ); ``` #### Disable multibyte functions To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of `mb_*` functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still it’s not recommended using this with input you have no control over as it’s not thoroughly covered by test cases. ```php $settings = \Sabberworm\CSS\Settings::create()->withMultibyteSupport(false); $parser = new \Sabberworm\CSS\Parser($css, $settings); ``` ### Manipulation The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset`, which you won’t use often. #### CSSList `CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector), but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: * `Document` – representing the root of a CSS file. * `MediaQuery` – represents a subsection of a `CSSList` that only applies to an output device matching the contained media query. To access the items stored in a `CSSList` – like the document you got back when calling `$parser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `RuleSet`, a `Import` or a `Charset`. To append a new item (selector, media query, etc.) to an existing `CSSList`, construct it using the constructor for this class and use the `append($oItem)` method. #### RuleSet `RuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist: * `AtRuleSet` – for generic at-rules which do not match the ones specifically mentioned like `@import`, `@charset` or `@media`. A common example for this is `@font-face`. * `DeclarationBlock` – a `RuleSet` constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements. Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`), while a `RuleSet` can only contain `Rule`s. If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)` (which accepts either a `Rule` instance or a rule name; optionally suffixed by a dash to remove all related rules). #### Rule `Rule`s just have a key (the rule) and a value. These values are all instances of a `Value`. #### Value `Value` is an abstract class that only defines the `render` method. The concrete subclasses for atomic value types are: * `Size` – consists of a numeric `size` value and a unit. * `Color` – colors can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are always stored as an array of ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form. * `CSSString` – this is just a wrapper for quoted strings to distinguish them from keywords; always output with double quotes. * `URL` – URLs in CSS; always output in URL("") notation. There is another abstract subclass of `Value`, `ValueList`. A `ValueList` represents a lists of `Value`s, separated by some separation character (mostly `,`, whitespace, or `/`). There are two types of `ValueList`s: * `RuleValueList` – The default type, used to represent all multi-valued rules like `font: bold 12px/3 Helvetica, Verdana, sans-serif;` (where the value would be a whitespace-separated list of the primitive value `bold`, a slash-separated list and a comma-separated list). * `CSSFunction` – A special kind of value that also contains a function name and where the values are the function’s arguments. Also handles equals-sign-separated argument lists like `filter: alpha(opacity=90);`. #### Convenience methods There are a few convenience methods on Document to ease finding, manipulating and deleting rules: * `getAllDeclarationBlocks()` – does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`. * `getAllRuleSets()` – does what it says; no matter how deeply nested your rule sets are. * `getAllValues()` – finds all `Value` objects inside `Rule`s. ## To-Do * More convenience methods (like `selectorsWithElement($sId/Class/TagName)`, `attributesOfType($type)`, `removeAttributesOfType($type)`) * Real multibyte support. Currently, only multibyte charsets whose first 255 code points take up only one byte and are identical with ASCII are supported (yes, UTF-8 fits this description). * Named color support (using `Color` instead of an anonymous string literal) ## Use cases ### Use `Parser` to prepend an ID to all selectors ```php $myId = "#my_id"; $parser = new \Sabberworm\CSS\Parser($css); $cssDocument = $parser->parse(); foreach ($cssDocument->getAllDeclarationBlocks() as $block) { foreach ($block->getSelectors() as $selector) { // Loop over all selector parts (the comma-separated strings in a // selector) and prepend the ID. $selector->setSelector($myId.' '.$selector->getSelector()); } } ``` ### Shrink all absolute sizes to half ```php $parser = new \Sabberworm\CSS\Parser($css); $cssDocument = $parser->parse(); foreach ($cssDocument->getAllValues() as $value) { if ($value instanceof CSSSize && !$value->isRelative()) { $value->setSize($value->getSize() / 2); } } ``` ### Remove unwanted rules ```php $parser = new \Sabberworm\CSS\Parser($css); $cssDocument = $parser->parse(); foreach($cssDocument->getAllRuleSets() as $oRuleSet) { // Note that the added dash will make this remove all rules starting with // `font-` (like `font-size`, `font-weight`, etc.) as well as a potential // `font-rule`. $oRuleSet->removeRule('font-'); $oRuleSet->removeRule('cursor'); } ``` ### Output To output the entire CSS document into a variable, just use `->render()`: ```php $parser = new \Sabberworm\CSS\Parser(file_get_contents('somefile.css')); $cssDocument = $parser->parse(); print $cssDocument->render(); ``` If you want to format the output, pass an instance of type `\Sabberworm\CSS\OutputFormat`: ```php $format = \Sabberworm\CSS\OutputFormat::create() ->indentWithSpaces(4)->setSpaceBetweenRules("\n"); print $cssDocument->render($format); ``` Or use one of the predefined formats: ```php print $cssDocument->render(Sabberworm\CSS\OutputFormat::createPretty()); print $cssDocument->render(Sabberworm\CSS\OutputFormat::createCompact()); ``` To see what you can do with output formatting, look at the tests in `tests/OutputFormatTest.php`. ## Examples ### Example 1 (At-Rules) #### Input ```css @charset "utf-8"; @font-face { font-family: "CrassRoots"; src: url("../media/cr.ttf"); } html, body { font-size: 1.6em; } @keyframes mymove { from { top: 0px; } to { top: 200px; } } ``` #### Structure (`var_dump()`) ```php class Sabberworm\CSS\CSSList\Document#4 (2) { protected $aContents => array(4) { [0] => class Sabberworm\CSS\Property\Charset#6 (2) { private $sCharset => class Sabberworm\CSS\Value\CSSString#5 (2) { private $sString => string(5) "utf-8" protected $iLineNo => int(1) } protected $iLineNo => int(1) } [1] => class Sabberworm\CSS\RuleSet\AtRuleSet#7 (4) { private $sType => string(9) "font-face" private $sArgs => string(0) "" private $aRules => array(2) { 'font-family' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#8 (4) { private $sRule => string(11) "font-family" private $mValue => class Sabberworm\CSS\Value\CSSString#9 (2) { private $sString => string(10) "CrassRoots" protected $iLineNo => int(4) } private $bIsImportant => bool(false) protected $iLineNo => int(4) } } 'src' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#10 (4) { private $sRule => string(3) "src" private $mValue => class Sabberworm\CSS\Value\URL#11 (2) { private $oURL => class Sabberworm\CSS\Value\CSSString#12 (2) { private $sString => string(15) "../media/cr.ttf" protected $iLineNo => int(5) } protected $iLineNo => int(5) } private $bIsImportant => bool(false) protected $iLineNo => int(5) } } } protected $iLineNo => int(3) } [2] => class Sabberworm\CSS\RuleSet\DeclarationBlock#13 (3) { private $aSelectors => array(2) { [0] => class Sabberworm\CSS\Property\Selector#14 (2) { private $sSelector => string(4) "html" private $iSpecificity => NULL } [1] => class Sabberworm\CSS\Property\Selector#15 (2) { private $sSelector => string(4) "body" private $iSpecificity => NULL } } private $aRules => array(1) { 'font-size' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#16 (4) { private $sRule => string(9) "font-size" private $mValue => class Sabberworm\CSS\Value\Size#17 (4) { private $fSize => double(1.6) private $sUnit => string(2) "em" private $bIsColorComponent => bool(false) protected $iLineNo => int(9) } private $bIsImportant => bool(false) protected $iLineNo => int(9) } } } protected $iLineNo => int(8) } [3] => class Sabberworm\CSS\CSSList\KeyFrame#18 (4) { private $vendorKeyFrame => string(9) "keyframes" private $animationName => string(6) "mymove" protected $aContents => array(2) { [0] => class Sabberworm\CSS\RuleSet\DeclarationBlock#19 (3) { private $aSelectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#20 (2) { private $sSelector => string(4) "from" private $iSpecificity => NULL } } private $aRules => array(1) { 'top' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#21 (4) { private $sRule => string(3) "top" private $mValue => class Sabberworm\CSS\Value\Size#22 (4) { private $fSize => double(0) private $sUnit => string(2) "px" private $bIsColorComponent => bool(false) protected $iLineNo => int(13) } private $bIsImportant => bool(false) protected $iLineNo => int(13) } } } protected $iLineNo => int(13) } [1] => class Sabberworm\CSS\RuleSet\DeclarationBlock#23 (3) { private $aSelectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#24 (2) { private $sSelector => string(2) "to" private $iSpecificity => NULL } } private $aRules => array(1) { 'top' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#25 (4) { private $sRule => string(3) "top" private $mValue => class Sabberworm\CSS\Value\Size#26 (4) { private $fSize => double(200) private $sUnit => string(2) "px" private $bIsColorComponent => bool(false) protected $iLineNo => int(14) } private $bIsImportant => bool(false) protected $iLineNo => int(14) } } } protected $iLineNo => int(14) } } protected $iLineNo => int(12) } } protected $iLineNo => int(1) } ``` #### Output (`render()`) ```css @charset "utf-8"; @font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} html, body {font-size: 1.6em;} @keyframes mymove {from {top: 0px;} to {top: 200px;}} ``` ### Example 2 (Values) #### Input ```css #header { margin: 10px 2em 1cm 2%; font-family: Verdana, Helvetica, "Gill Sans", sans-serif; color: red !important; } ``` #### Structure (`var_dump()`) ```php class Sabberworm\CSS\CSSList\Document#4 (2) { protected $aContents => array(1) { [0] => class Sabberworm\CSS\RuleSet\DeclarationBlock#5 (3) { private $aSelectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#6 (2) { private $sSelector => string(7) "#header" private $iSpecificity => NULL } } private $aRules => array(3) { 'margin' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#7 (4) { private $sRule => string(6) "margin" private $mValue => class Sabberworm\CSS\Value\RuleValueList#12 (3) { protected $aComponents => array(4) { [0] => class Sabberworm\CSS\Value\Size#8 (4) { private $fSize => double(10) private $sUnit => string(2) "px" private $bIsColorComponent => bool(false) protected $iLineNo => int(2) } [1] => class Sabberworm\CSS\Value\Size#9 (4) { private $fSize => double(2) private $sUnit => string(2) "em" private $bIsColorComponent => bool(false) protected $iLineNo => int(2) } [2] => class Sabberworm\CSS\Value\Size#10 (4) { private $fSize => double(1) private $sUnit => string(2) "cm" private $bIsColorComponent => bool(false) protected $iLineNo => int(2) } [3] => class Sabberworm\CSS\Value\Size#11 (4) { private $fSize => double(2) private $sUnit => string(1) "%" private $bIsColorComponent => bool(false) protected $iLineNo => int(2) } } protected $sSeparator => string(1) " " protected $iLineNo => int(2) } private $bIsImportant => bool(false) protected $iLineNo => int(2) } } 'font-family' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#13 (4) { private $sRule => string(11) "font-family" private $mValue => class Sabberworm\CSS\Value\RuleValueList#15 (3) { protected $aComponents => array(4) { [0] => string(7) "Verdana" [1] => string(9) "Helvetica" [2] => class Sabberworm\CSS\Value\CSSString#14 (2) { private $sString => string(9) "Gill Sans" protected $iLineNo => int(3) } [3] => string(10) "sans-serif" } protected $sSeparator => string(1) "," protected $iLineNo => int(3) } private $bIsImportant => bool(false) protected $iLineNo => int(3) } } 'color' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#16 (4) { private $sRule => string(5) "color" private $mValue => string(3) "red" private $bIsImportant => bool(true) protected $iLineNo => int(4) } } } protected $iLineNo => int(1) } } protected $iLineNo => int(1) } ``` #### Output (`render()`) ```css #header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;} ``` ## Contributors/Thanks to * [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations * [raxbg](https://github.com/raxbg) for contributions to parse `calc`, grid lines, and various bugfixes. * [westonruter](https://github.com/westonruter) for bugfixes and improvements. * [FMCorz](https://github.com/FMCorz) for many patches and suggestions, for being able to parse comments and IE hacks (in lenient mode). * [Lullabot](https://github.com/Lullabot) for a patch that allows to know the line number for each parsed token. * [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties. * [ossinkine](https://github.com/ossinkine) for a 150 time performance boost. * [GaryJones](https://github.com/GaryJones) for lots of input and [https://css-specificity.info/](https://css-specificity.info/). * [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration. * [nicolopignatelli](https://github.com/nicolopignatelli) for PSR-0 compatibility. * [diegoembarcadero](https://github.com/diegoembarcadero) for keyframe at-rule parsing. * [goetas](https://github.com/goetas) for @namespace at-rule support. * [View full list](https://github.com/sabberworm/PHP-CSS-Parser/contributors) ## Misc * Legacy Support: The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag. * Running Tests: To run all unit tests for this project, run `composer install` to install phpunit and use `./vendor/bin/phpunit`. CHANGELOG.md000066600000020740150515642400006366 0ustar00# Revision History ## 8.4.0 ### Features * Support for PHP 8.x * PHPDoc annotations * Allow usage of CSS variables inside color functions (by parsing them as regular functions) * Use PSR-12 code style * *No deprecations* ### Bugfixes * Improved handling of whitespace in `calc()` * Fix parsing units whose prefix is also a valid unit, like `vmin` * Allow passing an object to `CSSList#replace` * Fix PHP 7.3 warnings * Correctly parse keyframes with `%` * Don’t convert large numbers to scientific notation * Allow a file to end after an `@import` * Preserve case of CSS variables as specced * Allow identifiers to use escapes the same way as strings * No longer use `eval` for the comparison in `getSelectorsBySpecificity`, in case it gets passed untrusted input (CVE-2020-13756). Also fixed in 8.3.1, 8.2.1, 8.1.1, 8.0.1, 7.0.4, 6.0.2, 5.2.1, 5.1.3, 5.0.9, 4.0.1, 3.0.1, 2.0.1, 1.0.1. * Prevent an infinite loop when parsing invalid grid line names * Remove invalid unit `vm` * Retain rule order after expanding shorthands ### Backwards-incompatible changes * PHP ≥ 5.6 is now required * HHVM compatibility target dropped ## 8.3.0 (2019-02-22) * Refactor parsing logic to mostly reside in the class files whose data structure is to be parsed (this should eventually allow us to unit-test specific parts of the parsing logic individually). * Fix error in parsing `calc` expessions when the first operand is a negative number, thanks to @raxbg. * Support parsing CSS4 colors in hex notation with alpha values, thanks to @raxbg. * Swallow more errors in lenient mode, thanks to @raxbg. * Allow specifying arbitrary strings to output before and after declaration blocks, thanks to @westonruter. * *No backwards-incompatible changes* * *No deprecations* ## 8.2.0 (2018-07-13) * Support parsing `calc()`, thanks to @raxbg. * Support parsing grid-lines, again thanks to @raxbg. * Support parsing legacy IE filters (`progid:`) in lenient mode, thanks to @FMCorz * Performance improvements parsing large files, again thanks to @FMCorz * *No backwards-incompatible changes* * *No deprecations* ## 8.1.0 (2016-07-19) * Comments are no longer silently ignored but stored with the object with which they appear (no render support, though). Thanks to @FMCorz. * The IE hacks using `\0` and `\9` can now be parsed (and rendered) in lenient mode. Thanks (again) to @FMCorz. * Media queries with or without spaces before the query are parsed. Still no *real* parsing support, though. Sorry… * PHPUnit is now listed as a dev-dependency in composer.json. * *No backwards-incompatible changes* * *No deprecations* ## 8.0.0 (2016-06-30) * Store source CSS line numbers in tokens and parsing exceptions. * *No deprecations* ### Backwards-incompatible changes * Unrecoverable parser errors throw an exception of type `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`. ## 7.0.3 (2016-04-27) * Fixed parsing empty CSS when multibyte is off * *No backwards-incompatible changes* * *No deprecations* ## 7.0.2 (2016-02-11) * 150 time performance boost thanks to @[ossinkine](https://github.com/ossinkine) * *No backwards-incompatible changes* * *No deprecations* ## 7.0.1 (2015-12-25) * No more suppressed `E_NOTICE` * *No backwards-incompatible changes* * *No deprecations* ## 7.0.0 (2015-08-24) * Compatibility with PHP 7. Well timed, eh? * *No deprecations* ### Backwards-incompatible changes * The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`. ## 6.0.1 (2015-08-24) * Remove some declarations in interfaces incompatible with PHP 5.3 (< 5.3.9) * *No deprecations* ## 6.0.0 (2014-07-03) * Format output using Sabberworm\CSS\OutputFormat * *No backwards-incompatible changes* ### Deprecations * The parse() method replaces __toString with an optional argument (instance of the OutputFormat class) ## 5.2.0 (2014-06-30) * Support removing a selector from a declaration block using `$oBlock->removeSelector($mSelector)` * Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for exceptions during output rendering * *No deprecations* #### Backwards-incompatible changes * Outputting a declaration block that has no selectors throws an OuputException instead of outputting an invalid ` {…}` into the CSS document. ## 5.1.2 (2013-10-30) * Remove the use of consumeUntil in comment parsing. This makes it possible to parse comments such as `/** Perfectly valid **/` * Add fr relative size unit * Fix some issues with HHVM * *No backwards-incompatible changes* * *No deprecations* ## 5.1.1 (2013-10-28) * Updated CHANGELOG.md to reflect changes since 5.0.4 * *No backwards-incompatible changes* * *No deprecations* ## 5.1.0 (2013-10-24) * Performance enhancements by Michael M Slusarz * More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments) * *No backwards-incompatible changes* * *No deprecations* ## 5.0.8 (2013-08-15) * Make default settings’ multibyte parsing option dependent on whether or not the mbstring extension is actually installed. * *No backwards-incompatible changes* * *No deprecations* ## 5.0.7 (2013-08-04) * Fix broken decimal point output optimization * *No backwards-incompatible changes* * *No deprecations* ## 5.0.6 (2013-05-31) * Fix broken unit test * *No backwards-incompatible changes* * *No deprecations* ## 5.0.5 (2013-04-17) * Initial support for lenient parsing (setting this parser option will catch some exceptions internally and recover the parser’s state as neatly as possible). * *No backwards-incompatible changes* * *No deprecations* ## 5.0.4 (2013-03-21) * Don’t output floats with locale-aware separator chars * *No backwards-incompatible changes* * *No deprecations* ## 5.0.3 (2013-03-21) * More size units recognized * *No backwards-incompatible changes* * *No deprecations* ## 5.0.2 (2013-03-21) * CHANGELOG.md file added to distribution * *No backwards-incompatible changes* * *No deprecations* ## 5.0.1 (2013-03-20) * Internal cleanup * *No backwards-incompatible changes* * *No deprecations* ## 5.0.0 (2013-03-20) * Correctly parse all known CSS 3 units (including Hz and kHz). * Output RGB colors in short (#aaa or #ababab) notation * Be case-insensitive when parsing identifiers. * *No deprecations* ### Backwards-incompatible changes * `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to maybe return something other than `type(value, …)` (see above). ## 4.0.0 (2013-03-19) * Support for more @-rules * Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule classes * *No deprecations* ### Backwards-incompatible changes * `Sabberworm\CSS\RuleSet\AtRule` renamed to `Sabberworm\CSS\RuleSet\AtRuleSet` * `Sabberworm\CSS\CSSList\MediaQuery` renamed to `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and API (which also works for other block-list-based @-rules like `@supports`). ## 3.0.0 (2013-03-06) * Support for lenient parsing (on by default) * *No deprecations* ### Backwards-incompatible changes * All properties (like whether or not to use `mb_`-functions, which default charset to use and – new – whether or not to be forgiving when parsing) are now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be passed as the second argument to `Sabberworm\CSS\Parser->__construct()`. * Specifying a charset as the second argument to `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')` instead. * Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead. * `Sabberworm\CSS\Parser->parse()` may throw a `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode. ## 2.0.0 (2013-01-29) * Allow multiple rules of the same type per rule set ### Backwards-incompatible changes * `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which eliminates duplicate rules and lets the later rule of the same name win). * `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only remove the exact rule given instead of all the rules of the same type. To get the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`; ## 1.0 Initial release of a stable public API. ## 0.9 Last version not to use PSR-0 project organization semantics. composer.json000066600000004740150515642400007301 0ustar00{ "name": "sabberworm/php-css-parser", "type": "library", "description": "Parser for CSS Files written in PHP", "keywords": [ "parser", "css", "stylesheet" ], "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", "license": "MIT", "authors": [ { "name": "Raphael Schweikert" } ], "require": { "php": ">=5.6.20", "ext-iconv": "*" }, "require-dev": { "phpunit/phpunit": "^4.8.36", "codacy/coverage": "^1.4" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" }, "autoload": { "psr-4": { "Sabberworm\\CSS\\": "src/" } }, "autoload-dev": { "psr-4": { "Sabberworm\\CSS\\Tests\\": "tests/" } }, "scripts": { "ci": [ "@ci:static" ], "ci:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots bin src tests", "ci:php:sniffer": "@php ./.phive/phpcs.phar --standard=config/phpcs.xml bin src tests", "ci:php:stan": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon", "ci:static": [ "@ci:php:fixer", "@ci:php:sniffer", "@ci:php:stan" ], "fix:php": [ "@fix:php:fixer", "@fix:php:sniffer" ], "fix:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix bin src tests", "fix:php:sniffer": "@php ./.phive/phpcbf.phar --standard=config/phpcs.xml bin src tests", "phpstan:baseline": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon" }, "scripts-descriptions": { "ci": "Runs all dynamic and static code checks (i.e. currently, only the static checks).", "ci:php:fixer": "Checks the code style with PHP CS Fixer.", "ci:php:sniffer": "Checks the code style with PHP_CodeSniffer.", "ci:php:stan": "Checks the types with PHPStan.", "ci:static": "Runs all static code analysis checks for the code.", "fix:php": "Autofixes all autofixable issues in the PHP code.", "fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.", "fix:php:sniffer": "Fixes autofixable issues found by PHP_CodeSniffer.", "phpstand:baseline": "Updates the PHPStan baseline file to match the code." } }