csscomp.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #!/usr/bin/env python
  2. from block import Block
  3. def split_selectors(raw_selector):
  4. """
  5. Split a selector with commas and arbitrary whitespaces into a list of
  6. selector swith single-space whitespaces.
  7. """
  8. return [' '.join(s.split()) for s in raw_selector.split(',')]
  9. class StyleSheet(object):
  10. def __init__(self, css=None):
  11. # List of encountered blocks
  12. self.blocks = []
  13. # Map of property stringification to list of containing blocks
  14. self.property_locations = {}
  15. if css:
  16. self.parse_css(css)
  17. def parse_css(self, css):
  18. """
  19. Parse CSS code one character at a time. This is more efficient than
  20. simply splitting on brackets, especially for large style sheets. All
  21. comments are ignored (both inline and multiline).
  22. """
  23. stack = char = ''
  24. prev = block = None
  25. lineno = 1
  26. multiline_comment = inline_comment = False
  27. try:
  28. for c in css:
  29. char = c
  30. if multiline_comment:
  31. # Multiline comment end?
  32. if c == '/' and prev == '*':
  33. multiline_comment = False
  34. elif inline_comment:
  35. # Inline comment end?
  36. if c == '\n':
  37. inline_comment = False
  38. elif c == '{':
  39. # Block start
  40. selectors = split_selectors(stack)
  41. stack = ''
  42. assert len(selectors)
  43. block = Block(*selectors)
  44. elif c == '}':
  45. # Block end
  46. assert block is not None
  47. block = None
  48. elif c == ';':
  49. # Property definition
  50. assert block is not None
  51. name, value = map(str.strip, stack.split(':', 1))
  52. assert '\n' not in name
  53. block.add_property(name, value)
  54. elif c == '*' and prev == '/':
  55. # Multiline comment start
  56. multiline_comment = True
  57. elif c == '/' and prev == '/':
  58. # Inline comment start
  59. inline_comment = True
  60. else:
  61. if c == '\n':
  62. lineno += 1
  63. stack += c
  64. prev = c
  65. except AssertionError:
  66. raise Exception('unexpected \'%c\' on line %d' % (char, lineno))
  67. def generate_css(self, compress_whitespace=False, compress_color=False,
  68. compress_font=False, compress_dimension=False,
  69. compress_blocks=False):
  70. """
  71. Generate CSS code for the entire stylesheet.
  72. Options:
  73. compress_whitespace | Omit unnecessary whitespaces and semicolons.
  74. compress_color | Replace color codes/names with shorter synonyms.
  75. compress_font | Replace separate font statements with shortcut
  76. | font statement where possible.
  77. compress_dimension | Replace separate margin/padding statements with
  78. | shortcut statements where possible.
  79. compress_blocks | Combine or split blocks into blocks with
  80. | comma-separated selectors if it results in less
  81. | CSS code.
  82. """
  83. blocks = self.blocks
  84. if compress_blocks:
  85. blocks = self._compress_blocks()
  86. options = dict(compress_whitespace=compress_whitespace,
  87. compress_color=compress_color,
  88. compress_font=compress_font,
  89. compress_dimension=compress_dimension)
  90. newline = '' if compress_whitespace else '\n'
  91. return newline.join(block.generate_css(**options)
  92. for block in self.blocks)
  93. def _compress_blocks(self):
  94. pass
  95. #for block in self.blocks
  96. def compress(self, **kwargs):
  97. """
  98. Shortcut for `generate_css`, with all compression options enabled by
  99. default. Keyword argument names are preceded by 'compress_' before
  100. being passed to `generate_css`.
  101. """
  102. options = dict(whitespace=True, color=True, font=True, dimension=True,
  103. blocks=True)
  104. options.update(kwargs)
  105. options = dict([('compress_' + k, v) for k, v in options.items()])
  106. return self.generate_css(**options)
  107. def __str__(self):
  108. return self.generate_css()
  109. if __name__ == '__main__': # pragma: nocover
  110. # TODO: Command-line options parser
  111. pass