diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e6ae545c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.build +dist +.tmp +nbproject +review +vendor +.buildpath +.settings +.project diff --git a/CSSParser.php b/CSSParser.php deleted file mode 100644 index 77eb3faf..00000000 --- a/CSSParser.php +++ /dev/null @@ -1,825 +0,0 @@ -sText = $sText; - $this->iCurrentPosition = 0; - $this->setCharset($sDefaultCharset); - } - - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - $this->iLength = mb_strlen($this->sText, $this->sCharset); - } - - public function getCharset() { - return $this->sCharset; - } - - public function parse() { - $oResult = new CSSDocument(); - $this->parseDocument($oResult); - return $oResult; - } - - private function parseDocument(CSSDocument $oDocument) { - $this->consumeWhiteSpace(); - $this->parseList($oDocument, true); - } - - private function parseList(CSSList $oList, $bIsRoot = false) { - while(!$this->isEnd()) { - if($this->comes('@')) { - $oList->append($this->parseAtRule()); - } else if($this->comes('}')) { - $this->consume('}'); - if($bIsRoot) { - throw new Exception("Unopened {"); - } else { - return; - } - } else { - $oList->append($this->parseSelector()); - } - $this->consumeWhiteSpace(); - } - if(!$bIsRoot) { - throw new Exception("Unexpected end of document"); - } - } - - private function parseAtRule() { - $this->consume('@'); - $sIdentifier = $this->parseIdentifier(); - $this->consumeWhiteSpace(); - if($sIdentifier === 'media') { - $oResult = new CSSMediaQuery(); - $oResult->setQuery(trim($this->consumeUntil('{'))); - $this->consume('{'); - $this->consumeWhiteSpace(); - $this->parseList($oResult); - return $oResult; - } else if($sIdentifier === 'import') { - $oLocation = $this->parseURLValue(); - $this->consumeWhiteSpace(); - $sMediaQuery = null; - if(!$this->comes(';')) { - $sMediaQuery = $this->consumeUntil(';'); - } - $this->consume(';'); - return new CSSImport($oLocation, $sMediaQuery); - } else if($sIdentifier === 'charset') { - $sCharset = $this->parseStringValue(); - $this->consumeWhiteSpace(); - $this->consume(';'); - $this->setCharset($sCharset->getString()); - return new CSSCharset($sCharset); - } else { - //Unknown other at rule (font-face or such) - $this->consume('{'); - $this->consumeWhiteSpace(); - $oAtRule = new CSSAtRule($sIdentifier); - $this->parseRuleSet($oAtRule); - return $oAtRule; - } - } - - private function parseIdentifier() { - $sResult = $this->parseCharacter(true); - if($sResult === null) { - throw new Exception("Identifier expected, got {$this->peek(5)}"); - } - $sCharacter; - while(($sCharacter = $this->parseCharacter(true)) !== null) { - $sResult .= $sCharacter; - } - return $sResult; - } - - private function parseStringValue() { - $sBegin = $this->peek(); - $sQuote = null; - if($sBegin === "'") { - $sQuote = "'"; - } else if($sBegin === '"') { - $sQuote = '"'; - } - if($sQuote !== null) { - $this->consume($sQuote); - } - $sResult = ""; - $sContent = null; - if($sQuote === null) { - //Unquoted strings end in whitespace or with braces, brackets, parentheses - while(!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) { - $sResult .= $this->parseCharacter(false); - } - } else { - while(!$this->comes($sQuote)) { - $sContent = $this->parseCharacter(false); - if($sContent === null) { - throw new Exception("Non-well-formed quoted string {$this->peek(3)}"); - } - $sResult .= $sContent; - } - $this->consume($sQuote); - } - return new CSSString($sResult); - } - - private function parseCharacter($bIsForIdentifier) { - if($this->peek() === '\\') { - $this->consume('\\'); - if($this->comes('\n') || $this->comes('\r')) { - return ''; - } - $aMatches; - if(preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { - return $this->consume(1); - } - $sUnicode = $this->consumeExpression('/[0-9a-fA-F]+/u'); - if(mb_strlen($sUnicode, $this->sCharset) < 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); - } - } - } - $sUtf16 = ''; - if((strlen($sUnicode) % 2) === 1) { - $sUnicode = "0$sUnicode"; - } - for($i=0;$isCharset, $sUtf16); - } - if($bIsForIdentifier) { - if(preg_match('/[a-zA-Z0-9]|-|_/u', $this->peek()) === 1) { - return $this->consume(1); - } else if(ord($this->peek()) > 0xa1) { - return $this->consume(1); - } else { - return null; - } - } else { - return $this->consume(1); - } - // Does not reach here - return null; - } - - private function parseSelector() { - $oResult = new CSSSelector(); - $oResult->setSelector($this->consumeUntil('{')); - $this->consume('{'); - $this->consumeWhiteSpace(); - $this->parseRuleSet($oResult); - return $oResult; - } - - private function parseRuleSet($oRuleSet) { - while(!$this->comes('}')) { - $oRuleSet->addRule($this->parseRule()); - $this->consumeWhiteSpace(); - } - $this->consume('}'); - } - - private function parseRule() { - $oRule = new CSSRule($this->parseIdentifier()); - $this->consumeWhiteSpace(); - $this->consume(':'); - $this->consumeWhiteSpace(); - while(!($this->comes('}') || $this->comes(';') || $this->comes('!'))) { - $oRule->addValue($this->parseValue()); - $this->consumeWhiteSpace(); - } - if($this->comes('!')) { - $this->consume('!'); - $this->consumeWhiteSpace(); - $sImportantMarker = $this->consume(strlen('important')); - if(mb_convert_case($sImportantMarker, MB_CASE_LOWER) !== 'important') { - throw new Exception("! was followed by “".$sImportantMarker."”. Expected “important”"); - } - $oRule->setIsImportant(true); - } - if($this->comes(';')) { - $this->consume(';'); - } - return $oRule; - } - - private function parseValue() { - $aResult = array(); - do { - $this->consumeWhiteSpace(); - if(is_numeric($this->peek()) || $this->comes('-') || $this->comes('.')) { - $aResult[] = $this->parseNumericValue(); - } else if($this->comes('#') || $this->comes('rgb') || $this->comes('hsl')) { - $aResult[] = $this->parseColorValue(); - } else if($this->comes('url')){ - $aResult[] = $this->parseURLValue(); - } else if($this->comes("'") || $this->comes('"')){ - $aResult[] = $this->parseStringValue(); - } else { - $aResult[] = $this->parseIdentifier(); - } - $this->consumeWhiteSpace(); - } while($this->comes(',') && is_string($this->consume(','))); - - return $aResult; - } - - private function parseNumericValue() { - $sSize = ''; - if($this->comes('-')) { - $sSize .= $this->consume('-'); - } - while(is_numeric($this->peek()) || $this->comes('.')) { - if($this->comes('.')) { - $sSize .= $this->consume('.'); - } else { - $sSize .= $this->consume(1); - } - } - $fSize = floatval($sSize); - $sUnit = null; - if($this->comes('%')) { - $sUnit = $this->consume('%'); - } else if($this->comes('em')) { - $sUnit = $this->consume('em'); - } else if($this->comes('ex')) { - $sUnit = $this->consume('ex'); - } else if($this->comes('px')) { - $sUnit = $this->consume('px'); - } else if($this->comes('cm')) { - $sUnit = $this->consume('cm'); - } else if($this->comes('pt')) { - $sUnit = $this->consume('pt'); - } else if($this->comes('in')) { - $sUnit = $this->consume('in'); - } else if($this->comes('pc')) { - $sUnit = $this->consume('pc'); - } else if($this->comes('cm')) { - $sUnit = $this->consume('cm'); - } else if($this->comes('mm')) { - $sUnit = $this->consume('mm'); - } - return new CSSSize($fSize, $sUnit); - } - - private function parseColorValue() { - $aColor = array(); - if($this->comes('#')) { - $this->consume('#'); - $sValue = $this->parseIdentifier(); - if(mb_strlen($sValue, $this->sCharset) === 3) { - $sValue = $sValue[0].$sValue[0].$sValue[1].$sValue[1].$sValue[2].$sValue[2]; - } - $aColor = array('r' => new CSSSize(intval($sValue[0].$sValue[1], 16)), 'g' => new CSSSize(intval($sValue[2].$sValue[3], 16)), 'b' => new CSSSize(intval($sValue[4].$sValue[5], 16))); - } else { - $sColorMode = $this->parseIdentifier(); - $this->consumeWhiteSpace(); - $this->consume('('); - $iLength = mb_strlen($sColorMode, $this->sCharset); - for($i=0;$i<$iLength;$i++) { - $this->consumeWhiteSpace(); - $aColor[$sColorMode[$i]] = $this->parseNumericValue(); - $this->consumeWhiteSpace(); - if($i < ($iLength-1)) { - $this->consume(','); - } - } - $this->consume(')'); - } - return new CSSColor($aColor); - } - - private function parseURLValue() { - $bUseUrl = $this->comes('url'); - if($bUseUrl) { - $this->consume('url'); - $this->consumeWhiteSpace(); - $this->consume('('); - } - $this->consumeWhiteSpace(); - $oResult = new CSSURL($this->parseStringValue()); - if($bUseUrl) { - $this->consumeWhiteSpace(); - $this->consume(')'); - } - return $oResult; - } - - private function comes($sString, $iOffset = 0) { - if($this->isEnd()) { - return false; - } - return $this->peek($sString, $iOffset) == $sString; - } - - private function peek($iLength = 1, $iOffset = 0) { - if($this->isEnd()) { - return ''; - } - if(is_string($iLength)) { - $iLength = mb_strlen($iLength, $this->sCharset); - } - if(is_string($iOffset)) { - $iOffset = mb_strlen($iOffset, $this->sCharset); - } - return mb_substr($this->sText, $this->iCurrentPosition+$iOffset, $iLength, $this->sCharset); - } - - private function consume($mValue = 1) { - if(is_string($mValue)) { - $iLength = mb_strlen($mValue, $this->sCharset); - if(mb_substr($this->sText, $this->iCurrentPosition, $iLength, $this->sCharset) !== $mValue) { - throw new Exception("Expected $mValue, got ".$this->peek(5)); - } - $this->iCurrentPosition += mb_strlen($mValue, $this->sCharset); - return $mValue; - } else { - if($this->iCurrentPosition+$mValue > $this->iLength) { - throw new Exception("Tried to consume $mValue chars, exceeded file end"); - } - $sResult = mb_substr($this->sText, $this->iCurrentPosition, $mValue, $this->sCharset); - $this->iCurrentPosition += $mValue; - return $sResult; - } - } - - private function consumeExpression($mExpression) { - $aMatches; - if(preg_match($mExpression, $this->inputLeft(), $aMatches) === 1) { - if($aMatches[0][1] === $this->iCurrentPosition) { - return $this->consume($aMatches[0][0]); - } - } - throw new Exception("Expected pattern $mExpression not found, got: {$this->peek(5)}"); - } - - private function consumeWhiteSpace() { - do { - while(preg_match('/\\s/isSu', $this->peek()) === 1) { - $this->consume(1); - } - } while($this->consumeComment()); - } - - private function consumeComment() { - if($this->comes('/*')) { - $this->consumeUntil('*/'); - $this->consume('*/'); - return true; - } - return false; - } - - private function isEnd() { - return $this->iCurrentPosition >= $this->iLength; - } - - private function consumeUntil($sEnd) { - $iEndPos = mb_strpos($this->sText, $sEnd, $this->iCurrentPosition, $this->sCharset); - if($iEndPos === false) { - throw new Exception("Required $sEnd not found, got {$this->peek(5)}"); - } - return $this->consume($iEndPos-$this->iCurrentPosition); - } - - private function inputLeft() { - return mb_substr($this->sText, $this->iCurrentPosition, -1, $this->sCharset); - } -} - -abstract class CSSList { - private $aContents; - - public function __construct() { - $this->aContents = array(); - } - - public function append($oItem) { - $this->aContents[] = $oItem; - } - - public function __toString() { - $sResult = ''; - foreach($this->aContents as $oContent) { - $sResult .= $oContent->__toString(); - } - return $sResult; - } - - public function getContents() { - return $this->aContents; - } - - protected function allSelectors(&$aResult) { - foreach($this->aContents as $mContent) { - if($mContent instanceof CSSSelector) { - $aResult[] = $mContent; - } else if($mContent instanceof CSSList) { - $mContent->allSelectors($aResult); - } - } - } - - protected function allRuleSets(&$aResult) { - foreach($this->aContents as $mContent) { - if($mContent instanceof CSSRuleSet) { - $aResult[] = $mContent; - } else if($mContent instanceof CSSList) { - $mContent->allRuleSets($aResult); - } - } - } - - protected function allValues($oElement, &$aResult, $sSearchString = null) { - if($oElement instanceof CSSList) { - foreach($oElement->getContents() as $oContent) { - $this->allValues($oContent, $aResult, $sSearchString); - } - } else if($oElement instanceof CSSRuleSet) { - foreach($oElement->getRules($sSearchString) as $oRule) { - $this->allValues($oRule, $aResult, $sSearchString); - } - } else if($oElement instanceof CSSRule) { - foreach($oElement->getValues() as $aValues) { - foreach($aValues as $mValue) { - $aResult[] = $mValue; - } - } - } - } -} - -class CSSDocument extends CSSList { - public function getAllSelectors() { - $aResult = array(); - $this->allSelectors($aResult); - return $aResult; - } - - public function getAllRuleSets() { - $aResult = array(); - $this->allRuleSets($aResult); - return $aResult; - } - - public function getAllValues($mElement = null) { - $sSearchString = null; - if($mElement === null) { - $mElement = $this; - } else if(is_string($mElement)) { - $sSearchString = $mElement; - $mElement = $this; - } - $aResult = array(); - $this->allValues($mElement, $aResult, $sSearchString); - return $aResult; - } -} - -class CSSMediaQuery extends CSSList { - private $sQuery; - - public function __construct() { - parent::__construct(); - $this->sQuery = null; - } - - public function setQuery($sQuery) { - $this->sQuery = $sQuery; - } - - public function getQuery() { - return $this->sQuery; - } - - public function __toString() { - $sResult = "@media {$this->sQuery} {"; - $sResult .= parent::__toString(); - $sResult .= '}'; - return $sResult; - } -} - -class CSSImport { - private $oLocation; - private $sMediaQuery; - - public function __construct(CSSURL $oLocation, $sMediaQuery) { - $this->oLocation = $oLocation; - $this->sMediaQuery = $sMediaQuery; - } - - public function setLocation($oLocation) { - $this->oLocation = $oLocation; - } - - public function getLocation() { - return $this->oLocation; - } - - public function __toString() { - return "@import ".$this->oLocation->__toString().($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; - } -} - -class CSSCharset { - private $sCharset; - - public function __construct($sCharset) { - $this->sCharset = $sCharset; - } - - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - } - - public function getCharset() { - return $this->sCharset; - } - - public function __toString() { - return "@charset {$this->sCharset->__toString()};"; - } -} - -abstract class CSSRuleSet { - private $aRules; - - public function __construct() { - $this->aRules = array(); - } - - public function addRule(CSSRule $oRule) { - $this->aRules[$oRule->getRule()] = $oRule; - } - - public function getRules($mRule = null) { - if($mRule === null) { - return $this->aRules; - } - $aResult = array(); - if($mRule instanceof CSSRule) { - $mRule = $mRule->getRule(); - } - if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { - $sStart = substr($mRule, 0, -1); - foreach($this->aRules as $oRule) { - if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { - $aResult[$oRule->getRule()] = $this->aRules[$oRule->getRule()]; - } - } - } else if(isset($this->aRules[$mRule])) { - $aResult[$mRule] = $this->aRules[$mRule]; - } - return $aResult; - } - - public function removeRule($mRule) { - if($mRule instanceof CSSRule) { - $mRule = $mRule->getRule(); - } - if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { - $sStart = substr($mRule, 0, -1); - foreach($this->aRules as $oRule) { - if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { - unset($this->aRules[$oRule->getRule()]); - } - } - } else if(isset($this->aRules[$mRule])) { - unset($this->aRules[$mRule]); - } - } - - public function __toString() { - $sResult = ''; - foreach($this->aRules as $oRule) { - $sResult .= $oRule->__toString(); - } - return $sResult; - } -} - -class CSSAtRule extends CSSRuleSet { - private $sType; - - public function __construct($sType) { - parent::__construct(); - $this->sType = $sType; - } - - public function __toString() { - $sResult = "@{$this->sType} {"; - $sResult .= parent::__toString(); - $sResult .= '}'; - return $sResult; - } -} - -class CSSSelector extends CSSRuleSet { - private $aSelector; - - public function __construct() { - parent::__construct(); - $this->aSelector = array(); - } - - public function setSelector($mSelector) { - if(is_array($mSelector)) { - $this->aSelector = $mSelector; - } else { - $this->aSelector = explode(',', $mSelector); - } - foreach($this->aSelector as $iKey => $sSelector) { - $this->aSelector[$iKey] = trim($sSelector); - } - } - - public function getSelector() { - return $this->aSelector; - } - - public function __toString() { - $sResult = implode(', ', $this->aSelector).' {'; - $sResult .= parent::__toString(); - $sResult .= '}'; - return $sResult; - } -} - -class CSSRule { - private $sRule; - private $aValues; - private $bIsImportant; - - public function __construct($sRule) { - $this->sRule = $sRule; - $this->bIsImportant = false; - } - - public function setRule($sRule) { - $this->sRule = $sRule; - } - - public function getRule() { - return $this->sRule; - } - - public function addValue($mValue) { - $this->aValues[] = $mValue; - } - - public function setValues($aValues) { - $this->aValues = $aValues; - } - - public function getValues() { - return $this->aValues; - } - - public function setIsImportant($bIsImportant) { - $this->bIsImportant = $bIsImportant; - } - - public function getIsImportant() { - return $this->bIsImportant; - } - public function __toString() { - $sResult = "{$this->sRule}: "; - foreach($this->aValues as $aValues) { - $sResult .= implode(', ', $aValues).' '; - } - if($this->bIsImportant) { - $sResult .= '!important'; - } else { - $sResult = substr($sResult, 0, -1); - } - $sResult .= ';'; - return $sResult; - } -} - -abstract class CSSValue { - public abstract function __toString(); -} - -class CSSSize extends CSSValue { - private $fSize; - private $sUnit; - - public function __construct($fSize, $sUnit = null) { - $this->fSize = floatval($fSize); - $this->sUnit = $sUnit; - } - - 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 isRelative() { - if($this->sUnit === '%' || $this->sUnit === 'em' || $this->sUnit === 'ex') { - return true; - } - if($this->sUnit === null && $this->fSize != 0) { - return true; - } - return false; - } - - public function __toString() { - return $this->fSize.($this->sUnit === null ? '' : $this->sUnit); - } -} - -class CSSColor extends CSSValue { - private $aColor; - - public function __construct($aColor) { - $this->aColor = $aColor; - } - - public function setColor($aColor) { - $this->aColor = $aColor; - } - - public function getColor() { - return $this->aColor; - } - - public function getColorDescription() { - return implode('', array_keys($this->aColor)); - } - - public function __toString() { - return $this->getColorDescription().'('.implode(', ', $this->aColor).')'; - } -} - -class CSSString extends CSSValue { - private $sString; - - public function __construct($sString) { - $this->sString = $sString; - } - - public function setString($sString) { - $this->sString = $sString; - } - - public function getString() { - return $this->sString; - } - - public function __toString() { - return '"'.addslashes($this->sString).'"'; - } -} - -class CSSURL extends CSSValue { - private $oURL; - - public function __construct(CSSString $oURL) { - $this->oURL = $oURL; - } - - public function setURL(CSSString $oURL) { - $this->oURL = $oURL; - } - - public function getURL() { - return $this->oURL; - } - - public function __toString() { - return "url({$this->oURL->__toString()})"; - } -} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..da5949e5 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,9 @@ +PHP-CSS-Parser is freely distributable under the terms of an MIT-style license. + +Copyright (c) 2011-2012 Raphael Schweikert, http://sabberworm.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 9b24b8d2..594574f4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A Parser for CSS Files written in PHP. Allows extraction of CSS files into a dat ### Installation -Include the `CSSParser.php` file somewhere in your code using `require_once` (or `include_once`, if you prefer), it does not have any other dependencies. +Include the `CSSParser.php` file somewhere in your code using `require_once` (or `include_once`, if you prefer), the given `lib` folder needs to exist next to the file. ### Extraction @@ -26,11 +26,11 @@ The resulting CSS document structure can be manipulated prior to being output. ### Manipulation -The resulting data structure consists mainly of four basic types: `CSSList`, `CSSRuleSet`, `CSSRule` and `CSSValue`. There are two additional types used: `CSSImport` and `CSSCharset` which you won’t use often. +The resulting data structure consists mainly of five basic types: `CSSList`, `CSSRuleSet`, `CSSRule`, `CSSSelector` and `CSSValue`. There are two additional types used: `CSSImport` and `CSSCharset` which you won’t use often. #### CSSList -`CSSList` represents a generic CSS container, most likely containing selectors but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: +`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector) but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: * `CSSDocument` – representing the root of a CSS file. * `CSSMediaQuery` – represents a subsection of a CSSList that only applies to a output device matching the contained media query. @@ -40,7 +40,7 @@ The resulting data structure consists mainly of four basic types: `CSSList`, `CS `CSSRuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist: * `CSSAtRule` – for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face. -* `CSSSelector` – a selector; contains an array of selector strings (comma-separated in the CSS) as well as the rules to be applied to the matching elements. +* `CSSDeclarationBlock` – a RuleSet constrained by a `CSSSelector; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements. Note: A `CSSList` can contain other `CSSList`s (and `CSSImport`s as well as a `CSSCharset`) while a `CSSRuleSet` can only contain `CSSRule`s. @@ -67,7 +67,7 @@ If you want to manipulate a `CSSRuleSet`, use the methods `addRule(CSSRule $oRul There are a few convenience methods on CSSDocument to ease finding, manipulating and deleting rules: -* `getAllSelectors()` – does what it says; no matter how deeply nested your selectors are. +* `getAllDeclarationBlocks()` – does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`. * `getAllRuleSets()` – does what it says; no matter how deeply nested your rule sets are. * `getAllValues()` – finds all `CSSValue` objects inside `CSSRule`s. @@ -78,13 +78,11 @@ There are a few convenience methods on CSSDocument to ease finding, manipulating $sMyId = "#my_id"; $oParser = new CSSParser($sCssContents); $oCss = $oParser->parse(); - foreach($oCss->getAllSelectors() as $oSelector) { - $aSelector = $oSelector->getSelector(); - foreach($aSelector as $iKey => $sSelector) { + foreach($oCss->getAllDeclarationBlocks() as $oBlock) { + foreach($oBlock->getSelectors() as $oSelector) { //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id - $aSelector[$iKey] = "$sMyId $sSelector"; + $oSelector->setSelector($sMyId.' '.$oSelector->getSelector()); } - $oSelector->setSelector($aSelector); } #### Shrink all absolute sizes to half @@ -134,97 +132,91 @@ To output the entire CSS document into a variable, just use `->__toString()`: #### Structure (`var_dump()`) object(CSSDocument)#2 (1) { - ["aContents":"CSSList":private]=> - array(3) { - [0]=> - object(CSSCharset)#4 (1) { - ["sCharset":"CSSCharset":private]=> - object(CSSString)#3 (1) { - ["sString":"CSSString":private]=> - string(5) "utf-8" - } - } - [1]=> - object(CSSAtRule)#5 (2) { - ["sType":"CSSAtRule":private]=> - string(9) "font-face" - ["aRules":"CSSRuleSet":private]=> - array(2) { - ["font-family"]=> - object(CSSRule)#6 (3) { - ["sRule":"CSSRule":private]=> - string(11) "font-family" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - object(CSSString)#7 (1) { - ["sString":"CSSString":private]=> - string(10) "CrassRoots" - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - ["src"]=> - object(CSSRule)#8 (3) { - ["sRule":"CSSRule":private]=> - string(3) "src" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - object(CSSURL)#9 (1) { - ["oURL":"CSSURL":private]=> - object(CSSString)#10 (1) { - ["sString":"CSSString":private]=> - string(15) "../media/cr.ttf" - } - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - } - } - [2]=> - object(CSSSelector)#11 (2) { - ["aSelector":"CSSSelector":private]=> - array(2) { - [0]=> - string(4) "html" - [1]=> - string(4) "body" - } - ["aRules":"CSSRuleSet":private]=> - array(1) { - ["font-size"]=> - object(CSSRule)#12 (3) { - ["sRule":"CSSRule":private]=> - string(9) "font-size" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - object(CSSSize)#13 (2) { - ["fSize":"CSSSize":private]=> - float(1.6) - ["sUnit":"CSSSize":private]=> - string(2) "em" - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - } - } - } + ["aContents":"CSSList":private]=> + array(3) { + [0]=> + object(CSSCharset)#4 (1) { + ["sCharset":"CSSCharset":private]=> + object(CSSString)#3 (1) { + ["sString":"CSSString":private]=> + string(5) "utf-8" + } + } + [1]=> + object(CSSAtRule)#5 (2) { + ["sType":"CSSAtRule":private]=> + string(9) "font-face" + ["aRules":"CSSRuleSet":private]=> + array(2) { + ["font-family"]=> + object(CSSRule)#6 (3) { + ["sRule":"CSSRule":private]=> + string(11) "font-family" + ["mValue":"CSSRule":private]=> + object(CSSString)#7 (1) { + ["sString":"CSSString":private]=> + string(10) "CrassRoots" + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + ["src"]=> + object(CSSRule)#8 (3) { + ["sRule":"CSSRule":private]=> + string(3) "src" + ["mValue":"CSSRule":private]=> + object(CSSURL)#9 (1) { + ["oURL":"CSSURL":private]=> + object(CSSString)#10 (1) { + ["sString":"CSSString":private]=> + string(15) "../media/cr.ttf" + } + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + } + } + [2]=> + object(CSSDeclarationBlock)#11 (2) { + ["aSelectors":"CSSDeclarationBlock":private]=> + array(2) { + [0]=> + object(CSSSelector)#12 (2) { + ["sSelector":"CSSSelector":private]=> + string(4) "html" + ["iSpecificity":"CSSSelector":private]=> + NULL + } + [1]=> + object(CSSSelector)#13 (2) { + ["sSelector":"CSSSelector":private]=> + string(4) "body" + ["iSpecificity":"CSSSelector":private]=> + NULL + } + } + ["aRules":"CSSRuleSet":private]=> + array(1) { + ["font-size"]=> + object(CSSRule)#14 (3) { + ["sRule":"CSSRule":private]=> + string(9) "font-size" + ["mValue":"CSSRule":private]=> + object(CSSSize)#15 (3) { + ["fSize":"CSSSize":private]=> + float(1.6) + ["sUnit":"CSSSize":private]=> + string(2) "em" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + } + } + } } #### Output (`__toString()`) @@ -244,114 +236,116 @@ To output the entire CSS document into a variable, just use `->__toString()`: #### Structure (`var_dump()`) object(CSSDocument)#2 (1) { - ["aContents":"CSSList":private]=> - array(1) { - [0]=> - object(CSSSelector)#3 (2) { - ["aSelector":"CSSSelector":private]=> - array(1) { - [0]=> - string(7) "#header" - } - ["aRules":"CSSRuleSet":private]=> - array(3) { - ["margin"]=> - object(CSSRule)#4 (3) { - ["sRule":"CSSRule":private]=> - string(6) "margin" - ["aValues":"CSSRule":private]=> - array(4) { - [0]=> - array(1) { - [0]=> - object(CSSSize)#5 (2) { - ["fSize":"CSSSize":private]=> - float(10) - ["sUnit":"CSSSize":private]=> - string(2) "px" - } - } - [1]=> - array(1) { - [0]=> - object(CSSSize)#6 (2) { - ["fSize":"CSSSize":private]=> - float(2) - ["sUnit":"CSSSize":private]=> - string(2) "em" - } - } - [2]=> - array(1) { - [0]=> - object(CSSSize)#7 (2) { - ["fSize":"CSSSize":private]=> - float(1) - ["sUnit":"CSSSize":private]=> - string(2) "cm" - } - } - [3]=> - array(1) { - [0]=> - object(CSSSize)#8 (2) { - ["fSize":"CSSSize":private]=> - float(2) - ["sUnit":"CSSSize":private]=> - string(1) "%" - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - ["font-family"]=> - object(CSSRule)#9 (3) { - ["sRule":"CSSRule":private]=> - string(11) "font-family" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(4) { - [0]=> - string(7) "Verdana" - [1]=> - string(9) "Helvetica" - [2]=> - object(CSSString)#10 (1) { - ["sString":"CSSString":private]=> - string(9) "Gill Sans" - } - [3]=> - string(10) "sans-serif" - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - ["color"]=> - object(CSSRule)#11 (3) { - ["sRule":"CSSRule":private]=> - string(5) "color" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - string(3) "red" - } - } - ["bIsImportant":"CSSRule":private]=> - bool(true) - } - } - } - } + ["aContents":"CSSList":private]=> + array(1) { + [0]=> + object(CSSDeclarationBlock)#3 (2) { + ["aSelectors":"CSSDeclarationBlock":private]=> + array(1) { + [0]=> + object(CSSSelector)#4 (2) { + ["sSelector":"CSSSelector":private]=> + string(7) "#header" + ["iSpecificity":"CSSSelector":private]=> + NULL + } + } + ["aRules":"CSSRuleSet":private]=> + array(3) { + ["margin"]=> + object(CSSRule)#5 (3) { + ["sRule":"CSSRule":private]=> + string(6) "margin" + ["mValue":"CSSRule":private]=> + object(CSSRuleValueList)#10 (2) { + ["aComponents":protected]=> + array(4) { + [0]=> + object(CSSSize)#6 (3) { + ["fSize":"CSSSize":private]=> + float(10) + ["sUnit":"CSSSize":private]=> + string(2) "px" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + [1]=> + object(CSSSize)#7 (3) { + ["fSize":"CSSSize":private]=> + float(2) + ["sUnit":"CSSSize":private]=> + string(2) "em" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + [2]=> + object(CSSSize)#8 (3) { + ["fSize":"CSSSize":private]=> + float(1) + ["sUnit":"CSSSize":private]=> + string(2) "cm" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + [3]=> + object(CSSSize)#9 (3) { + ["fSize":"CSSSize":private]=> + float(2) + ["sUnit":"CSSSize":private]=> + string(1) "%" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + } + ["sSeparator":protected]=> + string(1) " " + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + ["font-family"]=> + object(CSSRule)#11 (3) { + ["sRule":"CSSRule":private]=> + string(11) "font-family" + ["mValue":"CSSRule":private]=> + object(CSSRuleValueList)#13 (2) { + ["aComponents":protected]=> + array(4) { + [0]=> + string(7) "Verdana" + [1]=> + string(9) "Helvetica" + [2]=> + object(CSSString)#12 (1) { + ["sString":"CSSString":private]=> + string(9) "Gill Sans" + } + [3]=> + string(10) "sans-serif" + } + ["sSeparator":protected]=> + string(1) "," + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + ["color"]=> + object(CSSRule)#14 (3) { + ["sRule":"CSSRule":private]=> + string(5) "color" + ["mValue":"CSSRule":private]=> + string(3) "red" + ["bIsImportant":"CSSRule":private]=> + bool(true) + } + } + } + } } #### Output (`__toString()`) - #header {margin: 10px 2em 1cm 2%;font-family: Verdana, Helvetica, "Gill Sans", sans-serif;color: red !important;} + #header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans", sans-serif;color: red !important;} ## To-Do @@ -359,6 +353,24 @@ To output the entire CSS document into a variable, just use `->__toString()`: * Options for output (compact, verbose, etc.) * Support for @namespace * Named color support (using `CSSColor` instead of an anonymous string literal) -* Allow for function-like property values other than hsl(), rgb(), rgba(), and url() (like -moz-linear-gradient(), for example). * Test suite * Adopt lenient parsing rules +* Support for @-rules (other than @media) that are CSSLists (to support @-webkit-keyframes) + +## Contributors/Thanks to + +* [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties. +* [GaryJones](https://github.com/GaryJones) for lots of input and [http://css-specificity.info/](http://css-specificity.info/). +* [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration. + +## License + +PHP-CSS-Parser is freely distributable under the terms of an MIT-style license. + +Copyright (c) 2011 Raphael Schweikert, http://sabberworm.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build.local.xml b/build.local.xml new file mode 100644 index 00000000..90aa3bc9 --- /dev/null +++ b/build.local.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/build.properties b/build.properties new file mode 100644 index 00000000..8ff72781 --- /dev/null +++ b/build.properties @@ -0,0 +1,13 @@ +project.name=CSS +project.channel= +project.majorVersion=0 +project.minorVersion=1 +project.patchLevel=0 +project.snapshot=true + +checkstyle.standard=Zend + +component.type=php-library +component.version=10 + +pear.local=/var/www/${project.channel} diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..a3b50792 --- /dev/null +++ b/build.xml @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The code coverage report is in file://${project.review.codecoveragedir} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Populating vendor/ with dependencies + + + + Your vendor/ folder has been built. + You only need to run 'phing build-vendor' again if you change the + dependencies listed in your package.xml file. + + + + + + + Creating vendor/ as a sandboxed PEAR install folder + + + + + + + + + + + + + + + + + Building release directory + + + + + + + + + + + + + + + + + + + + Creating ${project.tarfile} PEAR package + + + + + + + + + + + + project.lastBuiltTarfile=${project.tarfile} + + + Your PEAR package is in ${project.tarfile} + + + + + + + + + + + Please run 'phing pear-package' first, then try again. + + + + + + + + + + Cannot find PEAR package file ${project.lastBuiltTarfile} + Run 'phing pear-package' to create a new PEAR package, then try again + + + + + + + + + + + + Please run 'phing pear-package' first, then try again. + + + + + + + + + + Cannot find PEAR package file ${project.lastBuiltTarfile} + Run 'phing pear-package' to create a new PEAR package, then try again + + + + + + + + + + + + Please run 'phing pear-package' first, then try again. + + + + + + + + + + + + + + + + + + Cannot find PEAR package file ${project.lastBuiltTarfile} + Run 'phing pear-package' to create a new PEAR package, then try again + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package.xml b/package.xml new file mode 100644 index 00000000..c2c39177 --- /dev/null +++ b/package.xml @@ -0,0 +1,71 @@ + + + ${project.name} + ${project.channel} + Summary goes here + + Description goes here + + + Your name + Anything goes here + Your email address + yes + + ${build.date} + + + ${project.version} + ${project.majorVersion}.${project.minorVersion} + + + ${project.stability} + stable + + All rights reserved. + + No notes. + + + +${contents} + + + + + + 5.3.0 + + + 1.9.4 + + + Autoloader + pear.phix-project.org + 3.0.0 + 3.999.9999 + + + + + + + + X.Y.Z + X.Y + + + stable + stable + + Your release date + All rights reserved + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..864f7f78 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + src/tests/unit-tests + + + + + vendor + src/tests + + + src/bin + src/php + + + + + + + + + + + + + diff --git a/src/.empty b/src/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/README.txt b/src/README.txt new file mode 100644 index 00000000..4074f387 --- /dev/null +++ b/src/README.txt @@ -0,0 +1,68 @@ +Your src/ folder +================ + +This src/ folder is where you put all of your code for release. There's +a folder for each type of file that the PEAR Installer supports. You can +find out more about these file types online at: + +http://blog.stuartherbert.com/php/2011/04/04/explaining-file-roles/ + + * bin/ + + If you're creating any command-line tools, this is where you'd put + them. Files in here get installed into /usr/bin on Linux et al. + + There is more information available here: http://blog.stuartherbert.com/php/2011/04/06/php-components-shipping-a-command-line-program/ + + You can find an example here: https://github.com/stuartherbert/phix/tree/master/src/bin + + * data/ + + If you have any data files (any files that aren't PHP code, and which + don't belong in the www/ folder), this is the folder to put them in. + + There is more information available here: http://blog.stuartherbert.com/php/2011/04/11/php-components-shipping-data-files-with-your-components/ + + You can find an example here: https://github.com/stuartherbert/ComponentManagerPhpLibrary/tree/master/src/data + + * php/ + + This is where your component's PHP code belongs. Everything that goes + into this folder must be PSR0-compliant, so that it works with the + supplied autoloader. + + There is more information available here: http://blog.stuartherbert.com/php/2011/04/05/php-components-shipping-reusable-php-code/ + + You can find an example here: https://github.com/stuartherbert/ContractLib/tree/master/src/php + + * tests/functional-tests/ + + Right now, this folder is just a placeholder for future functionality. + You're welcome to make use of it yourself. + + * tests/integration-tests/ + + Right now, this folder is just a placeholder for future functionality. + You're welcome to make use of it yourself. + + * tests/unit-tests/ + + This is where all of your PHPUnit tests go. + + It needs to contain _exactly_ the same folder structure as the src/php/ + folder. For each of your PHP classes in src/php/, there should be a + corresponding test file in test/unit-tests. + + There is more information available here: http://blog.stuartherbert.com/php/2011/08/15/php-components-shipping-unit-tests-with-your-component/ + + You can find an example here: https://github.com/stuartherbert/ContractLib/tree/master/test/unit-tests + + * www/ + + This folder is for any files that should be published in a web server's + DocRoot folder. + + It's quite unusual for components to put anything in this folder, but + it is there just in case. + + There is more information available here: http://blog.stuartherbert.com/php/2011/08/16/php-components-shipping-web-pages-with-your-components/ diff --git a/src/bin/.empty b/src/bin/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/bin/cssdump b/src/bin/cssdump new file mode 100644 index 00000000..67d3a5af --- /dev/null +++ b/src/bin/cssdump @@ -0,0 +1,15 @@ +#!/usr/bin/env php +parse(); + +echo '#### Structure (`var_dump()`)'."\n"; +var_dump($oDoc); + +echo '#### Output (`__toString()`)'."\n"; +print $oDoc->__toString(); +echo "\n"; diff --git a/src/data/.empty b/src/data/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/docs/.empty b/src/docs/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/php/.empty b/src/php/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/php/Sabberworm/CSS/CSSAtRule.php b/src/php/Sabberworm/CSS/CSSAtRule.php new file mode 100644 index 00000000..70c1b350 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSAtRule.php @@ -0,0 +1,22 @@ +sType = $sType; + } + + public function __toString() { + $sResult = "@{$this->sType} {"; + $sResult .= parent::__toString(); + $sResult .= '}'; + return $sResult; + } +} diff --git a/src/php/Sabberworm/CSS/CSSCharset.php b/src/php/Sabberworm/CSS/CSSCharset.php new file mode 100644 index 00000000..e1875b16 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSCharset.php @@ -0,0 +1,30 @@ +sCharset = $sCharset; + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + } + + public function getCharset() { + return $this->sCharset; + } + + public function __toString() { + return "@charset {$this->sCharset->__toString()};"; + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSColor.php b/src/php/Sabberworm/CSS/CSSColor.php new file mode 100644 index 00000000..8f9b50a9 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSColor.php @@ -0,0 +1,22 @@ +aComponents; + } + + public function setColor($aColor) { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + public function getColorDescription() { + return $this->getName(); + } +} diff --git a/src/php/Sabberworm/CSS/CSSDeclarationBlock.php b/src/php/Sabberworm/CSS/CSSDeclarationBlock.php new file mode 100644 index 00000000..178ee60e --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSDeclarationBlock.php @@ -0,0 +1,570 @@ +aSelectors = array(); + } + + public function setSelectors($mSelector) { + if(is_array($mSelector)) { + $this->aSelectors = $mSelector; + } else { + $this->aSelectors = explode(',', $mSelector); + } + foreach($this->aSelectors as $iKey => $mSelector) { + if(!($mSelector instanceof CSSSelector)) { + $this->aSelectors[$iKey] = new CSSSelector($mSelector); + } + } + } + + /** + * @deprecated use getSelectors() + */ + public function getSelector() { + return $this->getSelectors(); + } + + /** + * @deprecated use setSelectors() + */ + public function setSelector($mSelector) { + $this->setSelectors($mSelector); + } + + 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(); + } + + /** + * 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 CSS3 + **/ + public function expandBorderShorthand() { + $aBorderRules = array( + 'border', 'border-left', 'border-right', 'border-top', 'border-bottom' + ); + $aBorderSizes = array( + 'thin', 'medium', 'thick' + ); + $aRules = $this->getRules(); + foreach ($aBorderRules as $sBorderRule) { + if(!isset($aRules[$sBorderRule])) continue; + $oRule = $aRules[$sBorderRule]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if($mValue instanceof CSSValue) { + $mNewValue = clone $mValue; + } else { + $mNewValue = $mValue; + } + if($mValue instanceof CSSSize) { + $sNewRuleName = $sBorderRule."-width"; + } else if($mValue instanceof CSSColor) { + $sNewRuleName = $sBorderRule."-color"; + } else { + if(in_array($mValue, $aBorderSizes)) { + $sNewRuleName = $sBorderRule."-width"; + } else/* if(in_array($mValue, $aBorderStyles))*/ { + $sNewRuleName = $sBorderRule."-style"; + } + } + $oNewRule = new CSSRule($sNewRuleName); + $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->getRules(); + foreach ($aExpansions as $sProperty => $sExpanded) { + if(!isset($aRules[$sProperty])) continue; + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $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 CSSRule(sprintf($sExpanded, $sPosition)); + $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->getRules(); + if(!isset($aRules['font'])) return; + $oRule = $aRules['font']; + // reset properties to 'normal' per http://www.w3.org/TR/CSS21/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 CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach($aValues as $mValue) { + if(!$mValue instanceof CSSValue) { + $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 CSSSize + && in_array($mValue->getSize(), range(100, 900, 100))) + ) { + $aFontProperties['font-weight'] = $mValue; + } else if($mValue instanceof CSSRuleValueList && $mValue->getListSeparator() == '/') { + list($oSize, $oHeight) = $mValue->getListComponents(); + $aFontProperties['font-size'] = $oSize; + $aFontProperties['line-height'] = $oHeight; + } else if($mValue instanceof CSSSize && $mValue->getUnit() !== null) { + $aFontProperties['font-size'] = $mValue; + } else { + $aFontProperties['font-family'] = $mValue; + } + } + foreach ($aFontProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $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/CSS21/colors.html#propdef-background + **/ + public function expandBackgroundShorthand() { + $aRules = $this->getRules(); + 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 CSSSize(0, '%'), new CSSSize(0, '%')) + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if(count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + return; + } + $iNumBgPos = 0; + foreach($aValues as $mValue) { + if(!$mValue instanceof CSSValue) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof CSSURL) { + $aBgProperties['background-image'] = $mValue; + } else if($mValue instanceof CSSColor) { + $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 CSSSize + ){ + 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 CSSRule($sProperty); + $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->getRules(); + if(!isset($aRules['list-style'])) return; + $oRule = $aRules['list-style']; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if(count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + return; + } + foreach($aValues as $mValue) { + if(!$mValue instanceof CSSValue) { + $mValue = mb_strtolower($mValue); + } + if($mValue instanceof CSSUrl) { + $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 CSSRule($sProperty); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue($mValue); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + } + + public function createShorthandProperties(array $aProperties, $sShorthand) { + $aRules = $this->getRules(); + $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 CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach($aValues as $mValue) { + $aNewValues[] = $mValue; + } + $this->removeRule($sProperty); + } + } + if(count($aNewValues)) { + $oNewRule = new CSSRule($sShorthand); + 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 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'); + } + + /* + * 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->getRules(); + 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 CSSRuleValueList) { + $aRuleValues[] = $mRuleValue; + } else { + $aRuleValues = $mRuleValue->getListComponents(); + } + $aValues[$sPosition] = $aRuleValues; + } + $oNewRule = new CSSRule($sProperty); + 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->getRules(); + if(!isset($aRules['font-size']) || !isset($aRules['font-family'])) { + return; + } + $oNewRule = new CSSRule('font'); + 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 CSSRuleValueList) { + $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 CSSRuleValueList) { + $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 CSSRuleValueList) { + $aLHValues[] = $mRuleValue; + } else { + $aLHValues = $mRuleValue->getListComponents(); + } + if($aLHValues[0] !== 'normal') { + $val = new CSSRuleValueList('/'); + $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 CSSRuleValueList) { + $aFFValues[] = $mRuleValue; + } else { + $aFFValues = $mRuleValue->getListComponents(); + } + $oFFValue = new CSSRuleValueList(','); + $oFFValue->setListComponents($aFFValues); + $oNewRule->addValue($oFFValue); + + $this->addRule($oNewRule); + foreach ($aFontProperties as $sProperty) { + $this->removeRule($sProperty); + } + } + + public function __toString() { + $sResult = implode(', ', $this->aSelectors).' {'; + $sResult .= parent::__toString(); + $sResult .= '}'."\n"; + return $sResult; + } +} diff --git a/src/php/Sabberworm/CSS/CSSDocument.php b/src/php/Sabberworm/CSS/CSSDocument.php new file mode 100644 index 00000000..04e903ed --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSDocument.php @@ -0,0 +1,88 @@ +allDeclarationBlocks($aResult); + return $aResult; + } + + /** + * @deprecated use getAllDeclarationBlocks() + */ + public function getAllSelectors() { + return $this->getAllDeclarationBlocks(); + } + + /** + * Returns all CSSRuleSet objects found recursively in the tree. + */ + public function getAllRuleSets() { + $aResult = array(); + $this->allRuleSets($aResult); + return $aResult; + } + + /** + * Returns all CSSValue objects found recursively in the tree. + * @param (object|string) $mElement the CSSList or CSSRuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{CSSRuleSet->getRules()}). + * @param (bool) $bSearchInFunctionArguments whether to also return CSSValue objects used as CSSFunction 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 CSSSelector objects found recursively in the tree. + * Note that this does not yield the full CSSDeclarationBlock 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) { + if(is_numeric($sSpecificitySearch) || is_numeric($sSpecificitySearch[0])) { + $sSpecificitySearch = "== $sSpecificitySearch"; + } + $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(); + } + } + + /* + * Create shorthands properties whenever possible + */ + public function createShorthands() + { + foreach($this->getAllDeclarationBlocks() as $oDeclaration) + { + $oDeclaration->createShorthands(); + } + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSFunction.php b/src/php/Sabberworm/CSS/CSSFunction.php new file mode 100644 index 00000000..c6d58193 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSFunction.php @@ -0,0 +1,28 @@ +sName = $sName; + parent::__construct($aArguments); + } + + public function getName() { + return $this->sName; + } + + public function setName($sName) { + $this->sName = $sName; + } + + public function getArguments() { + return $this->aComponents; + } + + public function __toString() { + $aArguments = parent::__toString(); + return "{$this->sName}({$aArguments})"; + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSImport.php b/src/php/Sabberworm/CSS/CSSImport.php new file mode 100644 index 00000000..829f1484 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSImport.php @@ -0,0 +1,28 @@ +oLocation = $oLocation; + $this->sMediaQuery = $sMediaQuery; + } + + public function setLocation($oLocation) { + $this->oLocation = $oLocation; + } + + public function getLocation() { + return $this->oLocation; + } + + public function __toString() { + return "@import ".$this->oLocation->__toString().($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; + } +} diff --git a/src/php/Sabberworm/CSS/CSSList.php b/src/php/Sabberworm/CSS/CSSList.php new file mode 100644 index 00000000..af6c8922 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSList.php @@ -0,0 +1,126 @@ +aContents = array(); + } + + public function append($oItem) { + $this->aContents[] = $oItem; + } + + /** + * Removes an item from the CSS list. + * @param CSSRuleSet|CSSImport|CSSCharset|CSSList $oItemToRemove May be a CSSRuleSet (most likely a CSSDeclarationBlock), a CSSImport, a CSSCharset or another CSSList (most likely a CSSMediaQuery) + */ + public function remove($oItemToRemove) { + $iKey = array_search($oItemToRemove, $this->aContents, true); + if($iKey !== false) { + unset($this->aContents[$iKey]); + } + } + + public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { + if($mSelector instanceof CSSDeclarationBlock) { + $mSelector = $mSelector->getSelectors(); + } + if(!is_array($mSelector)) { + $mSelector = explode(',', $mSelector); + } + foreach($mSelector as $iKey => &$mSel) { + if(!($mSel instanceof CSSSelector)) { + $mSel = new CSSSelector($mSel); + } + } + foreach($this->aContents as $iKey => $mItem) { + if(!($mItem instanceof CSSDeclarationBlock)) { + continue; + } + if($mItem->getSelectors() == $mSelector) { + unset($this->aContents[$iKey]); + if(!$bRemoveAll) { + return; + } + } + } + } + + public function __toString() { + $sResult = ''; + foreach($this->aContents as $oContent) { + $sResult .= $oContent->__toString(); + } + return $sResult; + } + + public function getContents() { + return $this->aContents; + } + + protected function allDeclarationBlocks(&$aResult) { + foreach($this->aContents as $mContent) { + if($mContent instanceof CSSDeclarationBlock) { + $aResult[] = $mContent; + } else if($mContent instanceof CSSList) { + $mContent->allDeclarationBlocks($aResult); + } + } + } + + protected function allRuleSets(&$aResult) { + foreach($this->aContents as $mContent) { + if($mContent instanceof CSSRuleSet) { + $aResult[] = $mContent; + } else if($mContent instanceof CSSList) { + $mContent->allRuleSets($aResult); + } + } + } + + protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { + if($oElement instanceof CSSList) { + foreach($oElement->getContents() as $oContent) { + $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } else if($oElement instanceof CSSRuleSet) { + foreach($oElement->getRules($sSearchString) as $oRule) { + $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } else if($oElement instanceof CSSRule) { + $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); + } else if($oElement instanceof CSSValueList) { + if($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { + foreach($oElement->getListComponents() as $mComponent) { + $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } + } else { + //Non-List CSSValue or String (CSS identifier) + $aResult[] = $oElement; + } + } + + protected function allSelectors(&$aResult, $sSpecificitySearch = null) { + foreach($this->getAllDeclarationBlocks() as $oBlock) { + foreach($oBlock->getSelectors() as $oSelector) { + if($sSpecificitySearch === null) { + $aResult[] = $oSelector; + } else { + $sComparison = "\$bRes = {$oSelector->getSpecificity()} $sSpecificitySearch;"; + eval($sComparison); + if($bRes) { + $aResult[] = $oSelector; + } + } + } + } + } +} diff --git a/src/php/Sabberworm/CSS/CSSMediaQuery.php b/src/php/Sabberworm/CSS/CSSMediaQuery.php new file mode 100644 index 00000000..3966bd46 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSMediaQuery.php @@ -0,0 +1,30 @@ +sQuery = null; + } + + public function setQuery($sQuery) { + $this->sQuery = $sQuery; + } + + public function getQuery() { + return $this->sQuery; + } + + public function __toString() { + $sResult = "@media {$this->sQuery} {"; + $sResult .= parent::__toString(); + $sResult .= '}'; + return $sResult; + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSParser.php b/src/php/Sabberworm/CSS/CSSParser.php new file mode 100644 index 00000000..e258d3a7 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSParser.php @@ -0,0 +1,461 @@ +sText = $sText; + $this->iCurrentPosition = 0; + $this->setCharset($sDefaultCharset); + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + $this->iLength = mb_strlen($this->sText, $this->sCharset); + } + + public function getCharset() { + return $this->sCharset; + } + + public function parse() { + $oResult = new CSSDocument(); + $this->parseDocument($oResult); + return $oResult; + } + + private function parseDocument(CSSDocument $oDocument) { + $this->consumeWhiteSpace(); + $this->parseList($oDocument, true); + } + + private function parseList(CSSList $oList, $bIsRoot = false) { + while(!$this->isEnd()) { + if($this->comes('@')) { + $oList->append($this->parseAtRule()); + } else if($this->comes('}')) { + $this->consume('}'); + if($bIsRoot) { + throw new \Exception("Unopened {"); + } else { + return; + } + } else { + $oList->append($this->parseSelector()); + } + $this->consumeWhiteSpace(); + } + if(!$bIsRoot) { + throw new \Exception("Unexpected end of document"); + } + } + + private function parseAtRule() { + $this->consume('@'); + $sIdentifier = $this->parseIdentifier(); + $this->consumeWhiteSpace(); + if($sIdentifier === 'media') { + $oResult = new CSSMediaQuery(); + $oResult->setQuery(trim($this->consumeUntil('{'))); + $this->consume('{'); + $this->consumeWhiteSpace(); + $this->parseList($oResult); + return $oResult; + } else if($sIdentifier === 'import') { + $oLocation = $this->parseURLValue(); + $this->consumeWhiteSpace(); + $sMediaQuery = null; + if(!$this->comes(';')) { + $sMediaQuery = $this->consumeUntil(';'); + } + $this->consume(';'); + return new CSSImport($oLocation, $sMediaQuery); + } else if($sIdentifier === 'charset') { + $sCharset = $this->parseStringValue(); + $this->consumeWhiteSpace(); + $this->consume(';'); + $this->setCharset($sCharset->getString()); + return new CSSCharset($sCharset); + } else { + //Unknown other at rule (font-face or such) + $this->consume('{'); + $this->consumeWhiteSpace(); + $oAtRule = new CSSAtRule($sIdentifier); + $this->parseRuleSet($oAtRule); + return $oAtRule; + } + } + + private function parseIdentifier($bAllowFunctions = true) { + $sResult = $this->parseCharacter(true); + if($sResult === null) { + throw new \Exception("Identifier expected, got {$this->peek(5)}"); + } + $sCharacter; + while(($sCharacter = $this->parseCharacter(true)) !== null) { + $sResult .= $sCharacter; + } + if($bAllowFunctions && $this->comes('(')) { + $this->consume('('); + $sResult = new CSSFunction($sResult, $this->parseValue(array('=', ','))); + $this->consume(')'); + } + return $sResult; + } + + private function parseStringValue() { + $sBegin = $this->peek(); + $sQuote = null; + if($sBegin === "'") { + $sQuote = "'"; + } else if($sBegin === '"') { + $sQuote = '"'; + } + if($sQuote !== null) { + $this->consume($sQuote); + } + $sResult = ""; + $sContent = null; + if($sQuote === null) { + //Unquoted strings end in whitespace or with braces, brackets, parentheses + while(!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) { + $sResult .= $this->parseCharacter(false); + } + } else { + while(!$this->comes($sQuote)) { + $sContent = $this->parseCharacter(false); + if($sContent === null) { + throw new \Exception("Non-well-formed quoted string {$this->peek(3)}"); + } + $sResult .= $sContent; + } + $this->consume($sQuote); + } + return new CSSString($sResult); + } + + private function parseCharacter($bIsForIdentifier) { + if($this->peek() === '\\') { + $this->consume('\\'); + if($this->comes('\n') || $this->comes('\r')) { + return ''; + } + $aMatches; + 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'); + if(mb_strlen($sUnicode, $this->sCharset) < 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) { + if(preg_match('/[a-zA-Z0-9]|-|_/u', $this->peek()) === 1) { + return $this->consume(1); + } else if(ord($this->peek()) > 0xa1) { + return $this->consume(1); + } else { + return null; + } + } else { + return $this->consume(1); + } + // Does not reach here + return null; + } + + private function parseSelector() { + $oResult = new CSSDeclarationBlock(); + $oResult->setSelector($this->consumeUntil('{')); + $this->consume('{'); + $this->consumeWhiteSpace(); + $this->parseRuleSet($oResult); + return $oResult; + } + + private function parseRuleSet($oRuleSet) { + while(!$this->comes('}')) { + $oRuleSet->addRule($this->parseRule()); + $this->consumeWhiteSpace(); + } + $this->consume('}'); + } + + private function parseRule() { + $oRule = new CSSRule($this->parseIdentifier()); + $this->consumeWhiteSpace(); + $this->consume(':'); + $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule())); + $oRule->setValue($oValue); + if($this->comes('!')) { + $this->consume('!'); + $this->consumeWhiteSpace(); + $sImportantMarker = $this->consume(strlen('important')); + if(mb_convert_case($sImportantMarker, MB_CASE_LOWER) !== 'important') { + throw new \Exception("! was followed by “".$sImportantMarker."”. Expected “important”"); + } + $oRule->setIsImportant(true); + } + if($this->comes(';')) { + $this->consume(';'); + } + return $oRule; + } + + private function parseValue($aListDelimiters) { + $aStack = array(); + $this->consumeWhiteSpace(); + while(!($this->comes('}') || $this->comes(';') || $this->comes('!') || $this->comes(')'))) { + if(count($aStack) > 0) { + $bFoundDelimiter = false; + foreach($aListDelimiters as $sDelimiter) { + if($this->comes($sDelimiter)) { + array_push($aStack, $this->consume($sDelimiter)); + $this->consumeWhiteSpace(); + $bFoundDelimiter = true; + break; + } + } + if(!$bFoundDelimiter) { + //Whitespace was the list delimiter + array_push($aStack, ' '); + } + } + array_push($aStack, $this->parsePrimitiveValue()); + $this->consumeWhiteSpace(); + } + 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;$iaddListComponent($aStack[$i]); + } + array_splice($aStack, $iStartPosition-1, $iLength*2-1, array($oList)); + } + } + return $aStack[0]; + } + + private static function listDelimiterForRule($sRule) { + if(preg_match('/^font($|-)/', $sRule)) { + return array(',', '/', ' '); + } + return array(',', ' ', '/'); + } + + private function parsePrimitiveValue() { + $oValue = null; + $this->consumeWhiteSpace(); + if(is_numeric($this->peek()) || (($this->comes('-') || $this->comes('.')) && is_numeric($this->peek(1, 1)))) { + $oValue = $this->parseNumericValue(); + } else if($this->comes('#') || $this->comes('rgb') || $this->comes('hsl')) { + $oValue = $this->parseColorValue(); + } else if($this->comes('url')){ + $oValue = $this->parseURLValue(); + } else if($this->comes("'") || $this->comes('"')){ + $oValue = $this->parseStringValue(); + } else { + $oValue = $this->parseIdentifier(); + } + $this->consumeWhiteSpace(); + return $oValue; + } + + private function parseNumericValue($bForColor = false) { + $sSize = ''; + if($this->comes('-')) { + $sSize .= $this->consume('-'); + } + while(is_numeric($this->peek()) || $this->comes('.')) { + if($this->comes('.')) { + $sSize .= $this->consume('.'); + } else { + $sSize .= $this->consume(1); + } + } + $fSize = floatval($sSize); + $sUnit = null; + if($this->comes('%')) { + $sUnit = $this->consume('%'); + } else if($this->comes('em')) { + $sUnit = $this->consume('em'); + } else if($this->comes('ex')) { + $sUnit = $this->consume('ex'); + } else if($this->comes('px')) { + $sUnit = $this->consume('px'); + } else if($this->comes('deg')) { + $sUnit = $this->consume('deg'); + } else if($this->comes('s')) { + $sUnit = $this->consume('s'); + } else if($this->comes('cm')) { + $sUnit = $this->consume('cm'); + } else if($this->comes('pt')) { + $sUnit = $this->consume('pt'); + } else if($this->comes('in')) { + $sUnit = $this->consume('in'); + } else if($this->comes('pc')) { + $sUnit = $this->consume('pc'); + } else if($this->comes('cm')) { + $sUnit = $this->consume('cm'); + } else if($this->comes('mm')) { + $sUnit = $this->consume('mm'); + } + return new CSSSize($fSize, $sUnit, $bForColor); + } + + private function parseColorValue() { + $aColor = array(); + if($this->comes('#')) { + $this->consume('#'); + $sValue = $this->parseIdentifier(false); + if(mb_strlen($sValue, $this->sCharset) === 3) { + $sValue = $sValue[0].$sValue[0].$sValue[1].$sValue[1].$sValue[2].$sValue[2]; + } + $aColor = array('r' => new CSSSize(intval($sValue[0].$sValue[1], 16), null, true), 'g' => new CSSSize(intval($sValue[2].$sValue[3], 16), null, true), 'b' => new CSSSize(intval($sValue[4].$sValue[5], 16), null, true)); + } else { + $sColorMode = $this->parseIdentifier(false); + $this->consumeWhiteSpace(); + $this->consume('('); + $iLength = mb_strlen($sColorMode, $this->sCharset); + for($i=0;$i<$iLength;$i++) { + $this->consumeWhiteSpace(); + $aColor[$sColorMode[$i]] = $this->parseNumericValue(true); + $this->consumeWhiteSpace(); + if($i < ($iLength-1)) { + $this->consume(','); + } + } + $this->consume(')'); + } + return new CSSColor($aColor); + } + + private function parseURLValue() { + $bUseUrl = $this->comes('url'); + if($bUseUrl) { + $this->consume('url'); + $this->consumeWhiteSpace(); + $this->consume('('); + } + $this->consumeWhiteSpace(); + $oResult = new CSSURL($this->parseStringValue()); + if($bUseUrl) { + $this->consumeWhiteSpace(); + $this->consume(')'); + } + return $oResult; + } + + private function comes($sString, $iOffset = 0) { + if($this->isEnd()) { + return false; + } + return $this->peek($sString, $iOffset) == $sString; + } + + private function peek($iLength = 1, $iOffset = 0) { + if($this->isEnd()) { + return ''; + } + if(is_string($iLength)) { + $iLength = mb_strlen($iLength, $this->sCharset); + } + if(is_string($iOffset)) { + $iOffset = mb_strlen($iOffset, $this->sCharset); + } + return mb_substr($this->sText, $this->iCurrentPosition+$iOffset, $iLength, $this->sCharset); + } + + private function consume($mValue = 1) { + if(is_string($mValue)) { + $iLength = mb_strlen($mValue, $this->sCharset); + if(mb_substr($this->sText, $this->iCurrentPosition, $iLength, $this->sCharset) !== $mValue) { + throw new \Exception("Expected $mValue, got ".$this->peek(5)); + } + $this->iCurrentPosition += mb_strlen($mValue, $this->sCharset); + return $mValue; + } else { + if($this->iCurrentPosition+$mValue > $this->iLength) { + throw new \Exception("Tried to consume $mValue chars, exceeded file end"); + } + $sResult = mb_substr($this->sText, $this->iCurrentPosition, $mValue, $this->sCharset); + $this->iCurrentPosition += $mValue; + return $sResult; + } + } + + private function consumeExpression($mExpression) { + $aMatches; + if(preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) { + return $this->consume($aMatches[0][0]); + } + throw new \Exception("Expected pattern $mExpression not found, got: {$this->peek(5)}"); + } + + private function consumeWhiteSpace() { + do { + while(preg_match('/\\s/isSu', $this->peek()) === 1) { + $this->consume(1); + } + } while($this->consumeComment()); + } + + private function consumeComment() { + if($this->comes('/*')) { + $this->consumeUntil('*/'); + $this->consume('*/'); + return true; + } + return false; + } + + private function isEnd() { + return $this->iCurrentPosition >= $this->iLength; + } + + private function consumeUntil($sEnd) { + $iEndPos = mb_strpos($this->sText, $sEnd, $this->iCurrentPosition, $this->sCharset); + if($iEndPos === false) { + throw new \Exception("Required $sEnd not found, got {$this->peek(5)}"); + } + return $this->consume($iEndPos-$this->iCurrentPosition); + } + + private function inputLeft() { + return mb_substr($this->sText, $this->iCurrentPosition, -1, $this->sCharset); + } +} + diff --git a/src/php/Sabberworm/CSS/CSSPrimitiveValue.php b/src/php/Sabberworm/CSS/CSSPrimitiveValue.php new file mode 100644 index 00000000..703ede57 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSPrimitiveValue.php @@ -0,0 +1,7 @@ +sRule = $sRule; + $this->mValue = null; + $this->bIsImportant = false; + } + + 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 CSSRuleValueList if necessary. + */ + public function setValues($aSpaceSeparatedValues) { + $oSpaceSeparatedList = null; + if(count($aSpaceSeparatedValues) > 1) { + $oSpaceSeparatedList = new CSSRuleValueList(' '); + } + foreach($aSpaceSeparatedValues as $aCommaSeparatedValues) { + $oCommaSeparatedList = null; + if(count($aCommaSeparatedValues) > 1) { + $oCommaSeparatedList = new CSSRuleValueList(','); + } + 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) CSSValueList object(s). + */ + public function getValues() { + if(!$this->mValue instanceof CSSRuleValueList) { + 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 CSSRuleValueList || $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 CSSRuleValueList 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 CSSRuleValueList || $this->mValue->getListSeparator() !== $sType) { + $mCurrentValue = $this->mValue; + $this->mValue = new CSSRuleValueList($sType); + if($mCurrentValue) { + $this->mValue->addListComponent($mCurrentValue); + } + } + foreach($mValue as $mValueItem) { + $this->mValue->addListComponent($mValueItem); + } + } + + public function setIsImportant($bIsImportant) { + $this->bIsImportant = $bIsImportant; + } + + public function getIsImportant() { + return $this->bIsImportant; + } + + public function __toString() { + $sResult = "{$this->sRule}: "; + if($this->mValue instanceof CSSValue) { //Can also be a CSSValueList + $sResult .= $this->mValue->__toString(); + } else { + $sResult .= $this->mValue; + } + if($this->bIsImportant) { + $sResult .= ' !important'; + } + $sResult .= ';'; + return $sResult; + } +} diff --git a/src/php/Sabberworm/CSS/CSSRuleSet.php b/src/php/Sabberworm/CSS/CSSRuleSet.php new file mode 100644 index 00000000..6c9a7fd1 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSRuleSet.php @@ -0,0 +1,70 @@ +aRules = array(); + } + + public function addRule(CSSRule $oRule) { + $this->aRules[$oRule->getRule()] = $oRule; + } + + /** + * Returns all rules matching the given pattern + * @param (null|string|CSSRule) $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 CSSRule 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('font' => $oRule) or array(). + */ + public function getRules($mRule = null) { + if($mRule === null) { + return $this->aRules; + } + $aResult = array(); + if($mRule instanceof CSSRule) { + $mRule = $mRule->getRule(); + } + if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { + $sStart = substr($mRule, 0, -1); + foreach($this->aRules as $oRule) { + if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { + $aResult[$oRule->getRule()] = $this->aRules[$oRule->getRule()]; + } + } + } else if(isset($this->aRules[$mRule])) { + $aResult[$mRule] = $this->aRules[$mRule]; + } + return $aResult; + } + + public function removeRule($mRule) { + if($mRule instanceof CSSRule) { + $mRule = $mRule->getRule(); + } + if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { + $sStart = substr($mRule, 0, -1); + foreach($this->aRules as $oRule) { + if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { + unset($this->aRules[$oRule->getRule()]); + } + } + } else if(isset($this->aRules[$mRule])) { + unset($this->aRules[$mRule]); + } + } + + public function __toString() { + $sResult = ''; + foreach($this->aRules as $oRule) { + $sResult .= $oRule->__toString(); + } + return $sResult; + } +} diff --git a/src/php/Sabberworm/CSS/CSSRuleValueList.php b/src/php/Sabberworm/CSS/CSSRuleValueList.php new file mode 100644 index 00000000..701d8df2 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSRuleValueList.php @@ -0,0 +1,9 @@ +\~]+)[\w]+ # elements + | + \:{1,2}( # pseudo-elements + after|before + |first-letter|first-line + |selection + ) + )/ix'; + + private $sSelector; + private $iSpecificity; + + public function __construct($sSelector, $bCalculateSpecificity = false) { + $this->setSelector($sSelector); + if($bCalculateSpecificity) { + $this->getSpecificity(); + } + } + + public function getSelector() { + return $this->sSelector; + } + + 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; + $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/src/php/Sabberworm/CSS/CSSSize.php b/src/php/Sabberworm/CSS/CSSSize.php new file mode 100644 index 00000000..d12110c0 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSSize.php @@ -0,0 +1,61 @@ +fSize = floatval($fSize); + $this->sUnit = $sUnit; + $this->bIsColorComponent = $bIsColorComponent; + } + + 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 CSSSize 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 CSSColor object. + */ + public function isSize() { + $aNonSizeUnits = array('deg', 'grad', 'rad', 'turns', 's', 'ms', 'Hz', 'kHz'); + if(in_array($this->sUnit, $aNonSizeUnits)) { + return false; + } + return !$this->isColorComponent(); + } + + public function isRelative() { + if($this->sUnit === '%' || $this->sUnit === 'em' || $this->sUnit === 'ex') { + return true; + } + if($this->sUnit === null && $this->fSize != 0) { + return true; + } + return false; + } + + public function __toString() { + return $this->fSize.($this->sUnit === null ? '' : $this->sUnit); + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSString.php b/src/php/Sabberworm/CSS/CSSString.php new file mode 100644 index 00000000..26c903c3 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSString.php @@ -0,0 +1,25 @@ +sString = $sString; + } + + public function setString($sString) { + $this->sString = $sString; + } + + public function getString() { + return $this->sString; + } + + public function __toString() { + $sString = addslashes($this->sString); + $sString = str_replace("\n", '\A', $sString); + return '"'.$sString.'"'; + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSURL.php b/src/php/Sabberworm/CSS/CSSURL.php new file mode 100644 index 00000000..7d83346e --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSURL.php @@ -0,0 +1,24 @@ +oURL = $oURL; + } + + public function setURL(CSSString $oURL) { + $this->oURL = $oURL; + } + + public function getURL() { + return $this->oURL; + } + + public function __toString() { + return "url({$this->oURL->__toString()})"; + } +} + diff --git a/src/php/Sabberworm/CSS/CSSValue.php b/src/php/Sabberworm/CSS/CSSValue.php new file mode 100644 index 00000000..da0bc6f6 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSValue.php @@ -0,0 +1,7 @@ +getListSeparator() === $sSeparator) { + $aComponents = $aComponents->getListComponents(); + } else 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; + } + + function __toString() { + return implode($this->sSeparator, $this->aComponents); + } +} diff --git a/src/tests/.empty b/src/tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/functional-tests/.empty b/src/tests/functional-tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/integration-tests/.empty b/src/tests/integration-tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/unit-tests/.empty b/src/tests/unit-tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/unit-tests/bin/.empty b/src/tests/unit-tests/bin/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/unit-tests/bootstrap.php b/src/tests/unit-tests/bootstrap.php new file mode 100644 index 00000000..c5203baf --- /dev/null +++ b/src/tests/unit-tests/bootstrap.php @@ -0,0 +1,33 @@ +parse(); + foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration) + { + $oDeclaration->expandBorderShorthand(); + } + $this->assertSame(trim((string)$oDoc), $sExpected); + } + public function expandBorderShorthandProvider() + { + return array( + array('body{ border: 2px solid rgb(0,0,0) }', 'body {border-width: 2px;border-style: solid;border-color: rgb(0,0,0);}'), + array('body{ border: none }', 'body {border-style: none;}'), + array('body{ border: 2px }', 'body {border-width: 2px;}'), + array('body{ border: rgb(255,0,0) }', 'body {border-color: rgb(255,0,0);}'), + 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 CSSParser($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 CSSParser($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: rgb(255,0,0);}','body {background-color: rgb(255,0,0);background-image: none;background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'), + array('body {background: rgb(255,0,0) url("foobar.png");}','body {background-color: rgb(255,0,0);background-image: url("foobar.png");background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'), + array('body {background: rgb(255,0,0) url("foobar.png") no-repeat;}','body {background-color: rgb(255,0,0);background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: 0% 0%;}'), + array('body {background: rgb(255,0,0) url("foobar.png") no-repeat center;}','body {background-color: rgb(255,0,0);background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: center center;}'), + array('body {background: rgb(255,0,0) url("foobar.png") no-repeat top left;}','body {background-color: rgb(255,0,0);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 CSSParser($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 CSSParser($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: rgb(0,0,0);}', 'body {border: 2px solid rgb(0,0,0);}'), + 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 CSSParser($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 CSSParser($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 CSSParser($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: rgb(255,0,0);}', 'body {background: rgb(255,0,0);}'), + array('body {background-color: rgb(255,0,0);background-image: url(foobar.png);}', 'body {background: rgb(255,0,0) url("foobar.png");}'), + array('body {background-color: rgb(255,0,0);background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: rgb(255,0,0) url("foobar.png") no-repeat;}'), + array('body {background-color: rgb(255,0,0);background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: rgb(255,0,0) url("foobar.png") no-repeat;}'), + array('body {background-color: rgb(255,0,0);background-image: url(foobar.png);background-repeat: no-repeat;background-position: center;}', 'body {background: rgb(255,0,0) url("foobar.png") no-repeat center;}'), + array('body {background-color: rgb(255,0,0);background-image: url(foobar.png);background-repeat: no-repeat;background-position: top left;}', 'body {background: rgb(255,0,0) url("foobar.png") no-repeat top left;}'), + ); + } + +} diff --git a/src/tests/unit-tests/php/Sabberworm/CSS/CSSParserTests.php b/src/tests/unit-tests/php/Sabberworm/CSS/CSSParserTests.php new file mode 100644 index 00000000..30ac014f --- /dev/null +++ b/src/tests/unit-tests/php/Sabberworm/CSS/CSSParserTests.php @@ -0,0 +1,278 @@ +assertNotEquals('', $oParser->parse()->__toString()); + } catch(Exception $e) { + $this->fail($e); + } + } + closedir($rHandle); + } + } + + /** + * @depends testCssFiles + */ + function testColorParsing() { + $oDoc = $this->parsedStructureForFile('colortest'); + foreach($oDoc->getAllRuleSets() as $oRuleSet) { + if(!$oRuleSet instanceof CSSDeclarationBlock) { + continue; + } + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if($sSelector == '#mine') { + $aColorRule = $oRuleSet->getRules('color'); + $aValues = $aColorRule['color']->getValues(); + $this->assertSame('red', $aValues[0][0]); + $aColorRule = $oRuleSet->getRules('background-'); + $aValues = $aColorRule['background-color']->getValues(); + $this->assertEquals(array('r' => new CSSSize(35.0, null, true), 'g' => new CSSSize(35.0, null, true), 'b' => new CSSSize(35.0, null, true)), $aValues[0][0]->getColor()); + $aColorRule = $oRuleSet->getRules('border-color'); + $aValues = $aColorRule['border-color']->getValues(); + $this->assertEquals(array('r' => new CSSSize(10.0, null, true), 'g' => new CSSSize(100.0, null, true), 'b' => new CSSSize(230.0, null, true), 'a' => new CSSSize(0.3, null, true)), $aValues[0][0]->getColor()); + $aColorRule = $oRuleSet->getRules('outline-color'); + $aValues = $aColorRule['outline-color']->getValues(); + $this->assertEquals(array('r' => new CSSSize(34.0, null, true), 'g' => new CSSSize(34.0, null, true), 'b' => new CSSSize(34.0, null, true)), $aValues[0][0]->getColor()); + } + } + foreach($oDoc->getAllValues('background-') as $oColor) { + if($oColor->getColorDescription() === 'hsl') { + $this->assertEquals(array('h' => new CSSSize(220.0, null, true), 's' => new CSSSize(10.0, null, true), 'l' => new CSSSize(220.0, null, true)), $oColor->getColor()); + } + } + foreach($oDoc->getAllValues('color') as $sColor) { + $this->assertSame('red', $sColor); + } + } + + 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['content']->getValues(); + $sCssString = $aContents[0][0]->__toString(); + if($sSelector == '.test-1') { + $this->assertSame('" "', $sCssString); + } + if($sSelector == '.test-2') { + $this->assertSame('"é"', $sCssString); + } + if($sSelector == '.test-3') { + $this->assertSame('" "', $sCssString); + } + if($sSelector == '.test-4') { + $this->assertSame('"𝄞"', $sCssString); + } + if($sSelector == '.test-5') { + $this->assertSame('"水"', $sCssString); + } + if($sSelector == '.test-6') { + $this->assertSame('"¥"', $sCssString); + } + if($sSelector == '.test-7') { + $this->assertSame('"\A"', $sCssString); + } + if($sSelector == '.test-8') { + $this->assertSame('"\"\""', $sCssString); + } + if($sSelector == '.test-9') { + $this->assertSame('"\"\\\'"', $sCssString); + } + if($sSelector == '.test-10') { + $this->assertSame('"\\\'\\\\"', $sCssString); + } + if($sSelector == '.test-11') { + $this->assertSame('"test"', $sCssString); + } + } + } + + 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 CSSSelector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100')); + } + + 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: 1.6em;}'."\n", $oDoc->__toString()); + 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: 1.6em;}'."\n", $oDoc->__toString()); + + $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;} +body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('font-'); + } + $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;} +body {color: green;}'."\n", $oDoc->__toString()); + } + + 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;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllValues(null) as $mValue) { + if($mValue instanceof CSSSize && $mValue->isSize() && !$mValue->isRelative()) { + $mValue->setSize($mValue->getSize()*3); + } + } + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oRule = $oBlock->getRules('font'); + $oRule = $oRule['font']; + $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['border-radius']; + $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;}'."\n", $oDoc->__toString()); + } + + function testFunctionSyntax() { + $oDoc = $this->parsedStructureForFile('functions'); + $sExpected = 'div.main {background-image: linear-gradient(rgb(0,0,0),rgb(255,255,255));} +.collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;margin-right: 0.2em;-moz-transition-property: -moz-transform;-moz-transition-duration: 0.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: 0.3s;} +.collapser.expanded + * {height: auto;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + + foreach($oDoc->getAllValues(null, true) as $mValue) { + if($mValue instanceof CSSSize && $mValue->isSize()) { + $mValue->setSize($mValue->getSize()*3); + } + } + $sExpected = str_replace(array('1.2em', '0.2em', '60%'), array('3.6em', '0.6em', '180%'), $sExpected); + $this->assertSame($sExpected, $oDoc->__toString()); + + foreach($oDoc->getAllValues(null, true) as $mValue) { + if($mValue instanceof CSSSize && !$mValue->isRelative() && !$mValue->isColorComponent()) { + $mValue->setSize($mValue->getSize()*2); + } + } + $sExpected = str_replace(array('0.2s', '0.3s', '90deg'), array('0.4s', '0.6s', '180deg'), $sExpected); + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testExpandShorthands() { + $oDoc = $this->parsedStructureForFile('expand-shorthands'); + $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid rgb(255,0,255);background: rgb(204,204,204) url("/images/foo.png") no-repeat left top;margin: 1em !important;padding: 2px 6px 3px;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + $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: rgb(255,0,255);border-right-color: rgb(255,0,255);border-bottom-color: rgb(255,0,255);border-left-color: rgb(255,0,255);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: rgb(204,204,204);background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;background-position: left top;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + } + + 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: rgb(153,153,153);border-style: dotted;background-color: rgb(255,255,255);background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;margin-bottom: 4px;margin-left: 5px;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + $oDoc->createShorthands(); + $sExpected = 'body {background: rgb(255,255,255) url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;border: 2px dotted rgb(153,153,153);font: bold 2em Helvetica,Arial,sans-serif;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testListValueRemoval() { + $oDoc = $this->parsedStructureForFile('atrules'); + foreach($oDoc->getContents() as $oItem) { + if($oItem instanceof CSSAtRule) { + $oDoc->remove($oItem); + break; + } + } + $this->assertSame('@charset "utf-8";html, body {font-size: 1.6em;}'."\n", $oDoc->__toString()); + + $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;}'."\n", $oDoc->__toString()); + + $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;}'."\n", $oDoc->__toString()); + } + + function parsedStructureForFile($sFileName) { + $sFile = dirname(__FILE__).DIRECTORY_SEPARATOR.'files'.DIRECTORY_SEPARATOR."$sFileName.css"; + $oParser = new CSSParser(file_get_contents($sFile)); + return $oParser->parse(); + } + +} diff --git a/src/tests/unit-tests/www/.empty b/src/tests/unit-tests/www/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/www/.empty b/src/www/.empty new file mode 100644 index 00000000..e69de29b diff --git a/tests/CSSParserTests.php b/tests/CSSParserTests.php deleted file mode 100644 index f46b17f5..00000000 --- a/tests/CSSParserTests.php +++ /dev/null @@ -1,69 +0,0 @@ -parse(); - } catch(Exception $e) { - $this->fail($e); - } - } - closedir($rHandle); - } - } - - /** - * @depends testCssFiles - */ - function testColorParsing() { - $oDoc = $this->parsedStructureForFile('colortest'); - foreach($oDoc->getAllRuleSets() as $oRuleSet) { - if(!$oRuleSet instanceof CSSSelector) { - continue; - } - $aSelector = $oRuleSet->getSelector(); - if($aSelector[0] === '#mine') { - $aColorRule = $oRuleSet->getRules('color'); - $aValues = $aColorRule['color']->getValues(); - $this->assertSame('red', $aValues[0][0]); - $aColorRule = $oRuleSet->getRules('background-'); - $aValues = $aColorRule['background-color']->getValues(); - $this->assertEquals(array('r' => new CSSSize(35.0), 'g' => new CSSSize(35.0), 'b' => new CSSSize(35.0)), $aValues[0][0]->getColor()); - $aColorRule = $oRuleSet->getRules('border-color'); - $aValues = $aColorRule['border-color']->getValues(); - $this->assertEquals(array('r' => new CSSSize(10.0), 'g' => new CSSSize(100.0), 'b' => new CSSSize(230.0), 'a' => new CSSSize(0.3)), $aValues[0][0]->getColor()); - $aColorRule = $oRuleSet->getRules('outline-color'); - $aValues = $aColorRule['outline-color']->getValues(); - $this->assertEquals(array('r' => new CSSSize(34.0), 'g' => new CSSSize(34.0), 'b' => new CSSSize(34.0)), $aValues[0][0]->getColor()); - } - } - foreach($oDoc->getAllValues('background-') as $oColor) { - if($oColor->getColorDescription() === 'hsl') { - $this->assertEquals(array('h' => new CSSSize(220.0), 's' => new CSSSize(10.0), 'l' => new CSSSize(220.0)), $oColor->getColor()); - } - } - foreach($oDoc->getAllValues('color') as $sColor) { - $this->assertSame('red', $sColor); - } - } - - function parsedStructureForFile($sFileName) { - $sFile = dirname(__FILE__).DIRECTORY_SEPARATOR.'files'.DIRECTORY_SEPARATOR."$sFileName.css"; - $oParser = new CSSParser(file_get_contents($sFile)); - return $oParser->parse(); - } -} \ No newline at end of file