00001 <?php
00043 class quailCSS {
00044
00048 var $dom;
00049
00053 var $uri;
00054
00058 var $type;
00059
00063 var $css;
00064
00068 var $css_string;
00069
00073 var $cms_mode;
00074
00078 var $inheritance_strings = array('inherit', 'currentColor');
00079
00083 var $style_index = array();
00084
00088 var $next_index = 0;
00089
00093 var $deprecated_style_elements = array('body', 'table', 'tr', 'td', 'th');
00094
00103 function __construct(&$dom, $uri, $type, $path, $cms_mode = false, $css_files = array()) {
00104 $this->dom =& $dom;
00105 $this->type = $type;
00106 $this->uri = $uri;
00107 $this->path = $path;
00108 $this->cms_mode = $cms_mode;
00109 $this->css_files = $css_files;
00110 }
00111
00112
00119 private function setStyles() {
00120 if(!is_array($this->css)) {
00121 return null;
00122 }
00123 foreach($this->css as $selector => $style) {
00124 $xpath = new DOMXPath($this->dom);
00125 $entries = @$xpath->query($this->getXpath($selector));
00126 if($entries->length) {
00127 foreach($entries as $e) {
00128 if(!$e->hasAttribute('quail_style_index')) {
00129 $e->setAttribute('quail_style_index', $this->next_index);
00130 $this->next_index++;
00131 }
00132 $this->addCSSToElement($e, $style, $this->getSpecificity($selector));
00133 }
00134 }
00135 }
00136 foreach($this->style_index as $k => $style) {
00137 foreach($style as $i => $values) {
00138 $this->style_index[$k][$i] = trim(strtolower($values['value']));
00139 }
00140 }
00141 }
00142
00150 private function addCSSToElement($element, $style, $specificity) {
00151 $index_id = $element->getAttribute('quail_style_index');
00152 foreach($style as $name => $value) {
00153 if(!$this->style_index[$index_id][$name] ||
00154 $this->style_index[$index_id][$name]['specificity'] < $specificity
00155 || strpos($value, '!important') !== false)
00156 {
00157 $this->style_index[$index_id][$name] = array(
00158 'value' => str_replace('!important', '', trim(strtolower($value))),
00159 'specificity' => $specificity,
00160 );
00161 }
00162 }
00163
00164 }
00165
00169 private function loadCSS() {
00170 if(count($this->css_files) > 0) {
00171 $css = $this->css_files;
00172 }
00173 else {
00174 $css = array();
00175 $header_styles = $this->dom->getElementsByTagName('style');
00176 foreach($header_styles as $header_style) {
00177 if($header_style->nodeValue) {
00178 $this->css_string .= $header_style->nodeValue;
00179 }
00180 }
00181 $style_sheets = $this->dom->getElementsByTagName('link');
00182
00183 foreach($style_sheets as $style) {
00184 if($style->hasAttribute('rel') &&
00185 strtolower($style->getAttribute('rel')) == 'stylesheet' &&
00186 $style->getAttribute('media') != 'print') {
00187 $css[] = $style->getAttribute('href');
00188 }
00189 }
00190 }
00191 foreach($css as $sheet) {
00192 $this->loadUri($sheet);
00193 }
00194 $this->loadImportedFiles();
00195 $this->css_string = str_replace(':link', '', $this->css_string);
00196 $this->formatCSS();
00197 }
00198
00202 private function loadImportedFiles() {
00203 $matches = array();
00204 preg_match_all('/@import (.*?);/i', $this->css_string, $matches);
00205 if(count($matches[1]) == 0) {
00206 return null;
00207 }
00208 foreach($matches[1] as $match) {
00209 $this->loadUri(trim(str_replace('url', '', $match), '"\')('));
00210 }
00211 preg_replace('/@import (.*?);/i', '', $this->css_string);
00212 }
00213
00219 public function getSpecificity($selector) {
00220 $selector = $this->parseSelector($selector);
00221 if($selector[0][0] == ' ') {
00222 unset($selector[0][0]);
00223 }
00224 $selector = $selector[0];
00225 $specificity = 0;
00226 foreach($selector as $part) {
00227 switch(substr(str_replace('*', '', $part), 0, 1)) {
00228 case '.':
00229 $specificity += 10;
00230 case '#':
00231 $specificity += 100;
00232 case ':':
00233 $specificity++;
00234 default:
00235 $specificity++;
00236 }
00237 if(strpos($part, '[id=') != false) {
00238 $specificity += 100;
00239 }
00240 }
00241 return $specificity;
00242 }
00243
00249 public function getStyle($element) {
00250
00251
00252
00253 if(!$this->css) {
00254 $this->loadCSS();
00255 $this->setStyles();
00256 }
00257 if(!is_a($element, 'DOMElement')) {
00258 return array();
00259 }
00260 $style = $this->getNodeStyle($element);
00261 $style = $this->walkUpTreeForInheritance($element, $style);
00262
00263 if($element->hasAttribute('style')) {
00264 $inline_styles = explode(';', $element->getAttribute('style'));
00265 foreach($inline_styles as $inline_style) {
00266 $s = explode(':', $inline_style);
00267 $style[$s[0]] = trim(strtolower($s[1]));
00268 }
00269 }
00270 if(!is_array($style)) {
00271 return array();
00272 }
00273 return $style;
00274 }
00275
00282 private function addSelector($key, $codestr) {
00283 if(strpos($key, '@import') !== false) {
00284 return null;
00285 }
00286 $key = strtolower($key);
00287 $codestr = strtolower($codestr);
00288 if(!isset($this->css[$key])) {
00289 $this->css[$key] = array();
00290 }
00291 $codes = explode(";",$codestr);
00292 if(count($codes) > 0) {
00293 foreach($codes as $code) {
00294 $code = trim($code);
00295 $explode = explode(":",$code,2);
00296 if(count($explode) > 1) {
00297 list($codekey, $codevalue) = $explode;
00298 if(strlen($codekey) > 0) {
00299 $this->css[$key][trim($codekey)] = trim($codevalue);
00300 }
00301 }
00302 }
00303 }
00304 }
00305
00315 private function getNodeStyle($element) {
00316 $style = array();
00317 if($element->hasAttribute('quail_style_index')) {
00318 $style = $this->style_index[$element->getAttribute('quail_style_index')];
00319 }
00320
00321 if($element->hasAttribute('bgcolor') && in_array($element->tagName, $this->deprecated_style_elements)) {
00322 $style['background-color'] = $element->getAttribute('bgcolor');
00323 }
00324 return $style;
00325 }
00326
00334 private function walkUpTreeForInheritance($element, $style) {
00335 while(property_exists($element->parentNode, 'tagName')) {
00336 $parent_style = $this->getNodeStyle($element->parentNode);
00337
00338 if(is_array($parent_style)) {
00339 foreach($parent_style as $k => $v) {
00340 if(!isset($style[$k]) || in_array($style[$k]['value'], $this->inheritance_strings)) {
00341 $style[$k] = $v;
00342 }
00343 }
00344 }
00345 $element = $element->parentNode;
00346 }
00347 return $style;
00348 }
00349
00354 private function loadUri($rel) {
00355 if($this->type == 'file') {
00356 $uri = substr($this->uri, 0, strrpos($this->uri, '/')) .'/'.$rel;
00357 }
00358 else {
00359 $uri = quail::getAbsolutePath($this->uri, $rel);
00360 }
00361 $this->css_string .= @file_get_contents($uri);
00362
00363 }
00364
00369 private function formatCSS() {
00370
00371 $str = preg_replace("/\/\*(.*)?\*\//Usi", "", $this->css_string);
00372
00373 $parts = explode("}",$str);
00374 if(count($parts) > 0) {
00375 foreach($parts as $part) {
00376 if(strpos($part, '{') !== false) {
00377 list($keystr,$codestr) = explode("{", $part);
00378 $keys = explode(",",trim($keystr));
00379 if(count($keys) > 0) {
00380 foreach($keys as $key) {
00381 if(strlen($key) > 0) {
00382 $key = str_replace("\n", "", $key);
00383 $key = str_replace("\\", "", $key);
00384 $this->addSelector($key, trim($codestr));
00385 }
00386 }
00387 }
00388 }
00389 }
00390 }
00391 return (count($this->css) > 0);
00392 }
00393
00399 private function getXpath($selector) {
00400 $query = $this->parseSelector($selector);
00401
00402 $xpath = '//';
00403 foreach($query[0] as $k => $q) {
00404 if($q == ' ' && $k)
00405 $xpath .= '//';
00406 elseif($q == '>' && $k)
00407 $xpath .= '/';
00408 elseif(substr($q, 0, 1) == '#')
00409 $xpath .= '[ @id = "'. str_replace('#', '', $q) .'" ]';
00410
00411 elseif(substr($q, 0, 1) == '.')
00412 $xpath .= '[ @class = "'. str_replace('.', '', $q) .'" ]';
00413 elseif(substr($q, 0, 1) == '[')
00414 $xpath .= str_replace('[id', '[ @ id', $q);
00415 else
00416 $xpath .= trim($q);
00417 }
00418 return str_replace('
00419 }
00420
00426 private function isChar($char) {
00427 return extension_loaded('mbstring')
00428 ? mb_eregi('\w', $char)
00429 : preg_match('@\w@', $char);
00430 }
00431
00437 private function parseSelector($query) {
00438
00439
00440 $query = trim(
00441 preg_replace('@\s+@', ' ',
00442 preg_replace('@\s*(>|\\+|~)\s*@', '\\1', $query)
00443 )
00444 );
00445 $queries = array(array());
00446 if (! $query)
00447 return $queries;
00448 $return =& $queries[0];
00449 $specialChars = array('>',' ');
00450
00451 $specialCharsMapping = array();
00452 $strlen = mb_strlen($query);
00453 $classChars = array('.', '-');
00454 $pseudoChars = array('-');
00455 $tagChars = array('*', '|', '-');
00456
00457
00458 $_query = array();
00459 for ($i=0; $i<$strlen; $i++)
00460 $_query[] = mb_substr($query, $i, 1);
00461 $query = $_query;
00462
00463 $i = 0;
00464 while( $i < $strlen) {
00465 $c = $query[$i];
00466 $tmp = '';
00467
00468 if ($this->isChar($c) || in_array($c, $tagChars)) {
00469 while(isset($query[$i])
00470 && ($this->isChar($query[$i]) || in_array($query[$i], $tagChars))) {
00471 $tmp .= $query[$i];
00472 $i++;
00473 }
00474 $return[] = $tmp;
00475
00476 } else if ( $c == '#') {
00477 $i++;
00478 while( isset($query[$i]) && ($this->isChar($query[$i]) || $query[$i] == '-')) {
00479 $tmp .= $query[$i];
00480 $i++;
00481 }
00482 $return[] = '#'.$tmp;
00483
00484 } else if (in_array($c, $specialChars)) {
00485 $return[] = $c;
00486 $i++;
00487
00488
00489
00490
00491
00492 } else if ( isset($specialCharsMapping[$c])) {
00493 $return[] = $specialCharsMapping[$c];
00494 $i++;
00495
00496 } else if ( $c == ',') {
00497 $queries[] = array();
00498 $return =& $queries[ count($queries)-1 ];
00499 $i++;
00500 while( isset($query[$i]) && $query[$i] == ' ')
00501 $i++;
00502
00503 } else if ($c == '.') {
00504 while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $classChars))) {
00505 $tmp .= $query[$i];
00506 $i++;
00507 }
00508 $return[] = $tmp;
00509
00510 } else if ($c == '~') {
00511 $spaceAllowed = true;
00512 $tmp .= $query[$i++];
00513 while( isset($query[$i])
00514 && ($this->isChar($query[$i])
00515 || in_array($query[$i], $classChars)
00516 || $query[$i] == '*'
00517 || ($query[$i] == ' ' && $spaceAllowed)
00518 )) {
00519 if ($query[$i] != ' ')
00520 $spaceAllowed = false;
00521 $tmp .= $query[$i];
00522 $i++;
00523 }
00524 $return[] = $tmp;
00525
00526 } else if ($c == '+') {
00527 $spaceAllowed = true;
00528 $tmp .= $query[$i++];
00529 while( isset($query[$i])
00530 && ($this->isChar($query[$i])
00531 || in_array($query[$i], $classChars)
00532 || $query[$i] == '*'
00533 || ($spaceAllowed && $query[$i] == ' ')
00534 )) {
00535 if ($query[$i] != ' ')
00536 $spaceAllowed = false;
00537 $tmp .= $query[$i];
00538 $i++;
00539 }
00540 $return[] = $tmp;
00541
00542 } else if ($c == '[') {
00543 $stack = 1;
00544 $tmp .= $c;
00545 while( isset($query[++$i])) {
00546 $tmp .= $query[$i];
00547 if ( $query[$i] == '[') {
00548 $stack++;
00549 } else if ( $query[$i] == ']') {
00550 $stack--;
00551 if (! $stack )
00552 break;
00553 }
00554 }
00555 $return[] = $tmp;
00556 $i++;
00557
00558 } else if ($c == ':') {
00559 $stack = 1;
00560 $tmp .= $query[$i++];
00561 while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $pseudoChars))) {
00562 $tmp .= $query[$i];
00563 $i++;
00564 }
00565
00566 if ( isset($query[$i]) && $query[$i] == '(') {
00567 $tmp .= $query[$i];
00568 $stack = 1;
00569 while( isset($query[++$i])) {
00570 $tmp .= $query[$i];
00571 if ( $query[$i] == '(') {
00572 $stack++;
00573 } else if ( $query[$i] == ')') {
00574 $stack--;
00575 if (! $stack )
00576 break;
00577 }
00578 }
00579 $return[] = $tmp;
00580 $i++;
00581 } else {
00582 $return[] = $tmp;
00583 }
00584 } else {
00585 $i++;
00586 }
00587 }
00588 foreach($queries as $k => $q) {
00589 if (isset($q[0])) {
00590 if (isset($q[0][0]) && $q[0][0] == ':')
00591 array_unshift($queries[$k], '*');
00592 if ($q[0] != '>')
00593 array_unshift($queries[$k], ' ');
00594 }
00595 }
00596 return $queries;
00597 }
00598
00599 }