1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:
27:
28: 29: 30:
31: require('data.inc.php');
32:
33: 34: 35:
36: require('class.csstidy_print.php');
37:
38: 39: 40:
41: require('class.csstidy_optimise.php');
42:
43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53:
54: class csstidy {
55:
56: 57: 58: 59: 60:
61: var $css = array();
62:
63: 64: 65: 66: 67:
68: var $tokens = array();
69:
70: 71: 72: 73: 74: 75:
76: var $print;
77:
78: 79: 80: 81: 82: 83:
84: var $optimise;
85:
86: 87: 88: 89: 90:
91: var $charset = '';
92:
93: 94: 95: 96: 97:
98: var $import = array();
99:
100: 101: 102: 103: 104:
105: var $namespace = '';
106:
107: 108: 109: 110: 111:
112: var $settings = array();
113:
114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127:
128: var $status = 'is';
129:
130:
131: 132: 133: 134: 135:
136: var $at = '';
137:
138: 139: 140: 141: 142:
143: var $selector = '';
144:
145: 146: 147: 148: 149:
150: var $property = '';
151:
152: 153: 154: 155: 156:
157: var $sel_separate = array();
158:
159: 160: 161: 162: 163:
164: var $value = '';
165:
166: 167: 168: 169: 170: 171: 172: 173: 174: 175:
176: var $sub_value = '';
177:
178: 179: 180: 181: 182: 183:
184: var $sub_value_arr = array();
185:
186: 187: 188: 189: 190:
191: var $str_char = '';
192: var $cur_string = '';
193:
194: 195: 196: 197: 198:
199: var $from = '';
200:
201: 202: 203: 204: 205:
206: var $str_in_str = false;
207:
208: 209: 210: 211: 212:
213: var $invalid_at = false;
214:
215: 216: 217: 218: 219:
220: var $added = false;
221:
222: 223: 224: 225: 226:
227: var $log = array();
228:
229: 230: 231: 232: 233:
234: var $line = 1;
235:
236: 237: 238: 239:
240: function csstidy()
241: {
242: $this->settings['remove_bslash'] = true;
243: $this->settings['compress_colors'] = true;
244: $this->settings['compress_font-weight'] = true;
245: $this->settings['lowercase_s'] = false;
246: $this->settings['optimise_shorthands'] = 1;
247: $this->settings['remove_last_;'] = false;
248: $this->settings['case_properties'] = 1;
249: $this->settings['sort_properties'] = false;
250: $this->settings['sort_selectors'] = false;
251: $this->settings['merge_selectors'] = 2;
252: $this->settings['discard_invalid_properties'] = false;
253: $this->settings['css_level'] = 'CSS2.1';
254: $this->settings['preserve_css'] = false;
255: $this->settings['timestamp'] = false;
256: $this->settings['log'] = false;
257:
258: $this->load_template('default');
259: $this->print = new csstidy_print($this);
260: $this->optimise = new csstidy_optimise($this);
261: }
262:
263: 264: 265: 266: 267: 268:
269: function get_cfg($setting)
270: {
271: if(isset($this->settings[$setting]))
272: {
273: return $this->settings[$setting];
274: }
275: return false;
276: }
277:
278: 279: 280: 281: 282: 283: 284:
285: function set_cfg($setting,$value)
286: {
287: if(isset($this->settings[$setting]) && $value !== '')
288: {
289: $this->settings[$setting] = $value;
290: return true;
291: }
292: return false;
293: }
294:
295: 296: 297: 298: 299: 300: 301:
302: function _add_token($type, $data, $do = false) {
303: if($this->get_cfg('preserve_css') || $do) {
304: $this->tokens[] = array($type, ($type == COMMENT) ? $data : trim($data));
305: }
306: }
307:
308: 309: 310: 311: 312: 313: 314:
315: function log($message,$type,$line = -1)
316: {
317: if (!$this->settings['log']) {
318: return;
319: }
320:
321: if($line === -1)
322: {
323: $line = $this->line;
324: }
325: $line = intval($line);
326: $add = array('m' => $message, 't' => $type);
327: if(!isset($this->log[$line]) || !in_array($add,$this->log[$line]))
328: {
329: $this->log[$line][] = $add;
330: }
331: }
332:
333: 334: 335: 336: 337: 338: 339:
340: function _unicode(&$string, &$i)
341: {
342: ++$i;
343: $add = '';
344: $tokens =& $GLOBALS['csstidy']['tokens'];
345: $replaced = false;
346:
347: while($i < strlen($string) && (ctype_xdigit($string{$i}) || ctype_space($string{$i})) && strlen($add) < 6)
348: {
349: $add .= $string{$i};
350:
351: if(ctype_space($string{$i})) {
352: break;
353: }
354: $i++;
355: }
356:
357: if(hexdec($add) > 47 && hexdec($add) < 58 || hexdec($add) > 64 && hexdec($add) < 91 || hexdec($add) > 96 && hexdec($add) < 123)
358: {
359: $this->log('Replaced unicode notation: Changed \\'. $add .' to ' . chr(hexdec($add)),'Information');
360: $add = chr(hexdec($add));
361: $replaced = true;
362: }
363: else {
364: $add = trim('\\'.$add);
365: }
366:
367: if(@ctype_xdigit($string{$i+1}) && ctype_space($string{$i})
368: && !$replaced || !ctype_space($string{$i})) {
369: $i--;
370: }
371:
372: if($add != '\\' || !$this->get_cfg('remove_bslash') || strpos($tokens, $string{$i+1}) !== false) {
373: return $add;
374: }
375:
376: if($add == '\\') {
377: $this->log('Removed unnecessary backslash','Information');
378: }
379: return '';
380: }
381:
382: 383: 384: 385: 386: 387: 388:
389: function load_template($content, $from_file=true)
390: {
391: $predefined_templates =& $GLOBALS['csstidy']['predefined_templates'];
392: if($content == 'high_compression' || $content == 'default' || $content == 'highest_compression' || $content == 'low_compression')
393: {
394: $this->template = $predefined_templates[$content];
395: return;
396: }
397:
398: if($from_file)
399: {
400: $content = strip_tags(file_get_contents($content),'<span>');
401: }
402: $content = str_replace("\r\n","\n",$content);
403: $template = explode('|',$content);
404:
405: for ($i = 0; $i < count($template); $i++ )
406: {
407: $this->template[$i] = $template[$i];
408: }
409: }
410:
411: 412: 413: 414: 415:
416: function parse_from_url($url)
417: {
418: return $this->parse(@file_get_contents($url));
419: }
420:
421: 422: 423: 424: 425: 426:
427: function is_token(&$string, $i)
428: {
429: $tokens =& $GLOBALS['csstidy']['tokens'];
430: return (strpos($tokens, $string{$i}) !== false && !$this->escaped($string,$i));
431: }
432:
433:
434: 435: 436: 437: 438: 439:
440: function parse($string) {
441: $old = setlocale(LC_ALL, 0);
442: setlocale(LC_ALL, 'C');
443:
444:
445: $this->print = new csstidy_print($this);
446: $this->optimise = new csstidy_optimise($this);
447:
448: $all_properties =& $GLOBALS['csstidy']['all_properties'];
449: $at_rules =& $GLOBALS['csstidy']['at_rules'];
450:
451: $this->css = array();
452: $this->print->input_css = $string;
453: $string = str_replace("\r\n","\n",$string) . ' ';
454: $cur_comment = '';
455: $s_id = 0;
456:
457: for ($i = 0, $size = strlen($string); $i < $size; $i++ )
458: {
459: if($string{$i} == "\n" || $string{$i} == "\r")
460: {
461: ++$this->line;
462: }
463:
464: switch($this->status)
465: {
466:
467: case 'at':
468: if($this->is_token($string,$i))
469: {
470: if($string{$i} == '/' && @$string{$i+1} == '*')
471: {
472: $this->status = 'ic'; ++$i;
473: $this->from = 'at';
474: }
475: elseif($string{$i} == '{')
476: {
477: $this->status = 'is';
478: $this->_add_token(AT_START, $this->at);
479: }
480: elseif($string{$i} == ',')
481: {
482: $this->at = trim($this->at).',';
483: }
484: elseif($string{$i} == '\\')
485: {
486: $this->at .= $this->_unicode($string,$i);
487: }
488: }
489: else
490: {
491: $lastpos = strlen($this->at)-1;
492: if(!( (ctype_space($this->at{$lastpos}) || $this->is_token($this->at,$lastpos) && $this->at{$lastpos} == ',') && ctype_space($string{$i})))
493: {
494: $this->at .= $string{$i};
495: }
496: }
497: break;
498:
499:
500: case 'is':
501: if($this->is_token($string,$i))
502: {
503: if($string{$i} == '/' && @$string{$i+1} == '*' && trim($this->selector) == '')
504: {
505: $this->status = 'ic'; ++$i;
506: $this->from = 'is';
507: }
508: elseif($string{$i} == '@' && trim($this->selector) == '')
509: {
510:
511: $this->invalid_at = true;
512: foreach($at_rules as $name => $type)
513: {
514: if(!strcasecmp(substr($string,$i+1,strlen($name)),$name))
515: {
516: ($type == 'at') ? $this->at = '@'.$name : $this->selector = '@'.$name;
517: $this->status = $type;
518: $i += strlen($name);
519: $this->invalid_at = false;
520: }
521: }
522:
523: if($this->invalid_at)
524: {
525: $this->selector = '@';
526: $invalid_at_name = '';
527: for($j = $i+1; $j < $size; ++$j)
528: {
529: if(!ctype_alpha($string{$j}))
530: {
531: break;
532: }
533: $invalid_at_name .= $string{$j};
534: }
535: $this->log('Invalid @-rule: '.$invalid_at_name.' (removed)','Warning');
536: }
537: }
538: elseif(($string{$i} == '"' || $string{$i} == "'"))
539: {
540: $this->cur_string = $string{$i};
541: $this->status = 'instr';
542: $this->str_char = $string{$i};
543: $this->from = 'is';
544: }
545: elseif($this->invalid_at && $string{$i} == ';')
546: {
547: $this->invalid_at = false;
548: $this->status = 'is';
549: }
550: elseif($string{$i} == '{')
551: {
552: $this->status = 'ip';
553: $this->_add_token(SEL_START, $this->selector);
554: $this->added = false;
555: }
556: elseif($string{$i} == '}')
557: {
558: $this->_add_token(AT_END, $this->at);
559: $this->at = '';
560: $this->selector = '';
561: $this->sel_separate = array();
562: ++$s_id;
563: }
564: elseif($string{$i} == ',')
565: {
566: $this->selector = trim($this->selector).',';
567: $this->sel_separate[] = strlen($this->selector);
568: }
569: elseif($string{$i} == '\\')
570: {
571: $this->selector .= $this->_unicode($string,$i);
572: }
573:
574: else if(!($string{$i} == '*' && @in_array($string{$i+1}, array('.', '#', '[', ':')))) {
575: $this->selector .= $string{$i};
576: }
577: }
578: else
579: {
580: $lastpos = strlen($this->selector)-1;
581: if($lastpos == -1 || !( (ctype_space($this->selector{$lastpos}) || $this->is_token($this->selector,$lastpos) && $this->selector{$lastpos} == ',') && ctype_space($string{$i})))
582: {
583: $this->selector .= $string{$i};
584: }
585: }
586: break;
587:
588:
589: case 'ip':
590: if($this->is_token($string,$i))
591: {
592: if(($string{$i} == ':' || $string{$i} == '=') && $this->property != '')
593: {
594: $this->status = 'iv';
595: if(!$this->get_cfg('discard_invalid_properties') || $this->property_is_valid($this->property)) {
596: $this->_add_token(PROPERTY, $this->property);
597: }
598: }
599: elseif($string{$i} == '/' && @$string{$i+1} == '*' && $this->property == '')
600: {
601: $this->status = 'ic'; ++$i;
602: $this->from = 'ip';
603: }
604: elseif($string{$i} == '}')
605: {
606: $this->explode_selectors();
607: $this->status = 'is';
608: $this->invalid_at = false;
609: $this->_add_token(SEL_END, $this->selector);
610: $this->selector = '';
611: $this->property = '';
612: }
613: elseif($string{$i} == ';')
614: {
615: $this->property = '';
616: }
617: elseif($string{$i} == '\\')
618: {
619: $this->property .= $this->_unicode($string,$i);
620: }
621: }
622: elseif(!ctype_space($string{$i}))
623: {
624: $this->property .= $string{$i};
625: }
626: break;
627:
628:
629: case 'iv':
630: $pn = (($string{$i} == "\n" || $string{$i} == "\r") && $this->property_is_next($string,$i+1) || $i == strlen($string)-1);
631: if($this->is_token($string,$i) || $pn)
632: {
633: if($string{$i} == '/' && @$string{$i+1} == '*')
634: {
635: $this->status = 'ic'; ++$i;
636: $this->from = 'iv';
637: }
638: elseif(($string{$i} == '"' || $string{$i} == "'" || $string{$i} == '('))
639: {
640: $this->cur_string = $string{$i};
641: $this->str_char = ($string{$i} == '(') ? ')' : $string{$i};
642: $this->status = 'instr';
643: $this->from = 'iv';
644: }
645: elseif($string{$i} == ',')
646: {
647: $this->sub_value = trim($this->sub_value).',';
648: }
649: elseif($string{$i} == '\\')
650: {
651: $this->sub_value .= $this->_unicode($string,$i);
652: }
653: elseif($string{$i} == ';' || $pn)
654: {
655: if($this->selector{0} == '@' && isset($at_rules[substr($this->selector,1)]) && $at_rules[substr($this->selector,1)] == 'iv')
656: {
657: $this->sub_value_arr[] = trim($this->sub_value);
658:
659: $this->status = 'is';
660:
661: switch($this->selector)
662: {
663: case '@charset': $this->charset = $this->sub_value_arr[0]; break;
664: case '@namespace': $this->namespace = implode(' ',$this->sub_value_arr); break;
665: case '@import': $this->import[] = implode(' ',$this->sub_value_arr); break;
666: }
667:
668: $this->sub_value_arr = array();
669: $this->sub_value = '';
670: $this->selector = '';
671: $this->sel_separate = array();
672: }
673: else
674: {
675: $this->status = 'ip';
676: }
677: }
678: elseif($string{$i} != '}')
679: {
680: $this->sub_value .= $string{$i};
681: }
682: if(($string{$i} == '}' || $string{$i} == ';' || $pn) && !empty($this->selector))
683: {
684: if($this->at == '')
685: {
686: $this->at = DEFAULT_AT;
687: }
688:
689:
690: if($this->get_cfg('lowercase_s'))
691: {
692: $this->selector = strtolower($this->selector);
693: }
694: $this->property = strtolower($this->property);
695:
696: $this->optimise->subvalue();
697: if($this->sub_value != '') {
698: $this->sub_value_arr[] = $this->sub_value;
699: $this->sub_value = '';
700: }
701:
702: $this->value = implode(' ',$this->sub_value_arr);
703:
704: $this->selector = trim($this->selector);
705:
706: $this->optimise->value();
707:
708: $valid = $this->property_is_valid($this->property);
709: if((!$this->invalid_at || $this->get_cfg('preserve_css')) && (!$this->get_cfg('discard_invalid_properties') || $valid))
710: {
711: $this->css_add_property($this->at,$this->selector,$this->property,$this->value, $s_id);
712: $this->_add_token(VALUE, $this->value);
713: $this->optimise->shorthands();
714: }
715: if(!$valid)
716: {
717: if($this->get_cfg('discard_invalid_properties'))
718: {
719: $this->log('Removed invalid property: '.$this->property,'Warning');
720: }
721: else
722: {
723: $this->log('Invalid property in '.strtoupper($this->get_cfg('css_level')).': '.$this->property,'Warning');
724: }
725: }
726:
727: $this->property = '';
728: $this->sub_value_arr = array();
729: $this->value = '';
730: }
731: if($string{$i} == '}')
732: {
733: $this->explode_selectors();
734: $this->_add_token(SEL_END, $this->selector);
735: $this->status = 'is';
736: $this->invalid_at = false;
737: $this->selector = '';
738: }
739: }
740: elseif(!$pn)
741: {
742: $this->sub_value .= $string{$i};
743:
744: if(ctype_space($string{$i}))
745: {
746: $this->optimise->subvalue();
747: if($this->sub_value != '') {
748: $this->sub_value_arr[] = $this->sub_value;
749: $this->sub_value = '';
750: }
751: }
752: }
753: break;
754:
755:
756: case 'instr':
757: if($this->str_char == ')' && ($string{$i} == '"' || $string{$i} == '\'') && !$this->str_in_str && !$this->escaped($string,$i))
758: {
759: $this->str_in_str = true;
760: }
761: elseif($this->str_char == ')' && ($string{$i} == '"' || $string{$i} == '\'') && $this->str_in_str && !$this->escaped($string,$i))
762: {
763: $this->str_in_str = false;
764: }
765: $temp_add = $string{$i};
766: if( ($string{$i} == "\n" || $string{$i} == "\r") && !($string{$i-1} == '\\' && !$this->escaped($string,$i-1)) )
767: {
768: $temp_add = "\\A ";
769: $this->log('Fixed incorrect newline in string','Warning');
770: }
771: if (!($this->str_char == ')' && in_array($string{$i}, $GLOBALS['csstidy']['whitespace']) && !$this->str_in_str)) {
772: $this->cur_string .= $temp_add;
773: }
774: if($string{$i} == $this->str_char && !$this->escaped($string,$i) && !$this->str_in_str)
775: {
776: $this->status = $this->from;
777: if (!preg_match('|[' . implode('', $GLOBALS['csstidy']['whitespace']) . ']|uis', $this->cur_string) && $this->property != 'content') {
778: if ($this->str_char == '"' || $this->str_char == '\'') {
779: if ($this->status != 'is') {
780: $this->cur_string = substr($this->cur_string, 1, -1);
781: }
782: } else if (strlen($this->cur_string) > 3 && ($this->cur_string[1] == '"' || $this->cur_string[1] == '\'')) {
783: $this->cur_string = $this->cur_string[0] . substr($this->cur_string, 2, -2) . substr($this->cur_string, -1);
784: }
785: }
786: if($this->from == 'iv')
787: {
788: $this->sub_value .= $this->cur_string;
789: }
790: elseif($this->from == 'is')
791: {
792: $this->selector .= $this->cur_string;
793: }
794: }
795: break;
796:
797:
798: case 'ic':
799: if($string{$i} == '*' && $string{$i+1} == '/')
800: {
801: $this->status = $this->from;
802: $i++;
803: $this->_add_token(COMMENT, $cur_comment);
804: $cur_comment = '';
805: }
806: else
807: {
808: $cur_comment .= $string{$i};
809: }
810: break;
811: }
812: }
813:
814: $this->optimise->postparse();
815:
816: $this->print->_reset();
817: setlocale(LC_ALL, $old);
818:
819: return !(empty($this->css) && empty($this->import) && empty($this->charset) && empty($this->tokens) && empty($this->namespace));
820: }
821:
822: 823: 824: 825:
826: function explode_selectors()
827: {
828:
829: if($this->get_cfg('merge_selectors') == 1)
830: {
831: $new_sels = array();
832: $lastpos = 0;
833: $this->sel_separate[] = strlen($this->selector);
834: foreach($this->sel_separate as $num => $pos)
835: {
836: if($num == count($this->sel_separate)-1) {
837: $pos += 1;
838: }
839:
840: $new_sels[] = substr($this->selector,$lastpos,$pos-$lastpos-1);
841: $lastpos = $pos;
842: }
843:
844: if(count($new_sels) > 1)
845: {
846: foreach($new_sels as $selector)
847: {
848: $this->merge_css_blocks($this->at,$selector,$this->css[$this->at][$this->selector]);
849: }
850: unset($this->css[$this->at][$this->selector]);
851: }
852: }
853: $this->sel_separate = array();
854: }
855:
856: 857: 858: 859: 860: 861: 862:
863: function escaped(&$string,$pos)
864: {
865: return !(@($string{$pos-1} != '\\') || $this->escaped($string,$pos-1));
866: }
867:
868: 869: 870: 871: 872: 873: 874: 875: 876:
877: function css_add_property($media,$selector,$property,$new_val,$selector_id = null)
878: {
879: $new_val = trim($new_val);
880:
881: if (!$this->get_cfg('preserve_css') && ($new_val != '')) {
882: $this->added = true;
883:
884: if (isset($this->css[$media][$selector][$property])) {
885: $ptr = &$this->css[$media][$selector][$property];
886: if ($this->is_important(end($ptr['p']))) {
887: return;
888: } elseif (!is_null($selector_id) &&
889: ($ptr['id'] == $selector_id)) {
890: $ptr['p'][] = $new_val;
891: return;
892: }
893: }
894:
895: $this->css[$media][$selector][$property] = array(
896: 'id' => (is_null($selector_id) ? -1 : $selector_id),
897: 'p' => array($new_val)
898: );
899: }
900: }
901:
902: 903: 904: 905: 906: 907: 908:
909: function merge_css_blocks($media, $selector, $css_add)
910: {
911: foreach ($css_add as $property => $value) {
912: $this->css_add_property($media, $selector, $property, $value);
913: }
914: }
915:
916: 917: 918: 919: 920: 921:
922: function is_important($value)
923: {
924: return (!strcasecmp(substr(str_replace($GLOBALS['csstidy']['whitespace'],'',$value),-10,10),'!important'));
925: }
926:
927: 928: 929: 930: 931: 932:
933: function gvw_important($value)
934: {
935: if($this->is_important($value))
936: {
937: $value = trim($value);
938: $value = substr($value,0,-9);
939: $value = trim($value);
940: $value = substr($value,0,-1);
941: $value = trim($value);
942: }
943: return $value;
944: }
945:
946: 947: 948: 949: 950: 951: 952:
953: function property_is_next($istring, $pos)
954: {
955: $all_properties =& $GLOBALS['csstidy']['all_properties'];
956: $istring = substr($istring,$pos,strlen($istring)-$pos);
957: $pos = strpos($istring,':');
958: if($pos === false)
959: {
960: return false;
961: }
962: $istring = strtolower(trim(substr($istring,0,$pos)));
963: if(isset($all_properties[$istring]))
964: {
965: $this->log('Added semicolon to the end of declaration','Warning');
966: return true;
967: }
968: return false;
969: }
970:
971: 972: 973: 974: 975: 976:
977: function property_is_valid($property) {
978: $all_properties =& $GLOBALS['csstidy']['all_properties'];
979: return (isset($all_properties[$property]) && strpos($all_properties[$property],strtoupper($this->get_cfg('css_level'))) !== false );
980: }
981:
982: 983: 984: 985: 986: 987: 988: 989:
990: function filterBySelector($input)
991: {
992: if (empty($input)) {
993: return '';
994: }
995:
996: $cssarray = empty($this->css)
997: ? array()
998: : reset($this->css);
999: $style = '';
1000:
1001: foreach (array_keys($cssarray) as $val) {
1002: foreach (array_map('trim', explode(',', $val)) as $entry) {
1003: if (in_array($entry, $input)) {
1004: foreach ($cssarray[$val] as $key2 => $val2) {
1005: foreach ($val2['p'] as $val3) {
1006: $style .= $key2 . ':' . $val3 . ';';
1007: }
1008: }
1009: continue;
1010: }
1011: }
1012: }
1013:
1014: return $style;
1015: }
1016:
1017: }
1018: