diff --git a/CSSParser.php b/CSSParser.php deleted file mode 100644 index c6585c2d..00000000 --- a/CSSParser.php +++ /dev/null @@ -1,496 +0,0 @@ -sText = $sText; - $this->iCurrentPosition = 0; - $this->setCharset($sDefaultCharset); - } - - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - $this->iLength = $this->strlen($this->sText); - } - - public function getCharset() { - return $this->sCharset; - } - - public function parse() { - $oResult = new CSSDocument(); - $this->parseDocument($oResult); - return $oResult; - } - - public function setUseMbFlag($bFlag){ - $this->bUseMbFunctions = (bool) $bFlag; - } - - 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($this->strlen($sUnicode) < 6) { - //Consume whitespace after incomplete unicode escape - if(preg_match('/\\s/isSu', $this->peek())) { - if($this->comes('\r\n')) { - $this->consume(2); - } else { - $this->consume(1); - } - } - } - $iUnicode = intval($sUnicode, 16); - $sUtf32 = ""; - for($i=0;$i<4;$i++) { - $sUtf32 .= chr($iUnicode & 0xff); - $iUnicode = $iUnicode >> 8; - } - return iconv('utf-32le', $this->sCharset, $sUtf32); - } - if($bIsForIdentifier) { - 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($this->strlen($sValue) === 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 = $this->strlen($sColorMode); - 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 = $this->strlen($iLength); - } - if(is_string($iOffset)) { - $iOffset = $this->strlen($iOffset); - } - return $this->substr($this->sText, $this->iCurrentPosition+$iOffset, $iLength); - } - - private function consume($mValue = 1) { - if(is_string($mValue)) { - $iLength = $this->strlen($mValue); - if($this->substr($this->sText, $this->iCurrentPosition, $iLength) !== $mValue) { - throw new Exception("Expected $mValue, got ".$this->peek(5)); - } - $this->iCurrentPosition += $this->strlen($mValue); - return $mValue; - } else { - if($this->iCurrentPosition+$mValue > $this->iLength) { - throw new Exception("Tried to consume $mValue chars, exceeded file end"); - } - $sResult = $this->substr($this->sText, $this->iCurrentPosition, $mValue); - $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 $this->substr($this->sText, $this->iCurrentPosition, -1); - } - - private function substr($string, $start, $length){ - if($this->bUseMbFunctions) { - return mb_substr($string, $start, $length, $this->sCharset); - } - else { - return substr($string, $start, $length); - } - } - - private function strlen($text) - { - if($this->bUseMbFunctions) { - return mb_strlen($text, $this->sCharset); - } - else { - return strlen($text); - } - - } -} - diff --git a/README.md b/README.md index 594574f4..badacd06 100644 --- a/README.md +++ b/README.md @@ -5,78 +5,84 @@ A Parser for CSS Files written in PHP. Allows extraction of CSS files into a dat ## Usage -### Installation +### Installation using composer -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. +Add php-css-parser to your composer.json + + { + "require": { + "sabberworm/php-css-parser": "*" + } + } ### Extraction To use the CSS Parser, create a new instance. The constructor takes the following form: - new CSSParser($sCssContents, $sCharset = 'utf-8'); + new Sabberworm\CSS\Parser($sText, $sDefaultCharset = 'utf-8'); The charset is used only if no @charset declaration is found in the CSS file. To read a file, for example, you’d do the following: - $oCssParser = new CSSParser(file_get_contents('somefile.css')); + $oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css')); $oCssDocument = $oCssParser->parse(); The resulting CSS document structure can be manipulated prior to being output. ### Manipulation -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. +The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset` which you won’t use often. #### CSSList `CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector) but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: -* `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. +* `Document` – representing the root of a CSS file. +* `MediaQuery` – represents a subsection of a CSSList that only applies to a output device matching the contained media query. -#### CSSRuleSet +#### RuleSet -`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: +`RuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist: -* `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. -* `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. +* `AtRule` – for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face. +* `DeclarationBlock` – a RuleSet constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements. -Note: A `CSSList` can contain other `CSSList`s (and `CSSImport`s as well as a `CSSCharset`) while a `CSSRuleSet` can only contain `CSSRule`s. +Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`) while a `RuleSet` can only contain `Rule`s. -#### CSSRule +#### Rule -`CSSRule`s just have a key (the rule) and multiple values (the part after the colon in the CSS file). This means the `values` attribute is an array consisting of arrays. The inner level of arrays is comma-separated in the CSS file while the outer level is whitespace-separated. +`Rule`s just have a key (the rule) and multiple values (the part after the colon in the CSS file). This means the `values` attribute is an array consisting of arrays. The inner level of arrays is comma-separated in the CSS file while the outer level is whitespace-separated. -#### CSSValue +#### Value -`CSSValue` is an abstract class that only defines the `__toString` method. The concrete subclasses are: +`Value` is an abstract class that only defines the `__toString` method. The concrete subclasses are: -* `CSSSize` – consists of a numeric `size` value and a unit. -* `CSSColor` – colors can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are alwas stored as an array of ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form. -* `CSSString` – this is just a wrapper for quoted strings to distinguish them from keywords; always output with double quotes. -* `CSSURL` – URLs in CSS; always output in URL("") notation. +* `Size` – consists of a numeric `size` value and a unit. +* `Color` – colors can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are alwas stored as an array of ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form. +* `String` – this is just a wrapper for quoted strings to distinguish them from keywords; always output with double quotes. +* `URL` – URLs in CSS; always output in URL("") notation. -To access the items stored in a `CSSList` – like the document you got back when calling `$oCssParser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `CSSRuleSet`, a `CSSImport` or a `CSSCharset`. +To access the items stored in a `CSSList` – like the document you got back when calling `$oCssParser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `RuleSet`, a `Import` or a `Charset`. To append a new item (selector, media query, etc.) to an existing `CSSList`, construct it using the constructor for this class and use the `append($oItem)` method. -If you want to manipulate a `CSSRuleSet`, use the methods `addRule(CSSRule $oRule)`, `getRules()` and `removeRule($mRule)` (which accepts either a CSSRule instance or a rule name; optionally suffixed by a dash to remove all related rules). +If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $oRule)`, `getRules()` and `removeRule($mRule)` (which accepts either a Rule instance or a rule name; optionally suffixed by a dash to remove all related rules). #### Convenience methods -There are a few convenience methods on CSSDocument to ease finding, manipulating and deleting rules: +There are a few convenience methods on Document to ease finding, manipulating and deleting rules: * `getAllDeclarationBlocks()` – does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`. * `getAllRuleSets()` – does what it says; no matter how deeply nested your rule sets are. -* `getAllValues()` – finds all `CSSValue` objects inside `CSSRule`s. +* `getAllValues()` – finds all `Value` objects inside `Rule`s. ### Use cases -#### Use `CSSParser` to prepend an id to all selectors +#### Use `Parser` to prepend an id to all selectors $sMyId = "#my_id"; - $oParser = new CSSParser($sCssContents); + $oParser = new Sabberworm\CSS\Parser($sText); $oCss = $oParser->parse(); foreach($oCss->getAllDeclarationBlocks() as $oBlock) { foreach($oBlock->getSelectors() as $oSelector) { @@ -87,7 +93,7 @@ There are a few convenience methods on CSSDocument to ease finding, manipulating #### Shrink all absolute sizes to half - $oParser = new CSSParser($sCssContents); + $oParser = new Sabberworm\CSS\Parser($sText); $oCss = $oParser->parse(); foreach($oCss->getAllValues() as $mValue) { if($mValue instanceof CSSSize && !$mValue->isRelative()) { @@ -97,7 +103,7 @@ There are a few convenience methods on CSSDocument to ease finding, manipulating #### Remove unwanted rules - $oParser = new CSSParser($sCssContents); + $oParser = new Sabberworm\CSS\Parser($sText); $oCss = $oParser->parse(); foreach($oCss->getAllRuleSets() as $oRuleSet) { $oRuleSet->removeRule('font-'); //Note that the added dash will make this remove all rules starting with font- (like font-size, font-weight, etc.) as well as a potential font-rule @@ -108,7 +114,7 @@ There are a few convenience methods on CSSDocument to ease finding, manipulating To output the entire CSS document into a variable, just use `->__toString()`: - $oCssParser = new CSSParser(file_get_contents('somefile.css')); + $oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css')); $oCssDocument = $oCssParser->parse(); print $oCssDocument->__toString(); @@ -352,7 +358,7 @@ To output the entire CSS document into a variable, just use `->__toString()`: * More convenience methods [like `selectorsWithElement($sId/Class/TagName)`, `removeSelector($oSelector)`, `attributesOfType($sType)`, `removeAttributesOfType($sType)`] * Options for output (compact, verbose, etc.) * Support for @namespace -* Named color support (using `CSSColor` instead of an anonymous string literal) +* Named color support (using `Color` instead of an anonymous string literal) * Test suite * Adopt lenient parsing rules * Support for @-rules (other than @media) that are CSSLists (to support @-webkit-keyframes) diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..bd3830ce --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "sabberworm/php-css-parser", + "type": "library", + "description": "Parser for CSS Files written in PHP", + "keywords": ["parser", "css", "stylesheet"], + "homepage": "http://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "license": "MIT", + "authors": [ + {"name": "Raphael Schweikert"} + ], + "require": { + "php": ">=5.3.2" + }, + "autoload": { + "psr-0": { "Sabberworm\\CSS": "lib/" } + } +} diff --git a/lib/CSSList.php b/lib/CSSList.php deleted file mode 100644 index b2a4b23f..00000000 --- a/lib/CSSList.php +++ /dev/null @@ -1,236 +0,0 @@ -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; - } - } - } - } - } -} - -/** -* The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered. -*/ -class CSSDocument extends CSSList { - /** - * Gets all CSSDeclarationBlock objects recursively. - */ - public function getAllDeclarationBlocks() { - $aResult = array(); - $this->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(); - } - } -} - -/** -* A CSSList consisting of the CSSList and CSSList objects found in a @media query. -*/ -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; - } -} diff --git a/lib/CSSProperties.php b/lib/CSSProperties.php deleted file mode 100644 index 15c9edbd..00000000 --- a/lib/CSSProperties.php +++ /dev/null @@ -1,124 +0,0 @@ -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 representing an @charset rule. -* The following restrictions apply: -* • May not be found in any CSSList other than the CSSDocument. -* • May only appear at the very top of a CSSDocument’s contents. -* • Must not appear more than once. -*/ -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()};"; - } -} - -/** -* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this class. -*/ -class CSSSelector { - const - NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ - (\.[\w]+) # classes - | - \[(\w+) # attributes - | - (\:( # pseudo classes - link|visited|active - |hover|focus - |lang - |target - |enabled|disabled|checked|indeterminate - |root - |nth-child|nth-last-child|nth-of-type|nth-last-of-type - |first-child|last-child|first-of-type|last-of-type - |only-child|only-of-type - |empty|contains - )) - /ix', - ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ - ((^|[\s\+\>\~]+)[\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/lib/CSSRule.php b/lib/CSSRule.php deleted file mode 100644 index d6ab6ac4..00000000 --- a/lib/CSSRule.php +++ /dev/null @@ -1,135 +0,0 @@ -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/lib/CSSRuleSet.php b/lib/CSSRuleSet.php deleted file mode 100644 index b1b5b1f4..00000000 --- a/lib/CSSRuleSet.php +++ /dev/null @@ -1,657 +0,0 @@ -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; - } -} - -/** -* A CSSRuleSet constructed by an unknown @-rule. @font-face rules are rendered into CSSAtRule objects. -*/ -class CSSAtRule extends CSSRuleSet { - private $sType; - - public function __construct($sType) { - parent::__construct(); - $this->sType = $sType; - } - - public function getType() { - return $this->sType; - } - - public function __toString() { - $sResult = "@{$this->sType} {"; - $sResult .= parent::__toString(); - $sResult .= '}'; - return $sResult; - } -} - -/** -* Declaration blocks are the parts of a css file which denote the rules belonging to a selector. -* Declaration blocks usually appear directly inside a CSSDocument or another CSSList (mostly a CSSMediaQuery). -*/ -class CSSDeclarationBlock extends CSSRuleSet { - - private $aSelectors; - - public function __construct() { - parent::__construct(); - $this->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/lib/CSSValue.php b/lib/CSSValue.php deleted file mode 100644 index 6dc3fb9c..00000000 --- a/lib/CSSValue.php +++ /dev/null @@ -1,110 +0,0 @@ -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); - } -} - -class CSSString extends CSSPrimitiveValue { - 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() { - $sString = addslashes($this->sString); - $sString = str_replace("\n", '\A', $sString); - return '"'.$sString.'"'; - } -} - -class CSSURL extends CSSPrimitiveValue { - 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/lib/CSSValueList.php b/lib/CSSValueList.php deleted file mode 100644 index 35269e23..00000000 --- a/lib/CSSValueList.php +++ /dev/null @@ -1,92 +0,0 @@ -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); - } -} - -class CSSRuleValueList extends CSSValueList { - public function __construct($sSeparator = ',') { - parent::__construct(array(), $sSeparator); - } -} - -class CSSFunction extends CSSValueList { - private $sName; - public function __construct($sName, $aArguments) { - $this->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})"; - } -} - -class CSSColor extends CSSFunction { - public function __construct($aColor) { - parent::__construct(implode('', array_keys($aColor)), $aColor); - } - - public function getColor() { - return $this->aComponents; - } - - public function setColor($aColor) { - $this->setName(implode('', array_keys($aColor))); - $this->aComponents = $aColor; - } - - public function getColorDescription() { - return $this->getName(); - } -} - - diff --git a/lib/Sabberworm/CSS/CSSList/CSSList.php b/lib/Sabberworm/CSS/CSSList/CSSList.php new file mode 100644 index 00000000..3c470207 --- /dev/null +++ b/lib/Sabberworm/CSS/CSSList/CSSList.php @@ -0,0 +1,135 @@ +aContents = array(); + } + + public function append($oItem) { + $this->aContents[] = $oItem; + } + + /** + * Removes an item from the CSS list. + * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery) + */ + 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 DeclarationBlock) { + $mSelector = $mSelector->getSelectors(); + } + if (!is_array($mSelector)) { + $mSelector = explode(',', $mSelector); + } + foreach ($mSelector as $iKey => &$mSel) { + if (!($mSel instanceof Selector)) { + $mSel = new Selector($mSel); + } + } + foreach ($this->aContents as $iKey => $mItem) { + if (!($mItem instanceof DeclarationBlock)) { + continue; + } + if ($mItem->getSelectors() == $mSelector) { + unset($this->aContents[$iKey]); + if (!$bRemoveAll) { + return; + } + } + } + } + + public function __toString() { + $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 DeclarationBlock) { + $aResult[] = $mContent; + } else if ($mContent instanceof CSSList) { + $mContent->allDeclarationBlocks($aResult); + } + } + } + + protected function allRuleSets(&$aResult) { + foreach ($this->aContents as $mContent) { + if ($mContent instanceof RuleSet) { + $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 RuleSet) { + foreach ($oElement->getRules($sSearchString) as $oRule) { + $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } else if ($oElement instanceof Rule) { + $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); + } else if ($oElement instanceof ValueList) { + if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { + foreach ($oElement->getListComponents() as $mComponent) { + $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } + } else { + //Non-List Value or 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/lib/Sabberworm/CSS/CSSList/Document.php b/lib/Sabberworm/CSS/CSSList/Document.php new file mode 100644 index 00000000..28ad5aa4 --- /dev/null +++ b/lib/Sabberworm/CSS/CSSList/Document.php @@ -0,0 +1,87 @@ +allDeclarationBlocks($aResult); + return $aResult; + } + + /** + * @deprecated use getAllDeclarationBlocks() + */ + public function getAllSelectors() { + return $this->getAllDeclarationBlocks(); + } + + /** + * Returns all RuleSet objects found recursively in the tree. + */ + public function getAllRuleSets() { + $aResult = array(); + $this->allRuleSets($aResult); + return $aResult; + } + + /** + * Returns all Value objects found recursively in the tree. + * @param (object|string) $mElement the CSSList or RuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{RuleSet->getRules()}). + * @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. + */ + public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { + $sSearchString = null; + if ($mElement === null) { + $mElement = $this; + } else if (is_string($mElement)) { + $sSearchString = $mElement; + $mElement = $this; + } + $aResult = array(); + $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); + return $aResult; + } + + /** + * Returns all Selector objects found recursively in the tree. + * Note that this does not yield the full DeclarationBlock that the selector belongs to (and, currently, there is no way to get to that). + * @param $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "=="). + * @example getSelectorsBySpecificity('>= 100') + */ + public function getSelectorsBySpecificity($sSpecificitySearch = null) { + 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/lib/Sabberworm/CSS/CSSList/MediaQuery.php b/lib/Sabberworm/CSS/CSSList/MediaQuery.php new file mode 100644 index 00000000..51f524f7 --- /dev/null +++ b/lib/Sabberworm/CSS/CSSList/MediaQuery.php @@ -0,0 +1,32 @@ +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; + } + +} diff --git a/lib/Sabberworm/CSS/Parser.php b/lib/Sabberworm/CSS/Parser.php new file mode 100644 index 00000000..bc6767b7 --- /dev/null +++ b/lib/Sabberworm/CSS/Parser.php @@ -0,0 +1,504 @@ +sText = $sText; + $this->iCurrentPosition = 0; + $this->setCharset($sDefaultCharset); + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + $this->iLength = $this->strlen($this->sText); + } + + public function getCharset() { + return $this->sCharset; + } + + public function parse() { + $oResult = new Document(); + $this->parseDocument($oResult); + return $oResult; + } + + public function setUseMbFlag($bFlag) { + $this->bUseMbFunctions = (bool) $bFlag; + } + + private function parseDocument(Document $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 MediaQuery(); + $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 Import($oLocation, $sMediaQuery); + } else if ($sIdentifier === 'charset') { + $sCharset = $this->parseStringValue(); + $this->consumeWhiteSpace(); + $this->consume(';'); + $this->setCharset($sCharset->getString()); + return new Charset($sCharset); + } else { + //Unknown other at rule (font-face or such) + $this->consume('{'); + $this->consumeWhiteSpace(); + $oAtRule = new AtRule($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 String($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 ($this->strlen($sUnicode) < 6) { + //Consume whitespace after incomplete unicode escape + if (preg_match('/\\s/isSu', $this->peek())) { + if ($this->comes('\r\n')) { + $this->consume(2); + } else { + $this->consume(1); + } + } + } + $iUnicode = intval($sUnicode, 16); + $sUtf32 = ""; + for ($i = 0; $i < 4; $i++) { + $sUtf32 .= chr($iUnicode & 0xff); + $iUnicode = $iUnicode >> 8; + } + return iconv('utf-32le', $this->sCharset, $sUtf32); + } + if ($bIsForIdentifier) { + 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 DeclarationBlock(); + $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 Rule($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; $i < count($aStack); $i+=2) { + if ($sDelimiter !== $aStack[$i]) { + break; + } + $iLength++; + } + $oList = new RuleValueList($sDelimiter); + for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) { + $oList->addListComponent($aStack[$i]); + } + array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList)); + } + } + 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 Size($fSize, $sUnit, $bForColor); + } + + private function parseColorValue() { + $aColor = array(); + if ($this->comes('#')) { + $this->consume('#'); + $sValue = $this->parseIdentifier(false); + if ($this->strlen($sValue) === 3) { + $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; + } + $aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true)); + } else { + $sColorMode = $this->parseIdentifier(false); + $this->consumeWhiteSpace(); + $this->consume('('); + $iLength = $this->strlen($sColorMode); + 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 Color($aColor); + } + + private function parseURLValue() { + $bUseUrl = $this->comes('url'); + if ($bUseUrl) { + $this->consume('url'); + $this->consumeWhiteSpace(); + $this->consume('('); + } + $this->consumeWhiteSpace(); + $oResult = new URL($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 = $this->strlen($iLength); + } + if (is_string($iOffset)) { + $iOffset = $this->strlen($iOffset); + } + return $this->substr($this->sText, $this->iCurrentPosition + $iOffset, $iLength); + } + + private function consume($mValue = 1) { + if (is_string($mValue)) { + $iLength = $this->strlen($mValue); + if ($this->substr($this->sText, $this->iCurrentPosition, $iLength) !== $mValue) { + throw new \Exception("Expected $mValue, got " . $this->peek(5)); + } + $this->iCurrentPosition += $this->strlen($mValue); + return $mValue; + } else { + if ($this->iCurrentPosition + $mValue > $this->iLength) { + throw new \Exception("Tried to consume $mValue chars, exceeded file end"); + } + $sResult = $this->substr($this->sText, $this->iCurrentPosition, $mValue); + $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 $this->substr($this->sText, $this->iCurrentPosition, -1); + } + + private function substr($string, $start, $length) { + if ($this->bUseMbFunctions) { + return mb_substr($string, $start, $length, $this->sCharset); + } else { + return substr($string, $start, $length); + } + } + + private function strlen($text) { + if ($this->bUseMbFunctions) { + return mb_strlen($text, $this->sCharset); + } else { + return strlen($text); + } + } + +} + diff --git a/lib/Sabberworm/CSS/Property/Charset.php b/lib/Sabberworm/CSS/Property/Charset.php new file mode 100644 index 00000000..1fb31a22 --- /dev/null +++ b/lib/Sabberworm/CSS/Property/Charset.php @@ -0,0 +1,32 @@ +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/lib/Sabberworm/CSS/Property/Import.php b/lib/Sabberworm/CSS/Property/Import.php new file mode 100644 index 00000000..7a303471 --- /dev/null +++ b/lib/Sabberworm/CSS/Property/Import.php @@ -0,0 +1,30 @@ +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).';'; + } +} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php new file mode 100644 index 00000000..60205827 --- /dev/null +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -0,0 +1,75 @@ +\~]+)[\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; + } + +} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Rule/Rule.php b/lib/Sabberworm/CSS/Rule/Rule.php new file mode 100644 index 00000000..d0a3f4c0 --- /dev/null +++ b/lib/Sabberworm/CSS/Rule/Rule.php @@ -0,0 +1,142 @@ +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 RuleValueList if necessary. + */ + public function setValues($aSpaceSeparatedValues) { + $oSpaceSeparatedList = null; + if (count($aSpaceSeparatedValues) > 1) { + $oSpaceSeparatedList = new RuleValueList(' '); + } + foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) { + $oCommaSeparatedList = null; + if (count($aCommaSeparatedValues) > 1) { + $oCommaSeparatedList = new RuleValueList(','); + } + foreach ($aCommaSeparatedValues as $mValue) { + if (!$oSpaceSeparatedList && !$oCommaSeparatedList) { + $this->mValue = $mValue; + return $mValue; + } + if ($oCommaSeparatedList) { + $oCommaSeparatedList->addListComponent($mValue); + } else { + $oSpaceSeparatedList->addListComponent($mValue); + } + } + if (!$oSpaceSeparatedList) { + $this->mValue = $oCommaSeparatedList; + return $oCommaSeparatedList; + } else { + $oSpaceSeparatedList->addListComponent($oCommaSeparatedList); + } + } + $this->mValue = $oSpaceSeparatedList; + return $oSpaceSeparatedList; + } + + /** + * @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s). + */ + public function getValues() { + if (!$this->mValue instanceof RuleValueList) { + return array(array($this->mValue)); + } + if ($this->mValue->getListSeparator() === ',') { + return array($this->mValue->getListComponents()); + } + $aResult = array(); + foreach ($this->mValue->getListComponents() as $mValue) { + if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { + $aResult[] = array($mValue); + continue; + } + if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { + $aResult[] = array(); + } + foreach ($mValue->getListComponents() as $mValue) { + $aResult[count($aResult) - 1][] = $mValue; + } + } + return $aResult; + } + + /** + * Adds a value to the existing value. Value will be appended if a RuleValueList exists of the given type. Otherwise, the existing value will be wrapped by one. + */ + public function addValue($mValue, $sType = ' ') { + if (!is_array($mValue)) { + $mValue = array($mValue); + } + if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { + $mCurrentValue = $this->mValue; + $this->mValue = new RuleValueList($sType); + 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 Value) { //Can also be a ValueList + $sResult .= $this->mValue->__toString(); + } else { + $sResult .= $this->mValue; + } + if ($this->bIsImportant) { + $sResult .= ' !important'; + } + $sResult .= ';'; + return $sResult; + } + +} diff --git a/lib/Sabberworm/CSS/RuleSet/AtRule.php b/lib/Sabberworm/CSS/RuleSet/AtRule.php new file mode 100644 index 00000000..4c2ff837 --- /dev/null +++ b/lib/Sabberworm/CSS/RuleSet/AtRule.php @@ -0,0 +1,28 @@ +sType = $sType; + } + + public function getType() { + return $this->sType; + } + + public function __toString() { + $sResult = "@{$this->sType} {"; + $sResult .= parent::__toString(); + $sResult .= '}'; + return $sResult; + } + +} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php new file mode 100644 index 00000000..af343670 --- /dev/null +++ b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -0,0 +1,585 @@ +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 Selector)) { + $this->aSelectors[$iKey] = new Selector($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 3 + * */ + 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 RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if ($mValue instanceof Value) { + $mNewValue = clone $mValue; + } else { + $mNewValue = $mValue; + } + if ($mValue instanceof Size) { + $sNewRuleName = $sBorderRule . "-width"; + } else if ($mValue instanceof Color) { + $sNewRuleName = $sBorderRule . "-color"; + } else { + if (in_array($mValue, $aBorderSizes)) { + $sNewRuleName = $sBorderRule . "-width"; + } else/* if(in_array($mValue, $aBorderStyles)) */ { + $sNewRuleName = $sBorderRule . "-style"; + } + } + $oNewRule = new Rule($sNewRuleName); + $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 RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + $top = $right = $bottom = $left = null; + switch (count($aValues)) { + case 1: + $top = $right = $bottom = $left = $aValues[0]; + break; + case 2: + $top = $bottom = $aValues[0]; + $left = $right = $aValues[1]; + break; + case 3: + $top = $aValues[0]; + $left = $right = $aValues[1]; + $bottom = $aValues[2]; + break; + case 4: + $top = $aValues[0]; + $right = $aValues[1]; + $bottom = $aValues[2]; + $left = $aValues[3]; + break; + } + foreach (array('top', 'right', 'bottom', 'left') as $sPosition) { + $oNewRule = new Rule(sprintf($sExpanded, $sPosition)); + $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/21/fonts.html#font-shorthand + $aFontProperties = array( + 'font-style' => 'normal', + 'font-variant' => 'normal', + 'font-weight' => 'normal', + 'font-size' => 'normal', + 'line-height' => 'normal' + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if (in_array($mValue, array('normal', 'inherit'))) { + foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) { + if (!isset($aFontProperties[$sProperty])) { + $aFontProperties[$sProperty] = $mValue; + } + } + } else if (in_array($mValue, array('italic', 'oblique'))) { + $aFontProperties['font-style'] = $mValue; + } else if ($mValue == 'small-caps') { + $aFontProperties['font-variant'] = $mValue; + } else if ( + in_array($mValue, array('bold', 'bolder', 'lighter')) + || ($mValue instanceof Size + && in_array($mValue->getSize(), range(100, 900, 100))) + ) { + $aFontProperties['font-weight'] = $mValue; + } else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { + list($oSize, $oHeight) = $mValue->getListComponents(); + $aFontProperties['font-size'] = $oSize; + $aFontProperties['line-height'] = $oHeight; + } else if ($mValue instanceof Size && $mValue->getUnit() !== null) { + $aFontProperties['font-size'] = $mValue; + } else { + $aFontProperties['font-family'] = $mValue; + } + } + foreach ($aFontProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->addValue($mValue); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('font'); + } + + /* + * Convert shorthand background declarations + * (e.g. background: url("chess.png") gray 50% repeat fixed;) + * into their constituent parts. + * @see http://www.w3.org/TR/21/colors.html#propdef-background + * */ + + 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 Size(0, '%'), new Size(0, '%')) + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if (count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + return; + } + $iNumBgPos = 0; + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof URL) { + $aBgProperties['background-image'] = $mValue; + } else if ($mValue instanceof Color) { + $aBgProperties['background-color'] = $mValue; + } else if (in_array($mValue, array('scroll', 'fixed'))) { + $aBgProperties['background-attachment'] = $mValue; + } else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) { + $aBgProperties['background-repeat'] = $mValue; + } else if (in_array($mValue, array('left', 'center', 'right', 'top', 'bottom')) + || $mValue instanceof Size + ) { + if ($iNumBgPos == 0) { + $aBgProperties['background-position'][0] = $mValue; + $aBgProperties['background-position'][1] = 'center'; + } else { + $aBgProperties['background-position'][$iNumBgPos] = $mValue; + } + $iNumBgPos++; + } + } + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $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 RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if (count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + return; + } + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof Url) { + $aListProperties['list-style-image'] = $mValue; + } else if (in_array($mValue, $aListStyleTypes)) { + $aListProperties['list-style-types'] = $mValue; + } else if (in_array($mValue, $aListStylePositions)) { + $aListProperties['list-style-position'] = $mValue; + } + } + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $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 RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + $aNewValues[] = $mValue; + } + $this->removeRule($sProperty); + } + } + if (count($aNewValues)) { + $oNewRule = new Rule($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 RuleValueList) { + $aRuleValues[] = $mRuleValue; + } else { + $aRuleValues = $mRuleValue->getListComponents(); + } + $aValues[$sPosition] = $aRuleValues; + } + $oNewRule = new Rule($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 Rule('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 RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if ($aValues[0] !== 'normal') { + $oNewRule->addValue($aValues[0]); + } + } + } + // Get the font-size value + $oRule = $aRules['font-size']; + $mRuleValue = $oRule->getValue(); + $aFSValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aFSValues[] = $mRuleValue; + } else { + $aFSValues = $mRuleValue->getListComponents(); + } + // But wait to know if we have line-height to add it + if (isset($aRules['line-height'])) { + $oRule = $aRules['line-height']; + $mRuleValue = $oRule->getValue(); + $aLHValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aLHValues[] = $mRuleValue; + } else { + $aLHValues = $mRuleValue->getListComponents(); + } + if ($aLHValues[0] !== 'normal') { + $val = new RuleValueList('/'); + $val->addListComponent($aFSValues[0]); + $val->addListComponent($aLHValues[0]); + $oNewRule->addValue($val); + } + } else { + $oNewRule->addValue($aFSValues[0]); + } + $oRule = $aRules['font-family']; + $mRuleValue = $oRule->getValue(); + $aFFValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aFFValues[] = $mRuleValue; + } else { + $aFFValues = $mRuleValue->getListComponents(); + } + $oFFValue = new RuleValueList(','); + $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/lib/Sabberworm/CSS/RuleSet/RuleSet.php b/lib/Sabberworm/CSS/RuleSet/RuleSet.php new file mode 100644 index 00000000..56863134 --- /dev/null +++ b/lib/Sabberworm/CSS/RuleSet/RuleSet.php @@ -0,0 +1,74 @@ +aRules = array(); + } + + public function addRule(Rule $oRule) { + $this->aRules[$oRule->getRule()] = $oRule; + } + + /** + * Returns all rules matching the given pattern + * @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()). + * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font. + * @example $oRuleSet->getRules('font') //returns array('font' => $oRule) or array(). + */ + public function getRules($mRule = null) { + if ($mRule === null) { + return $this->aRules; + } + $aResult = array(); + if ($mRule instanceof Rule) { + $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 Rule) { + $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/lib/Sabberworm/CSS/Value/CSSFunction.php b/lib/Sabberworm/CSS/Value/CSSFunction.php new file mode 100644 index 00000000..59c21167 --- /dev/null +++ b/lib/Sabberworm/CSS/Value/CSSFunction.php @@ -0,0 +1,31 @@ +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/lib/Sabberworm/CSS/Value/Color.php b/lib/Sabberworm/CSS/Value/Color.php new file mode 100644 index 00000000..0156f323 --- /dev/null +++ b/lib/Sabberworm/CSS/Value/Color.php @@ -0,0 +1,24 @@ +aComponents; + } + + public function setColor($aColor) { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + public function getColorDescription() { + return $this->getName(); + } + +} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Value/PrimitiveValue.php b/lib/Sabberworm/CSS/Value/PrimitiveValue.php new file mode 100644 index 00000000..9d61954a --- /dev/null +++ b/lib/Sabberworm/CSS/Value/PrimitiveValue.php @@ -0,0 +1,7 @@ +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 Size really represents a size (as in a length of something on screen). + * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. + */ + public function isSize() { + $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/lib/Sabberworm/CSS/Value/String.php b/lib/Sabberworm/CSS/Value/String.php new file mode 100644 index 00000000..b3e8b873 --- /dev/null +++ b/lib/Sabberworm/CSS/Value/String.php @@ -0,0 +1,27 @@ +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/lib/Sabberworm/CSS/Value/URL.php b/lib/Sabberworm/CSS/Value/URL.php new file mode 100644 index 00000000..731f7243 --- /dev/null +++ b/lib/Sabberworm/CSS/Value/URL.php @@ -0,0 +1,26 @@ +oURL = $oURL; + } + + public function setURL(String $oURL) { + $this->oURL = $oURL; + } + + public function getURL() { + return $this->oURL; + } + + public function __toString() { + return "url({$this->oURL->__toString()})"; + } + +} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Value/Value.php b/lib/Sabberworm/CSS/Value/Value.php new file mode 100644 index 00000000..77a0e640 --- /dev/null +++ b/lib/Sabberworm/CSS/Value/Value.php @@ -0,0 +1,8 @@ +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/tests/CSSDeclarationBlockTest.php b/tests/CSSDeclarationBlockTest.php deleted file mode 100644 index 0d311c7e..00000000 --- a/tests/CSSDeclarationBlockTest.php +++ /dev/null @@ -1,223 +0,0 @@ -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/tests/CSSParserTests.php b/tests/CSSParserTests.php deleted file mode 100644 index d42027b7..00000000 --- a/tests/CSSParserTests.php +++ /dev/null @@ -1,283 +0,0 @@ -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 testPrefixedGradient() { - $oDoc = $this->parsedStructureForFile('webkit'); - $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}'."\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/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php new file mode 100644 index 00000000..c46522e8 --- /dev/null +++ b/tests/Sabberworm/CSS/ParserTest.php @@ -0,0 +1,286 @@ +assertNotEquals('', $oParser->parse()->__toString()); + } catch (\Exception $e) { + $this->fail($e); + } + } + closedir($rHandle); + } + } + + /** + * @depends testFiles + */ + function testColorParsing() { + $oDoc = $this->parsedStructureForFile('colortest'); + foreach ($oDoc->getAllRuleSets() as $oRuleSet) { + if (!$oRuleSet instanceof DeclarationBlock) { + continue; + } + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if ($sSelector == '#mine') { + $aColorRule = $oRuleSet->getRules('color'); + $aValues = $aColorRule['color']->getValues(); + $this->assertSame('red', $aValues[0][0]); + $aColorRule = $oRuleSet->getRules('background-'); + $aValues = $aColorRule['background-color']->getValues(); + $this->assertEquals(array('r' => new Size(35.0, null, true), 'g' => new Size(35.0, null, true), 'b' => new Size(35.0, null, true)), $aValues[0][0]->getColor()); + $aColorRule = $oRuleSet->getRules('border-color'); + $aValues = $aColorRule['border-color']->getValues(); + $this->assertEquals(array('r' => new Size(10.0, null, true), 'g' => new Size(100.0, null, true), 'b' => new Size(230.0, null, true), 'a' => new Size(0.3, null, true)), $aValues[0][0]->getColor()); + $aColorRule = $oRuleSet->getRules('outline-color'); + $aValues = $aColorRule['outline-color']->getValues(); + $this->assertEquals(array('r' => new Size(34.0, null, true), 'g' => new Size(34.0, null, true), 'b' => new Size(34.0, null, true)), $aValues[0][0]->getColor()); + } + } + foreach ($oDoc->getAllValues('background-') as $oColor) { + if ($oColor->getColorDescription() === 'hsl') { + $this->assertEquals(array('h' => new Size(220.0, null, true), 's' => new Size(10.0, null, true), 'l' => new Size(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(); + $sString = $aContents[0][0]->__toString(); + if ($sSelector == '.test-1') { + $this->assertSame('" "', $sString); + } + if ($sSelector == '.test-2') { + $this->assertSame('"é"', $sString); + } + if ($sSelector == '.test-3') { + $this->assertSame('" "', $sString); + } + if ($sSelector == '.test-4') { + $this->assertSame('"𝄞"', $sString); + } + if ($sSelector == '.test-5') { + $this->assertSame('"水"', $sString); + } + if ($sSelector == '.test-6') { + $this->assertSame('"¥"', $sString); + } + if ($sSelector == '.test-7') { + $this->assertSame('"\A"', $sString); + } + if ($sSelector == '.test-8') { + $this->assertSame('"\"\""', $sString); + } + if ($sSelector == '.test-9') { + $this->assertSame('"\"\\\'"', $sString); + } + if ($sSelector == '.test-10') { + $this->assertSame('"\\\'\\\\"', $sString); + } + if ($sSelector == '.test-11') { + $this->assertSame('"test"', $sString); + } + } + } + + function testSpecificity() { + $oDoc = $this->parsedStructureForFile('specificity'); + $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); + $oDeclarationBlock = $oDeclarationBlock[0]; + $aSelectors = $oDeclarationBlock->getSelectors(); + foreach ($aSelectors as $oSelector) { + switch ($oSelector->getSelector()) { + case "#test .help": + $this->assertSame(110, $oSelector->getSpecificity()); + break; + case "#file": + $this->assertSame(100, $oSelector->getSpecificity()); + break; + case ".help:hover": + $this->assertSame(20, $oSelector->getSpecificity()); + break; + case "ol li::before": + $this->assertSame(3, $oSelector->getSpecificity()); + break; + case "li.green": + $this->assertSame(11, $oSelector->getSpecificity()); + break; + default: + $this->fail("specificity: untested selector " . $oSelector->getSelector()); + } + } + $this->assertEquals(array(new Selector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100')); + } + + 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 Size && $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 Size && $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 Size && !$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 testPrefixedGradient() { + $oDoc = $this->parsedStructureForFile('webkit'); + $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}' . "\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testListValueRemoval() { + $oDoc = $this->parsedStructureForFile('atrules'); + foreach ($oDoc->getContents() as $oItem) { + if ($oItem instanceof AtRule) { + $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__) . '/../../files' . DIRECTORY_SEPARATOR . "$sFileName.css"; + $oParser = new Parser(file_get_contents($sFile)); + return $oParser->parse(); + } + +} diff --git a/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php b/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php new file mode 100644 index 00000000..48b529f9 --- /dev/null +++ b/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php @@ -0,0 +1,208 @@ +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 Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandFontShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function expandFontShorthandProvider() { + return array( + array( + 'body{ margin: 1em; }', + 'body {margin: 1em;}' + ), + array( + 'body {font: 12px serif;}', + 'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' + ), + array( + 'body {font: italic 12px serif;}', + 'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' + ), + array( + 'body {font: italic bold 12px serif;}', + 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: normal;font-family: serif;}' + ), + array( + 'body {font: italic bold 12px/1.6 serif;}', + 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}' + ), + array( + 'body {font: italic small-caps bold 12px/1.6 serif;}', + 'body {font-style: italic;font-variant: small-caps;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}' + ), + ); + } + + /** + * @dataProvider expandBackgroundShorthandProvider + * */ + public function testExpandBackgroundShorthand($sCss, $sExpected) { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandBackgroundShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function expandBackgroundShorthandProvider() { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {background: 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 Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandDimensionsShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function expandDimensionsShorthandProvider() { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), + array('body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'), + array('body {margin: 1em 2em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}'), + array('body {margin: 1em 2em 3em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}'), + ); + } + + /** + * @dataProvider createBorderShorthandProvider + * */ + public function testCreateBorderShorthand($sCss, $sExpected) { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createBorderShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createBorderShorthandProvider() { + return array( + array('body {border-width: 2px;border-style: solid;border-color: 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 Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createFontShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createFontShorthandProvider() { + return array( + array('body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}', 'body {font: italic bold 12px serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6;}', 'body {font: italic bold 12px/1.6 serif;}'), + array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6; font-variant: small-caps;}', 'body {font: italic small-caps bold 12px/1.6 serif;}'), + array('body {margin: 1em;}', 'body {margin: 1em;}') + ); + } + + /** + * @dataProvider createDimensionsShorthandProvider + * */ + public function testCreateDimensionsShorthand($sCss, $sExpected) { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createDimensionsShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createDimensionsShorthandProvider() { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), + array('body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'), + array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}', 'body {margin: 1em 2em;}'), + array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}', 'body {margin: 1em 2em 3em;}'), + ); + } + + /** + * @dataProvider createBackgroundShorthandProvider + * */ + public function testCreateBackgroundShorthand($sCss, $sExpected) { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createBackgroundShorthand(); + } + $this->assertSame(trim((string) $oDoc), $sExpected); + } + + public function createBackgroundShorthandProvider() { + return array( + array('body {border: 1px;}', 'body {border: 1px;}'), + array('body {background-color: 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/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..7c4de814 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,10 @@ +parse();