From fe7fcf6b26cf01e3a2992738c19cd32cc33ec245 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Tue, 1 Apr 2014 14:44:48 -0400 Subject: [PATCH] Add new function: compress_partitioned; bump version to 0.9.2 --- csscompressor/__init__.py | 138 +++++++++++++++++++++----- csscompressor/tests/test_compress.py | 54 ++++++++++ csscompressor/tests/test_partition.py | 110 ++++++++++++++++++++ setup.py | 2 +- 4 files changed, 280 insertions(+), 24 deletions(-) create mode 100644 csscompressor/tests/test_compress.py create mode 100644 csscompressor/tests/test_partition.py diff --git a/csscompressor/__init__.py b/csscompressor/__init__.py index a428acd..7452e5b 100644 --- a/csscompressor/__init__.py +++ b/csscompressor/__init__.py @@ -12,7 +12,7 @@ __all__ = ('compress',) -__version__ = '0.9.1' +__version__ = '0.9.2' import re @@ -121,30 +121,30 @@ def _preserve_call_tokens(css, regexp, preserved_tokens, remove_ws=False): while not found_term and (end_idx + 1) <= max_idx: end_idx = css.find(term, end_idx + 1) - if end_idx > 0 and css[end_idx - 1] != '\\': - found_term = True - if term != ')': - end_idx = css.find(')', end_idx) + if end_idx > 0: + if css[end_idx - 1] != '\\': + found_term = True + if term != ')': + end_idx = css.find(')', end_idx) + else: + raise ValueError('malformed css') sb.append(css[append_idx:match.start(0)]) - if found_term: - token = css[start_idx:end_idx] + assert found_term - if remove_ws: - token = _ws_re.sub('', token) + token = css[start_idx:end_idx] - preserver = ('{0}(___YUICSSMIN_PRESERVED_TOKEN_{1}___)' - .format(name, len(preserved_tokens))) + if remove_ws: + token = _ws_re.sub('', token) - preserved_tokens.append(token) - sb.append(preserver) + preserver = ('{0}(___YUICSSMIN_PRESERVED_TOKEN_{1}___)' + .format(name, len(preserved_tokens))) - append_idx = end_idx + 1 + preserved_tokens.append(token) + sb.append(preserver) - else: - sb.append(css[match.start(0), match.end(0)]) - append_id = match.end(0) + append_idx = end_idx + 1 sb.append(css[append_idx:]) @@ -213,7 +213,7 @@ def _compress_hex_colors(css): return ''.join(buf) -def compress(css, max_linelen=0): +def _compress(css, max_linelen=0): start_idx = end_idx = 0 total_len = len(css) @@ -229,16 +229,18 @@ def compress(css, max_linelen=0): if start_idx < 0: break + suffix = '' end_idx = css.find('*/', start_idx + 2) if end_idx < 0: end_idx = total_len + suffix = '*/' token = css[start_idx + 2:end_idx] comments.append(token) css = (css[:start_idx + 2] + '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_{0}___'.format(len(comments)-1) + - css[end_idx:]) + css[end_idx:] + suffix) start_idx += 2 @@ -411,9 +413,6 @@ def compress(css, max_linelen=0): # Add "\" back to fix Opera -o-device-pixel-ratio query css = css.replace('___YUI_QUERY_FRACTION___', '/') - # Some source control tools don't like it when files containing lines longer - # than, say 8000 characters, are checked in. The linebreak option is used in - # that case to split long lines after a specific column. if max_linelen and len(css) > max_linelen: buf = [] start_pos = 0 @@ -436,10 +435,103 @@ def compress(css, max_linelen=0): # See SF bug #1980989 css = _many_semi_re.sub(';', css) + return css, preserved_tokens + + +def _apply_preserved(css, preserved_tokens): # restore preserved comments and strings for i, token in reversed(tuple(enumerate(preserved_tokens))): css = css.replace('___YUICSSMIN_PRESERVED_TOKEN_{0}___'.format(i), token) css = css.strip() - return css + + +def compress(css, max_linelen=0): + """Compress given CSS stylesheet. + + Parameters: + + - css : str + An str with CSS rules. + + - max_linelen : int = 0 + Some source control tools don't like it when files containing lines longer + than, say 8000 characters, are checked in. This option is used in + that case to split long lines after a specific column. + + Returns a ``str`` object with compressed CSS. + """ + + css, preserved_tokens = _compress(css, max_linelen=max_linelen) + css = _apply_preserved(css, preserved_tokens) + return css + + +def compress_partitioned(css, + max_linelen=0, + max_rules_per_file=4000): + """Compress given CSS stylesheet into a set of files. + + Parameters: + + - max_linelen : int = 0 + Has the same meaning as for "compress()" function. + + - max_rules_per_file : int = 0 + Internet Explorers <= 9 have an artificial max number of rules per CSS + file (4096; http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx) + When ``max_rules_per_file`` is a positive number, the function *always* returns + a list of ``str`` objects, each limited to contain less than the passed number + of rules. + + Always returns a ``list`` of ``str`` objects with compressed CSS. + """ + + assert max_rules_per_file > 0 + + css, preserved_tokens = _compress(css, max_linelen=max_linelen) + + bufs = [] + buf = [] + rules = 0 + while css: + if rules >= max_rules_per_file: + bufs.append(''.join(buf)) + rules = 0 + buf = [] + + nested = 0 + while True: + op_idx = css.find('{') + cl_idx = css.find('}') + + if cl_idx < 0: + raise ValueError('malformed CSS: non-balanced curly-braces') + + if op_idx < 0 or cl_idx < op_idx: # ... } ... { ... + nested -= 1 + + if nested < 0: + raise ValueError('malformed CSS: non-balanced curly-braces') + + buf.append(css[:cl_idx+1]) + css = css[cl_idx+1:] + + if not nested: # closing rules + break + + else: # ... { ... } ... + nested += 1 + + rule_line = css[:op_idx+1] + buf.append(rule_line) + css = css[op_idx+1:] + + rules += rule_line.count(',') + 1 + + bufs.append(''.join(buf)) + + bufs = [_apply_preserved(buf, preserved_tokens) for buf in bufs] + + return bufs diff --git a/csscompressor/tests/test_compress.py b/csscompressor/tests/test_compress.py new file mode 100644 index 0000000..8d11e01 --- /dev/null +++ b/csscompressor/tests/test_compress.py @@ -0,0 +1,54 @@ +## +# Copyright (c) 2013 Sprymix Inc. +# All rights reserved. +# +# See LICENSE for details. +## + + +from csscompressor.tests.base import BaseTest +from csscompressor import compress + +import unittest + + +class Tests(unittest.TestCase): + def test_linelen_1(self): + input = ''' + a {content: '}}'} + b {content: '}'} + c {content: '{'} + ''' + output = compress(input, max_linelen=2) + assert output == "a{content:'}}'}\nb{content:'}'}\nc{content:'{'}" + + def test_linelen_2(self): + input = '' + output = compress(input, max_linelen=2) + assert output == "" + + def test_linelen_3(self): + input = ''' + a {content: '}}'} + b {content: '}'} + c {content: '{'} + d {content: '{'} + ''' + output = compress(input, max_linelen=100) + assert output == "a{content:'}}'}b{content:'}'}c{content:'{'}\nd{content:'{'}" + + def test_compress_1(self): + input = ''' + a {content: '}}'} /* + b {content: '}'} + c {content: '{'} + d {content: '{'} + ''' + output = compress(input) + assert output == "a{content:'}}'}" + + def test_compress_2(self): + input = ''' + a {content: calc(10px-10%} + ''' + self.assertRaises(ValueError, compress, input) diff --git a/csscompressor/tests/test_partition.py b/csscompressor/tests/test_partition.py new file mode 100644 index 0000000..91ac4e1 --- /dev/null +++ b/csscompressor/tests/test_partition.py @@ -0,0 +1,110 @@ +## +# Copyright (c) 2013 Sprymix Inc. +# All rights reserved. +# +# See LICENSE for details. +## + + +from csscompressor.tests.base import BaseTest +from csscompressor import compress_partitioned + +import unittest + + +class Tests(unittest.TestCase): + def test_partition_1(self): + input = '' + output = compress_partitioned(input, max_rules_per_file=2) + assert output == [''] + + def test_partition_2(self): + input = ''' + a {content: '}}'} + b {content: '}'} + c {content: '{'} + ''' + + output = compress_partitioned(input, max_rules_per_file=2) + assert output == ["a{content:'}}'}b{content:'}'}", "c{content:'{'}"] + + def test_partition_3(self): + input = ''' + @media{ + a {p: 1} + b {p: 2} + x {p: 2} + } + @media{ + c {p: 1} + d {p: 2} + y {p: 2} + } + @media{ + e {p: 1} + f {p: 2} + z {p: 2} + } + ''' + + output = compress_partitioned(input, max_rules_per_file=2) + assert output == ['@media{a{p:1}b{p:2}x{p:2}}', + '@media{c{p:1}d{p:2}y{p:2}}', + '@media{e{p:1}f{p:2}z{p:2}}'] + + def test_partition_4(self): + input = ''' + @media{ + a {p: 1} + b {p: 2} + x {p: 2} + ''' + + self.assertRaises(ValueError, compress_partitioned, + input, max_rules_per_file=2) + + def test_partition_5(self): + input = ''' + @media{ + a {p: 1} + b {p: 2} + x {p: 2} + + @media{ + c {p: 1} + d {p: 2} + y {p: 2} + } + @media{ + e {p: 1} + f {p: 2} + z {p: 2} + } + ''' + + self.assertRaises(ValueError, compress_partitioned, + input, max_rules_per_file=2) + + def test_partition_6(self): + input = ''' + @media{}} + + a {p: 1} + b {p: 2} + x {p: 2} + ''' + + self.assertRaises(ValueError, compress_partitioned, + input, max_rules_per_file=2) + + def test_partition_7(self): + input = ''' + a, a1, a2 {color: red} + b, b2, b3 {color: red} + c, c3, c4, c5 {color: red} + d {color: red} + ''' + + output = compress_partitioned(input, max_rules_per_file=2) + assert output == ['a,a1,a2{color:red}', 'b,b2,b3{color:red}', + 'c,c3,c4,c5{color:red}', 'd{color:red}'] diff --git a/setup.py b/setup.py index 9a1fcfa..3898f99 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ except ImportError: setup( name='csscompressor', - version='0.9.1', + version='0.9.2', url='http://github.com/sprymix/csscompressor', license='BSD', author='Yury Selivanov',