/modules/custom-css/csstidy/class.csstidy.php

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