/static/june_2007_style/process_css.py

https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 267 lines · 180 code · 41 blank · 46 comment · 46 complexity · 77959e27fd4928d79a53540ade6c631f MD5 · raw file

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