From 361036a2716976d5b84ded214b03dc0abc110386 Mon Sep 17 00:00:00 2001 From: Kristof Date: Tue, 15 Mar 2011 23:13:05 +0100 Subject: [PATCH 1/8] handle Unicode strings correctly --- CSSParser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CSSParser.php b/CSSParser.php index 77eb3faf..af6a8c5d 100644 --- a/CSSParser.php +++ b/CSSParser.php @@ -363,8 +363,8 @@ private function consume($mValue = 1) { private function consumeExpression($mExpression) { $aMatches; - if(preg_match($mExpression, $this->inputLeft(), $aMatches) === 1) { - if($aMatches[0][1] === $this->iCurrentPosition) { + if(preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) { + if($aMatches[0][1] === 0) { return $this->consume($aMatches[0][0]); } } From ab9a58b8f426beee7c1b41fe12bee0f3faf5ba04 Mon Sep 17 00:00:00 2001 From: Kristof Date: Thu, 17 Mar 2011 01:26:10 +0100 Subject: [PATCH 2/8] handle Unicode strings correctly --- CSSParser.php | 180 +++++++++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 82 deletions(-) diff --git a/CSSParser.php b/CSSParser.php index 77eb3faf..8b1ade9d 100644 --- a/CSSParser.php +++ b/CSSParser.php @@ -4,17 +4,17 @@ * @package html * CSSParser class parses CSS from text into a data structure. */ -class CSSParser { +class CSSParser { private $sText; private $iCurrentPosition; private $iLength; - + public function __construct($sText, $sDefaultCharset = 'utf-8') { $this->sText = $sText; $this->iCurrentPosition = 0; $this->setCharset($sDefaultCharset); } - + public function setCharset($sCharset) { $this->sCharset = $sCharset; $this->iLength = mb_strlen($this->sText, $this->sCharset); @@ -23,18 +23,18 @@ public function setCharset($sCharset) { public function getCharset() { return $this->sCharset; } - + public function parse() { $oResult = new CSSDocument(); $this->parseDocument($oResult); return $oResult; } - + private function parseDocument(CSSDocument $oDocument) { $this->consumeWhiteSpace(); $this->parseList($oDocument, true); } - + private function parseList(CSSList $oList, $bIsRoot = false) { while(!$this->isEnd()) { if($this->comes('@')) { @@ -55,7 +55,7 @@ private function parseList(CSSList $oList, $bIsRoot = false) { throw new Exception("Unexpected end of document"); } } - + private function parseAtRule() { $this->consume('@'); $sIdentifier = $this->parseIdentifier(); @@ -91,7 +91,7 @@ private function parseAtRule() { return $oAtRule; } } - + private function parseIdentifier() { $sResult = $this->parseCharacter(true); if($sResult === null) { @@ -103,7 +103,7 @@ private function parseIdentifier() { } return $sResult; } - + private function parseStringValue() { $sBegin = $this->peek(); $sQuote = null; @@ -134,14 +134,13 @@ private function parseStringValue() { } 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); } @@ -160,10 +159,27 @@ private function parseCharacter($bIsForIdentifier) { if((strlen($sUnicode) % 2) === 1) { $sUnicode = "0$sUnicode"; } - for($i=0;$i 0xFFFF) { + $v_bis = $v - 0x10000; + $vh = $v_bis >> 10; // higher 10 bits of $v_bis + $vl = $v_bis & 0x3FF; // lower 10 bits of $v_bis + $w1 = 0xD800 + $vh; // first code unit of UTF-16 encoding + $w2 = 0xDC00 + $vl; // second code unit of UTF-16 encoding + + $sUnicode = str_pad(dechex($w1), 4, "0", STR_PAD_LEFT) . str_pad(dechex($w2), 4, "0", STR_PAD_LEFT); } - return iconv('utf-16', $this->sCharset, $sUtf16); + + $sUtf16 = pack("H*" , $sUnicode); + + $result = iconv('utf-16', $this->sCharset, $sUtf16); + return $result; } if($bIsForIdentifier) { if(preg_match('/[a-zA-Z0-9]|-|_/u', $this->peek()) === 1) { @@ -179,7 +195,7 @@ private function parseCharacter($bIsForIdentifier) { // Does not reach here return null; } - + private function parseSelector() { $oResult = new CSSSelector(); $oResult->setSelector($this->consumeUntil('{')); @@ -188,7 +204,7 @@ private function parseSelector() { $this->parseRuleSet($oResult); return $oResult; } - + private function parseRuleSet($oRuleSet) { while(!$this->comes('}')) { $oRuleSet->addRule($this->parseRule()); @@ -196,7 +212,7 @@ private function parseRuleSet($oRuleSet) { } $this->consume('}'); } - + private function parseRule() { $oRule = new CSSRule($this->parseIdentifier()); $this->consumeWhiteSpace(); @@ -220,7 +236,7 @@ private function parseRule() { } return $oRule; } - + private function parseValue() { $aResult = array(); do { @@ -238,10 +254,10 @@ private function parseValue() { } $this->consumeWhiteSpace(); } while($this->comes(',') && is_string($this->consume(','))); - + return $aResult; } - + private function parseNumericValue() { $sSize = ''; if($this->comes('-')) { @@ -279,7 +295,7 @@ private function parseNumericValue() { } return new CSSSize($fSize, $sUnit); } - + private function parseColorValue() { $aColor = array(); if($this->comes('#')) { @@ -306,7 +322,7 @@ private function parseColorValue() { } return new CSSColor($aColor); } - + private function parseURLValue() { $bUseUrl = $this->comes('url'); if($bUseUrl) { @@ -322,14 +338,14 @@ private function parseURLValue() { } 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 ''; @@ -342,7 +358,7 @@ private function peek($iLength = 1, $iOffset = 0) { } return mb_substr($this->sText, $this->iCurrentPosition+$iOffset, $iLength, $this->sCharset); } - + private function consume($mValue = 1) { if(is_string($mValue)) { $iLength = mb_strlen($mValue, $this->sCharset); @@ -360,17 +376,17 @@ private function consume($mValue = 1) { return $sResult; } } - + private function consumeExpression($mExpression) { $aMatches; - if(preg_match($mExpression, $this->inputLeft(), $aMatches) === 1) { - if($aMatches[0][1] === $this->iCurrentPosition) { + if(preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) { + if($aMatches[0][1] === 0) { 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) { @@ -378,7 +394,7 @@ private function consumeWhiteSpace() { } } while($this->consumeComment()); } - + private function consumeComment() { if($this->comes('/*')) { $this->consumeUntil('*/'); @@ -387,11 +403,11 @@ private function consumeComment() { } 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) { @@ -399,7 +415,7 @@ private function consumeUntil($sEnd) { } return $this->consume($iEndPos-$this->iCurrentPosition); } - + private function inputLeft() { return mb_substr($this->sText, $this->iCurrentPosition, -1, $this->sCharset); } @@ -407,15 +423,15 @@ private function inputLeft() { abstract class CSSList { private $aContents; - + public function __construct() { $this->aContents = array(); } - + public function append($oItem) { $this->aContents[] = $oItem; } - + public function __toString() { $sResult = ''; foreach($this->aContents as $oContent) { @@ -423,11 +439,11 @@ public function __toString() { } return $sResult; } - + public function getContents() { return $this->aContents; } - + protected function allSelectors(&$aResult) { foreach($this->aContents as $mContent) { if($mContent instanceof CSSSelector) { @@ -437,7 +453,7 @@ protected function allSelectors(&$aResult) { } } } - + protected function allRuleSets(&$aResult) { foreach($this->aContents as $mContent) { if($mContent instanceof CSSRuleSet) { @@ -447,7 +463,7 @@ protected function allRuleSets(&$aResult) { } } } - + protected function allValues($oElement, &$aResult, $sSearchString = null) { if($oElement instanceof CSSList) { foreach($oElement->getContents() as $oContent) { @@ -473,13 +489,13 @@ public function getAllSelectors() { $this->allSelectors($aResult); return $aResult; } - + public function getAllRuleSets() { $aResult = array(); $this->allRuleSets($aResult); return $aResult; } - + public function getAllValues($mElement = null) { $sSearchString = null; if($mElement === null) { @@ -496,12 +512,12 @@ public function getAllValues($mElement = null) { class CSSMediaQuery extends CSSList { private $sQuery; - + public function __construct() { parent::__construct(); $this->sQuery = null; } - + public function setQuery($sQuery) { $this->sQuery = $sQuery; } @@ -509,7 +525,7 @@ public function setQuery($sQuery) { public function getQuery() { return $this->sQuery; } - + public function __toString() { $sResult = "@media {$this->sQuery} {"; $sResult .= parent::__toString(); @@ -521,12 +537,12 @@ public function __toString() { class CSSImport { private $oLocation; private $sMediaQuery; - + public function __construct(CSSURL $oLocation, $sMediaQuery) { $this->oLocation = $oLocation; $this->sMediaQuery = $sMediaQuery; } - + public function setLocation($oLocation) { $this->oLocation = $oLocation; } @@ -534,7 +550,7 @@ public function setLocation($oLocation) { public function getLocation() { return $this->oLocation; } - + public function __toString() { return "@import ".$this->oLocation->__toString().($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; } @@ -542,11 +558,11 @@ public function __toString() { class CSSCharset { private $sCharset; - + public function __construct($sCharset) { $this->sCharset = $sCharset; } - + public function setCharset($sCharset) { $this->sCharset = $sCharset; } @@ -554,7 +570,7 @@ public function setCharset($sCharset) { public function getCharset() { return $this->sCharset; } - + public function __toString() { return "@charset {$this->sCharset->__toString()};"; } @@ -562,15 +578,15 @@ public function __toString() { abstract class CSSRuleSet { private $aRules; - + public function __construct() { $this->aRules = array(); } - + public function addRule(CSSRule $oRule) { $this->aRules[$oRule->getRule()] = $oRule; } - + public function getRules($mRule = null) { if($mRule === null) { return $this->aRules; @@ -591,7 +607,7 @@ public function getRules($mRule = null) { } return $aResult; } - + public function removeRule($mRule) { if($mRule instanceof CSSRule) { $mRule = $mRule->getRule(); @@ -607,7 +623,7 @@ public function removeRule($mRule) { unset($this->aRules[$mRule]); } } - + public function __toString() { $sResult = ''; foreach($this->aRules as $oRule) { @@ -619,12 +635,12 @@ public function __toString() { class CSSAtRule extends CSSRuleSet { private $sType; - + public function __construct($sType) { parent::__construct(); $this->sType = $sType; } - + public function __toString() { $sResult = "@{$this->sType} {"; $sResult .= parent::__toString(); @@ -635,12 +651,12 @@ public function __toString() { class CSSSelector extends CSSRuleSet { private $aSelector; - + public function __construct() { parent::__construct(); $this->aSelector = array(); } - + public function setSelector($mSelector) { if(is_array($mSelector)) { $this->aSelector = $mSelector; @@ -651,11 +667,11 @@ public function setSelector($mSelector) { $this->aSelector[$iKey] = trim($sSelector); } } - + public function getSelector() { return $this->aSelector; } - + public function __toString() { $sResult = implode(', ', $this->aSelector).' {'; $sResult .= parent::__toString(); @@ -668,12 +684,12 @@ class CSSRule { private $sRule; private $aValues; private $bIsImportant; - + public function __construct($sRule) { $this->sRule = $sRule; $this->bIsImportant = false; } - + public function setRule($sRule) { $this->sRule = $sRule; } @@ -681,11 +697,11 @@ public function setRule($sRule) { public function getRule() { return $this->sRule; } - + public function addValue($mValue) { $this->aValues[] = $mValue; } - + public function setValues($aValues) { $this->aValues = $aValues; } @@ -693,7 +709,7 @@ public function setValues($aValues) { public function getValues() { return $this->aValues; } - + public function setIsImportant($bIsImportant) { $this->bIsImportant = $bIsImportant; } @@ -723,12 +739,12 @@ public abstract function __toString(); class CSSSize extends CSSValue { private $fSize; private $sUnit; - + public function __construct($fSize, $sUnit = null) { $this->fSize = floatval($fSize); $this->sUnit = $sUnit; } - + public function setUnit($sUnit) { $this->sUnit = $sUnit; } @@ -736,7 +752,7 @@ public function setUnit($sUnit) { public function getUnit() { return $this->sUnit; } - + public function setSize($fSize) { $this->fSize = floatval($fSize); } @@ -744,7 +760,7 @@ public function setSize($fSize) { public function getSize() { return $this->fSize; } - + public function isRelative() { if($this->sUnit === '%' || $this->sUnit === 'em' || $this->sUnit === 'ex') { return true; @@ -754,7 +770,7 @@ public function isRelative() { } return false; } - + public function __toString() { return $this->fSize.($this->sUnit === null ? '' : $this->sUnit); } @@ -762,11 +778,11 @@ public function __toString() { class CSSColor extends CSSValue { private $aColor; - + public function __construct($aColor) { $this->aColor = $aColor; } - + public function setColor($aColor) { $this->aColor = $aColor; } @@ -774,11 +790,11 @@ public function setColor($aColor) { public function getColor() { return $this->aColor; } - + public function getColorDescription() { return implode('', array_keys($this->aColor)); } - + public function __toString() { return $this->getColorDescription().'('.implode(', ', $this->aColor).')'; } @@ -786,11 +802,11 @@ public function __toString() { class CSSString extends CSSValue { private $sString; - + public function __construct($sString) { $this->sString = $sString; } - + public function setString($sString) { $this->sString = $sString; } @@ -798,7 +814,7 @@ public function setString($sString) { public function getString() { return $this->sString; } - + public function __toString() { return '"'.addslashes($this->sString).'"'; } @@ -806,11 +822,11 @@ public function __toString() { class CSSURL extends CSSValue { private $oURL; - + public function __construct(CSSString $oURL) { $this->oURL = $oURL; } - + public function setURL(CSSString $oURL) { $this->oURL = $oURL; } @@ -818,7 +834,7 @@ public function setURL(CSSString $oURL) { public function getURL() { return $this->oURL; } - + public function __toString() { return "url({$this->oURL->__toString()})"; } From e5ad54b6ffd985927f1011dab525dbc2c8eaf583 Mon Sep 17 00:00:00 2001 From: Kristof Date: Thu, 17 Mar 2011 12:09:59 +0100 Subject: [PATCH 3/8] handle unicode character escapes properly --- CSSParser.php | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/CSSParser.php b/CSSParser.php index 8b1ade9d..b22e466c 100644 --- a/CSSParser.php +++ b/CSSParser.php @@ -144,7 +144,7 @@ private function parseCharacter($bIsForIdentifier) { if(preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { return $this->consume(1); } - $sUnicode = $this->consumeExpression('/[0-9a-fA-F]+/u'); + $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u'); if(mb_strlen($sUnicode, $this->sCharset) < 6) { //Consume whitespace after incomplete unicode escape if(preg_match('/\\s/isSu', $this->peek())) { @@ -155,17 +155,21 @@ private function parseCharacter($bIsForIdentifier) { } } } - $sUtf16 = ''; - if((strlen($sUnicode) % 2) === 1) { - $sUnicode = "0$sUnicode"; - } - // we need at least the Basic Multilingual Plane - $sUnicode = str_pad($sUnicode, 4, '0', STR_PAD_LEFT); + $sUnicode = str_pad($sUnicode, 6, '0', STR_PAD_LEFT); + + $v = hexdec($sUnicode); + + // do not replace + // - line feed + // - single and double quotation mark + if (in_array($v, array(0xa, 0x22, 0x27))) { + $result = '\\' . $sUnicode; + return $result; + } // if the character is above U+FFFF, it must be encoded with a surrogate pair // based on the "Example UTF-16 encoding procedure" on http://en.wikipedia.org/wiki/Utf-16 - $v = hexdec($sUnicode); if ($v > 0xFFFF) { $v_bis = $v - 0x10000; $vh = $v_bis >> 10; // higher 10 bits of $v_bis @@ -173,13 +177,25 @@ private function parseCharacter($bIsForIdentifier) { $w1 = 0xD800 + $vh; // first code unit of UTF-16 encoding $w2 = 0xDC00 + $vl; // second code unit of UTF-16 encoding - $sUnicode = str_pad(dechex($w1), 4, "0", STR_PAD_LEFT) . str_pad(dechex($w2), 4, "0", STR_PAD_LEFT); + $sUtf16Bytes = str_pad(dechex($w1), 4, "0", STR_PAD_LEFT) . str_pad(dechex($w2), 4, "0", STR_PAD_LEFT); + } + else { + // we need the Basic Multilingual Plane + $sUtf16Bytes = mb_substr($sUnicode, -4, 4, $this->sCharset); } - $sUtf16 = pack("H*" , $sUnicode); + $sUtf16 = pack("H*" , $sUtf16Bytes); $result = iconv('utf-16', $this->sCharset, $sUtf16); - return $result; + // use utf-16 big endian character set for the check, as we do not want a leading Byte Order Mark + $check = iconv($this->sCharset, 'utf-16be', $result); + if ($check !== $sUtf16) { + // character not representable in target character set, so keep the character escape intact + return '\\' . $sUnicode; + } + else { + return $result; + } } if($bIsForIdentifier) { if(preg_match('/[a-zA-Z0-9]|-|_/u', $this->peek()) === 1) { @@ -379,10 +395,8 @@ private function consume($mValue = 1) { private function consumeExpression($mExpression) { $aMatches; - if(preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) { - if($aMatches[0][1] === 0) { - return $this->consume($aMatches[0][0]); - } + if(preg_match($mExpression, $this->inputLeft(), $aMatches) === 1) { + return $this->consume($aMatches[0]); } throw new Exception("Expected pattern $mExpression not found, got: {$this->peek(5)}"); } @@ -816,7 +830,7 @@ public function getString() { } public function __toString() { - return '"'.addslashes($this->sString).'"'; + return '"'.$this->sString.'"'; } } From 83b6984c71d49baa215424b5cdbe55c9180a63cb Mon Sep 17 00:00:00 2001 From: Kristof Date: Sun, 27 Mar 2011 10:40:20 +0200 Subject: [PATCH 4/8] add //IGNORE flag to iconv conversion call --- CSSParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CSSParser.php b/CSSParser.php index b22e466c..fa92f072 100644 --- a/CSSParser.php +++ b/CSSParser.php @@ -186,7 +186,7 @@ private function parseCharacter($bIsForIdentifier) { $sUtf16 = pack("H*" , $sUtf16Bytes); - $result = iconv('utf-16', $this->sCharset, $sUtf16); + $result = iconv('utf-16', $this->sCharset . '//IGNORE', $sUtf16); // use utf-16 big endian character set for the check, as we do not want a leading Byte Order Mark $check = iconv($this->sCharset, 'utf-16be', $result); if ($check !== $sUtf16) { From 6ef7a24f10eb39975dbf172be452a7913f519094 Mon Sep 17 00:00:00 2001 From: Kristof Coomans Date: Sun, 12 Feb 2012 09:27:00 +0100 Subject: [PATCH 5/8] syncing with the original repository --- CSSParser.php | 638 ++++++----------------------- README.md | 424 +++++++++---------- lib/CSSList.php | 236 +++++++++++ lib/CSSProperties.php | 124 ++++++ lib/CSSRule.php | 135 ++++++ lib/CSSRuleSet.php | 653 ++++++++++++++++++++++++++++++ lib/CSSValue.php | 110 +++++ lib/CSSValueList.php | 92 +++++ tests/CSSDeclarationBlockTest.php | 223 ++++++++++ tests/CSSParserTests.php | 228 ++++++++++- tests/files/-tobedone.css | 7 + tests/files/atrules.css | 10 + tests/files/create-shorthands.css | 7 + tests/files/expand-shorthands.css | 7 + tests/files/functions.css | 22 + tests/files/ie.css | 6 + tests/files/nested.css | 17 + tests/files/slashed.css | 4 + tests/files/specificity.css | 7 + tests/files/unicode.css | 12 + tests/files/values.css | 11 + tests/files/whitespace.css | 3 + tests/quickdump.php | 15 + 23 files changed, 2261 insertions(+), 730 deletions(-) create mode 100644 lib/CSSList.php create mode 100644 lib/CSSProperties.php create mode 100644 lib/CSSRule.php create mode 100644 lib/CSSRuleSet.php create mode 100644 lib/CSSValue.php create mode 100644 lib/CSSValueList.php create mode 100644 tests/CSSDeclarationBlockTest.php create mode 100644 tests/files/-tobedone.css create mode 100644 tests/files/atrules.css create mode 100644 tests/files/create-shorthands.css create mode 100644 tests/files/expand-shorthands.css create mode 100644 tests/files/functions.css create mode 100644 tests/files/ie.css create mode 100644 tests/files/nested.css create mode 100644 tests/files/slashed.css create mode 100644 tests/files/specificity.css create mode 100644 tests/files/unicode.css create mode 100644 tests/files/values.css create mode 100644 tests/files/whitespace.css create mode 100644 tests/quickdump.php diff --git a/CSSParser.php b/CSSParser.php index fa92f072..54779275 100644 --- a/CSSParser.php +++ b/CSSParser.php @@ -1,20 +1,26 @@ sText = $sText; $this->iCurrentPosition = 0; $this->setCharset($sDefaultCharset); } - + public function setCharset($sCharset) { $this->sCharset = $sCharset; $this->iLength = mb_strlen($this->sText, $this->sCharset); @@ -23,18 +29,18 @@ public function setCharset($sCharset) { public function getCharset() { return $this->sCharset; } - + public function parse() { $oResult = new CSSDocument(); $this->parseDocument($oResult); return $oResult; } - + private function parseDocument(CSSDocument $oDocument) { $this->consumeWhiteSpace(); $this->parseList($oDocument, true); } - + private function parseList(CSSList $oList, $bIsRoot = false) { while(!$this->isEnd()) { if($this->comes('@')) { @@ -55,7 +61,7 @@ private function parseList(CSSList $oList, $bIsRoot = false) { throw new Exception("Unexpected end of document"); } } - + private function parseAtRule() { $this->consume('@'); $sIdentifier = $this->parseIdentifier(); @@ -91,8 +97,8 @@ private function parseAtRule() { return $oAtRule; } } - - private function parseIdentifier() { + + private function parseIdentifier($bAllowFunctions = true) { $sResult = $this->parseCharacter(true); if($sResult === null) { throw new Exception("Identifier expected, got {$this->peek(5)}"); @@ -101,9 +107,14 @@ private function parseIdentifier() { 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; @@ -134,13 +145,14 @@ private function parseStringValue() { } 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); } @@ -155,47 +167,13 @@ private function parseCharacter($bIsForIdentifier) { } } } - - $sUnicode = str_pad($sUnicode, 6, '0', STR_PAD_LEFT); - - $v = hexdec($sUnicode); - - // do not replace - // - line feed - // - single and double quotation mark - if (in_array($v, array(0xa, 0x22, 0x27))) { - $result = '\\' . $sUnicode; - return $result; - } - - // if the character is above U+FFFF, it must be encoded with a surrogate pair - // based on the "Example UTF-16 encoding procedure" on http://en.wikipedia.org/wiki/Utf-16 - if ($v > 0xFFFF) { - $v_bis = $v - 0x10000; - $vh = $v_bis >> 10; // higher 10 bits of $v_bis - $vl = $v_bis & 0x3FF; // lower 10 bits of $v_bis - $w1 = 0xD800 + $vh; // first code unit of UTF-16 encoding - $w2 = 0xDC00 + $vl; // second code unit of UTF-16 encoding - - $sUtf16Bytes = str_pad(dechex($w1), 4, "0", STR_PAD_LEFT) . str_pad(dechex($w2), 4, "0", STR_PAD_LEFT); - } - else { - // we need the Basic Multilingual Plane - $sUtf16Bytes = mb_substr($sUnicode, -4, 4, $this->sCharset); - } - - $sUtf16 = pack("H*" , $sUtf16Bytes); - - $result = iconv('utf-16', $this->sCharset . '//IGNORE', $sUtf16); - // use utf-16 big endian character set for the check, as we do not want a leading Byte Order Mark - $check = iconv($this->sCharset, 'utf-16be', $result); - if ($check !== $sUtf16) { - // character not representable in target character set, so keep the character escape intact - return '\\' . $sUnicode; - } - else { - return $result; + $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) { @@ -211,16 +189,16 @@ private function parseCharacter($bIsForIdentifier) { // Does not reach here return null; } - + private function parseSelector() { - $oResult = new CSSSelector(); + $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()); @@ -228,16 +206,13 @@ private function parseRuleSet($oRuleSet) { } $this->consume('}'); } - + private function parseRule() { $oRule = new CSSRule($this->parseIdentifier()); $this->consumeWhiteSpace(); $this->consume(':'); - $this->consumeWhiteSpace(); - while(!($this->comes('}') || $this->comes(';') || $this->comes('!'))) { - $oRule->addValue($this->parseValue()); - $this->consumeWhiteSpace(); - } + $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule())); + $oRule->setValue($oValue); if($this->comes('!')) { $this->consume('!'); $this->consumeWhiteSpace(); @@ -253,28 +228,77 @@ private function parseRule() { return $oRule; } - private function parseValue() { - $aResult = array(); - do { - $this->consumeWhiteSpace(); - if(is_numeric($this->peek()) || $this->comes('-') || $this->comes('.')) { - $aResult[] = $this->parseNumericValue(); - } else if($this->comes('#') || $this->comes('rgb') || $this->comes('hsl')) { - $aResult[] = $this->parseColorValue(); - } else if($this->comes('url')){ - $aResult[] = $this->parseURLValue(); - } else if($this->comes("'") || $this->comes('"')){ - $aResult[] = $this->parseStringValue(); - } else { - $aResult[] = $this->parseIdentifier(); + 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(); - } while($this->comes(',') && is_string($this->consume(','))); - - return $aResult; + } + 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 function parseNumericValue() { + 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('-'); @@ -296,6 +320,10 @@ private function parseNumericValue() { $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')) { @@ -309,26 +337,26 @@ private function parseNumericValue() { } else if($this->comes('mm')) { $sUnit = $this->consume('mm'); } - return new CSSSize($fSize, $sUnit); + return new CSSSize($fSize, $sUnit, $bForColor); } - + private function parseColorValue() { $aColor = array(); if($this->comes('#')) { $this->consume('#'); - $sValue = $this->parseIdentifier(); + $sValue = $this->parseIdentifier(false); if(mb_strlen($sValue, $this->sCharset) === 3) { $sValue = $sValue[0].$sValue[0].$sValue[1].$sValue[1].$sValue[2].$sValue[2]; } - $aColor = array('r' => new CSSSize(intval($sValue[0].$sValue[1], 16)), 'g' => new CSSSize(intval($sValue[2].$sValue[3], 16)), 'b' => new CSSSize(intval($sValue[4].$sValue[5], 16))); + $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(); + $sColorMode = $this->parseIdentifier(false); $this->consumeWhiteSpace(); $this->consume('('); $iLength = mb_strlen($sColorMode, $this->sCharset); for($i=0;$i<$iLength;$i++) { $this->consumeWhiteSpace(); - $aColor[$sColorMode[$i]] = $this->parseNumericValue(); + $aColor[$sColorMode[$i]] = $this->parseNumericValue(true); $this->consumeWhiteSpace(); if($i < ($iLength-1)) { $this->consume(','); @@ -338,7 +366,7 @@ private function parseColorValue() { } return new CSSColor($aColor); } - + private function parseURLValue() { $bUseUrl = $this->comes('url'); if($bUseUrl) { @@ -354,14 +382,14 @@ private function parseURLValue() { } 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 ''; @@ -374,7 +402,7 @@ private function peek($iLength = 1, $iOffset = 0) { } return mb_substr($this->sText, $this->iCurrentPosition+$iOffset, $iLength, $this->sCharset); } - + private function consume($mValue = 1) { if(is_string($mValue)) { $iLength = mb_strlen($mValue, $this->sCharset); @@ -392,15 +420,15 @@ private function consume($mValue = 1) { return $sResult; } } - + private function consumeExpression($mExpression) { $aMatches; - if(preg_match($mExpression, $this->inputLeft(), $aMatches) === 1) { - return $this->consume($aMatches[0]); + 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) { @@ -408,7 +436,7 @@ private function consumeWhiteSpace() { } } while($this->consumeComment()); } - + private function consumeComment() { if($this->comes('/*')) { $this->consumeUntil('*/'); @@ -417,11 +445,11 @@ private function consumeComment() { } 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) { @@ -429,427 +457,9 @@ private function consumeUntil($sEnd) { } return $this->consume($iEndPos-$this->iCurrentPosition); } - + private function inputLeft() { return mb_substr($this->sText, $this->iCurrentPosition, -1, $this->sCharset); } } -abstract class CSSList { - private $aContents; - - public function __construct() { - $this->aContents = array(); - } - - public function append($oItem) { - $this->aContents[] = $oItem; - } - - public function __toString() { - $sResult = ''; - foreach($this->aContents as $oContent) { - $sResult .= $oContent->__toString(); - } - return $sResult; - } - - public function getContents() { - return $this->aContents; - } - - protected function allSelectors(&$aResult) { - foreach($this->aContents as $mContent) { - if($mContent instanceof CSSSelector) { - $aResult[] = $mContent; - } else if($mContent instanceof CSSList) { - $mContent->allSelectors($aResult); - } - } - } - - protected function allRuleSets(&$aResult) { - foreach($this->aContents as $mContent) { - if($mContent instanceof CSSRuleSet) { - $aResult[] = $mContent; - } else if($mContent instanceof CSSList) { - $mContent->allRuleSets($aResult); - } - } - } - - protected function allValues($oElement, &$aResult, $sSearchString = null) { - if($oElement instanceof CSSList) { - foreach($oElement->getContents() as $oContent) { - $this->allValues($oContent, $aResult, $sSearchString); - } - } else if($oElement instanceof CSSRuleSet) { - foreach($oElement->getRules($sSearchString) as $oRule) { - $this->allValues($oRule, $aResult, $sSearchString); - } - } else if($oElement instanceof CSSRule) { - foreach($oElement->getValues() as $aValues) { - foreach($aValues as $mValue) { - $aResult[] = $mValue; - } - } - } - } -} - -class CSSDocument extends CSSList { - public function getAllSelectors() { - $aResult = array(); - $this->allSelectors($aResult); - return $aResult; - } - - public function getAllRuleSets() { - $aResult = array(); - $this->allRuleSets($aResult); - return $aResult; - } - - public function getAllValues($mElement = null) { - $sSearchString = null; - if($mElement === null) { - $mElement = $this; - } else if(is_string($mElement)) { - $sSearchString = $mElement; - $mElement = $this; - } - $aResult = array(); - $this->allValues($mElement, $aResult, $sSearchString); - return $aResult; - } -} - -class CSSMediaQuery extends CSSList { - private $sQuery; - - public function __construct() { - parent::__construct(); - $this->sQuery = null; - } - - public function setQuery($sQuery) { - $this->sQuery = $sQuery; - } - - public function getQuery() { - return $this->sQuery; - } - - public function __toString() { - $sResult = "@media {$this->sQuery} {"; - $sResult .= parent::__toString(); - $sResult .= '}'; - return $sResult; - } -} - -class CSSImport { - private $oLocation; - private $sMediaQuery; - - public function __construct(CSSURL $oLocation, $sMediaQuery) { - $this->oLocation = $oLocation; - $this->sMediaQuery = $sMediaQuery; - } - - public function setLocation($oLocation) { - $this->oLocation = $oLocation; - } - - public function getLocation() { - return $this->oLocation; - } - - public function __toString() { - return "@import ".$this->oLocation->__toString().($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; - } -} - -class CSSCharset { - private $sCharset; - - public function __construct($sCharset) { - $this->sCharset = $sCharset; - } - - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - } - - public function getCharset() { - return $this->sCharset; - } - - public function __toString() { - return "@charset {$this->sCharset->__toString()};"; - } -} - -abstract class CSSRuleSet { - private $aRules; - - public function __construct() { - $this->aRules = array(); - } - - public function addRule(CSSRule $oRule) { - $this->aRules[$oRule->getRule()] = $oRule; - } - - public function getRules($mRule = null) { - if($mRule === null) { - return $this->aRules; - } - $aResult = array(); - if($mRule instanceof CSSRule) { - $mRule = $mRule->getRule(); - } - if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { - $sStart = substr($mRule, 0, -1); - foreach($this->aRules as $oRule) { - if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { - $aResult[$oRule->getRule()] = $this->aRules[$oRule->getRule()]; - } - } - } else if(isset($this->aRules[$mRule])) { - $aResult[$mRule] = $this->aRules[$mRule]; - } - return $aResult; - } - - public function removeRule($mRule) { - if($mRule instanceof CSSRule) { - $mRule = $mRule->getRule(); - } - if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { - $sStart = substr($mRule, 0, -1); - foreach($this->aRules as $oRule) { - if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { - unset($this->aRules[$oRule->getRule()]); - } - } - } else if(isset($this->aRules[$mRule])) { - unset($this->aRules[$mRule]); - } - } - - public function __toString() { - $sResult = ''; - foreach($this->aRules as $oRule) { - $sResult .= $oRule->__toString(); - } - return $sResult; - } -} - -class CSSAtRule extends CSSRuleSet { - private $sType; - - public function __construct($sType) { - parent::__construct(); - $this->sType = $sType; - } - - public function __toString() { - $sResult = "@{$this->sType} {"; - $sResult .= parent::__toString(); - $sResult .= '}'; - return $sResult; - } -} - -class CSSSelector extends CSSRuleSet { - private $aSelector; - - public function __construct() { - parent::__construct(); - $this->aSelector = array(); - } - - public function setSelector($mSelector) { - if(is_array($mSelector)) { - $this->aSelector = $mSelector; - } else { - $this->aSelector = explode(',', $mSelector); - } - foreach($this->aSelector as $iKey => $sSelector) { - $this->aSelector[$iKey] = trim($sSelector); - } - } - - public function getSelector() { - return $this->aSelector; - } - - public function __toString() { - $sResult = implode(', ', $this->aSelector).' {'; - $sResult .= parent::__toString(); - $sResult .= '}'; - return $sResult; - } -} - -class CSSRule { - private $sRule; - private $aValues; - private $bIsImportant; - - public function __construct($sRule) { - $this->sRule = $sRule; - $this->bIsImportant = false; - } - - public function setRule($sRule) { - $this->sRule = $sRule; - } - - public function getRule() { - return $this->sRule; - } - - public function addValue($mValue) { - $this->aValues[] = $mValue; - } - - public function setValues($aValues) { - $this->aValues = $aValues; - } - - public function getValues() { - return $this->aValues; - } - - public function setIsImportant($bIsImportant) { - $this->bIsImportant = $bIsImportant; - } - - public function getIsImportant() { - return $this->bIsImportant; - } - public function __toString() { - $sResult = "{$this->sRule}: "; - foreach($this->aValues as $aValues) { - $sResult .= implode(', ', $aValues).' '; - } - if($this->bIsImportant) { - $sResult .= '!important'; - } else { - $sResult = substr($sResult, 0, -1); - } - $sResult .= ';'; - return $sResult; - } -} - -abstract class CSSValue { - public abstract function __toString(); -} - -class CSSSize extends CSSValue { - private $fSize; - private $sUnit; - - public function __construct($fSize, $sUnit = null) { - $this->fSize = floatval($fSize); - $this->sUnit = $sUnit; - } - - public function setUnit($sUnit) { - $this->sUnit = $sUnit; - } - - public function getUnit() { - return $this->sUnit; - } - - public function setSize($fSize) { - $this->fSize = floatval($fSize); - } - - public function getSize() { - return $this->fSize; - } - - public function isRelative() { - if($this->sUnit === '%' || $this->sUnit === 'em' || $this->sUnit === 'ex') { - return true; - } - if($this->sUnit === null && $this->fSize != 0) { - return true; - } - return false; - } - - public function __toString() { - return $this->fSize.($this->sUnit === null ? '' : $this->sUnit); - } -} - -class CSSColor extends CSSValue { - private $aColor; - - public function __construct($aColor) { - $this->aColor = $aColor; - } - - public function setColor($aColor) { - $this->aColor = $aColor; - } - - public function getColor() { - return $this->aColor; - } - - public function getColorDescription() { - return implode('', array_keys($this->aColor)); - } - - public function __toString() { - return $this->getColorDescription().'('.implode(', ', $this->aColor).')'; - } -} - -class CSSString extends CSSValue { - private $sString; - - public function __construct($sString) { - $this->sString = $sString; - } - - public function setString($sString) { - $this->sString = $sString; - } - - public function getString() { - return $this->sString; - } - - public function __toString() { - return '"'.$this->sString.'"'; - } -} - -class CSSURL extends CSSValue { - private $oURL; - - public function __construct(CSSString $oURL) { - $this->oURL = $oURL; - } - - public function setURL(CSSString $oURL) { - $this->oURL = $oURL; - } - - public function getURL() { - return $this->oURL; - } - - public function __toString() { - return "url({$this->oURL->__toString()})"; - } -} diff --git a/README.md b/README.md index 9b24b8d2..594574f4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A Parser for CSS Files written in PHP. Allows extraction of CSS files into a dat ### Installation -Include the `CSSParser.php` file somewhere in your code using `require_once` (or `include_once`, if you prefer), it does not have any other dependencies. +Include the `CSSParser.php` file somewhere in your code using `require_once` (or `include_once`, if you prefer), the given `lib` folder needs to exist next to the file. ### Extraction @@ -26,11 +26,11 @@ The resulting CSS document structure can be manipulated prior to being output. ### Manipulation -The resulting data structure consists mainly of four basic types: `CSSList`, `CSSRuleSet`, `CSSRule` and `CSSValue`. There are two additional types used: `CSSImport` and `CSSCharset` which you won’t use often. +The resulting data structure consists mainly of five basic types: `CSSList`, `CSSRuleSet`, `CSSRule`, `CSSSelector` and `CSSValue`. There are two additional types used: `CSSImport` and `CSSCharset` which you won’t use often. #### CSSList -`CSSList` represents a generic CSS container, most likely containing selectors but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: +`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector) but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: * `CSSDocument` – representing the root of a CSS file. * `CSSMediaQuery` – represents a subsection of a CSSList that only applies to a output device matching the contained media query. @@ -40,7 +40,7 @@ The resulting data structure consists mainly of four basic types: `CSSList`, `CS `CSSRuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist: * `CSSAtRule` – for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face. -* `CSSSelector` – a selector; contains an array of selector strings (comma-separated in the CSS) as well as the rules to be applied to the matching elements. +* `CSSDeclarationBlock` – a RuleSet constrained by a `CSSSelector; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements. Note: A `CSSList` can contain other `CSSList`s (and `CSSImport`s as well as a `CSSCharset`) while a `CSSRuleSet` can only contain `CSSRule`s. @@ -67,7 +67,7 @@ If you want to manipulate a `CSSRuleSet`, use the methods `addRule(CSSRule $oRul There are a few convenience methods on CSSDocument to ease finding, manipulating and deleting rules: -* `getAllSelectors()` – does what it says; no matter how deeply nested your selectors are. +* `getAllDeclarationBlocks()` – does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`. * `getAllRuleSets()` – does what it says; no matter how deeply nested your rule sets are. * `getAllValues()` – finds all `CSSValue` objects inside `CSSRule`s. @@ -78,13 +78,11 @@ There are a few convenience methods on CSSDocument to ease finding, manipulating $sMyId = "#my_id"; $oParser = new CSSParser($sCssContents); $oCss = $oParser->parse(); - foreach($oCss->getAllSelectors() as $oSelector) { - $aSelector = $oSelector->getSelector(); - foreach($aSelector as $iKey => $sSelector) { + foreach($oCss->getAllDeclarationBlocks() as $oBlock) { + foreach($oBlock->getSelectors() as $oSelector) { //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id - $aSelector[$iKey] = "$sMyId $sSelector"; + $oSelector->setSelector($sMyId.' '.$oSelector->getSelector()); } - $oSelector->setSelector($aSelector); } #### Shrink all absolute sizes to half @@ -134,97 +132,91 @@ To output the entire CSS document into a variable, just use `->__toString()`: #### Structure (`var_dump()`) object(CSSDocument)#2 (1) { - ["aContents":"CSSList":private]=> - array(3) { - [0]=> - object(CSSCharset)#4 (1) { - ["sCharset":"CSSCharset":private]=> - object(CSSString)#3 (1) { - ["sString":"CSSString":private]=> - string(5) "utf-8" - } - } - [1]=> - object(CSSAtRule)#5 (2) { - ["sType":"CSSAtRule":private]=> - string(9) "font-face" - ["aRules":"CSSRuleSet":private]=> - array(2) { - ["font-family"]=> - object(CSSRule)#6 (3) { - ["sRule":"CSSRule":private]=> - string(11) "font-family" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - object(CSSString)#7 (1) { - ["sString":"CSSString":private]=> - string(10) "CrassRoots" - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - ["src"]=> - object(CSSRule)#8 (3) { - ["sRule":"CSSRule":private]=> - string(3) "src" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - object(CSSURL)#9 (1) { - ["oURL":"CSSURL":private]=> - object(CSSString)#10 (1) { - ["sString":"CSSString":private]=> - string(15) "../media/cr.ttf" - } - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - } - } - [2]=> - object(CSSSelector)#11 (2) { - ["aSelector":"CSSSelector":private]=> - array(2) { - [0]=> - string(4) "html" - [1]=> - string(4) "body" - } - ["aRules":"CSSRuleSet":private]=> - array(1) { - ["font-size"]=> - object(CSSRule)#12 (3) { - ["sRule":"CSSRule":private]=> - string(9) "font-size" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - object(CSSSize)#13 (2) { - ["fSize":"CSSSize":private]=> - float(1.6) - ["sUnit":"CSSSize":private]=> - string(2) "em" - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - } - } - } + ["aContents":"CSSList":private]=> + array(3) { + [0]=> + object(CSSCharset)#4 (1) { + ["sCharset":"CSSCharset":private]=> + object(CSSString)#3 (1) { + ["sString":"CSSString":private]=> + string(5) "utf-8" + } + } + [1]=> + object(CSSAtRule)#5 (2) { + ["sType":"CSSAtRule":private]=> + string(9) "font-face" + ["aRules":"CSSRuleSet":private]=> + array(2) { + ["font-family"]=> + object(CSSRule)#6 (3) { + ["sRule":"CSSRule":private]=> + string(11) "font-family" + ["mValue":"CSSRule":private]=> + object(CSSString)#7 (1) { + ["sString":"CSSString":private]=> + string(10) "CrassRoots" + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + ["src"]=> + object(CSSRule)#8 (3) { + ["sRule":"CSSRule":private]=> + string(3) "src" + ["mValue":"CSSRule":private]=> + object(CSSURL)#9 (1) { + ["oURL":"CSSURL":private]=> + object(CSSString)#10 (1) { + ["sString":"CSSString":private]=> + string(15) "../media/cr.ttf" + } + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + } + } + [2]=> + object(CSSDeclarationBlock)#11 (2) { + ["aSelectors":"CSSDeclarationBlock":private]=> + array(2) { + [0]=> + object(CSSSelector)#12 (2) { + ["sSelector":"CSSSelector":private]=> + string(4) "html" + ["iSpecificity":"CSSSelector":private]=> + NULL + } + [1]=> + object(CSSSelector)#13 (2) { + ["sSelector":"CSSSelector":private]=> + string(4) "body" + ["iSpecificity":"CSSSelector":private]=> + NULL + } + } + ["aRules":"CSSRuleSet":private]=> + array(1) { + ["font-size"]=> + object(CSSRule)#14 (3) { + ["sRule":"CSSRule":private]=> + string(9) "font-size" + ["mValue":"CSSRule":private]=> + object(CSSSize)#15 (3) { + ["fSize":"CSSSize":private]=> + float(1.6) + ["sUnit":"CSSSize":private]=> + string(2) "em" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + } + } + } } #### Output (`__toString()`) @@ -244,114 +236,116 @@ To output the entire CSS document into a variable, just use `->__toString()`: #### Structure (`var_dump()`) object(CSSDocument)#2 (1) { - ["aContents":"CSSList":private]=> - array(1) { - [0]=> - object(CSSSelector)#3 (2) { - ["aSelector":"CSSSelector":private]=> - array(1) { - [0]=> - string(7) "#header" - } - ["aRules":"CSSRuleSet":private]=> - array(3) { - ["margin"]=> - object(CSSRule)#4 (3) { - ["sRule":"CSSRule":private]=> - string(6) "margin" - ["aValues":"CSSRule":private]=> - array(4) { - [0]=> - array(1) { - [0]=> - object(CSSSize)#5 (2) { - ["fSize":"CSSSize":private]=> - float(10) - ["sUnit":"CSSSize":private]=> - string(2) "px" - } - } - [1]=> - array(1) { - [0]=> - object(CSSSize)#6 (2) { - ["fSize":"CSSSize":private]=> - float(2) - ["sUnit":"CSSSize":private]=> - string(2) "em" - } - } - [2]=> - array(1) { - [0]=> - object(CSSSize)#7 (2) { - ["fSize":"CSSSize":private]=> - float(1) - ["sUnit":"CSSSize":private]=> - string(2) "cm" - } - } - [3]=> - array(1) { - [0]=> - object(CSSSize)#8 (2) { - ["fSize":"CSSSize":private]=> - float(2) - ["sUnit":"CSSSize":private]=> - string(1) "%" - } - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - ["font-family"]=> - object(CSSRule)#9 (3) { - ["sRule":"CSSRule":private]=> - string(11) "font-family" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(4) { - [0]=> - string(7) "Verdana" - [1]=> - string(9) "Helvetica" - [2]=> - object(CSSString)#10 (1) { - ["sString":"CSSString":private]=> - string(9) "Gill Sans" - } - [3]=> - string(10) "sans-serif" - } - } - ["bIsImportant":"CSSRule":private]=> - bool(false) - } - ["color"]=> - object(CSSRule)#11 (3) { - ["sRule":"CSSRule":private]=> - string(5) "color" - ["aValues":"CSSRule":private]=> - array(1) { - [0]=> - array(1) { - [0]=> - string(3) "red" - } - } - ["bIsImportant":"CSSRule":private]=> - bool(true) - } - } - } - } + ["aContents":"CSSList":private]=> + array(1) { + [0]=> + object(CSSDeclarationBlock)#3 (2) { + ["aSelectors":"CSSDeclarationBlock":private]=> + array(1) { + [0]=> + object(CSSSelector)#4 (2) { + ["sSelector":"CSSSelector":private]=> + string(7) "#header" + ["iSpecificity":"CSSSelector":private]=> + NULL + } + } + ["aRules":"CSSRuleSet":private]=> + array(3) { + ["margin"]=> + object(CSSRule)#5 (3) { + ["sRule":"CSSRule":private]=> + string(6) "margin" + ["mValue":"CSSRule":private]=> + object(CSSRuleValueList)#10 (2) { + ["aComponents":protected]=> + array(4) { + [0]=> + object(CSSSize)#6 (3) { + ["fSize":"CSSSize":private]=> + float(10) + ["sUnit":"CSSSize":private]=> + string(2) "px" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + [1]=> + object(CSSSize)#7 (3) { + ["fSize":"CSSSize":private]=> + float(2) + ["sUnit":"CSSSize":private]=> + string(2) "em" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + [2]=> + object(CSSSize)#8 (3) { + ["fSize":"CSSSize":private]=> + float(1) + ["sUnit":"CSSSize":private]=> + string(2) "cm" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + [3]=> + object(CSSSize)#9 (3) { + ["fSize":"CSSSize":private]=> + float(2) + ["sUnit":"CSSSize":private]=> + string(1) "%" + ["bIsColorComponent":"CSSSize":private]=> + bool(false) + } + } + ["sSeparator":protected]=> + string(1) " " + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + ["font-family"]=> + object(CSSRule)#11 (3) { + ["sRule":"CSSRule":private]=> + string(11) "font-family" + ["mValue":"CSSRule":private]=> + object(CSSRuleValueList)#13 (2) { + ["aComponents":protected]=> + array(4) { + [0]=> + string(7) "Verdana" + [1]=> + string(9) "Helvetica" + [2]=> + object(CSSString)#12 (1) { + ["sString":"CSSString":private]=> + string(9) "Gill Sans" + } + [3]=> + string(10) "sans-serif" + } + ["sSeparator":protected]=> + string(1) "," + } + ["bIsImportant":"CSSRule":private]=> + bool(false) + } + ["color"]=> + object(CSSRule)#14 (3) { + ["sRule":"CSSRule":private]=> + string(5) "color" + ["mValue":"CSSRule":private]=> + string(3) "red" + ["bIsImportant":"CSSRule":private]=> + bool(true) + } + } + } + } } #### Output (`__toString()`) - #header {margin: 10px 2em 1cm 2%;font-family: Verdana, Helvetica, "Gill Sans", sans-serif;color: red !important;} + #header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans", sans-serif;color: red !important;} ## To-Do @@ -359,6 +353,24 @@ To output the entire CSS document into a variable, just use `->__toString()`: * Options for output (compact, verbose, etc.) * Support for @namespace * Named color support (using `CSSColor` instead of an anonymous string literal) -* Allow for function-like property values other than hsl(), rgb(), rgba(), and url() (like -moz-linear-gradient(), for example). * Test suite * Adopt lenient parsing rules +* Support for @-rules (other than @media) that are CSSLists (to support @-webkit-keyframes) + +## Contributors/Thanks to + +* [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties. +* [GaryJones](https://github.com/GaryJones) for lots of input and [http://css-specificity.info/](http://css-specificity.info/). +* [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration. + +## License + +PHP-CSS-Parser is freely distributable under the terms of an MIT-style license. + +Copyright (c) 2011 Raphael Schweikert, http://sabberworm.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/CSSList.php b/lib/CSSList.php new file mode 100644 index 00000000..b2a4b23f --- /dev/null +++ b/lib/CSSList.php @@ -0,0 +1,236 @@ +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 new file mode 100644 index 00000000..15c9edbd --- /dev/null +++ b/lib/CSSProperties.php @@ -0,0 +1,124 @@ +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 new file mode 100644 index 00000000..d6ab6ac4 --- /dev/null +++ b/lib/CSSRule.php @@ -0,0 +1,135 @@ +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 new file mode 100644 index 00000000..2788f2ff --- /dev/null +++ b/lib/CSSRuleSet.php @@ -0,0 +1,653 @@ +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 __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 new file mode 100644 index 00000000..6dc3fb9c --- /dev/null +++ b/lib/CSSValue.php @@ -0,0 +1,110 @@ +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 new file mode 100644 index 00000000..35269e23 --- /dev/null +++ b/lib/CSSValueList.php @@ -0,0 +1,92 @@ +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/tests/CSSDeclarationBlockTest.php b/tests/CSSDeclarationBlockTest.php new file mode 100644 index 00000000..0d311c7e --- /dev/null +++ b/tests/CSSDeclarationBlockTest.php @@ -0,0 +1,223 @@ +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 index f46b17f5..66071c28 100644 --- a/tests/CSSParserTests.php +++ b/tests/CSSParserTests.php @@ -1,6 +1,8 @@ parse(); + $this->assertNotEquals('', $oParser->parse()->__toString()); } catch(Exception $e) { $this->fail($e); } @@ -32,28 +38,29 @@ function testCssFiles() { function testColorParsing() { $oDoc = $this->parsedStructureForFile('colortest'); foreach($oDoc->getAllRuleSets() as $oRuleSet) { - if(!$oRuleSet instanceof CSSSelector) { + if(!$oRuleSet instanceof CSSDeclarationBlock) { continue; } - $aSelector = $oRuleSet->getSelector(); - if($aSelector[0] === '#mine') { + $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), 'g' => new CSSSize(35.0), 'b' => new CSSSize(35.0)), $aValues[0][0]->getColor()); + $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), 'g' => new CSSSize(100.0), 'b' => new CSSSize(230.0), 'a' => new CSSSize(0.3)), $aValues[0][0]->getColor()); + $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), 'g' => new CSSSize(34.0), 'b' => new CSSSize(34.0)), $aValues[0][0]->getColor()); + $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), 's' => new CSSSize(10.0), 'l' => new CSSSize(220.0)), $oColor->getColor()); + $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) { @@ -61,9 +68,210 @@ function testColorParsing() { } } + function testUnicodeParsing() { + $oDoc = $this->parsedStructureForFile('unicode'); + foreach($oDoc->getAllDeclarationBlocks() as $oRuleSet) { + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if(substr($sSelector, 0, strlen('.test-')) !== '.test-') { + continue; + } + $aContentRules = $oRuleSet->getRules('content'); + $aContents = $aContentRules['content']->getValues(); + $sCssString = $aContents[0][0]->__toString(); + if($sSelector == '.test-1') { + $this->assertSame('" "', $sCssString); + } + if($sSelector == '.test-2') { + $this->assertSame('"é"', $sCssString); + } + if($sSelector == '.test-3') { + $this->assertSame('" "', $sCssString); + } + if($sSelector == '.test-4') { + $this->assertSame('"𝄞"', $sCssString); + } + if($sSelector == '.test-5') { + $this->assertSame('"水"', $sCssString); + } + if($sSelector == '.test-6') { + $this->assertSame('"¥"', $sCssString); + } + if($sSelector == '.test-7') { + $this->assertSame('"\A"', $sCssString); + } + if($sSelector == '.test-8') { + $this->assertSame('"\"\""', $sCssString); + } + if($sSelector == '.test-9') { + $this->assertSame('"\"\\\'"', $sCssString); + } + if($sSelector == '.test-10') { + $this->assertSame('"\\\'\\\\"', $sCssString); + } + if($sSelector == '.test-11') { + $this->assertSame('"test"', $sCssString); + } + } + } + + function testSpecificity() { + $oDoc = $this->parsedStructureForFile('specificity'); + $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); + $oDeclarationBlock = $oDeclarationBlock[0]; + $aSelectors = $oDeclarationBlock->getSelectors(); + foreach($aSelectors as $oSelector) { + switch($oSelector->getSelector()) { + case "#test .help": + $this->assertSame(110, $oSelector->getSpecificity()); + break; + case "#file": + $this->assertSame(100, $oSelector->getSpecificity()); + break; + case ".help:hover": + $this->assertSame(20, $oSelector->getSpecificity()); + break; + case "ol li::before": + $this->assertSame(3, $oSelector->getSpecificity()); + break; + case "li.green": + $this->assertSame(11, $oSelector->getSpecificity()); + break; + default: + $this->fail("specificity: untested selector ".$oSelector->getSelector()); + } + } + $this->assertEquals(array(new CSSSelector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100')); + } + + function testManipulation() { + $oDoc = $this->parsedStructureForFile('atrules'); + $this->assertSame('@charset "utf-8";@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}html, body {font-size: 1.6em;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + foreach($oBlock->getSelectors() as $oSelector) { + //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id + $oSelector->setSelector('#my_id '.$oSelector->getSelector()); + } + } + $this->assertSame('@charset "utf-8";@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}#my_id html, #my_id body {font-size: 1.6em;}'."\n", $oDoc->__toString()); + + $oDoc = $this->parsedStructureForFile('values'); + $this->assertSame('#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;font-size: 10px;color: red !important;} +body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('font-'); + } + $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;} +body {color: green;}'."\n", $oDoc->__toString()); + } + + function testSlashedValues() { + $oDoc = $this->parsedStructureForFile('slashed'); + $this->assertSame('.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllValues(null) as $mValue) { + if($mValue instanceof CSSSize && $mValue->isSize() && !$mValue->isRelative()) { + $mValue->setSize($mValue->getSize()*3); + } + } + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oRule = $oBlock->getRules('font'); + $oRule = $oRule['font']; + $oSpaceList = $oRule->getValue(); + $this->assertEquals(' ', $oSpaceList->getListSeparator()); + $oSlashList = $oSpaceList->getListComponents(); + $oCommaList = $oSlashList[1]; + $oSlashList = $oSlashList[0]; + $this->assertEquals(',', $oCommaList->getListSeparator()); + $this->assertEquals('/', $oSlashList->getListSeparator()); + $oRule = $oBlock->getRules('border-radius'); + $oRule = $oRule['border-radius']; + $oSlashList = $oRule->getValue(); + $this->assertEquals('/', $oSlashList->getListSeparator()); + $oSpaceList1 = $oSlashList->getListComponents(); + $oSpaceList2 = $oSpaceList1[1]; + $oSpaceList1 = $oSpaceList1[0]; + $this->assertEquals(' ', $oSpaceList1->getListSeparator()); + $this->assertEquals(' ', $oSpaceList2->getListSeparator()); + } + $this->assertSame('.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}'."\n", $oDoc->__toString()); + } + + function testFunctionSyntax() { + $oDoc = $this->parsedStructureForFile('functions'); + $sExpected = 'div.main {background-image: linear-gradient(rgb(0,0,0),rgb(255,255,255));} +.collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;margin-right: 0.2em;-moz-transition-property: -moz-transform;-moz-transition-duration: 0.2s;-moz-transform-origin: center 60%;} +.collapser.expanded::before, .collapser.expanded::-moz-before, .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);} +.collapser + * {height: 0;overflow: hidden;-moz-transition-property: height;-moz-transition-duration: 0.3s;} +.collapser.expanded + * {height: auto;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + + foreach($oDoc->getAllValues(null, true) as $mValue) { + if($mValue instanceof CSSSize && $mValue->isSize()) { + $mValue->setSize($mValue->getSize()*3); + } + } + $sExpected = str_replace(array('1.2em', '0.2em', '60%'), array('3.6em', '0.6em', '180%'), $sExpected); + $this->assertSame($sExpected, $oDoc->__toString()); + + foreach($oDoc->getAllValues(null, true) as $mValue) { + if($mValue instanceof CSSSize && !$mValue->isRelative() && !$mValue->isColorComponent()) { + $mValue->setSize($mValue->getSize()*2); + } + } + $sExpected = str_replace(array('0.2s', '0.3s', '90deg'), array('0.4s', '0.6s', '180deg'), $sExpected); + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testExpandShorthands() { + $oDoc = $this->parsedStructureForFile('expand-shorthands'); + $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid rgb(255,0,255);background: rgb(204,204,204) url("/images/foo.png") no-repeat left top;margin: 1em !important;padding: 2px 6px 3px;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + $oDoc->expandShorthands(); + $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;padding-left: 6px;border-top-color: rgb(255,0,255);border-right-color: rgb(255,0,255);border-bottom-color: rgb(255,0,255);border-left-color: rgb(255,0,255);border-top-style: solid;border-right-style: solid;border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;font-family: "Trebuchet MS",Georgia,serif;background-color: rgb(204,204,204);background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;background-position: left top;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testCreateShorthands() { + $oDoc = $this->parsedStructureForFile('create-shorthands'); + $sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;border-width: 2px;border-color: rgb(153,153,153);border-style: dotted;background-color: rgb(255,255,255);background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;margin-bottom: 4px;margin-left: 5px;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + $oDoc->createShorthands(); + $sExpected = 'body {background: rgb(255,255,255) url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;border: 2px dotted rgb(153,153,153);font: bold 2em Helvetica,Arial,sans-serif;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testListValueRemoval() { + $oDoc = $this->parsedStructureForFile('atrules'); + foreach($oDoc->getContents() as $oItem) { + if($oItem instanceof CSSAtRule) { + $oDoc->remove($oItem); + break; + } + } + $this->assertSame('@charset "utf-8";html, body {font-size: 1.6em;}'."\n", $oDoc->__toString()); + + $oDoc = $this->parsedStructureForFile('nested'); + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, false); + break; + } + $this->assertSame('html {some-other: -test(val1);} +@media screen {html {some: -test(val2);} +}#unrelated {other: yes;}'."\n", $oDoc->__toString()); + + $oDoc = $this->parsedStructureForFile('nested'); + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, true); + break; + } + $this->assertSame('@media screen {html {some: -test(val2);} +}#unrelated {other: yes;}'."\n", $oDoc->__toString()); + } + function parsedStructureForFile($sFileName) { $sFile = dirname(__FILE__).DIRECTORY_SEPARATOR.'files'.DIRECTORY_SEPARATOR."$sFileName.css"; $oParser = new CSSParser(file_get_contents($sFile)); return $oParser->parse(); } -} \ No newline at end of file + +} diff --git a/tests/files/-tobedone.css b/tests/files/-tobedone.css new file mode 100644 index 00000000..7ec1da90 --- /dev/null +++ b/tests/files/-tobedone.css @@ -0,0 +1,7 @@ +.some[selectors-may='contain-a-{'] { + +} + +.some { + filters: may(contain, a, ')'); +} diff --git a/tests/files/atrules.css b/tests/files/atrules.css new file mode 100644 index 00000000..adfa9f99 --- /dev/null +++ b/tests/files/atrules.css @@ -0,0 +1,10 @@ +@charset "utf-8"; + +@font-face { + font-family: "CrassRoots"; + src: url("../media/cr.ttf") +} + +html, body { + font-size: 1.6em +} diff --git a/tests/files/create-shorthands.css b/tests/files/create-shorthands.css new file mode 100644 index 00000000..c784d674 --- /dev/null +++ b/tests/files/create-shorthands.css @@ -0,0 +1,7 @@ +body +{ + font-size: 2em; font-family: Helvetica,Arial,sans-serif; font-weight: bold; + border-width: 2px; border-color: #999; border-style: dotted; + background-color: #fff; background-image: url('foobar.png'); background-repeat: repeat-y; + margin-top: 2px; margin-right: 3px; margin-bottom: 4px; margin-left: 5px; +} diff --git a/tests/files/expand-shorthands.css b/tests/files/expand-shorthands.css new file mode 100644 index 00000000..89aab1e2 --- /dev/null +++ b/tests/files/expand-shorthands.css @@ -0,0 +1,7 @@ +body { + font: italic 500 14px/1.618 "Trebuchet MS", Georgia, serif; + border: 2px solid #f0f; + background: #ccc url("/images/foo.png") no-repeat left top; + margin: 1em !important; + padding: 2px 6px 3px; +} diff --git a/tests/files/functions.css b/tests/files/functions.css new file mode 100644 index 00000000..eabbd246 --- /dev/null +++ b/tests/files/functions.css @@ -0,0 +1,22 @@ +div.main { background-image: linear-gradient(#000, #fff) } +.collapser::before, +.collapser::-moz-before, +.collapser::-webkit-before { + content: "»"; + font-size: 1.2em; + margin-right: .2em; + -moz-transition-property: -moz-transform; + -moz-transition-duration: .2s; + -moz-transform-origin: center 60%; +} +.collapser.expanded::before, +.collapser.expanded::-moz-before, +.collapser.expanded::-webkit-before { -moz-transform: rotate(90deg) } +.collapser + * { + height: 0; + overflow: hidden; + -moz-transition-property: height; + -moz-transition-duration: .3s; +} +.collapser.expanded + * { height: auto } + diff --git a/tests/files/ie.css b/tests/files/ie.css new file mode 100644 index 00000000..6c0fb381 --- /dev/null +++ b/tests/files/ie.css @@ -0,0 +1,6 @@ +.nav-thumb-wrapper:hover img, a.activeSlide img { + filter: alpha(opacity=100); + -moz-opacity: 1; + -khtml-opacity: 1; + opacity: 1; +} diff --git a/tests/files/nested.css b/tests/files/nested.css new file mode 100644 index 00000000..b59dc80e --- /dev/null +++ b/tests/files/nested.css @@ -0,0 +1,17 @@ +html { + some: -test(val1); +} + +html { + some-other: -test(val1); +} + +@media screen { + html { + some: -test(val2); + } +} + +#unrelated { + other: yes; +} diff --git a/tests/files/slashed.css b/tests/files/slashed.css new file mode 100644 index 00000000..5b629be5 --- /dev/null +++ b/tests/files/slashed.css @@ -0,0 +1,4 @@ +.test { + font: 12px/1.5 Verdana, Arial, sans-serif; + border-radius: 5px 10px 5px 10px / 10px 5px 10px 5px; +} diff --git a/tests/files/specificity.css b/tests/files/specificity.css new file mode 100644 index 00000000..82a2939a --- /dev/null +++ b/tests/files/specificity.css @@ -0,0 +1,7 @@ +#test .help, +#file, +.help:hover, +li.green, +ol li::before { + font-family: Helvetica; +} diff --git a/tests/files/unicode.css b/tests/files/unicode.css new file mode 100644 index 00000000..24823200 --- /dev/null +++ b/tests/files/unicode.css @@ -0,0 +1,12 @@ +.test-1 { content: "\20"; } /* Same as " " */ +.test-2 { content: "\E9"; } /* Same as "é" */ +.test-3 { content: "\0020"; } /* Same as " " */ +.test-5 { content: "\6C34" } /* Same as "水" */ +.test-6 { content: "\00A5" } /* Same as "¥" */ +.test-7 { content: '\a' } /* Same as "\A" (Newline) */ +.test-8 { content: "\"\22" } /* Same as "\"\"" */ +.test-9 { content: "\"\27" } /* Same as ""\"\'"" */ +.test-10 { content: "\'\\" } /* Same as "'\" */ +.test-11 { content: "\test" } /* Same as "test" */ + +.test-4 { content: "\1D11E" } /* Beyond the Basic Multilingual Plane */ diff --git a/tests/files/values.css b/tests/files/values.css new file mode 100644 index 00000000..1f41863e --- /dev/null +++ b/tests/files/values.css @@ -0,0 +1,11 @@ +#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; +} diff --git a/tests/files/whitespace.css b/tests/files/whitespace.css new file mode 100644 index 00000000..6b21c24f --- /dev/null +++ b/tests/files/whitespace.css @@ -0,0 +1,3 @@ +.test { + background-image : url ( 4px ) ; +} diff --git a/tests/quickdump.php b/tests/quickdump.php new file mode 100644 index 00000000..071a72b1 --- /dev/null +++ b/tests/quickdump.php @@ -0,0 +1,15 @@ +parse(); + +echo '#### Structure (`var_dump()`)'."\n"; +var_dump($oDoc); + +echo '#### Output (`__toString()`)'."\n"; +print $oDoc->__toString(); +echo "\n"; + From ab4e7c2761026f07b642b8788c17ec36bf834073 Mon Sep 17 00:00:00 2001 From: Kristof Coomans Date: Sun, 12 Feb 2012 10:42:47 +0100 Subject: [PATCH 6/8] restructuring for psr-0 and PEAR compatibility --- .gitignore | 6 + LICENSE.txt | 9 + build.local.xml | 6 + build.properties | 13 + build.xml | 440 ++++++++++++ lib/CSSRuleSet.php | 653 ------------------ lib/CSSValueList.php | 92 --- package.xml | 71 ++ phpunit.xml | 28 + src/.empty | 0 src/bin/.empty | 0 tests/quickdump.php => src/bin/cssdump | 6 +- src/data/.empty | 0 src/docs/.empty | 0 src/php/.empty | 0 src/php/Sabberworm/CSS/CSSAtRule.php | 22 + src/php/Sabberworm/CSS/CSSCharset.php | 30 + src/php/Sabberworm/CSS/CSSColor.php | 22 + .../Sabberworm/CSS/CSSDeclarationBlock.php | 570 +++++++++++++++ src/php/Sabberworm/CSS/CSSDocument.php | 88 +++ src/php/Sabberworm/CSS/CSSFunction.php | 28 + src/php/Sabberworm/CSS/CSSImport.php | 28 + {lib => src/php/Sabberworm/CSS}/CSSList.php | 120 +--- src/php/Sabberworm/CSS/CSSMediaQuery.php | 30 + .../php/Sabberworm/CSS/CSSParser.php | 12 +- src/php/Sabberworm/CSS/CSSPrimitiveValue.php | 7 + {lib => src/php/Sabberworm/CSS}/CSSRule.php | 2 + src/php/Sabberworm/CSS/CSSRuleSet.php | 70 ++ src/php/Sabberworm/CSS/CSSRuleValueList.php | 9 + .../php/Sabberworm/CSS/CSSSelector.php | 68 +- .../php/Sabberworm/CSS/CSSSize.php | 69 +- src/php/Sabberworm/CSS/CSSString.php | 25 + src/php/Sabberworm/CSS/CSSUrl.php | 24 + src/php/Sabberworm/CSS/CSSValue.php | 7 + src/php/Sabberworm/CSS/CSSValueList.php | 42 ++ src/tests/.empty | 0 src/tests/functional-tests/.empty | 0 src/tests/integration-tests/.empty | 0 src/tests/unit-tests/.empty | 0 src/tests/unit-tests/bin/.empty | 0 src/tests/unit-tests/bootstrap.php | 27 + src/tests/unit-tests/php/.empty | 0 src/tests/unit-tests/www/.empty | 0 src/www/.empty | 0 44 files changed, 1635 insertions(+), 989 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 build.local.xml create mode 100644 build.properties create mode 100644 build.xml delete mode 100644 lib/CSSRuleSet.php delete mode 100644 lib/CSSValueList.php create mode 100644 package.xml create mode 100644 phpunit.xml create mode 100644 src/.empty create mode 100644 src/bin/.empty rename tests/quickdump.php => src/bin/cssdump (55%) create mode 100644 src/data/.empty create mode 100644 src/docs/.empty create mode 100644 src/php/.empty create mode 100644 src/php/Sabberworm/CSS/CSSAtRule.php create mode 100644 src/php/Sabberworm/CSS/CSSCharset.php create mode 100644 src/php/Sabberworm/CSS/CSSColor.php create mode 100644 src/php/Sabberworm/CSS/CSSDeclarationBlock.php create mode 100644 src/php/Sabberworm/CSS/CSSDocument.php create mode 100644 src/php/Sabberworm/CSS/CSSFunction.php create mode 100644 src/php/Sabberworm/CSS/CSSImport.php rename {lib => src/php/Sabberworm/CSS}/CSSList.php (50%) create mode 100644 src/php/Sabberworm/CSS/CSSMediaQuery.php rename CSSParser.php => src/php/Sabberworm/CSS/CSSParser.php (97%) create mode 100644 src/php/Sabberworm/CSS/CSSPrimitiveValue.php rename {lib => src/php/Sabberworm/CSS}/CSSRule.php (99%) create mode 100644 src/php/Sabberworm/CSS/CSSRuleSet.php create mode 100644 src/php/Sabberworm/CSS/CSSRuleValueList.php rename lib/CSSProperties.php => src/php/Sabberworm/CSS/CSSSelector.php (53%) rename lib/CSSValue.php => src/php/Sabberworm/CSS/CSSSize.php (50%) create mode 100644 src/php/Sabberworm/CSS/CSSString.php create mode 100644 src/php/Sabberworm/CSS/CSSUrl.php create mode 100644 src/php/Sabberworm/CSS/CSSValue.php create mode 100644 src/php/Sabberworm/CSS/CSSValueList.php create mode 100644 src/tests/.empty create mode 100644 src/tests/functional-tests/.empty create mode 100644 src/tests/integration-tests/.empty create mode 100644 src/tests/unit-tests/.empty create mode 100644 src/tests/unit-tests/bin/.empty create mode 100644 src/tests/unit-tests/bootstrap.php create mode 100644 src/tests/unit-tests/php/.empty create mode 100644 src/tests/unit-tests/www/.empty create mode 100644 src/www/.empty diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e89c07b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.build +dist +.tmp +nbproject +review +vendor diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..da5949e5 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,9 @@ +PHP-CSS-Parser is freely distributable under the terms of an MIT-style license. + +Copyright (c) 2011-2012 Raphael Schweikert, http://sabberworm.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build.local.xml b/build.local.xml new file mode 100644 index 00000000..90aa3bc9 --- /dev/null +++ b/build.local.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/build.properties b/build.properties new file mode 100644 index 00000000..8ff72781 --- /dev/null +++ b/build.properties @@ -0,0 +1,13 @@ +project.name=CSS +project.channel= +project.majorVersion=0 +project.minorVersion=1 +project.patchLevel=0 +project.snapshot=true + +checkstyle.standard=Zend + +component.type=php-library +component.version=10 + +pear.local=/var/www/${project.channel} diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..a3b50792 --- /dev/null +++ b/build.xml @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The code coverage report is in file://${project.review.codecoveragedir} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Populating vendor/ with dependencies + + + + Your vendor/ folder has been built. + You only need to run 'phing build-vendor' again if you change the + dependencies listed in your package.xml file. + + + + + + + Creating vendor/ as a sandboxed PEAR install folder + + + + + + + + + + + + + + + + + Building release directory + + + + + + + + + + + + + + + + + + + + Creating ${project.tarfile} PEAR package + + + + + + + + + + + + project.lastBuiltTarfile=${project.tarfile} + + + Your PEAR package is in ${project.tarfile} + + + + + + + + + + + Please run 'phing pear-package' first, then try again. + + + + + + + + + + Cannot find PEAR package file ${project.lastBuiltTarfile} + Run 'phing pear-package' to create a new PEAR package, then try again + + + + + + + + + + + + Please run 'phing pear-package' first, then try again. + + + + + + + + + + Cannot find PEAR package file ${project.lastBuiltTarfile} + Run 'phing pear-package' to create a new PEAR package, then try again + + + + + + + + + + + + Please run 'phing pear-package' first, then try again. + + + + + + + + + + + + + + + + + + Cannot find PEAR package file ${project.lastBuiltTarfile} + Run 'phing pear-package' to create a new PEAR package, then try again + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/CSSRuleSet.php b/lib/CSSRuleSet.php deleted file mode 100644 index 2788f2ff..00000000 --- a/lib/CSSRuleSet.php +++ /dev/null @@ -1,653 +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 __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/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/package.xml b/package.xml new file mode 100644 index 00000000..c2c39177 --- /dev/null +++ b/package.xml @@ -0,0 +1,71 @@ + + + ${project.name} + ${project.channel} + Summary goes here + + Description goes here + + + Your name + Anything goes here + Your email address + yes + + ${build.date} + + + ${project.version} + ${project.majorVersion}.${project.minorVersion} + + + ${project.stability} + stable + + All rights reserved. + + No notes. + + + +${contents} + + + + + + 5.3.0 + + + 1.9.4 + + + Autoloader + pear.phix-project.org + 3.0.0 + 3.999.9999 + + + + + + + + X.Y.Z + X.Y + + + stable + stable + + Your release date + All rights reserved + + + + + + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..864f7f78 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + src/tests/unit-tests + + + + + vendor + src/tests + + + src/bin + src/php + + + + + + + + + + + + + diff --git a/src/.empty b/src/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/bin/.empty b/src/bin/.empty new file mode 100644 index 00000000..e69de29b diff --git a/tests/quickdump.php b/src/bin/cssdump similarity index 55% rename from tests/quickdump.php rename to src/bin/cssdump index 071a72b1..67d3a5af 100644 --- a/tests/quickdump.php +++ b/src/bin/cssdump @@ -1,8 +1,9 @@ +#!/usr/bin/env php parse(); @@ -12,4 +13,3 @@ echo '#### Output (`__toString()`)'."\n"; print $oDoc->__toString(); echo "\n"; - diff --git a/src/data/.empty b/src/data/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/docs/.empty b/src/docs/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/php/.empty b/src/php/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/php/Sabberworm/CSS/CSSAtRule.php b/src/php/Sabberworm/CSS/CSSAtRule.php new file mode 100644 index 00000000..70c1b350 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSAtRule.php @@ -0,0 +1,22 @@ +sType = $sType; + } + + public function __toString() { + $sResult = "@{$this->sType} {"; + $sResult .= parent::__toString(); + $sResult .= '}'; + return $sResult; + } +} diff --git a/src/php/Sabberworm/CSS/CSSCharset.php b/src/php/Sabberworm/CSS/CSSCharset.php new file mode 100644 index 00000000..e1875b16 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSCharset.php @@ -0,0 +1,30 @@ +sCharset = $sCharset; + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + } + + public function getCharset() { + return $this->sCharset; + } + + public function __toString() { + return "@charset {$this->sCharset->__toString()};"; + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSColor.php b/src/php/Sabberworm/CSS/CSSColor.php new file mode 100644 index 00000000..8f9b50a9 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSColor.php @@ -0,0 +1,22 @@ +aComponents; + } + + public function setColor($aColor) { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + public function getColorDescription() { + return $this->getName(); + } +} diff --git a/src/php/Sabberworm/CSS/CSSDeclarationBlock.php b/src/php/Sabberworm/CSS/CSSDeclarationBlock.php new file mode 100644 index 00000000..178ee60e --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSDeclarationBlock.php @@ -0,0 +1,570 @@ +aSelectors = array(); + } + + public function setSelectors($mSelector) { + if(is_array($mSelector)) { + $this->aSelectors = $mSelector; + } else { + $this->aSelectors = explode(',', $mSelector); + } + foreach($this->aSelectors as $iKey => $mSelector) { + if(!($mSelector instanceof CSSSelector)) { + $this->aSelectors[$iKey] = new CSSSelector($mSelector); + } + } + } + + /** + * @deprecated use getSelectors() + */ + public function getSelector() { + return $this->getSelectors(); + } + + /** + * @deprecated use setSelectors() + */ + public function setSelector($mSelector) { + $this->setSelectors($mSelector); + } + + public function getSelectors() { + return $this->aSelectors; + } + + /** + * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts. + **/ + public function expandShorthands() { + // border must be expanded before dimensions + $this->expandBorderShorthand(); + $this->expandDimensionsShorthand(); + $this->expandFontShorthand(); + $this->expandBackgroundShorthand(); + $this->expandListStyleShorthand(); + } + + /** + * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible. + **/ + public function createShorthands() { + $this->createBackgroundShorthand(); + $this->createDimensionsShorthand(); + // border must be shortened after dimensions + $this->createBorderShorthand(); + $this->createFontShorthand(); + $this->createListStyleShorthand(); + } + + /** + * Split shorthand border declarations (e.g. border: 1px red;) + * Additional splitting happens in expandDimensionsShorthand + * Multiple borders are not yet supported as of CSS3 + **/ + public function expandBorderShorthand() { + $aBorderRules = array( + 'border', 'border-left', 'border-right', 'border-top', 'border-bottom' + ); + $aBorderSizes = array( + 'thin', 'medium', 'thick' + ); + $aRules = $this->getRules(); + foreach ($aBorderRules as $sBorderRule) { + if(!isset($aRules[$sBorderRule])) continue; + $oRule = $aRules[$sBorderRule]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if($mValue instanceof CSSValue) { + $mNewValue = clone $mValue; + } else { + $mNewValue = $mValue; + } + if($mValue instanceof CSSSize) { + $sNewRuleName = $sBorderRule."-width"; + } else if($mValue instanceof CSSColor) { + $sNewRuleName = $sBorderRule."-color"; + } else { + if(in_array($mValue, $aBorderSizes)) { + $sNewRuleName = $sBorderRule."-width"; + } else/* if(in_array($mValue, $aBorderStyles))*/ { + $sNewRuleName = $sBorderRule."-style"; + } + } + $oNewRule = new CSSRule($sNewRuleName); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue(array($mNewValue)); + $this->addRule($oNewRule); + } + $this->removeRule($sBorderRule); + } + } + + /** + * Split shorthand dimensional declarations (e.g. margin: 0px auto;) + * into their constituent parts. + * Handles margin, padding, border-color, border-style and border-width. + **/ + public function expandDimensionsShorthand() { + $aExpansions = array( + 'margin' => 'margin-%s', + 'padding' => 'padding-%s', + 'border-color' => 'border-%s-color', + 'border-style' => 'border-%s-style', + 'border-width' => 'border-%s-width' + ); + $aRules = $this->getRules(); + foreach ($aExpansions as $sProperty => $sExpanded) { + if(!isset($aRules[$sProperty])) continue; + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + $top = $right = $bottom = $left = null; + switch(count($aValues)) { + case 1: + $top = $right = $bottom = $left = $aValues[0]; + break; + case 2: + $top = $bottom = $aValues[0]; + $left = $right = $aValues[1]; + break; + case 3: + $top = $aValues[0]; + $left = $right = $aValues[1]; + $bottom = $aValues[2]; + break; + case 4: + $top = $aValues[0]; + $right = $aValues[1]; + $bottom = $aValues[2]; + $left = $aValues[3]; + break; + } + foreach(array('top', 'right', 'bottom', 'left') as $sPosition) { + $oNewRule = new CSSRule(sprintf($sExpanded, $sPosition)); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue(${ + $sPosition}); + $this->addRule($oNewRule); + } + $this->removeRule($sProperty); + } + } + + /** + * Convert shorthand font declarations + * (e.g. font: 300 italic 11px/14px verdana, helvetica, sans-serif;) + * into their constituent parts. + **/ + public function expandFontShorthand() { + $aRules = $this->getRules(); + if(!isset($aRules['font'])) return; + $oRule = $aRules['font']; + // reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand + $aFontProperties = array( + 'font-style' => 'normal', + 'font-variant' => 'normal', + 'font-weight' => 'normal', + 'font-size' => 'normal', + 'line-height' => 'normal' + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach($aValues as $mValue) { + if(!$mValue instanceof CSSValue) { + $mValue = mb_strtolower($mValue); + } + if(in_array($mValue, array('normal', 'inherit'))) { + foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) { + if(!isset($aFontProperties[$sProperty])) { + $aFontProperties[$sProperty] = $mValue; + } + } + } else if(in_array($mValue, array('italic', 'oblique'))) { + $aFontProperties['font-style'] = $mValue; + } else if($mValue == 'small-caps') { + $aFontProperties['font-variant'] = $mValue; + } else if( + in_array($mValue, array('bold', 'bolder', 'lighter')) + || ($mValue instanceof CSSSize + && in_array($mValue->getSize(), range(100, 900, 100))) + ) { + $aFontProperties['font-weight'] = $mValue; + } else if($mValue instanceof CSSRuleValueList && $mValue->getListSeparator() == '/') { + list($oSize, $oHeight) = $mValue->getListComponents(); + $aFontProperties['font-size'] = $oSize; + $aFontProperties['line-height'] = $oHeight; + } else if($mValue instanceof CSSSize && $mValue->getUnit() !== null) { + $aFontProperties['font-size'] = $mValue; + } else { + $aFontProperties['font-family'] = $mValue; + } + } + foreach ($aFontProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $oNewRule->addValue($mValue); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('font'); + } + + /* + * Convert shorthand background declarations + * (e.g. background: url("chess.png") gray 50% repeat fixed;) + * into their constituent parts. + * @see http://www.w3.org/TR/CSS21/colors.html#propdef-background + **/ + public function expandBackgroundShorthand() { + $aRules = $this->getRules(); + if(!isset($aRules['background'])) return; + $oRule = $aRules['background']; + $aBgProperties = array( + 'background-color' => array('transparent'), 'background-image' => array('none'), + 'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'), + 'background-position' => array(new CSSSize(0, '%'), new CSSSize(0, '%')) + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if(count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + return; + } + $iNumBgPos = 0; + foreach($aValues as $mValue) { + if(!$mValue instanceof CSSValue) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof CSSURL) { + $aBgProperties['background-image'] = $mValue; + } else if($mValue instanceof CSSColor) { + $aBgProperties['background-color'] = $mValue; + } else if(in_array($mValue, array('scroll', 'fixed'))) { + $aBgProperties['background-attachment'] = $mValue; + } else if(in_array($mValue, array('repeat','no-repeat', 'repeat-x', 'repeat-y'))) { + $aBgProperties['background-repeat'] = $mValue; + } else if(in_array($mValue, array('left','center','right','top','bottom')) + || $mValue instanceof CSSSize + ){ + if($iNumBgPos == 0) { + $aBgProperties['background-position'][0] = $mValue; + $aBgProperties['background-position'][1] = 'center'; + } else { + $aBgProperties['background-position'][$iNumBgPos] = $mValue; + } + $iNumBgPos++; + } + } + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue($mValue); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + } + + public function expandListStyleShorthand() { + $aListProperties = array( + 'list-style-type' => 'disc', + 'list-style-position' => 'outside', + 'list-style-image' => 'none' + ); + $aListStyleTypes = array( + 'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', + 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', + 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', + 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana' + ); + $aListStylePositions = array( + 'inside', 'outside' + ); + $aRules = $this->getRules(); + if(!isset($aRules['list-style'])) return; + $oRule = $aRules['list-style']; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if(count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + return; + } + foreach($aValues as $mValue) { + if(!$mValue instanceof CSSValue) { + $mValue = mb_strtolower($mValue); + } + if($mValue instanceof CSSUrl) { + $aListProperties['list-style-image'] = $mValue; + } else if(in_array($mValue, $aListStyleTypes)) { + $aListProperties['list-style-types'] = $mValue; + } else if(in_array($mValue, $aListStylePositions)) { + $aListProperties['list-style-position'] = $mValue; + } + } + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new CSSRule($sProperty); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue($mValue); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + } + + public function createShorthandProperties(array $aProperties, $sShorthand) { + $aRules = $this->getRules(); + $aNewValues = array(); + foreach($aProperties as $sProperty) { + if(!isset($aRules[$sProperty])) continue; + $oRule = $aRules[$sProperty]; + if(!$oRule->getIsImportant()) { + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach($aValues as $mValue) { + $aNewValues[] = $mValue; + } + $this->removeRule($sProperty); + } + } + if(count($aNewValues)) { + $oNewRule = new CSSRule($sShorthand); + foreach($aNewValues as $mValue) { + $oNewRule->addValue($mValue); + } + $this->addRule($oNewRule); + } + } + + public function createBackgroundShorthand() { + $aProperties = array( + 'background-color', 'background-image', 'background-repeat', + 'background-position', 'background-attachment' + ); + $this->createShorthandProperties($aProperties, 'background'); + } + + public function createListStyleShorthand() { + $aProperties = array( + 'list-style-type', 'list-style-position', 'list-style-image' + ); + $this->createShorthandProperties($aProperties, 'list-style'); + } + + /** + * Combine border-color, border-style and border-width into border + * Should be run after create_dimensions_shorthand! + **/ + public function createBorderShorthand() { + $aProperties = array( + 'border-width', 'border-style', 'border-color' + ); + $this->createShorthandProperties($aProperties, 'border'); + } + + /* + * Looks for long format CSS dimensional properties + * (margin, padding, border-color, border-style and border-width) + * and converts them into shorthand CSS properties. + **/ + public function createDimensionsShorthand() { + $aPositions = array('top', 'right', 'bottom', 'left'); + $aExpansions = array( + 'margin' => 'margin-%s', + 'padding' => 'padding-%s', + 'border-color' => 'border-%s-color', + 'border-style' => 'border-%s-style', + 'border-width' => 'border-%s-width' + ); + $aRules = $this->getRules(); + foreach ($aExpansions as $sProperty => $sExpanded) { + $aFoldable = array(); + foreach($aRules as $sRuleName => $oRule) { + foreach ($aPositions as $sPosition) { + if($sRuleName == sprintf($sExpanded, $sPosition)) { + $aFoldable[$sRuleName] = $oRule; + } + } + } + // All four dimensions must be present + if(count($aFoldable) == 4) { + $aValues = array(); + foreach ($aPositions as $sPosition) { + $oRule = $aRules[sprintf($sExpanded, $sPosition)]; + $mRuleValue = $oRule->getValue(); + $aRuleValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aRuleValues[] = $mRuleValue; + } else { + $aRuleValues = $mRuleValue->getListComponents(); + } + $aValues[$sPosition] = $aRuleValues; + } + $oNewRule = new CSSRule($sProperty); + if((string)$aValues['left'][0] == (string)$aValues['right'][0]) { + if((string)$aValues['top'][0] == (string)$aValues['bottom'][0]) { + if((string)$aValues['top'][0] == (string)$aValues['left'][0]) { + // All 4 sides are equal + $oNewRule->addValue($aValues['top']); + } else { + // Top and bottom are equal, left and right are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + } + } else { + // Only left and right are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + $oNewRule->addValue($aValues['bottom']); + } + } else { + // No sides are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + $oNewRule->addValue($aValues['bottom']); + $oNewRule->addValue($aValues['right']); + } + $this->addRule($oNewRule); + foreach ($aPositions as $sPosition) + { + $this->removeRule(sprintf($sExpanded, $sPosition)); + } + } + } + } + + /** + * Looks for long format CSS font properties (e.g. font-weight) and + * tries to convert them into a shorthand CSS font property. + * At least font-size AND font-family must be present in order to create a shorthand declaration. + **/ + public function createFontShorthand() { + $aFontProperties = array( + 'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family' + ); + $aRules = $this->getRules(); + if(!isset($aRules['font-size']) || !isset($aRules['font-family'])) { + return; + } + $oNewRule = new CSSRule('font'); + foreach(array('font-style', 'font-variant', 'font-weight') as $sProperty) { + if(isset($aRules[$sProperty])) { + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if($aValues[0] !== 'normal') { + $oNewRule->addValue($aValues[0]); + } + } + } + // Get the font-size value + $oRule = $aRules['font-size']; + $mRuleValue = $oRule->getValue(); + $aFSValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aFSValues[] = $mRuleValue; + } else { + $aFSValues = $mRuleValue->getListComponents(); + } + // But wait to know if we have line-height to add it + if(isset($aRules['line-height'])) { + $oRule = $aRules['line-height']; + $mRuleValue = $oRule->getValue(); + $aLHValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aLHValues[] = $mRuleValue; + } else { + $aLHValues = $mRuleValue->getListComponents(); + } + if($aLHValues[0] !== 'normal') { + $val = new CSSRuleValueList('/'); + $val->addListComponent($aFSValues[0]); + $val->addListComponent($aLHValues[0]); + $oNewRule->addValue($val); + } + } else { + $oNewRule->addValue($aFSValues[0]); + } + $oRule = $aRules['font-family']; + $mRuleValue = $oRule->getValue(); + $aFFValues = array(); + if(!$mRuleValue instanceof CSSRuleValueList) { + $aFFValues[] = $mRuleValue; + } else { + $aFFValues = $mRuleValue->getListComponents(); + } + $oFFValue = new CSSRuleValueList(','); + $oFFValue->setListComponents($aFFValues); + $oNewRule->addValue($oFFValue); + + $this->addRule($oNewRule); + foreach ($aFontProperties as $sProperty) { + $this->removeRule($sProperty); + } + } + + public function __toString() { + $sResult = implode(', ', $this->aSelectors).' {'; + $sResult .= parent::__toString(); + $sResult .= '}'."\n"; + return $sResult; + } +} diff --git a/src/php/Sabberworm/CSS/CSSDocument.php b/src/php/Sabberworm/CSS/CSSDocument.php new file mode 100644 index 00000000..04e903ed --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSDocument.php @@ -0,0 +1,88 @@ +allDeclarationBlocks($aResult); + return $aResult; + } + + /** + * @deprecated use getAllDeclarationBlocks() + */ + public function getAllSelectors() { + return $this->getAllDeclarationBlocks(); + } + + /** + * Returns all CSSRuleSet objects found recursively in the tree. + */ + public function getAllRuleSets() { + $aResult = array(); + $this->allRuleSets($aResult); + return $aResult; + } + + /** + * Returns all CSSValue objects found recursively in the tree. + * @param (object|string) $mElement the CSSList or CSSRuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{CSSRuleSet->getRules()}). + * @param (bool) $bSearchInFunctionArguments whether to also return CSSValue objects used as CSSFunction arguments. + */ + public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { + $sSearchString = null; + if($mElement === null) { + $mElement = $this; + } else if(is_string($mElement)) { + $sSearchString = $mElement; + $mElement = $this; + } + $aResult = array(); + $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); + return $aResult; + } + + /** + * Returns all CSSSelector objects found recursively in the tree. + * Note that this does not yield the full CSSDeclarationBlock that the selector belongs to (and, currently, there is no way to get to that). + * @param $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "=="). + * @example getSelectorsBySpecificity('>= 100') + */ + public function getSelectorsBySpecificity($sSpecificitySearch = null) { + if(is_numeric($sSpecificitySearch) || is_numeric($sSpecificitySearch[0])) { + $sSpecificitySearch = "== $sSpecificitySearch"; + } + $aResult = array(); + $this->allSelectors($aResult, $sSpecificitySearch); + return $aResult; + } + + /** + * Expands all shorthand properties to their long value + */ + public function expandShorthands() + { + foreach($this->getAllDeclarationBlocks() as $oDeclaration) + { + $oDeclaration->expandShorthands(); + } + } + + /* + * Create shorthands properties whenever possible + */ + public function createShorthands() + { + foreach($this->getAllDeclarationBlocks() as $oDeclaration) + { + $oDeclaration->createShorthands(); + } + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSFunction.php b/src/php/Sabberworm/CSS/CSSFunction.php new file mode 100644 index 00000000..c6d58193 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSFunction.php @@ -0,0 +1,28 @@ +sName = $sName; + parent::__construct($aArguments); + } + + public function getName() { + return $this->sName; + } + + public function setName($sName) { + $this->sName = $sName; + } + + public function getArguments() { + return $this->aComponents; + } + + public function __toString() { + $aArguments = parent::__toString(); + return "{$this->sName}({$aArguments})"; + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSImport.php b/src/php/Sabberworm/CSS/CSSImport.php new file mode 100644 index 00000000..829f1484 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSImport.php @@ -0,0 +1,28 @@ +oLocation = $oLocation; + $this->sMediaQuery = $sMediaQuery; + } + + public function setLocation($oLocation) { + $this->oLocation = $oLocation; + } + + public function getLocation() { + return $this->oLocation; + } + + public function __toString() { + return "@import ".$this->oLocation->__toString().($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; + } +} diff --git a/lib/CSSList.php b/src/php/Sabberworm/CSS/CSSList.php similarity index 50% rename from lib/CSSList.php rename to src/php/Sabberworm/CSS/CSSList.php index b2a4b23f..af6c8922 100644 --- a/lib/CSSList.php +++ b/src/php/Sabberworm/CSS/CSSList.php @@ -1,9 +1,11 @@ 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/src/php/Sabberworm/CSS/CSSMediaQuery.php b/src/php/Sabberworm/CSS/CSSMediaQuery.php new file mode 100644 index 00000000..3966bd46 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSMediaQuery.php @@ -0,0 +1,30 @@ +sQuery = null; + } + + public function setQuery($sQuery) { + $this->sQuery = $sQuery; + } + + public function getQuery() { + return $this->sQuery; + } + + public function __toString() { + $sResult = "@media {$this->sQuery} {"; + $sResult .= parent::__toString(); + $sResult .= '}'; + return $sResult; + } +} \ No newline at end of file diff --git a/CSSParser.php b/src/php/Sabberworm/CSS/CSSParser.php similarity index 97% rename from CSSParser.php rename to src/php/Sabberworm/CSS/CSSParser.php index 54779275..c699284e 100644 --- a/CSSParser.php +++ b/src/php/Sabberworm/CSS/CSSParser.php @@ -1,16 +1,12 @@ aRules = array(); + } + + public function addRule(CSSRule $oRule) { + $this->aRules[$oRule->getRule()] = $oRule; + } + + /** + * Returns all rules matching the given pattern + * @param (null|string|CSSRule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a CSSRule behaves like calling getRules($mRule->getRule()). + * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font. + * @example $oRuleSet->getRules('font') //returns array('font' => $oRule) or array(). + */ + public function getRules($mRule = null) { + if($mRule === null) { + return $this->aRules; + } + $aResult = array(); + if($mRule instanceof CSSRule) { + $mRule = $mRule->getRule(); + } + if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { + $sStart = substr($mRule, 0, -1); + foreach($this->aRules as $oRule) { + if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { + $aResult[$oRule->getRule()] = $this->aRules[$oRule->getRule()]; + } + } + } else if(isset($this->aRules[$mRule])) { + $aResult[$mRule] = $this->aRules[$mRule]; + } + return $aResult; + } + + public function removeRule($mRule) { + if($mRule instanceof CSSRule) { + $mRule = $mRule->getRule(); + } + if(strrpos($mRule, '-')===strlen($mRule)-strlen('-')) { + $sStart = substr($mRule, 0, -1); + foreach($this->aRules as $oRule) { + if($oRule->getRule() === $sStart || strpos($oRule->getRule(), $mRule) === 0) { + unset($this->aRules[$oRule->getRule()]); + } + } + } else if(isset($this->aRules[$mRule])) { + unset($this->aRules[$mRule]); + } + } + + public function __toString() { + $sResult = ''; + foreach($this->aRules as $oRule) { + $sResult .= $oRule->__toString(); + } + return $sResult; + } +} diff --git a/src/php/Sabberworm/CSS/CSSRuleValueList.php b/src/php/Sabberworm/CSS/CSSRuleValueList.php new file mode 100644 index 00000000..701d8df2 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSRuleValueList.php @@ -0,0 +1,9 @@ +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).';'; - } -} +namespace Sabberworm\CSS; /** -* 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 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 = '/ + NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ (\.[\w]+) # classes | \[(\w+) # attributes @@ -75,7 +25,7 @@ class CSSSelector { |empty|contains )) /ix', - ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ + ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ ((^|[\s\+\>\~]+)[\w]+ # elements | \:{1,2}( # pseudo-elements @@ -84,21 +34,21 @@ class CSSSelector { |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; diff --git a/lib/CSSValue.php b/src/php/Sabberworm/CSS/CSSSize.php similarity index 50% rename from lib/CSSValue.php rename to src/php/Sabberworm/CSS/CSSSize.php index 6dc3fb9c..d12110c0 100644 --- a/lib/CSSValue.php +++ b/src/php/Sabberworm/CSS/CSSSize.php @@ -1,24 +1,18 @@ fSize = floatval($fSize); $this->sUnit = $sUnit; $this->bIsColorComponent = $bIsColorComponent; } - + public function setUnit($sUnit) { $this->sUnit = $sUnit; } @@ -26,7 +20,7 @@ public function setUnit($sUnit) { public function getUnit() { return $this->sUnit; } - + public function setSize($fSize) { $this->fSize = floatval($fSize); } @@ -40,9 +34,9 @@ public function isColorComponent() { } /** - * 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. - */ + * 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)) { @@ -50,7 +44,7 @@ public function isSize() { } return !$this->isColorComponent(); } - + public function isRelative() { if($this->sUnit === '%' || $this->sUnit === 'em' || $this->sUnit === 'ex') { return true; @@ -60,51 +54,8 @@ public function isRelative() { } 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()})"; + return $this->fSize.($this->sUnit === null ? '' : $this->sUnit); } -} - +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSString.php b/src/php/Sabberworm/CSS/CSSString.php new file mode 100644 index 00000000..26c903c3 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSString.php @@ -0,0 +1,25 @@ +sString = $sString; + } + + public function setString($sString) { + $this->sString = $sString; + } + + public function getString() { + return $this->sString; + } + + public function __toString() { + $sString = addslashes($this->sString); + $sString = str_replace("\n", '\A', $sString); + return '"'.$sString.'"'; + } +} \ No newline at end of file diff --git a/src/php/Sabberworm/CSS/CSSUrl.php b/src/php/Sabberworm/CSS/CSSUrl.php new file mode 100644 index 00000000..7d83346e --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSUrl.php @@ -0,0 +1,24 @@ +oURL = $oURL; + } + + public function setURL(CSSString $oURL) { + $this->oURL = $oURL; + } + + public function getURL() { + return $this->oURL; + } + + public function __toString() { + return "url({$this->oURL->__toString()})"; + } +} + diff --git a/src/php/Sabberworm/CSS/CSSValue.php b/src/php/Sabberworm/CSS/CSSValue.php new file mode 100644 index 00000000..da0bc6f6 --- /dev/null +++ b/src/php/Sabberworm/CSS/CSSValue.php @@ -0,0 +1,7 @@ +getListSeparator() === $sSeparator) { + $aComponents = $aComponents->getListComponents(); + } else if(!is_array($aComponents)) { + $aComponents = array($aComponents); + } + $this->aComponents = $aComponents; + $this->sSeparator = $sSeparator; + } + + public function addListComponent($mComponent) { + $this->aComponents[] = $mComponent; + } + + public function getListComponents() { + return $this->aComponents; + } + + public function setListComponents($aComponents) { + $this->aComponents = $aComponents; + } + + public function getListSeparator() { + return $this->sSeparator; + } + + public function setListSeparator($sSeparator) { + $this->sSeparator = $sSeparator; + } + + function __toString() { + return implode($this->sSeparator, $this->aComponents); + } +} diff --git a/src/tests/.empty b/src/tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/functional-tests/.empty b/src/tests/functional-tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/integration-tests/.empty b/src/tests/integration-tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/unit-tests/.empty b/src/tests/unit-tests/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/unit-tests/bin/.empty b/src/tests/unit-tests/bin/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/unit-tests/bootstrap.php b/src/tests/unit-tests/bootstrap.php new file mode 100644 index 00000000..6646cf07 --- /dev/null +++ b/src/tests/unit-tests/bootstrap.php @@ -0,0 +1,27 @@ + Date: Sat, 9 Jun 2012 10:04:25 +0200 Subject: [PATCH 7/8] restructuring for psr-0 and PEAR compatibility --- .gitignore | 3 + phpunit.xml => phpunit.xml.dist | 0 src/README.txt | 68 +++++++++++++++++++ .../Sabberworm/CSS/{CSSUrl.php => CSSURL.php} | 0 src/tests/unit-tests/bootstrap.php | 6 ++ .../tests/unit-tests}/files/-tobedone.css | 0 .../tests/unit-tests}/files/atrules.css | 0 .../tests/unit-tests}/files/colortest.css | 0 .../unit-tests}/files/create-shorthands.css | 0 .../unit-tests}/files/expand-shorthands.css | 0 .../tests/unit-tests}/files/functions.css | 0 {tests => src/tests/unit-tests}/files/ie.css | 0 .../tests/unit-tests}/files/important.css | 0 .../tests/unit-tests}/files/nested.css | 0 .../tests/unit-tests}/files/slashed.css | 0 .../tests/unit-tests}/files/specificity.css | 0 .../tests/unit-tests}/files/unicode.css | 0 .../tests/unit-tests}/files/values.css | 0 .../tests/unit-tests}/files/whitespace.css | 0 .../CSS}/CSSDeclarationBlockTest.php | 6 +- .../php/Sabberworm/CSS}/CSSParserTests.php | 9 +-- 21 files changed, 86 insertions(+), 6 deletions(-) rename phpunit.xml => phpunit.xml.dist (100%) create mode 100644 src/README.txt rename src/php/Sabberworm/CSS/{CSSUrl.php => CSSURL.php} (100%) rename {tests => src/tests/unit-tests}/files/-tobedone.css (100%) rename {tests => src/tests/unit-tests}/files/atrules.css (100%) rename {tests => src/tests/unit-tests}/files/colortest.css (100%) rename {tests => src/tests/unit-tests}/files/create-shorthands.css (100%) rename {tests => src/tests/unit-tests}/files/expand-shorthands.css (100%) rename {tests => src/tests/unit-tests}/files/functions.css (100%) rename {tests => src/tests/unit-tests}/files/ie.css (100%) rename {tests => src/tests/unit-tests}/files/important.css (100%) rename {tests => src/tests/unit-tests}/files/nested.css (100%) rename {tests => src/tests/unit-tests}/files/slashed.css (100%) rename {tests => src/tests/unit-tests}/files/specificity.css (100%) rename {tests => src/tests/unit-tests}/files/unicode.css (100%) rename {tests => src/tests/unit-tests}/files/values.css (100%) rename {tests => src/tests/unit-tests}/files/whitespace.css (100%) rename {tests => src/tests/unit-tests/php/Sabberworm/CSS}/CSSDeclarationBlockTest.php (98%) rename {tests => src/tests/unit-tests/php/Sabberworm/CSS}/CSSParserTests.php (98%) diff --git a/.gitignore b/.gitignore index e89c07b3..e6ae545c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ dist nbproject review vendor +.buildpath +.settings +.project diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 100% rename from phpunit.xml rename to phpunit.xml.dist diff --git a/src/README.txt b/src/README.txt new file mode 100644 index 00000000..4074f387 --- /dev/null +++ b/src/README.txt @@ -0,0 +1,68 @@ +Your src/ folder +================ + +This src/ folder is where you put all of your code for release. There's +a folder for each type of file that the PEAR Installer supports. You can +find out more about these file types online at: + +http://blog.stuartherbert.com/php/2011/04/04/explaining-file-roles/ + + * bin/ + + If you're creating any command-line tools, this is where you'd put + them. Files in here get installed into /usr/bin on Linux et al. + + There is more information available here: http://blog.stuartherbert.com/php/2011/04/06/php-components-shipping-a-command-line-program/ + + You can find an example here: https://github.com/stuartherbert/phix/tree/master/src/bin + + * data/ + + If you have any data files (any files that aren't PHP code, and which + don't belong in the www/ folder), this is the folder to put them in. + + There is more information available here: http://blog.stuartherbert.com/php/2011/04/11/php-components-shipping-data-files-with-your-components/ + + You can find an example here: https://github.com/stuartherbert/ComponentManagerPhpLibrary/tree/master/src/data + + * php/ + + This is where your component's PHP code belongs. Everything that goes + into this folder must be PSR0-compliant, so that it works with the + supplied autoloader. + + There is more information available here: http://blog.stuartherbert.com/php/2011/04/05/php-components-shipping-reusable-php-code/ + + You can find an example here: https://github.com/stuartherbert/ContractLib/tree/master/src/php + + * tests/functional-tests/ + + Right now, this folder is just a placeholder for future functionality. + You're welcome to make use of it yourself. + + * tests/integration-tests/ + + Right now, this folder is just a placeholder for future functionality. + You're welcome to make use of it yourself. + + * tests/unit-tests/ + + This is where all of your PHPUnit tests go. + + It needs to contain _exactly_ the same folder structure as the src/php/ + folder. For each of your PHP classes in src/php/, there should be a + corresponding test file in test/unit-tests. + + There is more information available here: http://blog.stuartherbert.com/php/2011/08/15/php-components-shipping-unit-tests-with-your-component/ + + You can find an example here: https://github.com/stuartherbert/ContractLib/tree/master/test/unit-tests + + * www/ + + This folder is for any files that should be published in a web server's + DocRoot folder. + + It's quite unusual for components to put anything in this folder, but + it is there just in case. + + There is more information available here: http://blog.stuartherbert.com/php/2011/08/16/php-components-shipping-web-pages-with-your-components/ diff --git a/src/php/Sabberworm/CSS/CSSUrl.php b/src/php/Sabberworm/CSS/CSSURL.php similarity index 100% rename from src/php/Sabberworm/CSS/CSSUrl.php rename to src/php/Sabberworm/CSS/CSSURL.php diff --git a/src/tests/unit-tests/bootstrap.php b/src/tests/unit-tests/bootstrap.php index 6646cf07..c5203baf 100644 --- a/src/tests/unit-tests/bootstrap.php +++ b/src/tests/unit-tests/bootstrap.php @@ -25,3 +25,9 @@ psr0_autoloader_searchFirst(APP_LIBDIR); psr0_autoloader_searchFirst(APP_TESTDIR); psr0_autoloader_searchFirst(APP_TOPDIR); + +// step 4: enable ContractLib if it is available +if (class_exists('Phix_Project\ContractLib\Contract')) +{ + \Phix_Project\ContractLib\Contract::EnforceWrappedContracts(); +} diff --git a/tests/files/-tobedone.css b/src/tests/unit-tests/files/-tobedone.css similarity index 100% rename from tests/files/-tobedone.css rename to src/tests/unit-tests/files/-tobedone.css diff --git a/tests/files/atrules.css b/src/tests/unit-tests/files/atrules.css similarity index 100% rename from tests/files/atrules.css rename to src/tests/unit-tests/files/atrules.css diff --git a/tests/files/colortest.css b/src/tests/unit-tests/files/colortest.css similarity index 100% rename from tests/files/colortest.css rename to src/tests/unit-tests/files/colortest.css diff --git a/tests/files/create-shorthands.css b/src/tests/unit-tests/files/create-shorthands.css similarity index 100% rename from tests/files/create-shorthands.css rename to src/tests/unit-tests/files/create-shorthands.css diff --git a/tests/files/expand-shorthands.css b/src/tests/unit-tests/files/expand-shorthands.css similarity index 100% rename from tests/files/expand-shorthands.css rename to src/tests/unit-tests/files/expand-shorthands.css diff --git a/tests/files/functions.css b/src/tests/unit-tests/files/functions.css similarity index 100% rename from tests/files/functions.css rename to src/tests/unit-tests/files/functions.css diff --git a/tests/files/ie.css b/src/tests/unit-tests/files/ie.css similarity index 100% rename from tests/files/ie.css rename to src/tests/unit-tests/files/ie.css diff --git a/tests/files/important.css b/src/tests/unit-tests/files/important.css similarity index 100% rename from tests/files/important.css rename to src/tests/unit-tests/files/important.css diff --git a/tests/files/nested.css b/src/tests/unit-tests/files/nested.css similarity index 100% rename from tests/files/nested.css rename to src/tests/unit-tests/files/nested.css diff --git a/tests/files/slashed.css b/src/tests/unit-tests/files/slashed.css similarity index 100% rename from tests/files/slashed.css rename to src/tests/unit-tests/files/slashed.css diff --git a/tests/files/specificity.css b/src/tests/unit-tests/files/specificity.css similarity index 100% rename from tests/files/specificity.css rename to src/tests/unit-tests/files/specificity.css diff --git a/tests/files/unicode.css b/src/tests/unit-tests/files/unicode.css similarity index 100% rename from tests/files/unicode.css rename to src/tests/unit-tests/files/unicode.css diff --git a/tests/files/values.css b/src/tests/unit-tests/files/values.css similarity index 100% rename from tests/files/values.css rename to src/tests/unit-tests/files/values.css diff --git a/tests/files/whitespace.css b/src/tests/unit-tests/files/whitespace.css similarity index 100% rename from tests/files/whitespace.css rename to src/tests/unit-tests/files/whitespace.css diff --git a/tests/CSSDeclarationBlockTest.php b/src/tests/unit-tests/php/Sabberworm/CSS/CSSDeclarationBlockTest.php similarity index 98% rename from tests/CSSDeclarationBlockTest.php rename to src/tests/unit-tests/php/Sabberworm/CSS/CSSDeclarationBlockTest.php index 0d311c7e..224ee6fc 100644 --- a/tests/CSSDeclarationBlockTest.php +++ b/src/tests/unit-tests/php/Sabberworm/CSS/CSSDeclarationBlockTest.php @@ -1,9 +1,11 @@ Date: Tue, 3 Jul 2012 20:58:36 +0200 Subject: [PATCH 8/8] Global namespace to PHP core Exception class --- src/php/Sabberworm/CSS/CSSParser.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/php/Sabberworm/CSS/CSSParser.php b/src/php/Sabberworm/CSS/CSSParser.php index c699284e..e258d3a7 100644 --- a/src/php/Sabberworm/CSS/CSSParser.php +++ b/src/php/Sabberworm/CSS/CSSParser.php @@ -44,7 +44,7 @@ private function parseList(CSSList $oList, $bIsRoot = false) { } else if($this->comes('}')) { $this->consume('}'); if($bIsRoot) { - throw new Exception("Unopened {"); + throw new \Exception("Unopened {"); } else { return; } @@ -54,7 +54,7 @@ private function parseList(CSSList $oList, $bIsRoot = false) { $this->consumeWhiteSpace(); } if(!$bIsRoot) { - throw new Exception("Unexpected end of document"); + throw new \Exception("Unexpected end of document"); } } @@ -97,7 +97,7 @@ private function parseAtRule() { private function parseIdentifier($bAllowFunctions = true) { $sResult = $this->parseCharacter(true); if($sResult === null) { - throw new Exception("Identifier expected, got {$this->peek(5)}"); + throw new \Exception("Identifier expected, got {$this->peek(5)}"); } $sCharacter; while(($sCharacter = $this->parseCharacter(true)) !== null) { @@ -133,7 +133,7 @@ private function parseStringValue() { while(!$this->comes($sQuote)) { $sContent = $this->parseCharacter(false); if($sContent === null) { - throw new Exception("Non-well-formed quoted string {$this->peek(3)}"); + throw new \Exception("Non-well-formed quoted string {$this->peek(3)}"); } $sResult .= $sContent; } @@ -214,7 +214,7 @@ private function parseRule() { $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”"); + throw new \Exception("! was followed by “".$sImportantMarker."”. Expected “important”"); } $oRule->setIsImportant(true); } @@ -403,13 +403,13 @@ private function consume($mValue = 1) { if(is_string($mValue)) { $iLength = mb_strlen($mValue, $this->sCharset); if(mb_substr($this->sText, $this->iCurrentPosition, $iLength, $this->sCharset) !== $mValue) { - throw new Exception("Expected $mValue, got ".$this->peek(5)); + throw new \Exception("Expected $mValue, got ".$this->peek(5)); } $this->iCurrentPosition += mb_strlen($mValue, $this->sCharset); return $mValue; } else { if($this->iCurrentPosition+$mValue > $this->iLength) { - throw new Exception("Tried to consume $mValue chars, exceeded file end"); + throw new \Exception("Tried to consume $mValue chars, exceeded file end"); } $sResult = mb_substr($this->sText, $this->iCurrentPosition, $mValue, $this->sCharset); $this->iCurrentPosition += $mValue; @@ -422,7 +422,7 @@ private function consumeExpression($mExpression) { 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)}"); + throw new \Exception("Expected pattern $mExpression not found, got: {$this->peek(5)}"); } private function consumeWhiteSpace() { @@ -449,7 +449,7 @@ private function isEnd() { 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)}"); + throw new \Exception("Required $sEnd not found, got {$this->peek(5)}"); } return $this->consume($iEndPos-$this->iCurrentPosition); }