#!/usr/bin/env python """ CSS processor for Galaxy style sheets. Supports the following features: - Nested rule definition - Mixins - Variable substitution in values """ import sys, string, os.path, os new_path = [ os.path.join( os.getcwd(), '..', '..', "lib" ) ] new_path.extend( sys.path[1:] ) # remove scripts/ from the path sys.path = new_path from galaxy import eggs import pkg_resources from galaxy.util.odict import odict from pyparsing import * #from odict import odict try: import Image except ImportError: from PIL import Image def cross_lists(*sets): """ Return the cross product of the arguments """ wheels = map(iter, sets) digits = [it.next() for it in wheels] while True: yield digits[:] for i in range(len(digits)-1, -1, -1): try: digits[i] = wheels[i].next() break except StopIteration: wheels[i] = iter(sets[i]) digits[i] = wheels[i].next() else: break def find_file( path, fname ): # Path can be a single directory or a ':' separated list if ':' in path: paths = path.split( ':' ) else: paths = [ path ] # Check in each directory for path in paths: fullname = os.path.join( path, fname ) if os.path.exists( fullname ): return fullname # Not found raise IOError( "File '%s' not found in path '%s'" % ( fname, paths ) ) def build_stylesheet_parser(): """ Returns a PyParsing parser object for CSS """ # Forward declerations for recursion rule = Forward() # Structural syntax, supressed from parser output lbrace = Literal("{").suppress() rbrace = Literal("}").suppress() colon = Literal(":").suppress() semi = Literal(";").suppress() ident = Word( alphas + "_", alphanums + "_-" ) # Properties prop_name = Word( alphas + "_-*", alphanums + "_-" ) prop_value = CharsNotIn( ";" ) # expand this as needed property_def = Group( prop_name + colon + prop_value + semi ).setResultsName( "property_def" ) # Selectors # Just match anything that looks like a selector, including element, class, # id, attribute, and pseudoclass. Attributes are not handled properly (spaces, # and even newlines in the quoted string are legal). simple_selector = Word( alphanums + "@.#*:()[]|=\"'_-" ) combinator = Literal( ">" ) | Literal( "+" ) selector = Group( simple_selector + ZeroOrMore( Optional( combinator ) + simple_selector ) ) selectors = Group( delimitedList( selector ) ) selector_mixin = Group( selector + semi ).setResultsName( "selector_mixin" ) # Rules rule << Group( selectors + lbrace + Group( ZeroOrMore( property_def | rule | selector_mixin ) ) + rbrace ).setResultsName( "rule" ) # A whole stylesheet stylesheet = ZeroOrMore( rule ) # C-style comments should be ignored, as should "##" comments stylesheet.ignore( cStyleComment ) stylesheet.ignore( "##" + restOfLine ) return stylesheet stylesheet_parser = build_stylesheet_parser() class CSSProcessor( object ): def process( self, file, out, variables, image_dir, out_dir ): # Build parse tree results = stylesheet_parser.parseFile( sys.stdin, parseAll=True ) # Expand rules (elimimate recursion and resolve mixins) rules = self.expand_rules( results ) # Expand variables (inplace) self.expand_variables( rules, variables ) # Do sprites self.make_sprites( rules, image_dir, out_dir ) # Print self.print_rules( rules, out ) def expand_rules( self, parse_results ): mixins = {} rules = [] # Visitor for recursively expanding rules def visitor( r, selector_prefixes ): # Concatenate combinations and build list of expanded selectors selectors = [ " ".join( s ) for s in r[0] ] full_selector_list = selector_prefixes + [selectors] full_selectors = [] for l in cross_lists( *full_selector_list ): full_selectors.append( " ".join( l ) ) # Separate properties from recursively defined rules properties = [] children = [] for dec in r[1]: type = dec.getName() if type == "property_def": properties.append( dec ) elif type == "selector_mixin": properties.extend( mixins[dec[0][0]] ) else: children.append( dec ) rules.append( ( full_selectors, properties ) ) # Save by name for mixins (not smart enough to combine rules!) for s in full_selectors: mixins[ s ] = properties; # Visit children for child in children: visitor( child, full_selector_list ) # Call at top level for p in parse_results: visitor( p, [] ) # Return the list of expanded rules return rules def expand_variables( self, rules, context ): for selectors, properties in rules: for p in properties: p[1] = string.Template( p[1] ).substitute( context ).strip() def make_sprites( self, rules, image_dir, out_dir ): pad = 10 class SpriteGroup( object ): def __init__( self, name ): self.name = name self.offset = 0 self.sprites = odict() def add_or_get_sprite( self, fname ): if fname in self.sprites: return self.sprites[fname] else: sprite = self.sprites[fname] = Sprite( fname, self.offset ) self.offset += sprite.image.size[1] + pad return sprite class Sprite( object ): def __init__( self, fname, offset ): self.fname = fname self.image = Image.open( find_file( image_dir, fname ) ) self.offset = offset sprite_groups = {} for i in range( len( rules ) ): properties = rules[i][1] new_properties = [] # Find sprite properties (and remove them). Last takes precedence sprite_group_name = None sprite_filename = None sprite_horiz_position = "0px" for name, value in properties: if name == "-sprite-group": sprite_group_name = value elif name == "-sprite-image": sprite_filename = value elif name == "-sprite-horiz-position": sprite_horiz_position = value else: new_properties.append( ( name, value ) ) # If a sprite filename was found, deal with it... if sprite_group_name and sprite_filename: if sprite_group_name not in sprite_groups: sprite_groups[sprite_group_name] = SpriteGroup( sprite_group_name ) sprite_group = sprite_groups[sprite_group_name] sprite = sprite_group.add_or_get_sprite( sprite_filename ) new_properties.append( ( "background", "url(%s.png) no-repeat %s -%dpx" % ( sprite_group.name, sprite_horiz_position, sprite.offset ) ) ) # Save changed properties rules[i] = ( rules[i][0], new_properties ) # Generate new images for group in sprite_groups.itervalues(): w = 0 h = 0 for sprite in group.sprites.itervalues(): sw, sh = sprite.image.size w = max( w, sw ) h += sh + pad master = Image.new( mode='RGBA', size=(w, h), color=(0,0,0,0) ) offset = 0 for sprite in group.sprites.itervalues(): master.paste( sprite.image, (0,offset) ) offset += sprite.image.size[1] + pad master.save( os.path.join( out_dir, group.name + ".png" ) ) def print_rules( self, rules, file ): for selectors, properties in rules: file.write( ",".join( selectors ) ) file.write( "{" ) for name, value in properties: file.write( "%s:%s;" % ( name, value ) ) file.write( "}\n" ) def main(): # Read variable definitions from a (sorta) ini file context = dict() for line in open( sys.argv[1] ): if line.startswith( '#' ): continue key, value = line.rstrip("\r\n").split( '=' ) if value.startswith( '"' ) and value.endswith( '"' ): value = value[1:-1] context[key] = value image_dir = sys.argv[2] out_dir = sys.argv[3] try: processor = CSSProcessor() processor.process( sys.stdin, sys.stdout, context, image_dir, out_dir ) except ParseException, e: print >> sys.stderr, "Error:", e print >> sys.stderr, e.markInputline() sys.exit( 1 ) if __name__ == "__main__": main()