Skip to content
Snippets Groups Projects
Commit 8912c7b1 authored by Taddeüs Kroes's avatar Taddeüs Kroes
Browse files

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

parent 5adff8a5
No related branches found
No related tags found
No related merge requests found
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): def compress_color(properties):
return 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)
parse.py 0 → 100644
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 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): class TestProperties(TestCase):
def test_compress_blocks(self):
pass
def test_compress_color(self): def test_compress_color(self):
pass 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 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): def test_split_selectors(self):
self.assertEqual(split_selectors('a, b'), ['a', 'b']) self.assertEqual(split_selectors('a, b'), ['a', 'b'])
self.assertEqual(split_selectors('a ,b'), ['a', 'b']) self.assertEqual(split_selectors('a ,b'), ['a', 'b'])
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment