csstidy

CSS Parser class.

Defined (1)

The class is defined in the following location(s).

/lib/CSSTidy/class.csstidy.php  
  1. class csstidy { 
  2.  
  3. /** 
  4. * Saves the parsed CSS. This array is empty if preserve_css is on. 
  5. * @var array 
  6. * @access public 
  7. */ 
  8. public $css = array(); 
  9. /** 
  10. * Saves the parsed CSS (raw) 
  11. * @var array 
  12. * @access private 
  13. */ 
  14. public $tokens = array(); 
  15. /** 
  16. * Printer class 
  17. * @see csstidy_print 
  18. * @var object 
  19. * @access public 
  20. */ 
  21. public $print; 
  22. /** 
  23. * Optimiser class 
  24. * @see csstidy_optimise 
  25. * @var object 
  26. * @access private 
  27. */ 
  28. public $optimise; 
  29. /** 
  30. * Saves the CSS charset (@charset) 
  31. * @var string 
  32. * @access private 
  33. */ 
  34. public $charset = ''; 
  35. /** 
  36. * Saves all @import URLs 
  37. * @var array 
  38. * @access private 
  39. */ 
  40. public $import = array(); 
  41. /** 
  42. * Saves the namespace 
  43. * @var string 
  44. * @access private 
  45. */ 
  46. public $namespace = ''; 
  47. /** 
  48. * Contains the version of csstidy 
  49. * @var string 
  50. * @access private 
  51. */ 
  52. public $version = '1.5.5'; 
  53. /** 
  54. * Stores the settings 
  55. * @var array 
  56. * @access private 
  57. */ 
  58. public $settings = array(); 
  59. /** 
  60. * Saves the parser-status. 
  61. * Possible values: 
  62. * - is = in selector 
  63. * - ip = in property 
  64. * - iv = in value 
  65. * - instr = in string (started at " or ' or ( ) 
  66. * - ic = in comment (ignore everything) 
  67. * - at = in @-block 
  68. * @var string 
  69. * @access private 
  70. */ 
  71. public $status = 'is'; 
  72. /** 
  73. * Saves the current at rule (@media) 
  74. * @var string 
  75. * @access private 
  76. */ 
  77. public $at = ''; 
  78. /** 
  79. * Saves the at rule for next selector (during @font-face or other @) 
  80. * @var string 
  81. * @access private 
  82. */ 
  83. public $next_selector_at = ''; 
  84.  
  85. /** 
  86. * Saves the current selector 
  87. * @var string 
  88. * @access private 
  89. */ 
  90. public $selector = ''; 
  91. /** 
  92. * Saves the current property 
  93. * @var string 
  94. * @access private 
  95. */ 
  96. public $property = ''; 
  97. /** 
  98. * Saves the position of , in selectors 
  99. * @var array 
  100. * @access private 
  101. */ 
  102. public $sel_separate = array(); 
  103. /** 
  104. * Saves the current value 
  105. * @var string 
  106. * @access private 
  107. */ 
  108. public $value = ''; 
  109. /** 
  110. * Saves the current sub-value 
  111. * Example for a subvalue: 
  112. * background:url(foo.png) red no-repeat; 
  113. * "url(foo.png)", "red", and "no-repeat" are subvalues,  
  114. * seperated by whitespace 
  115. * @var string 
  116. * @access private 
  117. */ 
  118. public $sub_value = ''; 
  119. /** 
  120. * Array which saves all subvalues for a property. 
  121. * @var array 
  122. * @see sub_value 
  123. * @access private 
  124. */ 
  125. public $sub_value_arr = array(); 
  126. /** 
  127. * Saves the stack of characters that opened the current strings 
  128. * @var array 
  129. * @access private 
  130. */ 
  131. public $str_char = array(); 
  132. public $cur_string = array(); 
  133. /** 
  134. * Status from which the parser switched to ic or instr 
  135. * @var array 
  136. * @access private 
  137. */ 
  138. public $from = array(); 
  139. /** 
  140. /** 
  141. * =true if in invalid at-rule 
  142. * @var bool 
  143. * @access private 
  144. */ 
  145. public $invalid_at = false; 
  146. /** 
  147. * =true if something has been added to the current selector 
  148. * @var bool 
  149. * @access private 
  150. */ 
  151. public $added = false; 
  152. /** 
  153. * Array which saves the message log 
  154. * @var array 
  155. * @access private 
  156. */ 
  157. public $log = array(); 
  158. /** 
  159. * Saves the line number 
  160. * @var integer 
  161. * @access private 
  162. */ 
  163. public $line = 1; 
  164. /** 
  165. * Marks if we need to leave quotes for a string 
  166. * @var array 
  167. * @access private 
  168. */ 
  169. public $quoted_string = array(); 
  170.  
  171. /** 
  172. * List of tokens 
  173. * @var string 
  174. */ 
  175. public $tokens_list = ""; 
  176.  
  177. /** 
  178. * Various CSS Data for CSSTidy 
  179. * @var array 
  180. */ 
  181. public $data = array(); 
  182.  
  183. /** 
  184. * Loads standard template and sets default settings 
  185. * @access private 
  186. * @version 1.3 
  187. */ 
  188. public function __construct() { 
  189. $data = array(); 
  190. include('data.inc.php'); 
  191. $this->data = $data; 
  192.  
  193. $this->settings['remove_bslash'] = true; 
  194. $this->settings['compress_colors'] = true; 
  195. $this->settings['compress_font-weight'] = true; 
  196. $this->settings['lowercase_s'] = false; 
  197. /** 
  198. 1 common shorthands optimization 
  199. 2 + font property optimization 
  200. 3 + background property optimization 
  201. */ 
  202. $this->settings['optimise_shorthands'] = 1; 
  203. $this->settings['remove_last_;'] = true; 
  204. $this->settings['space_before_important'] = false; 
  205. /** rewrite all properties with low case, better for later gzip OK, safe*/ 
  206. $this->settings['case_properties'] = 1; 
  207. /** sort properties in alpabetic order, better for later gzip 
  208. * but can cause trouble in case of overiding same propertie or using hack 
  209. */ 
  210. $this->settings['sort_properties'] = false; 
  211. /** 
  212. 1, 3, 5, etc -- enable sorting selectors inside @media: a{}b{}c{} 
  213. 2, 5, 8, etc -- enable sorting selectors inside one CSS declaration: a, b, c{} 
  214. preserve order by default cause it can break functionnality 
  215. */ 
  216. $this->settings['sort_selectors'] = false; 
  217. /** is dangeroues to be used: CSS is broken sometimes */ 
  218. $this->settings['merge_selectors'] = 0; 
  219. /** preserve or not browser hacks */ 
  220. $this->settings['discard_invalid_selectors'] = false; 
  221. $this->settings['discard_invalid_properties'] = false; 
  222. $this->settings['css_level'] = 'CSS3.0'; 
  223. $this->settings['preserve_css'] = false; 
  224. $this->settings['timestamp'] = false; 
  225. $this->settings['template'] = ''; // say that propertie exist 
  226. $this->set_cfg('template', 'default'); // call load_template 
  227. $this->optimise = new csstidy_optimise($this); 
  228.  
  229. $this->tokens_list = & $this->data['csstidy']['tokens']; 
  230.  
  231. /** 
  232. * Get the value of a setting. 
  233. * @param string $setting 
  234. * @access public 
  235. * @return mixed 
  236. * @version 1.0 
  237. */ 
  238. public function get_cfg($setting) { 
  239. if (isset($this->settings[$setting])) { 
  240. return $this->settings[$setting]; 
  241. return false; 
  242.  
  243. /** 
  244. * Load a template 
  245. * @param string $template used by set_cfg to load a template via a configuration setting 
  246. * @access private 
  247. * @version 1.4 
  248. */ 
  249. public function _load_template($template) { 
  250. switch ($template) { 
  251. case 'default': 
  252. $this->load_template('default'); 
  253. break; 
  254.  
  255. case 'highest': 
  256. $this->load_template('highest_compression'); 
  257. break; 
  258.  
  259. case 'high': 
  260. $this->load_template('high_compression'); 
  261. break; 
  262.  
  263. case 'low': 
  264. $this->load_template('low_compression'); 
  265. break; 
  266.  
  267. default: 
  268. $this->load_template($template); 
  269. break; 
  270.  
  271. /** 
  272. * Set the value of a setting. 
  273. * @param string $setting 
  274. * @param mixed $value 
  275. * @access public 
  276. * @return bool 
  277. * @version 1.0 
  278. */ 
  279. public function set_cfg($setting, $value=null) { 
  280. if (is_array($setting) && $value === null) { 
  281. foreach ($setting as $setprop => $setval) { 
  282. $this->settings[$setprop] = $setval; 
  283. if (array_key_exists('template', $setting)) { 
  284. $this->_load_template($this->settings['template']); 
  285. return true; 
  286. } elseif (isset($this->settings[$setting]) && $value !== '') { 
  287. $this->settings[$setting] = $value; 
  288. if ($setting === 'template') { 
  289. $this->_load_template($this->settings['template']); 
  290. return true; 
  291. return false; 
  292.  
  293. /** 
  294. * Adds a token to $this->tokens 
  295. * @param mixed $type 
  296. * @param string $data 
  297. * @param bool $do add a token even if preserve_css is off 
  298. * @access private 
  299. * @version 1.0 
  300. */ 
  301. public function _add_token($type, $data, $do = false) { 
  302. if ($this->get_cfg('preserve_css') || $do) { 
  303. $this->tokens[] = array($type, ($type == COMMENT) ? $data : trim($data)); 
  304.  
  305. /** 
  306. * Add a message to the message log 
  307. * @param string $message 
  308. * @param string $type 
  309. * @param integer $line 
  310. * @access private 
  311. * @version 1.0 
  312. */ 
  313. public function log($message, $type, $line = -1) { 
  314. if ($line === -1) { 
  315. $line = $this->line; 
  316. $line = intval($line); 
  317. $add = array('m' => $message, 't' => $type); 
  318. if (!isset($this->log[$line]) || !in_array($add, $this->log[$line])) { 
  319. $this->log[$line][] = $add; 
  320.  
  321. /** 
  322. * Parse unicode notations and find a replacement character 
  323. * @param string $string 
  324. * @param integer $i 
  325. * @access private 
  326. * @return string 
  327. * @version 1.2 
  328. */ 
  329. public function _unicode(&$string, &$i) { 
  330. ++$i; 
  331. $add = ''; 
  332. $replaced = false; 
  333.  
  334. while ($i < strlen($string) && (ctype_xdigit($string{$i}) || ctype_space($string{$i})) && strlen($add) < 6) { 
  335. $add .= $string{$i}; 
  336.  
  337. if (ctype_space($string{$i})) { 
  338. break; 
  339. $i++; 
  340.  
  341. if (hexdec($add) > 47 && hexdec($add) < 58 || hexdec($add) > 64 && hexdec($add) < 91 || hexdec($add) > 96 && hexdec($add) < 123) { 
  342. $this->log('Replaced unicode notation: Changed \\' . $add . ' to ' . chr(hexdec($add)), 'Information'); 
  343. $add = chr(hexdec($add)); 
  344. $replaced = true; 
  345. } else { 
  346. $add = trim('\\' . $add); 
  347.  
  348. if (@ctype_xdigit($string{$i + 1}) && ctype_space($string{$i}) 
  349. && !$replaced || !ctype_space($string{$i})) { 
  350. $i--; 
  351.  
  352. if ($add !== '\\' || !$this->get_cfg('remove_bslash') || strpos($this->tokens_list, $string{$i + 1}) !== false) { 
  353. return $add; 
  354.  
  355. if ($add === '\\') { 
  356. $this->log('Removed unnecessary backslash', 'Information'); 
  357. return ''; 
  358.  
  359. /** 
  360. * Write formatted output to a file 
  361. * @param string $filename 
  362. * @param string $doctype when printing formatted, is a shorthand for the document type 
  363. * @param bool $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet 
  364. * @param string $title when printing formatted, is the title to be added in the head of the document 
  365. * @param string $lang when printing formatted, gives a two-letter language code to be added to the output 
  366. * @access public 
  367. * @version 1.4 
  368. */ 
  369. public function write_page($filename, $doctype='xhtml1.1', $externalcss=true, $title='', $lang='en') { 
  370. $this->write($filename, true); 
  371.  
  372. /** 
  373. * Write plain output to a file 
  374. * @param string $filename 
  375. * @param bool $formatted whether to print formatted or not 
  376. * @param string $doctype when printing formatted, is a shorthand for the document type 
  377. * @param bool $externalcss when printing formatted, indicates whether styles to be attached internally or as an external stylesheet 
  378. * @param string $title when printing formatted, is the title to be added in the head of the document 
  379. * @param string $lang when printing formatted, gives a two-letter language code to be added to the output 
  380. * @param bool $pre_code whether to add pre and code tags around the code (for light HTML formatted templates) 
  381. * @access public 
  382. * @version 1.4 
  383. */ 
  384. public function write($filename, $formatted=false, $doctype='xhtml1.1', $externalcss=true, $title='', $lang='en', $pre_code=true) { 
  385. $filename .= ( $formatted) ? '.xhtml' : '.css'; 
  386.  
  387. if (!is_dir('temp')) { 
  388. $madedir = mkdir('temp'); 
  389. if (!$madedir) { 
  390. print 'Could not make directory "temp" in ' . dirname(__FILE__); 
  391. exit; 
  392. $handle = fopen('temp/' . $filename, 'w'); 
  393. if ($handle) { 
  394. if (!$formatted) { 
  395. fwrite($handle, $this->print->plain()); 
  396. } else { 
  397. fwrite($handle, $this->print->formatted_page($doctype, $externalcss, $title, $lang, $pre_code)); 
  398. fclose($handle); 
  399.  
  400. /** 
  401. * Loads a new template 
  402. * @param string $content either filename (if $from_file == true), content of a template file, "high_compression", "highest_compression", "low_compression", or "default" 
  403. * @param bool $from_file uses $content as filename if true 
  404. * @access public 
  405. * @version 1.1 
  406. * @see http://csstidy.sourceforge.net/templates.php 
  407. */ 
  408. public function load_template($content, $from_file=true) { 
  409. $predefined_templates = & $this->data['csstidy']['predefined_templates']; 
  410. if ($content === 'high_compression' || $content === 'default' || $content === 'highest_compression' || $content === 'low_compression') { 
  411. $this->template = $predefined_templates[$content]; 
  412. return; 
  413.  
  414.  
  415. if ($from_file) { 
  416. $content = strip_tags(file_get_contents($content), '<span>'); 
  417. $content = str_replace("\r\n", "\n", $content); // Unify newlines (because the output also only uses \n) 
  418. $template = explode('|', $content); 
  419.  
  420. for ($i = 0; $i < count($template); $i++) { 
  421. $this->template[$i] = $template[$i]; 
  422.  
  423. /** 
  424. * Starts parsing from URL 
  425. * @param string $url 
  426. * @access public 
  427. * @version 1.0 
  428. */ 
  429. public function parse_from_url($url) { 
  430. return $this->parse(@file_get_contents($url)); 
  431.  
  432. /** 
  433. * Checks if there is a token at the current position 
  434. * @param string $string 
  435. * @param integer $i 
  436. * @access public 
  437. * @version 1.11 
  438. */ 
  439. public function is_token(&$string, $i) { 
  440. return (strpos($this->tokens_list, $string{$i}) !== false && !$this->escaped($string, $i)); 
  441.  
  442. /** 
  443. * Parses CSS in $string. The code is saved as array in $this->css 
  444. * @param string $string the CSS code 
  445. * @access public 
  446. * @return bool 
  447. * @version 1.1 
  448. */ 
  449. public function parse($string) { 
  450. // Temporarily set locale to en_US in order to handle floats properly 
  451. $old = @setlocale(LC_ALL, 0); 
  452. @setlocale(LC_ALL, 'C'); 
  453.  
  454. // PHP bug? Settings need to be refreshed in PHP4 
  455. $this->print = new csstidy_print($this); 
  456. $this->optimise = new csstidy_optimise($this); 
  457.  
  458. $all_properties = & $this->data['csstidy']['all_properties']; 
  459. $at_rules = & $this->data['csstidy']['at_rules']; 
  460. $quoted_string_properties = & $this->data['csstidy']['quoted_string_properties']; 
  461.  
  462. $this->css = array(); 
  463. $this->print->input_css = $string; 
  464. $string = str_replace("\r\n", "\n", $string) . ' '; 
  465. $cur_comment = ''; 
  466.  
  467. for ($i = 0, $size = strlen($string); $i < $size; $i++) { 
  468. if ($string{$i} === "\n" || $string{$i} === "\r") { 
  469. ++$this->line; 
  470.  
  471. switch ($this->status) { 
  472. /** Case in at-block */ 
  473. case 'at': 
  474. if ($this->is_token($string, $i)) { 
  475. if ($string{$i} === '/' && @$string{$i + 1} === '*') { 
  476. $this->status = 'ic'; 
  477. ++$i; 
  478. $this->from[] = 'at'; 
  479. } elseif ($string{$i} === '{') { 
  480. $this->status = 'is'; 
  481. $this->at = $this->css_new_media_section($this->at); 
  482. $this->_add_token(AT_START, $this->at); 
  483. } elseif ($string{$i} === ', ') { 
  484. $this->at = trim($this->at) . ', '; 
  485. } elseif ($string{$i} === '\\') { 
  486. $this->at .= $this->_unicode($string, $i); 
  487. // fix for complicated media, i.e @media screen and (-webkit-min-device-pixel-ratio:1.5) 
  488. elseif (in_array($string{$i}, array('(', ')', ':', '.', '/'))) { 
  489. $this->at .= $string{$i}; 
  490. } else { 
  491. $lastpos = strlen($this->at) - 1; 
  492. if (!( (ctype_space($this->at{$lastpos}) || $this->is_token($this->at, $lastpos) && $this->at{$lastpos} === ', ') && ctype_space($string{$i}))) { 
  493. $this->at .= $string{$i}; 
  494. break; 
  495.  
  496. /** Case in-selector */ 
  497. case 'is': 
  498. if ($this->is_token($string, $i)) { 
  499. if ($string{$i} === '/' && @$string{$i + 1} === '*' && trim($this->selector) == '') { 
  500. $this->status = 'ic'; 
  501. ++$i; 
  502. $this->from[] = 'is'; 
  503. } elseif ($string{$i} === '@' && trim($this->selector) == '') { 
  504. // Check for at-rule 
  505. $this->invalid_at = true; 
  506. foreach ($at_rules as $name => $type) { 
  507. if (!strcasecmp(substr($string, $i + 1, strlen($name)), $name)) { 
  508. ($type === 'at') ? $this->at = '@' . $name : $this->selector = '@' . $name; 
  509. if ($type === 'atis') { 
  510. $this->next_selector_at = ($this->next_selector_at?$this->next_selector_at:($this->at?$this->at:DEFAULT_AT)); 
  511. $this->at = $this->css_new_media_section(' '); 
  512. $type = 'is'; 
  513. $this->status = $type; 
  514. $i += strlen($name); 
  515. $this->invalid_at = false; 
  516.  
  517. if ($this->invalid_at) { 
  518. $this->selector = '@'; 
  519. $invalid_at_name = ''; 
  520. for ($j = $i + 1; $j < $size; ++$j) { 
  521. if (!ctype_alpha($string{$j})) { 
  522. break; 
  523. $invalid_at_name .= $string{$j}; 
  524. $this->log('Invalid @-rule: ' . $invalid_at_name . ' (removed)', 'Warning'); 
  525. } elseif (($string{$i} === '"' || $string{$i} === "'")) { 
  526. $this->cur_string[] = $string{$i}; 
  527. $this->status = 'instr'; 
  528. $this->str_char[] = $string{$i}; 
  529. $this->from[] = 'is'; 
  530. /** fixing CSS3 attribute selectors, i.e. a[href$=".mp3" */ 
  531. $this->quoted_string[] = ($string{$i - 1} === '=' ); 
  532. } elseif ($this->invalid_at && $string{$i} === ';') { 
  533. $this->invalid_at = false; 
  534. $this->status = 'is'; 
  535. if ($this->next_selector_at) { 
  536. $this->at = $this->css_new_media_section($this->next_selector_at); 
  537. $this->next_selector_at = ''; 
  538. } elseif ($string{$i} === '{') { 
  539. $this->status = 'ip'; 
  540. if ($this->at == '') { 
  541. $this->at = $this->css_new_media_section(DEFAULT_AT); 
  542. $this->selector = $this->css_new_selector($this->at, $this->selector); 
  543. $this->_add_token(SEL_START, $this->selector); 
  544. $this->added = false; 
  545. } elseif ($string{$i} === '}') { 
  546. $this->_add_token(AT_END, $this->at); 
  547. $this->at = ''; 
  548. $this->selector = ''; 
  549. $this->sel_separate = array(); 
  550. } elseif ($string{$i} === ', ') { 
  551. $this->selector = trim($this->selector) . ', '; 
  552. $this->sel_separate[] = strlen($this->selector); 
  553. } elseif ($string{$i} === '\\') { 
  554. $this->selector .= $this->_unicode($string, $i); 
  555. } elseif ($string{$i} === '*' && @in_array($string{$i + 1}, array('.', '#', '[', ':')) && ($i==0 OR $string{$i - 1}!=='/')) { 
  556. // remove unnecessary universal selector, FS#147, but not comment in selector 
  557. } else { 
  558. $this->selector .= $string{$i}; 
  559. } else { 
  560. $lastpos = strlen($this->selector) - 1; 
  561. if ($lastpos == -1 || !( (ctype_space($this->selector{$lastpos}) || $this->is_token($this->selector, $lastpos) && $this->selector{$lastpos} === ', ') && ctype_space($string{$i}))) { 
  562. $this->selector .= $string{$i}; 
  563. break; 
  564.  
  565. /** Case in-property */ 
  566. case 'ip': 
  567. if ($this->is_token($string, $i)) { 
  568. if (($string{$i} === ':' || $string{$i} === '=') && $this->property != '') { 
  569. $this->status = 'iv'; 
  570. if (!$this->get_cfg('discard_invalid_properties') || $this->property_is_valid($this->property)) { 
  571. $this->property = $this->css_new_property($this->at, $this->selector, $this->property); 
  572. $this->_add_token(PROPERTY, $this->property); 
  573. } elseif ($string{$i} === '/' && @$string{$i + 1} === '*' && $this->property == '') { 
  574. $this->status = 'ic'; 
  575. ++$i; 
  576. $this->from[] = 'ip'; 
  577. } elseif ($string{$i} === '}') { 
  578. $this->explode_selectors(); 
  579. $this->status = 'is'; 
  580. $this->invalid_at = false; 
  581. $this->_add_token(SEL_END, $this->selector); 
  582. $this->selector = ''; 
  583. $this->property = ''; 
  584. if ($this->next_selector_at) { 
  585. $this->at = $this->css_new_media_section($this->next_selector_at); 
  586. $this->next_selector_at = ''; 
  587. } elseif ($string{$i} === ';') { 
  588. $this->property = ''; 
  589. } elseif ($string{$i} === '\\') { 
  590. $this->property .= $this->_unicode($string, $i); 
  591. // else this is dumb IE a hack, keep it 
  592. // including // 
  593. elseif (($this->property === '' && !ctype_space($string{$i})) 
  594. || ($this->property === '/' || $string{$i} === '/')) { 
  595. $this->property .= $string{$i}; 
  596. } elseif (!ctype_space($string{$i})) { 
  597. $this->property .= $string{$i}; 
  598. break; 
  599.  
  600. /** Case in-value */ 
  601. case 'iv': 
  602. $pn = (($string{$i} === "\n" || $string{$i} === "\r") && $this->property_is_next($string, $i + 1) || $i == strlen($string) - 1); 
  603. if ($this->is_token($string, $i) || $pn) { 
  604. if ($string{$i} === '/' && @$string{$i + 1} === '*') { 
  605. $this->status = 'ic'; 
  606. ++$i; 
  607. $this->from[] = 'iv'; 
  608. } elseif (($string{$i} === '"' || $string{$i} === "'" || $string{$i} === '(')) { 
  609. $this->cur_string[] = $string{$i}; 
  610. $this->str_char[] = ($string{$i} === '(') ? ')' : $string{$i}; 
  611. $this->status = 'instr'; 
  612. $this->from[] = 'iv'; 
  613. $this->quoted_string[] = in_array(strtolower($this->property), $quoted_string_properties); 
  614. } elseif ($string{$i} === ', ') { 
  615. $this->sub_value = trim($this->sub_value) . ', '; 
  616. } elseif ($string{$i} === '\\') { 
  617. $this->sub_value .= $this->_unicode($string, $i); 
  618. } elseif ($string{$i} === ';' || $pn) { 
  619. if ($this->selector{0} === '@' && isset($at_rules[substr($this->selector, 1)]) && $at_rules[substr($this->selector, 1)] === 'iv') { 
  620. /** Add quotes to charset, import, namespace */ 
  621. $this->sub_value_arr[] = trim($this->sub_value); 
  622.  
  623. $this->status = 'is'; 
  624.  
  625. switch ($this->selector) { 
  626. case '@charset': $this->charset = '"'.$this->sub_value_arr[0].'"'; 
  627. break; 
  628. case '@namespace': $this->namespace = implode(' ', $this->sub_value_arr); 
  629. break; 
  630. case '@import': $this->import[] = implode(' ', $this->sub_value_arr); 
  631. break; 
  632.  
  633. $this->sub_value_arr = array(); 
  634. $this->sub_value = ''; 
  635. $this->selector = ''; 
  636. $this->sel_separate = array(); 
  637. } else { 
  638. $this->status = 'ip'; 
  639. } elseif ($string{$i} !== '}') { 
  640. $this->sub_value .= $string{$i}; 
  641. if (($string{$i} === '}' || $string{$i} === ';' || $pn) && !empty($this->selector)) { 
  642. if ($this->at == '') { 
  643. $this->at = $this->css_new_media_section(DEFAULT_AT); 
  644.  
  645. // case settings 
  646. if ($this->get_cfg('lowercase_s')) { 
  647. $this->selector = strtolower($this->selector); 
  648. $this->property = strtolower($this->property); 
  649.  
  650. $this->optimise->subvalue(); 
  651. if ($this->sub_value != '') { 
  652. $this->sub_value_arr[] = $this->sub_value; 
  653. $this->sub_value = ''; 
  654.  
  655. $this->value = ''; 
  656. while (count($this->sub_value_arr)) { 
  657. $sub = array_shift($this->sub_value_arr); 
  658. if (strstr($this->selector, 'font-face')) { 
  659. $sub = $this->quote_font_format($sub); 
  660.  
  661. if ($sub != '') 
  662. $this->value .= ((!strlen($this->value) || substr($this->value, -1, 1) === ', ')?'':' ').$sub; 
  663.  
  664. $this->optimise->value(); 
  665.  
  666. $valid = $this->property_is_valid($this->property); 
  667. if ((!$this->invalid_at || $this->get_cfg('preserve_css')) && (!$this->get_cfg('discard_invalid_properties') || $valid)) { 
  668. $this->css_add_property($this->at, $this->selector, $this->property, $this->value); 
  669. $this->_add_token(VALUE, $this->value); 
  670. $this->optimise->shorthands(); 
  671. if (!$valid) { 
  672. if ($this->get_cfg('discard_invalid_properties')) { 
  673. $this->log('Removed invalid property: ' . $this->property, 'Warning'); 
  674. } else { 
  675. $this->log('Invalid property in ' . strtoupper($this->get_cfg('css_level')) . ': ' . $this->property, 'Warning'); 
  676.  
  677. $this->property = ''; 
  678. $this->sub_value_arr = array(); 
  679. $this->value = ''; 
  680. if ($string{$i} === '}') { 
  681. $this->explode_selectors(); 
  682. $this->_add_token(SEL_END, $this->selector); 
  683. $this->status = 'is'; 
  684. $this->invalid_at = false; 
  685. $this->selector = ''; 
  686. if ($this->next_selector_at) { 
  687. $this->at = $this->css_new_media_section($this->next_selector_at); 
  688. $this->next_selector_at = ''; 
  689. } elseif (!$pn) { 
  690. $this->sub_value .= $string{$i}; 
  691.  
  692. if (ctype_space($string{$i})) { 
  693. $this->optimise->subvalue(); 
  694. if ($this->sub_value != '') { 
  695. $this->sub_value_arr[] = $this->sub_value; 
  696. $this->sub_value = ''; 
  697. break; 
  698.  
  699. /** Case in string */ 
  700. case 'instr': 
  701. $_str_char = $this->str_char[count($this->str_char)-1]; 
  702. $_cur_string = $this->cur_string[count($this->cur_string)-1]; 
  703. $_quoted_string = $this->quoted_string[count($this->quoted_string)-1]; 
  704. $temp_add = $string{$i}; 
  705.  
  706. // Add another string to the stack. Strings can't be nested inside of quotes, only parentheses, but 
  707. // parentheticals can be nested more than once. 
  708. if ($_str_char === ")" && ($string{$i} === "(" || $string{$i} === '"' || $string{$i} === '\'') && !$this->escaped($string, $i)) { 
  709. $this->cur_string[] = $string{$i}; 
  710. $this->str_char[] = $string{$i} === '(' ? ')' : $string{$i}; 
  711. $this->from[] = 'instr'; 
  712. $this->quoted_string[] = ($_str_char === ')' && $string{$i} !== '(' && trim($_cur_string)==='(')?$_quoted_string:!($string{$i} === '('); 
  713. continue; 
  714.  
  715. if ($_str_char !== ")" && ($string{$i} === "\n" || $string{$i} === "\r") && !($string{$i - 1} === '\\' && !$this->escaped($string, $i - 1))) { 
  716. $temp_add = "\\A"; 
  717. $this->log('Fixed incorrect newline in string', 'Warning'); 
  718.  
  719. $_cur_string .= $temp_add; 
  720.  
  721. if ($string{$i} === $_str_char && !$this->escaped($string, $i)) { 
  722. $this->status = array_pop($this->from); 
  723.  
  724. if (!preg_match('|[' . implode('', $this->data['csstidy']['whitespace']) . ']|uis', $_cur_string) && $this->property !== 'content') { 
  725. if (!$_quoted_string) { 
  726. if ($_str_char !== ')') { 
  727. // Convert properties like 
  728. // font-family: 'Arial'; 
  729. // to 
  730. // font-family: Arial; 
  731. // or 
  732. // url("abc") 
  733. // to 
  734. // url(abc) 
  735. $_cur_string = substr($_cur_string, 1, -1); 
  736. } else { 
  737. $_quoted_string = false; 
  738.  
  739. array_pop($this->cur_string); 
  740. array_pop($this->quoted_string); 
  741. array_pop($this->str_char); 
  742.  
  743. if ($_str_char === ')') { 
  744. $_cur_string = '(' . trim(substr($_cur_string, 1, -1)) . ')'; 
  745.  
  746. if ($this->status === 'iv') { 
  747. if (!$_quoted_string) { 
  748. if (strpos($_cur_string, ', ') !== false) 
  749. // we can on only remove space next to ', ' 
  750. $_cur_string = implode(', ', array_map('trim', explode(', ', $_cur_string))); 
  751. // and multiple spaces (too expensive) 
  752. if (strpos($_cur_string, ' ') !== false) 
  753. $_cur_string = preg_replace(", \s+, ", ' ', $_cur_string); 
  754. $this->sub_value .= $_cur_string; 
  755. } elseif ($this->status === 'is') { 
  756. $this->selector .= $_cur_string; 
  757. } elseif ($this->status === 'instr') { 
  758. $this->cur_string[count($this->cur_string)-1] .= $_cur_string; 
  759. } else { 
  760. $this->cur_string[count($this->cur_string)-1] = $_cur_string; 
  761. break; 
  762.  
  763. /** Case in-comment */ 
  764. case 'ic': 
  765. if ($string{$i} === '*' && $string{$i + 1} === '/') { 
  766. $this->status = array_pop($this->from); 
  767. $i++; 
  768. $this->_add_token(COMMENT, $cur_comment); 
  769. $cur_comment = ''; 
  770. } else { 
  771. $cur_comment .= $string{$i}; 
  772. break; 
  773.  
  774. $this->optimise->postparse(); 
  775.  
  776. $this->print->_reset(); 
  777.  
  778. @setlocale(LC_ALL, $old); // Set locale back to original setting 
  779.  
  780. return!(empty($this->css) && empty($this->import) && empty($this->charset) && empty($this->tokens) && empty($this->namespace)); 
  781.  
  782.  
  783. /** 
  784. * format() in font-face needs quoted values for somes browser (FF at least) 
  785. * @param $value 
  786. * @return string 
  787. */ 
  788. public function quote_font_format($value) { 
  789. if (strncmp($value, 'format', 6) == 0) { 
  790. $p = strpos($value, ')', 7); 
  791. $end = substr($value, $p); 
  792. $format_strings = $this->parse_string_list(substr($value, 7, $p-7)); 
  793. if (!$format_strings) { 
  794. $value = ''; 
  795. } else { 
  796. $value = 'format('; 
  797.  
  798. foreach ($format_strings as $format_string) { 
  799. $value .= '"' . str_replace('"', '\\"', $format_string) . '", '; 
  800.  
  801. $value = substr($value, 0, -1) . $end; 
  802. return $value; 
  803.  
  804. /** 
  805. * Explodes selectors 
  806. * @access private 
  807. * @version 1.0 
  808. */ 
  809. public function explode_selectors() { 
  810. // Explode multiple selectors 
  811. if ($this->get_cfg('merge_selectors') === 1) { 
  812. $new_sels = array(); 
  813. $lastpos = 0; 
  814. $this->sel_separate[] = strlen($this->selector); 
  815. foreach ($this->sel_separate as $num => $pos) { 
  816. if ($num == count($this->sel_separate) - 1) { 
  817. $pos += 1; 
  818.  
  819. $new_sels[] = substr($this->selector, $lastpos, $pos - $lastpos - 1); 
  820. $lastpos = $pos; 
  821.  
  822. if (count($new_sels) > 1) { 
  823. foreach ($new_sels as $selector) { 
  824. if (isset($this->css[$this->at][$this->selector])) { 
  825. $this->merge_css_blocks($this->at, $selector, $this->css[$this->at][$this->selector]); 
  826. unset($this->css[$this->at][$this->selector]); 
  827. $this->sel_separate = array(); 
  828.  
  829. /** 
  830. * Checks if a character is escaped (and returns true if it is) 
  831. * @param string $string 
  832. * @param integer $pos 
  833. * @access public 
  834. * @return bool 
  835. * @version 1.02 
  836. */ 
  837. static function escaped(&$string, $pos) { 
  838. return!(@($string{$pos - 1} !== '\\') || csstidy::escaped($string, $pos - 1)); 
  839.  
  840. /** 
  841. * Adds a property with value to the existing CSS code 
  842. * @param string $media 
  843. * @param string $selector 
  844. * @param string $property 
  845. * @param string $new_val 
  846. * @access private 
  847. * @version 1.2 
  848. */ 
  849. public function css_add_property($media, $selector, $property, $new_val) { 
  850. if ($this->get_cfg('preserve_css') || trim($new_val) == '') { 
  851. return; 
  852.  
  853. $this->added = true; 
  854. if (isset($this->css[$media][$selector][$property])) { 
  855. if (($this->is_important($this->css[$media][$selector][$property]) && $this->is_important($new_val)) || !$this->is_important($this->css[$media][$selector][$property])) { 
  856. $this->css[$media][$selector][$property] = trim($new_val); 
  857. } else { 
  858. $this->css[$media][$selector][$property] = trim($new_val); 
  859.  
  860. /** 
  861. * Start a new media section. 
  862. * Check if the media is not already known,  
  863. * else rename it with extra spaces 
  864. * to avoid merging 
  865. * @param string $media 
  866. * @return string 
  867. */ 
  868. public function css_new_media_section($media) { 
  869. if ($this->get_cfg('preserve_css')) { 
  870. return $media; 
  871.  
  872. // if the last @media is the same as this 
  873. // keep it 
  874. if (!$this->css || !is_array($this->css) || empty($this->css)) { 
  875. return $media; 
  876. end($this->css); 
  877. list($at, ) = each($this->css); 
  878. if ($at == $media) { 
  879. return $media; 
  880. while (isset($this->css[$media])) 
  881. if (is_numeric($media)) 
  882. $media++; 
  883. else 
  884. $media .= ' '; 
  885. return $media; 
  886.  
  887. /** 
  888. * Start a new selector. 
  889. * If already referenced in this media section,  
  890. * rename it with extra space to avoid merging 
  891. * except if merging is required,  
  892. * or last selector is the same (merge siblings) 
  893. * never merge @font-face 
  894. * @param string $media 
  895. * @param string $selector 
  896. * @return string 
  897. */ 
  898. public function css_new_selector($media, $selector) { 
  899. if ($this->get_cfg('preserve_css')) { 
  900. return $selector; 
  901. $selector = trim($selector); 
  902. if (strncmp($selector, '@font-face', 10)!=0) { 
  903. if ($this->settings['merge_selectors'] != false) 
  904. return $selector; 
  905.  
  906. if (!$this->css || !isset($this->css[$media]) || !$this->css[$media]) 
  907. return $selector; 
  908.  
  909. // if last is the same, keep it 
  910. end($this->css[$media]); 
  911. list($sel, ) = each($this->css[$media]); 
  912. if ($sel == $selector) { 
  913. return $selector; 
  914.  
  915. while (isset($this->css[$media][$selector])) 
  916. $selector .= ' '; 
  917. return $selector; 
  918.  
  919. /** 
  920. * Start a new propertie. 
  921. * If already references in this selector,  
  922. * rename it with extra space to avoid override 
  923. * @param string $media 
  924. * @param string $selector 
  925. * @param string $property 
  926. * @return string 
  927. */ 
  928. public function css_new_property($media, $selector, $property) { 
  929. if ($this->get_cfg('preserve_css')) { 
  930. return $property; 
  931. if (!$this->css || !isset($this->css[$media][$selector]) || !$this->css[$media][$selector]) 
  932. return $property; 
  933.  
  934. while (isset($this->css[$media][$selector][$property])) 
  935. $property .= ' '; 
  936.  
  937. return $property; 
  938.  
  939. /** 
  940. * Adds CSS to an existing media/selector 
  941. * @param string $media 
  942. * @param string $selector 
  943. * @param array $css_add 
  944. * @access private 
  945. * @version 1.1 
  946. */ 
  947. public function merge_css_blocks($media, $selector, $css_add) { 
  948. foreach ($css_add as $property => $value) { 
  949. $this->css_add_property($media, $selector, $property, $value, false); 
  950.  
  951. /** 
  952. * Checks if $value is !important. 
  953. * @param string $value 
  954. * @return bool 
  955. * @access public 
  956. * @version 1.0 
  957. */ 
  958. public function is_important(&$value) { 
  959. return ( 
  960. strpos($value, '!') !== false // quick test 
  961. AND !strcasecmp(substr(str_replace($this->data['csstidy']['whitespace'], '', $value), -10, 10), '!important')); 
  962.  
  963. /** 
  964. * Returns a value without !important 
  965. * @param string $value 
  966. * @return string 
  967. * @access public 
  968. * @version 1.0 
  969. */ 
  970. public function gvw_important($value) { 
  971. if ($this->is_important($value)) { 
  972. $value = trim($value); 
  973. $value = substr($value, 0, -9); 
  974. $value = trim($value); 
  975. $value = substr($value, 0, -1); 
  976. $value = trim($value); 
  977. return $value; 
  978. return $value; 
  979.  
  980. /** 
  981. * Checks if the next word in a string from pos is a CSS property 
  982. * @param string $istring 
  983. * @param integer $pos 
  984. * @return bool 
  985. * @access private 
  986. * @version 1.2 
  987. */ 
  988. public function property_is_next($istring, $pos) { 
  989. $all_properties = & $this->data['csstidy']['all_properties']; 
  990. $istring = substr($istring, $pos, strlen($istring) - $pos); 
  991. $pos = strpos($istring, ':'); 
  992. if ($pos === false) { 
  993. return false; 
  994. $istring = strtolower(trim(substr($istring, 0, $pos))); 
  995. if (isset($all_properties[$istring])) { 
  996. $this->log('Added semicolon to the end of declaration', 'Warning'); 
  997. return true; 
  998. return false; 
  999.  
  1000. /** 
  1001. * Checks if a property is valid 
  1002. * @param string $property 
  1003. * @return bool; 
  1004. * @access public 
  1005. * @version 1.0 
  1006. */ 
  1007. public function property_is_valid($property) { 
  1008. if (in_array(trim($property), $this->data['csstidy']['multiple_properties'])) $property = trim($property); 
  1009. $all_properties = & $this->data['csstidy']['all_properties']; 
  1010. return (isset($all_properties[$property]) && strpos($all_properties[$property], strtoupper($this->get_cfg('css_level'))) !== false ); 
  1011.  
  1012. /** 
  1013. * Accepts a list of strings (e.g., the argument to format() in a @font-face src property) 
  1014. * and returns a list of the strings. Converts things like: 
  1015. * format(abc) => format("abc") 
  1016. * format(abc def) => format("abc", "def") 
  1017. * format(abc "def") => format("abc", "def") 
  1018. * format(abc, def, ghi) => format("abc", "def", "ghi") 
  1019. * format("abc", 'def') => format("abc", "def") 
  1020. * format("abc, def, ghi") => format("abc, def, ghi") 
  1021. * @param string 
  1022. * @return array 
  1023. */ 
  1024.  
  1025. public function parse_string_list($value) { 
  1026. $value = trim($value); 
  1027.  
  1028. // Case: empty 
  1029. if (!$value) return array(); 
  1030.  
  1031. $strings = array(); 
  1032.  
  1033. $in_str = false; 
  1034. $current_string = ''; 
  1035.  
  1036. for ($i = 0, $_len = strlen($value); $i < $_len; $i++) { 
  1037. if (($value{$i} === ', ' || $value{$i} === ' ') && $in_str === true) { 
  1038. $in_str = false; 
  1039. $strings[] = $current_string; 
  1040. $current_string = ''; 
  1041. } elseif ($value{$i} === '"' || $value{$i} === "'") { 
  1042. if ($in_str === $value{$i}) { 
  1043. $strings[] = $current_string; 
  1044. $in_str = false; 
  1045. $current_string = ''; 
  1046. continue; 
  1047. } elseif (!$in_str) { 
  1048. $in_str = $value{$i}; 
  1049. } else { 
  1050. if ($in_str) { 
  1051. $current_string .= $value{$i}; 
  1052. } else { 
  1053. if (!preg_match("/[\s, ]/", $value{$i})) { 
  1054. $in_str = true; 
  1055. $current_string = $value{$i}; 
  1056.  
  1057. if ($current_string) { 
  1058. $strings[] = $current_string; 
  1059.  
  1060. return $strings;