Added master.css stylesheet for HTML books.
[wolnelektury.git] / compress / filters / csstidy_python / csstidy.py
1 # CSSTidy - CSS Parse
2 #
3 # CSS Parser class
4 #
5 # This file is part of CSSTidy.
6 #
7 # CSSTidy is free software you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # CSSTidy is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with CSSTidy if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20 #
21 # @license http://opensource.org/licenses/gpl-license.php GNU Public License
22 # @package csstidy
23 # @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
24
25 import re
26
27 from optimizer import CSSOptimizer
28 from output import CSSPrinter
29 import data
30 from tools import SortedDict
31
32 class CSSTidy(object):
33     #Saves the parsed CSS
34     _css = ""
35     _raw_css = SortedDict()
36     _optimized_css = SortedDict()
37
38     #List of Tokens
39     _tokens = []
40
41     #Printer class
42     _output = None
43
44     #Optimiser class
45     _optimizer = None
46
47     #Saves the CSS charset (@charset)
48     _charset = ''
49
50     #Saves all @import URLs
51     _import = []
52
53     #Saves the namespace
54     _namespace = ''
55
56     #Contains the version of csstidy
57     _version = '1.3'
58
59     #Stores the settings
60     _settings = {}
61
62     # Saves the parser-status.
63     #
64     # Possible values:
65     # - is = in selector
66     # - ip = in property
67     # - iv = in value
68     # - instr = in string (started at " or ' or ( )
69     # - ic = in comment (ignore everything)
70     # - at = in @-block
71     _status = 'is'
72
73     #Saves the current at rule (@media)
74     _at = ''
75
76     #Saves the current selector
77     _selector = ''
78
79     #Saves the current property
80     _property = ''
81
82     #Saves the position of , in selectors
83     _sel_separate = []
84
85     #Saves the current value
86     _value = ''
87
88     #Saves the current sub-value
89     _sub_value = ''
90
91     #Saves all subvalues for a property.
92     _sub_value_arr = []
93
94     #Saves the char which opened the last string
95     _str_char = ''
96     _cur_string = ''
97
98     #Status from which the parser switched to ic or instr
99     _from = ''
100
101     #Variable needed to manage string-in-strings, for example url("foo.png")
102     _str_in_str = False
103
104     #=True if in invalid at-rule
105     _invalid_at = False
106
107     #=True if something has been added to the current selector
108     _added = False
109
110     #Saves the message log
111     _log = SortedDict()
112
113     #Saves the line number
114     _line = 1
115
116     def __init__(self):
117         self._settings['remove_bslash'] = True
118         self._settings['compress_colors'] = True
119         self._settings['compress_font-weight'] = True
120         self._settings['lowercase_s'] = False
121         self._settings['optimise_shorthands'] = 2
122         self._settings['remove_last_'] = False
123         self._settings['case_properties'] = 1
124         self._settings['sort_properties'] = False
125         self._settings['sort_selectors'] = False
126         self._settings['merge_selectors'] = 2
127         self._settings['discard_invalid_properties'] = False
128         self._settings['css_level'] = 'CSS2.1'
129         self._settings['preserve_css'] = False
130         self._settings['timestamp'] = False
131         self._settings['template'] = 'highest_compression'
132
133         #Maps self._status to methods
134         self.__statusMethod = {'is':self.__parseStatus_is, 'ip': self.__parseStatus_ip, 'iv':self.__parseStatus_iv, 'instr':self.__parseStatus_instr, 'ic':self.__parseStatus_ic, 'at':self.__parseStatus_at}
135
136         self._output = CSSPrinter(self)
137         self._optimizer = CSSOptimizer(self)
138
139     #Public Methods
140     def getSetting(self, setting):
141         return self._settings.get(setting, False)
142
143     #Set the value of a setting.
144     def setSetting(self, setting, value):
145         self._settings[setting] = value
146         return True
147
148     def log(self, message, ttype, line = -1):
149         if line == -1:
150             line = self._line
151
152         line = int(line)
153
154         add = {'m': message, 't': ttype}
155
156         if not self._log.has_key(line):
157             self._log[line] = []
158             self._log[line].append(add)
159         elif add not in self._log[line]:
160             self._log[line].append(add)
161
162
163     #Checks if a character is escaped (and returns True if it is)
164     def escaped(self, string, pos):
165         return not (string[pos-1] != '\\' or self.escaped(string, pos-1))
166
167     #Adds CSS to an existing media/selector
168     def merge_css_blocks(self, media, selector, css_add):
169         for prop, value in css_add.iteritems():
170             self.__css_add_property(media, selector, prop, value, False)
171
172     #Checks if $value is !important.
173     def is_important(self, value):
174         return '!important' in value.lower()
175
176     #Returns a value without !important
177     def gvw_important(self, value):
178         if self.is_important(value):
179             ret = value.strip()
180             ret = ret[0:-9]
181             ret = ret.strip()
182             ret = ret[0:-1]
183             ret = ret.strip()
184             return ret
185
186         return value
187
188     def parse(self, cssString):
189         #Switch from \r\n to \n
190         self._css = cssString.replace("\r\n", "\n") + ' '
191         self._raw_css = {}
192         self._optimized_css = {}
193         self._curComment = ''
194
195         #Start Parsing
196         i = 0
197         while i < len(cssString):
198             if self._css[i] == "\n" or self._css[i] == "\r":
199                 self._line += 1
200
201             i += self.__statusMethod[self._status](i)
202
203             i += 1;
204
205         self._optimized_css = self._optimizer.optimize(self._raw_css)
206
207     def parseFile(self, filename):
208         try:
209             f = open(filename, "r")
210             self.parse(f.read())
211         finally:
212             f.close()
213
214     #Private Methods
215     def __parseStatus_is(self, idx):
216         """
217             Parse in Selector
218         """
219         ret = 0
220
221         if self.__is_token(self._css, idx):
222             if self._css[idx] == '/' and self._css[idx+1] == '*' and self._selector.strip() == '':
223                 self._status = 'ic'
224                 self._from = 'is'
225                 return 1
226
227             elif self._css[idx] == '@' and self._selector.strip() == '':
228                 #Check for at-rule
229                 self._invalid_at = True
230
231                 for name, ttype in data.at_rules.iteritems():
232                     if self._css[idx+1:len(name)].lower() == name.lower():
233                         if ttype == 'at':
234                             self._at = '@' + name
235                         else:
236                             self._selector = '@' + name
237
238                         self._status = ttype
239                         self._invalid_at = False
240                         ret += len(name)
241
242                 if self._invalid_at:
243                     self._selector = '@'
244                     invalid_at_name = ''
245                     for j in xrange(idx+1, len(self._css)):
246                         if not self._css[j].isalpha():
247                             break;
248
249                         invalid_at_name += self._css[j]
250
251                     self.log('Invalid @-rule: ' + invalid_at_name + ' (removed)', 'Warning')
252
253             elif self._css[idx] == '"' or self._css[idx] == "'":
254                 self._cur_string = self._css[idx]
255                 self._status = 'instr'
256                 self._str_char = self._css[idx]
257                 self._from = 'is'
258
259             elif self._invalid_at and self._css[idx] == ';':
260                 self._invalid_at = False
261                 self._status = 'is'
262
263             elif self._css[idx] == '{':
264                 self._status = 'ip'
265                 self.__add_token(data.SEL_START, self._selector)
266                 self._added = False;
267
268             elif self._css[idx] == '}':
269                 self.__add_token(data.AT_END, self._at)
270                 self._at = ''
271                 self._selector = ''
272                 self._sel_separate = []
273
274             elif self._css[idx] == ',':
275                 self._selector = self._selector.strip() + ','
276                 self._sel_separate.append(len(self._selector))
277
278             elif self._css[idx] == '\\':
279                 self._selector += self.__unicode(idx)
280
281             #remove unnecessary universal selector,  FS#147
282             elif not (self._css[idx] == '*' and self._css[idx+1] in ('.', '#', '[', ':')):
283                 self._selector += self._css[idx]
284
285         else:
286             lastpos = len(self._selector)-1
287
288             if lastpos == -1 or not ((self._selector[lastpos].isspace() or self.__is_token(self._selector, lastpos) and self._selector[lastpos] == ',') and self._css[idx].isspace()):
289                 self._selector += self._css[idx]
290
291         return ret
292
293     def __parseStatus_ip(self, idx):
294         """
295             Parse in property
296         """
297         if self.__is_token(self._css, idx):
298             if (self._css[idx] == ':' or self._css[idx] == '=') and self._property != '':
299                 self._status = 'iv'
300
301                 if not self.getSetting('discard_invalid_properties') or self.__property_is_valid(self._property):
302                     self.__add_token(data.PROPERTY, self._property)
303
304             elif self._css[idx] == '/' and self._css[idx+1] == '*' and self._property == '':
305                 self._status = 'ic'
306                 self._from = 'ip'
307                 return 1
308
309             elif self._css[idx] == '}':
310                 self.__explode_selectors()
311                 self._status = 'is'
312                 self._invalid_at = False
313                 self.__add_token(data.SEL_END, self._selector)
314                 self._selector = ''
315                 self._property = ''
316
317             elif self._css[idx] == ';':
318                 self._property = ''
319
320             elif self._css[idx] == '\\':
321                 self._property += self.__unicode(idx)
322
323         elif not self._css[idx].isspace():
324             self._property += self._css[idx]
325
326         return 0
327
328     def __parseStatus_iv(self, idx):
329         """
330             Parse in value
331         """
332         pn = (( self._css[idx] == "\n" or self._css[idx] == "\r") and self.__property_is_next(idx+1) or idx == len(self._css)) #CHECK#
333         if self.__is_token(self._css, idx) or pn:
334             if self._css[idx] == '/' and self._css[idx+1] == '*':
335                 self._status = 'ic'
336                 self._from = 'iv'
337                 return 1
338
339             elif self._css[idx] == '"' or self._css[idx] == "'" or self._css[idx] == '(':
340                 self._cur_string = self._css[idx]
341                 self._str_char = ')' if self._css[idx] == '(' else self._css[idx]
342                 self._status = 'instr'
343                 self._from = 'iv'
344
345             elif self._css[idx] == ',':
346                 self._sub_value = self._sub_value.strip() + ','
347
348             elif self._css[idx] == '\\':
349                 self._sub_value += self.__unicode(idx)
350
351             elif self._css[idx] == ';' or pn:
352                 if len(self._selector) > 0 and self._selector[0] == '@' and data.at_rules.has_key(self._selector[1:]) and data.at_rules[self._selector[1:]] == 'iv':
353                     self._sub_value_arr.append(self._sub_value.strip())
354
355                     self._status = 'is'
356
357                     if '@charset' in self._selector:
358                         self._charset = self._sub_value_arr[0]
359
360                     elif '@namespace' in self._selector:
361                         self._namespace = ' '.join(self._sub_value_arr)
362
363                     elif '@import' in self._selector:
364                         self._import.append(' '.join(self._sub_value_arr))
365
366
367                     self._sub_value_arr = []
368                     self._sub_value = ''
369                     self._selector = ''
370                     self._sel_separate = []
371
372                 else:
373                     self._status = 'ip'
374
375             elif self._css[idx] != '}':
376                 self._sub_value += self._css[idx]
377
378             if (self._css[idx] == '}' or self._css[idx] == ';' or pn) and self._selector != '':
379                 if self._at == '':
380                     self._at = data.DEFAULT_AT
381
382                 #case settings
383                 if self.getSetting('lowercase_s'):
384                     self._selector = self._selector.lower()
385
386                 self._property = self._property.lower()
387
388                 if self._sub_value != '':
389                     self._sub_value_arr.append(self._sub_value)
390                     self._sub_value = ''
391
392                 self._value = ' '.join(self._sub_value_arr)
393
394
395                 self._selector = self._selector.strip()
396
397                 valid = self.__property_is_valid(self._property)
398
399                 if (not self._invalid_at or self.getSetting('preserve_css')) and (not self.getSetting('discard_invalid_properties') or valid):
400                     self.__css_add_property(self._at, self._selector, self._property, self._value)
401                     self.__add_token(data.VALUE, self._value)
402
403                 if not valid:
404                     if self.getSetting('discard_invalid_properties'):
405                         self.log('Removed invalid property: ' + self._property, 'Warning')
406
407                     else:
408                         self.log('Invalid property in ' + self.getSetting('css_level').upper() + ': ' + self._property, 'Warning')
409
410                 self._property = '';
411                 self._sub_value_arr = []
412                 self._value = ''
413
414             if self._css[idx] == '}':
415                 self.__explode_selectors()
416                 self.__add_token(data.SEL_END, self._selector)
417                 self._status = 'is'
418                 self._invalid_at = False
419                 self._selector = ''
420
421         elif not pn:
422             self._sub_value += self._css[idx]
423
424             if self._css[idx].isspace():
425                 if self._sub_value != '':
426                     self._sub_value_arr.append(self._sub_value)
427                     self._sub_value = ''
428
429         return 0
430
431     def __parseStatus_instr(self, idx):
432         """
433             Parse in String
434         """
435         if self._str_char == ')' and (self._css[idx] == '"' or self._css[idx] == "'") and not self.escaped(self._css, idx):
436             self._str_in_str = not self._str_in_str
437
438         temp_add = self._css[idx] # ...and no not-escaped backslash at the previous position
439         if (self._css[idx] == "\n" or self._css[idx] == "\r") and not (self._css[idx-1] == '\\' and not self.escaped(self._css, idx-1)):
440             temp_add = "\\A "
441             self.log('Fixed incorrect newline in string', 'Warning')
442
443         if not (self._str_char == ')' and self._css[idx].isspace() and not self._str_in_str):
444             self._cur_string += temp_add
445
446         if self._css[idx] == self._str_char and not self.escaped(self._css, idx) and not self._str_in_str:
447             self._status = self._from
448             regex = re.compile(r'([\s]+)', re.I | re.U | re.S)
449             if regex.match(self._cur_string) is None and self._property != 'content':
450                 if self._str_char == '"' or self._str_char == "'":
451                     self._cur_string = self._cur_string[1:-1]
452
453                 elif len(self._cur_string) > 3 and (self._cur_string[1] == '"' or self._cur_string[1] == "'"):
454                     self._cur_string = self._cur_string[0] + self._cur_string[2:-2] + self._cur_string[-1]
455
456             if self._from == 'iv':
457                 self._sub_value += self._cur_string
458
459             elif self._from == 'is':
460                 self._selector += self._cur_string
461
462         return 0
463
464     def __parseStatus_ic(self, idx):
465         """
466             Parse css In Comment
467         """
468         if self._css[idx] == '*' and self._css[idx+1] == '/':
469             self._status = self._from
470             self.__add_token(data.COMMENT, self._curComment)
471             self._curComment = ''
472             return 1
473
474         else:
475             self._curComment += self._css[idx]
476
477         return 0
478
479     def __parseStatus_at(self, idx):
480         """
481             Parse in at-block
482         """
483         if self.__is_token(string, idx):
484             if self._css[idx] == '/' and self._css[idx+1] == '*':
485                 self._status = 'ic'
486                 self._from = 'at'
487                 return 1
488
489             elif self._css[i] == '{':
490                 self._status = 'is'
491                 self.__add_token(data.AT_START, self._at)
492
493             elif self._css[i] == ',':
494                 self._at = self._at.strip() + ','
495
496             elif self._css[i] == '\\':
497                 self._at += self.__unicode(i)
498         else:
499             lastpos = len(self._at)-1
500             if not (self._at[lastpos].isspace() or self.__is_token(self._at, lastpos) and self._at[lastpos] == ',') and self._css[i].isspace():
501                 self._at += self._css[i]
502
503         return 0
504
505     def __explode_selectors(self):
506         #Explode multiple selectors
507         if self.getSetting('merge_selectors') == 1:
508             new_sels = []
509             lastpos = 0;
510             self._sel_separate.append(len(self._selector))
511
512             for num in xrange(len(self._sel_separate)):
513                 pos = self._sel_separate[num]
514                 if num == (len(self._sel_separate)): #CHECK#
515                     pos += 1
516
517                 new_sels.append(self._selector[lastpos:(pos-lastpos-1)])
518                 lastpos = pos
519
520             if len(new_sels) > 1:
521                 for selector in new_sels:
522                     self.merge_css_blocks(self._at, selector, self._raw_css[self._at][self._selector])
523
524                 del self._raw_css[self._at][self._selector]
525
526         self._sel_separate = []
527
528     #Adds a property with value to the existing CSS code
529     def __css_add_property(self, media, selector, prop, new_val):
530         if self.getSetting('preserve_css') or new_val.strip() == '':
531             return
532
533         if not self._raw_css.has_key(media):
534             self._raw_css[media] = SortedDict()
535
536         if not self._raw_css[media].has_key(selector):
537             self._raw_css[media][selector] = SortedDict()
538
539         self._added = True
540         if self._raw_css[media][selector].has_key(prop):
541             if (self.is_important(self._raw_css[media][selector][prop]) and self.is_important(new_val)) or not self.is_important(self._raw_css[media][selector][prop]):
542                 del self._raw_css[media][selector][prop]
543                 self._raw_css[media][selector][prop] = new_val.strip()
544
545         else:
546             self._raw_css[media][selector][prop] = new_val.strip()
547
548     #Checks if the next word in a string from pos is a CSS property
549     def __property_is_next(self, pos):
550         istring = self._css[pos: len(self._css)]
551         pos = istring.find(':')
552         if pos == -1:
553             return False;
554
555         istring = istring[:pos].strip().lower()
556         if data.all_properties.has_key(istring):
557             self.log('Added semicolon to the end of declaration', 'Warning')
558             return True
559
560         return False;
561
562     #Checks if a property is valid
563     def __property_is_valid(self, prop):
564         return (data.all_properties.has_key(prop) and data.all_properties[prop].find(self.getSetting('css_level').upper()) != -1)
565
566     #Adds a token to self._tokens
567     def __add_token(self, ttype, cssdata, do=False):
568         if self.getSetting('preserve_css') or do:
569             if ttype == data.COMMENT:
570                 token = [ttype, cssdata]
571             else:
572                 token = [ttype, cssdata.strip()]
573
574             self._tokens.append(token)
575
576     #Parse unicode notations and find a replacement character
577     def __unicode(self, idx):
578        ##FIX##
579        return ''
580
581     #Starts parsing from URL
582     ##USED?
583     def __parse_from_url(self, url):
584         try:
585             if "http" in url.lower() or "https" in url.lower():
586                 f = urllib.urlopen(url)
587             else:
588                 f = open(url)
589
590             data = f.read()
591             return self.parse(data)
592         finally:
593             f.close()
594
595     #Checks if there is a token at the current position
596     def __is_token(self, string, idx):
597         return (string[idx] in data.tokens and not self.escaped(string, idx))
598
599
600     #Property Methods
601     def _getOutput(self):
602         self._output.prepare(self._optimized_css)
603         return self._output.render
604
605     def _getLog(self):
606         ret = ""
607         ks = self._log.keys()
608         ks.sort()
609         for line in ks:
610             for msg in self._log[line]:
611                 ret += "Type: " + msg['t'] + "\n"
612                 ret += "Message: " + msg['m'] + "\n"
613             ret += "\n"
614
615         return ret
616
617     def _getCSS(self):
618         return self._css
619
620
621     #Properties
622     Output = property(_getOutput, None)
623     Log = property(_getLog, None)
624     CSS = property(_getCSS, None)
625
626
627 if __name__ == '__main__':
628     import sys
629     tidy = CSSTidy()
630     f = open(sys.argv[1], "r")
631     css = f.read()
632     f.close()
633     tidy.parse(css)
634     tidy.Output('file', filename="Stylesheet.min.css")
635     print tidy.Output()
636     #print tidy._import