Переглянути джерело

Reordered file structure and added support for media queries in the parser

Taddeus Kroes 13 роки тому
батько
коміт
8912c7b183
10 змінених файлів з 326 додано та 277 видалено
  1. 0 62
      block.py
  2. 7 0
      compress.py
  3. 83 0
      csscom.py
  4. 0 132
      csscomp.py
  5. 94 0
      generate.py
  6. 78 0
      parse.py
  7. 0 80
      tests/test_block.py
  8. 5 1
      tests/test_compress.py
  9. 57 0
      tests/test_generate.py
  10. 2 2
      tests/test_parse.py

+ 0 - 62
block.py

@@ -1,62 +0,0 @@
-import properties as props
-
-
-class Block(object):
-    """
-    A Block is a stylesheet block of the following form:
-
-    <selector>,
-    ... {
-        <property>: <value>;
-        ...
-    }
-    """
-    def __init__(self, *args):
-        self.selectors = sorted(set(args))
-        self.properties = set()
-
-    def add_property(self, name, value):
-        self.properties.add((name, value))
-
-    def generate_css(self, compress_whitespace=False, compress_color=False,
-                     compress_font=False, compress_dimension=False):
-        if compress_whitespace:
-            comma = ','
-            colon = ':'
-            newline = ''
-            lbracket = '{'
-            rbracket = '}'
-        else:
-            comma = ',\n'
-            colon = ': '
-            newline = '\n\t'
-            lbracket = ' {'
-            rbracket = '\n}'
-
-        properties = self.properties
-
-        if compress_color:
-            properties = props.compress_color(properties)
-
-        if compress_font:
-            properties = props.compress_font(properties)
-
-        if compress_dimension:
-            properties = props.compress_dimension(properties)
-
-        selector = comma.join(self.selectors)
-        properties = [newline + name + colon + value
-                      for name, value in self.properties]
-        inner = ';'.join(properties)
-
-        if len(properties) and not compress_whitespace:
-            inner += ';'
-
-        return selector + lbracket + inner + rbracket
-
-    def __repr__(self):  # pragma: nocover
-        return '<Block "%s" properties=%d>' \
-               % (', '.join(self.selectors), len(self.properties))
-
-    def __str__(self):
-        return self.generate_css()

+ 7 - 0
properties.py → compress.py

@@ -1,3 +1,10 @@
+def compress_blocks(blocks):
+    # Map of property stringification to list of containing blocks
+    #property_locations = {}
+
+    return blocks
+
+
 def compress_color(properties):
     return properties
 

+ 83 - 0
csscom.py

