PageRenderTime 65ms CodeModel.GetById 13ms app.highlight 46ms RepoModel.GetById 1ms app.codeStats 0ms

/static/june_2007_style/process_css.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 267 lines | 180 code | 41 blank | 46 comment | 55 complexity | 77959e27fd4928d79a53540ade6c631f MD5 | raw file
  1#!/usr/bin/env python
  2
  3"""
  4CSS processor for Galaxy style sheets. Supports the following features:
  5
  6- Nested rule definition
  7- Mixins
  8- Variable substitution in values
  9
 10"""
 11
 12import sys, string, os.path, os
 13
 14new_path = [ os.path.join( os.getcwd(), '..', '..', "lib" ) ]
 15new_path.extend( sys.path[1:] ) # remove scripts/ from the path
 16sys.path = new_path
 17
 18from galaxy import eggs
 19import pkg_resources
 20from galaxy.util.odict import odict
 21
 22from pyparsing import *
 23#from odict import odict
 24
 25try:
 26    import Image
 27except ImportError:
 28    from PIL import Image
 29
 30def cross_lists(*sets):
 31    """
 32    Return the cross product of the arguments
 33    """
 34    wheels = map(iter, sets) 
 35    digits = [it.next() for it in wheels]
 36    while True:
 37        yield digits[:]
 38        for i in range(len(digits)-1, -1, -1):
 39            try:
 40                digits[i] = wheels[i].next()
 41                break
 42            except StopIteration:
 43                wheels[i] = iter(sets[i])
 44                digits[i] = wheels[i].next()
 45        else:
 46            break
 47        
 48def find_file( path, fname ):
 49    # Path can be a single directory or a ':' separated list
 50    if ':' in path:
 51        paths = path.split( ':' )
 52    else:
 53        paths = [ path ]
 54    # Check in each directory
 55    for path in paths:
 56        fullname = os.path.join( path, fname )
 57        if os.path.exists( fullname ):
 58            return fullname
 59    # Not found
 60    raise IOError( "File '%s' not found in path '%s'" % ( fname, paths ) )
 61
 62def build_stylesheet_parser():
 63    """
 64    Returns a PyParsing parser object for CSS
 65    """
 66
 67    # Forward declerations for recursion
 68    rule = Forward()
 69    
 70    # Structural syntax, supressed from parser output
 71    lbrace = Literal("{").suppress()
 72    rbrace = Literal("}").suppress()
 73    colon  = Literal(":").suppress()
 74    semi   = Literal(";").suppress()
 75    
 76    ident = Word( alphas + "_", alphanums + "_-" ) 
 77    
 78    # Properties
 79    prop_name  = Word( alphas + "_-*", alphanums + "_-" ) 
 80    prop_value  = CharsNotIn( ";" )  # expand this as needed
 81    property_def = Group( prop_name + colon + prop_value + semi ).setResultsName( "property_def" )
 82    
 83    # Selectors
 84    #   Just match anything that looks like a selector, including element, class,
 85    #   id, attribute, and pseudoclass. Attributes are not handled properly (spaces,
 86    #   and even newlines in the quoted string are legal).
 87    simple_selector = Word( alphanums + "@.#*:()[]|=\"'_-" )
 88    combinator = Literal( ">" ) | Literal( "+" )
 89    selector = Group( simple_selector + ZeroOrMore( Optional( combinator ) + simple_selector ) )
 90    selectors = Group( delimitedList( selector ) )
 91    
 92    selector_mixin = Group( selector + semi ).setResultsName( "selector_mixin" )
 93    
 94    # Rules
 95    rule << Group( selectors +
 96                   lbrace +
 97                   Group( ZeroOrMore( property_def | rule | selector_mixin ) ) + 
 98                   rbrace ).setResultsName( "rule" )
 99    
