PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/parser.php

http://github.com/TurbineCSS/Turbine
PHP | 1329 lines | 837 code | 111 blank | 381 comment | 141 complexity | 4f0b4cbc1c07813ee0773e8c1d9a49e9 MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of Turbine
  4. * http://github.com/SirPepe/Turbine
  5. *
  6. * Copyright Peter Kröner
  7. * Licensed under GNU LGPL 3, see license.txt or http://www.gnu.org/licenses/
  8. */
  9. /**
  10. * Parser2
  11. * Turbine syntax parser
  12. */
  13. class Parser2 extends Base{
  14. /**
  15. * @var bool $debug Print $this->parsed?
  16. */
  17. public $debug = false;
  18. /**
  19. * @var string $file The current file
  20. */
  21. public $file = null;
  22. /**
  23. * @var array $debuginfo Collects parser debugging information
  24. */
  25. public $debuginfo = array();
  26. /**
  27. * @var array $code The loaded turbine code (before parsing)
  28. */
  29. public $code = array();
  30. /**
  31. * @var array $tokenized_properties The list of properties where multiple values are to be combined on output using a space character
  32. */
  33. public $tokenized_properties = array('filter', 'behavior');
  34. /**
  35. * @var array $listed_properties The list of properties where multiple values are to be combined on output using a comma
  36. */
  37. public $listed_properties = array('plugins', '-ms-filter');
  38. /**
  39. * @var array $quoted_properties The list of properties where the value needs to be quoted as a whole before output
  40. */
  41. public $quoted_properties = array('-ms-filter');
  42. /**
  43. * @var array $last_properties The list of properties that must be output AFTER all other plugins, in order of output
  44. */
  45. public $last_properties = array('filter', '-ms-filter');
  46. /**
  47. * @var array $special_properties The list of properties that are invisible and can not be inherited or copied
  48. */
  49. public $special_properties = array('label', '_label');
  50. /**
  51. * @var string $indention_char The Whitespace character(s) used for indention
  52. */
  53. public $indention_char = false;
  54. /**
  55. * @var array $state The parser state
  56. * st = in string
  57. */
  58. public $state = null;
  59. /**
  60. * @var array $prev_state The previous parser state
  61. * st = in string
  62. */
  63. public $prev_state = null;
  64. /**
  65. * @var string $string_state The current string character (", ' or paranthesis)
  66. */
  67. public $string_state = null;
  68. /**
  69. * @var string $token The current token
  70. */
  71. public $token = '';
  72. /**
  73. * @var int $tabwidth The default number or spaces for a tab
  74. */
  75. public $tabwidth = null;
  76. /**
  77. * @var array $current Holds the current values
  78. * se = Selector
  79. * pr = Property
  80. * va = Value
  81. * me = @media block
  82. * fi = @font-face index
  83. * ci = @css line index
  84. */
  85. public $current = array(
  86. 'se' => '',
  87. 'pr' => '',
  88. 'va' => '',
  89. 'me' => 'global',
  90. 'fi' => -1,
  91. 'ci' => 0
  92. );
  93. /**
  94. * @var array $selector_stack The selectors the current line is nested in
  95. */
  96. private $selector_stack = array();
  97. /**
  98. * @var array $parsed The parsed data structure
  99. */
  100. public $parsed = array('global' =>
  101. array(
  102. '@import' => array(),
  103. '@font-face' => array()
  104. )
  105. );
  106. /**
  107. * Factory
  108. * Creates and returns a new CSS Parser object
  109. * @return object $this The CSS Parser object
  110. */
  111. public static function factory(){
  112. return new Parser2();
  113. }
  114. /**
  115. * Constructor
  116. */
  117. public function __construct(){
  118. parent::__construct();
  119. }
  120. /**
  121. * load_string
  122. * Loads a css string line by line and appends it to the unparsed css code if $overwrite is false
  123. * @param string $string the css to load
  124. * @param bool $overwrite overwrite already loaded css or append the new string?
  125. * @return object $this The CSS Parser object
  126. */
  127. public function load_string($string, $overwrite = false){
  128. $lines = explode("\n", $string);
  129. if($overwrite){
  130. $this->code = $lines;
  131. }
  132. else{
  133. $this->code = array_merge($this->code, $lines);
  134. }
  135. $this->file = null;
  136. return $this;
  137. }
  138. /**
  139. * load_file
  140. * Loads a file
  141. * @param string $file The css files to load
  142. * @param bool $overwrite overwrite already loaded css or append the new string?
  143. * @return object $this The CSS Parser object
  144. */
  145. public function load_file($file, $overwrite = false){
  146. $this->load_string(file_get_contents($file), $overwrite);
  147. $this->file = $file;
  148. return $this;
  149. }
  150. /**
  151. * load_files
  152. * Loads a number of files
  153. * @param string $files File(s) to load, sepperated by ;
  154. * @return object $this The CSS Parser object
  155. */
  156. public function load_files($files){
  157. $files = explode(';', $files);
  158. foreach($files as $file){
  159. $this->load_file($file, false);
  160. }
  161. return $this;
  162. }
  163. /**
  164. * parse
  165. * Reads the loaded css line by line into $this->parsed
  166. * @return object $this The CSS Parser object
  167. */
  168. public function parse(){
  169. // Preprocess the code and get the indention char(s)
  170. $this->preprocess();
  171. $this->set_indention_char();
  172. // Loop through the lines
  173. $loc = count($this->code);
  174. for($i = 0; $i < $loc; $i++){
  175. $debug = array();
  176. $this->token = '';
  177. $line = $this->code[$i];
  178. $level = $this->get_indention_level($line); // Get this line's indention level
  179. $debug['line'] = $line;
  180. if(isset($this->code[$i + 1])){
  181. $nextline = $this->code[$i + 1];
  182. $nextlevel = $this->get_indention_level($nextline); // Get the next line's indention level
  183. }
  184. // If the current line is empty, ignore it and reset the selector stack
  185. if($line == ''){
  186. $this->selector_stack = array();
  187. $debug['type'] = 'Reset';
  188. $debug['stack'] = 'Reset';
  189. }
  190. // Else parse the line
  191. else{
  192. // Line begins with "@media" = parse this as a @media-line
  193. if(substr(trim($line), 0, 6) == '@media'){
  194. $this->parse_media_line($line);
  195. $this->selector_stack = array();
  196. $debug['type'] = 'Media';
  197. $debug['stack'] = 'Reset';
  198. }
  199. // Line begins with "@import" = Parse @import rule
  200. elseif(substr(trim($line), 0, 7) == '@import'){
  201. $this->parse_import_line($line);
  202. $this->selector_stack = array();
  203. $debug['type'] = 'Import';
  204. $debug['stack'] = 'Reset';
  205. }
  206. // Line begins with "@css" = Parse as literal css
  207. elseif(substr(trim($line), 0, 4) == '@css'){
  208. $this->parse_css_line($line);
  209. $this->selector_stack = array();
  210. $debug['type'] = 'CSS';
  211. $debug['stack'] = 'Reset';
  212. }
  213. // Else parse normal line
  214. else{
  215. // Next line is indented = parse this as a selector
  216. if($nextline != '' && $nextlevel > $level){
  217. $this->parse_selector_line($line, $level);
  218. $debug['type'] = 'Selector';
  219. $debug['stack'] = $this->selector_stack;
  220. }
  221. // Else parse as a property-value-pair
  222. else{
  223. $this->parse_property_line($line);
  224. $this->reset_current_property();
  225. $debug['type'] = 'Property/Value';
  226. $debug['stack'] = $this->selector_stack;
  227. }
  228. }
  229. }
  230. // If the next line is outdented, slice the selector stack accordingly
  231. if($nextline != '' && $nextlevel < $level){
  232. $this->selector_stack = array_slice($this->selector_stack, 0, $nextlevel);
  233. $this->current['se'] = end($this->selector_stack);
  234. }
  235. // Debugging stuff
  236. $debug['media'] = $this->current['me'];
  237. $this->debuginfo[] = $debug;
  238. unset($debug);
  239. }
  240. // EOF for while_parsing
  241. call_user_func_array(
  242. array($this, 'while_parsing_plugins'),
  243. array('EOF', 'EOF')
  244. );
  245. // Dump $this->parsed when configured to do so
  246. if($this->debug){
  247. print_r($this->parsed);
  248. }
  249. return $this;
  250. }
  251. /**
  252. * while_parsing_plugins
  253. * Run the plugins for the while_parsing hook
  254. * @param string $type The type of the line that's being processed
  255. * @param string $line The line (or a part of a line) that's being processed
  256. * @return void
  257. */
  258. private function while_parsing_plugins($type, &$line){
  259. global $plugin_list;
  260. $this->apply_plugins('while_parsing', $plugin_list, $type, $line);
  261. }
  262. /**
  263. * set_indention_char
  264. * Sets the indention char
  265. * @param string $char The whitespace char(s) used for indention
  266. * @return void
  267. */
  268. public function set_indention_char($char = null){
  269. if(!$char){
  270. $char = Parser2::get_indention_char($this->code);
  271. }
  272. $this->indention_char = $char;
  273. }
  274. /**
  275. * get_indention_char
  276. * Find out which whitespace char(s) are used for indention
  277. * @param array $lines The code in question
  278. * @return string $matches[1] The whitespace char(s) used for indention
  279. */
  280. public function get_indention_char($lines){
  281. $linecount = count($this->code);
  282. $tab_lines = array();
  283. $space_lines = array();
  284. $corrupt_lines = array();
  285. for($i = 0; $i < $linecount; $i++){
  286. $line = $this->code[$i];
  287. $line_number = $i;
  288. // If the line is not empty
  289. if(strlen(trim($line)) > 0){
  290. // Search indentations
  291. if(preg_match('/^(\s+)[a-z\-]+:.+?/i', $line, $matches)){
  292. $indentation = $matches[1];
  293. // Check for tabs
  294. if(strpos($indentation, "\t") !== false){
  295. // Check for spaces.
  296. if(strpos($indentation, ' ') !== false){
  297. $corrupt_lines[$line_number] = $line;
  298. }
  299. else{
  300. array_push($tab_lines, $line_number);
  301. }
  302. continue;
  303. }
  304. // Check for spaces
  305. else{
  306. $length = strlen($indentation);
  307. // Set the tab width if it is still unknown
  308. if($this->tabwidth === null){
  309. $this->tabwidth = $length;
  310. }
  311. // Valid indentation ?
  312. if(($length % $this->tabwidth) == 0){
  313. array_push($space_lines, $line_number);
  314. }
  315. else{
  316. $corrupt_lines[$line_number] = $line;
  317. }
  318. }
  319. }
  320. }
  321. }
  322. // Fix lines. Replace tabs by spaces
  323. if(count($space_lines) > count($tab_lines)){
  324. $final_indentation = str_repeat(' ', $this->tabwidth);
  325. foreach($tab_lines as $line_number){
  326. $this->code[$line_number] = str_replace("\t", $final_indentation, $this->code[$line_number]);
  327. }
  328. }
  329. // Fix lines. Replace spaces by tabs
  330. else{
  331. $final_indentation = "\t";
  332. foreach($space_lines as $line_number){
  333. $this->code[$line_number] = str_replace(str_repeat(' ', $this->tabwidth), $final_indentation, $this->code[$line_number]);
  334. }
  335. }
  336. // Report corrupt lines
  337. if(count($corrupt_lines) > 0 && $this->config['debug_level'] > 0){
  338. $fixed_line = '';
  339. foreach(array_keys($corrupt_lines) as $line_number){
  340. $fixed_line .= $line_number + 1; // add 1, because arrays start at 0
  341. $file = ($this->file) ? 'in file ' . $this->file . ',' : 'in';
  342. $this->report_error('Possible broken indentation detected ' . $file . ' line ' . $fixed_line . ': ' . trim($corrupt_lines[$line_number]));
  343. }
  344. }
  345. return $final_indentation;
  346. }
  347. /**
  348. * preprocess
  349. * Clean up the code
  350. * @return void
  351. */
  352. protected function preprocess(){
  353. $this->preprocess_clean();
  354. $this->preprocess_concatenate_selectors();
  355. $this->parse_tabwidth();
  356. }
  357. /**
  358. * preprocess_clean
  359. * Strip comment lines and whitespace
  360. * @return void
  361. */
  362. private function preprocess_clean(){
  363. $processed = array(); // The remaining, cleaned up lines
  364. $comment_state = false; // The block comment state
  365. $previous_line = ''; // The line before the line being processed
  366. $loc = count($this->code);
  367. for($i = 0; $i < $loc; $i++){
  368. // Handle block comment lines
  369. if(trim($this->code[$i]) == '--'){
  370. $comment_state = ($comment_state) ? false : true;
  371. }
  372. // Handle normal lines
  373. elseif(!$comment_state){
  374. // Ignore lines containing nothing but a comment
  375. if(!preg_match('/^[\s]*\/\/.*?$/', $this->code[$i])){
  376. // Lines containing non-whitespace
  377. if(preg_match('/[\S]/', $this->code[$i])){
  378. $processed[] = $this->code[$i];
  379. $previous_line = $this->code[$i];
  380. }
  381. // Lines with nothing but whitespace
  382. else{
  383. // Only add this line if the previous one had any non-whitespace
  384. if($previous_line != ''){
  385. $processed[] = '';
  386. $previous_line = '';
  387. }
  388. }
  389. }
  390. }
  391. }
  392. $this->code = $processed;
  393. }
  394. /**
  395. * preprocess_concatenate_selectors
  396. * Concatenates multiline selectors
  397. * @return void
  398. */
  399. private function preprocess_concatenate_selectors(){
  400. $processed = array();
  401. $loc = count($this->code);
  402. for($i = 0; $i < $loc; $i++){
  403. $line = $this->code[$i];
  404. if($line != ''){
  405. while(substr($line, -1) == ','){
  406. $line .= ' '.$this->code[++$i];
  407. }
  408. }
  409. $processed[] = $line;
  410. }
  411. $this->code = $processed;
  412. }
  413. /**
  414. * parse_tabwidth
  415. * Looks for user defined tab with
  416. * @return void
  417. */
  418. protected function parse_tabwidth(){
  419. if(preg_match('/\s+tabwidth:\s+(\d){1,2}/i', implode($this->code), $match)){
  420. $this->tabwidth = $match[1];
  421. }
  422. }
  423. /**
  424. * get_indention_level
  425. * Returns the indention level for a line
  426. * @param string $line The line to get the indention level for
  427. * @return int $level The indention level
  428. */
  429. public function get_indention_level($line){
  430. $level = 0;
  431. if(substr($line, 0, strlen($this->indention_char)) == $this->indention_char){
  432. $level = 1 + $this->get_indention_level(substr($line, strlen($this->indention_char)));
  433. }
  434. return $level;
  435. }
  436. /**
  437. * switch_string_state
  438. * Manages the string state
  439. * @param string $char A single char
  440. * @return void
  441. */
  442. protected function switch_string_state($char){
  443. $strings = array('"', "'", '(');
  444. if($this->state != 'st'){
  445. if(in_array($char, $strings)){ // Enter string state
  446. $this->prev_state = $this->state;
  447. $this->string_state = $char;
  448. $this->state = 'st';
  449. }
  450. }
  451. else{
  452. if($char == $this->string_state || ($char == ')' && $this->string_state == '(')){ // Leave string state
  453. $this->string_state = null;
  454. $this->state = $this->prev_state;
  455. }
  456. }
  457. }
  458. /**
  459. * parse_media_line
  460. * Parses an @media line
  461. * @param string $line A line containing an @media switch
  462. * @return void
  463. */
  464. protected function parse_media_line($line){
  465. $this->current['me'] = '';
  466. $this->current['se'] = '';
  467. $this->current['pr'] = '';
  468. $this->current['va'] = '';
  469. $line = trim($line);
  470. $len = strlen($line);
  471. for($i = 0; $i < $len; $i++ ){
  472. $this->switch_string_state($line{$i});
  473. if($this->state != 'st' && $line{$i} == '/' && $line{$i+1} == '/'){ // Break on comment
  474. break;
  475. }
  476. $this->token .= $line{$i};
  477. }
  478. $media = trim(preg_replace('/[\s]+/', ' ', $this->token)); // Trim whitespace from token
  479. $this->current['me'] = (trim(substr($media, 6)) != 'none') ? $media : 'global'; // Use token as current @media or reset to global
  480. // Fire the "while_parsing" plugins
  481. call_user_func_array(
  482. array($this, 'while_parsing_plugins'),
  483. array('@media', &$this->current['me'])
  484. );
  485. }
  486. /**
  487. * parse_selector_line
  488. * Parses a selector line
  489. * @param string $line A line containing a selector
  490. * @param int $level The lines' indention level
  491. * @return void
  492. */
  493. protected function parse_selector_line($line, $level){
  494. $line = trim($line);
  495. $len = strlen($line);
  496. for($i = 0; $i < $len; $i++ ){
  497. $this->switch_string_state($line{$i});
  498. if($this->state != 'st' && $line{$i} == '/' && $line{$i+1} == '/'){ // Break on comment
  499. break;
  500. }
  501. $this->token .= $line{$i};
  502. }
  503. // Trim whitespace
  504. $selector = trim(preg_replace('/\s+/', ' ', $this->token));
  505. // Combine selector with the nesting stack
  506. $selector = $this->merge_selectors($this->array_get_previous($this->selector_stack, $level), $selector);
  507. // Increase font-face index if this is an @font-face element
  508. if($selector == '@font-face'){
  509. $this->current['fi']++;
  510. }
  511. // Use as current selector
  512. $this->current['se'] = $selector;
  513. // Add to the selector stack
  514. $this->selector_stack[$level] = $selector;
  515. // Fire the "while_parsing" plugins
  516. call_user_func_array(
  517. array($this, 'while_parsing_plugins'),
  518. array('selector', &$this->current['se'])
  519. );
  520. }
  521. /**
  522. * merge_selectors
  523. * Merges two selectors
  524. * @param array $parent
  525. * @param array $child
  526. * @return array $selectors
  527. */
  528. protected function merge_selectors($parent, $child){
  529. // If the parent is empty, don't do anything to the child
  530. if(empty($parent)){
  531. return $child;
  532. }
  533. // Else combine the selectors
  534. else{
  535. $parent = $this->tokenize($parent, ',');
  536. $child = $this->tokenize($child, ',');
  537. // Merge the tokenized selectors
  538. $selectors = array();
  539. foreach($parent as $p){
  540. $selector = array();
  541. foreach($child as $c){
  542. $selector[] = $p.' '.$c;
  543. }
  544. $selectors[] = $selector;
  545. }
  546. return $this->implode_selectors($selectors);
  547. }
  548. }
  549. /**
  550. * implode_selectors
  551. * Recursivly combines selectors
  552. * @param array $selectors A list of selectors
  553. * @return string The combined selector
  554. */
  555. protected function implode_selectors($selectors){
  556. $num = count($selectors);
  557. for($i = 0; $i < $num; $i++){
  558. if(is_array($selectors[$i])){
  559. $selectors[$i] = $this->implode_selectors($selectors[$i]);
  560. }
  561. }
  562. return implode(', ', $selectors);
  563. }
  564. /**
  565. * parse_import_line
  566. * Parses an @import line
  567. * @param string $line A line containing @import
  568. * @return void
  569. */
  570. protected function parse_import_line($line){
  571. $line = trim($line);
  572. $line = substr($line, 7); // Strip "@import"
  573. $len = strlen($line);
  574. for($i = 0; $i < $len; $i++){
  575. $this->switch_string_state($line{$i});
  576. if($this->state != 'st' && $line{$i} == '/' && $line{$i+1} == '/'){ // Break on comment
  577. break;
  578. }
  579. $this->token .= $line{$i};
  580. }
  581. $this->token = trim($this->token);
  582. // Fire the "while_parsing" plugins
  583. call_user_func_array(
  584. array($this, 'while_parsing_plugins'),
  585. array('@import', &$this->token)
  586. );
  587. // Merge into tree
  588. $this->parsed['global']['@import'][][0] = $this->token;
  589. }
  590. /**
  591. * Parse a line of literal css
  592. * @param string $line
  593. * @return void
  594. */
  595. protected function parse_css_line($line){
  596. $line = trim($line);
  597. $line = substr($line, 4); // Strip "@css"
  598. $selector = '@css-'.$this->current['ci']++; // Build the selector using the @css-Index
  599. $line = trim($line);
  600. // Fire the while_parsing plugins
  601. call_user_func_array(
  602. array($this, 'while_parsing_plugins'),
  603. array('@css', &$line)
  604. );
  605. // Merge into tree
  606. $this->parsed[$this->current['me']][$selector] = array(
  607. '_value' => array($line)
  608. );
  609. }
  610. /**
  611. * parse_property_line
  612. * Parses a property/value line
  613. * @param string $line A line containing one (or more) property-value-pairs
  614. * @return void
  615. */
  616. protected function parse_property_line($line){
  617. $line = trim($line);
  618. $len = strlen($line);
  619. $this->state = 'pr';
  620. for($i = 0; $i < $len; $i++ ){
  621. $this->switch_string_state($line{$i});
  622. if($this->state != 'st' && $line{$i} == '/' && $line{$i+1} == '/'){ // Break on comment
  623. if(trim($this->token) != ''){
  624. $this->current['va'] = trim($this->token);
  625. $this->token = '';
  626. // Fire the "while_parsing" plugins
  627. call_user_func_array(
  628. array($this, 'while_parsing_plugins'),
  629. array('property', &$this->current['pr'])
  630. );
  631. call_user_func_array(
  632. array($this, 'while_parsing_plugins'),
  633. array('value', &$this->current['va'])
  634. );
  635. // Merge into the tree
  636. $this->merge();
  637. }
  638. break;
  639. }
  640. elseif($this->state == 'pr' && $line{$i} == ':'){ // End property state on colon
  641. $this->current['pr'] = trim($this->token);
  642. $this->state = 'va';
  643. $this->token = '';
  644. }
  645. elseif($i + 1 == $len || ($this->state != 'st' && $line{$i} == ';')){ // End pair on line end or semicolon
  646. if($i + 1 == $len && $line{$i} != ';'){
  647. $this->token .= $line{$i};
  648. }
  649. $this->current['va'] = trim($this->token);
  650. $this->state = 'pr';
  651. $this->token = '';
  652. // Fire the "while_parsing" plugins
  653. call_user_func_array(
  654. array($this, 'while_parsing_plugins'),
  655. array('property', &$this->current['pr'])
  656. );
  657. call_user_func_array(
  658. array($this, 'while_parsing_plugins'),
  659. array('value', &$this->current['va'])
  660. );
  661. // Merge into the tree
  662. $this->merge();
  663. }
  664. else{
  665. $this->token .= $line{$i};
  666. }
  667. }
  668. }
  669. /**
  670. * merge
  671. * Merges the current values into $this->parsed
  672. * @return void
  673. */
  674. protected function merge(){
  675. global $plugin_list;
  676. // The current values
  677. $me = $this->current['me'];
  678. $se = $this->current['se'];
  679. $pr = $this->current['pr'];
  680. $va = $this->current['va'];
  681. $fi = $this->current['fi'];
  682. // Merge only if a property and a value do exist
  683. if($pr !== '' && $va !== ''){
  684. // Make special properties invisible
  685. if(in_array($pr, $this->special_properties)){
  686. $pr = '_' . $pr;
  687. }
  688. // Special destination for @font-face
  689. if($se == '@font-face'){
  690. $dest =& $this->parsed[$me][$se][$fi][$pr];
  691. }
  692. else{
  693. // Merge plugin list for for @turbine
  694. if($se == '@turbine' && $pr == 'plugins' && isset($this->parsed[$me][$se][$pr])){
  695. $new_plugins = $this->tokenize(str_replace(';', '', $va), ',');
  696. $old_plugins = $this->tokenize(str_replace(';', '', $this->get_final_value($this->parsed[$me][$se][$pr])), ',');
  697. $merged_plugins = array_unique(array_merge($new_plugins, $old_plugins));
  698. $va = implode(', ', $merged_plugins);
  699. }
  700. $dest =& $this->parsed[$me][$se][$pr];
  701. }
  702. // Set the value array if not aleady present
  703. if(!isset($dest)){
  704. $dest = array();
  705. }
  706. // Add the value to the destination
  707. $dest[] = $va;
  708. }
  709. }
  710. /**
  711. * reset_current_property
  712. * Empties the current value and property token
  713. * @return void
  714. */
  715. private function reset_current_property(){
  716. $this->current['pr'] = '';
  717. $this->current['va'] = '';
  718. }
  719. /**
  720. * check_sanity
  721. * Check sanity before output. Complain if somthing stupid comes along, but don't stop it - that's the web developer's job
  722. * @return void
  723. */
  724. private function check_sanity(){
  725. // Loop through the blocks
  726. foreach($this->parsed as $block => $content){
  727. // Loop through elements
  728. foreach($content as $selector => $rules){
  729. $this->check_sanity_filters($selector, $rules);
  730. }
  731. }
  732. }
  733. /**
  734. * check_sanity_filters
  735. * Check if something could potentially prevent IE's filters from working
  736. * @param string $selector The selector that is being checked
  737. * @param array $rules The rules to check
  738. * @return void
  739. */
  740. private function check_sanity_filters($selector, $rules){
  741. if(isset($rules['overflow']) && (isset($rules['filter']) || isset($rules['-ms-filter']))){
  742. if($this->get_final_value($rules['overflow'], 'overflow') == 'visible'){
  743. $this->report_error('Potential problem: Filters and overflow:visible are present at selector '
  744. . $selector . '. Filter may enforce unwanted overflow:hidden in Internet Explorer.');
  745. }
  746. }
  747. }
  748. /**
  749. * glue
  750. * Turn the current parsed array back into CSS code
  751. * @param bool $compressed Compress CSS? (removes whitespace)
  752. * @return string $output The final CSS code
  753. */
  754. public function glue($compressed = false){
  755. $this->check_sanity();
  756. $output = '';
  757. // Whitspace characters
  758. $s = ' ';
  759. $t = "\t";
  760. $n = "\r\n";
  761. // Forget the whitespace if we're compressing
  762. if($compressed){
  763. $s = $t = $n = '';
  764. }
  765. // Loop through the blocks
  766. foreach($this->parsed as $block => $content){
  767. $indented = false;
  768. // Is current block an @media-block? If so, open the block
  769. $media_block = (substr($block, 0, 6) === '@media');
  770. if($media_block){
  771. $output .= $block . $s;
  772. $output .= '{' . $n;
  773. $indented = true;
  774. }
  775. // Read contents
  776. foreach($content as $selector => $rules){
  777. // @import rules
  778. if($selector == '@import'){
  779. $output .= $this->glue_import($rules, $compressed);
  780. }
  781. // @font-face rules
  782. elseif($selector == '@font-face'){
  783. $output .= $this->glue_font_face($rules, $compressed);
  784. }
  785. // @css line
  786. elseif(preg_match('/@css-[0-9]+/', $selector)){
  787. $output .= $this->glue_css($rules, $indented, $compressed);
  788. }
  789. // Normal css rules
  790. else{
  791. $output .= $this->glue_rule($selector, $rules, $indented, $compressed);
  792. }
  793. }
  794. // If @media-block, close block
  795. if($media_block){
  796. $output .= '}' . $n;
  797. }
  798. }
  799. return $output;
  800. }
  801. /**
  802. * glue_import
  803. * Turn parsed @import lines into output
  804. * @param array $imports List of @import statements
  805. * @param bool $compressed Compress CSS? (removes whitespace)
  806. * @return string $output Formatted CSS
  807. */
  808. private function glue_import($imports, $compressed){
  809. $output = '';
  810. $n = ($compressed) ? '' : "\r\n";
  811. foreach($imports as $import){
  812. $semicolon = (substr($import[0], -1) == ';') ? '' : ';';
  813. $output .= '@import ' . $import[0] . $semicolon . $n;
  814. }
  815. return $output;
  816. }
  817. /**
  818. * glue_font_face
  819. * Turn parsed @font-face elements into output
  820. * @param array $imports List of @import statements
  821. * @param bool $compressed Compress CSS? (removes whitespace)
  822. * @return string $output Formatted CSS
  823. */
  824. private function glue_font_face($fonts, $compressed){
  825. $output = '';
  826. // Whitspace characters
  827. $s = ' ';
  828. $n = "\r\n";
  829. // Forget the whitespace if we're compressing
  830. if($compressed){
  831. $s = $n = '';
  832. }
  833. // Build the @font-face rules
  834. foreach($fonts as $font => $styles){
  835. if(!empty($styles)){
  836. $output .= '@font-face'.$s.'{'.$n;
  837. $output .= $this->glue_properties($styles, '', $compressed);
  838. $output .= '}'.$n;
  839. }
  840. }
  841. return $output;
  842. }
  843. /**
  844. * glue_css
  845. * Turn a parsed @css line into output
  846. * @param mixed $contents @css line contents
  847. * @param string $indented Indent the rule? (forn use inside @media blocks)
  848. * @param bool $compressed Compress CSS? (removes whitespace)
  849. * @return string $output Formatted CSS
  850. */
  851. private function glue_css($contents, $indented, $compressed){
  852. $value = array_pop($contents['_value']);
  853. // Set the indention prefix
  854. $prefix = ($indented && !$compressed) ? "\t" : '';
  855. // Construct and return the result
  856. $output = $prefix.$value;
  857. return $output;
  858. }
  859. /**
  860. * glue_rule
  861. * Turn rules into css output
  862. * @param string $selector Selector to use for this css rule
  863. * @param mixed $rules Rule contents
  864. * @param string $indented Indent the rule? (forn use inside @media blocks)
  865. * @param bool $compressed Compress CSS? (removes whitespace)
  866. * @return string $output Formatted CSS
  867. */
  868. private function glue_rule($selector, $rules, $indented, $compressed){
  869. $output = '';
  870. // Whitspace characters
  871. $s = ' ';
  872. $t = "\t";
  873. $n = "\r\n";
  874. // Forget the whitespace if we're compressing
  875. if($compressed){
  876. $s = $t = $n = '';
  877. }
  878. // Set the indention prefix
  879. $prefix = ($indented && !$compressed) ? $t : '';
  880. // Strip whitespace from selectors when compressing
  881. if($compressed){
  882. $selector = implode(',', $this->tokenize($selector, ','));
  883. }
  884. // Constuct the selector
  885. $output .= $prefix . $selector . $s;
  886. $output .= '{';
  887. // Add comments
  888. if(isset($rules['_comments']['selector']) && !$compressed){
  889. $rules['_comments']['selector'] = array_unique($rules['_comments']['selector']);
  890. $output .= ' /* ' . implode(', ', $rules['_comments']['selector']) . ' */';
  891. }
  892. $output .= $n;
  893. // Add the properties
  894. $output .= $this->glue_properties($rules, $prefix, $compressed);
  895. $output .= $prefix.'}'.$n;
  896. return $output;
  897. }
  898. /**
  899. * glue_properties
  900. * Combine property sets
  901. * @param mixed $rules Property-value-pairs
  902. * @param string $prefix Prefix
  903. * @param bool $compressed Compress CSS? (removes whitespace)
  904. * @return string $output Formatted CSS
  905. */
  906. private function glue_properties($rules, $prefix, $compressed){
  907. $output = '';
  908. // Whitspace characters
  909. $s = ' ';
  910. $t = "\t";
  911. $n = "\r\n";
  912. // Forget the whitespace if we're compressing
  913. if($compressed){
  914. $s = $t = $n = '';
  915. }
  916. // Reorder for output
  917. foreach($this->last_properties as $property){
  918. if(isset($rules[$property])){
  919. $content = $rules[$property]; // Make a copy
  920. unset($rules[$property]); // Remove the original
  921. $rules[$property] = $content; // Re-insert the property at the end
  922. }
  923. }
  924. // Keep count of the properties
  925. $num_properties = $this->count_properties($rules);
  926. $count_properties = 0;
  927. // Build output
  928. foreach($rules as $property => $values){
  929. // Ignore empty properties (might happen because of errors in plugins) and non-content-properties
  930. if(!empty($property) && $property{0} != '_'){
  931. $count_properties++;
  932. // Clean up values
  933. $values = $this->get_final_value_array($values, $property, $compressed);
  934. // Output property lines
  935. $num_values = count($values);
  936. $count_values = 0;
  937. foreach($values as $value){
  938. $count_values++;
  939. $output .= $prefix . $t . $property . ':' . $s . $value;
  940. // When compressing, omit the last semicolon
  941. if(!$compressed || $num_properties != $count_properties || $num_values != $count_values){
  942. $output .= ';';
  943. }
  944. // Add comments
  945. if(isset($rules['_comments'][$property]) && !$compressed){
  946. $rules['_comments'][$property] = array_unique($rules['_comments'][$property]);
  947. $output .= ' /* ' . implode(', ', $rules['_comments'][$property]) . ' */';
  948. }
  949. $output .= $n;
  950. }
  951. }
  952. }
  953. return $output;
  954. }
  955. /**
  956. * get_final_value
  957. * Returns the last and/or most !important value from a list of values
  958. * @param array $values A list of values
  959. * @param string $property The property the values belong to
  960. * @param bool $compressed Compress CSS? (removes whitespace)
  961. * @return string $final The final value
  962. */
  963. public function get_final_value($values, $property = NULL, $compressed = false){
  964. // Remove duplicates
  965. $values = array_unique($values);
  966. // If there's only one value, there's only one thing to return
  967. if(count($values) == 1){
  968. $final = array_pop($values);
  969. // Remove quotes in values on quoted properties (important for -ms-filter property)
  970. if(in_array($property, $this->quoted_properties)){
  971. $final = str_replace('"', "'", trim($final, '"'));
  972. }
  973. }
  974. // Otherwise find the last and/or most !important value
  975. else{
  976. // Check if we are dealing with IE-filters
  977. if(in_array($property,array('filter','-ms-filter'))){
  978. $filters = array();
  979. $filters['chroma'] = array();
  980. $filters['matrix'] = array();
  981. $filters['standard'] = array();
  982. $transformfilters = array();
  983. $num_values = count($values);
  984. reset($values);
  985. for($i = 0; $i < $num_values; $i++){
  986. if(stristr(current($values),'chroma')){
  987. $filters['chroma'][] = current($values);
  988. }
  989. elseif(stristr(current($values),'matrix')){
  990. $filters['matrix'][] = current($values);
  991. }
  992. else{
  993. $filters['standard'][] = current($values);
  994. }
  995. next($values);
  996. }
  997. reset($values);
  998. $values = array_merge($filters['chroma'],$filters['matrix'],$filters['standard']);
  999. }
  1000. // Check if we are dealing with IE-behaviors
  1001. if(in_array($property,array('behavior'))){
  1002. $behavior = array();
  1003. $behavior['borderradius'] = array();
  1004. $behavior['transform'] = array();
  1005. $behavior['standard'] = array();
  1006. $transformfilters = array();
  1007. $num_values = count($values);
  1008. reset($values);
  1009. for($i = 0; $i < $num_values; $i++){
  1010. if(stristr(current($values),'borderradius.htc')){
  1011. $behavior['borderradius'][] = current($values);
  1012. }
  1013. elseif(stristr(current($values),'transform.htc')){
  1014. $behavior['transform'][] = current($values);
  1015. }
  1016. else{
  1017. $behavior['standard'][] = current($values);
  1018. }
  1019. next($values);
  1020. }
  1021. reset($values);
  1022. $values = array_merge($behavior['borderradius'],$behavior['transform'],$behavior['standard']);
  1023. }
  1024. // Whitspace characters
  1025. $s = ' ';
  1026. // Forget the whitespace if we're compressing
  1027. if($compressed){
  1028. $s = '';
  1029. }
  1030. // The final value
  1031. $final = '';
  1032. $num_values = count($values);
  1033. for($i = 0; $i < $num_values; $i++){
  1034. // Tokenized properties
  1035. if(in_array($property, $this->tokenized_properties)){
  1036. if($final != ''){
  1037. $final .= ' ';
  1038. }
  1039. // Remove quotes in values on quoted properties (important for -ms-filter property)
  1040. if(in_array($property, $this->quoted_properties)){
  1041. $values[$i] = str_replace('"', "'", trim($values[$i],'"'));
  1042. }
  1043. $final .= $values[$i];
  1044. }
  1045. // Listed properties
  1046. elseif(in_array($property, $this->listed_properties)){
  1047. if($final != ''){
  1048. $final .= ','.$s;
  1049. }
  1050. // Remove quotes in values on quoted properties (important for -ms-filter property)
  1051. if(in_array($property, $this->quoted_properties)){
  1052. $values[$i] = str_replace('"', "'", trim($values[$i],'"'));
  1053. }
  1054. $final .= $values[$i];
  1055. }
  1056. // Normal properties
  1057. else{
  1058. if(strpos($values[$i], '!important') || !strpos($final, '!important')){
  1059. $final = $values[$i];
  1060. }
  1061. }
  1062. }
  1063. }
  1064. // Add quotes to quoted properties
  1065. if(in_array($property, $this->quoted_properties)){
  1066. $final = '"' . $final . '"';
  1067. }
  1068. $final = trim($final);
  1069. return $final;
  1070. }
  1071. /**
  1072. * get_final_value_array
  1073. * Returns a cleaned up version of an array of values
  1074. * @param array $values A list of values
  1075. * @param string $property The property the values belong to
  1076. * @param bool $compressed Compress CSS? (removes whitespace)
  1077. * @return array $final The final values
  1078. */
  1079. public function get_final_value_array($values, $property = NULL, $compressed = false){
  1080. // In the case of listed/tokenized properties, get a single combined final value
  1081. if(in_array($property, $this->tokenized_properties) || in_array($property, $this->listed_properties)){
  1082. $final = array($this->get_final_value($values, $property, $compressed));
  1083. }
  1084. // Otherwise just clean up the value array and return it
  1085. else{
  1086. $final = array_unique($values);
  1087. }
  1088. return $final;
  1089. }
  1090. /**
  1091. * count_properties
  1092. * Counts properties excluding hidden properties (prefixed with _)
  1093. * @param array $properties The rules containing the properties to count
  1094. * @return int $count
  1095. */
  1096. private function count_properties($properties){
  1097. $count = 0;
  1098. foreach($properties as $property => $value){
  1099. if($property{0} != '_'){
  1100. $count++;
  1101. }
  1102. }
  1103. return $count;
  1104. }
  1105. /**
  1106. * tokenize
  1107. * Tokenizes $str, respecting css string delimeters
  1108. * @param string $str
  1109. * @param mixed $separator
  1110. * @return array $tokens
  1111. */
  1112. public function tokenize($str, $separator = array(' ', ' ')){
  1113. $tokens = array();
  1114. $current = '';
  1115. $string_delimeters = array('"', "'", '(');
  1116. $current_string_delimeter = null;
  1117. if(!is_array($separator)){
  1118. $separator = array($separator);
  1119. }
  1120. $strlen = strlen($str);
  1121. for($i = 0; $i < $strlen; $i++){
  1122. if($current_string_delimeter === null){
  1123. // End current token
  1124. if(in_array($str{$i}, $separator)){
  1125. $token = trim($current);
  1126. if(strlen($token) > 0 && !in_array($token, $separator)){
  1127. $tokens[] = $token;
  1128. }
  1129. $current = '';
  1130. $i++;
  1131. }
  1132. // Begin string state
  1133. elseif(in_array($str{$i}, $string_delimeters)){
  1134. $current_string_delimeter = $str{$i};
  1135. }
  1136. }
  1137. else{
  1138. // End string state
  1139. if($str{$i} === $current_string_delimeter || ($current_string_delimeter == '(' && $str{$i} === ')')){
  1140. $current_string_delimeter = null;
  1141. }
  1142. }
  1143. // Add to the current token
  1144. if(isset($str{$i})){
  1145. $current .= $str{$i};
  1146. }
  1147. // Handle the last token
  1148. if($i == $strlen - 1){
  1149. $lasttoken = trim($current);
  1150. if($lasttoken){
  1151. $tokens[] = $lasttoken;
  1152. }
  1153. }
  1154. }
  1155. return $tokens;
  1156. }
  1157. /**
  1158. * comment
  1159. * Adds a comment
  1160. * @param array &$item
  1161. * @param mixed $property
  1162. * @param string $comment
  1163. * @return void
  1164. */
  1165. public static function comment(&$item, $property = null, $comment){
  1166. if(!$property){
  1167. $property = 'selector';
  1168. }
  1169. if(!isset($item['_comments'][$property])){
  1170. $item['_comments'][$property] = array($comment);
  1171. }
  1172. else{
  1173. $item['_comments'][$property][] = $comment;
  1174. }
  1175. }
  1176. /**
  1177. * reset
  1178. * Resets the parser
  1179. * @return void
  1180. */
  1181. public function reset(){
  1182. $this->code = array();
  1183. $this->parsed = array('global' =>
  1184. array(
  1185. '@import' => array(),
  1186. '@font-face' => array()
  1187. )
  1188. );
  1189. $this->state = null;
  1190. $this->prev_state = null;
  1191. $this->string_state = null;
  1192. $this->token = '';
  1193. $this->selector_stack = array();
  1194. $this->current = array(
  1195. 'se' => null,
  1196. 'pr' => null,
  1197. 'va' => null,
  1198. 'me' => 'global',
  1199. 'fi' => -1,
  1200. 'ci' => 0
  1201. );
  1202. $this->options = array(
  1203. 'indention_char' => " "
  1204. );
  1205. }
  1206. }
  1207. ?>