1<?php 2 3class CSSRuleset { 4 var $rules; 5 var $tag_filtered; 6 var $_lastId; 7 8 function CSSRuleset() { 9 $this->rules = array(); 10 $this->tag_filtered = array(); 11 $this->_lastId = 0; 12 } 13 14 function parse_style_node($root, &$pipeline) { 15 // Check if this style node have 'media' attribute 16 // and if we're using this media; 17 // 18 // Note that, according to the HTML 4.01 p.14.2.3 19 // This attribute specifies the intended destination medium for style information. 20 // It may be a single media descriptor or a comma-separated list. 21 // The default value for this attribute is "screen". 22 // 23 $media_list = array("screen"); 24 if ($root->has_attribute("media")) { 25 // Note that there may be whitespace symbols around commas, so we should not just use 'explode' function 26 $media_list = preg_split("/\s*,\s*/",trim($root->get_attribute("media"))); 27 }; 28 29 if (!is_allowed_media($media_list)) { 30 if (defined('DEBUG_MODE')) { 31 error_log(sprintf('No allowed (%s) media types found in CSS stylesheet media types (%s). Stylesheet ignored.', 32 join(',', config_get_allowed_media()), 33 join(',', $media_list))); 34 }; 35 return; 36 }; 37 38 if (!isset($GLOBALS['g_stylesheet_title']) || 39 $GLOBALS['g_stylesheet_title'] === "") { 40 $GLOBALS['g_stylesheet_title'] = $root->get_attribute("title"); 41 }; 42 43 if (!$root->has_attribute("title") || $root->get_attribute("title") === $GLOBALS['g_stylesheet_title']) { 44 /** 45 * Check if current node is empty (then, we don't need to parse its contents) 46 */ 47 $content = trim($root->get_content()); 48 if ($content != "") { 49 $this->parse_css($content, $pipeline); 50 }; 51 }; 52 } 53 54 function scan_styles($root, &$pipeline) { 55 switch ($root->node_type()) { 56 case XML_ELEMENT_NODE: 57 $tagname = strtolower($root->tagname()); 58 59 if ($tagname === 'style') { 60 // Parse <style ...> ... </style> nodes 61 // 62 $this->parse_style_node($root, $pipeline); 63 64 } elseif ($tagname === 'link') { 65 // Parse <link rel="stylesheet" ...> nodes 66 // 67 $rel = strtolower($root->get_attribute("rel")); 68 69 $type = strtolower($root->get_attribute("type")); 70 if ($root->has_attribute("media")) { 71 $media = explode(",",$root->get_attribute("media")); 72 } else { 73 $media = array(); 74 }; 75 76 if ($rel == "stylesheet" && 77 ($type == "text/css" || $type == "") && 78 (count($media) == 0 || is_allowed_media($media))) { 79 // Attempt to escape URL automaticaly 80 $url_autofix = new AutofixUrl(); 81 $src = $url_autofix->apply(trim($root->get_attribute('href'))); 82 83 if ($src) { 84 $this->css_import($src, $pipeline); 85 }; 86 }; 87 }; 88 89 // Note that we continue processing here! 90 case XML_DOCUMENT_NODE: 91 92 // Scan all child nodes 93 $child = $root->first_child(); 94 while ($child) { 95 $this->scan_styles($child, $pipeline); 96 $child = $child->next_sibling(); 97 }; 98 break; 99 }; 100 } 101 102 function parse_css($css, &$pipeline, $baseindex = 0) { 103 $allowed_media = implode("|",config_get_allowed_media()); 104 105 // remove the UTF8 byte-order mark from the beginning of the file (several high-order symbols at the beginning) 106 $pos = 0; 107 $len = strlen($css); 108 while (ord($css{$pos}) > 127 && $pos < $len) { $pos ++; }; 109 $css = substr($css, $pos); 110 111 // Process @media rules; 112 // basic syntax is: 113 // @media <media>(,<media>)* { <rules> } 114 // 115 116 while (preg_match("/^(.*?)@media([^{]+){(.*)$/s",$css,$matches)) { 117 $head = $matches[1]; 118 $media = $matches[2]; 119 $rest = $matches[3]; 120 121 // Process CSS rules placed before the first @media declaration - they should be applied to 122 // all media types 123 // 124 $this->parse_css_media($head, $pipeline, $baseindex); 125 126 // Extract the media content 127 if (!preg_match("/^((?:[^{}]*{[^{}]*})*)[^{}]*\s*}(.*)$/s", $rest, $matches)) { 128 die("CSS media syntax error\n"); 129 } else { 130 $content = $matches[1]; 131 $tail = $matches[2]; 132 }; 133 134 // Check if this media is to be processed 135 if (preg_match("/".$allowed_media."/i", $media)) { 136 $this->parse_css_media($content, $pipeline, $baseindex); 137 }; 138 139 // Process the rest of CSS file 140 $css = $tail; 141 }; 142 143 // The rest of CSS file belogs to common media, process it too 144 $this->parse_css_media($css, $pipeline, $baseindex); 145 } 146 147 function css_import($src, &$pipeline) { 148 // Update the base url; 149 // all urls will be resolved relatively to the current stylesheet url 150 $url = $pipeline->guess_url($src); 151 $data = $pipeline->fetch($url); 152 153 /** 154 * If referred file could not be fetched return immediately 155 */ 156 if (is_null($data)) { return; }; 157 158 $css = $data->get_content(); 159 if (!empty($css)) { 160 /** 161 * Sometimes, external stylesheets contain <!-- and --> at the beginning and 162 * at the end; we should remove these characters, as they may break parsing of 163 * first and last rules 164 */ 165 $css = preg_replace('/^\s*<!--/', '', $css); 166 $css = preg_replace('/-->\s*$/', '', $css); 167 168 $this->parse_css($css, $pipeline); 169 }; 170 171 $pipeline->pop_base_url(); 172 } 173 174 function parse_css_import($import, &$pipeline) { 175 if (preg_match("/@import\s+[\"'](.*)[\"'];/",$import, $matches)) { 176 // @import "<url>" 177 $this->css_import(trim($matches[1]), $pipeline); 178 } elseif (preg_match("/@import\s+url\((.*)\);/",$import, $matches)) { 179 // @import url() 180 $this->css_import(trim(css_remove_value_quotes($matches[1])), $pipeline); 181 } elseif (preg_match("/@import\s+(.*);/",$import, $matches)) { 182 // @import <url> 183 $this->css_import(trim(css_remove_value_quotes($matches[1])), $pipeline); 184 }; 185 } 186 187 function parse_css_media($css, &$pipeline, $baseindex = 0) { 188 // Remove comments 189 $css = preg_replace("#/\*.*?\*/#is","",$css); 190 191 // Extract @page rules 192 $css = parse_css_atpage_rules($css, $pipeline); 193 194 // Extract @import rules 195 if ($num = preg_match_all("/@import[^;]+;/",$css, $matches, PREG_PATTERN_ORDER)) { 196 for ($i=0; $i<$num; $i++) { 197 $this->parse_css_import($matches[0][$i], $pipeline); 198 } 199 }; 200 201 // Remove @import rules so they will not break further processing 202 $css = preg_replace("/@import[^;]+;/","", $css); 203 204 while (preg_match("/([^{}]*){(.*?)}(.*)/is", $css, $matches)) { 205 // Drop extracted part 206 $css = $matches[3]; 207 208 // Save extracted part 209 $raw_selectors = $matches[1]; 210 $raw_properties = $matches[2]; 211 212 $selectors = parse_css_selectors($raw_selectors); 213 214 $properties = parse_css_properties($raw_properties, $pipeline); 215 216 foreach ($selectors as $selector) { 217 $this->_lastId ++; 218 $rule = array($selector, 219 $properties, 220 $pipeline->get_base_url(), 221 $this->_lastId + $baseindex); 222 $this->add_rule($rule, 223 $pipeline); 224 }; 225 }; 226 } 227 228 function add_rule(&$rule, &$pipeline) { 229 $rule_obj = new CSSRule($rule, $pipeline); 230 $this->rules[] = $rule_obj; 231 232 $tag = $this->detect_applicable_tag($rule_obj->get_selector()); 233 if (is_null($tag)) { 234 $tag = "*"; 235 } 236 $this->tag_filtered[$tag][] = $rule_obj; 237 } 238 239 function apply(&$root, &$state, &$pipeline) { 240 $local_css = array(); 241 242 if (isset($this->tag_filtered[strtolower($root->tagname())])) { 243 $local_css = $this->tag_filtered[strtolower($root->tagname())]; 244 }; 245 246 if (isset($this->tag_filtered["*"])) { 247 $local_css = array_merge($local_css, $this->tag_filtered["*"]); 248 }; 249 250 $applicable = array(); 251 252 foreach ($local_css as $rule) { 253 if ($rule->match($root)) { 254 $applicable[] = $rule; 255 }; 256 }; 257 258 usort($applicable, "cmp_rule_objs"); 259 260 foreach ($applicable as $rule) { 261 switch ($rule->get_pseudoelement()) { 262 case SELECTOR_PSEUDOELEMENT_BEFORE: 263 $handler =& CSS::get_handler(CSS_HTML2PS_PSEUDOELEMENTS); 264 $handler->replace($handler->get($state->getState()) | CSS_HTML2PS_PSEUDOELEMENTS_BEFORE, $state); 265 break; 266 case SELECTOR_PSEUDOELEMENT_AFTER: 267 $handler =& CSS::get_handler(CSS_HTML2PS_PSEUDOELEMENTS); 268 $handler->replace($handler->get($state->getState()) | CSS_HTML2PS_PSEUDOELEMENTS_AFTER, $state); 269 break; 270 default: 271 $rule->apply($root, $state, $pipeline); 272 break; 273 }; 274 }; 275 } 276 277 function apply_pseudoelement($element_type, &$root, &$state, &$pipeline) { 278 $local_css = array(); 279 280 if (isset($this->tag_filtered[strtolower($root->tagname())])) { 281 $local_css = $this->tag_filtered[strtolower($root->tagname())]; 282 }; 283 284 if (isset($this->tag_filtered["*"])) { 285 $local_css = array_merge($local_css, $this->tag_filtered["*"]); 286 }; 287 288 $applicable = array(); 289 290 for ($i=0; $i<count($local_css); $i++) { 291 $rule =& $local_css[$i]; 292 if ($rule->get_pseudoelement() == $element_type) { 293 if ($rule->match($root)) { 294 $applicable[] =& $rule; 295 }; 296 }; 297 }; 298 299 usort($applicable, "cmp_rule_objs"); 300 301 // Note that filtered rules already have pseudoelement mathing (see condition above) 302 303 foreach ($applicable as $rule) { 304 $rule->apply($root, $state, $pipeline); 305 }; 306 } 307 308 // Check if only tag with a specific name can match this selector 309 // 310 function detect_applicable_tag($selector) { 311 switch (selector_get_type($selector)) { 312 case SELECTOR_TAG: 313 return $selector[1]; 314 case SELECTOR_TAG_CLASS: 315 return $selector[1]; 316 case SELECTOR_SEQUENCE: 317 foreach ($selector[1] as $subselector) { 318 $tag = $this->detect_applicable_tag($subselector); 319 if ($tag) { return $tag; }; 320 }; 321 return null; 322 default: 323 return null; 324 } 325 } 326} 327 328?>