1 # CSSTidy - CSS Optimizer
5 # This file is part of CSSTidy.
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.
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.
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
21 # @license http://opensource.org/licenses/gpl-license.php GNU Public License
23 # @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
26 from tools import SortedDict
29 class CSSOptimizer(object):
30 def __init__(self, parser):
33 self._optimized_css = SortedDict
37 def optimize(self, raw_css):
38 if self.parser.getSetting('preserve_css'):
41 self._optimized_css = raw_css
43 if self.parser.getSetting('merge_selectors') == 2:
44 self.__merge_selectors()
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)
52 if self.parser.getSetting('optimise_shorthands') >= 2:
53 cssdata = self.__merge_bg(cssdata)
55 for item, value in cssdata.iteritems():
56 value = self.__compress_numbers(item, value)
57 value = self.__compress_important(value)
59 if item in data.color_values and self.parser.getSetting('compress_colors'):
61 value = self.__compress_color(value)
63 self.parser.log('In "' + selector + '" Optimised ' + item + ': Changed ' + old + ' to ' + value, 'Information')
65 if item == 'font-weight' and self.parser.getSetting('compress_font-weight'):
68 self.parser.log('In "' + selector + '" Optimised font-weight: Changed "bold" to "700"', 'Information')
70 elif value == 'normal':
72 self.parser.log('In "' + selector + '" Optimised font-weight: Changed "normal" to "400"', 'Information')
74 self._optimized_css[media][selector][item] = value
77 return self._optimized_css
81 def __merge_bg(self, cssdata):
83 Merges all background properties
84 @cssdata (dict) is a dictionary of the selector properties
86 #Max number of background images. CSS3 not yet fully implemented
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(',')
94 elif cssdata.has_key('background-color'):
95 clr = len(cssdata['background-color'].split(','))
98 number_of_values = max(img, clr, 1)
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):
109 cur_value = cssdata[bg_property]
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']):
116 if self.parser.is_important(cur_value):
117 important = ' !important'
118 cur_value = self.parser.gvw_important(cur_value)
120 #Do not add default values
121 if cur_value == default_value:
124 temp = cur_value.split(',')
127 if bg_property == 'background-size':
128 new_bg_value += '(' + temp[i] + ') '
131 new_bg_value += temp[i] + ' '
133 new_bg_value = new_bg_value.strip()
134 if i != (number_of_values-1):
137 #Delete all background-properties
138 for bg_property, default_value in data.background_prop_default.iteritems():
140 del cssdata[bg_property]
144 #Add new background property
145 if new_bg_value != '':
146 cssdata['background'] = new_bg_value + important
150 def __merge_4value_shorthands(self, cssdata):
152 Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
153 @cssdata (dict) is a dictionary of the selector properties
155 for key, value in data.shorthands.iteritems():
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]):
161 val = cssdata[value[i]]
162 if self.parser.is_important(val):
163 important = '!important'
164 cssdata[key] += self.parser.gvw_important(val) + ' '
167 cssdata[key] += val + ' '
169 del cssdata[value[i]]
170 if cssdata.has_key(key):
171 cssdata[key] = self.__shorthand(cssdata[key] + important.strip())
176 def __merge_selectors(self):
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
188 raw_css = self._optimized_css.copy()
191 for media, css in raw_css.iteritems():
192 for selector_one, value_one in css.iteritems():
193 newsel = selector_one
195 for selector_two, value_two in css.iteritems():
196 if selector_one == selector_two:
197 #We need to skip self
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))
206 if not add.has_key(media):
207 add[media] = SortedDict()
209 add[media][newsel] = value_one
210 delete.append((media, selector_one))
214 del self._optimized_css[item[0]][item[1]]
216 #Must have already been deleted
219 for media, css in add.iteritems():
220 self._optimized_css[media].update(css)
224 def __shorthand(self, value):
226 Compresses shorthand values. Example: margin:1px 1px 1px 1px . margin:1px
233 if self.parser.is_important(value):
234 value_list = self.parser.gvw_important(value)
235 important = '!important'
240 value_list = value_list.split(' ')
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
246 elif value_list[1] == value_list[3] and value_list[0] == value_list[2]:
247 ret = value_list[0] + ' ' + value_list[1] + important
249 elif value_list[1] == value_list[3]:
250 ret = value_list[0] + ' ' + value_list[1] + ' ' + value_list[2] + important
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
256 elif value_list[0] == value_list[2]:
257 return value_list[0] + ' ' + value_list[1] + important
259 elif len(value_list) == 2:
260 if value_list[0] == value_list[1]:
261 ret = value_list[0] + important
264 self.parser.log('Optimised shorthand notation: Changed "' + value + '" to "' + ret + '"', 'Information')
268 def __compress_important(self, value):
270 Removes unnecessary whitespace in ! important
273 if self.parser.is_important(value):
274 value = self.parser.gvw_important(value) + '!important'
278 def __compress_numbers(self, prop, value):
280 Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
281 @value (string) is the posible number to be compressed
286 value = value.split('/')
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 ('+', '-') )):
294 if prop in data.color_values:
295 value[l] = '#' + value[l]
304 if is_floatable and float(value[l]) == 0:
307 elif value[l][0] != '#':
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
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'
319 elif not unit_found and prop not in data.shorthands:
320 value[l] = self.__remove_leading_zeros(float(value[l]))
324 return '/'.join(value)
328 def __remove_leading_zeros(self, float_val):
330 Removes the leading zeros from a float value
335 if abs(float_val) < 1:
337 float_val = '-' . str(float_val)[2:]
339 float_val = str(float_val)[1:]
341 return str(float_val)
343 def __compress_color(self, color):
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
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(',')
357 c = round((255*color_tmp[i])/100)
359 if color_tmp[i] > 255:
365 if color_tmp[i] < 16:
366 color += '0' + str(hex(color_tmp[i])).replace('0x', '')
368 color += str(hex(color_tmp[i])).replace('0x', '')
371 if data.replace_colors.has_key(color.lower()):
372 color = data.replace_colors[color.lower()]
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]
380 if data.optimize_colors.has_key(color.lower()):
381 color = data.optimize_colors[color.lower()]