7cd284cfc52ab935708c4cece0b175b087977f8d
[redakcja.git] / apps / compress / filters / csstidy_python / optimizer.py
1 # CSSTidy - CSS Optimizer
2 #
3 # CSS Optimizer 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 data
26 from tools import SortedDict
27
28
29 class CSSOptimizer(object):
30     def __init__(self, parser):
31         #raw_css is a dict
32         self.parser = parser
33         self._optimized_css = SortedDict
34
35
36 #PUBLIC METHODS
37     def optimize(self, raw_css):
38         if self.parser.getSetting('preserve_css'):
39             return raw_css
40
41         self._optimized_css = raw_css
42
43         if self.parser.getSetting('merge_selectors') == 2:
44             self.__merge_selectors()
45
46         ##OPTIMIZE##
47         for media, css in self._optimized_css.iteritems():
48             for selector, cssdata in css.iteritems():
49                 if self.parser.getSetting('optimise_shorthands') >= 1:
50                     cssdata = self.__merge_4value_shorthands(cssdata)
51
52                 if self.parser.getSetting('optimise_shorthands') >= 2:
53                     cssdata = self.__merge_bg(cssdata)
54
55                 for item, value in cssdata.iteritems():
56                     value = self.__compress_numbers(item, value)
57                     value = self.__compress_important(value)
58
59                     if item in data.color_values and self.parser.getSetting('compress_colors'):
60                         old = value[:]
61                         value = self.__compress_color(value)
62                         if old != value:
63                             self.parser.log('In "' + selector + '" Optimised ' + item + ': Changed ' + old + ' to ' + value, 'Information')
64
65                     if item == 'font-weight' and self.parser.getSetting('compress_font-weight'):
66                         if value  == 'bold':
67                             value = '700'
68                             self.parser.log('In "' + selector + '" Optimised font-weight: Changed "bold" to "700"', 'Information')
69
70                         elif value == 'normal':
71                             value = '400'
72                             self.parser.log('In "' + selector + '" Optimised font-weight: Changed "normal" to "400"', 'Information')
73
74                     self._optimized_css[media][selector][item] = value
75
76
77         return self._optimized_css
78
79
80 #PRIVATE METHODS
81     def __merge_bg(self, cssdata):
82         """
83             Merges all background properties
84             @cssdata (dict) is a dictionary of the selector properties
85         """
86         #Max number of background images. CSS3 not yet fully implemented
87         img = 1
88         clr = 1
89         bg_img_list = []
90         if cssdata.has_key('background-image'):
91             img = len(cssdata['background-image'].split(','))
92             bg_img_list = self.parser.gvw_important(cssdata['background-image']).split(',')
93
94         elif cssdata.has_key('background-color'):
95             clr = len(cssdata['background-color'].split(','))
96
97
98         number_of_values = max(img, clr, 1)
99
100         new_bg_value = ''
101         important = ''
102
103         for i in xrange(number_of_values):
104             for bg_property, default_value in data.background_prop_default.iteritems():
105                 #Skip if property does not exist
106                 if not cssdata.has_key(bg_property):
107                     continue
108
109                 cur_value = cssdata[bg_property]
110
111                 #Skip some properties if there is no background image
112                 if (len(bg_img_list) > i and bg_img_list[i] == 'none') and bg_property in frozenset(['background-size', 'background-position', 'background-attachment', 'background-repeat']):
113                     continue
114
115                 #Remove !important
116                 if self.parser.is_important(cur_value):
117                     important = ' !important'
118                     cur_value = self.parser.gvw_important(cur_value)
119
120                 #Do not add default values
121                 if cur_value == default_value:
122                     continue
123
124                 temp = cur_value.split(',')
125
126                 if len(temp) > i:
127                     if bg_property == 'background-size':
128                         new_bg_value += '(' + temp[i] + ') '
129
130                     else:
131                         new_bg_value += temp[i] + ' '
132
133             new_bg_value = new_bg_value.strip()
134             if i != (number_of_values-1):
135                 new_bg_value += ','
136
137         #Delete all background-properties
138         for bg_property, default_value in data.background_prop_default.iteritems():
139             try:
140                 del cssdata[bg_property]
141             except:
142                 pass
143
144         #Add new background property
145         if new_bg_value != '':
146             cssdata['background'] = new_bg_value + important
147
148         return cssdata
149
150     def __merge_4value_shorthands(self, cssdata):
151         """
152             Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
153             @cssdata (dict) is a dictionary of the selector properties
154         """
155         for key, value in data.shorthands.iteritems():
156             important = ''
157             if value != 0 and cssdata.has_key(value[0]) and cssdata.has_key(value[1]) and cssdata.has_key(value[2]) and cssdata.has_key(value[3]):
158                 cssdata[key] = ''
159
160                 for i in xrange(4):
161                     val = cssdata[value[i]]
162                     if self.parser.is_important(val):
163                         important = '!important'
164                         cssdata[key] += self.parser.gvw_important(val) + ' '
165
166                     else:
167                         cssdata[key] += val + ' '
168
169                     del cssdata[value[i]]
170             if cssdata.has_key(key):
171                 cssdata[key] = self.__shorthand(cssdata[key] + important.strip())
172
173         return cssdata
174
175
176     def __merge_selectors(self):
177         """
178             Merges selectors with same properties. Example: a{color:red} b{color:red} . a,b{color:red}
179             Very basic and has at least one bug. Hopefully there is a replacement soon.
180             @selector_one (string) is the current selector
181             @value_one (dict) is a dictionary of the selector properties
182             Note: Currently is the elements of a selector are identical, but in a different order, they are not merged
183         """
184
185         ##OPTIMIZE##
186         ##FIX##
187
188         raw_css = self._optimized_css.copy()
189         delete = []
190         add = SortedDict()
191         for media, css in raw_css.iteritems():
192             for selector_one, value_one in css.iteritems():
193                 newsel = selector_one
194
195                 for selector_two, value_two in css.iteritems():
196                     if selector_one == selector_two:
197                         #We need to skip self
198                         continue
199
200                     if value_one == value_two:
201                         #Ok, we need to merge these two selectors
202                         newsel += ', ' + selector_two
203                         delete.append((media, selector_two))
204
205
206         if not add.has_key(media):
207             add[media] = SortedDict()
208
209         add[media][newsel] = value_one
210         delete.append((media, selector_one))
211
212         for item in delete:
213             try:
214                 del self._optimized_css[item[0]][item[1]]
215             except:
216                 #Must have already been deleted
217                 continue
218
219         for media, css in add.iteritems():
220             self._optimized_css[media].update(css)
221
222
223
224     def __shorthand(self, value):
225         """
226             Compresses shorthand values. Example: margin:1px 1px 1px 1px . margin:1px
227             @value (string)
228         """
229
230         ##FIX##
231
232         important = '';
233         if self.parser.is_important(value):
234             value_list = self.parser.gvw_important(value)
235             important = '!important'
236         else:
237             value_list = value
238
239         ret = value
240         value_list = value_list.split(' ')
241
242         if len(value_list) == 4:
243             if value_list[0] == value_list[1] and value_list[0] == value_list[2] and value_list[0] == value_list[3]:
244                 ret = value_list[0] + important
245
246             elif value_list[1] == value_list[3] and value_list[0] == value_list[2]:
247                 ret = value_list[0] + ' ' + value_list[1] + important
248
249             elif value_list[1] == value_list[3]:
250                 ret = value_list[0] + ' ' + value_list[1] + ' ' + value_list[2] + important
251
252         elif len(value_list) == 3:
253             if value_list[0] == value_list[1] and value_list[0] == value_list[2]:
254                 ret = value_list[0] + important
255
256             elif value_list[0] == value_list[2]:
257                 return value_list[0] + ' ' + value_list[1] + important
258
259         elif len(value_list) == 2:
260             if value_list[0] == value_list[1]:
261                 ret = value_list[0] + important
262
263         if ret != value:
264             self.parser.log('Optimised shorthand notation: Changed "' + value + '" to "' + ret + '"', 'Information')
265
266         return ret
267
268     def __compress_important(self, value):
269         """
270             Removes unnecessary whitespace in ! important
271             @value (string)
272         """
273         if self.parser.is_important(value):
274             value = self.parser.gvw_important(value) + '!important'
275
276         return value
277
278     def __compress_numbers(self, prop, value):
279         """
280             Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
281             @value (string) is the posible number to be compressed
282         """
283
284         ##FIX##
285
286         value = value.split('/')
287
288         for l in xrange(len(value)):
289             #continue if no numeric value
290             if not (len(value[l]) > 0 and (value[l][0].isdigit() or value[l][0] in ('+', '-') )):
291                 continue
292
293             #Fix bad colors
294             if prop in data.color_values:
295                 value[l] = '#' + value[l]
296
297             is_floatable = False
298             try:
299                 float(value[l])
300                 is_floatable = True
301             except:
302                 pass
303
304             if is_floatable and float(value[l]) == 0:
305                 value[l] = '0'
306
307             elif value[l][0] != '#':
308                 unit_found = False
309                 for unit in data.units:
310                     pos = value[l].lower().find(unit)
311                     if pos != -1 and prop not in data.shorthands:
312                         value[l] = self.__remove_leading_zeros(float(value[l][:pos])) + unit
313                         unit_found = True
314                         break;
315
316                 if not unit_found and prop in data.unit_values and prop not in data.shorthands:
317                     value[l] = self.__remove_leading_zeros(float(value[l])) + 'px'
318
319                 elif not unit_found and prop not in data.shorthands:
320                     value[l] = self.__remove_leading_zeros(float(value[l]))
321
322
323         if len(value) > 1:
324             return '/'.join(value)
325
326         return value[0]
327
328     def __remove_leading_zeros(self, float_val):
329         """
330             Removes the leading zeros from a float value
331             @float_val (float)
332             @returns (string)
333         """
334         #Remove leading zero
335         if abs(float_val) < 1:
336             if float_val < 0:
337                 float_val = '-' . str(float_val)[2:]
338             else:
339                 float_val = str(float_val)[1:]
340
341         return str(float_val)
342
343     def __compress_color(self, color):
344         """
345             Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
346             @color (string) the {posible} color to change
347         """
348
349         #rgb(0,0,0) . #000000 (or #000 in this case later)
350         if color[:4].lower() == 'rgb(':
351             color_tmp = color[4:(len(color)-5)]
352             color_tmp = color_tmp.split(',')
353
354             for c in color_tmp:
355                 c = c.strip()
356                 if c[:-1] == '%':
357                     c = round((255*color_tmp[i])/100)
358
359                 if color_tmp[i] > 255:
360                     color_tmp[i] = 255
361
362             color = '#'
363
364             for i in xrange(3):
365                 if color_tmp[i] < 16:
366                     color += '0' + str(hex(color_tmp[i])).replace('0x', '')
367                 else:
368                     color += str(hex(color_tmp[i])).replace('0x', '')
369
370         #Fix bad color names
371         if data.replace_colors.has_key(color.lower()):
372             color = data.replace_colors[color.lower()]
373
374         #aabbcc . #abc
375         if len(color) == 7:
376             color_temp = color.lower()
377             if color_temp[0] == '#' and color_temp[1] == color_temp[2] and color_temp[3] == color_temp[4] and color_temp[5] == color_temp[6]:
378                 color = '#' + color[1] + color[3] + color[5]
379
380         if data.optimize_colors.has_key(color.lower()):
381             color = data.optimize_colors[color.lower()]
382
383         return color