100    # A whole stylesheet
101    stylesheet = ZeroOrMore( rule )
102    
103    # C-style comments should be ignored, as should "##" comments
104    stylesheet.ignore( cStyleComment )
105    stylesheet.ignore( "##" + restOfLine )
106    
107    return stylesheet
108
109stylesheet_parser = build_stylesheet_parser()
110
111class CSSProcessor( object ):
112    
113    def process( self, file, out, variables, image_dir, out_dir ):
114        # Build parse tree
115        results = stylesheet_parser.parseFile( sys.stdin, parseAll=True )
116        # Expand rules (elimimate recursion and resolve mixins)
117        rules = self.expand_rules( results )
118        # Expand variables (inplace)
119        self.expand_variables( rules, variables )
120        # Do sprites
121        self.make_sprites( rules, image_dir, out_dir )
122        # Print
123        self.print_rules( rules, out )
124        
125    def expand_rules( self, parse_results ):
126        mixins = {}
127        rules = []
128        # Visitor for recursively expanding rules
129        def visitor( r, selector_prefixes ):
130            # Concatenate combinations and build list of expanded selectors
131            selectors = [ " ".join( s ) for s in r[0] ]
132            full_selector_list = selector_prefixes + [selectors] 
133            full_selectors = []
134            for l in cross_lists( *full_selector_list ):
135                full_selectors.append(  " ".join( l ) )
136            # Separate properties from recursively defined rules
137            properties = []
138            children = []
139            for dec in r[1]:
140                type = dec.getName()
141                if type == "property_def":
142                    properties.append( dec )
143                elif type == "selector_mixin":
144                    properties.extend( mixins[dec[0][0]] )
145                else:
146                    children.append( dec )
147            rules.append( ( full_selectors, properties ) )
148            # Save by name for mixins (not smart enough to combine rules!)        
149            for s in full_selectors:
150                mixins[ s ] = properties;
151            # Visit children
152            for child in children:
153                visitor( child, full_selector_list )
154        # Call at top level
155        for p in parse_results:
156            visitor( p, [] )
157        # Return the list of expanded rules
158        return rules
159            
160    def expand_variables( self, rules, context ):
161        for selectors, properties in rules:
162            for p in properties:
163                p[1] = string.Template( p[1] ).substitute( context ).strip()
164    
165    def make_sprites( self, rules, image_dir, out_dir ):
166        
167        pad = 10
168        
169        class SpriteGroup( object ):
170            def __init__( self, name ):
171                self.name = name
172                self.offset = 0
173                self.sprites = odict()
174            def add_or_get_sprite( self, fname ):
175                if fname in self.sprites:
176                    return self.sprites[fname]
177                else:
178                    sprite = self.sprites[fname] = Sprite( fname, self.offset )
179                    self.offset += sprite.image.size[1] + pad
180                    return sprite
181        
182        class Sprite( object ):
183            def __init__( self, fname, offset ):
184                self.fname = fname
185                self.image = Image.open( find_file( image_dir, fname ) )
186                self.offset = offset
187                
188        sprite_groups = {}
189        
190        for i in range( len( rules ) ):
191            properties = rules[i][1]
192            new_properties = []
193            # Find sprite properties (and remove them). Last takes precedence
194            sprite_group_name = None
195            sprite_filename = None
196            sprite_horiz_position = "0px"
197            for name, value in properties:
198                if name == "-sprite-group":
199                    sprite_group_name = value
200                elif name == "-sprite-image":
201                    sprite_filename = value
202                elif name == "-sprite-horiz-position":
203                    sprite_horiz_position = value
204                else:
205                    new_properties.append( ( name, value ) )
206            # If a sprite filename was found, deal with it... 
207            if sprite_group_name and sprite_filename:
208                if sprite_group_name not in sprite_groups:
209                    sprite_groups[sprite_group_name] = SpriteGroup( sprite_group_name )
210                sprite_group = sprite_groups[sprite_group_name]
211                sprite = sprite_group.add_or_get_sprite( sprite_filename )
212                new_properties.append( ( "background", "url(%s.png) no-repeat %s -%dpx" % ( sprite_group.name, sprite_horiz_position, sprite.offset ) ) )
213            # Save changed properties
214            rules[i] = ( rules[i][0], new_properties )
215        
216        # Generate new images
217        for group in sprite_groups.itervalues():
218            w = 0
219            h = 0
220            for sprite in group.sprites.itervalues():
221                sw, sh = sprite.image.size
222                w = max( w, sw )
223                h += sh + pad
224            master = Image.new( mode='RGBA', size=(w, h), color=(0,0,0,0) )
225            offset = 0
226            for sprite in group.sprites.itervalues():
227                master.paste( sprite.image, (0,offset) )
228                offset += sprite.image.size[1] + pad
229            master.save( os.path.join( out_dir, group.name + ".png" ) )
230            
231    def print_rules( self, rules, file ):
232        for selectors, properties in rules:
233            file.write( ",".join( selectors ) )
234            file.write( "{" )
235            for name, value in properties:
236                file.write( "%s:%s;" % ( name, value ) )
237            file.write( "}\n" )
238
239def main():
240
241    # Read variable definitions from a (sorta) ini file
242    context = dict()
243    for line in open( sys.argv[1] ):
244        if line.startswith( '#' ):
245            continue
246        key, value = line.rstrip("\r\n").split( '=' )
247        if value.startswith( '"' ) and value.endswith( '"' ):
248            value = value[1:-1]
249        context[key] = value
250        
251    image_dir = sys.argv[2]
252    out_dir = sys.argv[3]
253
254    try:
255        
256        processor = CSSProcessor()
257        processor.process( sys.stdin, sys.stdout, context, image_dir, out_dir )
258        
259    except ParseException, e:
260        
261        print >> sys.stderr, "Error:", e
262        print >> sys.stderr, e.markInputline()
263        sys.exit( 1 )
264    
265
266if __name__ == "__main__":
267    main()