@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+from argparse import ArgumentParser
+
+from parse import parse_groups
+from generate import generate_group
+
+
+def compress_css(css, compress_blocks=True, compress_whitespace=True,
+                 compress_color=True, compress_font=True,
+                 compress_dimension=True, sort_properties=True):
+    groups = parse_groups(css)
+    options = dict(compress_blocks=compress_blocks,
+                   compress_whitespace=compress_whitespace,
+                   compress_color=compress_color,
+                   compress_font=compress_font,
+                   compress_dimension=compress_dimension,
+                   sort_properties=sort_properties)
+    compressed_groups = [generate_group(selectors, blocks, **options)
+                         for selectors, blocks in groups]
+    newlines = '' if compress_whitespace else '\n\n'
+    return newlines.join(compressed_groups)
+
+
+def parse_options():
+    parser = ArgumentParser(description='Just another CSS compressor.')
+    parser.add_argument('files', metavar='FILE', nargs='+',
+                        help='list of CSS files to compress')
+    parser.add_argument('-cw', '--compress-whitespace', action='store_true',
+                        help='omit unnecessary whitespaces and semicolons')
+    parser.add_argument('-cc', '--compress-color', action='store_true',
+                        help='replace color codes/names with shorter synonyms')
+    parser.add_argument('-cf', '--compress-font', action='store_true',
+                        help='replace separate font statements with shortcut '
+                             'font statement where possible')
+    parser.add_argument('-cd', '--compress-dimension', action='store_true',
+                        help='replace separate margin/padding statements with '
+                             'shortcut statements where possible')
+    parser.add_argument('-cb', '--compress-blocks', action='store_true',
+                        help='combine or split blocks into blocks with '
+                             'comma-separated selectors if it results in less '
+                             'css code')
+    parser.add_argument('-nc', '--no-compression', action='store_true',
+                        help='don\'t apply any compression, just generate CSS')
+    parser.add_argument('-ns', '--no-sort', action='store_false',
+                        dest='sort_properties', help='sort property names')
+    parser.add_argument('-o', '--output', help='filename for compressed '
+                                               'output (default is stdout)')
+    args = parser.parse_args()
+
+    # Enable all compression options if none are explicitely enabled
+    if not any([args.compress_whitespace, args.compress_color,
+                args.compress_font, args.compress_dimension,
+                args.compress_blocks]) and not args.no_compression:
+        args.compress_whitespace = args.compress_color = args.compress_font = \
+                args.compress_dimension = args.compress_blocks = True
+
+    return args
+
+
+def _content(filename):
+    handle = open(filename, 'r')
+    content = '\n' + handle.read()
+    handle.close()
+    return content
+
+
+if __name__ == '__main__':  # pragma: nocover
+    args = parse_options()
+    options = dict(args._get_kwargs())
+    files = options.pop('files')
+    output_file = options.pop('output')
+    del options['no_compression']
+
+    try:
+        css = '\n'.join(_content(filename) for filename in files)
+        compressed = compress_css(css, **options)
+
+        if output_file:
+            open(output_file, 'w').write(compressed)
+        else:
+            print compressed,
+    except IOError as e:
+        print e

+ 0 - 132
csscomp.py

@@ -1,132 +0,0 @@
-#!/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

+ 94 - 0
generate.py

@@ -0,0 +1,94 @@
+from operator import itemgetter
+
+import compress
+
+
+def _indent(text, tab='\t'):
+    indented = ''
+
+    for i, line in enumerate(text.split('\n')):
+        if i:
+            indented += '\n'
+
+        if len(line):
+            indented += tab
+
+        indented += line
+
+    return indented
+
+
+def _indented_block(selectors, inner, compress_whitespace=False, tab='\t'):
+    if compress_whitespace:
+        comma = ','
+        lbracket = '{'
+        rbracket = '}'
+    else:
+        # TODO: use this?: comma = ',\n'
+        comma = ', '
+        lbracket = ' {'
+        rbracket = '\n}'
+        inner = _indent(inner, tab=tab)
+
+    selector = comma.join(sorted(set(selectors)))
+    return selector + lbracket + inner + rbracket
+
+
+def generate_block(selectors, properties, compress_whitespace=False,
+                   sort_properties=False, tab='\t'):
+    """
+    Generate CSS code for a single block.
+    """
+    if sort_properties:
+        properties.sort(key=itemgetter(0))
+
+    if compress_whitespace:
+        newline = ''
+        colon = ':'
+    else:
+        newline = '\n'
+        colon = ': '
+
+    properties = [newline + name + colon + value
+                  for name, value in properties]
+    inner = ';'.join(properties)
+
+    if not compress_whitespace:
+        inner += ';'
+
+    return _indented_block(selectors, inner,
+                           compress_whitespace=compress_whitespace, tab=tab)
+
+
+def generate_group(selectors, blocks, compress_blocks=True,
+                   compress_whitespace=True, compress_color=True,
+                   compress_font=True, compress_dimension=True,
+                   sort_properties=True, tab='\t'):
+    compressed_blocks = []
+
+    if compress_blocks:
+        blocks = compress.compress_blocks(blocks)
+
+    for block_selectors, properties in blocks:
+        if compress_color:
+            properties = compress.compress_color(properties)
+
+        if compress_font:
+            properties = compress.compress_font(properties)
+
+        if compress_dimension:
+            properties = compress.compress_dimension(properties)
+
+        compressed_blocks.append(generate_block(block_selectors, properties,
+            compress_whitespace=compress_whitespace,
+            sort_properties=sort_properties, tab=tab))
+
+    newline = '' if compress_whitespace else '\n'
+    inner = newline.join(compressed_blocks)
+
+    if selectors is None:
+        # Root-level group
+        return inner
+
+    return _indented_block(selectors, newline + inner,
+                           compress_whitespace=compress_whitespace, tab=tab)

