From b4e4fbc06bcee07fdf1221f1553e9f7ae3d96198 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 3 May 2021 20:55:57 +0200 Subject: [PATCH] Autofix all autofixable PSR-12 code style issues These changes were all automatically done by PHP_CodeSniffer, and there are no manual changes. --- .../CSS/CSSList/AtRuleBlockList.php | 114 +- lib/Sabberworm/CSS/CSSList/CSSBlockList.php | 179 +- lib/Sabberworm/CSS/CSSList/CSSList.php | 683 ++++---- lib/Sabberworm/CSS/CSSList/Document.php | 219 +-- lib/Sabberworm/CSS/CSSList/KeyFrame.php | 121 +- lib/Sabberworm/CSS/Comment/Comment.php | 94 +- lib/Sabberworm/CSS/Comment/Commentable.php | 29 +- lib/Sabberworm/CSS/OutputFormat.php | 650 ++++---- lib/Sabberworm/CSS/Parser.php | 71 +- .../CSS/Parsing/OutputException.php | 12 +- lib/Sabberworm/CSS/Parsing/ParserState.php | 664 ++++---- .../CSS/Parsing/SourceException.php | 29 +- .../CSS/Parsing/UnexpectedEOFException.php | 4 +- .../CSS/Parsing/UnexpectedTokenException.php | 46 +- lib/Sabberworm/CSS/Property/AtRule.php | 29 +- lib/Sabberworm/CSS/Property/CSSNamespace.php | 146 +- lib/Sabberworm/CSS/Property/Charset.php | 144 +- lib/Sabberworm/CSS/Property/Import.php | 194 +-- .../CSS/Property/KeyframeSelector.php | 8 +- lib/Sabberworm/CSS/Property/Selector.php | 84 +- lib/Sabberworm/CSS/Renderable.php | 21 +- lib/Sabberworm/CSS/Rule/Rule.php | 501 +++--- lib/Sabberworm/CSS/RuleSet/AtRuleSet.php | 107 +- .../CSS/RuleSet/DeclarationBlock.php | 1289 ++++++++------- lib/Sabberworm/CSS/RuleSet/RuleSet.php | 455 +++--- lib/Sabberworm/CSS/Settings.php | 93 +- lib/Sabberworm/CSS/Value/CSSFunction.php | 86 +- lib/Sabberworm/CSS/Value/CSSString.php | 120 +- lib/Sabberworm/CSS/Value/CalcFunction.php | 113 +- .../CSS/Value/CalcRuleValueList.php | 28 +- lib/Sabberworm/CSS/Value/Color.php | 229 +-- lib/Sabberworm/CSS/Value/LineName.php | 78 +- lib/Sabberworm/CSS/Value/PrimitiveValue.php | 9 +- lib/Sabberworm/CSS/Value/RuleValueList.php | 12 +- lib/Sabberworm/CSS/Value/Size.php | 252 +-- lib/Sabberworm/CSS/Value/URL.php | 102 +- lib/Sabberworm/CSS/Value/Value.php | 234 +-- lib/Sabberworm/CSS/Value/ValueList.php | 102 +- .../CSS/CSSList/AtRuleBlockListTest.php | 37 +- tests/Sabberworm/CSS/CSSList/DocumentTest.php | 33 +- tests/Sabberworm/CSS/OutputFormatTest.php | 189 ++- tests/Sabberworm/CSS/ParserTest.php | 1444 +++++++++-------- .../CSS/RuleSet/DeclarationBlockTest.php | 580 +++---- .../CSS/RuleSet/LenientParsingTest.php | 122 +- tests/bootstrap.php | 5 +- tests/quickdump.php | 8 +- 46 files changed, 5097 insertions(+), 4672 deletions(-) diff --git a/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php b/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php index dbcc99c6..37fa3140 100644 --- a/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php +++ b/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php @@ -7,64 +7,70 @@ /** * A BlockList constructed by an unknown @-rule. @media rules are rendered into AtRuleBlockList objects. */ -class AtRuleBlockList extends CSSBlockList implements AtRule { - /** - * @var string - */ - private $sType; +class AtRuleBlockList extends CSSBlockList implements AtRule +{ + /** + * @var string + */ + private $sType; - /** - * @var string - */ - private $sArgs; + /** + * @var string + */ + private $sArgs; - /** - * @param string $sType - * @param string $sArgs - * @param int $iLineNo - */ - public function __construct($sType, $sArgs = '', $iLineNo = 0) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; - } + /** + * @param string $sType + * @param string $sArgs + * @param int $iLineNo + */ + public function __construct($sType, $sArgs = '', $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->sType = $sType; + $this->sArgs = $sArgs; + } - /** - * @return string - */ - public function atRuleName() { - return $this->sType; - } + /** + * @return string + */ + public function atRuleName() + { + return $this->sType; + } - /** - * @return string - */ - public function atRuleArgs() { - return $this->sArgs; - } + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sArgs; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } - /** - * @return string - */ - public function render(\Sabberworm\CSS\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 string + */ + public function render(\Sabberworm\CSS\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; + } - public function isRootList() { - return false; - } - -} \ No newline at end of file + public function isRootList() + { + return false; + } +} diff --git a/lib/Sabberworm/CSS/CSSList/CSSBlockList.php b/lib/Sabberworm/CSS/CSSList/CSSBlockList.php index 15742423..67b490b5 100644 --- a/lib/Sabberworm/CSS/CSSList/CSSBlockList.php +++ b/lib/Sabberworm/CSS/CSSList/CSSBlockList.php @@ -13,95 +13,100 @@ * A CSSBlockList is a CSSList whose DeclarationBlocks are guaranteed to contain valid declaration blocks or at-rules. * Most CSSLists conform to this category but some at-rules (such as @keyframes) do not. */ -abstract class CSSBlockList extends CSSList { - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - } +abstract class CSSBlockList extends CSSList +{ + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + } - protected function allDeclarationBlocks(&$aResult) { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof DeclarationBlock) { - $aResult[] = $mContent; - } else if ($mContent instanceof CSSBlockList) { - $mContent->allDeclarationBlocks($aResult); - } - } - } + protected function allDeclarationBlocks(&$aResult) + { + foreach ($this->aContents as $mContent) { + if ($mContent instanceof DeclarationBlock) { + $aResult[] = $mContent; + } elseif ($mContent instanceof CSSBlockList) { + $mContent->allDeclarationBlocks($aResult); + } + } + } - protected function allRuleSets(&$aResult) { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof RuleSet) { - $aResult[] = $mContent; - } else if ($mContent instanceof CSSBlockList) { - $mContent->allRuleSets($aResult); - } - } - } + protected function allRuleSets(&$aResult) + { + foreach ($this->aContents as $mContent) { + if ($mContent instanceof RuleSet) { + $aResult[] = $mContent; + } elseif ($mContent instanceof CSSBlockList) { + $mContent->allRuleSets($aResult); + } + } + } - protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { - if ($oElement instanceof CSSBlockList) { - foreach ($oElement->getContents() as $oContent) { - $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); - } - } else if ($oElement instanceof RuleSet) { - foreach ($oElement->getRules($sSearchString) as $oRule) { - $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); - } - } else if ($oElement instanceof Rule) { - $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); - } else if ($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; - } - } - - protected function allSelectors(&$aResult, $sSpecificitySearch = null) { - $aDeclarationBlocks = array(); - $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; - } - } - } - } - } + protected function allValues($oElement, &$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; + } + } + protected function allSelectors(&$aResult, $sSpecificitySearch = null) + { + $aDeclarationBlocks = array(); + $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; + } + } + } + } + } } diff --git a/lib/Sabberworm/CSS/CSSList/CSSList.php b/lib/Sabberworm/CSS/CSSList/CSSList.php index a58fda50..100f11cc 100644 --- a/lib/Sabberworm/CSS/CSSList/CSSList.php +++ b/lib/Sabberworm/CSS/CSSList/CSSList.php @@ -24,361 +24,380 @@ * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects. * Also, it may contain Import and Charset objects stemming from @-rules. */ -abstract class CSSList implements Renderable, Commentable { - /** - * @var array - */ - protected $aComments; +abstract class CSSList implements Renderable, Commentable +{ + /** + * @var array + */ + protected $aComments; - /** - * @var array - */ - protected $aContents; + /** + * @var array + */ + protected $aContents; - /** - * @var int - */ - protected $iLineNo; + /** + * @var int + */ + protected $iLineNo; - /** - * @param int $iLineNo - */ - public function __construct($iLineNo = 0) { - $this->aComments = array(); - $this->aContents = array(); - $this->iLineNo = $iLineNo; - } + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + $this->aComments = array(); + $this->aContents = array(); + $this->iLineNo = $iLineNo; + } - public static function parseList(ParserState $oParserState, CSSList $oList) { - $bIsRoot = $oList instanceof Document; - if(is_string($oParserState)) { - $oParserState = new ParserState($oParserState); - } - $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()); - } - } + public static function parseList(ParserState $oParserState, CSSList $oList) + { + $bIsRoot = $oList instanceof Document; + if (is_string($oParserState)) { + $oParserState = new ParserState($oParserState); + } + $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()); + } + } - 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; - } else if ($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); - } - } + 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); + } + } - 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(array(';', ParserState::EOF))); - } - $oParserState->consumeUntil(array(';', ParserState::EOF), true, true); - return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); - } else if ($sIdentifier === 'charset') { - $sCharset = CSSString::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $oParserState->consumeUntil(array(';', ParserState::EOF), true, true); - return new Charset($sCharset, $iIdentifierLineNum); - } else if (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; - } else if ($sIdentifier === 'namespace') { - $sPrefix = null; - $mUrl = Value::parsePrimitiveValue($oParserState); - if (!$oParserState->comes(';')) { - $sPrefix = $mUrl; - $mUrl = Value::parsePrimitiveValue($oParserState); - } - $oParserState->consumeUntil(array(';', 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; - } - } + 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(array(';', ParserState::EOF))); + } + $oParserState->consumeUntil(array(';', ParserState::EOF), true, true); + return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); + } elseif ($sIdentifier === 'charset') { + $sCharset = CSSString::parse($oParserState); + $oParserState->consumeWhiteSpace(); + $oParserState->consumeUntil(array(';', 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(array(';', 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. - */ - private static function identifierIs($sIdentifier, $sMatch) { - return (strcasecmp($sIdentifier, $sMatch) === 0) - ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; - } + /** + * 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. + */ + 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; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - /** - * Prepend item to list of contents. - * - * @param RuleSet|Import|Charset|CSSList $oItem Item. - */ - public function prepend($oItem) { - array_unshift($this->aContents, $oItem); - } + /** + * Prepend item to list of contents. + * + * @param RuleSet|Import|Charset|CSSList $oItem Item. + */ + public function prepend($oItem) + { + array_unshift($this->aContents, $oItem); + } - /** - * Append item to list of contents. - * - * @param RuleSet|Import|Charset|CSSList $oItem Item. - */ - public function append($oItem) { - $this->aContents[] = $oItem; - } + /** + * Append item to list of contents. + * + * @param RuleSet|Import|Charset|CSSList $oItem Item. + */ + public function append($oItem) + { + $this->aContents[] = $oItem; + } - /** - * Splice the list of contents. - * - * @param int $iOffset Offset. - * @param int $iLength Length. Optional. - * @param RuleSet[] $mReplacement Replacement. Optional. - */ - public function splice($iOffset, $iLength = null, $mReplacement = null) { - array_splice($this->aContents, $iOffset, $iLength, $mReplacement); - } + /** + * Splice the list of contents. + * + * @param int $iOffset Offset. + * @param int $iLength Length. Optional. + * @param RuleSet[] $mReplacement Replacement. Optional. + */ + 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; - } + /** + * 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 $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery) - */ - 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, array($mNewItem)); - } - return true; - } - return false; - } + /** + * Replaces 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) + */ + 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, array($mNewItem)); + } + return true; + } + return false; + } - /** - * Set the contents. - * @param array $aContents Objects to set as content. - */ - public function setContents(array $aContents) { - $this->aContents = array(); - foreach ($aContents as $content) { - $this->append($content); - } - } + /** + * Set the contents. + * @param array $aContents Objects to set as content. + */ + public function setContents(array $aContents) + { + $this->aContents = array(); + foreach ($aContents as $content) { + $this->append($content); + } + } - /** - * Removes a declaration block from the CSS list if it matches all given selectors. - * @param array|string $mSelector The selectors to match. - * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks - */ - 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; - } - } - } - } + /** + * Removes a declaration block from the CSS list if it matches all given selectors. + * @param array|string $mSelector The selectors to match. + * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks + */ + 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; + } + } + } + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } - /** - * @return string - */ - public function render(\Sabberworm\CSS\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; - } + /** + * @return string + */ + public function render(\Sabberworm\CSS\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(); - } + if (!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterBlocks(); + } - return $sResult; - } + return $sResult; + } - /** - * Return true if the list can not be further outdented. Only important when rendering. - */ - public abstract function isRootList(); + /** + * Return true if the list can not be further outdented. Only important when rendering. + */ + abstract public function isRootList(); - /** - * @return array - */ - public function getContents() { - return $this->aContents; - } + /** + * @return array + */ + public function getContents() + { + return $this->aContents; + } - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + /** + * @param array $aComments Array of comments. + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + /** + * @param array $aComments Array containing Comment objects. + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } } diff --git a/lib/Sabberworm/CSS/CSSList/Document.php b/lib/Sabberworm/CSS/CSSList/Document.php index 8abcb358..55932f19 100644 --- a/lib/Sabberworm/CSS/CSSList/Document.php +++ b/lib/Sabberworm/CSS/CSSList/Document.php @@ -7,118 +7,129 @@ /** * The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered. */ -class Document extends CSSBlockList { - /** - * Document constructor. - * @param int $iLineNo - */ - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - } +class Document extends CSSBlockList +{ + /** + * Document constructor. + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + } - /** - * @param ParserState $oParserState - * - * @return Document - * - * @throws \Sabberworm\CSS\Parsing\SourceException - */ - public static function parse(ParserState $oParserState) { - $oDocument = new Document($oParserState->currentLine()); - CSSList::parseList($oParserState, $oDocument); - return $oDocument; - } + /** + * @param ParserState $oParserState + * + * @return Document + * + * @throws \Sabberworm\CSS\Parsing\SourceException + */ + public static function parse(ParserState $oParserState) + { + $oDocument = new Document($oParserState->currentLine()); + CSSList::parseList($oParserState, $oDocument); + return $oDocument; + } - /** - * Gets all DeclarationBlock objects recursively. - */ - public function getAllDeclarationBlocks() { - $aResult = array(); - $this->allDeclarationBlocks($aResult); - return $aResult; - } + /** + * Gets all DeclarationBlock objects recursively. + */ + public function getAllDeclarationBlocks() + { + $aResult = array(); + $this->allDeclarationBlocks($aResult); + return $aResult; + } - /** - * @deprecated use getAllDeclarationBlocks() - */ - public function getAllSelectors() { - return $this->getAllDeclarationBlocks(); - } + /** + * @deprecated use getAllDeclarationBlocks() + */ + public function getAllSelectors() + { + return $this->getAllDeclarationBlocks(); + } - /** - * Returns all RuleSet objects found recursively in the tree. - */ - public function getAllRuleSets() { - $aResult = array(); - $this->allRuleSets($aResult); - return $aResult; - } + /** + * Returns all RuleSet objects found recursively in the tree. + */ + public function getAllRuleSets() + { + $aResult = array(); + $this->allRuleSets($aResult); + return $aResult; + } - /** - * Returns all Value objects found recursively in the tree. - * @param (object|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 (@see{RuleSet->getRules()}). - * @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. - */ - public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { - $sSearchString = null; - if ($mElement === null) { - $mElement = $this; - } else if (is_string($mElement)) { - $sSearchString = $mElement; - $mElement = $this; - } - $aResult = array(); - $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); - return $aResult; - } + /** + * Returns all Value objects found recursively in the tree. + * @param (object|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 (@see{RuleSet->getRules()}). + * @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. + */ + public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) + { + $sSearchString = null; + if ($mElement === null) { + $mElement = $this; + } elseif (is_string($mElement)) { + $sSearchString = $mElement; + $mElement = $this; + } + $aResult = array(); + $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 $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "=="). - * @example getSelectorsBySpecificity('>= 100') - */ - public function getSelectorsBySpecificity($sSpecificitySearch = null) { - $aResult = array(); - $this->allSelectors($aResult, $sSpecificitySearch); - 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 $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "=="). + * @example getSelectorsBySpecificity('>= 100') + */ + public function getSelectorsBySpecificity($sSpecificitySearch = null) + { + $aResult = array(); + $this->allSelectors($aResult, $sSpecificitySearch); + return $aResult; + } - /** - * Expands all shorthand properties to their long value - */ - public function expandShorthands() { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandShorthands(); - } - } + /** + * Expands all shorthand properties to their long value + */ + public function expandShorthands() + { + foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandShorthands(); + } + } - /** - * Create shorthands properties whenever possible - */ - public function createShorthands() { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createShorthands(); - } - } + /** + * Create shorthands properties whenever possible + */ + public function createShorthands() + { + foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createShorthands(); + } + } - /** - * Override render() to make format argument optional - * - * @param \Sabberworm\CSS\OutputFormat|null $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) { - if($oOutputFormat === null) { - $oOutputFormat = new \Sabberworm\CSS\OutputFormat(); - } - return parent::render($oOutputFormat); - } + /** + * Override render() to make format argument optional + * + * @param \Sabberworm\CSS\OutputFormat|null $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) + { + if ($oOutputFormat === null) { + $oOutputFormat = new \Sabberworm\CSS\OutputFormat(); + } + return parent::render($oOutputFormat); + } - public function isRootList() { - return true; - } - -} \ No newline at end of file + public function isRootList() + { + return true; + } +} diff --git a/lib/Sabberworm/CSS/CSSList/KeyFrame.php b/lib/Sabberworm/CSS/CSSList/KeyFrame.php index d788be98..0d744933 100644 --- a/lib/Sabberworm/CSS/CSSList/KeyFrame.php +++ b/lib/Sabberworm/CSS/CSSList/KeyFrame.php @@ -4,70 +4,81 @@ use Sabberworm\CSS\Property\AtRule; -class KeyFrame extends CSSList implements AtRule { - /** - * @var string|null - */ - private $vendorKeyFrame; +class KeyFrame extends CSSList implements AtRule +{ + /** + * @var string|null + */ + private $vendorKeyFrame; - /** - * @var string|null - */ - private $animationName; + /** + * @var string|null + */ + private $animationName; - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - $this->vendorKeyFrame = null; - $this->animationName = null; - } + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + $this->vendorKeyFrame = null; + $this->animationName = null; + } - public function setVendorKeyFrame($vendorKeyFrame) { - $this->vendorKeyFrame = $vendorKeyFrame; - } + public function setVendorKeyFrame($vendorKeyFrame) + { + $this->vendorKeyFrame = $vendorKeyFrame; + } - public function getVendorKeyFrame() { - return $this->vendorKeyFrame; - } + public function getVendorKeyFrame() + { + return $this->vendorKeyFrame; + } - public function setAnimationName($animationName) { - $this->animationName = $animationName; - } + public function setAnimationName($animationName) + { + $this->animationName = $animationName; + } - public function getAnimationName() { - return $this->animationName; - } + public function getAnimationName() + { + return $this->animationName; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - return $sResult; - } + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } - public function isRootList() { - return false; - } + public function isRootList() + { + return false; + } - /** - * @return string|null - */ - public function atRuleName() { - return $this->vendorKeyFrame; - } + /** + * @return string|null + */ + public function atRuleName() + { + return $this->vendorKeyFrame; + } - /** - * @return string|null - */ - public function atRuleArgs() { - return $this->animationName; - } + /** + * @return string|null + */ + public function atRuleArgs() + { + return $this->animationName; + } } diff --git a/lib/Sabberworm/CSS/Comment/Comment.php b/lib/Sabberworm/CSS/Comment/Comment.php index 70521b16..973b3d31 100644 --- a/lib/Sabberworm/CSS/Comment/Comment.php +++ b/lib/Sabberworm/CSS/Comment/Comment.php @@ -4,48 +4,54 @@ use Sabberworm\CSS\Renderable; -class Comment implements Renderable { - protected $iLineNo; - protected $sComment; - - public function __construct($sComment = '', $iLineNo = 0) { - $this->sComment = $sComment; - $this->iLineNo = $iLineNo; - } - - /** - * @return string - */ - public function getComment() { - return $this->sComment; - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - /** - * @return string - */ - public function setComment($sComment) { - $this->sComment = $sComment; - } - - /** - * @return string - */ - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '/*' . $this->sComment . '*/'; - } - +class Comment implements Renderable +{ + protected $iLineNo; + protected $sComment; + + public function __construct($sComment = '', $iLineNo = 0) + { + $this->sComment = $sComment; + $this->iLineNo = $iLineNo; + } + + /** + * @return string + */ + public function getComment() + { + return $this->sComment; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * @return string + */ + public function setComment($sComment) + { + $this->sComment = $sComment; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + return '/*' . $this->sComment . '*/'; + } } diff --git a/lib/Sabberworm/CSS/Comment/Commentable.php b/lib/Sabberworm/CSS/Comment/Commentable.php index 3100f17a..8ecf8dcc 100644 --- a/lib/Sabberworm/CSS/Comment/Commentable.php +++ b/lib/Sabberworm/CSS/Comment/Commentable.php @@ -2,22 +2,21 @@ namespace Sabberworm\CSS\Comment; -interface Commentable { +interface Commentable +{ - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments); - - /** - * @return array - */ - public function getComments(); - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments); + /** + * @param array $aComments Array of comments. + */ + public function addComments(array $aComments); + /** + * @return array + */ + public function getComments(); + /** + * @param array $aComments Array containing Comment objects. + */ + public function setComments(array $aComments); } diff --git a/lib/Sabberworm/CSS/OutputFormat.php b/lib/Sabberworm/CSS/OutputFormat.php index f7ebb5a5..e643ddc2 100644 --- a/lib/Sabberworm/CSS/OutputFormat.php +++ b/lib/Sabberworm/CSS/OutputFormat.php @@ -9,314 +9,348 @@ * * @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule. */ -class OutputFormat { - /** - * Value format - */ - // " means double-quote, ' means single-quote - public $sStringQuotingType = '"'; - // Output RGB colors in hash notation if possible - public $bRGBHashNotation = true; - - /** - * Declaration format - */ - // Semicolon after the last rule of a declaration block can be omitted. To do that, set this false. - public $bSemicolonAfterLastRule = true; - - /** - * Spacing - * Note that these strings are not sanity-checked: the value should only consist of whitespace - * Any newline character will be indented according to the current level. - * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`) - */ - public $sSpaceAfterRuleName = ' '; - - public $sSpaceBeforeRules = ''; - public $sSpaceAfterRules = ''; - public $sSpaceBetweenRules = ''; - - public $sSpaceBeforeBlocks = ''; - public $sSpaceAfterBlocks = ''; - public $sSpaceBetweenBlocks = "\n"; - - // Content injected in and around @-rule blocks. - public $sBeforeAtRuleBlock = ''; - public $sAfterAtRuleBlock = ''; - - // This is what’s printed before and after the comma if a declaration block contains multiple selectors. - public $sSpaceBeforeSelectorSeparator = ''; - public $sSpaceAfterSelectorSeparator = ' '; - // This is what’s printed after the comma of value lists - public $sSpaceBeforeListArgumentSeparator = ''; - public $sSpaceAfterListArgumentSeparator = ''; - - public $sSpaceBeforeOpeningBrace = ' '; - - // Content injected in and around declaration blocks. - public $sBeforeDeclarationBlock = ''; - public $sAfterDeclarationBlockSelectors = ''; - public $sAfterDeclarationBlock = ''; - - /** - * Indentation - */ - // Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. - public $sIndentation = "\t"; - - /** - * Output exceptions. - */ - public $bIgnoreExceptions = false; - - - private $oFormatter = null; - private $oNextLevelFormat = null; - private $iIndentationLevel = 0; - - public function __construct() { - } - - public function get($sName) { - $aVarPrefixes = array('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; - } - - public function set($aNames, $mValue) { - $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); - if(is_string($aNames) && strpos($aNames, '*') !== false) { - $aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames)); - } else if(!is_array($aNames)) { - $aNames = array($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; - } - - public function __call($sMethodName, $aArguments) { - if(strpos($sMethodName, 'set') === 0) { - return $this->set(substr($sMethodName, 3), $aArguments[0]); - } else if(strpos($sMethodName, 'get') === 0) { - return $this->get(substr($sMethodName, 3)); - } else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) { - return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments); - } else { - throw new \Exception('Unknown OutputFormat method called: '.$sMethodName); - } - } - - public function indentWithTabs($iNumber = 1) { - return $this->setIndentation(str_repeat("\t", $iNumber)); - } - - public function indentWithSpaces($iNumber = 2) { - return $this->setIndentation(str_repeat(" ", $iNumber)); - } - - public function nextLevel() { - if($this->oNextLevelFormat === null) { - $this->oNextLevelFormat = clone $this; - $this->oNextLevelFormat->iIndentationLevel++; - $this->oNextLevelFormat->oFormatter = null; - } - return $this->oNextLevelFormat; - } - - public function beLenient() { - $this->bIgnoreExceptions = true; - } - - public function getFormatter() { - if($this->oFormatter === null) { - $this->oFormatter = new OutputFormatter($this); - } - return $this->oFormatter; - } - - public function level() { - return $this->iIndentationLevel; - } - - /** - * Create format. - * - * @return OutputFormat Format. - */ - public static function create() { - return new OutputFormat(); - } - - /** - * Create compact format. - * - * @return OutputFormat Format. - */ - public static function createCompact() { - $format = self::create(); - $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); - return $format; - } - - /** - * Create pretty format. - * - * @return OutputFormat Format. - */ - public static function createPretty() { - $format = self::create(); - $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' ')); - return $format; - } +class OutputFormat +{ + /** + * Value format + */ + // " means double-quote, ' means single-quote + public $sStringQuotingType = '"'; + // Output RGB colors in hash notation if possible + public $bRGBHashNotation = true; + + /** + * Declaration format + */ + // Semicolon after the last rule of a declaration block can be omitted. To do that, set this false. + public $bSemicolonAfterLastRule = true; + + /** + * Spacing + * Note that these strings are not sanity-checked: the value should only consist of whitespace + * Any newline character will be indented according to the current level. + * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`) + */ + public $sSpaceAfterRuleName = ' '; + + public $sSpaceBeforeRules = ''; + public $sSpaceAfterRules = ''; + public $sSpaceBetweenRules = ''; + + public $sSpaceBeforeBlocks = ''; + public $sSpaceAfterBlocks = ''; + public $sSpaceBetweenBlocks = "\n"; + + // Content injected in and around @-rule blocks. + public $sBeforeAtRuleBlock = ''; + public $sAfterAtRuleBlock = ''; + + // This is what’s printed before and after the comma if a declaration block contains multiple selectors. + public $sSpaceBeforeSelectorSeparator = ''; + public $sSpaceAfterSelectorSeparator = ' '; + // This is what’s printed after the comma of value lists + public $sSpaceBeforeListArgumentSeparator = ''; + public $sSpaceAfterListArgumentSeparator = ''; + + public $sSpaceBeforeOpeningBrace = ' '; + + // Content injected in and around declaration blocks. + public $sBeforeDeclarationBlock = ''; + public $sAfterDeclarationBlockSelectors = ''; + public $sAfterDeclarationBlock = ''; + + /** + * Indentation + */ + // Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. + public $sIndentation = "\t"; + + /** + * Output exceptions. + */ + public $bIgnoreExceptions = false; + + + private $oFormatter = null; + private $oNextLevelFormat = null; + private $iIndentationLevel = 0; + + public function __construct() + { + } + + public function get($sName) + { + $aVarPrefixes = array('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; + } + + public function set($aNames, $mValue) + { + $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); + if (is_string($aNames) && strpos($aNames, '*') !== false) { + $aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames)); + } elseif (!is_array($aNames)) { + $aNames = array($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; + } + + public function __call($sMethodName, $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('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) { + return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments); + } else { + throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName); + } + } + + public function indentWithTabs($iNumber = 1) + { + return $this->setIndentation(str_repeat("\t", $iNumber)); + } + + public function indentWithSpaces($iNumber = 2) + { + return $this->setIndentation(str_repeat(" ", $iNumber)); + } + + public function nextLevel() + { + if ($this->oNextLevelFormat === null) { + $this->oNextLevelFormat = clone $this; + $this->oNextLevelFormat->iIndentationLevel++; + $this->oNextLevelFormat->oFormatter = null; + } + return $this->oNextLevelFormat; + } + + public function beLenient() + { + $this->bIgnoreExceptions = true; + } + + public function getFormatter() + { + if ($this->oFormatter === null) { + $this->oFormatter = new OutputFormatter($this); + } + return $this->oFormatter; + } + + public function level() + { + return $this->iIndentationLevel; + } + + /** + * Create format. + * + * @return OutputFormat Format. + */ + public static function create() + { + return new OutputFormat(); + } + + /** + * Create compact format. + * + * @return OutputFormat Format. + */ + public static function createCompact() + { + $format = self::create(); + $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); + return $format; + } + + /** + * Create pretty format. + * + * @return OutputFormat Format. + */ + public static function createPretty() + { + $format = self::create(); + $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' ')); + return $format; + } } -class OutputFormatter { - private $oFormat; - - public function __construct(OutputFormat $oFormat) { - $this->oFormat = $oFormat; - } - - public function space($sName, $sType = null) { - $sSpaceString = $this->oFormat->get("Space$sName"); - // If $sSpaceString is an array, we have multple 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); - } - - public function spaceAfterRuleName() { - return $this->space('AfterRuleName'); - } - - public function spaceBeforeRules() { - return $this->space('BeforeRules'); - } - - public function spaceAfterRules() { - return $this->space('AfterRules'); - } - - public function spaceBetweenRules() { - return $this->space('BetweenRules'); - } - - public function spaceBeforeBlocks() { - return $this->space('BeforeBlocks'); - } - - public function spaceAfterBlocks() { - return $this->space('AfterBlocks'); - } - - public function spaceBetweenBlocks() { - return $this->space('BetweenBlocks'); - } - - public function spaceBeforeSelectorSeparator() { - return $this->space('BeforeSelectorSeparator'); - } - - public function spaceAfterSelectorSeparator() { - return $this->space('AfterSelectorSeparator'); - } - - public function spaceBeforeListArgumentSeparator($sSeparator) { - return $this->space('BeforeListArgumentSeparator', $sSeparator); - } - - public function spaceAfterListArgumentSeparator($sSeparator) { - return $this->space('AfterListArgumentSeparator', $sSeparator); - } - - public function spaceBeforeOpeningBrace() { - return $this->space('BeforeOpeningBrace'); - } - - /** - * Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting. - */ - 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() - */ - public function implode($sSeparator, $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 \Sabberworm\CSS\Renderable) { - $sResult .= $mValue->render($oFormat); - } else { - $sResult .= $mValue; - } - } - return $sResult; - } - - 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); - } - - private function prepareSpace($sSpaceString) { - return str_replace("\n", "\n".$this->indent(), $sSpaceString); - } - - private function indent() { - return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); - } +class OutputFormatter +{ + private $oFormat; + + public function __construct(OutputFormat $oFormat) + { + $this->oFormat = $oFormat; + } + + public function space($sName, $sType = null) + { + $sSpaceString = $this->oFormat->get("Space$sName"); + // If $sSpaceString is an array, we have multple 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); + } + + public function spaceAfterRuleName() + { + return $this->space('AfterRuleName'); + } + + public function spaceBeforeRules() + { + return $this->space('BeforeRules'); + } + + public function spaceAfterRules() + { + return $this->space('AfterRules'); + } + + public function spaceBetweenRules() + { + return $this->space('BetweenRules'); + } + + public function spaceBeforeBlocks() + { + return $this->space('BeforeBlocks'); + } + + public function spaceAfterBlocks() + { + return $this->space('AfterBlocks'); + } + + public function spaceBetweenBlocks() + { + return $this->space('BetweenBlocks'); + } + + public function spaceBeforeSelectorSeparator() + { + return $this->space('BeforeSelectorSeparator'); + } + + public function spaceAfterSelectorSeparator() + { + return $this->space('AfterSelectorSeparator'); + } + + public function spaceBeforeListArgumentSeparator($sSeparator) + { + return $this->space('BeforeListArgumentSeparator', $sSeparator); + } + + public function spaceAfterListArgumentSeparator($sSeparator) + { + return $this->space('AfterListArgumentSeparator', $sSeparator); + } + + public function spaceBeforeOpeningBrace() + { + return $this->space('BeforeOpeningBrace'); + } + + /** + * Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting. + */ + 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() + */ + public function implode($sSeparator, $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 \Sabberworm\CSS\Renderable) { + $sResult .= $mValue->render($oFormat); + } else { + $sResult .= $mValue; + } + } + return $sResult; + } + + 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); + } + + private function prepareSpace($sSpaceString) + { + return str_replace("\n", "\n" . $this->indent(), $sSpaceString); + } + + private function indent() + { + return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); + } } diff --git a/lib/Sabberworm/CSS/Parser.php b/lib/Sabberworm/CSS/Parser.php index 7714a31e..6f017c89 100644 --- a/lib/Sabberworm/CSS/Parser.php +++ b/lib/Sabberworm/CSS/Parser.php @@ -8,41 +8,46 @@ /** * Parser class parses CSS from text into a data structure. */ -class Parser { - /** - * @var ParserState - */ - private $oParserState; +class Parser +{ + /** + * @var ParserState + */ + private $oParserState; - /** - * Parser constructor. - * Note that that iLineNo starts from 1 and not 0 - * - * @param $sText - * @param Settings|null $oParserSettings - * @param int $iLineNo - */ - public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) { - if ($oParserSettings === null) { - $oParserSettings = Settings::create(); - } - $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); - } + /** + * Parser constructor. + * Note that that iLineNo starts from 1 and not 0 + * + * @param $sText + * @param Settings|null $oParserSettings + * @param int $iLineNo + */ + public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) + { + if ($oParserSettings === null) { + $oParserSettings = Settings::create(); + } + $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); + } - public function setCharset($sCharset) { - $this->oParserState->setCharset($sCharset); - } + public function setCharset($sCharset) + { + $this->oParserState->setCharset($sCharset); + } - public function getCharset() { - $this->oParserState->getCharset(); - } + public function getCharset() + { + $this->oParserState->getCharset(); + } - /** - * @return Document - * - * @throws Parsing\SourceException - */ - public function parse() { - return Document::parse($this->oParserState); - } + /** + * @return Document + * + * @throws Parsing\SourceException + */ + public function parse() + { + return Document::parse($this->oParserState); + } } diff --git a/lib/Sabberworm/CSS/Parsing/OutputException.php b/lib/Sabberworm/CSS/Parsing/OutputException.php index 1c811770..a8b906fb 100644 --- a/lib/Sabberworm/CSS/Parsing/OutputException.php +++ b/lib/Sabberworm/CSS/Parsing/OutputException.php @@ -5,8 +5,10 @@ /** * Thrown if the CSS parsers attempts to print something invalid */ -class OutputException extends SourceException { - public function __construct($sMessage, $iLineNo = 0) { - parent::__construct($sMessage, $iLineNo); - } -} \ No newline at end of file +class OutputException extends SourceException +{ + public function __construct($sMessage, $iLineNo = 0) + { + parent::__construct($sMessage, $iLineNo); + } +} diff --git a/lib/Sabberworm/CSS/Parsing/ParserState.php b/lib/Sabberworm/CSS/Parsing/ParserState.php index 73340700..d68ebabf 100644 --- a/lib/Sabberworm/CSS/Parsing/ParserState.php +++ b/lib/Sabberworm/CSS/Parsing/ParserState.php @@ -1,4 +1,5 @@ oParserSettings = $oParserSettings; - $this->sText = $sText; - $this->iCurrentPosition = 0; - $this->iLineNo = $iLineNo; - $this->setCharset($this->oParserSettings->sDefaultCharset); - } - - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - $this->aText = $this->strsplit($this->sText); - if( is_array($this->aText) ) { - $this->iLength = count($this->aText); - } - } - - public function getCharset() { - $this->oParserHelper->getCharset(); - return $this->sCharset; - } - - public function currentLine() { - return $this->iLineNo; - } - - public function currentColumn() { - return $this->iCurrentPosition; - } - - public function getSettings() { - return $this->oParserSettings; - } - - 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; - } - - 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; - } - - public function consumeWhiteSpace() { - $comments = array(); - 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; - } - - public function comes($sString, $bCaseInsensitive = false) { - $sPeek = $this->peek(strlen($sString)); - return ($sPeek == '') - ? false - : $this->streql($sPeek, $sString, $bCaseInsensitive); - } - - public function peek($iLength = 1, $iOffset = 0) { - $iOffset += $this->iCurrentPosition; - if ($iOffset >= $this->iLength) { - return ''; - } - return $this->substr($iOffset, $iLength); - } - - 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; - } - } - - 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 false|Comment - */ - 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; - } - - public function isEnd() { - return $this->iCurrentPosition >= $this->iLength; - } - - public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) { - $aEnd = is_array($aEnd) ? $aEnd : array($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); - } - - private function inputLeft() { - return $this->substr($this->iCurrentPosition, -1); - } - - public function streql($sString1, $sString2, $bCaseInsensitive = true) { - if($bCaseInsensitive) { - return $this->strtolower($sString1) === $this->strtolower($sString2); - } else { - return $sString1 === $sString2; - } - } - - public function backtrack($iAmount) { - $this->iCurrentPosition -= $iAmount; - } - - public function strlen($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strlen($sString, $this->sCharset); - } else { - return strlen($sString); - } - } - - 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; - } - - private function strtolower($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strtolower($sString, $this->sCharset); - } else { - return strtolower($sString); - } - } - - private function strsplit($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - if ($this->streql($this->sCharset, 'utf-8')) { - return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY); - } else { - $iLength = mb_strlen($sString, $this->sCharset); - $aResult = array(); - for ($i = 0; $i < $iLength; ++$i) { - $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); - } - return $aResult; - } - } else { - if($sString === '') { - return array(); - } else { - return str_split($sString); - } - } - } - - private function strpos($sString, $sNeedle, $iOffset) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); - } else { - return strpos($sString, $sNeedle, $iOffset); - } - } +class ParserState +{ + const EOF = null; + + private $oParserSettings; + + private $sText; + + private $aText; + private $iCurrentPosition; + private $sCharset; + private $iLength; + private $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); + } + + public function setCharset($sCharset) + { + $this->sCharset = $sCharset; + $this->aText = $this->strsplit($this->sText); + if (is_array($this->aText)) { + $this->iLength = count($this->aText); + } + } + + public function getCharset() + { + $this->oParserHelper->getCharset(); + return $this->sCharset; + } + + public function currentLine() + { + return $this->iLineNo; + } + + public function currentColumn() + { + return $this->iCurrentPosition; + } + + public function getSettings() + { + return $this->oParserSettings; + } + + 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; + } + + 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; + } + + public function consumeWhiteSpace() + { + $comments = array(); + 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; + } + + public function comes($sString, $bCaseInsensitive = false) + { + $sPeek = $this->peek(strlen($sString)); + return ($sPeek == '') + ? false + : $this->streql($sPeek, $sString, $bCaseInsensitive); + } + + public function peek($iLength = 1, $iOffset = 0) + { + $iOffset += $this->iCurrentPosition; + if ($iOffset >= $this->iLength) { + return ''; + } + return $this->substr($iOffset, $iLength); + } + + 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; + } + } + + 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 false|Comment + */ + 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; + } + + public function isEnd() + { + return $this->iCurrentPosition >= $this->iLength; + } + + public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) + { + $aEnd = is_array($aEnd) ? $aEnd : array($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); + } + + private function inputLeft() + { + return $this->substr($this->iCurrentPosition, -1); + } + + public function streql($sString1, $sString2, $bCaseInsensitive = true) + { + if ($bCaseInsensitive) { + return $this->strtolower($sString1) === $this->strtolower($sString2); + } else { + return $sString1 === $sString2; + } + } + + public function backtrack($iAmount) + { + $this->iCurrentPosition -= $iAmount; + } + + public function strlen($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strlen($sString, $this->sCharset); + } else { + return strlen($sString); + } + } + + 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; + } + + private function strtolower($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strtolower($sString, $this->sCharset); + } else { + return strtolower($sString); + } + } + + private function strsplit($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + if ($this->streql($this->sCharset, 'utf-8')) { + return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY); + } else { + $iLength = mb_strlen($sString, $this->sCharset); + $aResult = array(); + for ($i = 0; $i < $iLength; ++$i) { + $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); + } + return $aResult; + } + } else { + if ($sString === '') { + return array(); + } else { + return str_split($sString); + } + } + } + + private function strpos($sString, $sNeedle, $iOffset) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); + } else { + return strpos($sString, $sNeedle, $iOffset); + } + } } diff --git a/lib/Sabberworm/CSS/Parsing/SourceException.php b/lib/Sabberworm/CSS/Parsing/SourceException.php index 9bb99138..4021a602 100644 --- a/lib/Sabberworm/CSS/Parsing/SourceException.php +++ b/lib/Sabberworm/CSS/Parsing/SourceException.php @@ -2,17 +2,20 @@ namespace Sabberworm\CSS\Parsing; -class SourceException extends \Exception { - private $iLineNo; - public function __construct($sMessage, $iLineNo = 0) { - $this->iLineNo = $iLineNo; - if (!empty($iLineNo)) { - $sMessage .= " [line no: $iLineNo]"; - } - parent::__construct($sMessage); - } +class SourceException extends \Exception +{ + private $iLineNo; + public function __construct($sMessage, $iLineNo = 0) + { + $this->iLineNo = $iLineNo; + if (!empty($iLineNo)) { + $sMessage .= " [line no: $iLineNo]"; + } + parent::__construct($sMessage); + } - public function getLineNo() { - return $this->iLineNo; - } -} \ No newline at end of file + public function getLineNo() + { + return $this->iLineNo; + } +} diff --git a/lib/Sabberworm/CSS/Parsing/UnexpectedEOFException.php b/lib/Sabberworm/CSS/Parsing/UnexpectedEOFException.php index e3d89489..c08e90b5 100644 --- a/lib/Sabberworm/CSS/Parsing/UnexpectedEOFException.php +++ b/lib/Sabberworm/CSS/Parsing/UnexpectedEOFException.php @@ -6,4 +6,6 @@ * Thrown if the CSS parsers encounters end of file it did not expect * Extends UnexpectedTokenException in order to preserve backwards compatibility */ -class UnexpectedEOFException extends UnexpectedTokenException {} +class UnexpectedEOFException extends UnexpectedTokenException +{ +} diff --git a/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php b/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php index 0ef88184..9baa42ca 100644 --- a/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php +++ b/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php @@ -5,27 +5,29 @@ /** * Thrown if the CSS parsers encounters a token it did not expect */ -class UnexpectedTokenException extends SourceException { - private $sExpected; - private $sFound; - // Possible values: literal, identifier, count, expression, search - private $sMatchType; +class UnexpectedTokenException extends SourceException +{ + private $sExpected; + private $sFound; + // Possible values: literal, identifier, count, expression, search + private $sMatchType; - public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) { - $this->sExpected = $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}”."; - } else if($this->sMatchType === 'count') { - $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; - } else if($this->sMatchType === 'identifier') { - $sMessage = "Identifier expected. Got “{$sFound}”"; - } else if($this->sMatchType === 'custom') { - $sMessage = trim("$sExpected $sFound"); - } + public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) + { + $this->sExpected = $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); - } -} \ No newline at end of file + parent::__construct($sMessage, $iLineNo); + } +} diff --git a/lib/Sabberworm/CSS/Property/AtRule.php b/lib/Sabberworm/CSS/Property/AtRule.php index aaeadb33..291c388b 100644 --- a/lib/Sabberworm/CSS/Property/AtRule.php +++ b/lib/Sabberworm/CSS/Property/AtRule.php @@ -5,19 +5,20 @@ use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Comment\Commentable; -interface AtRule extends Renderable, Commentable { - // Since there are more set rules than block rules, we’re whitelisting the block rules and have anything else be treated as a set rule. - const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; - // …and more font-specific ones (to be used inside font-feature-values) - const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; +interface AtRule extends Renderable, Commentable +{ + // Since there are more set rules than block rules, we’re whitelisting the block rules and have anything else be treated as a set rule. + const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; + // …and more font-specific ones (to be used inside font-feature-values) + const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; - /** - * @return string|null - */ - public function atRuleName(); + /** + * @return string|null + */ + public function atRuleName(); - /** - * @return string|null - */ - public function atRuleArgs(); -} \ No newline at end of file + /** + * @return string|null + */ + public function atRuleArgs(); +} diff --git a/lib/Sabberworm/CSS/Property/CSSNamespace.php b/lib/Sabberworm/CSS/Property/CSSNamespace.php index cf75de2e..6d179d6a 100644 --- a/lib/Sabberworm/CSS/Property/CSSNamespace.php +++ b/lib/Sabberworm/CSS/Property/CSSNamespace.php @@ -5,82 +5,96 @@ /** * CSSNamespace represents an @namespace rule. */ -class CSSNamespace implements AtRule { - private $mUrl; - private $sPrefix; - private $iLineNo; - protected $aComments; +class CSSNamespace implements AtRule +{ + private $mUrl; + private $sPrefix; + private $iLineNo; + protected $aComments; - public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) { - $this->mUrl = $mUrl; - $this->sPrefix = $sPrefix; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } + public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) + { + $this->mUrl = $mUrl; + $this->sPrefix = $sPrefix; + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';'; - } + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ') . $this->mUrl->render($oOutputFormat) . ';'; + } - public function getUrl() { - return $this->mUrl; - } + public function getUrl() + { + return $this->mUrl; + } - public function getPrefix() { - return $this->sPrefix; - } + public function getPrefix() + { + return $this->sPrefix; + } - public function setUrl($mUrl) { - $this->mUrl = $mUrl; - } + public function setUrl($mUrl) + { + $this->mUrl = $mUrl; + } - public function setPrefix($sPrefix) { - $this->sPrefix = $sPrefix; - } + public function setPrefix($sPrefix) + { + $this->sPrefix = $sPrefix; + } - /** - * @return string - */ - public function atRuleName() { - return 'namespace'; - } + /** + * @return string + */ + public function atRuleName() + { + return 'namespace'; + } - /** - * @return array - */ - public function atRuleArgs() { - $aResult = array($this->mUrl); - if($this->sPrefix) { - array_unshift($aResult, $this->sPrefix); - } - return $aResult; - } + /** + * @return array + */ + public function atRuleArgs() + { + $aResult = array($this->mUrl); + if ($this->sPrefix) { + array_unshift($aResult, $this->sPrefix); + } + return $aResult; + } - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } - public function getComments() { - return $this->aComments; - } + public function getComments() + { + return $this->aComments; + } - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } +} diff --git a/lib/Sabberworm/CSS/Property/Charset.php b/lib/Sabberworm/CSS/Property/Charset.php index 65dabc8a..f90d8374 100644 --- a/lib/Sabberworm/CSS/Property/Charset.php +++ b/lib/Sabberworm/CSS/Property/Charset.php @@ -9,83 +9,95 @@ * • May only appear at the very top of a Document’s contents. * • Must not appear more than once. */ -class Charset implements AtRule { - /** - * @var string - */ - private $sCharset; +class Charset implements AtRule +{ + /** + * @var string + */ + private $sCharset; - /** - * @var int - */ - protected $iLineNo; + /** + * @var int + */ + protected $iLineNo; - /** - * @var array - */ - protected $aComment; + /** + * @var array + */ + protected $aComment; - /** - * @param string $sCharset - * @param int $iLineNo - */ - public function __construct($sCharset, $iLineNo = 0) { - $this->sCharset = $sCharset; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } + /** + * @param string $sCharset + * @param int $iLineNo + */ + public function __construct($sCharset, $iLineNo = 0) + { + $this->sCharset = $sCharset; + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - } + public function setCharset($sCharset) + { + $this->sCharset = $sCharset; + } - public function getCharset() { - return $this->sCharset; - } + public function getCharset() + { + return $this->sCharset; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "@charset {$this->sCharset->render($oOutputFormat)};"; - } + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + return "@charset {$this->sCharset->render($oOutputFormat)};"; + } - /** - * @return string - */ - public function atRuleName() { - return 'charset'; - } + /** + * @return string + */ + public function atRuleName() + { + return 'charset'; + } - /** - * @return string - */ - public function atRuleArgs() { - return $this->sCharset; - } + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sCharset; + } - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } - public function getComments() { - return $this->aComments; - } + public function getComments() + { + return $this->aComments; + } - public function setComments(array $aComments) { - $this->aComments = $aComments; - } + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } } diff --git a/lib/Sabberworm/CSS/Property/Import.php b/lib/Sabberworm/CSS/Property/Import.php index 162ba206..ff168040 100644 --- a/lib/Sabberworm/CSS/Property/Import.php +++ b/lib/Sabberworm/CSS/Property/Import.php @@ -7,94 +7,106 @@ /** * Class representing an @import rule. */ -class Import implements AtRule { - /** - * @var URL - */ - private $oLocation; - - /** - * @var string - */ - private $sMediaQuery; - - /** - * @var int - */ - protected $iLineNo; - - /** - * @var array - */ - 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 = array(); - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - public function setLocation($oLocation) { - $this->oLocation = $oLocation; - } - - public function getLocation() { - return $this->oLocation; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\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 = array($this->oLocation); - if($this->sMediaQuery) { - array_push($aResult, $this->sMediaQuery); - } - return $aResult; - } - - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - public function getComments() { - return $this->aComments; - } - - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file +class Import implements AtRule +{ + /** + * @var URL + */ + private $oLocation; + + /** + * @var string + */ + private $sMediaQuery; + + /** + * @var int + */ + protected $iLineNo; + + /** + * @var array + */ + 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 = array(); + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + public function setLocation($oLocation) + { + $this->oLocation = $oLocation; + } + + public function getLocation() + { + return $this->oLocation; + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\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 = array($this->oLocation); + if ($this->sMediaQuery) { + array_push($aResult, $this->sMediaQuery); + } + return $aResult; + } + + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + + public function getComments() + { + return $this->aComments; + } + + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } +} diff --git a/lib/Sabberworm/CSS/Property/KeyframeSelector.php b/lib/Sabberworm/CSS/Property/KeyframeSelector.php index 1b4be469..e8f4ee85 100644 --- a/lib/Sabberworm/CSS/Property/KeyframeSelector.php +++ b/lib/Sabberworm/CSS/Property/KeyframeSelector.php @@ -4,11 +4,12 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class KeyframeSelector extends Selector { +class KeyframeSelector extends Selector +{ - //Regexes for specificity calculations + //Regexes for specificity calculations - const SELECTOR_VALIDATION_RX = '/ + const SELECTOR_VALIDATION_RX = '/ ^( (?: [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters @@ -19,5 +20,4 @@ class KeyframeSelector extends Selector { (\d+%) # keyframe animation progress percentage (e.g. 50%) $ /ux'; - } diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index bd04b889..7a17477f 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -7,10 +7,11 @@ /** * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this class. */ -class Selector { +class Selector +{ - //Regexes for specificity calculations - const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ + //Regexes for specificity calculations + const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ (\.[\w]+) # classes | \[(\w+) # attributes @@ -29,7 +30,7 @@ class Selector { )) /ix'; - const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ + const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ ((^|[\s\+\>\~]+)[\w]+ # elements | \:{1,2}( # pseudo-elements @@ -37,7 +38,7 @@ class Selector { )) /ix'; - const SELECTOR_VALIDATION_RX = '/ + const SELECTOR_VALIDATION_RX = '/ ^( (?: [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters @@ -47,44 +48,49 @@ class Selector { )$ /ux'; - private $sSelector; - private $iSpecificity; + private $sSelector; + private $iSpecificity; - public static function isValid($sSelector) { - return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector); - } + public static function isValid($sSelector) + { + return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector); + } - public function __construct($sSelector, $bCalculateSpecificity = false) { - $this->setSelector($sSelector); - if ($bCalculateSpecificity) { - $this->getSpecificity(); - } - } + public function __construct($sSelector, $bCalculateSpecificity = false) + { + $this->setSelector($sSelector); + if ($bCalculateSpecificity) { + $this->getSpecificity(); + } + } - public function getSelector() { - return $this->sSelector; - } + public function getSelector() + { + return $this->sSelector; + } - public function setSelector($sSelector) { - $this->sSelector = trim($sSelector); - $this->iSpecificity = null; - } + public function setSelector($sSelector) + { + $this->sSelector = trim($sSelector); + $this->iSpecificity = null; + } - public function __toString() { - return $this->getSelector(); - } - - 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; - } + public function __toString() + { + return $this->getSelector(); + } + 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; + } } diff --git a/lib/Sabberworm/CSS/Renderable.php b/lib/Sabberworm/CSS/Renderable.php index 862580c5..b2cc3467 100644 --- a/lib/Sabberworm/CSS/Renderable.php +++ b/lib/Sabberworm/CSS/Renderable.php @@ -2,15 +2,16 @@ namespace Sabberworm\CSS; -interface Renderable { - public function __toString(); +interface Renderable +{ + public function __toString(); - /** - * @param OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); + /** + * @param OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); - public function getLineNo(); -} \ No newline at end of file + public function getLineNo(); +} diff --git a/lib/Sabberworm/CSS/Rule/Rule.php b/lib/Sabberworm/CSS/Rule/Rule.php index 55f89c11..130b5d71 100644 --- a/lib/Sabberworm/CSS/Rule/Rule.php +++ b/lib/Sabberworm/CSS/Rule/Rule.php @@ -12,244 +12,267 @@ * RuleSets contains Rule objects which always have a key and a value. * In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];” */ -class Rule implements Renderable, Commentable { - - private $sRule; - private $mValue; - private $bIsImportant; - private $aIeHack; - protected $iLineNo; - protected $iColNo; - protected $aComments; - - public function __construct($sRule, $iLineNo = 0, $iColNo = 0) { - $this->sRule = $sRule; - $this->mValue = null; - $this->bIsImportant = false; - $this->aIeHack = array(); - $this->iLineNo = $iLineNo; - $this->iColNo = $iColNo; - $this->aComments = array(); - } - - 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; - } - - private static function listDelimiterForRule($sRule) { - if (preg_match('/^font($|-)/', $sRule)) { - return array(',', '/', ' '); - } - return array(',', ' ', '/'); - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - /** - * @return int - */ - public function getColNo() { - return $this->iColNo; - } - - public function setPosition($iLine, $iColumn) { - $this->iColNo = $iColumn; - $this->iLineNo = $iLine; - } - - public function setRule($sRule) { - $this->sRule = $sRule; - } - - public function getRule() { - return $this->sRule; - } - - public function getValue() { - return $this->mValue; - } - - public function setValue($mValue) { - $this->mValue = $mValue; - } - - /** - * @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary. - */ - public function setValues($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; - } - - /** - * @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s). - */ - public function getValues() { - if (!$this->mValue instanceof RuleValueList) { - return array(array($this->mValue)); - } - if ($this->mValue->getListSeparator() === ',') { - return array($this->mValue->getListComponents()); - } - $aResult = array(); - foreach ($this->mValue->getListComponents() as $mValue) { - if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { - $aResult[] = array($mValue); - continue; - } - if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { - $aResult[] = array(); - } - 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. - */ - public function addValue($mValue, $sType = ' ') { - if (!is_array($mValue)) { - $mValue = array($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); - } - } - - public function addIeHack($iModifier) { - $this->aIeHack[] = $iModifier; - } - - public function setIeHack(array $aModifiers) { - $this->aIeHack = $aModifiers; - } - - public function getIeHack() { - return $this->aIeHack; - } - - public function setIsImportant($bIsImportant) { - $this->bIsImportant = $bIsImportant; - } - - public function getIsImportant() { - return $this->bIsImportant; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\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 Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } +class Rule implements Renderable, Commentable +{ + private $sRule; + private $mValue; + private $bIsImportant; + private $aIeHack; + protected $iLineNo; + protected $iColNo; + protected $aComments; + + public function __construct($sRule, $iLineNo = 0, $iColNo = 0) + { + $this->sRule = $sRule; + $this->mValue = null; + $this->bIsImportant = false; + $this->aIeHack = array(); + $this->iLineNo = $iLineNo; + $this->iColNo = $iColNo; + $this->aComments = array(); + } + + 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; + } + + private static function listDelimiterForRule($sRule) + { + if (preg_match('/^font($|-)/', $sRule)) { + return array(',', '/', ' '); + } + return array(',', ' ', '/'); + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * @return int + */ + public function getColNo() + { + return $this->iColNo; + } + + public function setPosition($iLine, $iColumn) + { + $this->iColNo = $iColumn; + $this->iLineNo = $iLine; + } + + public function setRule($sRule) + { + $this->sRule = $sRule; + } + + public function getRule() + { + return $this->sRule; + } + + public function getValue() + { + return $this->mValue; + } + + public function setValue($mValue) + { + $this->mValue = $mValue; + } + + /** + * @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary. + */ + public function setValues($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; + } + + /** + * @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s). + */ + public function getValues() + { + if (!$this->mValue instanceof RuleValueList) { + return array(array($this->mValue)); + } + if ($this->mValue->getListSeparator() === ',') { + return array($this->mValue->getListComponents()); + } + $aResult = array(); + foreach ($this->mValue->getListComponents() as $mValue) { + if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { + $aResult[] = array($mValue); + continue; + } + if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { + $aResult[] = array(); + } + 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. + */ + public function addValue($mValue, $sType = ' ') + { + if (!is_array($mValue)) { + $mValue = array($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); + } + } + + public function addIeHack($iModifier) + { + $this->aIeHack[] = $iModifier; + } + + public function setIeHack(array $aModifiers) + { + $this->aIeHack = $aModifiers; + } + + public function getIeHack() + { + return $this->aIeHack; + } + + public function setIsImportant($bIsImportant) + { + $this->bIsImportant = $bIsImportant; + } + + public function getIsImportant() + { + return $this->bIsImportant; + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\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 Array of comments. + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments Array containing Comment objects. + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } } diff --git a/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php b/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php index 9b892ae3..20c906b5 100644 --- a/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php +++ b/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php @@ -7,60 +7,65 @@ /** * A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRuleSet objects. */ -class AtRuleSet extends RuleSet implements AtRule { - /** - * @var string - */ - private $sType; +class AtRuleSet extends RuleSet implements AtRule +{ + /** + * @var string + */ + private $sType; - /** - * @var string - */ - private $sArgs; + /** + * @var string + */ + private $sArgs; - /** - * @param string $sType - * @param string $sArgs - * @param int $iLineNo - */ - public function __construct($sType, $sArgs = '', $iLineNo = 0) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; - } + /** + * @param string $sType + * @param string $sArgs + * @param int $iLineNo + */ + public function __construct($sType, $sArgs = '', $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->sType = $sType; + $this->sArgs = $sArgs; + } - /** - * @return string - */ - public function atRuleName() { - return $this->sType; - } + /** + * @return string + */ + public function atRuleName() + { + return $this->sType; + } - /** - * @return string - */ - public function atRuleArgs() { - return $this->sArgs; - } + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sArgs; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sArgs = $this->sArgs; - if($sArgs) { - $sArgs = ' ' . $sArgs; - } - $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - return $sResult; - } - -} \ No newline at end of file + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + $sArgs = $this->sArgs; + if ($sArgs) { + $sArgs = ' ' . $sArgs; + } + $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } +} diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php index c3bcdd07..8b82dc43 100644 --- a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php +++ b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -19,660 +19,689 @@ * Declaration blocks are the parts of a css file which denote the rules belonging to a selector. * Declaration blocks usually appear directly inside a Document or another CSSList (mostly a MediaQuery). */ -class DeclarationBlock extends RuleSet { - /** - * @var array - */ - private $aSelectors; +class DeclarationBlock extends RuleSet +{ + /** + * @var array + */ + private $aSelectors; - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - $this->aSelectors = array(); - } + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + $this->aSelectors = array(); + } - public static function parse(ParserState $oParserState, $oList = NULL) { - $aComments = array(); - $oResult = new DeclarationBlock($oParserState->currentLine()); - try { - $aSelectorParts = array(); - $sStringWrapperChar = false; - do { - $aSelectorParts[] = $oParserState->consume(1) . $oParserState->consumeUntil(array('{', '}', '\'', '"'), false, false, $aComments); - if ( in_array($oParserState->peek(), array('\'', '"')) && substr(end($aSelectorParts), -1) != "\\" ) { - if ( $sStringWrapperChar === false ) { - $sStringWrapperChar = $oParserState->peek(); - } else if ($sStringWrapperChar == $oParserState->peek()) { - $sStringWrapperChar = false; - } - } - } while (!in_array($oParserState->peek(), array('{', '}')) || $sStringWrapperChar !== false); - $oResult->setSelector(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; - } + public static function parse(ParserState $oParserState, $oList = null) + { + $aComments = array(); + $oResult = new DeclarationBlock($oParserState->currentLine()); + try { + $aSelectorParts = array(); + $sStringWrapperChar = false; + do { + $aSelectorParts[] = $oParserState->consume(1) . $oParserState->consumeUntil(array('{', '}', '\'', '"'), false, false, $aComments); + if (in_array($oParserState->peek(), array('\'', '"')) && substr(end($aSelectorParts), -1) != "\\") { + if ($sStringWrapperChar === false) { + $sStringWrapperChar = $oParserState->peek(); + } elseif ($sStringWrapperChar == $oParserState->peek()) { + $sStringWrapperChar = false; + } + } + } while (!in_array($oParserState->peek(), array('{', '}')) || $sStringWrapperChar !== false); + $oResult->setSelector(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; + } - 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); - } - } - } - } + 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 selector of the block - 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; - } + // remove one of the selector of the block + 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; + } - /** - * @deprecated use getSelectors() - */ - public function getSelector() { - return $this->getSelectors(); - } + /** + * @deprecated use getSelectors() + */ + public function getSelector() + { + return $this->getSelectors(); + } - /** - * @deprecated use setSelectors() - */ - public function setSelector($mSelector, $oList = NULL) { - $this->setSelectors($mSelector, $oList); - } + /** + * @deprecated use setSelectors() + */ + public function setSelector($mSelector, $oList = null) + { + $this->setSelectors($mSelector, $oList); + } - /** - * Get selectors. - * - * @return array Selectors. - */ - public function getSelectors() { - return $this->aSelectors; - } + /** + * Get selectors. + * + * @return array Selectors. + */ + public function getSelectors() + { + return $this->aSelectors; + } - /** - * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts. - * */ - public function expandShorthands() { - // border must be expanded before dimensions - $this->expandBorderShorthand(); - $this->expandDimensionsShorthand(); - $this->expandFontShorthand(); - $this->expandBackgroundShorthand(); - $this->expandListStyleShorthand(); - } + /** + * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts. + * */ + public function expandShorthands() + { + // border must be expanded before dimensions + $this->expandBorderShorthand(); + $this->expandDimensionsShorthand(); + $this->expandFontShorthand(); + $this->expandBackgroundShorthand(); + $this->expandListStyleShorthand(); + } - /** - * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible. - * */ - public function createShorthands() { - $this->createBackgroundShorthand(); - $this->createDimensionsShorthand(); - // border must be shortened after dimensions - $this->createBorderShorthand(); - $this->createFontShorthand(); - $this->createListStyleShorthand(); - } + /** + * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible. + * */ + public function createShorthands() + { + $this->createBackgroundShorthand(); + $this->createDimensionsShorthand(); + // border must be shortened after dimensions + $this->createBorderShorthand(); + $this->createFontShorthand(); + $this->createListStyleShorthand(); + } - /** - * Split shorthand border declarations (e.g. border: 1px red;) - * Additional splitting happens in expandDimensionsShorthand - * Multiple borders are not yet supported as of 3 - * */ - public function expandBorderShorthand() { - $aBorderRules = array( - 'border', 'border-left', 'border-right', 'border-top', 'border-bottom' - ); - $aBorderSizes = array( - 'thin', 'medium', 'thick' - ); - $aRules = $this->getRulesAssoc(); - foreach ($aBorderRules as $sBorderRule) { - if (!isset($aRules[$sBorderRule])) - continue; - $oRule = $aRules[$sBorderRule]; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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"; - } else if ($mValue instanceof Color) { - $sNewRuleName = $sBorderRule . "-color"; - } else { - if (in_array($mValue, $aBorderSizes)) { - $sNewRuleName = $sBorderRule . "-width"; - } else/* if(in_array($mValue, $aBorderStyles)) */ { - $sNewRuleName = $sBorderRule . "-style"; - } - } - $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue(array($mNewValue)); - $this->addRule($oNewRule); - } - $this->removeRule($sBorderRule); - } - } + /** + * Split shorthand border declarations (e.g. border: 1px red;) + * Additional splitting happens in expandDimensionsShorthand + * Multiple borders are not yet supported as of 3 + * */ + public function expandBorderShorthand() + { + $aBorderRules = array( + 'border', 'border-left', 'border-right', 'border-top', 'border-bottom' + ); + $aBorderSizes = array( + 'thin', 'medium', 'thick' + ); + $aRules = $this->getRulesAssoc(); + foreach ($aBorderRules as $sBorderRule) { + if (!isset($aRules[$sBorderRule])) { + continue; + } + $oRule = $aRules[$sBorderRule]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + 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 /* if(in_array($mValue, $aBorderStyles)) */ { + $sNewRuleName = $sBorderRule . "-style"; + } + } + $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue(array($mNewValue)); + $this->addRule($oNewRule); + } + $this->removeRule($sBorderRule); + } + } - /** - * Split shorthand dimensional declarations (e.g. margin: 0px auto;) - * into their constituent parts. - * Handles margin, padding, border-color, border-style and border-width. - * */ - public function expandDimensionsShorthand() { - $aExpansions = array( - '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 = array(); - 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 (array('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); - } - } + /** + * Split shorthand dimensional declarations (e.g. margin: 0px auto;) + * into their constituent parts. + * Handles margin, padding, border-color, border-style and border-width. + * */ + public function expandDimensionsShorthand() + { + $aExpansions = array( + '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 = array(); + 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 (array('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); + } + } - /** - * Convert shorthand font declarations - * (e.g. font: 300 italic 11px/14px verdana, helvetica, sans-serif;) - * into their constituent parts. - * */ - 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 = array( - 'font-style' => 'normal', - 'font-variant' => 'normal', - 'font-weight' => 'normal', - 'font-size' => 'normal', - 'line-height' => 'normal' - ); - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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, array('normal', 'inherit'))) { - foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) { - if (!isset($aFontProperties[$sProperty])) { - $aFontProperties[$sProperty] = $mValue; - } - } - } else if (in_array($mValue, array('italic', 'oblique'))) { - $aFontProperties['font-style'] = $mValue; - } else if ($mValue == 'small-caps') { - $aFontProperties['font-variant'] = $mValue; - } else if ( - in_array($mValue, array('bold', 'bolder', 'lighter')) - || ($mValue instanceof Size - && in_array($mValue->getSize(), range(100, 900, 100))) - ) { - $aFontProperties['font-weight'] = $mValue; - } else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { - list($oSize, $oHeight) = $mValue->getListComponents(); - $aFontProperties['font-size'] = $oSize; - $aFontProperties['line-height'] = $oHeight; - } else if ($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'); - } + /** + * Convert shorthand font declarations + * (e.g. font: 300 italic 11px/14px verdana, helvetica, sans-serif;) + * into their constituent parts. + * */ + 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 = array( + 'font-style' => 'normal', + 'font-variant' => 'normal', + 'font-weight' => 'normal', + 'font-size' => 'normal', + 'line-height' => 'normal' + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + 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, array('normal', 'inherit'))) { + foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) { + if (!isset($aFontProperties[$sProperty])) { + $aFontProperties[$sProperty] = $mValue; + } + } + } elseif (in_array($mValue, array('italic', 'oblique'))) { + $aFontProperties['font-style'] = $mValue; + } elseif ($mValue == 'small-caps') { + $aFontProperties['font-variant'] = $mValue; + } elseif ( + in_array($mValue, array('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'); + } - /* - * Convert 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 - * */ + /* + * Convert 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 + * */ - public function expandBackgroundShorthand() { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['background'])) - return; - $oRule = $aRules['background']; - $aBgProperties = array( - 'background-color' => array('transparent'), 'background-image' => array('none'), - 'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'), - 'background-position' => array(new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo)) - ); - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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; - } else if ($mValue instanceof Color) { - $aBgProperties['background-color'] = $mValue; - } else if (in_array($mValue, array('scroll', 'fixed'))) { - $aBgProperties['background-attachment'] = $mValue; - } else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) { - $aBgProperties['background-repeat'] = $mValue; - } else if (in_array($mValue, array('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'); - } + public function expandBackgroundShorthand() + { + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['background'])) { + return; + } + $oRule = $aRules['background']; + $aBgProperties = array( + 'background-color' => array('transparent'), 'background-image' => array('none'), + 'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'), + 'background-position' => array(new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo)) + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + 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, array('scroll', 'fixed'))) { + $aBgProperties['background-attachment'] = $mValue; + } elseif (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) { + $aBgProperties['background-repeat'] = $mValue; + } elseif ( + in_array($mValue, array('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'); + } - public function expandListStyleShorthand() { - $aListProperties = array( - 'list-style-type' => 'disc', - 'list-style-position' => 'outside', - 'list-style-image' => 'none' - ); - $aListStyleTypes = array( - '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 = array( - 'inside', 'outside' - ); - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['list-style'])) - return; - $oRule = $aRules['list-style']; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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; - } else if (in_array($mValue, $aListStyleTypes)) { - $aListProperties['list-style-types'] = $mValue; - } else if (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'); - } + public function expandListStyleShorthand() + { + $aListProperties = array( + 'list-style-type' => 'disc', + 'list-style-position' => 'outside', + 'list-style-image' => 'none' + ); + $aListStyleTypes = array( + '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 = array( + 'inside', 'outside' + ); + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['list-style'])) { + return; + } + $oRule = $aRules['list-style']; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + 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'); + } - public function createShorthandProperties(array $aProperties, $sShorthand) { - $aRules = $this->getRulesAssoc(); - $aNewValues = array(); - foreach ($aProperties as $sProperty) { - if (!isset($aRules[$sProperty])) - continue; - $oRule = $aRules[$sProperty]; - if (!$oRule->getIsImportant()) { - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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); - } - } + public function createShorthandProperties(array $aProperties, $sShorthand) + { + $aRules = $this->getRulesAssoc(); + $aNewValues = array(); + foreach ($aProperties as $sProperty) { + if (!isset($aRules[$sProperty])) { + continue; + } + $oRule = $aRules[$sProperty]; + if (!$oRule->getIsImportant()) { + $mRuleValue = $oRule->getValue(); + $aValues = array(); + 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); + } + } - public function createBackgroundShorthand() { - $aProperties = array( - 'background-color', 'background-image', 'background-repeat', - 'background-position', 'background-attachment' - ); - $this->createShorthandProperties($aProperties, 'background'); - } + public function createBackgroundShorthand() + { + $aProperties = array( + 'background-color', 'background-image', 'background-repeat', + 'background-position', 'background-attachment' + ); + $this->createShorthandProperties($aProperties, 'background'); + } - public function createListStyleShorthand() { - $aProperties = array( - 'list-style-type', 'list-style-position', 'list-style-image' - ); - $this->createShorthandProperties($aProperties, 'list-style'); - } + public function createListStyleShorthand() + { + $aProperties = array( + 'list-style-type', 'list-style-position', 'list-style-image' + ); + $this->createShorthandProperties($aProperties, 'list-style'); + } - /** - * Combine border-color, border-style and border-width into border - * Should be run after create_dimensions_shorthand! - * */ - public function createBorderShorthand() { - $aProperties = array( - 'border-width', 'border-style', 'border-color' - ); - $this->createShorthandProperties($aProperties, 'border'); - } + /** + * Combine border-color, border-style and border-width into border + * Should be run after create_dimensions_shorthand! + * */ + public function createBorderShorthand() + { + $aProperties = array( + '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. - * */ + /* + * Looks for long format CSS dimensional properties + * (margin, padding, border-color, border-style and border-width) + * and converts them into shorthand CSS properties. + * */ - public function createDimensionsShorthand() { - $aPositions = array('top', 'right', 'bottom', 'left'); - $aExpansions = array( - '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 = array(); - 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 = array(); - foreach ($aPositions as $sPosition) { - $oRule = $aRules[sprintf($sExpanded, $sPosition)]; - $mRuleValue = $oRule->getValue(); - $aRuleValues = array(); - 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)); - } - } - } - } + public function createDimensionsShorthand() + { + $aPositions = array('top', 'right', 'bottom', 'left'); + $aExpansions = array( + '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 = array(); + 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 = array(); + foreach ($aPositions as $sPosition) { + $oRule = $aRules[sprintf($sExpanded, $sPosition)]; + $mRuleValue = $oRule->getValue(); + $aRuleValues = array(); + 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. - * */ - public function createFontShorthand() { - $aFontProperties = array( - '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 (array('font-style', 'font-variant', 'font-weight') as $sProperty) { - if (isset($aRules[$sProperty])) { - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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 = array(); - 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 = array(); - 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 = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aFFValues[] = $mRuleValue; - } else { - $aFFValues = $mRuleValue->getListComponents(); - } - $oFFValue = new RuleValueList(',', $this->iLineNo); - $oFFValue->setListComponents($aFFValues); - $oNewRule->addValue($oFFValue); + /** + * 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. + * */ + public function createFontShorthand() + { + $aFontProperties = array( + '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 (array('font-style', 'font-variant', 'font-weight') as $sProperty) { + if (isset($aRules[$sProperty])) { + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + 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 = array(); + 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 = array(); + 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 = array(); + 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); - } - } + $this->addRule($oNewRule); + foreach ($aFontProperties as $sProperty) { + $this->removeRule($sProperty); + } + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - * - * @throws OutputException - */ - public function render(\Sabberworm\CSS\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; - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + * + * @throws OutputException + */ + public function render(\Sabberworm\CSS\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; + } } diff --git a/lib/Sabberworm/CSS/RuleSet/RuleSet.php b/lib/Sabberworm/CSS/RuleSet/RuleSet.php index 79bfcd04..a0f413da 100644 --- a/lib/Sabberworm/CSS/RuleSet/RuleSet.php +++ b/lib/Sabberworm/CSS/RuleSet/RuleSet.php @@ -12,225 +12,238 @@ * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block. * However, unknown At-Rules (like @font-face) are also rule sets. */ -abstract class RuleSet implements Renderable, Commentable { - - private $aRules; - protected $iLineNo; - protected $aComments; - - public function __construct($iLineNo = 0) { - $this->aRules = array(); - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } - - 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(array("\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; - } - - public function addRule(Rule $oRule, Rule $oSibling = null) { - $sRule = $oRule->getRule(); - if(!isset($this->aRules[$sRule])) { - $this->aRules[$sRule] = array(); - } - - $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, array($oRule)); - } - - /** - * Returns all rules matching the given rule name - * - * @param null|string|Rule $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()). - * - * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font. - * @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array(). - * - * @return array Rules. - */ - public function getRules($mRule = null) { - if ($mRule instanceof Rule) { - $mRule = $mRule->getRule(); - } - $aResult = array(); - 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; - } - - /** - * Override all the rules of this set. - * @param Rule[] $aRules The rules to override with. - */ - public function setRules(array $aRules) { - $this->aRules = array(); - 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. - * @param (string) $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()). - * 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 @link{getRules()} would yield an indexed array containing both. - * @return Rule[] Rules. - */ - public function getRulesAssoc($mRule = null) { - $aResult = array(); - foreach($this->getRules($mRule) as $oRule) { - $aResult[$oRule->getRule()] = $oRule; - } - return $aResult; - } - - /** - * Remove a rule from this RuleSet. This accepts all the possible values that @link{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 behvaiour, use removeRule($oRule->getRule()). - * @param (null|string|Rule) $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. - */ - 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]); - } - } - } - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\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 Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } - +abstract class RuleSet implements Renderable, Commentable +{ + + private $aRules; + protected $iLineNo; + protected $aComments; + + public function __construct($iLineNo = 0) + { + $this->aRules = array(); + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } + + 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(array("\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; + } + + public function addRule(Rule $oRule, Rule $oSibling = null) + { + $sRule = $oRule->getRule(); + if (!isset($this->aRules[$sRule])) { + $this->aRules[$sRule] = array(); + } + + $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, array($oRule)); + } + + /** + * Returns all rules matching the given rule name + * + * @param null|string|Rule $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()). + * + * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font. + * @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array(). + * + * @return array Rules. + */ + public function getRules($mRule = null) + { + if ($mRule instanceof Rule) { + $mRule = $mRule->getRule(); + } + $aResult = array(); + 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; + } + + /** + * Override all the rules of this set. + * @param Rule[] $aRules The rules to override with. + */ + public function setRules(array $aRules) + { + $this->aRules = array(); + 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. + * @param (string) $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()). + * 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 @link{getRules()} would yield an indexed array containing both. + * @return Rule[] Rules. + */ + public function getRulesAssoc($mRule = null) + { + $aResult = array(); + foreach ($this->getRules($mRule) as $oRule) { + $aResult[$oRule->getRule()] = $oRule; + } + return $aResult; + } + + /** + * Remove a rule from this RuleSet. This accepts all the possible values that @link{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 behvaiour, use removeRule($oRule->getRule()). + * @param (null|string|Rule) $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. + */ + 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]); + } + } + } + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\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 Array of comments. + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments Array containing Comment objects. + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } } diff --git a/lib/Sabberworm/CSS/Settings.php b/lib/Sabberworm/CSS/Settings.php index cb89a863..ad89d4eb 100644 --- a/lib/Sabberworm/CSS/Settings.php +++ b/lib/Sabberworm/CSS/Settings.php @@ -9,46 +9,53 @@ * * Configure parser behaviour here. */ -class Settings { - /** - * Multi-byte string support. If true (mbstring extension must be enabled), will use (slower) mb_strlen, mb_convert_case, mb_substr and mb_strpos functions. Otherwise, the normal (ASCII-Only) functions will be used. - */ - public $bMultibyteSupport; - - /** - * The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8. - */ - public $sDefaultCharset = 'utf-8'; - - /** - * Lenient parsing. When used (which is true by default), the parser will not choke on unexpected tokens but simply ignore them. - */ - public $bLenientParsing = true; - - private function __construct() { - $this->bMultibyteSupport = extension_loaded('mbstring'); - } - - public static function create() { - return new Settings(); - } - - public function withMultibyteSupport($bMultibyteSupport = true) { - $this->bMultibyteSupport = $bMultibyteSupport; - return $this; - } - - public function withDefaultCharset($sDefaultCharset) { - $this->sDefaultCharset = $sDefaultCharset; - return $this; - } - - public function withLenientParsing($bLenientParsing = true) { - $this->bLenientParsing = $bLenientParsing; - return $this; - } - - public function beStrict() { - return $this->withLenientParsing(false); - } -} \ No newline at end of file +class Settings +{ + /** + * Multi-byte string support. If true (mbstring extension must be enabled), will use (slower) mb_strlen, mb_convert_case, mb_substr and mb_strpos functions. Otherwise, the normal (ASCII-Only) functions will be used. + */ + public $bMultibyteSupport; + + /** + * The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8. + */ + public $sDefaultCharset = 'utf-8'; + + /** + * Lenient parsing. When used (which is true by default), the parser will not choke on unexpected tokens but simply ignore them. + */ + public $bLenientParsing = true; + + private function __construct() + { + $this->bMultibyteSupport = extension_loaded('mbstring'); + } + + public static function create() + { + return new Settings(); + } + + public function withMultibyteSupport($bMultibyteSupport = true) + { + $this->bMultibyteSupport = $bMultibyteSupport; + return $this; + } + + public function withDefaultCharset($sDefaultCharset) + { + $this->sDefaultCharset = $sDefaultCharset; + return $this; + } + + public function withLenientParsing($bLenientParsing = true) + { + $this->bLenientParsing = $bLenientParsing; + return $this; + } + + public function beStrict() + { + return $this->withLenientParsing(false); + } +} diff --git a/lib/Sabberworm/CSS/Value/CSSFunction.php b/lib/Sabberworm/CSS/Value/CSSFunction.php index 28775505..9de442ea 100644 --- a/lib/Sabberworm/CSS/Value/CSSFunction.php +++ b/lib/Sabberworm/CSS/Value/CSSFunction.php @@ -2,44 +2,50 @@ namespace Sabberworm\CSS\Value; -class CSSFunction extends ValueList { - - protected $sName; - - 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); - } - - public function getName() { - return $this->sName; - } - - public function setName($sName) { - $this->sName = $sName; - } - - public function getArguments() { - return $this->aComponents; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $aArguments = parent::render($oOutputFormat); - return "{$this->sName}({$aArguments})"; - } - +class CSSFunction extends ValueList +{ + + protected $sName; + + 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); + } + + public function getName() + { + return $this->sName; + } + + public function setName($sName) + { + $this->sName = $sName; + } + + public function getArguments() + { + return $this->aComponents; + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + $aArguments = parent::render($oOutputFormat); + return "{$this->sName}({$aArguments})"; + } } diff --git a/lib/Sabberworm/CSS/Value/CSSString.php b/lib/Sabberworm/CSS/Value/CSSString.php index 8ab5b7d8..3de9416a 100644 --- a/lib/Sabberworm/CSS/Value/CSSString.php +++ b/lib/Sabberworm/CSS/Value/CSSString.php @@ -5,67 +5,73 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; -class CSSString extends PrimitiveValue { +class CSSString extends PrimitiveValue +{ - private $sString; + private $sString; - public function __construct($sString, $iLineNo = 0) { - $this->sString = $sString; - parent::__construct($iLineNo); - } + public function __construct($sString, $iLineNo = 0) + { + $this->sString = $sString; + parent::__construct($iLineNo); + } - public static function parse(ParserState $oParserState) { - $sBegin = $oParserState->peek(); - $sQuote = null; - if ($sBegin === "'") { - $sQuote = "'"; - } else if ($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()); - } + 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()); + } - public function setString($sString) { - $this->sString = $sString; - } + public function setString($sString) + { + $this->sString = $sString; + } - public function getString() { - return $this->sString; - } + public function getString() + { + return $this->sString; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sString = addslashes($this->sString); - $sString = str_replace("\n", '\A', $sString); - return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); - } - -} \ No newline at end of file + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + $sString = addslashes($this->sString); + $sString = str_replace("\n", '\A', $sString); + return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); + } +} diff --git a/lib/Sabberworm/CSS/Value/CalcFunction.php b/lib/Sabberworm/CSS/Value/CalcFunction.php index 0269fc50..21427516 100644 --- a/lib/Sabberworm/CSS/Value/CalcFunction.php +++ b/lib/Sabberworm/CSS/Value/CalcFunction.php @@ -5,61 +5,62 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class CalcFunction extends CSSFunction { - const T_OPERAND = 1; - const T_OPERATOR = 2; - - public static function parse(ParserState $oParserState) { - $aOperators = array('+', '-', '*', '/'); - $sFunction = trim($oParserState->consumeUntil('(', 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; - } else if ($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()); - } +class CalcFunction extends CSSFunction +{ + const T_OPERAND = 1; + const T_OPERATOR = 2; + public static function parse(ParserState $oParserState) + { + $aOperators = array('+', '-', '*', '/'); + $sFunction = trim($oParserState->consumeUntil('(', 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()); + } } diff --git a/lib/Sabberworm/CSS/Value/CalcRuleValueList.php b/lib/Sabberworm/CSS/Value/CalcRuleValueList.php index 512ee546..1e8d3458 100644 --- a/lib/Sabberworm/CSS/Value/CalcRuleValueList.php +++ b/lib/Sabberworm/CSS/Value/CalcRuleValueList.php @@ -2,18 +2,20 @@ namespace Sabberworm\CSS\Value; -class CalcRuleValueList extends RuleValueList { - public function __construct($iLineNo = 0) { - parent::__construct(array(), ',', $iLineNo); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return $oOutputFormat->implode(' ', $this->aComponents); - } +class CalcRuleValueList extends RuleValueList +{ + public function __construct($iLineNo = 0) + { + parent::__construct(array(), ',', $iLineNo); + } + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + return $oOutputFormat->implode(' ', $this->aComponents); + } } diff --git a/lib/Sabberworm/CSS/Value/Color.php b/lib/Sabberworm/CSS/Value/Color.php index 0c21665c..c134b5a6 100644 --- a/lib/Sabberworm/CSS/Value/Color.php +++ b/lib/Sabberworm/CSS/Value/Color.php @@ -4,114 +4,123 @@ use Sabberworm\CSS\Parsing\ParserState; -class Color extends CSSFunction { - - public function __construct($aColor, $iLineNo = 0) { - parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); - } - - public static function parse(ParserState $oParserState) { - $aColor = array(); - 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]; - } else if ($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 = array( - '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 = array( - '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()); - } - - 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; - } - - public function getColor() { - return $this->aComponents; - } - - public function setColor($aColor) { - $this->setName(implode('', array_keys($aColor))); - $this->aComponents = $aColor; - } - - public function getColorDescription() { - return $this->getName(); - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\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); - } +class Color extends CSSFunction +{ + + public function __construct($aColor, $iLineNo = 0) + { + parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); + } + + public static function parse(ParserState $oParserState) + { + $aColor = array(); + 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 = array( + '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 = array( + '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()); + } + + 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; + } + + public function getColor() + { + return $this->aComponents; + } + + public function setColor($aColor) + { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + public function getColorDescription() + { + return $this->getName(); + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\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); + } } diff --git a/lib/Sabberworm/CSS/Value/LineName.php b/lib/Sabberworm/CSS/Value/LineName.php index 677e80e8..7d3a8b91 100644 --- a/lib/Sabberworm/CSS/Value/LineName.php +++ b/lib/Sabberworm/CSS/Value/LineName.php @@ -5,44 +5,48 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class LineName extends ValueList { - public function __construct($aComponents = array(), $iLineNo = 0) { - parent::__construct($aComponents, ' ', $iLineNo); - } +class LineName extends ValueList +{ + public function __construct($aComponents = array(), $iLineNo = 0) + { + parent::__construct($aComponents, ' ', $iLineNo); + } - public static function parse(ParserState $oParserState) { - $oParserState->consume('['); - $oParserState->consumeWhiteSpace(); - $aNames = array(); - 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()); - } + public static function parse(ParserState $oParserState) + { + $oParserState->consume('['); + $oParserState->consumeWhiteSpace(); + $aNames = array(); + 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()); + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']'; - } + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']'; + } } diff --git a/lib/Sabberworm/CSS/Value/PrimitiveValue.php b/lib/Sabberworm/CSS/Value/PrimitiveValue.php index 187ce7e6..05b099b5 100644 --- a/lib/Sabberworm/CSS/Value/PrimitiveValue.php +++ b/lib/Sabberworm/CSS/Value/PrimitiveValue.php @@ -2,9 +2,10 @@ namespace Sabberworm\CSS\Value; -abstract class PrimitiveValue extends Value { - public function __construct($iLineNo = 0) { +abstract class PrimitiveValue extends Value +{ + public function __construct($iLineNo = 0) + { parent::__construct($iLineNo); } - -} \ No newline at end of file +} diff --git a/lib/Sabberworm/CSS/Value/RuleValueList.php b/lib/Sabberworm/CSS/Value/RuleValueList.php index d89c326c..3474d7b1 100644 --- a/lib/Sabberworm/CSS/Value/RuleValueList.php +++ b/lib/Sabberworm/CSS/Value/RuleValueList.php @@ -2,8 +2,10 @@ namespace Sabberworm\CSS\Value; -class RuleValueList extends ValueList { - public function __construct($sSeparator = ',', $iLineNo = 0) { - parent::__construct(array(), $sSeparator, $iLineNo); - } -} \ No newline at end of file +class RuleValueList extends ValueList +{ + public function __construct($sSeparator = ',', $iLineNo = 0) + { + parent::__construct(array(), $sSeparator, $iLineNo); + } +} diff --git a/lib/Sabberworm/CSS/Value/Size.php b/lib/Sabberworm/CSS/Value/Size.php index 730f2dd4..41aa1bc6 100644 --- a/lib/Sabberworm/CSS/Value/Size.php +++ b/lib/Sabberworm/CSS/Value/Size.php @@ -4,124 +4,136 @@ use Sabberworm\CSS\Parsing\ParserState; -class Size extends PrimitiveValue { - - const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) - const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr'; - const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz'; - - private static $SIZE_UNITS = null; - - private $fSize; - private $sUnit; - private $bIsColorComponent; - - public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) { - parent::__construct($iLineNo); - $this->fSize = floatval($fSize); - $this->sUnit = $sUnit; - $this->bIsColorComponent = $bIsColorComponent; - } - - 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(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine()); - } - - private static function getSizeUnits() { - if(self::$SIZE_UNITS === null) { - self::$SIZE_UNITS = array(); - foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) { - $iSize = strlen($val); - if(!isset(self::$SIZE_UNITS[$iSize])) { - self::$SIZE_UNITS[$iSize] = array(); - } - self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; - } - - krsort(self::$SIZE_UNITS, SORT_NUMERIC); - } - - return self::$SIZE_UNITS; - } - - public function setUnit($sUnit) { - $this->sUnit = $sUnit; - } - - public function getUnit() { - return $this->sUnit; - } - - public function setSize($fSize) { - $this->fSize = floatval($fSize); - } - - public function getSize() { - return $this->fSize; - } - - 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, explode('/', self::NON_SIZE_UNITS))) { - return false; - } - return !$this->isColorComponent(); - } - - public function isRelative() { - if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) { - return true; - } - if ($this->sUnit === null && $this->fSize != 0) { - return true; - } - return false; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\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(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $sSize) . ($this->sUnit === null ? '' : $this->sUnit); - } - +class Size extends PrimitiveValue +{ + + const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) + const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr'; + const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz'; + + private static $SIZE_UNITS = null; + + private $fSize; + private $sUnit; + private $bIsColorComponent; + + public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->fSize = floatval($fSize); + $this->sUnit = $sUnit; + $this->bIsColorComponent = $bIsColorComponent; + } + + 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(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine()); + } + + private static function getSizeUnits() + { + if (self::$SIZE_UNITS === null) { + self::$SIZE_UNITS = array(); + foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS . '/' . Size::RELATIVE_SIZE_UNITS . '/' . Size::NON_SIZE_UNITS) as $val) { + $iSize = strlen($val); + if (!isset(self::$SIZE_UNITS[$iSize])) { + self::$SIZE_UNITS[$iSize] = array(); + } + self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; + } + + krsort(self::$SIZE_UNITS, SORT_NUMERIC); + } + + return self::$SIZE_UNITS; + } + + public function setUnit($sUnit) + { + $this->sUnit = $sUnit; + } + + public function getUnit() + { + return $this->sUnit; + } + + public function setSize($fSize) + { + $this->fSize = floatval($fSize); + } + + public function getSize() + { + return $this->fSize; + } + + 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, explode('/', self::NON_SIZE_UNITS))) { + return false; + } + return !$this->isColorComponent(); + } + + public function isRelative() + { + if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) { + return true; + } + if ($this->sUnit === null && $this->fSize != 0) { + return true; + } + return false; + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\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(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $sSize) . ($this->sUnit === null ? '' : $this->sUnit); + } } diff --git a/lib/Sabberworm/CSS/Value/URL.php b/lib/Sabberworm/CSS/Value/URL.php index 00a741ee..cc683d01 100644 --- a/lib/Sabberworm/CSS/Value/URL.php +++ b/lib/Sabberworm/CSS/Value/URL.php @@ -4,51 +4,57 @@ use Sabberworm\CSS\Parsing\ParserState; -class URL extends PrimitiveValue { - - private $oURL; - - public function __construct(CSSString $oURL, $iLineNo = 0) { - parent::__construct($iLineNo); - $this->oURL = $oURL; - } - - 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; - } - - - public function setURL(CSSString $oURL) { - $this->oURL = $oURL; - } - - public function getURL() { - return $this->oURL; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "url({$this->oURL->render($oOutputFormat)})"; - } - -} \ No newline at end of file +class URL extends PrimitiveValue +{ + + private $oURL; + + public function __construct(CSSString $oURL, $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->oURL = $oURL; + } + + 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; + } + + + public function setURL(CSSString $oURL) + { + $this->oURL = $oURL; + } + + public function getURL() + { + return $this->oURL; + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + return "url({$this->oURL->render($oOutputFormat)})"; + } +} diff --git a/lib/Sabberworm/CSS/Value/Value.php b/lib/Sabberworm/CSS/Value/Value.php index 56815306..31f66dcd 100644 --- a/lib/Sabberworm/CSS/Value/Value.php +++ b/lib/Sabberworm/CSS/Value/Value.php @@ -6,126 +6,136 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Renderable; -abstract class Value implements Renderable { - protected $iLineNo; +abstract class Value implements Renderable +{ + protected $iLineNo; - public function __construct($iLineNo = 0) { - $this->iLineNo = $iLineNo; - } + public function __construct($iLineNo = 0) + { + $this->iLineNo = $iLineNo; + } - public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) { - $aStack = array(); - $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, array($oList)); - } - } - if (!isset($aStack[0])) { - throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine()); - } - return $aStack[0]; - } + public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) + { + $aStack = array(); + $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, array($oList)); + } + } + if (!isset($aStack[0])) { + throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine()); + } + return $aStack[0]; + } - public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) { - $sResult = $oParserState->parseIdentifier($bIgnoreCase); + public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) + { + $sResult = $oParserState->parseIdentifier($bIgnoreCase); - if ($oParserState->comes('(')) { - $oParserState->consume('('); - $aArguments = Value::parseValue($oParserState, array('=', ' ', ',')); - $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); - $oParserState->consume(')'); - } + if ($oParserState->comes('(')) { + $oParserState->consume('('); + $aArguments = Value::parseValue($oParserState, array('=', ' ', ',')); + $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); + $oParserState->consume(')'); + } - return $sResult; - } + return $sResult; + } - 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); - } else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { - $oValue = Color::parse($oParserState); - } else if ($oParserState->comes('url', true)) { - $oValue = URL::parse($oParserState); - } else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) { - $oValue = CalcFunction::parse($oParserState); - } else if ($oParserState->comes("'") || $oParserState->comes('"')) { - $oValue = CSSString::parse($oParserState); - } else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { - $oValue = self::parseMicrosoftFilter($oParserState); - } else if ($oParserState->comes("[")) { - $oValue = LineName::parse($oParserState); - } else if ($oParserState->comes("U+")) { - $oValue = self::parseUnicodeRangeValue($oParserState); - } else { - $oValue = self::parseIdentifierOrFunction($oParserState); - } - $oParserState->consumeWhiteSpace(); - return $oValue; - } + 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; + } - private static function parseMicrosoftFilter(ParserState $oParserState) { - $sFunction = $oParserState->consumeUntil('(', false, true); - $aArguments = Value::parseValue($oParserState, array(',', '=')); - return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); - } + private static function parseMicrosoftFilter(ParserState $oParserState) + { + $sFunction = $oParserState->consumeUntil('(', false, true); + $aArguments = Value::parseValue($oParserState, array(',', '=')); + return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); + } - private static function parseUnicodeRangeValue(ParserState $oParserState) { - $iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits - $sRange = ""; - $oParserState->consume("U+"); - do { - if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them - $sRange .= $oParserState->consume(1); - } while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); - return "U+{$sRange}"; - } + private static function parseUnicodeRangeValue(ParserState $oParserState) + { + $iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits + $sRange = ""; + $oParserState->consume("U+"); + do { + if ($oParserState->comes('-')) { + $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them + } + $sRange .= $oParserState->consume(1); + } while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); + return "U+{$sRange}"; + } - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9 - //public abstract function __toString(); - //public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); + //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9 + //public abstract function __toString(); + //public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); } diff --git a/lib/Sabberworm/CSS/Value/ValueList.php b/lib/Sabberworm/CSS/Value/ValueList.php index 5bfc0ba9..c567b61e 100644 --- a/lib/Sabberworm/CSS/Value/ValueList.php +++ b/lib/Sabberworm/CSS/Value/ValueList.php @@ -2,51 +2,59 @@ namespace Sabberworm\CSS\Value; -abstract class ValueList extends Value { - - protected $aComponents; - protected $sSeparator; - - public function __construct($aComponents = array(), $sSeparator = ',', $iLineNo = 0) { - parent::__construct($iLineNo); - if (!is_array($aComponents)) { - $aComponents = array($aComponents); - } - $this->aComponents = $aComponents; - $this->sSeparator = $sSeparator; - } - - public function addListComponent($mComponent) { - $this->aComponents[] = $mComponent; - } - - public function getListComponents() { - return $this->aComponents; - } - - public function setListComponents($aComponents) { - $this->aComponents = $aComponents; - } - - public function getListSeparator() { - return $this->sSeparator; - } - - public function setListSeparator($sSeparator) { - $this->sSeparator = $sSeparator; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @param \Sabberworm\CSS\OutputFormat $oOutputFormat - * - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents); - } - +abstract class ValueList extends Value +{ + + protected $aComponents; + protected $sSeparator; + + public function __construct($aComponents = array(), $sSeparator = ',', $iLineNo = 0) + { + parent::__construct($iLineNo); + if (!is_array($aComponents)) { + $aComponents = array($aComponents); + } + $this->aComponents = $aComponents; + $this->sSeparator = $sSeparator; + } + + public function addListComponent($mComponent) + { + $this->aComponents[] = $mComponent; + } + + public function getListComponents() + { + return $this->aComponents; + } + + public function setListComponents($aComponents) + { + $this->aComponents = $aComponents; + } + + public function getListSeparator() + { + return $this->sSeparator; + } + + public function setListSeparator($sSeparator) + { + $this->sSeparator = $sSeparator; + } + + public function __toString() + { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @param \Sabberworm\CSS\OutputFormat $oOutputFormat + * + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) + { + return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents); + } } diff --git a/tests/Sabberworm/CSS/CSSList/AtRuleBlockListTest.php b/tests/Sabberworm/CSS/CSSList/AtRuleBlockListTest.php index d3d1ec6b..3d0268d3 100644 --- a/tests/Sabberworm/CSS/CSSList/AtRuleBlockListTest.php +++ b/tests/Sabberworm/CSS/CSSList/AtRuleBlockListTest.php @@ -4,24 +4,25 @@ use Sabberworm\CSS\Parser; -class AtRuleBlockListTest extends \PHPUnit\Framework\TestCase { +class AtRuleBlockListTest extends \PHPUnit\Framework\TestCase +{ - public function testMediaQueries() { - $sCss = '@media(min-width: 768px){.class{color:red}}'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $oMediaQuery = $aContents[0]; - $this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function'); - $this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value'); - - $sCss = '@media (min-width: 768px) {.class{color:red}}'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $oMediaQuery = $aContents[0]; - $this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function'); - $this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value'); - } + public function testMediaQueries() + { + $sCss = '@media(min-width: 768px){.class{color:red}}'; + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + $aContents = $oDoc->getContents(); + $oMediaQuery = $aContents[0]; + $this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function'); + $this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value'); + $sCss = '@media (min-width: 768px) {.class{color:red}}'; + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + $aContents = $oDoc->getContents(); + $oMediaQuery = $aContents[0]; + $this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function'); + $this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value'); + } } diff --git a/tests/Sabberworm/CSS/CSSList/DocumentTest.php b/tests/Sabberworm/CSS/CSSList/DocumentTest.php index b0ad811d..7f47bb4f 100644 --- a/tests/Sabberworm/CSS/CSSList/DocumentTest.php +++ b/tests/Sabberworm/CSS/CSSList/DocumentTest.php @@ -4,23 +4,24 @@ use Sabberworm\CSS\Parser; -class DocumentTest extends \PHPUnit\Framework\TestCase { +class DocumentTest extends \PHPUnit\Framework\TestCase +{ - public function testOverrideContents() { - $sCss = '.thing { left: 10px; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $this->assertCount(1, $aContents); + public function testOverrideContents() + { + $sCss = '.thing { left: 10px; }'; + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + $aContents = $oDoc->getContents(); + $this->assertCount(1, $aContents); - $sCss2 = '.otherthing { right: 10px; }'; - $oParser2 = new Parser($sCss); - $oDoc2 = $oParser2->parse(); - $aContents2 = $oDoc2->getContents(); - - $oDoc->setContents(array($aContents[0], $aContents2[0])); - $aFinalContents = $oDoc->getContents(); - $this->assertCount(2, $aFinalContents); - } + $sCss2 = '.otherthing { right: 10px; }'; + $oParser2 = new Parser($sCss); + $oDoc2 = $oParser2->parse(); + $aContents2 = $oDoc2->getContents(); + $oDoc->setContents(array($aContents[0], $aContents2[0])); + $aFinalContents = $oDoc->getContents(); + $this->assertCount(2, $aFinalContents); + } } diff --git a/tests/Sabberworm/CSS/OutputFormatTest.php b/tests/Sabberworm/CSS/OutputFormatTest.php index d40e9edf..819830da 100644 --- a/tests/Sabberworm/CSS/OutputFormatTest.php +++ b/tests/Sabberworm/CSS/OutputFormatTest.php @@ -24,68 +24,81 @@ EOT; -class OutputFormatTest extends \PHPunit\Framework\TestCase { - private $oParser; - private $oDocument; - - function setUp() { - global $TEST_CSS; - $this->oParser = new Parser($TEST_CSS); - $this->oDocument = $this->oParser->parse(); - } - - public function testPlain() { - $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} +class OutputFormatTest extends \PHPunit\Framework\TestCase +{ + private $oParser; + private $oDocument; + + function setUp() + { + global $TEST_CSS; + $this->oParser = new Parser($TEST_CSS); + $this->oDocument = $this->oParser->parse(); + } + + public function testPlain() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render()); - } - - public function testCompact() { - $this->assertSame('.main,.test{font:italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background:white;}@media screen{.main{background-size:100% 100%;font-size:1.3em;background-color:#fff;}}', $this->oDocument->render(OutputFormat::createCompact())); - } - - public function testPretty() { - global $TEST_CSS; - $this->assertSame($TEST_CSS, $this->oDocument->render(OutputFormat::createPretty())); - } - - public function testSpaceAfterListArgumentSeparator() { - $this->assertSame('.main, .test {font: italic normal bold 16px/ 1.2 "Helvetica", Verdana, sans-serif;background: white;} + } + + public function testCompact() + { + $this->assertSame('.main,.test{font:italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background:white;}@media screen{.main{background-size:100% 100%;font-size:1.3em;background-color:#fff;}}', $this->oDocument->render(OutputFormat::createCompact())); + } + + public function testPretty() + { + global $TEST_CSS; + $this->assertSame($TEST_CSS, $this->oDocument->render(OutputFormat::createPretty())); + } + + public function testSpaceAfterListArgumentSeparator() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/ 1.2 "Helvetica", Verdana, sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(" "))); - } + } - public function testSpaceAfterListArgumentSeparatorComplex() { - $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica", Verdana, sans-serif;background: white;} + public function testSpaceAfterListArgumentSeparatorComplex() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica", Verdana, sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(array('default' => ' ', ',' => "\t", '/' => '', ' ' => '')))); - } + } - public function testSpaceAfterSelectorSeparator() { - $this->assertSame('.main, + public function testSpaceAfterSelectorSeparator() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n"))); - } + } - public function testStringQuotingType() { - $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 \'Helvetica\',Verdana,sans-serif;background: white;} + public function testStringQuotingType() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 \'Helvetica\',Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setStringQuotingType("'"))); - } + } - public function testRGBHashNotation() { - $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} + public function testRGBHashNotation() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: rgb(255,255,255);}}', $this->oDocument->render(OutputFormat::create()->setRGBHashNotation(false))); - } + } - public function testSemicolonAfterLastRule() { - $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white} + public function testSemicolonAfterLastRule() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff}}', $this->oDocument->render(OutputFormat::create()->setSemicolonAfterLastRule(false))); - } + } - public function testSpaceAfterRuleName() { - $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} + public function testSpaceAfterRuleName() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterRuleName("\t"))); - } + } - public function testSpaceRules() { - $this->assertSame('.main, .test { + public function testSpaceRules() + { + $this->assertSame('.main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; background: white; } @@ -94,19 +107,21 @@ public function testSpaceRules() { font-size: 1.3em; background-color: #fff; }}', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n"))); - } + } - public function testSpaceBlocks() { - $this->assertSame(' + public function testSpaceBlocks() + { + $this->assertSame(' .main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen { .main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;} } ', $this->oDocument->render(OutputFormat::create()->set('Space*Blocks', "\n"))); - } + } - public function testSpaceBoth() { - $this->assertSame(' + public function testSpaceBoth() + { + $this->assertSame(' .main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; background: white; @@ -119,14 +134,16 @@ public function testSpaceBoth() { } } ', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n"))); - } + } - public function testSpaceBetweenBlocks() { - $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceBetweenBlocks(''))); - } + public function testSpaceBetweenBlocks() + { + $this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceBetweenBlocks(''))); + } - public function testIndentation() { - $this->assertSame(' + public function testIndentation() + { + $this->assertSame(' .main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; background: white; @@ -139,32 +156,34 @@ public function testIndentation() { } } ', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setIndentation(''))); - } - - public function testSpaceBeforeBraces() { - $this->assertSame('.main, .test{font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} + } + + public function testSpaceBeforeBraces() + { + $this->assertSame('.main, .test{font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen{.main{background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceBeforeOpeningBrace(''))); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\OutputException - */ - public function testIgnoreExceptionsOff() { - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); - $this->assertSame('.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} + } + + /** + * @expectedException Sabberworm\CSS\Parsing\OutputException + */ + public function testIgnoreExceptionsOff() + { + $aBlocks = $this->oDocument->getAllDeclarationBlocks(); + $oFirstBlock = $aBlocks[0]; + $oFirstBlock->removeSelector('.main'); + $this->assertSame('.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false))); - $oFirstBlock->removeSelector('.test'); - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)); - } - - public function testIgnoreExceptionsOn() { - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); - $oFirstBlock->removeSelector('.test'); - $this->assertSame('@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(true))); - } - -} \ No newline at end of file + $oFirstBlock->removeSelector('.test'); + $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)); + } + + public function testIgnoreExceptionsOn() + { + $aBlocks = $this->oDocument->getAllDeclarationBlocks(); + $oFirstBlock = $aBlocks[0]; + $oFirstBlock->removeSelector('.main'); + $oFirstBlock->removeSelector('.test'); + $this->assertSame('@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(true))); + } +} diff --git a/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php index 84d19484..4094a8f6 100644 --- a/tests/Sabberworm/CSS/ParserTest.php +++ b/tests/Sabberworm/CSS/ParserTest.php @@ -10,169 +10,176 @@ use Sabberworm\CSS\Value\URL; use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class ParserTest extends \PHPunit\Framework\TestCase { - - function testFiles() { - $sDirectory = __DIR__ . '/../../files'; - if ($rHandle = opendir($sDirectory)) { - /* This is the correct way to loop over the directory. */ - while (false !== ($sFileName = readdir($rHandle))) { - if (strpos($sFileName, '.') === 0) { - continue; - } - if (strrpos($sFileName, '.css') !== strlen($sFileName) - strlen('.css')) { - continue; - } - if (strpos($sFileName, '-') === 0) { - //Either a file which SHOULD fail (at least in strict mode) or a future test of a as-of-now missing feature - continue; - } - $oParser = new Parser(file_get_contents($sDirectory . DIRECTORY_SEPARATOR . $sFileName)); - try { - $this->assertNotEquals('', $oParser->parse()->render()); - } catch (\Exception $e) { - $this->fail($e); - } - } - closedir($rHandle); - } - } - - /** - * @depends testFiles - */ - function testColorParsing() { - $oDoc = $this->parsedStructureForFile('colortest'); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - if (!$oRuleSet instanceof DeclarationBlock) { - continue; - } - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if ($sSelector === '#mine') { - $aColorRule = $oRuleSet->getRules('color'); - $oColor = $aColorRule[0]->getValue(); - $this->assertSame('red', $oColor); - $aColorRule = $oRuleSet->getRules('background-'); - $oColor = $aColorRule[0]->getValue(); - $this->assertEquals(array('r' => new Size(35.0, null, true, $oColor->getLineNo()), 'g' => new Size(35.0, null, true, $oColor->getLineNo()), 'b' => new Size(35.0, null, true, $oColor->getLineNo())), $oColor->getColor()); - $aColorRule = $oRuleSet->getRules('border-color'); - $oColor = $aColorRule[0]->getValue(); - $this->assertEquals(array('r' => new Size(10.0, null, true, $oColor->getLineNo()), 'g' => new Size(100.0, null, true, $oColor->getLineNo()), 'b' => new Size(230.0, null, true, $oColor->getLineNo())), $oColor->getColor()); - $oColor = $aColorRule[1]->getValue(); - $this->assertEquals(array('r' => new Size(10.0, null, true, $oColor->getLineNo()), 'g' => new Size(100.0, null, true, $oColor->getLineNo()), 'b' => new Size(231.0, null, true, $oColor->getLineNo()), 'a' => new Size("0000.3", null, true, $oColor->getLineNo())), $oColor->getColor()); - $aColorRule = $oRuleSet->getRules('outline-color'); - $oColor = $aColorRule[0]->getValue(); - $this->assertEquals(array('r' => new Size(34.0, null, true, $oColor->getLineNo()), 'g' => new Size(34.0, null, true, $oColor->getLineNo()), 'b' => new Size(34.0, null, true, $oColor->getLineNo())), $oColor->getColor()); - } else if($sSelector === '#yours') { - $aColorRule = $oRuleSet->getRules('background-color'); - $oColor = $aColorRule[0]->getValue(); - $this->assertEquals(array('h' => new Size(220.0, null, true, $oColor->getLineNo()), 's' => new Size(10.0, '%', true, $oColor->getLineNo()), 'l' => new Size(220.0, '%', true, $oColor->getLineNo())), $oColor->getColor()); - $oColor = $aColorRule[1]->getValue(); - $this->assertEquals(array('h' => new Size(220.0, null, true, $oColor->getLineNo()), 's' => new Size(10.0, '%', true, $oColor->getLineNo()), 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), 'a' => new Size(0000.3, null, true, $oColor->getLineNo())), $oColor->getColor()); - } - } - foreach ($oDoc->getAllValues('color') as $sColor) { - $this->assertSame('red', $sColor); - } - $this->assertSame('#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;background-color: #232323;} +class ParserTest extends \PHPunit\Framework\TestCase +{ + + function testFiles() + { + $sDirectory = __DIR__ . '/../../files'; + if ($rHandle = opendir($sDirectory)) { + /* This is the correct way to loop over the directory. */ + while (false !== ($sFileName = readdir($rHandle))) { + if (strpos($sFileName, '.') === 0) { + continue; + } + if (strrpos($sFileName, '.css') !== strlen($sFileName) - strlen('.css')) { + continue; + } + if (strpos($sFileName, '-') === 0) { + //Either a file which SHOULD fail (at least in strict mode) or a future test of a as-of-now missing feature + continue; + } + $oParser = new Parser(file_get_contents($sDirectory . DIRECTORY_SEPARATOR . $sFileName)); + try { + $this->assertNotEquals('', $oParser->parse()->render()); + } catch (\Exception $e) { + $this->fail($e); + } + } + closedir($rHandle); + } + } + + /** + * @depends testFiles + */ + function testColorParsing() + { + $oDoc = $this->parsedStructureForFile('colortest'); + foreach ($oDoc->getAllRuleSets() as $oRuleSet) { + if (!$oRuleSet instanceof DeclarationBlock) { + continue; + } + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if ($sSelector === '#mine') { + $aColorRule = $oRuleSet->getRules('color'); + $oColor = $aColorRule[0]->getValue(); + $this->assertSame('red', $oColor); + $aColorRule = $oRuleSet->getRules('background-'); + $oColor = $aColorRule[0]->getValue(); + $this->assertEquals(array('r' => new Size(35.0, null, true, $oColor->getLineNo()), 'g' => new Size(35.0, null, true, $oColor->getLineNo()), 'b' => new Size(35.0, null, true, $oColor->getLineNo())), $oColor->getColor()); + $aColorRule = $oRuleSet->getRules('border-color'); + $oColor = $aColorRule[0]->getValue(); + $this->assertEquals(array('r' => new Size(10.0, null, true, $oColor->getLineNo()), 'g' => new Size(100.0, null, true, $oColor->getLineNo()), 'b' => new Size(230.0, null, true, $oColor->getLineNo())), $oColor->getColor()); + $oColor = $aColorRule[1]->getValue(); + $this->assertEquals(array('r' => new Size(10.0, null, true, $oColor->getLineNo()), 'g' => new Size(100.0, null, true, $oColor->getLineNo()), 'b' => new Size(231.0, null, true, $oColor->getLineNo()), 'a' => new Size("0000.3", null, true, $oColor->getLineNo())), $oColor->getColor()); + $aColorRule = $oRuleSet->getRules('outline-color'); + $oColor = $aColorRule[0]->getValue(); + $this->assertEquals(array('r' => new Size(34.0, null, true, $oColor->getLineNo()), 'g' => new Size(34.0, null, true, $oColor->getLineNo()), 'b' => new Size(34.0, null, true, $oColor->getLineNo())), $oColor->getColor()); + } elseif ($sSelector === '#yours') { + $aColorRule = $oRuleSet->getRules('background-color'); + $oColor = $aColorRule[0]->getValue(); + $this->assertEquals(array('h' => new Size(220.0, null, true, $oColor->getLineNo()), 's' => new Size(10.0, '%', true, $oColor->getLineNo()), 'l' => new Size(220.0, '%', true, $oColor->getLineNo())), $oColor->getColor()); + $oColor = $aColorRule[1]->getValue(); + $this->assertEquals(array('h' => new Size(220.0, null, true, $oColor->getLineNo()), 's' => new Size(10.0, '%', true, $oColor->getLineNo()), 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), 'a' => new Size(0000.3, null, true, $oColor->getLineNo())), $oColor->getColor()); + } + } + foreach ($oDoc->getAllValues('color') as $sColor) { + $this->assertSame('red', $sColor); + } + $this->assertSame('#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;background-color: #232323;} #yours {background-color: hsl(220,10%,220%);background-color: hsla(220,10%,220%,.3);} #variables {background-color: rgb(var(--some-rgb));background-color: rgb(var(--r),var(--g),var(--b));background-color: rgb(255,var(--g),var(--b));background-color: rgb(255,255,var(--b));background-color: rgb(255,var(--rg));background-color: hsl(var(--some-hsl));} #variables-alpha {background-color: rgba(var(--some-rgb),.1);background-color: rgba(var(--some-rg),255,.1);background-color: hsla(var(--some-hsl),.1);}', $oDoc->render()); - } - - function testUnicodeParsing() { - $oDoc = $this->parsedStructureForFile('unicode'); - foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) { - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if (substr($sSelector, 0, strlen('.test-')) !== '.test-') { - continue; - } - $aContentRules = $oRuleSet->getRules('content'); - $aContents = $aContentRules[0]->getValues(); - $sString = $aContents[0][0]->__toString(); - if ($sSelector == '.test-1') { - $this->assertSame('" "', $sString); - } - if ($sSelector == '.test-2') { - $this->assertSame('"é"', $sString); - } - if ($sSelector == '.test-3') { - $this->assertSame('" "', $sString); - } - if ($sSelector == '.test-4') { - $this->assertSame('"𝄞"', $sString); - } - if ($sSelector == '.test-5') { - $this->assertSame('"水"', $sString); - } - if ($sSelector == '.test-6') { - $this->assertSame('"¥"', $sString); - } - if ($sSelector == '.test-7') { - $this->assertSame('"\A"', $sString); - } - if ($sSelector == '.test-8') { - $this->assertSame('"\"\""', $sString); - } - if ($sSelector == '.test-9') { - $this->assertSame('"\"\\\'"', $sString); - } - if ($sSelector == '.test-10') { - $this->assertSame('"\\\'\\\\"', $sString); - } - if ($sSelector == '.test-11') { - $this->assertSame('"test"', $sString); - } - } - } - - function testUnicodeRangeParsing() { - $oDoc = $this->parsedStructureForFile('unicode-range'); - $sExpected = "@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}"; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testSpecificity() { - $oDoc = $this->parsedStructureForFile('specificity'); - $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); - $oDeclarationBlock = $oDeclarationBlock[0]; - $aSelectors = $oDeclarationBlock->getSelectors(); - foreach ($aSelectors as $oSelector) { - switch ($oSelector->getSelector()) { - case "#test .help": - $this->assertSame(110, $oSelector->getSpecificity()); - break; - case "#file": - $this->assertSame(100, $oSelector->getSpecificity()); - break; - case ".help:hover": - $this->assertSame(20, $oSelector->getSpecificity()); - break; - case "ol li::before": - $this->assertSame(3, $oSelector->getSpecificity()); - break; - case "li.green": - $this->assertSame(11, $oSelector->getSpecificity()); - break; - default: - $this->fail("specificity: untested selector " . $oSelector->getSelector()); - } - } - $this->assertEquals(array(new Selector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100')); - $this->assertEquals(array(new Selector('#test .help', true), new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('>= 100')); - $this->assertEquals(array(new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('=== 100')); - $this->assertEquals(array(new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('== 100')); - $this->assertEquals(array(new Selector('#file', true), new Selector('.help:hover', true), new Selector('li.green', true), new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity('<= 100')); - $this->assertEquals(array(new Selector('.help:hover', true), new Selector('li.green', true), new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity('< 100')); - $this->assertEquals(array(new Selector('li.green', true)), $oDoc->getSelectorsBySpecificity('11')); - $this->assertEquals(array(new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity(3)); - } - - function testManipulation() { - $oDoc = $this->parsedStructureForFile('atrules'); - $this->assertSame('@charset "utf-8"; + } + + function testUnicodeParsing() + { + $oDoc = $this->parsedStructureForFile('unicode'); + foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) { + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if (substr($sSelector, 0, strlen('.test-')) !== '.test-') { + continue; + } + $aContentRules = $oRuleSet->getRules('content'); + $aContents = $aContentRules[0]->getValues(); + $sString = $aContents[0][0]->__toString(); + if ($sSelector == '.test-1') { + $this->assertSame('" "', $sString); + } + if ($sSelector == '.test-2') { + $this->assertSame('"é"', $sString); + } + if ($sSelector == '.test-3') { + $this->assertSame('" "', $sString); + } + if ($sSelector == '.test-4') { + $this->assertSame('"𝄞"', $sString); + } + if ($sSelector == '.test-5') { + $this->assertSame('"水"', $sString); + } + if ($sSelector == '.test-6') { + $this->assertSame('"¥"', $sString); + } + if ($sSelector == '.test-7') { + $this->assertSame('"\A"', $sString); + } + if ($sSelector == '.test-8') { + $this->assertSame('"\"\""', $sString); + } + if ($sSelector == '.test-9') { + $this->assertSame('"\"\\\'"', $sString); + } + if ($sSelector == '.test-10') { + $this->assertSame('"\\\'\\\\"', $sString); + } + if ($sSelector == '.test-11') { + $this->assertSame('"test"', $sString); + } + } + } + + function testUnicodeRangeParsing() + { + $oDoc = $this->parsedStructureForFile('unicode-range'); + $sExpected = "@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}"; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testSpecificity() + { + $oDoc = $this->parsedStructureForFile('specificity'); + $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); + $oDeclarationBlock = $oDeclarationBlock[0]; + $aSelectors = $oDeclarationBlock->getSelectors(); + foreach ($aSelectors as $oSelector) { + switch ($oSelector->getSelector()) { + case "#test .help": + $this->assertSame(110, $oSelector->getSpecificity()); + break; + case "#file": + $this->assertSame(100, $oSelector->getSpecificity()); + break; + case ".help:hover": + $this->assertSame(20, $oSelector->getSpecificity()); + break; + case "ol li::before": + $this->assertSame(3, $oSelector->getSpecificity()); + break; + case "li.green": + $this->assertSame(11, $oSelector->getSpecificity()); + break; + default: + $this->fail("specificity: untested selector " . $oSelector->getSelector()); + } + } + $this->assertEquals(array(new Selector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100')); + $this->assertEquals(array(new Selector('#test .help', true), new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('>= 100')); + $this->assertEquals(array(new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('=== 100')); + $this->assertEquals(array(new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('== 100')); + $this->assertEquals(array(new Selector('#file', true), new Selector('.help:hover', true), new Selector('li.green', true), new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity('<= 100')); + $this->assertEquals(array(new Selector('.help:hover', true), new Selector('li.green', true), new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity('< 100')); + $this->assertEquals(array(new Selector('li.green', true)), $oDoc->getSelectorsBySpecificity('11')); + $this->assertEquals(array(new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity(3)); + } + + function testManipulation() + { + $oDoc = $this->parsedStructureForFile('atrules'); + $this->assertSame('@charset "utf-8"; @font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} html, body {font-size: -.6em;} @keyframes mymove {from {top: 0px;} @@ -187,13 +194,13 @@ function testManipulation() { regexp("https:.*") {body {color: purple;background: yellow;}} @media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}} @region-style #intro {p {color: blue;}}', $oDoc->render()); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { - //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id - $oSelector->setSelector('#my_id ' . $oSelector->getSelector()); - } - } - $this->assertSame('@charset "utf-8"; + foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { + foreach ($oBlock->getSelectors() as $oSelector) { + //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id + $oSelector->setSelector('#my_id ' . $oSelector->getSelector()); + } + } + $this->assertSame('@charset "utf-8"; @font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} #my_id html, #my_id body {font-size: -.6em;} @keyframes mymove {from {top: 0px;} @@ -209,582 +216,631 @@ function testManipulation() { @media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}} @region-style #intro {#my_id p {color: blue;}}', $oDoc->render()); - $oDoc = $this->parsedStructureForFile('values'); - $this->assertSame('#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;font-size: 10px;color: red !important;background-color: green;background-color: rgba(0,128,0,.7);frequency: 30Hz;} + $oDoc = $this->parsedStructureForFile('values'); + $this->assertSame('#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;font-size: 10px;color: red !important;background-color: green;background-color: rgba(0,128,0,.7);frequency: 30Hz;} body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}', $oDoc->render()); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('font-'); - } - $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;background-color: rgba(0,128,0,.7);frequency: 30Hz;} + foreach ($oDoc->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('font-'); + } + $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;background-color: rgba(0,128,0,.7);frequency: 30Hz;} body {color: green;}', $oDoc->render()); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('background-'); - } - $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;} + foreach ($oDoc->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('background-'); + } + $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;} body {color: green;}', $oDoc->render()); - } - - function testRuleGetters() { - $oDoc = $this->parsedStructureForFile('values'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oHeaderBlock = $aBlocks[0]; - $oBodyBlock = $aBlocks[1]; - $aHeaderRules = $oHeaderBlock->getRules('background-'); - $this->assertCount(2, $aHeaderRules); - $this->assertSame('background-color', $aHeaderRules[0]->getRule()); - $this->assertSame('background-color', $aHeaderRules[1]->getRule()); - $aHeaderRules = $oHeaderBlock->getRulesAssoc('background-'); - $this->assertCount(1, $aHeaderRules); - $this->assertTrue($aHeaderRules['background-color']->getValue() instanceof \Sabberworm\CSS\Value\Color); - $this->assertSame('rgba', $aHeaderRules['background-color']->getValue()->getColorDescription()); - $oHeaderBlock->removeRule($aHeaderRules['background-color']); - $aHeaderRules = $oHeaderBlock->getRules('background-'); - $this->assertCount(1, $aHeaderRules); - $this->assertSame('green', $aHeaderRules[0]->getValue()); - } - - function testSlashedValues() { - $oDoc = $this->parsedStructureForFile('slashed'); - $this->assertSame('.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}', $oDoc->render()); - foreach ($oDoc->getAllValues(null) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) { - $mValue->setSize($mValue->getSize() * 3); - } - } - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oRule = $oBlock->getRules('font'); - $oRule = $oRule[0]; - $oSpaceList = $oRule->getValue(); - $this->assertEquals(' ', $oSpaceList->getListSeparator()); - $oSlashList = $oSpaceList->getListComponents(); - $oCommaList = $oSlashList[1]; - $oSlashList = $oSlashList[0]; - $this->assertEquals(',', $oCommaList->getListSeparator()); - $this->assertEquals('/', $oSlashList->getListSeparator()); - $oRule = $oBlock->getRules('border-radius'); - $oRule = $oRule[0]; - $oSlashList = $oRule->getValue(); - $this->assertEquals('/', $oSlashList->getListSeparator()); - $oSpaceList1 = $oSlashList->getListComponents(); - $oSpaceList2 = $oSpaceList1[1]; - $oSpaceList1 = $oSpaceList1[0]; - $this->assertEquals(' ', $oSpaceList1->getListSeparator()); - $this->assertEquals(' ', $oSpaceList2->getListSeparator()); - } - $this->assertSame('.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}', $oDoc->render()); - } - - function testFunctionSyntax() { - $oDoc = $this->parsedStructureForFile('functions'); - $sExpected = 'div.main {background-image: linear-gradient(#000,#fff);} + } + + function testRuleGetters() + { + $oDoc = $this->parsedStructureForFile('values'); + $aBlocks = $oDoc->getAllDeclarationBlocks(); + $oHeaderBlock = $aBlocks[0]; + $oBodyBlock = $aBlocks[1]; + $aHeaderRules = $oHeaderBlock->getRules('background-'); + $this->assertCount(2, $aHeaderRules); + $this->assertSame('background-color', $aHeaderRules[0]->getRule()); + $this->assertSame('background-color', $aHeaderRules[1]->getRule()); + $aHeaderRules = $oHeaderBlock->getRulesAssoc('background-'); + $this->assertCount(1, $aHeaderRules); + $this->assertTrue($aHeaderRules['background-color']->getValue() instanceof \Sabberworm\CSS\Value\Color); + $this->assertSame('rgba', $aHeaderRules['background-color']->getValue()->getColorDescription()); + $oHeaderBlock->removeRule($aHeaderRules['background-color']); + $aHeaderRules = $oHeaderBlock->getRules('background-'); + $this->assertCount(1, $aHeaderRules); + $this->assertSame('green', $aHeaderRules[0]->getValue()); + } + + function testSlashedValues() + { + $oDoc = $this->parsedStructureForFile('slashed'); + $this->assertSame('.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}', $oDoc->render()); + foreach ($oDoc->getAllValues(null) as $mValue) { + if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) { + $mValue->setSize($mValue->getSize() * 3); + } + } + foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oRule = $oBlock->getRules('font'); + $oRule = $oRule[0]; + $oSpaceList = $oRule->getValue(); + $this->assertEquals(' ', $oSpaceList->getListSeparator()); + $oSlashList = $oSpaceList->getListComponents(); + $oCommaList = $oSlashList[1]; + $oSlashList = $oSlashList[0]; + $this->assertEquals(',', $oCommaList->getListSeparator()); + $this->assertEquals('/', $oSlashList->getListSeparator()); + $oRule = $oBlock->getRules('border-radius'); + $oRule = $oRule[0]; + $oSlashList = $oRule->getValue(); + $this->assertEquals('/', $oSlashList->getListSeparator()); + $oSpaceList1 = $oSlashList->getListComponents(); + $oSpaceList2 = $oSpaceList1[1]; + $oSpaceList1 = $oSpaceList1[0]; + $this->assertEquals(' ', $oSpaceList1->getListSeparator()); + $this->assertEquals(' ', $oSpaceList2->getListSeparator()); + } + $this->assertSame('.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}', $oDoc->render()); + } + + function testFunctionSyntax() + { + $oDoc = $this->parsedStructureForFile('functions'); + $sExpected = 'div.main {background-image: linear-gradient(#000,#fff);} .collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;margin-right: .2em;-moz-transition-property: -moz-transform;-moz-transition-duration: .2s;-moz-transform-origin: center 60%;} .collapser.expanded::before, .collapser.expanded::-moz-before, .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);} .collapser + * {height: 0;overflow: hidden;-moz-transition-property: height;-moz-transition-duration: .3s;} .collapser.expanded + * {height: auto;}'; - $this->assertSame($sExpected, $oDoc->render()); - - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize()) { - $mValue->setSize($mValue->getSize() * 3); - } - } - $sExpected = str_replace(array('1.2em', '.2em', '60%'), array('3.6em', '.6em', '180%'), $sExpected); - $this->assertSame($sExpected, $oDoc->render()); - - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) { - $mValue->setSize($mValue->getSize() * 2); - } - } - $sExpected = str_replace(array('.2s', '.3s', '90deg'), array('.4s', '.6s', '180deg'), $sExpected); - $this->assertSame($sExpected, $oDoc->render()); - } - - function testExpandShorthands() { - $oDoc = $this->parsedStructureForFile('expand-shorthands'); - $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid #f0f;background: #ccc url("/images/foo.png") no-repeat left top;margin: 1em !important;padding: 2px 6px 3px;}'; - $this->assertSame($sExpected, $oDoc->render()); - $oDoc->expandShorthands(); - $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;padding-left: 6px;border-top-color: #f0f;border-right-color: #f0f;border-bottom-color: #f0f;border-left-color: #f0f;border-top-style: solid;border-right-style: solid;border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;font-family: "Trebuchet MS",Georgia,serif;background-color: #ccc;background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;background-position: left top;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCreateShorthands() { - $oDoc = $this->parsedStructureForFile('create-shorthands'); - $sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;border-width: 2px;border-color: #999;border-style: dotted;background-color: #fff;background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;margin-bottom: 4px;margin-left: 5px;}'; - $this->assertSame($sExpected, $oDoc->render()); - $oDoc->createShorthands(); - $sExpected = 'body {background: #fff url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;border: 2px dotted #999;font: bold 2em Helvetica,Arial,sans-serif;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testNamespaces() { - $oDoc = $this->parsedStructureForFile('namespaces'); - $sExpected = '@namespace toto "http://toto.example.org"; + $this->assertSame($sExpected, $oDoc->render()); + + foreach ($oDoc->getAllValues(null, true) as $mValue) { + if ($mValue instanceof Size && $mValue->isSize()) { + $mValue->setSize($mValue->getSize() * 3); + } + } + $sExpected = str_replace(array('1.2em', '.2em', '60%'), array('3.6em', '.6em', '180%'), $sExpected); + $this->assertSame($sExpected, $oDoc->render()); + + foreach ($oDoc->getAllValues(null, true) as $mValue) { + if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) { + $mValue->setSize($mValue->getSize() * 2); + } + } + $sExpected = str_replace(array('.2s', '.3s', '90deg'), array('.4s', '.6s', '180deg'), $sExpected); + $this->assertSame($sExpected, $oDoc->render()); + } + + function testExpandShorthands() + { + $oDoc = $this->parsedStructureForFile('expand-shorthands'); + $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid #f0f;background: #ccc url("/images/foo.png") no-repeat left top;margin: 1em !important;padding: 2px 6px 3px;}'; + $this->assertSame($sExpected, $oDoc->render()); + $oDoc->expandShorthands(); + $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;padding-left: 6px;border-top-color: #f0f;border-right-color: #f0f;border-bottom-color: #f0f;border-left-color: #f0f;border-top-style: solid;border-right-style: solid;border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;font-family: "Trebuchet MS",Georgia,serif;background-color: #ccc;background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;background-position: left top;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testCreateShorthands() + { + $oDoc = $this->parsedStructureForFile('create-shorthands'); + $sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;border-width: 2px;border-color: #999;border-style: dotted;background-color: #fff;background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;margin-bottom: 4px;margin-left: 5px;}'; + $this->assertSame($sExpected, $oDoc->render()); + $oDoc->createShorthands(); + $sExpected = 'body {background: #fff url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;border: 2px dotted #999;font: bold 2em Helvetica,Arial,sans-serif;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testNamespaces() + { + $oDoc = $this->parsedStructureForFile('namespaces'); + $sExpected = '@namespace toto "http://toto.example.org"; @namespace "http://example.com/foo"; @namespace foo url("http://www.example.com/"); @namespace foo url("http://www.example.com/"); foo|test {gaga: 1;} |test {gaga: 2;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testInnerColors() { - $oDoc = $this->parsedStructureForFile('inner-color'); - $sExpected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testPrefixedGradient() { - $oDoc = $this->parsedStructureForFile('webkit'); - $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testListValueRemoval() { - $oDoc = $this->parsedStructureForFile('atrules'); - foreach ($oDoc->getContents() as $oItem) { - if ($oItem instanceof AtRule) { - $oDoc->remove($oItem); - continue; - } - } - $this->assertSame('html, body {font-size: -.6em;}', $oDoc->render()); - - $oDoc = $this->parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, false); - break; - } - $this->assertSame('html {some-other: -test(val1);} + $this->assertSame($sExpected, $oDoc->render()); + } + + function testInnerColors() + { + $oDoc = $this->parsedStructureForFile('inner-color'); + $sExpected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testPrefixedGradient() + { + $oDoc = $this->parsedStructureForFile('webkit'); + $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testListValueRemoval() + { + $oDoc = $this->parsedStructureForFile('atrules'); + foreach ($oDoc->getContents() as $oItem) { + if ($oItem instanceof AtRule) { + $oDoc->remove($oItem); + continue; + } + } + $this->assertSame('html, body {font-size: -.6em;}', $oDoc->render()); + + $oDoc = $this->parsedStructureForFile('nested'); + foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, false); + break; + } + $this->assertSame('html {some-other: -test(val1);} @media screen {html {some: -test(val2);}} #unrelated {other: yes;}', $oDoc->render()); - $oDoc = $this->parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, true); - break; - } - $this->assertSame('@media screen {html {some: -test(val2);}} + $oDoc = $this->parsedStructureForFile('nested'); + foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, true); + break; + } + $this->assertSame('@media screen {html {some: -test(val2);}} #unrelated {other: yes;}', $oDoc->render()); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\OutputException - */ - function testSelectorRemoval() { - $oDoc = $this->parsedStructureForFile('1readme'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oBlock1 = $aBlocks[0]; - $this->assertTrue($oBlock1->removeSelector('html')); - $sExpected = '@charset "utf-8"; + } + + /** + * @expectedException Sabberworm\CSS\Parsing\OutputException + */ + function testSelectorRemoval() + { + $oDoc = $this->parsedStructureForFile('1readme'); + $aBlocks = $oDoc->getAllDeclarationBlocks(); + $oBlock1 = $aBlocks[0]; + $this->assertTrue($oBlock1->removeSelector('html')); + $sExpected = '@charset "utf-8"; @font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} body {font-size: 1.6em;}'; - $this->assertSame($sExpected, $oDoc->render()); - $this->assertFalse($oBlock1->removeSelector('html')); - $this->assertTrue($oBlock1->removeSelector('body')); - // This tries to output a declaration block without a selector and throws. - $oDoc->render(); - } - - function testComments() { - $oDoc = $this->parsedStructureForFile('comments'); - $sExpected = '@import url("some/url.css") screen; + $this->assertSame($sExpected, $oDoc->render()); + $this->assertFalse($oBlock1->removeSelector('html')); + $this->assertTrue($oBlock1->removeSelector('body')); + // This tries to output a declaration block without a selector and throws. + $oDoc->render(); + } + + function testComments() + { + $oDoc = $this->parsedStructureForFile('comments'); + $sExpected = '@import url("some/url.css") screen; .foo, #bar {background-color: #000;} @media screen {#foo.bar {position: absolute;}}'; - $this->assertSame($sExpected, $oDoc->render()); - } + $this->assertSame($sExpected, $oDoc->render()); + } - function testUrlInFile() { - $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;} + function testUrlInFile() + { + $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;} body {background-url: url("http://somesite.com/images/someimage.gif");}'; - $this->assertSame($sExpected, $oDoc->render()); - } + $this->assertSame($sExpected, $oDoc->render()); + } - function testHexAlphaInFile() { - $oDoc = $this->parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {background: rgba(17,34,51,.27);} + function testHexAlphaInFile() + { + $oDoc = $this->parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'div {background: rgba(17,34,51,.27);} div {background: rgba(17,34,51,.27);}'; - $this->assertSame($sExpected, $oDoc->render()); - } + $this->assertSame($sExpected, $oDoc->render()); + } - function testCalcInFile() { - $oDoc = $this->parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {width: calc(100% / 4);} + function testCalcInFile() + { + $oDoc = $this->parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'div {width: calc(100% / 4);} div {margin-top: calc(-120% - 4px);} div {height: -webkit-calc(9 / 16 * 100%) !important;width: -moz-calc(( 50px - 50% ) * 2);} div {width: calc(50% - ( ( 4% ) * .5 ));}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCalcNestedInFile() { - $oDoc = $this->parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true)); - $sExpected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testGridLineNameInFile() { - $oDoc = $this->parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true)); - $sExpected = "div {grid-template-columns: [linename] 100px;}\nspan {grid-template-columns: [linename1 linename2] 100px;}"; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testEmptyGridLineNameLenientInFile() { - $oDoc = $this->parsedStructureForFile('empty-grid-linename'); - $sExpected = '.test {grid-template-columns: [] 100px;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testInvalidGridLineNameInFile() { - $oDoc = $this->parsedStructureForFile('invalid-grid-linename', Settings::create()->withMultibyteSupport(true)); - $sExpected = "div {}"; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testUnmatchedBracesInFile() { - $oDoc = $this->parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testInvalidSelectorsInFile() { - $oDoc = $this->parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@keyframes mymove {from {top: 0px;}} + $this->assertSame($sExpected, $oDoc->render()); + } + + function testCalcNestedInFile() + { + $oDoc = $this->parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true)); + $sExpected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testGridLineNameInFile() + { + $oDoc = $this->parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true)); + $sExpected = "div {grid-template-columns: [linename] 100px;}\nspan {grid-template-columns: [linename1 linename2] 100px;}"; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testEmptyGridLineNameLenientInFile() + { + $oDoc = $this->parsedStructureForFile('empty-grid-linename'); + $sExpected = '.test {grid-template-columns: [] 100px;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testInvalidGridLineNameInFile() + { + $oDoc = $this->parsedStructureForFile('invalid-grid-linename', Settings::create()->withMultibyteSupport(true)); + $sExpected = "div {}"; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testUnmatchedBracesInFile() + { + $oDoc = $this->parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testInvalidSelectorsInFile() + { + $oDoc = $this->parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@keyframes mymove {from {top: 0px;}} #test {color: white;background: green;} #test {display: block;background: white;color: black;}'; - $this->assertSame($sExpected, $oDoc->render()); + $this->assertSame($sExpected, $oDoc->render()); - $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} .super-menu > li:first-of-type {border-left-width: 0;} .super-menu > li:last-of-type {border-right-width: 0;} html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} body {background-color: red;}'; - $this->assertSame($sExpected, $oDoc->render()); - } + $this->assertSame($sExpected, $oDoc->render()); + } - function testSelectorEscapesInFile() { - $oDoc = $this->parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); - $sExpected = '#\# {color: red;} + function testSelectorEscapesInFile() + { + $oDoc = $this->parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); + $sExpected = '#\# {color: red;} .col-sm-1\/5 {width: 20%;}'; - $this->assertSame($sExpected, $oDoc->render()); + $this->assertSame($sExpected, $oDoc->render()); - $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} .super-menu > li:first-of-type {border-left-width: 0;} .super-menu > li:last-of-type {border-right-width: 0;} html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} body {background-color: red;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testIdentifierEscapesInFile() { - $oDoc = $this->parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {font: 14px Font Awesome\ 5 Pro;font: 14px Font Awesome\} 5 Pro;font: 14px Font Awesome\; 5 Pro;f\;ont: 14px Font Awesome\; 5 Pro;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testSelectorIgnoresInFile() { - $oDoc = $this->parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); - $sExpected = '.some[selectors-may=\'contain-a-{\'] {} + $this->assertSame($sExpected, $oDoc->render()); + } + + function testIdentifierEscapesInFile() + { + $oDoc = $this->parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'div {font: 14px Font Awesome\ 5 Pro;font: 14px Font Awesome\} 5 Pro;font: 14px Font Awesome\; 5 Pro;f\;ont: 14px Font Awesome\; 5 Pro;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testSelectorIgnoresInFile() + { + $oDoc = $this->parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); + $sExpected = '.some[selectors-may=\'contain-a-{\'] {} .this-selector .valid {width: 100px;} @media only screen and (min-width: 200px) {.test {prop: val;}}'; - $this->assertSame($sExpected, $oDoc->render()); - } + $this->assertSame($sExpected, $oDoc->render()); + } - function testKeyframeSelectors() { - $oDoc = $this->parsedStructureForFile('keyframe-selector-validation', Settings::create()->withMultibyteSupport(true)); + function testKeyframeSelectors() + { + $oDoc = $this->parsedStructureForFile('keyframe-selector-validation', Settings::create()->withMultibyteSupport(true)); $sExpected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);} 50% {-webkit-transform: scale(1.2,1.2);} 100% {-webkit-transform: scale(1,1);}}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testLineNameFailure() { - $this->parsedStructureForFile('-empty-grid-linename', Settings::create()->withLenientParsing(false)); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testCalcFailure() { - $this->parsedStructureForFile('-calc-no-space-around-minus', Settings::create()->withLenientParsing(false)); - } - - function testUrlInFileMbOff() { - $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); - $sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;} + $this->assertSame($sExpected, $oDoc->render()); + } + + /** + * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + function testLineNameFailure() + { + $this->parsedStructureForFile('-empty-grid-linename', Settings::create()->withLenientParsing(false)); + } + + /** + * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + function testCalcFailure() + { + $this->parsedStructureForFile('-calc-no-space-around-minus', Settings::create()->withLenientParsing(false)); + } + + function testUrlInFileMbOff() + { + $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); + $sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;} body {background-url: url("http://somesite.com/images/someimage.gif");}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testEmptyFile() { - $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); - $sExpected = ''; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testEmptyFileMbOff() { - $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); - $sExpected = ''; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCharsetLenient1() { - $oDoc = $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); - $sExpected = '#id {prop: var(--val);}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCharsetLenient2() { - $oDoc = $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); - $sExpected = '@media print {}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testTrailingWhitespace() { - $oDoc = $this->parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); - $sExpected = 'div {width: 200px;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testCharsetFailure1() { - $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(false)); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testCharsetFailure2() { - $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(false)); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\SourceException - */ - function testUnopenedClosingBracketFailure() { - $this->parsedStructureForFile('-unopened-close-brackets', Settings::create()->withLenientParsing(false)); - } - - /** - * Ensure that a missing property value raises an exception. - * - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - * @covers \Sabberworm\CSS\Value\Value::parseValue() - */ - function testMissingPropertyValueStrict() { - $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(false)); - } - - /** - * Ensure that a missing property value is ignored when in lenient parsing mode. - * - * @covers \Sabberworm\CSS\Value\Value::parseValue() - */ - function testMissingPropertyValueLenient() { - $parsed = $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true)); - $rulesets = $parsed->getAllRuleSets(); - $this->assertCount( 1, $rulesets ); - $block = $rulesets[0]; - $this->assertTrue( $block instanceof DeclarationBlock ); - $this->assertEquals( array( 'div' ), $block->getSelectors() ); - $rules = $block->getRules(); - $this->assertCount( 1, $rules ); - $rule = $rules[0]; - $this->assertEquals( 'display', $rule->getRule() ); - $this->assertEquals( 'inline-block', $rule->getValue() ); - } - - /** - * Parse structure for file. - * - * @param string $sFileName Filename. - * @param null|obJeCt $oSettings Settings. - * - * @return CSSList\Document Parsed document. - */ - function parsedStructureForFile($sFileName, $oSettings = null) { - $sFile = __DIR__ . '/../../files' . DIRECTORY_SEPARATOR . "$sFileName.css"; - $oParser = new Parser(file_get_contents($sFile), $oSettings); - return $oParser->parse(); - } - - /** - * @depends testFiles - */ - function testLineNumbersParsing() { - $oDoc = $this->parsedStructureForFile('line-numbers'); - // array key is the expected line number - $aExpected = array( - 1 => array('Sabberworm\CSS\Property\Charset'), - 3 => array('Sabberworm\CSS\Property\CSSNamespace'), - 5 => array('Sabberworm\CSS\RuleSet\AtRuleSet'), - 11 => array('Sabberworm\CSS\RuleSet\DeclarationBlock'), - // Line Numbers of the inner declaration blocks - 17 => array('Sabberworm\CSS\CSSList\KeyFrame', 18, 20), - 23 => array('Sabberworm\CSS\Property\Import'), - 25 => array('Sabberworm\CSS\RuleSet\DeclarationBlock') - ); - - $aActual = array(); - foreach ($oDoc->getContents() as $oContent) { - $aActual[$oContent->getLineNo()] = array(get_class($oContent)); - if ($oContent instanceof KeyFrame) { - foreach ($oContent->getContents() as $block) { - $aActual[$oContent->getLineNo()][] = $block->getLineNo(); - } - } - } - - $aUrlExpected = array(7, 26); // expected line numbers - $aUrlActual = array(); - foreach ($oDoc->getAllValues() as $oValue) { - if ($oValue instanceof URL) { - $aUrlActual[] = $oValue->getLineNo(); - } - } - - // Checking for the multiline color rule lines 27-31 - $aExpectedColorLines = array(28, 29, 30); - $aDeclBlocks = $oDoc->getAllDeclarationBlocks(); - // Choose the 2nd one - $oDeclBlock = $aDeclBlocks[1]; - $aRules = $oDeclBlock->getRules(); - // Choose the 2nd one - $oColor = $aRules[1]->getValue(); - $this->assertEquals(27, $aRules[1]->getLineNo()); - - foreach ($oColor->getColor() as $oSize) { - $aActualColorLines[] = $oSize->getLineNo(); - } - - $this->assertEquals($aExpectedColorLines, $aActualColorLines); - $this->assertEquals($aUrlExpected, $aUrlActual); - $this->assertEquals($aExpected, $aActual); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - * Credit: This test by @sabberworm (from https://github.com/sabberworm/PHP-CSS-Parser/pull/105#issuecomment-229643910 ) - */ - function testUnexpectedTokenExceptionLineNo() { - $oParser = new Parser("\ntest: 1;", Settings::create()->beStrict()); - try { - $oParser->parse(); - } catch (UnexpectedTokenException $e) { - $this->assertSame(2, $e->getLineNo()); - throw $e; - } - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testIeHacksStrictParsing() { - // We can't strictly parse IE hacks. - $this->parsedStructureForFile('ie-hacks', Settings::create()->beStrict()); - } - - function testIeHacksParsing() { - $oDoc = $this->parsedStructureForFile('ie-hacks', Settings::create()->withLenientParsing(true)); - $sExpected = 'p {padding-right: .75rem \9;background-image: none \9;color: red \9\0;background-color: red \9\0;background-color: red \9\0 !important;content: "red \0";content: "red઼";}'; - $this->assertEquals($sExpected, $oDoc->render()); - } - - /** - * @depends testFiles - */ - function testCommentExtracting() { - $oDoc = $this->parsedStructureForFile('comments'); - $aNodes = $oDoc->getContents(); - - // Import property. - $importComments = $aNodes[0]->getComments(); - $this->assertCount(1, $importComments); - $this->assertEquals("*\n * Comments Hell.\n ", $importComments[0]->getComment()); - - // Declaration block. - $fooBarBlock = $aNodes[1]; - $fooBarBlockComments = $fooBarBlock->getComments(); - // TODO Support comments in selectors. - // $this->assertCount(2, $fooBarBlockComments); - // $this->assertEquals("* Number 4 *", $fooBarBlockComments[0]->getComment()); - // $this->assertEquals("* Number 5 *", $fooBarBlockComments[1]->getComment()); - - // Declaration rules. - $fooBarRules = $fooBarBlock->getRules(); - $fooBarRule = $fooBarRules[0]; - $fooBarRuleComments = $fooBarRule->getComments(); - $this->assertCount(1, $fooBarRuleComments); - $this->assertEquals(" Number 6 ", $fooBarRuleComments[0]->getComment()); - - // Media property. - $mediaComments = $aNodes[2]->getComments(); - $this->assertCount(0, $mediaComments); - - // Media children. - $mediaRules = $aNodes[2]->getContents(); - $fooBarComments = $mediaRules[0]->getComments(); - $this->assertCount(1, $fooBarComments); - $this->assertEquals("* Number 10 *", $fooBarComments[0]->getComment()); - - // Media -> declaration -> rule. - $fooBarRules = $mediaRules[0]->getRules(); - $fooBarChildComments = $fooBarRules[0]->getComments(); - $this->assertCount(1, $fooBarChildComments); - $this->assertEquals("* Number 10b *", $fooBarChildComments[0]->getComment()); - } - - function testFlatCommentExtracting() { - $parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); - $divRules = $contents[0]->getRules(); - $comments = $divRules[0]->getComments(); - $this->assertCount(1, $comments); - $this->assertEquals("Find Me!", $comments[0]->getComment()); - } - - function testTopLevelCommentExtracting() { - $parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); - $comments = $contents[0]->getComments(); - $this->assertCount(1, $comments); - $this->assertEquals("Find Me!", $comments[0]->getComment()); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testMicrosoftFilterStrictParsing() { - $oDoc = $this->parsedStructureForFile('ms-filter', Settings::create()->beStrict()); - } - - function testMicrosoftFilterParsing() { - $oDoc = $this->parsedStructureForFile('ms-filter'); - $sExpected = ".test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#80000000\",endColorstr=\"#00000000\",GradientType=1);}"; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testLargeSizeValuesInFile() { - $oDoc = $this->parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false)); - $sExpected = '.overlay {z-index: 10000000000000000000000;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testLonelyImport() { - $oDoc = $this->parsedStructureForFile('lonely-import'); - $sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);"; - $this->assertSame($sExpected, $oDoc->render()); - } + $this->assertSame($sExpected, $oDoc->render()); + } + + function testEmptyFile() + { + $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); + $sExpected = ''; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testEmptyFileMbOff() + { + $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); + $sExpected = ''; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testCharsetLenient1() + { + $oDoc = $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); + $sExpected = '#id {prop: var(--val);}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testCharsetLenient2() + { + $oDoc = $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); + $sExpected = '@media print {}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testTrailingWhitespace() + { + $oDoc = $this->parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); + $sExpected = 'div {width: 200px;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + function testCharsetFailure1() + { + $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(false)); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + function testCharsetFailure2() + { + $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(false)); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\SourceException + */ + function testUnopenedClosingBracketFailure() + { + $this->parsedStructureForFile('-unopened-close-brackets', Settings::create()->withLenientParsing(false)); + } + + /** + * Ensure that a missing property value raises an exception. + * + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * @covers \Sabberworm\CSS\Value\Value::parseValue() + */ + function testMissingPropertyValueStrict() + { + $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(false)); + } + + /** + * Ensure that a missing property value is ignored when in lenient parsing mode. + * + * @covers \Sabberworm\CSS\Value\Value::parseValue() + */ + function testMissingPropertyValueLenient() + { + $parsed = $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true)); + $rulesets = $parsed->getAllRuleSets(); + $this->assertCount(1, $rulesets); + $block = $rulesets[0]; + $this->assertTrue($block instanceof DeclarationBlock); + $this->assertEquals(array( 'div' ), $block->getSelectors()); + $rules = $block->getRules(); + $this->assertCount(1, $rules); + $rule = $rules[0]; + $this->assertEquals('display', $rule->getRule()); + $this->assertEquals('inline-block', $rule->getValue()); + } + + /** + * Parse structure for file. + * + * @param string $sFileName Filename. + * @param null|obJeCt $oSettings Settings. + * + * @return CSSList\Document Parsed document. + */ + function parsedStructureForFile($sFileName, $oSettings = null) + { + $sFile = __DIR__ . '/../../files' . DIRECTORY_SEPARATOR . "$sFileName.css"; + $oParser = new Parser(file_get_contents($sFile), $oSettings); + return $oParser->parse(); + } + + /** + * @depends testFiles + */ + function testLineNumbersParsing() + { + $oDoc = $this->parsedStructureForFile('line-numbers'); + // array key is the expected line number + $aExpected = array( + 1 => array('Sabberworm\CSS\Property\Charset'), + 3 => array('Sabberworm\CSS\Property\CSSNamespace'), + 5 => array('Sabberworm\CSS\RuleSet\AtRuleSet'), + 11 => array('Sabberworm\CSS\RuleSet\DeclarationBlock'), + // Line Numbers of the inner declaration blocks + 17 => array('Sabberworm\CSS\CSSList\KeyFrame', 18, 20), + 23 => array('Sabberworm\CSS\Property\Import'), + 25 => array('Sabberworm\CSS\RuleSet\DeclarationBlock') + ); + + $aActual = array(); + foreach ($oDoc->getContents() as $oContent) { + $aActual[$oContent->getLineNo()] = array(get_class($oContent)); + if ($oContent instanceof KeyFrame) { + foreach ($oContent->getContents() as $block) { + $aActual[$oContent->getLineNo()][] = $block->getLineNo(); + } + } + } + + $aUrlExpected = array(7, 26); // expected line numbers + $aUrlActual = array(); + foreach ($oDoc->getAllValues() as $oValue) { + if ($oValue instanceof URL) { + $aUrlActual[] = $oValue->getLineNo(); + } + } + + // Checking for the multiline color rule lines 27-31 + $aExpectedColorLines = array(28, 29, 30); + $aDeclBlocks = $oDoc->getAllDeclarationBlocks(); + // Choose the 2nd one + $oDeclBlock = $aDeclBlocks[1]; + $aRules = $oDeclBlock->getRules(); + // Choose the 2nd one + $oColor = $aRules[1]->getValue(); + $this->assertEquals(27, $aRules[1]->getLineNo()); + + foreach ($oColor->getColor() as $oSize) { + $aActualColorLines[] = $oSize->getLineNo(); + } + + $this->assertEquals($aExpectedColorLines, $aActualColorLines); + $this->assertEquals($aUrlExpected, $aUrlActual); + $this->assertEquals($aExpected, $aActual); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * Credit: This test by @sabberworm (from https://github.com/sabberworm/PHP-CSS-Parser/pull/105#issuecomment-229643910 ) + */ + function testUnexpectedTokenExceptionLineNo() + { + $oParser = new Parser("\ntest: 1;", Settings::create()->beStrict()); + try { + $oParser->parse(); + } catch (UnexpectedTokenException $e) { + $this->assertSame(2, $e->getLineNo()); + throw $e; + } + } + + /** + * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + function testIeHacksStrictParsing() + { + // We can't strictly parse IE hacks. + $this->parsedStructureForFile('ie-hacks', Settings::create()->beStrict()); + } + + function testIeHacksParsing() + { + $oDoc = $this->parsedStructureForFile('ie-hacks', Settings::create()->withLenientParsing(true)); + $sExpected = 'p {padding-right: .75rem \9;background-image: none \9;color: red \9\0;background-color: red \9\0;background-color: red \9\0 !important;content: "red \0";content: "red઼";}'; + $this->assertEquals($sExpected, $oDoc->render()); + } + + /** + * @depends testFiles + */ + function testCommentExtracting() + { + $oDoc = $this->parsedStructureForFile('comments'); + $aNodes = $oDoc->getContents(); + + // Import property. + $importComments = $aNodes[0]->getComments(); + $this->assertCount(1, $importComments); + $this->assertEquals("*\n * Comments Hell.\n ", $importComments[0]->getComment()); + + // Declaration block. + $fooBarBlock = $aNodes[1]; + $fooBarBlockComments = $fooBarBlock->getComments(); + // TODO Support comments in selectors. + // $this->assertCount(2, $fooBarBlockComments); + // $this->assertEquals("* Number 4 *", $fooBarBlockComments[0]->getComment()); + // $this->assertEquals("* Number 5 *", $fooBarBlockComments[1]->getComment()); + + // Declaration rules. + $fooBarRules = $fooBarBlock->getRules(); + $fooBarRule = $fooBarRules[0]; + $fooBarRuleComments = $fooBarRule->getComments(); + $this->assertCount(1, $fooBarRuleComments); + $this->assertEquals(" Number 6 ", $fooBarRuleComments[0]->getComment()); + + // Media property. + $mediaComments = $aNodes[2]->getComments(); + $this->assertCount(0, $mediaComments); + + // Media children. + $mediaRules = $aNodes[2]->getContents(); + $fooBarComments = $mediaRules[0]->getComments(); + $this->assertCount(1, $fooBarComments); + $this->assertEquals("* Number 10 *", $fooBarComments[0]->getComment()); + + // Media -> declaration -> rule. + $fooBarRules = $mediaRules[0]->getRules(); + $fooBarChildComments = $fooBarRules[0]->getComments(); + $this->assertCount(1, $fooBarChildComments); + $this->assertEquals("* Number 10b *", $fooBarChildComments[0]->getComment()); + } + + function testFlatCommentExtracting() + { + $parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}'); + $doc = $parser->parse(); + $contents = $doc->getContents(); + $divRules = $contents[0]->getRules(); + $comments = $divRules[0]->getComments(); + $this->assertCount(1, $comments); + $this->assertEquals("Find Me!", $comments[0]->getComment()); + } + + function testTopLevelCommentExtracting() + { + $parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}'); + $doc = $parser->parse(); + $contents = $doc->getContents(); + $comments = $contents[0]->getComments(); + $this->assertCount(1, $comments); + $this->assertEquals("Find Me!", $comments[0]->getComment()); + } + + /** + * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + function testMicrosoftFilterStrictParsing() + { + $oDoc = $this->parsedStructureForFile('ms-filter', Settings::create()->beStrict()); + } + + function testMicrosoftFilterParsing() + { + $oDoc = $this->parsedStructureForFile('ms-filter'); + $sExpected = ".test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#80000000\",endColorstr=\"#00000000\",GradientType=1);}"; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testLargeSizeValuesInFile() + { + $oDoc = $this->parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false)); + $sExpected = '.overlay {z-index: 10000000000000000000000;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + + function testLonelyImport() + { + $oDoc = $this->parsedStructureForFile('lonely-import'); + $sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);"; + $this->assertSame($sExpected, $oDoc->render()); + } } diff --git a/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php b/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php index b74fb8a2..c088f17c 100644 --- a/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php +++ b/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php @@ -6,285 +6,303 @@ use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\Value\Size; -class DeclarationBlockTest extends \PHPUnit\Framework\TestCase { - - /** - * @dataProvider expandBorderShorthandProvider - * */ - public function testExpandBorderShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBorderShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandBorderShorthandProvider() { - return array( - array('body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'), - array('body{ border: none }', 'body {border-style: none;}'), - array('body{ border: 2px }', 'body {border-width: 2px;}'), - array('body{ border: #f00 }', 'body {border-color: #f00;}'), - array('body{ border: 1em solid }', 'body {border-width: 1em;border-style: solid;}'), - array('body{ margin: 1em; }', 'body {margin: 1em;}') - ); - } - - /** - * @dataProvider expandFontShorthandProvider - * */ - public function testExpandFontShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandFontShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandFontShorthandProvider() { - return array( - array( - 'body{ margin: 1em; }', - 'body {margin: 1em;}' - ), - array( - 'body {font: 12px serif;}', - 'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' - ), - array( - 'body {font: italic 12px serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' - ), - array( - 'body {font: italic bold 12px serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: normal;font-family: serif;}' - ), - array( - 'body {font: italic bold 12px/1.6 serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}' - ), - array( - 'body {font: italic small-caps bold 12px/1.6 serif;}', - 'body {font-style: italic;font-variant: small-caps;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}' - ), - ); - } - - /** - * @dataProvider expandBackgroundShorthandProvider - * */ - public function testExpandBackgroundShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBackgroundShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandBackgroundShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {background: #f00;}', 'body {background-color: #f00;background-image: none;background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'), - array('body {background: #f00 url("foobar.png");}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'), - array('body {background: #f00 url("foobar.png") no-repeat;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: 0% 0%;}'), - array('body {background: #f00 url("foobar.png") no-repeat center;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: center center;}'), - array('body {background: #f00 url("foobar.png") no-repeat top left;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: top left;}'), - ); - } - - /** - * @dataProvider expandDimensionsShorthandProvider - * */ - public function testExpandDimensionsShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandDimensionsShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandDimensionsShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), - array('body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'), - array('body {margin: 1em 2em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}'), - array('body {margin: 1em 2em 3em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}'), - ); - } - - /** - * @dataProvider createBorderShorthandProvider - * */ - public function testCreateBorderShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBorderShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createBorderShorthandProvider() { - return array( - array('body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'), - array('body {border-style: none;}', 'body {border: none;}'), - array('body {border-width: 1em;border-style: solid;}', 'body {border: 1em solid;}'), - array('body {margin: 1em;}', 'body {margin: 1em;}') - ); - } - - /** - * @dataProvider createFontShorthandProvider - * */ - public function testCreateFontShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createFontShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createFontShorthandProvider() { - return array( - array('body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'), - array('body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'), - array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}', 'body {font: italic bold 12px serif;}'), - array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6;}', 'body {font: italic bold 12px/1.6 serif;}'), - array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6; font-variant: small-caps;}', 'body {font: italic small-caps bold 12px/1.6 serif;}'), - array('body {margin: 1em;}', 'body {margin: 1em;}') - ); - } - - /** - * @dataProvider createDimensionsShorthandProvider - * */ - public function testCreateDimensionsShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createDimensionsShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createDimensionsShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), - array('body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'), - array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}', 'body {margin: 1em 2em;}'), - array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}', 'body {margin: 1em 2em 3em;}'), - ); - } - - /** - * @dataProvider createBackgroundShorthandProvider - * */ - public function testCreateBackgroundShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBackgroundShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createBackgroundShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {background-color: #f00;}', 'body {background: #f00;}'), - array('body {background-color: #f00;background-image: url(foobar.png);}', 'body {background: #f00 url("foobar.png");}'), - array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'), - array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'), - array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;background-position: center;}', 'body {background: #f00 url("foobar.png") no-repeat center;}'), - array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;background-position: top left;}', 'body {background: #f00 url("foobar.png") no-repeat top left;}'), - ); - } - - public function testOverrideRules() { - $sCss = '.wrapper { left: 10px; text-align: left; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $oRule = new Rule('right'); - $oRule->setValue('-10px'); - $aContents = $oDoc->getContents(); - $oWrapper = $aContents[0]; - - $this->assertCount(2, $oWrapper->getRules()); - $aContents[0]->setRules(array($oRule)); - - $aRules = $oWrapper->getRules(); - $this->assertCount(1, $aRules); - $this->assertEquals('right', $aRules[0]->getRule()); - $this->assertEquals('-10px', $aRules[0]->getValue()); - } - - public function testRuleInsertion() { - $sCss = '.wrapper { left: 10px; text-align: left; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $oWrapper = $aContents[0]; - - $oFirst = $oWrapper->getRules('left'); - $this->assertCount(1, $oFirst); - $oFirst = $oFirst[0]; - - $oSecond = $oWrapper->getRules('text-'); - $this->assertCount(1, $oSecond); - $oSecond = $oSecond[0]; - - $oBefore = new Rule('left'); - $oBefore->setValue(new Size(16, 'em')); - - $oMiddle = new Rule('text-align'); - $oMiddle->setValue(new Size(1)); - - $oAfter = new Rule('border-bottom-width'); - $oAfter->setValue(new Size(1, 'px')); - - $oWrapper->addRule($oAfter); - $oWrapper->addRule($oBefore, $oFirst); - $oWrapper->addRule($oMiddle, $oSecond); - - $aRules = $oWrapper->getRules(); - - $this->assertSame($oBefore, $aRules[0]); - $this->assertSame($oFirst, $aRules[1]); - $this->assertSame($oMiddle, $aRules[2]); - $this->assertSame($oSecond, $aRules[3]); - $this->assertSame($oAfter, $aRules[4]); - - $this->assertSame('.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', $oDoc->render()); - } - - public function testOrderOfElementsMatchingOriginalOrderAfterExpandingShorthands() - { - $sCss = '.rule{padding:5px;padding-top: 20px}'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aDocs = $oDoc->getAllDeclarationBlocks(); - - $this->assertCount(1, $aDocs); - - $oDeclaration = array_pop($aDocs); - $oDeclaration->expandShorthands(); - - $this->assertEquals( - array( - 'padding-top' => 'padding-top: 20px;', - 'padding-right' => 'padding-right: 5px;', - 'padding-bottom' => 'padding-bottom: 5px;', - 'padding-left' => 'padding-left: 5px;', - ), - array_map('strval', $oDeclaration->getRulesAssoc()) - ); - } - +class DeclarationBlockTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @dataProvider expandBorderShorthandProvider + * */ + public function testExpandBorderShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandBorderShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function expandBorderShorthandProvider() + { + return array( + array('body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'), + array('body{ border: none }', 'body {border-style: none;}'), + array('body{ border: 2px }', 'body {border-width: 2px;}'), + array('body{ border: #f00 }', 'body {border-color: #f00;}'), + array('body{ border: 1em solid }', 'body {border-width: 1em;border-style: solid;}'), + array('body{ margin: 1em; }', 'body {margin: 1em;}') + ); + } + + /** + * @dataProvider expandFontShorthandProvider + * */ + public function testExpandFontShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandFontShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function expandFontShorthandProvider() + { + return array( + array( + 'body{ margin: 1em; }', + 'body {margin: 1em;}' + ), + array( + 'body {font: 12px serif;}', + 'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' + ), + array( + 'body {font: italic 12px serif;}', + 'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' + ), + array( + 'body {font: italic bold 12px serif;}', + 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: normal;font-family: serif;}' + ), + array( + 'body {font: italic bold 12px/1.6 serif;}', + 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}' + ), + array( + 'body {font: italic small-caps bold 12px/1.6 serif;}', + 'body {font-style: italic;font-variant: small-caps;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}' + ), + ); + } + + /** + * @dataProvider expandBackgroundShorthandProvider + * */ + public function testExpandBackgroundShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandBackgroundShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function expandBackgroundShorthandProvider() + { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {background: #f00;}', 'body {background-color: #f00;background-image: none;background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'), + array('body {background: #f00 url("foobar.png");}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'), + array('body {background: #f00 url("foobar.png") no-repeat;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: 0% 0%;}'), + array('body {background: #f00 url("foobar.png") no-repeat center;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: center center;}'), + array('body {background: #f00 url("foobar.png") no-repeat top left;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: top left;}'), + ); + } + + /** + * @dataProvider expandDimensionsShorthandProvider + * */ + public function testExpandDimensionsShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandDimensionsShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function expandDimensionsShorthandProvider() + { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), + array('body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'), + array('body {margin: 1em 2em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}'), + array('body {margin: 1em 2em 3em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}'), + ); + } + + /** + * @dataProvider createBorderShorthandProvider + * */ + public function testCreateBorderShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createBorderShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createBorderShorthandProvider() + { + return array( + array('body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'), + array('body {border-style: none;}', 'body {border: none;}'), + array('body {border-width: 1em;border-style: solid;}', 'body {border: 1em solid;}'), + array('body {margin: 1em;}', 'body {margin: 1em;}') + ); + } + + /** + * @dataProvider createFontShorthandProvider + * */ + public function testCreateFontShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createFontShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createFontShorthandProvider() + { + return array( + array('body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}', 'body {font: italic bold 12px serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6;}', 'body {font: italic bold 12px/1.6 serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6; font-variant: small-caps;}', 'body {font: italic small-caps bold 12px/1.6 serif;}'), + array('body {margin: 1em;}', 'body {margin: 1em;}') + ); + } + + /** + * @dataProvider createDimensionsShorthandProvider + * */ + public function testCreateDimensionsShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createDimensionsShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createDimensionsShorthandProvider() + { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), + array('body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'), + array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}', 'body {margin: 1em 2em;}'), + array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}', 'body {margin: 1em 2em 3em;}'), + ); + } + + /** + * @dataProvider createBackgroundShorthandProvider + * */ + public function testCreateBackgroundShorthand($sCss, $sExpected) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createBackgroundShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createBackgroundShorthandProvider() + { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {background-color: #f00;}', 'body {background: #f00;}'), + array('body {background-color: #f00;background-image: url(foobar.png);}', 'body {background: #f00 url("foobar.png");}'), + array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'), + array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'), + array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;background-position: center;}', 'body {background: #f00 url("foobar.png") no-repeat center;}'), + array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;background-position: top left;}', 'body {background: #f00 url("foobar.png") no-repeat top left;}'), + ); + } + + public function testOverrideRules() + { + $sCss = '.wrapper { left: 10px; text-align: left; }'; + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + $oRule = new Rule('right'); + $oRule->setValue('-10px'); + $aContents = $oDoc->getContents(); + $oWrapper = $aContents[0]; + + $this->assertCount(2, $oWrapper->getRules()); + $aContents[0]->setRules(array($oRule)); + + $aRules = $oWrapper->getRules(); + $this->assertCount(1, $aRules); + $this->assertEquals('right', $aRules[0]->getRule()); + $this->assertEquals('-10px', $aRules[0]->getValue()); + } + + public function testRuleInsertion() + { + $sCss = '.wrapper { left: 10px; text-align: left; }'; + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + $aContents = $oDoc->getContents(); + $oWrapper = $aContents[0]; + + $oFirst = $oWrapper->getRules('left'); + $this->assertCount(1, $oFirst); + $oFirst = $oFirst[0]; + + $oSecond = $oWrapper->getRules('text-'); + $this->assertCount(1, $oSecond); + $oSecond = $oSecond[0]; + + $oBefore = new Rule('left'); + $oBefore->setValue(new Size(16, 'em')); + + $oMiddle = new Rule('text-align'); + $oMiddle->setValue(new Size(1)); + + $oAfter = new Rule('border-bottom-width'); + $oAfter->setValue(new Size(1, 'px')); + + $oWrapper->addRule($oAfter); + $oWrapper->addRule($oBefore, $oFirst); + $oWrapper->addRule($oMiddle, $oSecond); + + $aRules = $oWrapper->getRules(); + + $this->assertSame($oBefore, $aRules[0]); + $this->assertSame($oFirst, $aRules[1]); + $this->assertSame($oMiddle, $aRules[2]); + $this->assertSame($oSecond, $aRules[3]); + $this->assertSame($oAfter, $aRules[4]); + + $this->assertSame('.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', $oDoc->render()); + } + + public function testOrderOfElementsMatchingOriginalOrderAfterExpandingShorthands() + { + $sCss = '.rule{padding:5px;padding-top: 20px}'; + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + $aDocs = $oDoc->getAllDeclarationBlocks(); + + $this->assertCount(1, $aDocs); + + $oDeclaration = array_pop($aDocs); + $oDeclaration->expandShorthands(); + + $this->assertEquals( + array( + 'padding-top' => 'padding-top: 20px;', + 'padding-right' => 'padding-right: 5px;', + 'padding-bottom' => 'padding-bottom: 5px;', + 'padding-left' => 'padding-left: 5px;', + ), + array_map('strval', $oDeclaration->getRulesAssoc()) + ); + } } diff --git a/tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php b/tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php index 39268963..0b967f3d 100644 --- a/tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php +++ b/tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php @@ -5,72 +5,80 @@ use Sabberworm\CSS\Parser; use Sabberworm\CSS\Settings; -class LenientParsingTest extends \PHPUnit\Framework\TestCase { +class LenientParsingTest extends \PHPUnit\Framework\TestCase +{ - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - public function testFaultToleranceOff() { - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); - } + /** + * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + public function testFaultToleranceOff() + { + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); + $oParser->parse(); + } - public function testFaultToleranceOn() { - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame('.test1 {}'."\n".'.test2 {hello: 2.2;hello: 2000000000000.2;}'."\n".'#test {}'."\n".'#test2 {help: none;}', $oResult->render()); - } + public function testFaultToleranceOn() + { + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oResult = $oParser->parse(); + $this->assertSame('.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" . '#test2 {help: none;}', $oResult->render()); + } - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - public function testEndToken() { - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); - } + /** + * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + public function testEndToken() + { + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css"; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); + $oParser->parse(); + } - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - public function testEndToken2() { - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); - } + /** + * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException + */ + public function testEndToken2() + { + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css"; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); + $oParser->parse(); + } - public function testEndTokenPositive() { - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame("", $oResult->render()); - } + public function testEndTokenPositive() + { + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css"; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oResult = $oParser->parse(); + $this->assertSame("", $oResult->render()); + } - public function testEndToken2Positive() { - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame('#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}', $oResult->render()); - } + public function testEndToken2Positive() + { + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css"; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oResult = $oParser->parse(); + $this->assertSame('#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}', $oResult->render()); + } - public function testLocaleTrap() { - setlocale(LC_ALL, "pt_PT", "no"); - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame('.test1 {}'."\n".'.test2 {hello: 2.2;hello: 2000000000000.2;}'."\n".'#test {}'."\n".'#test2 {help: none;}', $oResult->render()); - } + public function testLocaleTrap() + { + setlocale(LC_ALL, "pt_PT", "no"); + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oResult = $oParser->parse(); + $this->assertSame('.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" . '#test2 {help: none;}', $oResult->render()); + } - public function testCaseInsensitivity() { - $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "case-insensitivity.css"; - $oParser = new Parser(file_get_contents($sFile)); - $oResult = $oParser->parse(); - $this->assertSame('@charset "utf-8"; + public function testCaseInsensitivity() + { + $sFile = __DIR__ . '/../../../files' . DIRECTORY_SEPARATOR . "case-insensitivity.css"; + $oParser = new Parser(file_get_contents($sFile)); + $oResult = $oParser->parse(); + $this->assertSame('@charset "utf-8"; @import url("test.css"); @media screen {} #myid {case: insensitive !important;frequency: 30Hz;font-size: 1em;color: #ff0;color: hsl(40,40%,30%);font-family: Arial;}', $oResult->render()); - } - + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index fe39a2a0..41b1f5ea 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,8 +1,7 @@ parse(); -echo "\n".'#### Input'."\n\n```css\n"; +echo "\n" . '#### Input' . "\n\n```css\n"; print $sSource; -echo "\n```\n\n".'#### Structure (`var_dump()`)'."\n\n```php\n"; +echo "\n```\n\n" . '#### Structure (`var_dump()`)' . "\n\n```php\n"; var_dump($oDoc); -echo "\n```\n\n".'#### Output (`render()`)'."\n\n```css\n"; +echo "\n```\n\n" . '#### Output (`render()`)' . "\n\n```css\n"; print $oDoc->render(); echo "\n```\n";