Commit 5adff8a5 authored by Taddeüs Kroes's avatar Taddeüs Kroes

Initial commit

parents
*.pyc
*.swp
*~
coverage
.coverage
TEST_OPTIONS := --failfast --catch
COVERAGE_OPTIONS := --branch
COVERAGE_DIR := coverage
.PHONY: test coverage pyclean clean
test:
@PYTHONDONTWRITEBYTECODE=x python -m unittest discover -s tests \
-p 'test_*.py' $(TEST_OPTIONS)
coverage:
@python-coverage erase
@rm -rf $(COVERAGE_DIR)
@PYTHONDONTWRITEBYTECODE=x PYTHONPATH=. python-coverage run --source=. \
--omit=tests/* $(COVERAGE_OPTIONS) tests/run.py
@python-coverage report
@python-coverage html --directory=$(COVERAGE_DIR)
pyclean:
find -name \*.pyc -delete
clean: pyclean
rm -rf $(COVERAGE_DIR)
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()
#!/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
def compress_color(properties):
return properties
def compress_font(properties):
return properties
def compress_dimension(properties):
return properties
import unittest
tests = unittest.defaultTestLoader.discover('tests', 'test_*.py')
run_options = dict(failfast=True)
if __name__ == '__main__':
unittest.TextTestRunner(**run_options).run(tests)
print
from unittest import TestCase
from csscomp import split_selectors, StyleSheet
class TestBase(TestCase):
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('\na ,b '), ['a', 'b'])
self.assertEqual(split_selectors('a, b\nc'), ['a', 'b c'])
self.assertEqual(split_selectors('a,\nb c ,d\n'), ['a', 'b c', 'd'])
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
class TestProperties(TestCase):
def test_compress_color(self):
pass
def test_compress_font(self):
pass
def test_compress_dimension(self):
pass
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