From ac00fdf87d31a876c2079067d44670a405d861c5 Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Thu, 29 Mar 2012 15:18:28 -0700 Subject: [PATCH 01/13] Completed basic style elements and style attributes with specificity. --- phpQuery/phpQuery/phpQueryObject.php | 130 ++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 3 deletions(-) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 9693cb9..acd8ff5 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -75,6 +75,17 @@ 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... * @@ -1366,10 +1377,123 @@ public function __loadSuccess($html) { * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery * @todo */ - public function css() { - // TODO - return $this; + public function css($property_name, $value = FALSE) { + if(!isset($this->cssIsParsed[$this->getDocumentID()])) { + $this->parseCSS(); + } + $data = phpQuery::data($this->get(0), 'phpquery_css', null, $this->getDocumentID()); + if(!$value) { + return $data[$property_name]['value']; + } + $data[$property_name]['value'] = $value; + phpQuery::data($this->get(0), 'phpquery_css', $data, $this->getDocumentID()); + } + + protected function parseCSS() { + if(!isset($this->cssString[$this->getDocumentID()])) { + $this->cssString[$this->getDocumentID()] = ''; + } + foreach(phpQuery::pq('style', $this->getDocumentID()) as $style) { + $this->cssString[$this->getDocumentID()] .= phpQuery::pq($style)->text(); + } + $str = preg_replace("/\/\*(.*)?\*\//Usi", "", $this->cssString[$this->getDocumentID()]); + + $parts = explode("}",$str); + if(count($parts) > 0) { + foreach($parts as $part) { + if(strpos($part, '{') !== false) { + list($keystr,$codestr) = explode("{", $part); + $keys = explode(",",trim($keystr)); + if(count($keys) > 0) { + foreach($keys as $key) { + if(strlen($key) > 0) { + $key = str_replace("\n", "", $key); + $key = str_replace("\\", "", $key); + $this->addCSSSelector($key, trim($codestr)); + } + } + } + } + } + } + $this->addStyleOverrides(); + } + + protected function addStyleOverrides() { + $specificity = 1000; + foreach(phpQuery::pq('*[style]', $this->getDocumentID()) as $el) { + $existing = pq($el)->data('phpquery_css'); + $codes = explode(";", phpQuery::pq($el)->attr('style')); + foreach($codes as $code) { + $explode = explode(":",$code,2); + if(count($explode) > 1) { + list($code_key, $code_value) = $explode; + if(strlen($code_key) > 0) { + if(!isset($existing[$code_key]) || $specificity >= $existing[$code_key]['specificity']) { + $existing[trim(strtolower($code_key))] = array('specificity' => $specificity, + 'value' => trim(strtolower($code_value))); + } + } + } + phpQuery::pq($el)->data('phpquery_css', $existing); + } + } + } + + protected function addCSSSelector($key, $code) { + foreach(phpQuery::pq($key, $this->getDocumentID()) as $el) { + $existing = pq($el)->data('phpquery_css'); + $specificity = $this->getSpecificity($key); + $codes = explode(";",$code); + if(count($codes) > 0) { + foreach($codes as $code) { + $code = trim($code); + $explode = explode(":",$code,2); + if(count($explode) > 1) { + list($code_key, $code_value) = $explode; + if(strlen($code_key) > 0) { + if(!isset($existing[$code_key]) || $specificity >= $existing[$code_key]['specificity']) { + $existing[trim(strtolower($code_key))] = array('specificity' => $specificity, + 'value' => trim(strtolower($code_value))); + } + } + } + } + } + phpQuery::pq($el)->data('phpquery_css', $existing); + } + } + + /** + * Returns a specificity count to the given selector. + * Higher specificity means it overrides other styles. + * @param string selector The CSS Selector + */ + public function getSpecificity($selector) { + $selector = $this->parseSelector($selector); + if($selector[0][0] == ' ') { + unset($selector[0][0]); + } + $selector = $selector[0]; + $specificity = 0; + foreach($selector as $part) { + switch(substr(str_replace('*', '', $part), 0, 1)) { + case '.': + $specificity += 10; + case '#': + $specificity += 100; + case ':': + $specificity++; + default: + $specificity++; + } + if(strpos($part, '[id=') != false) { + $specificity += 100; + } + } + return $specificity; } + /** * @todo * From 77b025ac52ef9d7bd167215993a433e6e0a72d0d Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Thu, 29 Mar 2012 17:22:30 -0700 Subject: [PATCH 02/13] Added extra comments and the ability of a user to add extra CSS values. --- phpQuery/phpQuery/phpQueryObject.php | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index acd8ff5..6fc89bc 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1371,14 +1371,28 @@ 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($property_name, $value = FALSE) { - if(!isset($this->cssIsParsed[$this->getDocumentID()])) { + if(!isset($this->cssIsParsed[$this->getDocumentID()]) || $this->cssIsParsed[$this->getDocumentID()] = false) { $this->parseCSS(); } $data = phpQuery::data($this->get(0), 'phpquery_css', null, $this->getDocumentID()); @@ -1390,6 +1404,7 @@ public function css($property_name, $value = FALSE) { } protected function parseCSS() { + var_dump($this->cssString[$this->getDocumentID()]); if(!isset($this->cssString[$this->getDocumentID()])) { $this->cssString[$this->getDocumentID()] = ''; } From 2ac2c92de9f6fd4a8a45a92ef1f7125086b5bc78 Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Fri, 30 Mar 2012 07:49:56 -0700 Subject: [PATCH 03/13] Added CSSParser library. --- phpQuery/CSSParser/CSSParser.php | 465 +++++++++++++ phpQuery/CSSParser/README.md | 376 ++++++++++ phpQuery/CSSParser/lib/CSSList.php | 236 +++++++ phpQuery/CSSParser/lib/CSSProperties.php | 124 ++++ phpQuery/CSSParser/lib/CSSRule.php | 135 ++++ phpQuery/CSSParser/lib/CSSRuleSet.php | 653 ++++++++++++++++++ phpQuery/CSSParser/lib/CSSValue.php | 110 +++ phpQuery/CSSParser/lib/CSSValueList.php | 92 +++ .../tests/CSSDeclarationBlockTest.php | 223 ++++++ phpQuery/CSSParser/tests/CSSParserTests.php | 277 ++++++++ phpQuery/CSSParser/tests/files/-tobedone.css | 7 + phpQuery/CSSParser/tests/files/atrules.css | 10 + phpQuery/CSSParser/tests/files/colortest.css | 10 + .../tests/files/create-shorthands.css | 7 + .../tests/files/expand-shorthands.css | 7 + phpQuery/CSSParser/tests/files/functions.css | 22 + phpQuery/CSSParser/tests/files/ie.css | 6 + phpQuery/CSSParser/tests/files/important.css | 8 + phpQuery/CSSParser/tests/files/nested.css | 17 + phpQuery/CSSParser/tests/files/slashed.css | 4 + .../CSSParser/tests/files/specificity.css | 7 + phpQuery/CSSParser/tests/files/unicode.css | 12 + phpQuery/CSSParser/tests/files/values.css | 11 + phpQuery/CSSParser/tests/files/whitespace.css | 3 + phpQuery/CSSParser/tests/quickdump.php | 15 + 25 files changed, 2837 insertions(+) create mode 100755 phpQuery/CSSParser/CSSParser.php create mode 100755 phpQuery/CSSParser/README.md create mode 100755 phpQuery/CSSParser/lib/CSSList.php create mode 100755 phpQuery/CSSParser/lib/CSSProperties.php create mode 100755 phpQuery/CSSParser/lib/CSSRule.php create mode 100755 phpQuery/CSSParser/lib/CSSRuleSet.php create mode 100755 phpQuery/CSSParser/lib/CSSValue.php create mode 100755 phpQuery/CSSParser/lib/CSSValueList.php create mode 100755 phpQuery/CSSParser/tests/CSSDeclarationBlockTest.php create mode 100755 phpQuery/CSSParser/tests/CSSParserTests.php create mode 100755 phpQuery/CSSParser/tests/files/-tobedone.css create mode 100755 phpQuery/CSSParser/tests/files/atrules.css create mode 100755 phpQuery/CSSParser/tests/files/colortest.css create mode 100755 phpQuery/CSSParser/tests/files/create-shorthands.css create mode 100755 phpQuery/CSSParser/tests/files/expand-shorthands.css create mode 100755 phpQuery/CSSParser/tests/files/functions.css create mode 100755 phpQuery/CSSParser/tests/files/ie.css create mode 100755 phpQuery/CSSParser/tests/files/important.css create mode 100755 phpQuery/CSSParser/tests/files/nested.css create mode 100755 phpQuery/CSSParser/tests/files/slashed.css create mode 100755 phpQuery/CSSParser/tests/files/specificity.css create mode 100755 phpQuery/CSSParser/tests/files/unicode.css create mode 100755 phpQuery/CSSParser/tests/files/values.css create mode 100755 phpQuery/CSSParser/tests/files/whitespace.css create mode 100755 phpQuery/CSSParser/tests/quickdump.php 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"; + From 8387fb7f15a3ab2c96222915d43d2e24a9cfb4aa Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Fri, 30 Mar 2012 10:23:58 -0700 Subject: [PATCH 04/13] Completed integrating CSSParser and added bubbling of selectors. --- phpQuery/phpQuery.php | 1 + phpQuery/phpQuery/phpQueryObject.php | 151 ++++++++++----------------- 2 files changed, 58 insertions(+), 94 deletions(-) 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/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 6fc89bc..7d56322 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1404,109 +1404,72 @@ public function css($property_name, $value = FALSE) { } protected function parseCSS() { - var_dump($this->cssString[$this->getDocumentID()]); if(!isset($this->cssString[$this->getDocumentID()])) { $this->cssString[$this->getDocumentID()] = ''; } foreach(phpQuery::pq('style', $this->getDocumentID()) as $style) { $this->cssString[$this->getDocumentID()] .= phpQuery::pq($style)->text(); } - $str = preg_replace("/\/\*(.*)?\*\//Usi", "", $this->cssString[$this->getDocumentID()]); - - $parts = explode("}",$str); - if(count($parts) > 0) { - foreach($parts as $part) { - if(strpos($part, '{') !== false) { - list($keystr,$codestr) = explode("{", $part); - $keys = explode(",",trim($keystr)); - if(count($keys) > 0) { - foreach($keys as $key) { - if(strlen($key) > 0) { - $key = str_replace("\n", "", $key); - $key = str_replace("\\", "", $key); - $this->addCSSSelector($key, trim($codestr)); - } - } - } - } - } + + $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('*[style]', $this->getDocumentID()) as $el) { + $existing = pq($el)->data('phpquery_css'); + $CssParser = new CSSParser('#ruleset {'. pq($el)->attr('style') .'}'); + $CssDocument = $CssParser->parse(); + $ruleset = $CssDocument->getAllRulesets(); + $ruleset = reset($ruleset); + $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' => 1000, + 'value' => $value); + } + } + phpQuery::pq($el)->data('phpquery_css', $existing); + $this->bubbleCSS(phpQuery::pq($el)); } - $this->addStyleOverrides(); - } - - protected function addStyleOverrides() { - $specificity = 1000; - foreach(phpQuery::pq('*[style]', $this->getDocumentID()) as $el) { - $existing = pq($el)->data('phpquery_css'); - $codes = explode(";", phpQuery::pq($el)->attr('style')); - foreach($codes as $code) { - $explode = explode(":",$code,2); - if(count($explode) > 1) { - list($code_key, $code_value) = $explode; - if(strlen($code_key) > 0) { - if(!isset($existing[$code_key]) || $specificity >= $existing[$code_key]['specificity']) { - $existing[trim(strtolower($code_key))] = array('specificity' => $specificity, - 'value' => trim(strtolower($code_value))); - } - } - } - phpQuery::pq($el)->data('phpquery_css', $existing); - } - } } - protected function addCSSSelector($key, $code) { - foreach(phpQuery::pq($key, $this->getDocumentID()) as $el) { - $existing = pq($el)->data('phpquery_css'); - $specificity = $this->getSpecificity($key); - $codes = explode(";",$code); - if(count($codes) > 0) { - foreach($codes as $code) { - $code = trim($code); - $explode = explode(":",$code,2); - if(count($explode) > 1) { - list($code_key, $code_value) = $explode; - if(strlen($code_key) > 0) { - if(!isset($existing[$code_key]) || $specificity >= $existing[$code_key]['specificity']) { - $existing[trim(strtolower($code_key))] = array('specificity' => $specificity, - 'value' => trim(strtolower($code_value))); - } - } - } - } - } - phpQuery::pq($el)->data('phpquery_css', $existing); - } - } - - /** - * Returns a specificity count to the given selector. - * Higher specificity means it overrides other styles. - * @param string selector The CSS Selector - */ - public function getSpecificity($selector) { - $selector = $this->parseSelector($selector); - if($selector[0][0] == ' ') { - unset($selector[0][0]); - } - $selector = $selector[0]; - $specificity = 0; - foreach($selector as $part) { - switch(substr(str_replace('*', '', $part), 0, 1)) { - case '.': - $specificity += 10; - case '#': - $specificity += 100; - case ':': - $specificity++; - default: - $specificity++; - } - if(strpos($part, '[id=') != false) { - $specificity += 100; - } - } - return $specificity; + 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)); + } + } } /** From 54fb468aae4cfd4778bc6e325762e36afad51b59 Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Fri, 30 Mar 2012 11:55:23 -0700 Subject: [PATCH 05/13] Fixed show() and hide() and also how css syles bubble down when settings CSS on a parent. --- phpQuery/phpQuery/phpQueryObject.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 7d56322..6a72b51 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1397,10 +1397,17 @@ public function css($property_name, $value = FALSE) { } $data = phpQuery::data($this->get(0), 'phpquery_css', null, $this->getDocumentID()); if(!$value) { - return $data[$property_name]['value']; + if(isset($data[$property_name])) { + return $data[$property_name]['value']; + } + return null; } - $data[$property_name]['value'] = $value; + $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())); } protected function parseCSS() { @@ -1442,7 +1449,7 @@ protected function parseCSS() { $ruleset = reset($ruleset); $ruleset->expandShorthands(); foreach($ruleset->getRules() as $rule => $value) { - if(!isset($existing[$rule]) || $existing[$rule]['specificity'] < $specificity) { + if(!isset($existing[$rule]) || 1000 > $existing[$rule]['specificity']) { $value = $value->getValue(); $value = (is_object($value)) ? $value->__toString() @@ -1477,7 +1484,10 @@ protected function bubbleCSS($element) { * */ public function show(){ - // TODO + $display = ($this->data('phpquery_display_state')) + ? $this->data('phpquery_display_state') + : 'block'; + $this->css('display', $display); return $this; } /** @@ -1485,7 +1495,8 @@ public function show(){ * */ public function hide(){ - // TODO + $this->data('phpquery_display_state', $this->css('display')); + $this->css('display', 'none'); return $this; } /** From 806c88b0c77d0299388f62fb9716b69b9766dd24 Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Fri, 30 Mar 2012 14:29:21 -0700 Subject: [PATCH 06/13] Added default CSS file, from http://www.w3.org/TR/CSS21/sample.html --- phpQuery/phpQuery/default.css | 80 ++++++++++++++++++++++++++++ phpQuery/phpQuery/phpQueryObject.php | 2 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 phpQuery/phpQuery/default.css diff --git a/phpQuery/phpQuery/default.css b/phpQuery/phpQuery/default.css new file mode 100644 index 0000000..fd70cf3 --- /dev/null +++ b/phpQuery/phpQuery/default.css @@ -0,0 +1,80 @@ +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;} +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 } +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 6a72b51..0426ef4 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1412,7 +1412,7 @@ public function css($property_name, $value = FALSE) { protected function parseCSS() { if(!isset($this->cssString[$this->getDocumentID()])) { - $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(); From 1a1cf76d4025bc237dde14cec5f15ddc50ffca0c Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Fri, 30 Mar 2012 14:49:40 -0700 Subject: [PATCH 07/13] Added default font size. --- phpQuery/phpQuery/default.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpQuery/phpQuery/default.css b/phpQuery/phpQuery/default.css index fd70cf3..e862b6b 100644 --- a/phpQuery/phpQuery/default.css +++ b/phpQuery/phpQuery/default.css @@ -21,7 +21,7 @@ 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;} +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 } From 22a50bd151629dec1ec28956f936ee8f74a2be64 Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Fri, 30 Mar 2012 15:31:07 -0700 Subject: [PATCH 08/13] Added attribute-equivalencies for things like 'bgcolor' --- phpQuery/phpQuery/phpQueryObject.php | 47 ++++++++++++++++++---------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 0426ef4..7ea100a 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -91,6 +91,12 @@ class phpQueryObject * * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery */ + + protected $attribute_css_mapping = array( + 'bgcolor' => 'background-color', + 'text' => 'color', + ); + public function __construct($documentID) { // if ($documentID instanceof self) // var_dump($documentID->getDocumentID()); @@ -1441,25 +1447,34 @@ protected function parseCSS() { } } } - foreach(phpQuery::pq('*[style]', $this->getDocumentID()) as $el) { + foreach(phpQuery::pq('*', $this->getDocumentID()) as $el) { $existing = pq($el)->data('phpquery_css'); - $CssParser = new CSSParser('#ruleset {'. pq($el)->attr('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); + $style = pq($el)->attr('style'); + $attribute_style = ''; + foreach($this->attribute_css_mapping as $map => $css_equivalent) { + if($el->hasAttribute($map)) { + $attribute_style .= $css_equivalent .':'. pq($el)->attr($map) .';'; + } + } + if(strlen($style) || strlen($attribute_style)) { + $CssParser = new CSSParser('#ruleset {'. ltrim($style .';'. $attribute_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)); } - phpQuery::pq($el)->data('phpquery_css', $existing); - $this->bubbleCSS(phpQuery::pq($el)); } } From d1ffc378aa29d711a5ebf360b154041d873328eb Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Sat, 31 Mar 2012 14:33:43 -0700 Subject: [PATCH 09/13] Added more elegant handling of style attributes, as well as a default color for links. --- phpQuery/phpQuery/default.css | 1 + phpQuery/phpQuery/phpQueryObject.php | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/phpQuery/phpQuery/default.css b/phpQuery/phpQuery/default.css index e862b6b..5fb4aac 100644 --- a/phpQuery/phpQuery/default.css +++ b/phpQuery/phpQuery/default.css @@ -36,6 +36,7 @@ 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 } diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 7ea100a..2683f01 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1450,14 +1450,14 @@ protected function parseCSS() { foreach(phpQuery::pq('*', $this->getDocumentID()) as $el) { $existing = pq($el)->data('phpquery_css'); $style = pq($el)->attr('style'); - $attribute_style = ''; + $style = strlen($style) ? explode(';', $style) : array(); foreach($this->attribute_css_mapping as $map => $css_equivalent) { if($el->hasAttribute($map)) { - $attribute_style .= $css_equivalent .':'. pq($el)->attr($map) .';'; + $style[] = $css_equivalent .':'. pq($el)->attr($map) .';'; } } - if(strlen($style) || strlen($attribute_style)) { - $CssParser = new CSSParser('#ruleset {'. ltrim($style .';'. $attribute_style, ';') .'}'); + if(count($style) || count($attribute_style)) { + $CssParser = new CSSParser('#ruleset {'. implode(';', $style) .'}'); $CssDocument = $CssParser->parse(); $ruleset = $CssDocument->getAllRulesets(); $ruleset = reset($ruleset); From 666ea2c048825e40cee1c92ad1ac0a7d2412877d Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Sat, 31 Mar 2012 16:18:44 -0700 Subject: [PATCH 10/13] Changed parseCSS to public so users can initiate parsing manually rather than depending on the css method. --- phpQuery/phpQuery/phpQueryObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 2683f01..2b401bd 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1416,7 +1416,7 @@ public function css($property_name, $value = FALSE) { $this->bubbleCSS(phpQuery::pq($this->get(0), $this->getDocumentID())); } - protected function parseCSS() { + public function parseCSS() { if(!isset($this->cssString[$this->getDocumentID()])) { $this->cssString[$this->getDocumentID()] = file_get_contents(dirname(__FILE__) .'/default.css'); } From 1090fce09d1e7994b5479e0e84cad752706d8e01 Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Sat, 31 Mar 2012 16:23:50 -0700 Subject: [PATCH 11/13] Whoops, dangling removed variable. --- phpQuery/phpQuery/phpQueryObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index 2b401bd..acd180d 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1456,7 +1456,7 @@ public function parseCSS() { $style[] = $css_equivalent .':'. pq($el)->attr($map) .';'; } } - if(count($style) || count($attribute_style)) { + if(count($style)) { $CssParser = new CSSParser('#ruleset {'. implode(';', $style) .'}'); $CssDocument = $CssParser->parse(); $ruleset = $CssDocument->getAllRulesets(); From 5db48a207a51d254226307ea87ce44d256272f9e Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Tue, 3 Apr 2012 15:57:16 -0700 Subject: [PATCH 12/13] Fixed specificity problem when later rules with the same specificity should override.o --- phpQuery/phpQuery/phpQueryObject.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index acd180d..f1ec62b 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -1433,7 +1433,7 @@ public function parseCSS() { $existing = pq($el)->data('phpquery_css'); $ruleset->expandShorthands(); foreach($ruleset->getRules() as $rule => $value) { - if(!isset($existing[$rule]) || $existing[$rule]['specificity'] < $specificity) { + if(!isset($existing[$rule]) || $existing[$rule]['specificity'] <= $specificity) { $value = $value->getValue(); $value = (is_object($value)) ? $value->__toString() @@ -1453,7 +1453,7 @@ public function parseCSS() { $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) .';'; + $style[] = $css_equivalent .':'. pq($el)->attr($map); } } if(count($style)) { @@ -1463,7 +1463,7 @@ public function parseCSS() { $ruleset = reset($ruleset); $ruleset->expandShorthands(); foreach($ruleset->getRules() as $rule => $value) { - if(!isset($existing[$rule]) || 1000 > $existing[$rule]['specificity']) { + if(!isset($existing[$rule]) || 1000 >= $existing[$rule]['specificity']) { $value = $value->getValue(); $value = (is_object($value)) ? $value->__toString() From a15b3943605c544462a6df6517c36a19f66dd51a Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Sat, 7 Apr 2012 14:16:08 -0700 Subject: [PATCH 13/13] Added width and height attribute support. --- phpQuery/phpQuery/phpQueryObject.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpQuery/phpQuery/phpQueryObject.php b/phpQuery/phpQuery/phpQueryObject.php index f1ec62b..570ecab 100644 --- a/phpQuery/phpQuery/phpQueryObject.php +++ b/phpQuery/phpQuery/phpQueryObject.php @@ -95,6 +95,8 @@ class phpQueryObject protected $attribute_css_mapping = array( 'bgcolor' => 'background-color', 'text' => 'color', + 'width' => 'width', + 'height' => 'height' ); public function __construct($documentID) {