|
|
@@ -0,0 +1,132 @@
|
|
|
+#!/usr/bin/env python
|
|
|
+from block import Block
|
|
|
+
|
|
|
+
|
|
|
+def split_selectors(raw_selector):
|
|
|
+ """
|
|
|
+ Split a selector with commas and arbitrary whitespaces into a list of
|
|
|
+ selector swith single-space whitespaces.
|
|
|
+ """
|
|
|
+ return [' '.join(s.split()) for s in raw_selector.split(',')]
|
|
|
+
|
|
|
+
|
|
|
+class StyleSheet(object):
|
|
|
+ def __init__(self, css=None):
|
|
|
+ # List of encountered blocks
|
|
|
+ self.blocks = []
|
|
|
+
|
|
|
+ # Map of property stringification to list of containing blocks
|
|
|
+ self.property_locations = {}
|
|
|
+
|
|
|
+ if css:
|
|
|
+ self.parse_css(css)
|
|
|
+
|
|
|
+ def parse_css(self, css):
|
|
|
+ """
|
|
|
+ Parse CSS code one character at a time. This is more efficient than
|
|
|
+ simply splitting on brackets, especially for large style sheets. All
|
|
|
+ comments are ignored (both inline and multiline).
|
|
|
+ """
|
|
|
+ stack = char = ''
|
|
|
+ prev = block = None
|
|
|
+ lineno = 1
|
|
|
+ multiline_comment = inline_comment = False
|
|
|
+
|
|
|
+ try:
|
|
|
+ for c in css:
|
|
|
+ char = c
|
|
|
+
|
|
|
+ if multiline_comment:
|
|
|
+ # Multiline comment end?
|
|
|
+ if c == '/' and prev == '*':
|
|
|
+ multiline_comment = False
|
|
|
+ elif inline_comment:
|
|
|
+ # Inline comment end?
|
|
|
+ if c == '\n':
|
|
|
+ inline_comment = False
|
|
|
+ elif c == '{':
|
|
|
+ # Block start
|
|
|
+ selectors = split_selectors(stack)
|
|
|
+ stack = ''
|
|
|
+ assert len(selectors)
|
|
|
+ block = Block(*selectors)
|
|
|
+ elif c == '}':
|
|
|
+ # Block end
|
|
|
+ assert block is not None
|
|
|
+ block = None
|
|
|
+ elif c == ';':
|
|
|
+ # Property definition
|
|
|
+ assert block is not None
|
|
|
+ name, value = map(str.strip, stack.split(':', 1))
|
|
|
+ assert '\n' not in name
|
|
|
+ block.add_property(name, value)
|
|
|
+ elif c == '*' and prev == '/':
|
|
|
+ # Multiline comment start
|
|
|
+ multiline_comment = True
|
|
|
+ elif c == '/' and prev == '/':
|
|
|
+ # Inline comment start
|
|
|
+ inline_comment = True
|
|
|
+ else:
|
|
|
+ if c == '\n':
|
|
|
+ lineno += 1
|
|
|
+
|
|
|
+ stack += c
|
|
|
+ prev = c
|
|
|
+ except AssertionError:
|
|
|
+ raise Exception('unexpected \'%c\' on line %d' % (char, lineno))
|
|
|
+
|
|
|
+ def generate_css(self, compress_whitespace=False, compress_color=False,
|
|
|
+ compress_font=False, compress_dimension=False,
|
|
|
+ compress_blocks=False):
|
|
|
+ """
|
|
|
+ Generate CSS code for the entire stylesheet.
|
|
|
+
|
|
|
+ Options:
|
|
|
+ compress_whitespace | Omit unnecessary whitespaces and semicolons.
|
|
|
+ compress_color | Replace color codes/names with shorter synonyms.
|
|
|
+ compress_font | Replace separate font statements with shortcut
|
|
|
+ | font statement where possible.
|
|
|
+ compress_dimension | Replace separate margin/padding statements with
|
|
|
+ | shortcut statements where possible.
|
|
|
+ compress_blocks | Combine or split blocks into blocks with
|
|
|
+ | comma-separated selectors if it results in less
|
|
|
+ | CSS code.
|
|
|
+ """
|
|
|
+ blocks = self.blocks
|
|
|
+
|
|
|
+ if compress_blocks:
|
|
|
+ blocks = self._compress_blocks()
|
|
|
+
|
|
|
+ options = dict(compress_whitespace=compress_whitespace,
|
|
|
+ compress_color=compress_color,
|
|
|
+ compress_font=compress_font,
|
|
|
+ compress_dimension=compress_dimension)
|
|
|
+ newline = '' if compress_whitespace else '\n'
|
|
|
+
|
|
|
+ return newline.join(block.generate_css(**options)
|
|
|
+ for block in self.blocks)
|
|
|
+
|
|
|
+ def _compress_blocks(self):
|
|
|
+ pass
|
|
|
+ #for block in self.blocks
|
|
|
+
|
|
|
+ def compress(self, **kwargs):
|
|
|
+ """
|
|
|
+ Shortcut for `generate_css`, with all compression options enabled by
|
|
|
+ default. Keyword argument names are preceded by 'compress_' before
|
|
|
+ being passed to `generate_css`.
|
|
|
+ """
|
|
|
+ options = dict(whitespace=True, color=True, font=True, dimension=True,
|
|
|
+ blocks=True)
|
|
|
+ options.update(kwargs)
|
|
|
+ options = dict([('compress_' + k, v) for k, v in options.items()])
|
|
|
+
|
|
|
+ return self.generate_css(**options)
|
|
|
+
|
|
|
+ def __str__(self):
|
|
|
+ return self.generate_css()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__': # pragma: nocover
|
|
|
+ # TODO: Command-line options parser
|
|
|
+ pass
|