--- /dev/null
+# CSSTidy - CSS Optimizer
+#
+# CSS Optimizer class
+#
+# This file is part of CSSTidy.
+#
+# CSSTidy is free software you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation either version 2 of the License, or
+# (at your option) any later version.
+#
+# CSSTidy is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with CSSTidy if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# @license http://opensource.org/licenses/gpl-license.php GNU Public License
+# @package csstidy
+# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
+
+import data
+from tools import SortedDict
+
+
+class CSSOptimizer(object):
+ def __init__(self, parser):
+ #raw_css is a dict
+ self.parser = parser
+ self._optimized_css = SortedDict
+
+
+#PUBLIC METHODS
+ def optimize(self, raw_css):
+ if self.parser.getSetting('preserve_css'):
+ return raw_css
+
+ self._optimized_css = raw_css
+
+ if self.parser.getSetting('merge_selectors') == 2:
+ self.__merge_selectors()
+
+ ##OPTIMIZE##
+ for media, css in self._optimized_css.iteritems():
+ for selector, cssdata in css.iteritems():
+ if self.parser.getSetting('optimise_shorthands') >= 1:
+ cssdata = self.__merge_4value_shorthands(cssdata)
+
+ if self.parser.getSetting('optimise_shorthands') >= 2:
+ cssdata = self.__merge_bg(cssdata)
+
+ for item, value in cssdata.iteritems():
+ value = self.__compress_numbers(item, value)
+ value = self.__compress_important(value)
+
+ if item in data.color_values and self.parser.getSetting('compress_colors'):
+ old = value[:]
+ value = self.__compress_color(value)
+ if old != value:
+ self.parser.log('In "' + selector + '" Optimised ' + item + ': Changed ' + old + ' to ' + value, 'Information')
+
+ if item == 'font-weight' and self.parser.getSetting('compress_font-weight'):
+ if value == 'bold':
+ value = '700'
+ self.parser.log('In "' + selector + '" Optimised font-weight: Changed "bold" to "700"', 'Information')
+
+ elif value == 'normal':
+ value = '400'
+ self.parser.log('In "' + selector + '" Optimised font-weight: Changed "normal" to "400"', 'Information')
+
+ self._optimized_css[media][selector][item] = value
+
+
+ return self._optimized_css
+
+
+#PRIVATE METHODS
+ def __merge_bg(self, cssdata):
+ """
+ Merges all background properties
+ @cssdata (dict) is a dictionary of the selector properties
+ """
+ #Max number of background images. CSS3 not yet fully implemented
+ img = 1
+ clr = 1
+ bg_img_list = []
+ if cssdata.has_key('background-image'):
+ img = len(cssdata['background-image'].split(','))
+ bg_img_list = self.parser.gvw_important(cssdata['background-image']).split(',')
+
+ elif cssdata.has_key('background-color'):
+ clr = len(cssdata['background-color'].split(','))
+
+
+ number_of_values = max(img, clr, 1)
+
+ new_bg_value = ''
+ important = ''
+
+ for i in xrange(number_of_values):
+ for bg_property, default_value in data.background_prop_default.iteritems():
+ #Skip if property does not exist
+ if not cssdata.has_key(bg_property):
+ continue
+
+ cur_value = cssdata[bg_property]
+
+ #Skip some properties if there is no background image
+ 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']):
+ continue
+
+ #Remove !important
+ if self.parser.is_important(cur_value):
+ important = ' !important'
+ cur_value = self.parser.gvw_important(cur_value)
+
+ #Do not add default values
+ if cur_value == default_value:
+ continue
+
+ temp = cur_value.split(',')
+
+ if len(temp) > i:
+ if bg_property == 'background-size':
+ new_bg_value += '(' + temp[i] + ') '
+
+ else:
+ new_bg_value += temp[i] + ' '
+
+ new_bg_value = new_bg_value.strip()
+ if i != (number_of_values-1):
+ new_bg_value += ','
+
+ #Delete all background-properties
+ for bg_property, default_value in data.background_prop_default.iteritems():
+ try:
+ del cssdata[bg_property]
+ except:
+ pass
+
+ #Add new background property
+ if new_bg_value != '':
+ cssdata['background'] = new_bg_value + important
+
+ return cssdata
+
+ def __merge_4value_shorthands(self, cssdata):
+ """
+ Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
+ @cssdata (dict) is a dictionary of the selector properties
+ """
+ for key, value in data.shorthands.iteritems():
+ important = ''
+ 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]):
+ cssdata[key] = ''
+
+ for i in xrange(4):
+ val = cssdata[value[i]]
+ if self.parser.is_important(val):
+ important = '!important'
+ cssdata[key] += self.parser.gvw_important(val) + ' '
+
+ else:
+ cssdata[key] += val + ' '
+
+ del cssdata[value[i]]
+ if cssdata.has_key(key):
+ cssdata[key] = self.__shorthand(cssdata[key] + important.strip())
+
+ return cssdata
+
+
+ def __merge_selectors(self):
+ """
+ Merges selectors with same properties. Example: a{color:red} b{color:red} . a,b{color:red}
+ Very basic and has at least one bug. Hopefully there is a replacement soon.
+ @selector_one (string) is the current selector
+ @value_one (dict) is a dictionary of the selector properties
+ Note: Currently is the elements of a selector are identical, but in a different order, they are not merged
+ """
+
+ ##OPTIMIZE##
+ ##FIX##
+
+ raw_css = self._optimized_css.copy()
+ delete = []
+ add = SortedDict()
+ for media, css in raw_css.iteritems():
+ for selector_one, value_one in css.iteritems():
+ newsel = selector_one
+
+ for selector_two, value_two in css.iteritems():
+ if selector_one == selector_two:
+ #We need to skip self
+ continue
+
+ if value_one == value_two:
+ #Ok, we need to merge these two selectors
+ newsel += ', ' + selector_two
+ delete.append((media, selector_two))
+
+
+ if not add.has_key(media):
+ add[media] = SortedDict()
+
+ add[media][newsel] = value_one
+ delete.append((media, selector_one))
+
+ for item in delete:
+ try:
+ del self._optimized_css[item[0]][item[1]]
+ except:
+ #Must have already been deleted
+ continue
+
+ for media, css in add.iteritems():
+ self._optimized_css[media].update(css)
+
+
+
+ def __shorthand(self, value):
+ """
+ Compresses shorthand values. Example: margin:1px 1px 1px 1px . margin:1px
+ @value (string)
+ """
+
+ ##FIX##
+
+ important = '';
+ if self.parser.is_important(value):
+ value_list = self.parser.gvw_important(value)
+ important = '!important'
+ else:
+ value_list = value
+
+ ret = value
+ value_list = value_list.split(' ')
+
+ if len(value_list) == 4:
+ if value_list[0] == value_list[1] and value_list[0] == value_list[2] and value_list[0] == value_list[3]:
+ ret = value_list[0] + important
+
+ elif value_list[1] == value_list[3] and value_list[0] == value_list[2]:
+ ret = value_list[0] + ' ' + value_list[1] + important
+
+ elif value_list[1] == value_list[3]:
+ ret = value_list[0] + ' ' + value_list[1] + ' ' + value_list[2] + important
+
+ elif len(value_list) == 3:
+ if value_list[0] == value_list[1] and value_list[0] == value_list[2]:
+ ret = value_list[0] + important
+
+ elif value_list[0] == value_list[2]:
+ return value_list[0] + ' ' + value_list[1] + important
+
+ elif len(value_list) == 2:
+ if value_list[0] == value_list[1]:
+ ret = value_list[0] + important
+
+ if ret != value:
+ self.parser.log('Optimised shorthand notation: Changed "' + value + '" to "' + ret + '"', 'Information')
+
+ return ret
+
+ def __compress_important(self, value):
+ """
+ Removes unnecessary whitespace in ! important
+ @value (string)
+ """
+ if self.parser.is_important(value):
+ value = self.parser.gvw_important(value) + '!important'
+
+ return value
+
+ def __compress_numbers(self, prop, value):
+ """
+ Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
+ @value (string) is the posible number to be compressed
+ """
+
+ ##FIX##
+
+ value = value.split('/')
+
+ for l in xrange(len(value)):
+ #continue if no numeric value
+ if not (len(value[l]) > 0 and (value[l][0].isdigit() or value[l][0] in ('+', '-') )):
+ continue
+
+ #Fix bad colors
+ if prop in data.color_values:
+ value[l] = '#' + value[l]
+
+ is_floatable = False
+ try:
+ float(value[l])
+ is_floatable = True
+ except:
+ pass
+
+ if is_floatable and float(value[l]) == 0:
+ value[l] = '0'
+
+ elif value[l][0] != '#':
+ unit_found = False
+ for unit in data.units:
+ pos = value[l].lower().find(unit)
+ if pos != -1 and prop not in data.shorthands:
+ value[l] = self.__remove_leading_zeros(float(value[l][:pos])) + unit
+ unit_found = True
+ break;
+
+ if not unit_found and prop in data.unit_values and prop not in data.shorthands:
+ value[l] = self.__remove_leading_zeros(float(value[l])) + 'px'
+
+ elif not unit_found and prop not in data.shorthands:
+ value[l] = self.__remove_leading_zeros(float(value[l]))
+
+
+ if len(value) > 1:
+ return '/'.join(value)
+
+ return value[0]
+
+ def __remove_leading_zeros(self, float_val):
+ """
+ Removes the leading zeros from a float value
+ @float_val (float)
+ @returns (string)
+ """
+ #Remove leading zero
+ if abs(float_val) < 1:
+ if float_val < 0:
+ float_val = '-' . str(float_val)[2:]
+ else:
+ float_val = str(float_val)[1:]
+
+ return str(float_val)
+
+ def __compress_color(self, color):
+ """
+ Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
+ @color (string) the {posible} color to change
+ """
+
+ #rgb(0,0,0) . #000000 (or #000 in this case later)
+ if color[:4].lower() == 'rgb(':
+ color_tmp = color[4:(len(color)-5)]
+ color_tmp = color_tmp.split(',')
+
+ for c in color_tmp:
+ c = c.strip()
+ if c[:-1] == '%':
+ c = round((255*color_tmp[i])/100)
+
+ if color_tmp[i] > 255:
+ color_tmp[i] = 255
+
+ color = '#'
+
+ for i in xrange(3):
+ if color_tmp[i] < 16:
+ color += '0' + str(hex(color_tmp[i])).replace('0x', '')
+ else:
+ color += str(hex(color_tmp[i])).replace('0x', '')
+
+ #Fix bad color names
+ if data.replace_colors.has_key(color.lower()):
+ color = data.replace_colors[color.lower()]
+
+ #aabbcc . #abc
+ if len(color) == 7:
+ color_temp = color.lower()
+ 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]:
+ color = '#' + color[1] + color[3] + color[5]
+
+ if data.optimize_colors.has_key(color.lower()):
+ color = data.optimize_colors[color.lower()]
+
+ return color
\ No newline at end of file