+ 78 - 0
parse.py

@@ -0,0 +1,78 @@
+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(',')]
+
+
+def parse_groups(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_char = None
+    lineno = 1
+    properties = []
+    current_group = root_group = []
+    groups = [(None, root_group)]
+    selectors = None
+    multiline_comment = inline_comment = False
+
+    try:
+        for c in css:
+            char = c
+
+            if multiline_comment:
+                # Multiline comment end?
+                if c == '/' and prev_char == '*':
+                    multiline_comment = False
+            elif inline_comment:
+                # Inline comment end?
+                if c == '\n':
+                    inline_comment = False
+            elif c == '{':
+                # Block start
+                if selectors is not None:
+                    # Block is nested, save group selector
+                    current_group = []
+                    groups.append((selectors, current_group))
+
+                selectors = split_selectors(stack)
+                #print stack.strip(), '->', selectors
+                stack = ''
+                assert len(selectors)
+            elif c == '}':
+                if selectors is None:
+                    # Closing group
+                    current_group = root_group
+                else:
+                    # Closing block
+                    current_group.append((selectors, properties))
+                    selectors = None
+                    properties = []
+            elif c == ';':
+                # Property definition
+                assert selectors is not None
+                name, value = map(str.strip, stack.split(':', 1))
+                assert '\n' not in name
+                properties.append((name, value))
+                stack = ''
+            elif c == '*' and prev_char == '/':
+                # Multiline comment start
+                multiline_comment = True
+            elif c == '/' and prev_char == '/':
+                # Inline comment start
+                inline_comment = True
+            else:
+                if c == '\n':
+                    lineno += 1
+
+                stack += c
+                prev_char = c
+    except AssertionError:
+        raise Exception('unexpected \'%c\' on line %d' % (char, lineno))
+
+    return groups

+ 0 - 80
tests/test_block.py

@@ -1,80 +0,0 @@
-from unittest import TestCase
-
-from block import Block
-
-
-class TestBlock(TestCase):
-    def test_constructor(self):
-        s = '#foo div'
-        b = Block(s)
-        self.assertEqual(b.selectors, [s])
-        self.assertEqual(b.properties, set())
-
-
-class TestBlockAddProperty(TestCase):
-    def setUp(self):
-        self.b = Block('foo')
-
-    def test_single_value(self):
-        self.b.add_property('foo', 'bar')
-        self.assertEqual(self.b.properties, set([('foo', 'bar')]))
-
-    def test_double_value(self):
-        self.b.add_property('foo', 'bar')
-        self.b.add_property('foo', 'bar')
-        self.assertEqual(self.b.properties, set([('foo', 'bar')]))
-
-    def test_multiple_values(self):
-        self.b.add_property('foo', 'bar')
-        self.b.add_property('foo', 'baz')
-        self.assertEqual(self.b.properties, set([('foo', 'bar'),
-                                                 ('foo', 'baz')]))
-
-
-class TestBlockGenerateCss(TestCase):
-    def setUp(self):
-        self.div = Block('div')
-        self.div.add_property('color', '#000')
-        self.div.add_property('font-weight', 'bold')
-
-    def test_nocompress_empty(self):
-        div = Block('div')
-        self.assertEqual(div.generate_css(), 'div {\n}')
-
-    def test_nocompress_single_property(self):
-        div = Block('div')
-        div.add_property('color', '#000')
-        self.assertEqualCss(div.generate_css(), '''
-div {
-    color: #000;
-}''')
-
-    def test_nocompress_multiple_properties(self):
-        self.assertEqualCss(self.div.generate_css(), '''
-div {
-    color: #000;
-    font-weight: bold;
-}''')
-
-    def test_nocompress_multiple_selectors(self):
-        div = Block('div', 'p')
-        div.add_property('color', '#000')
-        self.assertEqualCss(div.generate_css(), '''
-div,
-p {
-    color: #000;
-}''')
-
-    def test_nocompress_multiple_selectors_sorted(self):
-        self.assertMultiLineEqual(Block('div', 'p').generate_css(),
-                                  Block('p', 'div').generate_css())
-
-    def test_compress_whitespace(self):
-        self.assertEqual(self.div.generate_css(compress_whitespace=True),
-                         'div{color:#000;font-weight:bold}')
-
-    def test___str__(self):
-        self.assertEqual(str(self.div), self.div.generate_css())
-
-    def assertEqualCss(self, a, b):
-        self.assertMultiLineEqual(a, b.strip().replace('    ', '\t'))

+ 5 - 1
tests/test_properties.py → tests/test_compress.py

@@ -1,9 +1,13 @@
 from unittest import TestCase
 
-from properties import compress_color, compress_font, compress_dimension
+from compress import compress_blocks, compress_color, compress_font, \
+        compress_dimension
 
 
 class TestProperties(TestCase):
+    def test_compress_blocks(self):
+        pass
+
     def test_compress_color(self):
         pass
 

+ 57 - 0
tests/test_generate.py

@@ -0,0 +1,57 @@
+from unittest import TestCase
+
+from generate import generate_block, generate_group
+
+
+class TestGenerateBlock(TestCase):
+    def setUp(self):
+        self.selector = 'div'
+        self.properties = [('color', '#000'), ('font-weight', 'bold')]
+
+    def test_nocompress_empty(self):
+        self.assertGenerates(['div'], [], 'div {\n}')
+
+    def test_nocompress_single_property(self):
+        selectors = ['div']
+        properties = [('color', '#000')]
+        self.assertGenerates(selectors, properties, '''
+div {
+    color: #000;
+}''')
+
+    def test_nocompress_multiple_properties(self):
+        selectors = ['div']
+        properties = [('color', '#000'), ('font-weight', 'bold')]
+        self.assertGenerates(selectors, properties, '''
+div {
+    color: #000;
+    font-weight: bold;
+}''')
+
+    def test_nocompress_multiple_selectors(self):
+        selectors = ['div', 'p']
+        properties = [('color', '#000')]
+        self.assertGenerates(selectors, properties, '''
+div,
+p {
+    color: #000;
+}''')
+
+    def test_nocompress_multiple_selectors_sorted(self):
+        self.assertMultiLineEqual(generate_block(['div', 'p'], []),
+                                  generate_block(['p', 'div'], []))
+
+    def test_compress_whitespace(self):
+        selectors = ['div']
+        properties = [('color', '#000'), ('font-weight', 'bold')]
+        self.assertGenerates(selectors, properties,
+                             'div{color:#000;font-weight:bold}',
+                             compress_whitespace=True)
+
+    def assertGenerates(self, selectors, props, css, **kwargs):
+        self.assertMultiLineEqual(generate_block(selectors, props, **kwargs),
+                                  css.strip().replace('    ', '\t'))
+
+
+class TestGenerateGroup(TestCase):
+    pass

+ 2 - 2
tests/test_base.py → tests/test_parse.py

@@ -1,9 +1,9 @@
 from unittest import TestCase
 
-from csscomp import split_selectors, StyleSheet
+from parse import split_selectors, parse_groups
 
 
-class TestBase(TestCase):
+class TestParse(TestCase):
     def test_split_selectors(self):
         self.assertEqual(split_selectors('a, b'), ['a', 'b'])
         self.assertEqual(split_selectors('a ,b'), ['a', 'b'])