mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
273 lines
9.4 KiB
Python
Executable File
273 lines
9.4 KiB
Python
Executable File
#!/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 LessTemplate( string.Template ):
|
|
delimeter = "@"
|
|
|
|
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()
|
|
# Less style uses @ to prefix variables
|
|
p[1] = LessTemplate( 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()
|