Commit 8912c7b1 authored by Taddeüs Kroes's avatar Taddeüs Kroes

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

parent 5adff8a5
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()
def compress_blocks(blocks):
# Map of property stringification to list of containing blocks
#property_locations = {}
return blocks
def compress_color(properties):
return properties
......
#!/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
#!/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
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)
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
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'))
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
......
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
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'])
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment