diff --git a/lesscpy/lessc/formatter.py b/lesscpy/lessc/formatter.py index 7aab98c..d02c579 100644 --- a/lesscpy/lessc/formatter.py +++ b/lesscpy/lessc/formatter.py @@ -7,6 +7,35 @@ """ class Formatter(object): def format(self, parse, minify=False, xminify=False): + """ + """ + eb = '\n' + if xminify: + eb = '' + minify = True + self.minify = minify + self.items = {} + if minify: + self.items.update({ + 'nl': '', + 'tab': '', + 'ws': '', + 'endblock': eb + }) + else: + self.items.update({ + 'nl': '\n', + 'tab': '\t', + 'ws': ' ', + 'endblock': eb + }) + self.out = [u.format(self.items) + for u in parse.result + if u] + return ''.join(self.out).strip() + + + def xformat(self, parse, minify=False, xminify=False): """ Format css output from parser @param Parse-result object: Parse-result object @param bool: Minify flag diff --git a/lesscpy/lessc/parser.py b/lesscpy/lessc/parser.py index cc944fc..deb38e0 100644 --- a/lesscpy/lessc/parser.py +++ b/lesscpy/lessc/parser.py @@ -76,12 +76,27 @@ class LessParser(object): self.scope.push() self.target = filename self.result = self.parser.parse(filename, lexer=self.lex, debug=debuglevel) +# [print(r) for r in self.result] def scopemap(self): """ Output scopemap. """ if self.result: - utility.print_recursive(self.result) +# utility.print_recursive(self.result) + self._scopemap_aux(self.result) + + def _scopemap_aux(self, ll, lvl=0): + pad = ''.join(['\t.'] * lvl) + t = type(ll) + if t is list: + for p in ll: + self._scopemap_aux(p, lvl) + elif hasattr(ll, 'tokens'): + print(pad, t) + self._scopemap_aux(list(utility.flatten(ll.tokens)), lvl+1) +# else: +# print(pad, t) + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -98,6 +113,8 @@ class LessParser(object): """ if len(p) == 3: p[1].append(p[2]) + else: + p[1] = [p[1]] p[0] = p[1] def p_unit(self, p): @@ -106,7 +123,7 @@ class LessParser(object): | block_decl | mixin_decl """ - p[0] = [p[1]] + p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -181,12 +198,15 @@ class LessParser(object): def p_media_open(self, p): """ block_open : css_media t_ws identifier brace_open """ - p[0] = [p[1], p[3]] + ident = [p[1], p[2]] + ident.extend(p[3].tokens) + p[3].tokens = ident + p[0] = p[3] def p_font_face_open(self, p): """ block_open : css_font_face t_ws brace_open """ - p[0] = p[1] + p[0] = Identifier([p[1], p[2]]) # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -258,12 +278,12 @@ class LessParser(object): """ variable_decl : variable ':' style_list ';' """ try: - v = Variable(p) -# v.parse(self.scope) -# if self.scope.current == '__mixin__': -# self.stash[v.name()] = v -# else: -# self.scope.add_variable(v) + v = Variable(list(p)[1:]) + v.parse(self.scope) + if self.scope.in_mixin(): + self.stash[v.name] = v + else: + self.scope.add_variable(v) except SyntaxError as e: self.handle_error(e, p) p[0] = None @@ -278,12 +298,6 @@ class LessParser(object): | prop_open less_arguments ';' """ p[0] = Property(list(p)[1:-1]) - if not self.scope.in_mixin(): - try: - p[0].parse(self.scope) - except SyntaxError as e: - self.handle_error(e, p) - p[0] = None def p_prop_open(self, p): """ prop_open : property ':' diff --git a/lesscpy/lessc/scope.py b/lesscpy/lessc/scope.py index 35560f9..57e4f12 100644 --- a/lesscpy/lessc/scope.py +++ b/lesscpy/lessc/scope.py @@ -36,7 +36,7 @@ class Scope(list): def add_variable(self, variable): """ """ - self[-1]['__variables__'][variable.name()] = variable + self[-1]['__variables__'][variable.name] = variable def variables(self, name): """ diff --git a/lesscpy/lessc/utility.py b/lesscpy/lessc/utility.py index a3ef40a..4295850 100644 --- a/lesscpy/lessc/utility.py +++ b/lesscpy/lessc/utility.py @@ -6,6 +6,7 @@ """ import collections +import re def flatten(l): """ @@ -69,41 +70,36 @@ def destring(v): def analyze_number(var, err=''): """ Analyse number for type and split from unit @param str: value + @raises: SyntaxError @return: tuple (number, unit) """ - u = None + n, u = split_unit(var) if type(var) is not str: return (var, u) if is_color(var): return (var, 'color') - var = var.strip() - n = var - if not '.' in var: - try: - n = int(var) - return (n, u) - except (ValueError, TypeError): - pass - try: - n = float(var) - except (ValueError, TypeError): - try: - n = ''.join([c for c in var if c in '0123456789.-']) - n = float(n) if '.' in n else int(n) - u = ''.join([c for c in var if c not in '0123456789.-']) - except ValueError: - raise SyntaxError('%s ´%s´' % (err, var)) + if is_int(n): + n = int(n) + elif is_float(n): + n = float(n) + else: + raise SyntaxError('%s ´%s´' % (err, var)) return (n, u) -def with_unit(n, u): +def with_unit(n, u=None): """ Return number with unit @param int/float: value @param str: unit @return: mixed """ + if type(n) is tuple: + n, u = n if n == 0: return 0 if u: - return "%s%s" % (str(n), u) + n = str(n) + if n.startswith('.'): + n = '0' + n + return "%s%s" % (n, u) return n def is_color(v): @@ -130,24 +126,32 @@ def is_variable(v): return (v.startswith('@') or v.startswith('-@')) return False -def print_recursive(ll, lvl=0): - """ Scopemap printer - @param list: parser result - @param int: depth +def is_int(v): """ - pad = ''.join(['.'] * lvl) - t = type(ll) - if t is list: - for l in ll: print_recursive(l, lvl+1) - elif hasattr(ll, '_p'): - print(pad, type(ll)) - print(pad, '[') - print_recursive(list(flatten(ll._p)), lvl+1) - print(pad, ']') - elif t is str: - print("%s '%s'" % (pad, ll)) - else: - print("%s %s" % (pad, ll)) + """ + try: + int(str(v)) + return True + except (ValueError, TypeError): + pass + return False - +def is_float(v): + """ + """ + if not is_int(v): + try: + float(str(v)) + return True + except (ValueError, TypeError): + pass + return False + +def split_unit(v): + """ + """ + r = re.search('^(\-?[\d\.]+)(.*)$', str(v)) + return r.groups() if r else ('','') + + \ No newline at end of file diff --git a/lesscpy/plib/__init__.py b/lesscpy/plib/__init__.py index 93a794e..4807374 100644 --- a/lesscpy/plib/__init__.py +++ b/lesscpy/plib/__init__.py @@ -4,6 +4,7 @@ __all__ = [ 'Expression', 'Identifier', 'Mixin', + 'Node', 'Property', 'Statement', 'String', @@ -14,6 +15,7 @@ from .call import Call from .expression import Expression from .identifier import Identifier from .mixin import Mixin +from .node import Node from .property import Property from .statement import Statement from .string import String diff --git a/lesscpy/plib/block.py b/lesscpy/plib/block.py index 62d02bd..2554801 100644 --- a/lesscpy/plib/block.py +++ b/lesscpy/plib/block.py @@ -3,5 +3,26 @@ from .node import Node from lesscpy.lessc import utility class Block(Node): + pass + def parse(self, scope): - pass \ No newline at end of file + ident, inner = self.tokens + self.name = ident.parse(scope) + self.parsed = [p.parse(scope) + for p in inner + if p and type(p) is not type(self)] + self.inner = [p.parse(scope) + for p in inner + if p and type(p) is type(self)] + return self + + def format(self, fills): + """ + """ + f = "%(identifier)s%(ws)s{%(nl)s%(proplist)s}%(nl)s%(endblock)s" + fills.update({ + 'identifier': self.name, + 'proplist': ''.join([p.format(fills) for p in self.parsed]), + 'endblock': ''.join([p.format(fills) for p in self.inner]), + }) + return f % fills \ No newline at end of file diff --git a/lesscpy/plib/call.py b/lesscpy/plib/call.py index 48ab81d..434b340 100644 --- a/lesscpy/plib/call.py +++ b/lesscpy/plib/call.py @@ -1,5 +1,17 @@ """ """ from .node import Node +import lesscpy.lessc.utility as utility class Call(Node): - pass \ No newline at end of file + def parse(self, scope): + self.parsed = [p.parse(scope) + if hasattr(p, 'parse') + else p + for p in utility.flatten(self.tokens)] + return self + + def format(self, fills): + return ''.join([p.format(fills) + if hasattr(p, 'format') + else p + for p in self.parsed]) \ No newline at end of file diff --git a/lesscpy/plib/expression.py b/lesscpy/plib/expression.py index f89966c..e914d66 100644 --- a/lesscpy/plib/expression.py +++ b/lesscpy/plib/expression.py @@ -1,5 +1,70 @@ """ """ from .node import Node +from lesscpy.lessc import utility + class Expression(Node): - pass \ No newline at end of file + def parse(self, scope): + """ Parse Node + @param list: current scope + """ + assert(len(self.tokens) == 3) + expr = [t.parse(scope) if hasattr(t, 'parse') + else t + for t in self.tokens] + expr = [self.neg(t, scope) for t in expr] + A, O, B = [e[0] + if type(e) is tuple + else e + for e in expr] + a, ua = utility.analyze_number(A, 'Illegal element in expression') + b, ub = utility.analyze_number(B, 'Illegal element in expression') + if(a is False or b is False): + return ''.join([A, O, B]) + if ua == 'color' or ub == 'color': + return color.LessColor().process(expression) + out = self.operate(a, b, O) + if type(a) is int and type(b) is int: + out = int(out) + return self.with_units(out, ua, ub) + + def neg(self, t, scope): + """ + """ + if t and type(t) is list and t[0] == '-': + v = t[1] + if len(t) > 1 and hasattr(t[1], 'parse'): + v = t[1].parse(scope) + if type(v) is str: + return '-' + v + return -v + return t + + def with_units(self, v, ua, ub): + """ + """ + if v == 0: return v; + if ua and ub: + if ua == ub: + return str(v) + ua + else: + raise SyntaxError("Error in expression %s != %s" % (ua, ub)) + elif ua: + return str(v) + ua + elif ub: + return str(v) + ub + return v + + def operate(self, a, b, o): + """ + """ + operation = { + '+': '__add__', + '-': '__sub__', + '*': '__mul__', + '/': '__truediv__' + }.get(o[0]) + v = getattr(a, operation)(b) + if v is NotImplemented: + v = getattr(b, operation)(a) + return v diff --git a/lesscpy/plib/identifier.py b/lesscpy/plib/identifier.py index 2b2d04f..fdab483 100644 --- a/lesscpy/plib/identifier.py +++ b/lesscpy/plib/identifier.py @@ -1,5 +1,9 @@ """ """ from .node import Node +from lesscpy.lessc import utility class Identifier(Node): - pass \ No newline at end of file + def parse(self, scope): + """ + """ + return ''.join(utility.flatten(self.tokens)).strip() \ No newline at end of file diff --git a/lesscpy/plib/node.py b/lesscpy/plib/node.py index 2811ddf..6a5f395 100644 --- a/lesscpy/plib/node.py +++ b/lesscpy/plib/node.py @@ -2,9 +2,10 @@ """ class Node(object): def __init__(self, p): - self._p = p + self.tokens = p def parse(self, scope): - pass -# print(type(self), list(self._p)) -# print() + return self + + def format(self, fills): + return str(type(self)) diff --git a/lesscpy/plib/property.py b/lesscpy/plib/property.py index aa81bad..1acdb3b 100644 --- a/lesscpy/plib/property.py +++ b/lesscpy/plib/property.py @@ -1,8 +1,31 @@ """ """ from .node import Node +from lesscpy.lessc import utility class Property(Node): + pass + def parse(self, scope): - pass -# print(type(self), list(self._p)) -# print() \ No newline at end of file + property, style = self.tokens + self.property = property[0] + self.parsed = [] + if style: + self.parsed = [p.parse(scope) + if hasattr(p, 'parse') + else p + for p in utility.flatten(style)] + self.parsed = [p for p in self.parsed if p] + return self + + def format(self, fills): + """ + """ + f = "%(tab)s%(property)s:%(ws)s%(style)s;%(nl)s" + fills.update({ + 'property': self.property, + 'style': ''.join([p.format(fills) + if hasattr(p, 'format') + else p + for p in self.parsed]), + }) + return f % fills diff --git a/lesscpy/plib/variable.py b/lesscpy/plib/variable.py index cd42908..903016c 100644 --- a/lesscpy/plib/variable.py +++ b/lesscpy/plib/variable.py @@ -2,4 +2,7 @@ """ from .node import Node class Variable(Node): - pass \ No newline at end of file + def parse(self, scope): + """ + """ + self.name = self.tokens.pop(0) \ No newline at end of file diff --git a/lesscpy/test/testexpression.py b/lesscpy/test/testexpression.py new file mode 100644 index 0000000..a2dfec0 --- /dev/null +++ b/lesscpy/test/testexpression.py @@ -0,0 +1,66 @@ +import unittest +if __name__ == '__main__': + import bootstrap +from lesscpy.plib.expression import Expression + +class TestExpression(unittest.TestCase): + def test_basic(self): + for test in [ + ['0', '+', '0', 0], + ['2', '+', '2', 4], + ['2.0', '+', '2', 4.0], + ['2', '+', '2.0', 4.0], + ['2.0', '+', '2.0', 4.0], + [('2.0',), '+', '2.0', 4.0], + [('2.0',), '+', ('2.0',), 4.0], + ['0px', '+', '0', 0], + ['2px', '+', '2', '4px'], + ['2.0px', '+', '2', '4.0px'], + [('2px', ' '), '+', '2.0', '4.0px'], + ['2.0px', '+', '2.0', '4.0px'], + ]: + e = Expression(test[:3]) + self.assertEqual(test[3], e.parse(None), str(test)) + + def test_neg(self): + for test in [ + ['-0', '+', '0', 0], + ['-2', '+', '-2', -4], + ['-2.0', '+', '-2', -4.0], + ['-2', '+', '-2.0', -4.0], + ['-2.0', '+', '-2.0', -4.0], + ['-0', '-', '0', 0], + ['-2', '-', '-2', 0], + ['-2.0', '-', '2', -4.0], + ['-2', '-', '-2.0', 0], + ['2.0', '-', '-2.0', 4.0], + ['-0px', '+', '0', 0], + ['-2px', '+', '-2', '-4px'], + ['-2.0', '+', '-2px', '-4.0px'], + ['-2em', '+', '-2.0', '-4.0em'], + ['-2.0s', '+', '-2.0s', '-4.0s'], + ]: + e = Expression(test[:3]) + self.assertEqual(test[3], e.parse(None), str(test)) + + def testnest(self): + e = Expression(['2', '-', '-2']) + f = Expression([['-', e], '*', '-3']) + self.assertEqual(f.parse(None), 12) + g = Expression(['2', '*', ['-', f]]) + self.assertEqual(g.parse(None), -24) + h = Expression(['34', '-', ['-', g]]) + self.assertEqual(h.parse(None), 10) + i = Expression([f, '-', h]) + self.assertEqual(i.parse(None), 2) + + def testdiv(self): + e = Expression(['1', '/', '3']) + + def testinput(self): + e = Expression(['1a', '+', '1b']) + self.assertRaises(SyntaxError, e.parse, None) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/lesscpy/test/testutility.py b/lesscpy/test/testutility.py new file mode 100644 index 0000000..628b813 --- /dev/null +++ b/lesscpy/test/testutility.py @@ -0,0 +1,95 @@ +import unittest +if __name__ == '__main__': + import bootstrap +import lesscpy.lessc.utility as utility + +class TestUtility(unittest.TestCase): + def testanalyze(self): + test = utility.analyze_number + self.assertEqual((0, ''), test('0')) + self.assertEqual((1, ''), test('1')) + self.assertEqual(type(test('1')[0]), int) + self.assertEqual(type(test('-1')[0]), int) + self.assertEqual((1.0, ''), test('1.0')) + self.assertEqual(type(test('-1.0')[0]), float) + self.assertEqual((0, 'px'), test('0px')) + self.assertEqual((1, 'px'), test('1px')) + self.assertEqual((1.0, 'px'), test('1.0px')) + self.assertEqual((0, 'px'), test('-0px')) + self.assertEqual((-1, 'px'), test('-1px')) + self.assertEqual(type(test('-1px')[0]), int) + self.assertEqual((-1.0, 'px'), test('-1.0px')) + self.assertEqual(type(test('-1.0px')[0]), float) + self.assertRaises(SyntaxError, test, 'gg') + self.assertRaises(SyntaxError, test, '-o') + self.assertRaises(SyntaxError, test, '') + + def testsplit_unit(self): + test = utility.split_unit + self.assertEqual(('', ''), test(None)) + self.assertEqual(('', ''), test(False)) + self.assertEqual(('', ''), test('qwerty')) + self.assertEqual(('1', ''), test(1)) + self.assertEqual(('1', ''), test('1')) + self.assertEqual(('1', 'px'), test('1px')) + self.assertEqual(('-1', 'px'), test('-1px')) + + def testis_int(self): + test = utility.is_int + self.assertTrue(test(1)) + self.assertTrue(test('1')) + self.assertTrue(test('-1')) + self.assertTrue(test(-1)) + self.assertFalse(test(False)) + self.assertFalse(test(None)) + self.assertFalse(test(0.0)) + + def testis_float(self): + test = utility.is_float + self.assertFalse(test(1)) + self.assertFalse(test('1')) + self.assertFalse(test(False)) + self.assertFalse(test(None)) + self.assertTrue(test(0.0)) + self.assertTrue(test(-0.0)) + self.assertTrue(test('77.0565')) + self.assertTrue(test('-0.0')) + + def testis_color(self): + test = utility.is_color + self.assertTrue(test('#123')) + self.assertTrue(test('#123456')) + self.assertTrue(test('#Df3')) + self.assertTrue(test('#AbCdEf')) + self.assertFalse(test('#AbCdEg')) + self.assertFalse(test('#h12345')) + self.assertFalse(test('#12345')) + self.assertFalse(test('AbCdEf')) + self.assertFalse(test('')) + self.assertFalse(test(False)) + self.assertFalse(test([])) + + def testis_variable(self): + test = utility.is_variable + self.assertTrue(test('@var')) + self.assertTrue(test('-@var')) + self.assertFalse(test('var')) + self.assertFalse(test('')) + self.assertFalse(test(False)) + self.assertFalse(test([])) + + def testwith_unit(self): + test = utility.with_unit + self.assertEqual('1px', test((1, 'px'))) + self.assertEqual('1px', test(1, 'px')) + self.assertEqual('1.0px', test(1.0, 'px')) + self.assertEqual('0.0px', test('.0', 'px')) + self.assertEqual('0.6px', test(.6, 'px')) + self.assertEqual(1, test(1)) + self.assertEqual(1, test(1, None)) + self.assertEqual(1, test(1,)) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file