csstidy
[ class tree: csstidy ] [ index: csstidy ] [ all elements ]

Source for file css_parser.php

Documentation is available at css_parser.php

  1. <?php
  2. /**
  3. * CSSTidy - CSS Parser and Optimiser
  4. *
  5. * CSS Parser class
  6. * This class represents a CSS parser which reads CSS code and saves it in an array.
  7. * In opposite to most other CSS parsers, it does not use regular expressions and
  8. * thus has full CSS2 support and a higher reliability.
  9. * Additional to that it applies some optimisations and fixes to the CSS code.
  10. * An online version should be available here: http://cdburnerxp.se/cssparse/css_optimiser.php
  11. *
  12. * This file is part of CSSTidy.
  13. *
  14. * CSSTidy is free software; you can redistribute it and/or modify
  15. * it under the terms of the GNU General Public License as published by
  16. * the Free Software Foundation; either version 2 of the License, or
  17. * (at your option) any later version.
  18. *
  19. * CSSTidy is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU General Public License
  25. * along with CSSTidy; if not, write to the Free Software
  26. * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  27. *
  28. * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  29. * @package csstidy
  30. * @author Florian Schmitz (floele at gmail dot com) 2005
  31. */
  32.  
  33. /**
  34. * Various CSS data needed for correct optimisations etc.
  35. *
  36. * @version 1.2
  37. */
  38. require('data.inc.php');
  39.  
  40. /**
  41. * All functions which are not directly related to the parser class
  42. *
  43. * Not required. If this file is not included, csstidy does without these functions.
  44. * @version 1.2
  45. */
  46. @include('functions.inc.php');
  47.  
  48. /**
  49. * CSS Parser class
  50. *
  51. * This class represents a CSS parser which reads CSS code and saves it in an array.
  52. * In opposite to most other CSS parsers, it does not use regular expressions and
  53. * thus has full CSS2 support and a higher reliability.
  54. * Additional to that it applies some optimisations and fixes to the CSS code.
  55. * An online version should be available here: http://cdburnerxp.se/cssparse/css_optimiser.php
  56. * @package csstidy
  57. * @author Florian Schmitz (floele at gmail dot com) 2005
  58. * @version 1.1
  59. */
  60. class csstidy {
  61.  
  62. /**
  63. * Saves the parsed CSS
  64. * @var array
  65. * @access public
  66. */
  67. var $css = array();
  68.  
  69. /**
  70. * Saves the CSS charset (@charset)
  71. * @var string
  72. * @access private
  73. */
  74. var $charset = '';
  75.  
  76. /**
  77. * Saves all @import URLs
  78. * @var array
  79. * @access private
  80. */
  81. var $import = array();
  82.  
  83. /**
  84. * Saves the namespace
  85. * @var string
  86. * @access private
  87. */
  88. var $namespace = '';
  89.  
  90. /**
  91. * Saves the input CSS string
  92. * @var string
  93. * @access private
  94. */
  95. var $input_css = '';
  96.  
  97. /**
  98. * Saves the formatted CSS string
  99. * @var string
  100. * @access public
  101. */
  102. var $output_css = '';
  103.  
  104. /**
  105. * Saves the formatted CSS string (plain text)
  106. * @var string
  107. * @access public
  108. */
  109. var $output_css_plain = '';
  110.  
  111. /**
  112. * Saves the templates
  113. * @var array
  114. * @access private
  115. * @see http://cdburnerxp.se/cssparse/template.htm
  116. */
  117. var $template = array();
  118.  
  119. /**
  120. * Contains the version of csstidy
  121. * @var string
  122. * @access private
  123. */
  124. var $version = '1.1';
  125.  
  126. /**
  127. * Stores comments
  128. * @var array
  129. * @access private
  130. */
  131. var $comments = array();
  132.  
  133. /**
  134. * Stores the settings
  135. * @var array
  136. * @access private
  137. */
  138. var $settings = array();
  139.  
  140. /**
  141. * Saves the parser-status.
  142. *
  143. * Possible values:
  144. * - is = in selector
  145. * - ip = in property
  146. * - iv = in value
  147. * - instr = in string (started at " or ' or ( )
  148. * - ic = in comment (ignore everything)
  149. * - at = in @-block
  150. *
  151. * @var string
  152. * @access private
  153. */
  154. var $status = 'is';
  155.  
  156.  
  157. /**
  158. * Saves the current at rule (@media)
  159. * @var string
  160. * @access private
  161. */
  162. var $at = '';
  163.  
  164. /**
  165. * Saves the current selector
  166. * @var string
  167. * @access private
  168. */
  169. var $selector = '';
  170.  
  171. /**
  172. * Saves the current property
  173. * @var string
  174. * @access private
  175. */
  176. var $property = '';
  177.  
  178. /**
  179. * Saves the position of , in selectors
  180. * @var array
  181. * @access private
  182. */
  183. var $sel_seperate = array();
  184.  
  185. /**
  186. * Saves the current value
  187. * @var string
  188. * @access private
  189. */
  190. var $value = '';
  191.  
  192. /**
  193. * Saves the current sub-value
  194. *
  195. * Example for a subvalue:
  196. * background:url(foo.png) red no-repeat;
  197. * "url(foo.png)", "red", and "no-repeat" are subvalues,
  198. * seperated by whitespace
  199. * @var string
  200. * @access private
  201. */
  202. var $sub_value = '';
  203.  
  204. /**
  205. * Array which saves all subvalues for a property.
  206. * @var array
  207. * @see sub_value
  208. * @access private
  209. */
  210. var $sub_value_arr = array();
  211.  
  212. /**
  213. * Saves the char which opened the last string
  214. * @var string
  215. * @access private
  216. */
  217. var $str_char = '';
  218.  
  219. /**
  220. * Status where the string has been started (is or iv)
  221. * @var string
  222. * @access private
  223. */
  224. var $str_from = '';
  225.  
  226. /**
  227. * Variable needed to manage string-in-strings, for example url("foo.png")
  228. * @var string
  229. * @access private
  230. */
  231. var $str_in_str = false;
  232.  
  233. /**
  234. * =true if in invalid at-rule
  235. * @var bool
  236. * @access private
  237. */
  238. var $invalid_at = false;
  239.  
  240. /**
  241. * =true if something has been added to the current selector
  242. * @var bool
  243. * @access private
  244. */
  245. var $added = false;
  246.  
  247. /**
  248. * Status where the comment has been started
  249. * @var string
  250. * @access private
  251. */
  252. var $comment_from = '';
  253.  
  254. /**
  255. * Array which saves the message log
  256. * @var array
  257. * @access private
  258. */
  259. var $log = array();
  260.  
  261. /**
  262. * Saves the line number
  263. * @var integer
  264. * @access private
  265. */
  266. var $line = 1;
  267.  
  268. /**
  269. * Loads standard template and sets default settings
  270. * @access private
  271. * @version 1.1
  272. */
  273. function csstidy()
  274. {
  275. $this->settings['remove_bslash'] = true;
  276. $this->settings['compress_colors'] = true;
  277. $this->settings['compress_font-weight'] = true;
  278. $this->settings['lowercase_s'] = false;
  279. $this->settings['save_ie_hacks'] = false;
  280. $this->settings['optimise_shorthands'] = true;
  281. $this->settings['only_safe_optimisations'] = true;
  282. $this->settings['remove_last_;'] = false;
  283. $this->settings['uppercase_properties'] = false;
  284. $this->settings['sort_properties'] = false;
  285. $this->settings['sort_selectors'] = false;
  286. $this->settings['merge_selectors'] = 2;
  287. $this->settings['discard_invalid_properties'] = false;
  288. $this->settings['save_comments'] = false;
  289. $this->settings['css_level'] = 'CSS2.1';
  290.  
  291. $this->load_template('default');
  292. }
  293.  
  294. /**
  295. * Get the value of a setting.
  296. * @param string $setting
  297. * @access public
  298. * @return mixed
  299. * @version 1.0
  300. */
  301. function get_cfg($setting)
  302. {
  303. if(isset($this->settings[$setting]))
  304. {
  305. return $this->settings[$setting];
  306. }
  307. return false;
  308. }
  309.  
  310. /**
  311. * Set the value of a setting.
  312. * @param string $setting
  313. * @param mixed $value
  314. * @access public
  315. * @return bool
  316. * @version 1.0
  317. */
  318. function set_cfg($setting,$value)
  319. {
  320. if(isset($this->settings[$setting]) && $value !== '')
  321. {
  322. $this->settings[$setting] = $value;
  323. return true;
  324. }
  325. return false;
  326. }
  327.  
  328. /**
  329. * Add a message to the message log
  330. * @param string $message
  331. * @param string $type
  332. * @param integer $line
  333. * @access private
  334. * @version 1.0
  335. */
  336. function log($message,$type,$line = -1)
  337. {
  338. if($line === -1)
  339. {
  340. $line = $this->line;
  341. }
  342. $line = intval($line);
  343. $add = array('m' => $message, 't' => $type);
  344. if(!isset($this->log[$line]) || !in_array($add,$this->log[$line]))
  345. {
  346. $this->log[$line][] = $add;
  347. }
  348. }
  349.  
  350. /**
  351. * Parse unicode notations and find a replacement character
  352. * @param string $string
  353. * @param integer $i
  354. * @access private
  355. * @return string
  356. * @version 1.2
  357. */
  358. function unicode(&$string,&$i)
  359. {
  360. ++$i;
  361. $add = '';
  362. $tokens =& $GLOBALS['csstidy']['tokens'];
  363. $replaced = false;
  364. while($i < strlen($string) && (ctype_xdigit($string{$i}) || ctype_space($string{$i})) && strlen($add) < 6)
  365. {
  366. $add .= $string{$i};
  367.  
  368. if(ctype_space($string{$i}))
  369. {
  370. break;
  371. }
  372. $i++;
  373. }
  374.  
  375. if(hexdec($add) > 47 && hexdec($add) < 58 || hexdec($add) > 64 && hexdec($add) < 91 || hexdec($add) > 96 && hexdec($add) < 123)
  376. {
  377. $this->log('Replaced unicode notation: Changed \\'.$add.' to '.chr(hexdec($add)),'Information');
  378. $add = chr(hexdec($add));
  379. $replaced = true;
  380. }
  381. else
  382. {
  383. $add = trim('\\'.$add);
  384. }
  385.  
  386. if(@ctype_xdigit($string{$i+1}) && ctype_space($string{$i}) && !$replaced || !ctype_space($string{$i}))
  387. {
  388. $i--;
  389. }
  390. if($add != '\\' || !$this->get_cfg('remove_bslash') || strpos($tokens, $string{$i+1}) !== false)
  391. {
  392. return $add;
  393. }
  394. if($add == '\\')
  395. {
  396. $this->log('Removed unnecessary backslash','Information');
  397. }
  398. return '';
  399. }
  400.  
  401. /**
  402. * Compresses shorthand values. Example: margin:1px 1px 1px 1px -> margin:1px
  403. * @param string $value
  404. * @access private
  405. * @return string
  406. * @version 1.0
  407. */
  408. function shorthand($value)
  409. {
  410. $important = '';
  411. if(csstidy::is_important($value))
  412. {
  413. $values = csstidy::gvw_important($value);
  414. $important = ' !important';
  415. }
  416. else $values = $value;
  417. $values = explode(' ',$values);
  418. switch(count($values))
  419. {
  420. case 4:
  421. if($values[0] == $values[1] && $values[0] == $values[2] && $values[0] == $values[3])
  422. {
  423. return $values[0].$important;
  424. }
  425. elseif($values[1] == $values[3] && $values[0] == $values[2])
  426. {
  427. return $values[0].' '.$values[1].$important;
  428. }
  429. elseif($values[1] == $values[3])
  430. {
  431. return $values[0].' '.$values[1].' '.$values[2].$important;
  432. }
  433. break;
  434. case 3:
  435. if($values[0] == $values[1] && $values[0] == $values[2])
  436. {
  437. return $values[0].$important;
  438. }
  439. elseif($values[0] == $values[2])
  440. {
  441. return $values[0].' '.$values[1].$important;
  442. }
  443. break;
  444. case 2:
  445. if($values[0] == $values[1])
  446. {
  447. return $values[0].$important;
  448. }
  449. break;
  450. }
  451. return $value;
  452. }
  453.  
  454. /**
  455. * Get compression ratio and prints the code if necessary.
  456. * @access public
  457. * @return float
  458. * @version 1.1
  459. */
  460. function get_ratio()
  461. {
  462. if(empty($this->output_css_plain))
  463. {
  464. $this->print_code($this->css);
  465. }
  466. return $ratio = round(((strlen($this->input_css))-(strlen($this->output_css_plain)))/(strlen($this->input_css)),3)*100;
  467. }
  468.  
  469. /**
  470. * Get difference between the old and new code in bytes and prints the code if necessary.
  471. * @access public
  472. * @return string
  473. * @version 1.1
  474. */
  475. function get_diff()
  476. {
  477. if(empty($this->output_css_plain))
  478. {
  479. $this->print_code($this->css);
  480. }
  481. $diff = (strlen($this->output_css_plain))-(strlen($this->input_css));
  482. if($diff > 0)
  483. {
  484. return '+'.$diff;
  485. }
  486. elseif($diff == 0)
  487. {
  488. return '+-'.$diff;
  489. }
  490. else
  491. {
  492. return $diff;
  493. }
  494. }
  495.  
  496. /**
  497. * Get the size of either input or output CSS in KB
  498. * @param string $loc default is "output"
  499. * @access public
  500. * @return integer
  501. * @version 1.0
  502. */
  503. function size($loc = 'output')
  504. {
  505. if($loc == 'output' && empty($this->output_css))
  506. {
  507. $this->print_code($this->css);
  508. }
  509. if($loc == 'input')
  510. {
  511. return (strlen($this->input_css)/1000);
  512. }
  513. else
  514. {
  515. return (strlen(html_entity_decode(strip_tags($this->output_css)))/1000);
  516. }
  517. }
  518.  
  519. /**
  520. * Loads a new template
  521. * @param string $content either filename (if $from_file == true), content of a template file, "high_compression", "highest_compression", "low_compression", or "default"
  522. * @param bool $from_file uses $content as filename if true
  523. * @access public
  524. * @version 1.1
  525. * @see http://csstidy.sourceforge.net/templates.php
  526. */
  527. function load_template($content,$from_file=true)
  528. {
  529. $predefined_templates =& $GLOBALS['csstidy']['predefined_templates'];
  530. if($content == 'high_compression' || $content == 'default' || $content == 'highest_compression' || $content == 'low_compression')
  531. {
  532. $this->template = $predefined_templates[$content];
  533. return;
  534. }
  535. if($from_file)
  536. {
  537. $content = strip_tags(file_get_contents($content),'<span>');
  538. }
  539. $content = str_replace("\r\n","\n",$content); // Unify newlines (because the output also only uses \n)
  540. $template = explode('|',$content);
  541.  
  542. for ($i = 0; $i < count($template); $i++ )
  543. {
  544. $this->template[$i] = $template[$i];
  545. }
  546. }
  547.  
  548. /**
  549. * Starts parsing from URL
  550. * @param string $url
  551. * @access public
  552. * @version 1.0
  553. */
  554. function parse_from_url($url)
  555. {
  556. $content = @file_get_contents($url);
  557. return $this->parse($content);
  558. }
  559.  
  560. /**
  561. * Checks if there is a token at the current position
  562. * @param string $string
  563. * @param integer $i
  564. * @access public
  565. * @version 1.1
  566. */
  567. function is_token(&$string,$i)
  568. {
  569. $tokens =& $GLOBALS['csstidy']['tokens'];
  570. return (strpos($tokens, $string{$i}) !== false && !csstidy::escaped($string,$i));
  571. }
  572.  
  573.  
  574. /**
  575. * Parses CSS in $string. The code is saved as array in $this->css
  576. * @param string $string the CSS code
  577. * @access public
  578. * @return bool
  579. * @version 1.1
  580. */
  581. function parse($string) {
  582.  
  583. $shorthands =& $GLOBALS['csstidy']['shorthands'];
  584. $all_properties =& $GLOBALS['csstidy']['all_properties'];
  585. $at_rules =& $GLOBALS['csstidy']['at_rules'];
  586.  
  587. $this->css = array();
  588. $this->input_css = $string;
  589. $string = str_replace("\r\n","\n",$string) . ' ';
  590. $cur_comment = '';
  591.  
  592. for ($i = 0, $size = strlen($string); $i < $size; $i++ )
  593. {
  594. if($string{$i} == "\n" || $string{$i} == "\r")
  595. {
  596. ++$this->line;
  597. }
  598. switch($this->status)
  599. {
  600. /* Case in at-block */
  601. case 'at':
  602. if(csstidy::is_token($string,$i))
  603. {
  604. if($string{$i} == '/' && @$string{$i+1} == '*')
  605. {
  606. $this->status = 'ic'; ++$i;
  607. $this->comment_from = 'at';
  608. }
  609. elseif($string{$i} == '{')
  610. {
  611. $this->status = 'is';
  612. }
  613. elseif($string{$i} == ',')
  614. {
  615. $this->at = trim($this->at).',';
  616. }
  617. elseif($string{$i} == '\\')
  618. {
  619. $this->at .= $this->unicode($string,$i);
  620. }
  621. }
  622. else
  623. {
  624. $lastpos = strlen($this->at)-1;
  625. if(!( (ctype_space($this->at{$lastpos}) || csstidy::is_token($this->at,$lastpos) && $this->at{$lastpos} == ',') && ctype_space($string{$i})))
  626. {
  627. $this->at .= $string{$i};
  628. }
  629. }
  630. break;
  631. /* Case in-selector */
  632. case 'is':
  633. if(csstidy::is_token($string,$i))
  634. {
  635. if($string{$i} == '/' && @$string{$i+1} == '*' && trim($this->selector) == '')
  636. {
  637. $this->status = 'ic'; ++$i;
  638. $this->comment_from = 'is';
  639. }
  640. elseif($string{$i} == '@' && trim($this->selector) == '')
  641. {
  642. // Check for at-rule
  643. $this->invalid_at = true;
  644. foreach($at_rules as $name => $type)
  645. {
  646. if(!strcasecmp(substr($string,$i+1,strlen($name)),$name))
  647. {
  648. ($type == 'at') ? $this->at = '@'.$name : $this->selector = '@'.$name;
  649. $this->status = $type;
  650. $i += strlen($name);
  651. $this->invalid_at = false;
  652. }
  653. }
  654. if($this->invalid_at)
  655. {
  656. $this->selector = '@'.$this->selector;
  657. $invalid_at_name = '';
  658. for($j = $i+1; $j < $size; ++$j)
  659. {
  660. if(!ctype_alpha($string{$j}))
  661. {
  662. break;
  663. }
  664. $invalid_at_name .= $string{$j};
  665. }
  666. $this->log('Invalid @-rule: '.$invalid_at_name.' (removed)','Warning');
  667. }
  668. }
  669. elseif(($string{$i} == '"' || $string{$i} == "'"))
  670. {
  671. $this->selector .= $string{$i};
  672. $this->status = 'instr';
  673. $this->str_char = $string{$i};
  674. $this->str_from = 'is';
  675. }
  676. elseif($this->invalid_at && $string{$i} == ';')
  677. {
  678. $this->invalid_at = false;
  679. $this->status = 'is';
  680. }
  681. elseif($string{$i} == '{')
  682. {
  683. $this->status = 'ip';
  684. $this->added = false;
  685. }
  686. elseif($string{$i} == '}')
  687. {
  688. $this->at = '';
  689. $this->selector = '';
  690. $this->sel_seperate = array();
  691. }
  692. elseif($string{$i} == ',')
  693. {
  694. $this->selector = trim($this->selector).',';
  695. $this->sel_seperate[] = strlen($this->selector);
  696. }
  697. elseif($string{$i} == '\\')
  698. {
  699. $this->selector .= $this->unicode($string,$i);
  700. }
  701. else $this->selector .= $string{$i};
  702. }
  703. else
  704. {
  705. $lastpos = strlen($this->selector)-1;
  706. if($lastpos == -1 || !( (ctype_space($this->selector{$lastpos}) || csstidy::is_token($this->selector,$lastpos) && $this->selector{$lastpos} == ',') && ctype_space($string{$i})))
  707. {
  708. $this->selector .= $string{$i};
  709. }
  710. }
  711. break;
  712. /* Case in-property */
  713. case 'ip':
  714. if(csstidy::is_token($string,$i))
  715. {
  716. if(($string{$i} == ':' || $string{$i} == '=') && $this->property != '')
  717. {
  718. $this->status = 'iv';
  719. }
  720. elseif($string{$i} == '/' && @$string{$i+1} == '*' && $this->property == '')
  721. {
  722. $this->status = 'ic'; ++$i;
  723. $this->comment_from = 'ip';
  724. }
  725. elseif($string{$i} == '}')
  726. {
  727. $this->explode_selectors();
  728. $this->status = 'is';
  729. $this->invalid_at = false;
  730. if($this->selector{0} != '@' && !$this->added)
  731. {
  732. $this->log('Removed empty selector: '.trim($this->selector),'Information');
  733. }
  734. $this->selector = '';
  735. $this->property = '';
  736. }
  737. elseif($string{$i} == ';')
  738. {
  739. $this->property = '';
  740. }
  741. elseif($string{$i} == '\\')
  742. {
  743. $this->property .= $this->unicode($string,$i);
  744. }
  745. }
  746. elseif(!ctype_space($string{$i}))
  747. {
  748. $this->property .= $string{$i};
  749. }
  750. break;
  751. /* Case in-value */
  752. case 'iv':
  753. $pn = (ctype_space($string{$i}) && $this->property_is_next($string,$i+1) || $i == strlen($string)-1);
  754. if(csstidy::is_token($string,$i) || $pn)
  755. {
  756. if($string{$i} == '/' && @$string{$i+1} == '*')
  757. {
  758. $this->status = 'ic'; ++$i;
  759. $this->comment_from = 'iv';
  760. }
  761. elseif(($string{$i} == '"' || $string{$i} == "'" || $string{$i} == '('))
  762. {
  763. $this->sub_value .= $string{$i};
  764. $this->str_char = ($string{$i} == '(') ? ')' : $string{$i};
  765. $this->status = 'instr';
  766. $this->str_from = 'iv';
  767. }
  768. elseif($string{$i} == ',')
  769. {
  770. $this->sub_value = trim($this->sub_value).',';
  771. }
  772. elseif($string{$i} == '\\')
  773. {
  774. $this->sub_value .= $this->unicode($string,$i);
  775. }
  776. elseif($string{$i} == ';' || $pn)
  777. {
  778. if($this->selector{0} == '@' && isset($at_rules[substr($this->selector,1)]) && $at_rules[substr($this->selector,1)] == 'iv')
  779. {
  780. $this->sub_value_arr[] = trim($this->sub_value);
  781. $this->status = 'is';
  782. switch($this->selector)
  783. {
  784. case '@charset': $this->charset = $this->sub_value_arr[0]; break;
  785. case '@namespace': $this->namespace = implode(' ',$this->sub_value_arr); break;
  786. case '@import': $this->import[] = implode(' ',$this->sub_value_arr); break;
  787. }
  788. $this->sub_value_arr = array();
  789. $this->sub_value = '';
  790. $this->selector = '';
  791. $this->sel_seperate = array();
  792. }
  793. else
  794. {
  795. $this->status = 'ip';
  796. }
  797. }
  798. elseif($string{$i} != '}')
  799. {
  800. $this->sub_value .= $string{$i};
  801. }
  802. if(($string{$i} == '}' || $string{$i} == ';' || $pn) && !empty($this->selector))
  803. {
  804. if($this->at == '')
  805. {
  806. $this->at = 'standard';
  807. }
  808. // case settings
  809. if($this->get_cfg('lowercase_s'))
  810. {
  811. $this->selector = strtolower($this->selector);
  812. }
  813. $this->property = strtolower($this->property);
  814. $this->optimise_add_subvalue();
  815. $this->value = implode(' ',$this->sub_value_arr);
  816. $this->selector = trim($this->selector);
  817. // Remove whitespace at ! important
  818. if($this->value != $this->c_important($this->value))
  819. {
  820. $this->log('Optimised !important','Information');
  821. }
  822. // optimise shorthand properties
  823. if(isset($shorthands[$this->property]))
  824. {
  825. $temp = csstidy::shorthand($this->value);
  826. if($temp != $this->value)
  827. {
  828. $this->log('Optimised shorthand notation ('.$this->property.'): Changed "'.$this->value.'" to "'.$temp.'"','Information');
  829. }
  830. $this->value = $temp;
  831. }
  832. // Compress font-weight
  833. if($this->property == 'font-weight' && $this->get_cfg('compress_font-weight'))
  834. {
  835. $this->c_font_weight($this->value);
  836. }
  837.  
  838. $valid = (isset($all_properties[$this->property]) && strpos($all_properties[$this->property],strtoupper($this->get_cfg('css_level'))) !== false );
  839. if(!$this->invalid_at && trim($this->value) !== '' && (!$this->get_cfg('discard_invalid_properties') || $valid))
  840. {
  841. $this->css_add_property($this->at,$this->selector,$this->property,$this->value);
  842. // Further Optimisation
  843. if($this->property === 'background' && $this->get_cfg('optimise_shorthands') && function_exists('dissolve_short_bg') && !$this->get_cfg('only_safe_optimisations'))
  844. {
  845. unset($this->css[$this->at][$this->selector]['background']);
  846. $this->merge_css_blocks($this->at,$this->selector,dissolve_short_bg($this->value));
  847. }
  848. if(isset($shorthands[$this->property]) && $this->get_cfg('optimise_shorthands') && function_exists('dissolve_4value_shorthands'))
  849. {
  850. $this->merge_css_blocks($this->at,$this->selector,dissolve_4value_shorthands($this->property,$this->value));
  851. if(is_array($shorthands[$this->property]))
  852. {
  853. unset($this->css[$this->at][$this->selector][$this->property]);
  854. }
  855. }
  856. }
  857. if(!$valid)
  858. {
  859. if($this->get_cfg('discard_invalid_properties'))
  860. {
  861. $this->log('Removed invalid property: '.$this->property,'Warning');
  862. }
  863. else
  864. {
  865. $this->log('Invalid property in '.strtoupper($this->get_cfg('css_level')).': '.$this->property,'Warning');
  866. }
  867. }
  868. $this->property = '';
  869. $this->sub_value_arr = array();
  870. $this->value = '';
  871. if($this->get_cfg('save_comments') && $cur_comment != '')
  872. {
  873. $this->comments[$this->at.$this->selector] = rtrim($cur_comment,"\n\r");
  874. $cur_comment = '';
  875. }
  876. }
  877. if($string{$i} == '}')
  878. {
  879. $this->explode_selectors();
  880. $this->status = 'is';
  881. if($this->selector{0} != '@' && !$this->added)
  882. {
  883. $this->log('Removed empty selector: '.trim($this->selector),'Information');
  884. }
  885. $this->invalid_at = false;
  886. $this->selector = '';
  887. }
  888. }
  889. elseif(!$pn)
  890. {
  891. $this->sub_value .= $string{$i};
  892.  
  893. if(ctype_space($string{$i}))
  894. {
  895. $this->optimise_add_subvalue();
  896. }
  897. }
  898. break;
  899. /* Case in string */
  900. case 'instr':
  901. if($this->str_char == ')' && $string{$i} == '"' && !$this->str_in_str && !csstidy::escaped($string,$i))
  902. {
  903. $this->str_in_str = true;
  904. }
  905. elseif($this->str_char == ')' && $string{$i} == '"' && $this->str_in_str && !csstidy::escaped($string,$i))
  906. {
  907. $this->str_in_str = false;
  908. }
  909. if($string{$i} == $this->str_char && !csstidy::escaped($string,$i) && !$this->str_in_str)
  910. {
  911. $this->status = $this->str_from;
  912. }
  913. $temp_add = $string{$i};
  914. // ...and no not-escaped backslash at the previous position
  915. if( ($string{$i} == "\n" || $string{$i} == "\r") && !($string{$i-1} == '\\' && !csstidy::escaped($string,$i-1)) )
  916. {
  917. $temp_add = "\\A ";
  918. $this->log('Fixed incorrect newline in string','Warning');
  919. }
  920. if($this->str_from == 'iv')
  921. {
  922. $this->sub_value .= $temp_add;
  923. }
  924. elseif($this->str_from == 'is')
  925. {
  926. $this->selector .= $temp_add;
  927. }
  928. break;
  929. /* Case in-comment */
  930. case 'ic':
  931. if($string{$i} == '*' && $string{$i+1} == '/')
  932. {
  933. $this->status = $this->comment_from;
  934. if($this->comment_from == 'is') $cur_comment .= $this->template[15];
  935. $i++;
  936. }
  937. elseif($this->comment_from == 'is' && $this->get_cfg('save_comments'))
  938. {
  939. $cur_comment .= $string{$i};
  940. }
  941. break;
  942. }
  943. }
  944. if($this->get_cfg('merge_selectors') == 2)
  945. {
  946. foreach($this->css as $medium => $value)
  947. {
  948. csstidy::merge_selectors($this->css[$medium]);
  949. }
  950. }
  951.  
  952. if($this->get_cfg('optimise_shorthands'))
  953. {
  954. foreach($this->css as $medium => $value)
  955. {
  956. foreach($value as $selector => $value1)
  957. {
  958. if(function_exists('merge_4value_shorthands')) $this->css[$medium][$selector] = merge_4value_shorthands($this->css[$medium][$selector]);
  959. if(function_exists('merge_bg') && !$this->get_cfg('only_safe_optimisations')) $this->css[$medium][$selector] = merge_bg($this->css[$medium][$selector]);
  960. if(empty($this->css[$medium][$selector]))
  961. {
  962. unset($this->css[$medium][$selector]);
  963. }
  964. }
  965. }
  966. }
  967.  
  968. return (empty($this->css) && empty($this->import) && empty($this->charset) && empty($this->namespace)) ? false : true;
  969. }
  970.  
  971. /**
  972. * Explodes selectors
  973. * @access private
  974. * @version 1.0
  975. */
  976. function explode_selectors()
  977. {
  978. // Explode multiple selectors
  979. if($this->get_cfg('merge_selectors') == 1)
  980. {
  981. $new_sels = array();
  982. $lastpos = 0;
  983. $this->sel_seperate[] = strlen($this->selector);
  984. foreach($this->sel_seperate as $num => $pos)
  985. {
  986. if($num == count($this->sel_seperate)-1) {
  987. $pos += 1;
  988. }
  989. $new_sels[] = substr($this->selector,$lastpos,$pos-$lastpos-1);
  990. $lastpos = $pos;
  991. }
  992. if(count($new_sels) > 1)
  993. {
  994. foreach($new_sels as $selector)
  995. {
  996. $this->merge_css_blocks($this->at,$selector,$this->css[$this->at][$this->selector]);
  997. }
  998. unset($this->css[$this->at][$this->selector]);
  999. }
  1000. }
  1001. $this->sel_seperate = array();
  1002. }
  1003.  
  1004. /**
  1005. * Optimises a sub-value and adds it to sub_value_arr
  1006. * @access private
  1007. * @version 1.0
  1008. */
  1009. function optimise_add_subvalue()
  1010. {
  1011. $replace_colors =& $GLOBALS['csstidy']['replace_colors'];
  1012. $this->sub_value = trim($this->sub_value);
  1013. if($this->sub_value != '')
  1014. {
  1015. if(function_exists('compress_numbers'))
  1016. {
  1017. $temp = compress_numbers($this->sub_value,$this->property);
  1018. if($temp != $this->sub_value)
  1019. {
  1020. if(strlen($temp) > strlen($this->sub_value))
  1021. {
  1022. $this->log('Fixed invalid number: Changed "'.$this->sub_value.'" to "'.$temp.'"','Warning');
  1023. }
  1024. else
  1025. $this->log('Optimised number: Changed "'.$this->sub_value.'" to "'.$temp.'"','Information');
  1026. $this->sub_value = $temp;
  1027. }
  1028. }
  1029. if($this->get_cfg('compress_colors') && function_exists('cut_color'))
  1030. {
  1031. $temp = cut_color($this->sub_value);
  1032. if($temp != $this->sub_value)
  1033. {
  1034. if(isset($replace_colors[$this->sub_value]))
  1035. {
  1036. $this->log('Fixed invalid color name: Changed "'.$this->sub_value.'" to "'.$temp.'"','Warning');
  1037. }
  1038. else
  1039. $this->log('Optimised color: Changed "'.$this->sub_value.'" to "'.$temp.'"','Information');
  1040. $this->sub_value = $temp;
  1041. }
  1042. }
  1043. $this->sub_value_arr[] = $this->sub_value;
  1044. $this->sub_value = '';
  1045. }
  1046. }
  1047.  
  1048. /**
  1049. * Checks if a character is escaped (and returns true if it is)
  1050. * @param string $string
  1051. * @param integer $pos
  1052. * @access public
  1053. * @return bool
  1054. * @version 1.01
  1055. */
  1056. function escaped(&$string,$pos)
  1057. {
  1058. return !(@($string{$pos-1} != '\\') || csstidy::escaped($string,$pos-1));
  1059. }
  1060.  
  1061. /**
  1062. * Adds a property with value to the existing CSS code
  1063. * @param string $media
  1064. * @param string $selector
  1065. * @param string $property
  1066. * @param string $new_val
  1067. * @param bool $ie if $ie == false IE Hacks are not saved even if it is enabled in settings
  1068. * @access private
  1069. * @version 1.1
  1070. */
  1071. function css_add_property($media,$selector,$property,$new_val,$ie=true)
  1072. {
  1073. $whitespace =& $GLOBALS['csstidy']['whitespace'];
  1074. if($this->get_cfg('save_ie_hacks') && $ie)
  1075. {
  1076. if(isset($this->css[$media][$selector][$property]) && csstidy::is_important($this->css[$media][$selector][$property])
  1077. && substr(str_replace($whitespace,'',$selector),0,5) !== '*html')
  1078. {
  1079. $this->log('Converted !important-hack in selector "'.$this->selector.'"','Information');
  1080. if(substr(str_replace($whitespace,'',$selector),0,4) == 'html')
  1081. {
  1082. $this->css_add_property($media,'* '.$selector,$property,$new_val.' !important',false);
  1083. }
  1084. else
  1085. {
  1086. $this->css_add_property($media,'* html '.$selector,$property,$new_val.' !important',false);
  1087. }
  1088. }
  1089. else
  1090. {
  1091. $this->css_add_property($media,$selector,$property,$new_val,false);
  1092. }
  1093. }
  1094. else
  1095. {
  1096. $this->added = true;
  1097. if(isset($this->css[$media][$selector][$property]))
  1098. {
  1099. if((csstidy::is_important($this->css[$media][$selector][$property]) && csstidy::is_important($new_val)) || !csstidy::is_important($this->css[$media][$selector][$property]))
  1100. {
  1101. unset($this->css[$media][$selector][$property]);
  1102. $this->css[$media][$selector][$property] = trim($new_val);
  1103. }
  1104. }
  1105. else
  1106. {
  1107. $this->css[$media][$selector][$property] = trim($new_val);
  1108. }
  1109. }
  1110. }
  1111.  
  1112. /**
  1113. * Adds CSS to an existing media/selector
  1114. * @param string $media
  1115. * @param string $selector
  1116. * @param array $css_add
  1117. * @access private
  1118. * @version 1.1
  1119. */
  1120. function merge_css_blocks($media,$selector,$css_add)
  1121. {
  1122. foreach($css_add as $property => $value)
  1123. {
  1124. $this->css_add_property($media,$selector,$property,$value,false);
  1125. }
  1126. }
  1127.  
  1128. /**
  1129. * Merges selectors with same properties. Example: a{color:red} b{color:red} -> a,b{color:red}
  1130. * Very basic and has at least one bug. Hopefully there is a replacement soon.
  1131. * @param array $array
  1132. * @return array
  1133. * @access public
  1134. * @version 1.2
  1135. */
  1136. function merge_selectors(&$array)
  1137. {
  1138. $css = $array;
  1139. foreach($css as $key => $value)
  1140. {
  1141. if(!isset($css[$key]))
  1142. {
  1143. continue;
  1144. }
  1145. $newsel = '';
  1146. // Check if properties also exist in another selector
  1147. $keys = array();
  1148. // PHP bug (?) without $css = $array; here
  1149. foreach($css as $selector => $vali)
  1150. {
  1151. if($selector == $key)
  1152. {
  1153. continue;
  1154. }
  1155. if($css[$key] === $vali)
  1156. {
  1157. $keys[] = $selector;
  1158. }
  1159. }
  1160.  
  1161. if(!empty($keys))
  1162. {
  1163. $newsel = $key;
  1164. unset($css[$key]);
  1165. foreach($keys as $selector)
  1166. {
  1167. unset($css[$selector]);
  1168. $newsel .= ','.$selector;
  1169. }
  1170. $css[$newsel] = $value;
  1171. }
  1172. }
  1173. $array = $css;
  1174. }
  1175.  
  1176.  
  1177. /**
  1178. * Checks if $value is !important.
  1179. * @param string $value
  1180. * @return bool
  1181. * @access public
  1182. * @version 1.0
  1183. */
  1184. function is_important(&$value)
  1185. {
  1186. $whitespace =& $GLOBALS['csstidy']['whitespace'];
  1187. if(!strcasecmp(substr(str_replace($whitespace,'',$value),-10,10),'!important'))
  1188. {
  1189. return true;
  1190. }
  1191. return false;
  1192. }
  1193.  
  1194. /**
  1195. * Returns a value without !important
  1196. * @param string $value
  1197. * @return string
  1198. * @access public
  1199. * @version 1.0
  1200. */
  1201. function gvw_important($value)
  1202. {
  1203. if(csstidy::is_important($value))
  1204. {
  1205. $value = trim($value);
  1206. $value = substr($value,0,-9);
  1207. $value = trim($value);
  1208. $value = substr($value,0,-1);
  1209. $value = trim($value);
  1210. return $value;
  1211. }
  1212. return $value;
  1213. }
  1214.  
  1215. /**
  1216. * Removes unnecessary whitespace in ! important
  1217. * @param string $string
  1218. * @return string
  1219. * @access public
  1220. * @version 1.1
  1221. */
  1222. function c_important(&$string)
  1223. {
  1224. if(csstidy::is_important($string))
  1225. {
  1226. $string = csstidy::gvw_important($string) . ' !important';
  1227. }
  1228. return $string;
  1229. }
  1230.  
  1231. /**
  1232. * Same as htmlspecialchars, only that chars are not replaced if $plain !== true. This makes print_code() cleaner.
  1233. * @param string $string
  1234. * @param bool $plain
  1235. * @return string
  1236. * @see csstidy::print_code()
  1237. * @access public
  1238. * @version 1.0
  1239. */
  1240. function htmlsp($string, $plain)
  1241. {
  1242. if($plain !== true)
  1243. {
  1244. return htmlspecialchars($string);
  1245. }
  1246. return $string;
  1247. }
  1248.  
  1249. /**
  1250. * Returns the formatted CSS Code and saves it into $this->output_css
  1251. * @param array $css
  1252. * @return string
  1253. * @access public
  1254. * @version 1.3
  1255. */
  1256. function print_code($css = NULL,$plain = false)
  1257. {
  1258. $output = '';
  1259. if($css === NULL)
  1260. {
  1261. $css =& $this->css;
  1262. }
  1263. $template = $this->template;
  1264.  
  1265. if($plain)
  1266. {
  1267. $template = array_map("strip_tags", $template);
  1268. }
  1269. if(!empty($this->charset))
  1270. {
  1271. $output .= $template[0].'@charset '.$template[5].$this->charset.$template[6].$template[12];
  1272. }
  1273. if(!empty($this->import))
  1274. {
  1275. for ($i = 0, $size = count($this->import); $i < $size; $i ++) {
  1276. $output .= $template[0].'@import '.$template[5].$this->import[$i].$template[6].$template[12];
  1277. }
  1278. }
  1279. if(!empty($this->namespace))
  1280. {
  1281. $output .= $template[0].'@namespace '.$template[5].$this->namespace.$template[6].$template[12];
  1282. }
  1283. ksort($css);
  1284. foreach($css as $medium => $val)
  1285. {
  1286. if ($medium !== 'standard')
  1287. {
  1288. $output .= $template[0].csstidy::htmlsp($medium,$plain).$template[1];
  1289. }
  1290. if ($this->get_cfg('sort_selectors')) ksort($val);
  1291.  
  1292. foreach($val as $selector => $vali)
  1293. {
  1294. if ($this->get_cfg('sort_properties')) ksort($vali);
  1295. if ($medium !== 'standard') $output .= $template[10];
  1296. if(isset($this->comments[$medium.$selector]))
  1297. {
  1298. $output .= $template[13].'/*'.$this->comments[$medium.$selector].'*/'.$template[14];
  1299. }
  1300. $output .= ($selector{0} !== '@') ? $template[2].csstidy::htmlsp($selector,$plain) : $template[0].csstidy::htmlsp($selector,$plain);
  1301. $output .= ($medium !== 'standard') ? $template[11] : $template[3];
  1302. $i = 0; foreach($vali as $property => $valj)
  1303. {
  1304. $i++;
  1305. if ($medium !== 'standard') $output .= $template[10];
  1306. $output .= $template[4];
  1307. $output .= ($this->get_cfg('uppercase_properties')) ? csstidy::htmlsp(strtoupper($property),$plain) : csstidy::htmlsp($property,$plain);
  1308. $output .= ':'.$template[5].csstidy::htmlsp($valj,$plain);
  1309. $output .= ($this->get_cfg('remove_last_;') && $i === count($vali)) ? str_replace(';','',$template[6]) : $template[6];
  1310. }
  1311. if ($medium !== 'standard') $output .= $template[10];
  1312. $output .= $template[7];
  1313. if (( end($val) !== $vali && $medium !== 'standard') || $medium === 'standard') $output .= $template[8];
  1314. }
  1315. if ($medium != 'standard') $output .= $template[9];
  1316. }
  1317.  
  1318. $output = trim($output);
  1319. if($plain !== true)
  1320. {
  1321. $this->output_css = $output;
  1322. $this->print_code($css, true);
  1323. }
  1324. else
  1325. {
  1326. $this->output_css_plain = $output;
  1327. }
  1328. return $output;
  1329. }
  1330.  
  1331. /**
  1332. * Checks if the next word in a string from pos is a CSS property
  1333. * @param string $istring
  1334. * @param integer $pos
  1335. * @return bool
  1336. * @access private
  1337. * @version 1.2
  1338. */
  1339. function property_is_next($istring, $pos)
  1340. {
  1341. $all_properties =& $GLOBALS['csstidy']['all_properties'];
  1342. $istring = substr($istring,$pos,strlen($istring)-$pos);
  1343. $pos = strpos($istring,':');
  1344. if($pos === false)
  1345. {
  1346. return false;
  1347. }
  1348. $istring = strtolower(trim(substr($istring,0,$pos)));
  1349. if(isset($all_properties[$istring]))
  1350. {
  1351. $this->log('Added semicolon to the end of declaration','Warning');
  1352. return true;
  1353. }
  1354. return false;
  1355. }
  1356.  
  1357. /**
  1358. * Compresses font-weight (not very effective but anyway :-p )
  1359. * @param string $value
  1360. * @access private
  1361. * @return bool
  1362. * @version 1.1
  1363. */
  1364. function c_font_weight(&$value)
  1365. {
  1366. $important = '';
  1367. if(csstidy::is_important($value))
  1368. {
  1369. $important = ' !important';
  1370. $value = csstidy::gvw_important($value);
  1371. }
  1372. if($value == 'bold')
  1373. {
  1374. $value = '700'.$important;
  1375. $this->log('Optimised font-weight: Changed "bold" to "700"','Information');
  1376. }
  1377. else if($value == 'normal')
  1378. {
  1379. $value = '400'.$important;
  1380. $this->log('Optimised font-weight: Changed "normal" to "400"','Information');
  1381. }
  1382. }
  1383.  
  1384. }
  1385. ?>

Documentation generated on Fri, 3 Feb 2006 16:21:43 +0100 by phpDocumentor 1.3.0RC3