diff --git a/phpQuery/CSSParser/CSSParser.php b/phpQuery/CSSParser/CSSParser.php new file mode 100755 index 0000000..5477927 --- /dev/null +++ b/phpQuery/CSSParser/CSSParser.php @@ -0,0 +1,465 @@ +sText = $sText; + $this->iCurrentPosition = 0; + $this->setCharset($sDefaultCharset); + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + $this->iLength = mb_strlen($this->sText, $this->sCharset); + } + + public function getCharset() { + return $this->sCharset; + } + + public function parse() { + $oResult = new CSSDocument(); + $this->parseDocument($oResult); + return $oResult; + } + + private function parseDocument(CSSDocument $oDocument) { + $this->consumeWhiteSpace(); + $this->parseList($oDocument, true); + } + + private function parseList(CSSList $oList, $bIsRoot = false) { + while(!$this->isEnd()) { + if($this->comes('@')) { + $oList->append($this->parseAtRule()); + } else if($this->comes('}')) { + $this->consume('}'); + if($bIsRoot) { + throw new Exception("Unopened {"); + } else { + return; + } + } else { + $oList->append($this->parseSelector()); + } + $this->consumeWhiteSpace(); + } + if(!$bIsRoot) { + throw new Exception("Unexpected end of document"); + } + } + + private function parseAtRule() { + $this->consume('@'); + $sIdentifier = $this->parseIdentifier(); + $this->consumeWhiteSpace(); + if($sIdentifier === 'media') { + $oResult = new CSSMediaQuery(); + $oResult->setQuery(trim($this->consumeUntil('{'))); + $this->consume('{'); + $this->consumeWhiteSpace(); + $this->parseList($oResult); + return $oResult; + } else if($sIdentifier === 'import') { + $oLocation = $this->parseURLValue(); + $this->consumeWhiteSpace(); + $sMediaQuery = null; + if(!$this->comes(';')) { + $sMediaQuery = $this->consumeUntil(';'); + } + $this->consume(';'); + return new CSSImport($oLocation, $sMediaQuery); + } else if($sIdentifier === 'charset') { + $sCharset = $this->parseStringValue(); + $this->consumeWhiteSpace(); + $this->consume(';'); + $this->setCharset($sCharset->getString()); + return new CSSCharset($sCharset); + } else { + //Unknown other at rule (font-face or such) + $this->consume('{'); + $this->consumeWhiteSpace(); + $oAtRule = new CSSAtRule($sIdentifier); + $this->parseRuleSet($oAtRule); + return $oAtRule; + } + } + + private function parseIdentifier($bAllowFunctions = true) { + $sResult = $this->parseCharacter(true); + if($sResult === null) { + throw new Exception("Identifier expected, got {$this->peek(5)}"); + } + $sCharacter; + while(($sCharacter = $this->parseCharacter(true)) !== null) { + $sResult .= $sCharacter; + } + if($bAllowFunctions && $this->comes('(')) { + $this->consume('('); + $sResult = new CSSFunction($sResult, $this->parseValue(array('=', ','))); + $this->consume(')'); + } + return $sResult; + } + + private function parseStringValue() { + $sBegin = $this->peek(); + $sQuote = null; + if($sBegin === "'") { + $sQuote = "'"; + } else if($sBegin === '"') { + $sQuote = '"'; + } + if($sQuote !== null) { + $this->consume($sQuote); + } + $sResult = ""; + $sContent = null; + if($sQuote === null) { + //Unquoted strings end in whitespace or with braces, brackets, parentheses + while(!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) { + $sResult .= $this->parseCharacter(false); + } + } else { + while(!$this->comes($sQuote)) { + $sContent = $this->parseCharacter(false); + if($sContent === null) { + throw new Exception("Non-well-formed quoted string {$this->peek(3)}"); + } + $sResult .= $sContent; + } + $this->consume($sQuote); + } + return new CSSString($sResult); + } + + private function parseCharacter($bIsForIdentifier) { + if($this->peek() === '\\') { + $this->consume('\\'); + if($this->comes('\n') || $this->comes('\r')) { + return ''; + } + $aMatches; + if(preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { + return $this->consume(1); + } + $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u'); + if(mb_strlen($sUnicode, $this->sCharset) < 6) { + //Consume whitespace after incomplete unicode escape + if(preg_match('/\\s/isSu', $this->peek())) { + if($this->comes('\r\n')) { + $this->consume(2); + } else { + $this->consume(1); + } + } + } + $iUnicode = intval($sUnicode, 16); + $sUtf32 = ""; + for($i=0;$i<4;$i++) { + $sUtf32 .= chr($iUnicode & 0xff); + $iUnicode = $iUnicode >> 8; + } + return iconv('utf-32le', $this->sCharset, $sUtf32); + } + if($bIsForIdentifier) { + if(preg_match('/[a-zA-Z0-9]|-|_/u', $this->peek()) === 1) { + return $this->consume(1); + } else if(ord($this->peek()) > 0xa1) { + return $this->consume(1); + } else { + return null; + } + } else { + return $this->consume(1); + } + // Does not reach here + return null; + } + + private function parseSelector() { + $oResult = new CSSDeclarationBlock(); + $oResult->setSelector($this->consumeUntil('{')); + $this->consume('{'); + $this->consumeWhiteSpace(); + $this->parseRuleSet($oResult); + return $oResult; + } + + private function parseRuleSet($oRuleSet) { + while(!$this->comes('}')) { + $oRuleSet->addRule($this->parseRule()); + $this->consumeWhiteSpace(); + } + $this->consume('}'); + } + + private function parseRule() { + $oRule = new CSSRule($this->parseIdentifier()); + $this->consumeWhiteSpace(); + $this->consume(':'); + $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule())); + $oRule->setValue($oValue); + if($this->comes('!')) { + $this->consume('!'); + $this->consumeWhiteSpace(); + $sImportantMarker = $this->consume(strlen('important')); + if(mb_convert_case($sImportantMarker, MB_CASE_LOWER) !== 'important') { + throw new Exception("! was followed by “".$sImportantMarker."”. Expected “important”"); + } + $oRule->setIsImportant(true); + } + if($this->comes(';')) { + $this->consume(';'); + } + return $oRule; + } + + private function parseValue($aListDelimiters) { + $aStack = array(); + $this->consumeWhiteSpace(); + while(!($this->comes('}') || $this->comes(';') || $this->comes('!') || $this->comes(')'))) { + if(count($aStack) > 0) { + $bFoundDelimiter = false; + foreach($aListDelimiters as $sDelimiter) { + if($this->comes($sDelimiter)) { + array_push($aStack, $this->consume($sDelimiter)); + $this->consumeWhiteSpace(); + $bFoundDelimiter = true; + break; + } + } + if(!$bFoundDelimiter) { + //Whitespace was the list delimiter + array_push($aStack, ' '); + } + } + array_push($aStack, $this->parsePrimitiveValue()); + $this->consumeWhiteSpace(); + } + foreach($aListDelimiters as $sDelimiter) { + if(count($aStack) === 1) { + return $aStack[0]; + } + $iStartPosition = null; + while(($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { + $iLength = 2; //Number of elements to be joined + for($i=$iStartPosition+2;$iaddListComponent($aStack[$i]); + } + array_splice($aStack, $iStartPosition-1, $iLength*2-1, array($oList)); + } + } + return $aStack[0]; + } + + private static function listDelimiterForRule($sRule) { + if(preg_match('/^font($|-)/', $sRule)) { + return array(',', '/', ' '); + } + return array(',', ' ', '/'); + } + + private function parsePrimitiveValue() { + $oValue = null; + $this->consumeWhiteSpace(); + if(is_numeric($this->peek()) || (($this->comes('-') || $this->comes('.')) && is_numeric($this->peek(1, 1)))) { + $oValue = $this->parseNumericValue(); + } else if($this->comes('#') || $this->comes('rgb') || $this->comes('hsl')) { + $oValue = $this->parseColorValue(); + } else if($this->comes('url')){ + $oValue = $this->parseURLValue(); + } else if($this->comes("'") || $this->comes('"')){ + $oValue = $this->parseStringValue(); + } else { + $oValue = $this->parseIdentifier(); + } + $this->consumeWhiteSpace(); + return $oValue; + } + + private function parseNumericValue($bForColor = false) { + $sSize = ''; + if($this->comes('-')) { + $sSize .= $this->consume('-'); + } + while(is_numeric($this->peek()) || $this->comes('.')) { + if($this->comes('.')) { + $sSize .= $this->consume('.'); + } else { + $sSize .= $this->consume(1); + } + } + $fSize = floatval($sSize); + $sUnit = null; + if($this->comes('%')) { + $sUnit = $this->consume('%'); + } else if($this->comes('em')) { + $sUnit = $this->consume('em'); + } else if($this->comes('ex')) { + $sUnit = $this->consume('ex'); + } else if($this->comes('px')) { + $sUnit = $this->consume('px'); + } else if($this->comes('deg')) { + $sUnit = $this->consume('deg'); + } else if($this->comes('s')) { + $sUnit = $this->consume('s'); + } else if($this->comes('cm')) { + $sUnit = $this->consume('cm'); + } else if($this->comes('pt')) { + $sUnit = $this->consume('pt'); + } else if($this->comes('in')) { + $sUnit = $this->consume('in'); + } else if($this->comes('pc')) { + $sUnit = $this->consume('pc'); + } else if($this->comes('cm')) { + $sUnit = $this->consume('cm'); + } else if($this->comes('mm')) { + $sUnit = $this->consume('mm'); + } + return new CSSSize($fSize, $sUnit, $bForColor); + } + + private function parseColorValue() { + $aColor = array(); + if($this->comes('#')) { + $this->consume('#'); + $sValue = $this->parseIdentifier(false); + if(mb_strlen($sValue, $this->sCharset) === 3) { + $sValue = $sValue[0].$sValue[0].$sValue[1].$sValue[1].$sValue[2].$sValue[2]; + } + $aColor = array('r' => new CSSSize(intval($sValue[0].$sValue[1], 16), null, true), 'g' => new CSSSize(intval($sValue[2].$sValue[3], 16), null, true), 'b' => new CSSSize(intval($sValue[4].$sValue[5], 16), null, true)); + } else { + $sColorMode = $this->parseIdentifier(false); + $this->consumeWhiteSpace(); + $this->consume('('); + $iLength = mb_strlen($sColorMode, $this->sCharset); + for($i=0;$i<$iLength;$i++) { + $this->consumeWhiteSpace(); + $aColor[$sColorMode[$i]] = $this->parseNumericValue(true); + $this->consumeWhiteSpace(); + if($i < ($iLength-1)) { + $this->consume(','); + } + } + $this->consume(')'); + } + return new CSSColor($aColor); + } + + private function parseURLValue() { + $bUseUrl = $this->comes('url'); + if($bUseUrl) { + $this->consume('url'); + $this->consumeWhiteSpace(); + $this->consume('('); + } + $this->consumeWhiteSpace(); + $oResult = new CSSURL($this->parseStringValue()); + if($bUseUrl) { + $this->consumeWhiteSpace(); + $this->consume(')'); + } + return $oResult; + } + + private function comes($sString, $iOffset = 0) { + if($this->isEnd()) { + return false; + } + return $this->peek($sString, $iOffset) == $sString; + } + + private function peek($iLength = 1, $iOffset = 0) { + if($this->isEnd()) { + return ''; + } + if(is_string($iLength)) { + $iLength = mb_strlen($iLength, $this->sCharset); + } + if(is_string($iOffset)) { + $iOffset = mb_strlen($iOffset, $this->sCharset); + } + return mb_substr($this->sText, $this->iCurrentPosition+$iOffset, $iLength, $this->sCharset); + } + + private function consume($mValue = 1) { + if(is_string($mValue)) { + $iLength = mb_strlen($mValue, $this->sCharset); + if(mb_substr($this->sText, $this->iCurrentPosition, $iLength, $this->sCharset) !== $mValue) { + throw new Exception("Expected $mValue, got ".$this->peek(5)); + } + $this->iCurrentPosition += mb_strlen($mValue, $this->sCharset); + return $mValue; + } else { + if($this->iCurrentPosition+$mValue > $this->iLength) { + throw new Exception("Tried to consume $mValue chars, exceeded file end"); + } + $sResult = mb_substr($this->sText, $this->iCurrentPosition, $mValue, $this->sCharset); + $this->iCurrentPosition += $mValue; + return $sResult; + } + } + + private function consumeExpression($mExpression) { + $aMatches; + if(preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) { + return $this->consume($aMatches[0][0]); + } + throw new Exception("Expected pattern $mExpression not found, got: {$this->peek(5)}"); + } + + private function consumeWhiteSpace() { + do { + while(preg_match('/\\s/isSu', $this->peek()) === 1) { + $this->consume(1); + } + } while($this->consumeComment()); + } + + private function consumeComment() { + if($this->comes('/*')) { + $this->consumeUntil('*/'); + $this->consume('*/'); + return true; + } + return false; + } + + private function isEnd() { + return $this->iCurrentPosition >= $this->iLength; + } + + private function consumeUntil($sEnd) { + $iEndPos = mb_strpos($this->sText, $sEnd, $this->iCurrentPosition, $this->sCharset); + if($iEndPos === false) { + throw new Exception("Required $sEnd not found, got {$this->peek(5)}"); + } + return $this->consume($iEndPos-$this->iCurrentPosition); + } + + private function inputLeft() { + return mb_substr($this->sText, $this->iCurrentPosition, -1, $this->sCharset); + } +} + diff --git a/phpQuery/CSSParser/README.md b/phpQuery/CSSParser/README.md new file mode 100755 index 0000000..594574f --- /dev/null +++ b/phpQuery/CSSParser/README.md @@ -0,0 +1,376 @@ +PHP CSS Parser +-------------- + +A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS. + +## Usage + +### Installation + +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 + +To use the CSS Parser, create a new instance. The constructor takes the following form: + + new CSSParser($sCssContents, $sCharset = 'utf-8'); + +The charset is used only if no @charset declaration is found in the CSS file. + +To read a file, for example, you’d do the following: + + $oCssParser = new CSSParser(file_get_contents('somefile.css')); + $oCssDocument = $oCssParser->parse(); + +The resulting CSS document structure can be manipulated prior to being output. + +### Manipulation + +The resulting data structure consists mainly of five basic types: `CSSList`, `CSSRuleSet`, `CSSRule`, `CSSSelector` and `CSSValue`. There are two additional types used: `CSSImport` and `CSSCharset` which you won’t use often. + +#### CSSList + +`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector) but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: + +* `CSSDocument` – representing the root of a CSS file. +* `CSSMediaQuery` – represents a subsection of a CSSList that only applies to a output device matching the contained media query. + +#### CSSRuleSet + +`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. +* `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. + +#### CSSRule + +`CSSRule`s just have a key (the rule) and multiple values (the part after the colon in the CSS file). This means the `values` attribute is an array consisting of arrays. The inner level of arrays is comma-separated in the CSS file while the outer level is whitespace-separated. + +#### CSSValue + +`CSSValue` is an abstract class that only defines the `__toString` method. The concrete subclasses are: + +* `CSSSize` – consists of a numeric `size` value and a unit. +* `CSSColor` – colors can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are alwas stored as an array of ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form. +* `CSSString` – this is just a wrapper for quoted strings to distinguish them from keywords; always output with double quotes. +* `CSSURL` – URLs in CSS; always output in URL("") notation. + +To access the items stored in a `CSSList` – like the document you got back when calling `$oCssParser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `CSSRuleSet`, a `CSSImport` or a `CSSCharset`. + +To append a new item (selector, media query, etc.) to an existing `CSSList`, construct it using the constructor for this class and use the `append($oItem)` method. + +If you want to manipulate a `CSSRuleSet`, use the methods `addRule(CSSRule $oRule)`, `getRules()` and `removeRule($mRule)` (which accepts either a CSSRule instance or a rule name; optionally suffixed by a dash to remove all related rules). + +#### Convenience methods + +There are a few convenience methods on CSSDocument to ease finding, manipulating and deleting rules: + +* `getAllDeclarationBlocks()` – does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`. +* `getAllRuleSets()` – does what it says; no matter how deeply nested your rule sets are. +* `getAllValues()` – finds all `CSSValue` objects inside `CSSRule`s. + +### Use cases + +#### Use `CSSParser` to prepend an id to all selectors + + $sMyId = "#my_id"; + $oParser = new CSSParser($sCssContents); + $oCss = $oParser->parse(); + 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 + $oSelector->setSelector($sMyId.' '.$oSelector->getSelector()); + } + } + +#### Shrink all absolute sizes to half + + $oParser = new CSSParser($sCssContents); + $oCss = $oParser->parse(); + foreach($oCss->getAllValues() as $mValue) { + if($mValue instanceof CSSSize && !$mValue->isRelative()) { + $mValue->setSize($mValue->getSize()/2); + } + } + +#### Remove unwanted rules + + $oParser = new CSSParser($sCssContents); + $oCss = $oParser->parse(); + foreach($oCss->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('font-'); //Note that the added dash will make this remove all rules starting with font- (like font-size, font-weight, etc.) as well as a potential font-rule + $oRuleSet->removeRule('cursor'); + } + +### Output + +To output the entire CSS document into a variable, just use `->__toString()`: + + $oCssParser = new CSSParser(file_get_contents('somefile.css')); + $oCssDocument = $oCssParser->parse(); + print $oCssDocument->__toString(); + +## Examples + +### Example 1 (At-Rules) + +#### Input + + @charset "utf-8"; + + @font-face { + font-family: "CrassRoots"; + src: url("../media/cr.ttf") + } + + html, body { + font-size: 1.6em + } + +#### 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" + ["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()`) + + @charset "utf-8";@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}html, body {font-size: 1.6em;} + +### Example 2 (Values) + +#### Input + + #header { + margin: 10px 2em 1cm 2%; + font-family: Verdana, Helvetica, "Gill Sans", sans-serif; + color: red !important; + } + +#### Structure (`var_dump()`) + + object(CSSDocument)#2 (1) { + ["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;} + +## To-Do + +* More convenience methods [like `selectorsWithElement($sId/Class/TagName)`, `removeSelector($oSelector)`, `attributesOfType($sType)`, `removeAttributesOfType($sType)`] +* Options for output (compact, verbose, etc.) +* Support for @namespace +* Named color support (using `CSSColor` instead of an anonymous string literal) +* 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/phpQuery/CSSParser/lib/CSSList.php b/phpQuery/CSSParser/lib/CSSList.php new file mode 100755 index 0000000..b2a4b23 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/lib/CSSProperties.php b/phpQuery/CSSParser/lib/CSSProperties.php new file mode 100755 index 0000000..15c9edb --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/lib/CSSRule.php b/phpQuery/CSSParser/lib/CSSRule.php new file mode 100755 index 0000000..d6ab6ac --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/lib/CSSRuleSet.php b/phpQuery/CSSParser/lib/CSSRuleSet.php new file mode 100755 index 0000000..2788f2f --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/lib/CSSValue.php b/phpQuery/CSSParser/lib/CSSValue.php new file mode 100755 index 0000000..6dc3fb9 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/lib/CSSValueList.php b/phpQuery/CSSParser/lib/CSSValueList.php new file mode 100755 index 0000000..35269e2 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/CSSDeclarationBlockTest.php b/phpQuery/CSSParser/tests/CSSDeclarationBlockTest.php new file mode 100755 index 0000000..0d311c7 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/CSSParserTests.php b/phpQuery/CSSParser/tests/CSSParserTests.php new file mode 100755 index 0000000..66071c2 --- /dev/null +++ b/phpQuery/CSSParser/tests/CSSParserTests.php @@ -0,0 +1,277 @@ +assertNotEquals('', $oParser->parse()->__toString()); + } catch(Exception $e) { + $this->fail($e); + } + } + closedir($rHandle); + } + } + + /** + * @depends testCssFiles + */ + function testColorParsing() { + $oDoc = $this->parsedStructureForFile('colortest'); + foreach($oDoc->getAllRuleSets() as $oRuleSet) { + if(!$oRuleSet instanceof CSSDeclarationBlock) { + continue; + } + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if($sSelector == '#mine') { + $aColorRule = $oRuleSet->getRules('color'); + $aValues = $aColorRule['color']->getValues(); + $this->assertSame('red', $aValues[0][0]); + $aColorRule = $oRuleSet->getRules('background-'); + $aValues = $aColorRule['background-color']->getValues(); + $this->assertEquals(array('r' => new CSSSize(35.0, null, true), 'g' => new CSSSize(35.0, null, true), 'b' => new CSSSize(35.0, null, true)), $aValues[0][0]->getColor()); + $aColorRule = $oRuleSet->getRules('border-color'); + $aValues = $aColorRule['border-color']->getValues(); + $this->assertEquals(array('r' => new CSSSize(10.0, null, true), 'g' => new CSSSize(100.0, null, true), 'b' => new CSSSize(230.0, null, true), 'a' => new CSSSize(0.3, null, true)), $aValues[0][0]->getColor()); + $aColorRule = $oRuleSet->getRules('outline-color'); + $aValues = $aColorRule['outline-color']->getValues(); + $this->assertEquals(array('r' => new CSSSize(34.0, null, true), 'g' => new CSSSize(34.0, null, true), 'b' => new CSSSize(34.0, null, true)), $aValues[0][0]->getColor()); + } + } + foreach($oDoc->getAllValues('background-') as $oColor) { + if($oColor->getColorDescription() === 'hsl') { + $this->assertEquals(array('h' => new CSSSize(220.0, null, true), 's' => new CSSSize(10.0, null, true), 'l' => new CSSSize(220.0, null, true)), $oColor->getColor()); + } + } + foreach($oDoc->getAllValues('color') as $sColor) { + $this->assertSame('red', $sColor); + } + } + + function testUnicodeParsing() { + $oDoc = $this->parsedStructureForFile('unicode'); + foreach($oDoc->getAllDeclarationBlocks() as $oRuleSet) { + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if(substr($sSelector, 0, strlen('.test-')) !== '.test-') { + continue; + } + $aContentRules = $oRuleSet->getRules('content'); + $aContents = $aContentRules['content']->getValues(); + $sCssString = $aContents[0][0]->__toString(); + if($sSelector == '.test-1') { + $this->assertSame('" "', $sCssString); + } + if($sSelector == '.test-2') { + $this->assertSame('"é"', $sCssString); + } + if($sSelector == '.test-3') { + $this->assertSame('" "', $sCssString); + } + if($sSelector == '.test-4') { + $this->assertSame('"𝄞"', $sCssString); + } + if($sSelector == '.test-5') { + $this->assertSame('"水"', $sCssString); + } + if($sSelector == '.test-6') { + $this->assertSame('"¥"', $sCssString); + } + if($sSelector == '.test-7') { + $this->assertSame('"\A"', $sCssString); + } + if($sSelector == '.test-8') { + $this->assertSame('"\"\""', $sCssString); + } + if($sSelector == '.test-9') { + $this->assertSame('"\"\\\'"', $sCssString); + } + if($sSelector == '.test-10') { + $this->assertSame('"\\\'\\\\"', $sCssString); + } + if($sSelector == '.test-11') { + $this->assertSame('"test"', $sCssString); + } + } + } + + function testSpecificity() { + $oDoc = $this->parsedStructureForFile('specificity'); + $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); + $oDeclarationBlock = $oDeclarationBlock[0]; + $aSelectors = $oDeclarationBlock->getSelectors(); + foreach($aSelectors as $oSelector) { + switch($oSelector->getSelector()) { + case "#test .help": + $this->assertSame(110, $oSelector->getSpecificity()); + break; + case "#file": + $this->assertSame(100, $oSelector->getSpecificity()); + break; + case ".help:hover": + $this->assertSame(20, $oSelector->getSpecificity()); + break; + case "ol li::before": + $this->assertSame(3, $oSelector->getSpecificity()); + break; + case "li.green": + $this->assertSame(11, $oSelector->getSpecificity()); + break; + default: + $this->fail("specificity: untested selector ".$oSelector->getSelector()); + } + } + $this->assertEquals(array(new CSSSelector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100')); + } + + function testManipulation() { + $oDoc = $this->parsedStructureForFile('atrules'); + $this->assertSame('@charset "utf-8";@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}html, body {font-size: 1.6em;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + foreach($oBlock->getSelectors() as $oSelector) { + //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id + $oSelector->setSelector('#my_id '.$oSelector->getSelector()); + } + } + $this->assertSame('@charset "utf-8";@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}#my_id html, #my_id body {font-size: 1.6em;}'."\n", $oDoc->__toString()); + + $oDoc = $this->parsedStructureForFile('values'); + $this->assertSame('#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;font-size: 10px;color: red !important;} +body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('font-'); + } + $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;} +body {color: green;}'."\n", $oDoc->__toString()); + } + + function testSlashedValues() { + $oDoc = $this->parsedStructureForFile('slashed'); + $this->assertSame('.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}'."\n", $oDoc->__toString()); + foreach($oDoc->getAllValues(null) as $mValue) { + if($mValue instanceof CSSSize && $mValue->isSize() && !$mValue->isRelative()) { + $mValue->setSize($mValue->getSize()*3); + } + } + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oRule = $oBlock->getRules('font'); + $oRule = $oRule['font']; + $oSpaceList = $oRule->getValue(); + $this->assertEquals(' ', $oSpaceList->getListSeparator()); + $oSlashList = $oSpaceList->getListComponents(); + $oCommaList = $oSlashList[1]; + $oSlashList = $oSlashList[0]; + $this->assertEquals(',', $oCommaList->getListSeparator()); + $this->assertEquals('/', $oSlashList->getListSeparator()); + $oRule = $oBlock->getRules('border-radius'); + $oRule = $oRule['border-radius']; + $oSlashList = $oRule->getValue(); + $this->assertEquals('/', $oSlashList->getListSeparator()); + $oSpaceList1 = $oSlashList->getListComponents(); + $oSpaceList2 = $oSpaceList1[1]; + $oSpaceList1 = $oSpaceList1[0]; + $this->assertEquals(' ', $oSpaceList1->getListSeparator()); + $this->assertEquals(' ', $oSpaceList2->getListSeparator()); + } + $this->assertSame('.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}'."\n", $oDoc->__toString()); + } + + function testFunctionSyntax() { + $oDoc = $this->parsedStructureForFile('functions'); + $sExpected = 'div.main {background-image: linear-gradient(rgb(0,0,0),rgb(255,255,255));} +.collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;margin-right: 0.2em;-moz-transition-property: -moz-transform;-moz-transition-duration: 0.2s;-moz-transform-origin: center 60%;} +.collapser.expanded::before, .collapser.expanded::-moz-before, .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);} +.collapser + * {height: 0;overflow: hidden;-moz-transition-property: height;-moz-transition-duration: 0.3s;} +.collapser.expanded + * {height: auto;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + + foreach($oDoc->getAllValues(null, true) as $mValue) { + if($mValue instanceof CSSSize && $mValue->isSize()) { + $mValue->setSize($mValue->getSize()*3); + } + } + $sExpected = str_replace(array('1.2em', '0.2em', '60%'), array('3.6em', '0.6em', '180%'), $sExpected); + $this->assertSame($sExpected, $oDoc->__toString()); + + foreach($oDoc->getAllValues(null, true) as $mValue) { + if($mValue instanceof CSSSize && !$mValue->isRelative() && !$mValue->isColorComponent()) { + $mValue->setSize($mValue->getSize()*2); + } + } + $sExpected = str_replace(array('0.2s', '0.3s', '90deg'), array('0.4s', '0.6s', '180deg'), $sExpected); + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testExpandShorthands() { + $oDoc = $this->parsedStructureForFile('expand-shorthands'); + $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid rgb(255,0,255);background: rgb(204,204,204) url("/images/foo.png") no-repeat left top;margin: 1em !important;padding: 2px 6px 3px;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + $oDoc->expandShorthands(); + $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;padding-left: 6px;border-top-color: rgb(255,0,255);border-right-color: rgb(255,0,255);border-bottom-color: rgb(255,0,255);border-left-color: rgb(255,0,255);border-top-style: solid;border-right-style: solid;border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;font-family: "Trebuchet MS",Georgia,serif;background-color: rgb(204,204,204);background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;background-position: left top;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testCreateShorthands() { + $oDoc = $this->parsedStructureForFile('create-shorthands'); + $sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;border-width: 2px;border-color: rgb(153,153,153);border-style: dotted;background-color: rgb(255,255,255);background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;margin-bottom: 4px;margin-left: 5px;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + $oDoc->createShorthands(); + $sExpected = 'body {background: rgb(255,255,255) url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;border: 2px dotted rgb(153,153,153);font: bold 2em Helvetica,Arial,sans-serif;}'."\n"; + $this->assertSame($sExpected, $oDoc->__toString()); + } + + function testListValueRemoval() { + $oDoc = $this->parsedStructureForFile('atrules'); + foreach($oDoc->getContents() as $oItem) { + if($oItem instanceof CSSAtRule) { + $oDoc->remove($oItem); + break; + } + } + $this->assertSame('@charset "utf-8";html, body {font-size: 1.6em;}'."\n", $oDoc->__toString()); + + $oDoc = $this->parsedStructureForFile('nested'); + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, false); + break; + } + $this->assertSame('html {some-other: -test(val1);} +@media screen {html {some: -test(val2);} +}#unrelated {other: yes;}'."\n", $oDoc->__toString()); + + $oDoc = $this->parsedStructureForFile('nested'); + foreach($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, true); + break; + } + $this->assertSame('@media screen {html {some: -test(val2);} +}#unrelated {other: yes;}'."\n", $oDoc->__toString()); + } + + function parsedStructureForFile($sFileName) { + $sFile = dirname(__FILE__).DIRECTORY_SEPARATOR.'files'.DIRECTORY_SEPARATOR."$sFileName.css"; + $oParser = new CSSParser(file_get_contents($sFile)); + return $oParser->parse(); + } + +} diff --git a/phpQuery/CSSParser/tests/files/-tobedone.css b/phpQuery/CSSParser/tests/files/-tobedone.css new file mode 100755 index 0000000..7ec1da9 --- /dev/null +++ b/phpQuery/CSSParser/tests/files/-tobedone.css @@ -0,0 +1,7 @@ +.some[selectors-may='contain-a-{'] { + +} + +.some { + filters: may(contain, a, ')'); +} diff --git a/phpQuery/CSSParser/tests/files/atrules.css b/phpQuery/CSSParser/tests/files/atrules.css new file mode 100755 index 0000000..adfa9f9 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/colortest.css b/phpQuery/CSSParser/tests/files/colortest.css new file mode 100755 index 0000000..41fe2a2 --- /dev/null +++ b/phpQuery/CSSParser/tests/files/colortest.css @@ -0,0 +1,10 @@ +#mine { + color: red; + border-color: rgba(10, 100, 230, 0.3); + outline-color: #222; + background-color: #232323; +} + +#yours { + background-color: hsl(220, 10, 220); +} diff --git a/phpQuery/CSSParser/tests/files/create-shorthands.css b/phpQuery/CSSParser/tests/files/create-shorthands.css new file mode 100755 index 0000000..c784d67 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/expand-shorthands.css b/phpQuery/CSSParser/tests/files/expand-shorthands.css new file mode 100755 index 0000000..89aab1e --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/functions.css b/phpQuery/CSSParser/tests/files/functions.css new file mode 100755 index 0000000..eabbd24 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/ie.css b/phpQuery/CSSParser/tests/files/ie.css new file mode 100755 index 0000000..6c0fb38 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/important.css b/phpQuery/CSSParser/tests/files/important.css new file mode 100755 index 0000000..edf24a8 --- /dev/null +++ b/phpQuery/CSSParser/tests/files/important.css @@ -0,0 +1,8 @@ +div.rating-cancel,div.star-rating{float:left;width:17px;height:15px;text-indent:-999em;cursor:pointer;display:block;background:transparent;overflow:hidden} +div.rating-cancel,div.rating-cancel a{background:url(images/delete.gif) no-repeat 0 -16px} +div.star-rating,div.star-rating a{background:url(images/star.gif) no-repeat 0 0px} +div.rating-cancel a,div.star-rating a{display:block;width:16px;height:100%;background-position:0 0px;border:0} +div.star-rating-on a{background-position:0 -16px!important} +div.star-rating-hover a{background-position:0 -32px} +div.star-rating-readonly a{cursor:default !important} +div.star-rating{background:transparent!important; overflow:hidden!important} \ No newline at end of file diff --git a/phpQuery/CSSParser/tests/files/nested.css b/phpQuery/CSSParser/tests/files/nested.css new file mode 100755 index 0000000..b59dc80 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/slashed.css b/phpQuery/CSSParser/tests/files/slashed.css new file mode 100755 index 0000000..5b629be --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/specificity.css b/phpQuery/CSSParser/tests/files/specificity.css new file mode 100755 index 0000000..82a2939 --- /dev/null +++ b/phpQuery/CSSParser/tests/files/specificity.css @@ -0,0 +1,7 @@ +#test .help, +#file, +.help:hover, +li.green, +ol li::before { + font-family: Helvetica; +} diff --git a/phpQuery/CSSParser/tests/files/unicode.css b/phpQuery/CSSParser/tests/files/unicode.css new file mode 100755 index 0000000..2482320 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/values.css b/phpQuery/CSSParser/tests/files/values.css new file mode 100755 index 0000000..1f41863 --- /dev/null +++ b/phpQuery/CSSParser/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/phpQuery/CSSParser/tests/files/whitespace.css b/phpQuery/CSSParser/tests/files/whitespace.css new file mode 100755 index 0000000..6b21c24 --- /dev/null +++ b/phpQuery/CSSParser/tests/files/whitespace.css @@ -0,0 +1,3 @@ +.test { + background-image : url ( 4px ) ; +} diff --git a/phpQuery/CSSParser/tests/quickdump.php b/phpQuery/CSSParser/tests/quickdump.php new file mode 100755 index 0000000..071a72b --- /dev/null +++ b/phpQuery/CSSParser/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"; + diff --git a/phpQuery/phpQuery.php b/phpQuery/phpQuery.php index 08f22fc..16e4c45 100644 --- a/phpQuery/phpQuery.php +++ b/phpQuery/phpQuery.php @@ -24,6 +24,7 @@ require_once(dirname(__FILE__).'/phpQuery/Callback.php'); require_once(dirname(__FILE__).'/phpQuery/phpQueryObject.php'); require_once(dirname(__FILE__).'/phpQuery/compat/mbstring.php'); +require_once(dirname(__FILE__).'/CSSParser/CSSParser.php'); /** * Static namespace for phpQuery functions. * diff --git a/phpQuery/phpQuery/default.css b/phpQuery/phpQuery/default.css new file mode 100644 index 0000000..5fb4aac --- /dev/null +++ b/phpQuery/phpQuery/default.css @@ -0,0 +1,81 @@ +html, address, +blockquote, +body, dd, div, +dl, dt, fieldset, form, +frame, frameset, +h1, h2, h3, h4, +h5, h6, noframes, +ol, p, ul, center, +dir, hr, menu, pre { display: block; unicode-bidi: embed } +span { display: inline } +li { display: list-item } +head { display: none } +table { display: table } +tr { display: table-row } +thead { display: table-header-group } +tbody { display: table-row-group } +tfoot { display: table-footer-group } +col { display: table-column } +colgroup { display: table-column-group } +td, th { display: table-cell } +caption { display: table-caption } +th { font-weight: bolder; text-align: center } +caption { text-align: center } +body { margin: 8px; color: #000; background-color: #fff; font-size: 16px;} +h1 { font-size: 2em; margin: .67em 0 } +h2 { font-size: 1.5em; margin: .75em 0 } +h3 { font-size: 1.17em; margin: .83em 0 } +h4, p, +blockquote, ul, +fieldset, form, +ol, dl, dir, +menu { margin: 1.12em 0 } +h5 { font-size: .83em; margin: 1.5em 0 } +h6 { font-size: .75em; margin: 1.67em 0 } +h1, h2, h3, h4, +h5, h6, b, +strong { font-weight: bolder; display: inline; } +blockquote { margin-left: 40px; margin-right: 40px } +a { color: #0000ff; } +i, cite, em, +var, address { font-style: italic } +i, cite, em { display: inline } +pre, tt, code, +kbd, samp { font-family: monospace } +pre { white-space: pre } +button, textarea, +input, select { display: inline-block } +big { font-size: 1.17em; display: inline; } +small, sub, sup { font-size: .83em; display: inline; } +sub { vertical-align: sub } +sup { vertical-align: super } +table { border-spacing: 2px; } +thead, tbody, +tfoot { vertical-align: middle } +td, th, tr { vertical-align: inherit } +s, strike, del { text-decoration: line-through } +hr { border: 1px inset } +ol, ul, dir, +menu, dd { margin-left: 40px } +ol { list-style-type: decimal } +ol ul, ul ol, +ul ul, ol ol { margin-top: 0; margin-bottom: 0 } +u, ins { text-decoration: underline } +br:before { content: "\A"; white-space: pre-line } +center { text-align: center } +:link, :visited { text-decoration: underline } +:focus { outline: thin dotted invert } + +/* Begin bidirectionality settings (do not change) */ +BDO[DIR="ltr"] { direction: ltr; unicode-bidi: bidi-override } +BDO[DIR="rtl"] { direction: rtl; unicode-bidi: bidi-override } + +*[DIR="ltr"] { direction: ltr; unicode-bidi: embed } +*[DIR="rtl"] { direction: rtl; unicode-bidi: embed } + +@media print { + h1 { page-break-before: always } + h1, h2, h3, + h4, h5, h6 { page-break-after: avoid } + ul, ol, dl { page-break-before: avoid } +} \ No newline at end of file diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 9693cb9..570ecab 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -75,11 +75,30 @@ class phpQueryObject * @access private */ protected $current = null; + + /** + * Indicates whether CSS has been parsed or not. We only parse CSS if needed. + * @access private + */ + protected $cssIsParsed = array(); + /** + * A collection of complete CSS selector strings. + * @access private; + */ + protected $cssString = array(); /** * Enter description here... * * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery */ + + protected $attribute_css_mapping = array( + 'bgcolor' => 'background-color', + 'text' => 'color', + 'width' => 'width', + 'height' => 'height' + ); + public function __construct($documentID) { // if ($documentID instanceof self) // var_dump($documentID->getDocumentID()); @@ -1360,22 +1379,132 @@ public function __loadSuccess($html) { ->markup($html); } } + /** - * Enter description here... + * Allows users to enter strings of CSS selectors. Useful + * when the CSS is loaded via style or @imports that phpQuery can't load + * because it doesn't know the URL context of the request. + */ + public function addCSS($string) { + if(!isset($this->cssString[$this->getDocumentID()])) { + $this->cssString[$this->getDocumentID()] = ''; + } + $this->cssString[$this->getDocumentID()] .= $string; + $this->parseCSS(); + } + /** + * Either sets the CSS property of an object or retrieves the + * CSS property of a proejct. * - * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery + * @return string of css property value * @todo */ - public function css() { - // TODO - return $this; - } + public function css($property_name, $value = FALSE) { + if(!isset($this->cssIsParsed[$this->getDocumentID()]) || $this->cssIsParsed[$this->getDocumentID()] = false) { + $this->parseCSS(); + } + $data = phpQuery::data($this->get(0), 'phpquery_css', null, $this->getDocumentID()); + if(!$value) { + if(isset($data[$property_name])) { + return $data[$property_name]['value']; + } + return null; + } + $specificity = (isset($data[$property_name])) + ? $data[$property_name]['specificity'] + 1 + : 1000; + $data[$property_name] = array('specificity' => $specificity, 'value' => $value); + phpQuery::data($this->get(0), 'phpquery_css', $data, $this->getDocumentID()); + $this->bubbleCSS(phpQuery::pq($this->get(0), $this->getDocumentID())); + } + + public function parseCSS() { + if(!isset($this->cssString[$this->getDocumentID()])) { + $this->cssString[$this->getDocumentID()] = file_get_contents(dirname(__FILE__) .'/default.css'); + } + foreach(phpQuery::pq('style', $this->getDocumentID()) as $style) { + $this->cssString[$this->getDocumentID()] .= phpQuery::pq($style)->text(); + } + + $CssParser = new CSSParser($this->cssString[$this->getDocumentID()]); + $CssDocument = $CssParser->parse(); + foreach($CssDocument->getAllRuleSets() as $ruleset) { + foreach($ruleset->getSelector() as $selector) { + $specificity = $selector->getSpecificity(); + foreach(phpQuery::pq($selector->getSelector(), $this->getDocumentID()) as $el) { + $existing = pq($el)->data('phpquery_css'); + $ruleset->expandShorthands(); + foreach($ruleset->getRules() as $rule => $value) { + if(!isset($existing[$rule]) || $existing[$rule]['specificity'] <= $specificity) { + $value = $value->getValue(); + $value = (is_object($value)) + ? $value->__toString() + : $value; + $existing[$rule] = array('specificity' => $specificity, + 'value' => $value); + } + } + phpQuery::pq($el)->data('phpquery_css', $existing); + $this->bubbleCSS(phpQuery::pq($el)); + } + } + } + foreach(phpQuery::pq('*', $this->getDocumentID()) as $el) { + $existing = pq($el)->data('phpquery_css'); + $style = pq($el)->attr('style'); + $style = strlen($style) ? explode(';', $style) : array(); + foreach($this->attribute_css_mapping as $map => $css_equivalent) { + if($el->hasAttribute($map)) { + $style[] = $css_equivalent .':'. pq($el)->attr($map); + } + } + if(count($style)) { + $CssParser = new CSSParser('#ruleset {'. implode(';', $style) .'}'); + $CssDocument = $CssParser->parse(); + $ruleset = $CssDocument->getAllRulesets(); + $ruleset = reset($ruleset); + $ruleset->expandShorthands(); + foreach($ruleset->getRules() as $rule => $value) { + if(!isset($existing[$rule]) || 1000 >= $existing[$rule]['specificity']) { + $value = $value->getValue(); + $value = (is_object($value)) + ? $value->__toString() + : $value; + $existing[$rule] = array('specificity' => 1000, + 'value' => $value); + } + } + phpQuery::pq($el)->data('phpquery_css', $existing); + $this->bubbleCSS(phpQuery::pq($el)); + } + } + } + + protected function bubbleCSS($element) { + $style = $element->data('phpquery_css'); + foreach($element->children() as $element_child) { + $existing = phpQuery::pq($element_child)->data('phpquery_css'); + foreach($style as $rule => $value) { + if(!isset($existing[$rule]) || $value['specificity'] > $existing[$rule]['specificity']) { + $existing[$rule] = $value; + } + } + phpQuery::pq($element_child)->data('phpquery_css', $existing); + if(phpQuery::pq($element_child)->children()->length) { + $this->bubbleCSS(phpQuery::pq($element_child)); + } + } + } + /** * @todo * */ public function show(){ - // TODO + $display = ($this->data('phpquery_display_state')) + ? $this->data('phpquery_display_state') + : 'block'; + $this->css('display', $display); return $this; } /** @@ -1383,7 +1512,8 @@ public function show(){ * */ public function hide(){ - // TODO + $this->data('phpquery_display_state', $this->css('display')); + $this->css('display', 'none'); return $this; } /**