From 5f498748521b44721076e4422953a621e8b675b5 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Fri, 19 Jul 2013 11:09:30 +0200 Subject: [PATCH 01/23] Add tox.ini for easier with several virtual environments --- requirements.txt | 1 + tox.ini | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 requirements.txt create mode 100644 tox.ini diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..90412f0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +ply diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1d001e0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py26,py27,pep8 + +[testenv] +deps = -r{toxinidir}/requirements.txt +commands = python lesscpy/test/__main__.py + +[testenv:pep8] +deps = pep8 +commands = pep8 --repeat --show-source --ignore=E501 --exclude=.venv,.tox,dist,doc lesscpy From 0d9b732e825dda3a1daca2ca98dfc888e6b092c2 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Fri, 19 Jul 2013 11:10:05 +0200 Subject: [PATCH 02/23] Add .travis.yml for automated testing on Github --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6c84fed --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "2.6" + - "2.7" + #- "3.3" +# command to install dependencies +install: "pip install -r requirements.txt --use-mirrors" +# command to run tests +script: python lesscpy/test/__main__.py + From 2fe3b74c136b016447e22219521b31c06badf967 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Fri, 19 Jul 2013 11:21:39 +0200 Subject: [PATCH 03/23] Ignore MANIFEST --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fb269b5..46eae60 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ parser.out build dist *.pyc +MANIFEST From c0a265e1462fffedac8f73507dadd3ec8829b4df Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Fri, 19 Jul 2013 11:21:51 +0200 Subject: [PATCH 04/23] Fix PEP-8 issues (with autopep8 -a) --- lesscpy/lessc/__init__.py | 4 +- lesscpy/lessc/color.py | 110 +++++------ lesscpy/lessc/formatter.py | 15 +- lesscpy/lessc/lexer.py | 103 ++++++----- lesscpy/lessc/parser.py | 329 +++++++++++++++++---------------- lesscpy/lessc/scope.py | 63 ++++--- lesscpy/lessc/utility.py | 70 ++++--- lesscpy/lib/colors.py | 296 ++++++++++++++--------------- lesscpy/lib/css.py | 92 ++++----- lesscpy/lib/dom.py | 10 +- lesscpy/plib/__init__.py | 26 +-- lesscpy/plib/block.py | 28 +-- lesscpy/plib/call.py | 62 ++++--- lesscpy/plib/deferred.py | 44 ++--- lesscpy/plib/expression.py | 37 ++-- lesscpy/plib/identifier.py | 53 +++--- lesscpy/plib/mixin.py | 62 ++++--- lesscpy/plib/node.py | 19 +- lesscpy/plib/property.py | 22 ++- lesscpy/plib/statement.py | 9 +- lesscpy/plib/string.py | 7 +- lesscpy/plib/variable.py | 11 +- lesscpy/scripts/compiler.py | 71 +++---- lesscpy/test/__main__.py | 2 +- lesscpy/test/bootstrap.py | 2 +- lesscpy/test/testcolor.py | 67 +++---- lesscpy/test/testexpression.py | 14 +- lesscpy/test/testidentifier.py | 33 ++-- lesscpy/test/testissues.py | 24 ++- lesscpy/test/testless.py | 31 +++- lesscpy/test/testscope.py | 5 +- lesscpy/test/testutility.py | 23 +-- 32 files changed, 924 insertions(+), 820 deletions(-) diff --git a/lesscpy/lessc/__init__.py b/lesscpy/lessc/__init__.py index 4fe32e1..5fa344c 100644 --- a/lesscpy/lessc/__init__.py +++ b/lesscpy/lessc/__init__.py @@ -1,4 +1,4 @@ """ - Main lesscss parse library. Contains lexer and parser, along with + Main lesscss parse library. Contains lexer and parser, along with utility classes -""" \ No newline at end of file +""" diff --git a/lesscpy/lessc/color.py b/lesscpy/lessc/color.py index 4be850d..f10683e 100644 --- a/lesscpy/lessc/color.py +++ b/lesscpy/lessc/color.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.lessc.color :synopsis: Lesscpy Color functions - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -11,7 +11,9 @@ import colorsys from . import utility from lesscpy.lib import colors + class Color(): + def process(self, expression): """ Process color expression args: @@ -25,11 +27,13 @@ class Color(): r = ['#'] for i in range(3): v = self.operate(c1[i], c2[i], o) - if v > 0xff: v = 0xff - if v < 0: v = 0 + if v > 0xff: + v = 0xff + if v < 0: + v = 0 r.append("%02x" % v) return ''.join(r) - + def operate(self, left, right, operation): """ Do operation on colors args: @@ -46,7 +50,7 @@ class Color(): '/': '__truediv__' }.get(operation) return getattr(left, operation)(right) - + def rgb(self, *args): """ Translate rgb(...) to color string raises: @@ -60,13 +64,13 @@ class Color(): try: return self._rgbatohex(map(int, args)) except ValueError: - if all((a for a in args - if a[-1] == '%' + if all((a for a in args + if a[-1] == '%' and 100 >= int(a[:-1]) >= 0)): return self._rgbatohex([int(a[:-1]) * 255 / 100.0 for a in args]) raise ValueError('Illegal color values') - + def rgba(self, *args): """ Translate rgba(...) to color string raises: @@ -78,13 +82,13 @@ class Color(): try: return self._rgbatohex(map(int, args)) except ValueError: - if all((a for a in args - if a[-1] == '%' + if all((a for a in args + if a[-1] == '%' and 100 >= int(a[:-1]) >= 0)): return self._rgbatohex([int(a[:-1]) * 255 / 100.0 for a in args]) raise ValueError('Illegal color values') - + def hsl(self, *args): """ Translate hsl(...) to color string raises: @@ -96,13 +100,15 @@ class Color(): return self.hsla(*args) elif len(args) == 3: h, s, l = args - if type(l) == str: l = int(l.strip('%')) - if type(s) == str: s = int(s.strip('%')) + if isinstance(l, str): + l = int(l.strip('%')) + if isinstance(s, str): + s = int(s.strip('%')) rgb = colorsys.hls_to_rgb(int(h) / 360.0, l / 100.0, s / 100.0) color = (round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') - + def hsla(self, *args): """ Translate hsla(...) to color string raises: @@ -112,14 +118,16 @@ class Color(): """ if len(args) == 4: h, s, l, a = args - if type(l) == str: l = int(l.strip('%')) - if type(s) == str: s = int(s.strip('%')) + if isinstance(l, str): + l = int(l.strip('%')) + if isinstance(s, str): + s = int(s.strip('%')) rgb = colorsys.hls_to_rgb(int(h) / 360.0, l / 100.0, s / 100.0) color = [float(round(c * 255)) for c in rgb] color.append(round(float(a[:-1]) / 100.0, 2)) return "rgba(%s,%s,%s,%s)" % tuple(color) raise ValueError('Illegal color values') - + def hue(self, color, *args): """ Return the hue value of a color args: @@ -133,7 +141,7 @@ class Color(): h, l, s = self._hextohls(color) return round(h * 360.0, 3) raise ValueError('Illegal color values') - + def saturation(self, color, *args): """ Return the saturation value of a color args: @@ -147,7 +155,7 @@ class Color(): h, l, s = self._hextohls(color) return s * 100.0 raise ValueError('Illegal color values') - + def lightness(self, color, *args): """ Return the lightness value of a color args: @@ -161,12 +169,12 @@ class Color(): h, l, s = self._hextohls(color) return l * 100.0 raise ValueError('Illegal color values') - + def opacity(self, *args): """ """ pass - + def lighten(self, color, diff, *args): """ Lighten a color args: @@ -178,7 +186,7 @@ class Color(): if color and diff: return self._ophsl(color, diff, 1, '__add__') raise ValueError('Illegal color values') - + def darken(self, color, diff, *args): """ Darken a color args: @@ -190,7 +198,7 @@ class Color(): if color and diff: return self._ophsl(color, diff, 1, '__sub__') raise ValueError('Illegal color values') - + def saturate(self, color, diff, *args): """ Saturate a color args: @@ -202,7 +210,7 @@ class Color(): if color and diff: return self._ophsl(color, diff, 2, '__add__') raise ValueError('Illegal color values') - + def desaturate(self, color, diff, *args): """ Desaturate a color args: @@ -214,11 +222,11 @@ class Color(): if color and diff: return self._ophsl(color, diff, 2, '__sub__') raise ValueError('Illegal color values') - + def _clamp(self, value): # Clamp value return min(1, max(0, value)) - + def grayscale(self, color, *args): """ Simply 100% desaturate. args: @@ -234,7 +242,7 @@ class Color(): """Wrapper for grayscale, other spelling """ return self.grayscale(color, *args) - + def spin(self, color, degree, *args): """ Spin color by degree. (Increase / decrease hue) args: @@ -246,7 +254,7 @@ class Color(): str """ if color and degree: - if type(degree) == str: + if isinstance(degree, str): degree = int(degree.strip('%')) h, l, s = self._hextohls(color) h = ((h * 360.0) + degree) % 360.0 @@ -255,29 +263,29 @@ class Color(): color = (round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') - + def mix(self, color1, color2, weight=50, *args): """This algorithm factors in both the user-provided weight and the difference between the alpha values of the two colors to decide how to perform the weighted average of the two RGB values. - + It works by first normalizing both parameters to be within [-1, 1], where 1 indicates "only use color1", -1 indicates "only use color 0", and all values in between indicated a proportionately weighted average. - + Once we have the normalized variables w and a, we apply the formula (w + a)/(1 + w*a) to get the combined weight (in [-1, 1]) of color1. This formula has two especially nice properties: - + * When either w or a are -1 or 1, the combined weight is also that number (cases where w * a == -1 are undefined, and handled as a special case). - + * When a is 0, the combined weight is w, and vice versa - + Finally, the weight of color1 is renormalized to be within [0, 1] and the weight of color2 is given by 1 minus the weight of color1. - + Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein http://sass-lang.com args: @@ -290,13 +298,13 @@ class Color(): str """ if color1 and color2: - if type(weight) == str: + if isinstance(weight, str): weight = int(weight.strip('%')) weight = ((weight / 100.0) * 2) - 1 rgb1 = self._hextorgb(color1) rgb2 = self._hextorgb(color2) alpha = 0 - w1 = (((weight if weight * alpha == -1 + w1 = (((weight if weight * alpha == -1 else weight + alpha) / (1 + weight * alpha)) + 1) w1 = w1 / 2.0 w2 = 1 - w1 @@ -307,7 +315,7 @@ class Color(): ] return self._rgbatohex(rgb) raise ValueError('Illegal color values') - + def fmt(self, color): """ Format CSS Hex color code. uppercase becomes lowercase, 3 digit codes expand to 6 digit. @@ -324,15 +332,16 @@ class Color(): color = ''.join([c * 2 for c in color]) return '#%s' % color raise ValueError('Cannot format non-color') - + def _rgbatohex(self, rgba): - return '#%s' % ''.join(["%02x" % v for v in - [0xff - if h > 0xff else - 0 if h < 0 else h + return '#%s' % ''.join(["%02x" % v for v in + [0xff + if h > 0xff else + 0 if h < 0 else h for h in rgba] ]) - def _hextorgb(self, hex): + + def _hextorgb(self, hex): if hex.lower() in colors.lessColors: hex = colors.lessColors[hex.lower()] hex = hex.strip() @@ -341,22 +350,19 @@ class Color(): if len(hex) == 3: hex = [c * 2 for c in hex] else: - hex = [hex[i:i+2] for i in range(0, len(hex), 2)] + hex = [hex[i:i + 2] for i in range(0, len(hex), 2)] return tuple(int(c, 16) for c in hex) return [int(hex, 16)] * 3 - + def _hextohls(self, hex): rgb = self._hextorgb(hex) return colorsys.rgb_to_hls(*[c / 255.0 for c in rgb]) - + def _ophsl(self, color, diff, idx, op): - if type(diff) == str: diff = int(diff.strip('%')) + if isinstance(diff, str): + diff = int(diff.strip('%')) hls = list(self._hextohls(color)) hls[idx] = self._clamp(getattr(hls[idx], op)(diff / 100.0)) rgb = colorsys.hls_to_rgb(*hls) color = (round(c * 255) for c in rgb) return self._rgbatohex(color) - - - - \ No newline at end of file diff --git a/lesscpy/lessc/formatter.py b/lesscpy/lessc/formatter.py index 3066a87..796b79e 100644 --- a/lesscpy/lessc/formatter.py +++ b/lesscpy/lessc/formatter.py @@ -2,21 +2,24 @@ """ .. module:: lesscpy.lessc.formatter :synopsis: CSS Formatter class. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ + + class Formatter(object): + def __init__(self, args): self.args = args - + def format(self, parse): """ """ if not parse.result: return '' - eb = '\n' + eb = '\n' if self.args.xminify: eb = '' self.args.minify = True @@ -36,9 +39,7 @@ class Formatter(object): 'ws': ' ', 'eb': eb }) - self.out = [u.fmt(self.items) - for u in parse.result + self.out = [u.fmt(self.items) + for u in parse.result if u] return ''.join(self.out).strip() - - \ No newline at end of file diff --git a/lesscpy/lessc/lexer.py b/lesscpy/lessc/lexer.py index 833e4a5..662f776 100644 --- a/lesscpy/lessc/lexer.py +++ b/lesscpy/lessc/lexer.py @@ -1,10 +1,10 @@ """ Lexer for LESSCSS. - + http://www.dabeaz.com/ply/ply.html http://www.w3.org/TR/CSS21/grammar.html#scanner http://lesscss.org/#docs - + Copyright (c) See LICENSE for details. @@ -15,11 +15,12 @@ import ply.lex as lex from lesscpy.lib import dom from lesscpy.lib import css + class LessLexer: states = ( - ('parn', 'inclusive'), + ('parn', 'inclusive'), ) - literals = ',<>{}=%!/*-+:;~&'; + literals = ',<>{}=%!/*-+:;~&' tokens = [ 'css_ident', 'css_dom', @@ -36,7 +37,7 @@ class LessLexer: 'css_vendor_hack', 'css_uri', 'css_ms_filter', - + 'less_variable', 'less_comment', 'less_string', @@ -44,31 +45,31 @@ class LessLexer: 'less_when', 'less_and', 'less_not', - + 't_ws', 't_popen', 't_pclose', ] reserved = { - '@media' : 'css_media', + '@media': 'css_media', '@page': 'css_page', - '@import' : 'css_import', - '@charset' : 'css_charset', - '@font-face' : 'css_font_face', - '@namespace' : 'css_namespace', - '@keyframes' : 'css_keyframes', - '@-moz-keyframes' : 'css_keyframes', - '@-webkit-keyframes' : 'css_keyframes', - '@-ms-keyframes' : 'css_keyframes', - '@-o-keyframes' : 'css_keyframes', - + '@import': 'css_import', + '@charset': 'css_charset', + '@font-face': 'css_font_face', + '@namespace': 'css_namespace', + '@keyframes': 'css_keyframes', + '@-moz-keyframes': 'css_keyframes', + '@-webkit-keyframes': 'css_keyframes', + '@-ms-keyframes': 'css_keyframes', + '@-o-keyframes': 'css_keyframes', + '@arguments': 'less_arguments', } tokens += list(set(reserved.values())) # Tokens with significant following whitespace significant_ws = [ - 'css_class', - 'css_id', + 'css_class', + 'css_id', 'css_dom', 'css_property', 'css_vendor_property', @@ -79,23 +80,23 @@ class LessLexer: '&', ] significant_ws += list(set(reserved.values())) - + def __init__(self): - self.build(reflags=re.UNICODE|re.IGNORECASE) + self.build(reflags=re.UNICODE | re.IGNORECASE) self.last = None self.next = None self.pretok = True - + def t_css_filter(self, t): (r'\[[^\]]*\]' - '|(not|lang|nth-[a-z\-]+)\(.+\)' - '|and[ \t]\([^><\{]+\)') + '|(not|lang|nth-[a-z\-]+)\(.+\)' + '|and[ \t]\([^><\{]+\)') return t - + def t_css_ms_filter(self, t): r'progid:[^;]*' return t - + def t_css_ident(self, t): (r'([\-\.\#]?' '|@[@\-]?)' @@ -140,19 +141,19 @@ class LessLexer: t.type = 'css_vendor_property' t.value = t.value.strip() return t - + def t_less_variable(self, t): r'@\w+' return t - + def t_css_color(self, t): r'\#[0-9]([0-9a-f]{5}|[0-9a-f]{2})' return t - + def t_css_number(self, t): r'-?(\d*\.\d+|\d+)(s|%|in|ex|[ecm]m|p[txc]|deg|g?rad|ms?|k?hz)?' return t - + def t_parn_css_uri(self, t): (r'data:[^\)]+' '|(([a-z]+://)?' @@ -162,7 +163,7 @@ class LessLexer: '(\#[a-z]+)?)' ')+') return t - + def t_parn_css_ident(self, t): (r'(([_a-z]' '|[\200-\377]' @@ -172,11 +173,11 @@ class LessLexer: '|\\\[0-9a-f]{1,6}' '|\\\[^\r\n\s0-9a-f])*)') return t - + def t_newline(self, t): r'[\n\r]+' t.lexer.lineno += t.value.count('\n') - + def t_css_comment(self, t): r'(/\*(.|\n|\r)*?\*/)' t.lexer.lineno += t.value.count('\n') @@ -185,12 +186,12 @@ class LessLexer: def t_less_comment(self, t): r'//.*' pass - + def t_css_important(self, t): r'!\s*important' t.value = '!important' return t - + def t_t_ws(self, t): r'[ \t\f\v]+' t.value = ' ' @@ -200,37 +201,38 @@ class LessLexer: r'\(' t.lexer.push_state('parn') return t - + def t_less_open_format(self, t): r'%\(' t.lexer.push_state('parn') return t - + def t_t_pclose(self, t): r'\)' t.lexer.pop_state() return t - + def t_less_string(self, t): (r'"([^"@]*@\{[^"\}]+\}[^"]*)+"' - '|\'([^\'@]*@\{[^\'\}]+\}[^\']*)+\'') + '|\'([^\'@]*@\{[^\'\}]+\}[^\']*)+\'') t.lexer.lineno += t.value.count('\n') return t - + def t_css_string(self, t): r'"[^"]*"|\'[^\']*\'' t.lexer.lineno += t.value.count('\n') return t - + # Error handling rule def t_error(self, t): - raise SyntaxError("Illegal character '%s' line %d" % (t.value[0], t.lexer.lineno)) + raise SyntaxError("Illegal character '%s' line %d" % + (t.value[0], t.lexer.lineno)) t.lexer.skip(1) - + # Build the lexer def build(self, **kwargs): - self.lexer = lex.lex(module=self, **kwargs) - + self.lexer = lex.lex(module=self, **kwargs) + def file(self, filename): """ Lex file. @@ -238,14 +240,14 @@ class LessLexer: with open(filename) as f: self.lexer.input(f.read()) return self - + def input(self, filename): """ Wrapper for file """ with open(filename) as f: self.lexer.input(f.read()) - + def token(self): """ Token function. Contains 2 hacks: @@ -260,10 +262,11 @@ class LessLexer: return t while True: t = self.lexer.token() - if not t: return t + if not t: + return t if t.type == 't_ws' and ( - self.pretok or (self.last - and self.last.type not in self.significant_ws)): + self.pretok or (self.last + and self.last.type not in self.significant_ws)): continue self.pretok = False if t.type == '}' and self.last and self.last.type not in '{;}': diff --git a/lesscpy/lessc/parser.py b/lesscpy/lessc/parser.py index 55488a1..a744679 100644 --- a/lesscpy/lessc/parser.py +++ b/lesscpy/lessc/parser.py @@ -3,11 +3,11 @@ from __future__ import print_function """ .. module:: lesscpy.lessc.parser :synopsis: Lesscss parser. - + http://www.dabeaz.com/ply/ply.html http://www.w3.org/TR/CSS21/grammar.html#scanner http://lesscss.org/#docs - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -21,23 +21,25 @@ from .scope import Scope from .color import Color from lesscpy.plib import * + class LessParser(object): precedence = ( - ('left', '+', '-'), - ('left', '*', '/'), + ('left', '+', '-'), + ('left', '*', '/'), ) - def __init__(self, - lex_optimize=True, - yacc_optimize=True, - tabfile='yacctab', - yacc_debug=False, - scope=None, - outputdir='/tmp', - importlvl=0, - verbose=False - ): + + def __init__(self, + lex_optimize=True, + yacc_optimize=True, + tabfile='yacctab', + yacc_debug=False, + scope=None, + outputdir='/tmp', + importlvl=0, + verbose=False + ): """ Parser object - + Kwargs: lex_optimize (bool): Optimize lexer yacc_optimize (bool): Optimize parser @@ -48,19 +50,19 @@ class LessParser(object): importlvl (int): Import depth verbose (bool): Verbose mode """ - self.verbose = verbose - self.importlvl = importlvl - self.lex = lexer.LessLexer() + self.verbose = verbose + self.importlvl = importlvl + self.lex = lexer.LessLexer() if not tabfile: tabfile = 'yacctab' - + self.ignored = ('css_comment', 'less_comment', 'css_vendor_hack') - - self.tokens = [t for t in self.lex.tokens + + self.tokens = [t for t in self.lex.tokens if t not in self.ignored] self.parser = ply.yacc.yacc( - module=self, + module=self, start='tunit', debug=yacc_debug, optimize=yacc_optimize, @@ -71,22 +73,24 @@ class LessParser(object): self.stash = {} self.result = None self.target = None - + def parse(self, filename='', debuglevel=0): """ Parse file. kwargs: filename (str): File to parse debuglevel (int): Parser debuglevel """ - if self.verbose: print('Compiling target: %s' % filename, file=sys.stderr) + if self.verbose: + print('Compiling target: %s' % filename, file=sys.stderr) self.scope.push() self.target = filename - self.result = self.parser.parse(filename, lexer=self.lex, debug=debuglevel) + self.result = self.parser.parse( + filename, lexer=self.lex, debug=debuglevel) self.post_parse() - + def post_parse(self): """ Post parse cycle. nodejs version allows calls to mixins - not yet defined or known to the parser. We defer all calls + not yet defined or known to the parser. We defer all calls to mixins until after first cycle when all names are known. """ if self.result: @@ -97,35 +101,35 @@ class LessParser(object): except SyntaxError as e: self.handle_error(e, 0) self.result = list(utility.flatten(out)) - + def scopemap(self): """ Output scopemap. """ utility.debug_print(self.result) - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_tunit(self, p): """ tunit : unit_list """ p[0] = [u for u in p[1] if u] - + def p_unit_list(self, p): """ unit_list : unit_list unit | unit """ - if type(p[1]) is list: + if isinstance(p[1], list): if len(p) >= 3: - if type(p[2]) is list: + if isinstance(p[2], list): p[1].extend(p[2]) else: p[1].append(p[2]) else: - p[1] = [p[1]] + p[1] = [p[1]] p[0] = p[1] - + def p_unit(self, p): """ unit : statement | variable_decl @@ -135,39 +139,41 @@ class LessParser(object): | import_statement """ p[0] = p[1] - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# - +# + def p_statement_aux(self, p): """ statement : css_charset t_ws css_string ';' | css_namespace t_ws css_string ';' """ p[0] = Statement(list(p)[1:], p.lineno(1)) p[0].parse(None) - + def p_statement_namespace(self, p): """ statement : css_namespace t_ws word css_string ';' """ p[0] = Statement(list(p)[1:], p.lineno(1)) p[0].parse(None) - + def p_statement_import(self, p): """ import_statement : css_import t_ws css_string ';' | css_import t_ws css_string dom ';' """ if self.importlvl > 8: - raise ImportError('Recrusive import level too deep > 8 (circular import ?)') + raise ImportError( + 'Recrusive import level too deep > 8 (circular import ?)') ipath = utility.destring(p[3]) fn, fe = os.path.splitext(ipath) if not fe or fe.lower() == '.less': try: cpath = os.path.dirname(os.path.abspath(self.target)) - if not fe: ipath += '.less' + if not fe: + ipath += '.less' filename = "%s%s%s" % (cpath, os.sep, ipath) if os.path.exists(filename): - recurse = LessParser(importlvl=self.importlvl+1, + recurse = LessParser(importlvl=self.importlvl + 1, verbose=self.verbose, scope=self.scope) recurse.parse(filename=filename, debuglevel=0) p[0] = recurse.result @@ -181,10 +187,10 @@ class LessParser(object): p[0] = Statement(list(p)[1:], p.lineno(1)) p[0].parse(None) sys.stdout.flush() - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_block(self, p): """ block_decl : block_open declaration_list brace_close @@ -192,7 +198,7 @@ class LessParser(object): p[0] = Block(list(p)[1:-1], p.lineno(3)) self.scope.pop() self.scope.add_block(p[0]) - + def p_block_replace(self, p): """ block_decl : identifier ';' """ @@ -203,7 +209,7 @@ class LessParser(object): else: # fallback to mixin. Allow calls to mixins without parens p[0] = Deferred(p[1], None, p.lineno(2)) - + def p_block_open(self, p): """ block_open : identifier brace_open """ @@ -213,7 +219,7 @@ class LessParser(object): pass p[0] = p[1] self.scope.current = p[1] - + def p_font_face_open(self, p): """ block_open : css_font_face t_ws brace_open """ @@ -221,7 +227,7 @@ class LessParser(object): # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_mixin(self, p): """ mixin_decl : open_mixin declaration_list brace_close @@ -241,12 +247,12 @@ class LessParser(object): p[0].append(p[5]) else: p[0].append(None) - + def p_mixin_guard(self, p): """ mixin_guard : less_when mixin_guard_cond_list """ p[0] = p[2] - + def p_mixin_guard_cond_list_aux(self, p): """ mixin_guard_cond_list : mixin_guard_cond_list ',' mixin_guard_cond | mixin_guard_cond_list less_and mixin_guard_cond @@ -254,24 +260,24 @@ class LessParser(object): p[1].append(p[2]) p[1].append(p[3]) p[0] = p[1] - + def p_mixin_guard_cond_list(self, p): """ mixin_guard_cond_list : mixin_guard_cond """ p[0] = [p[1]] - + def p_mixin_guard_cond_rev(self, p): """ mixin_guard_cond : less_not t_popen argument mixin_guard_cmp argument t_pclose | less_not t_popen argument t_pclose """ p[0] = utility.reverse_guard(list(p)[3:-1]) - + def p_mixin_guard_cond(self, p): """ mixin_guard_cond : t_popen argument mixin_guard_cmp argument t_pclose | t_popen argument t_pclose """ p[0] = list(p)[2:-1] - + def p_mixin_guard_cmp(self, p): """ mixin_guard_cmp : '>' | '<' @@ -282,24 +288,24 @@ class LessParser(object): | '<' '>' """ p[0] = ''.join(list(p)[1:]) - + def p_call_mixin(self, p): """ call_mixin : identifier t_popen mixin_args_list t_pclose ';' """ p[1].parse(None) p[0] = Deferred(p[1], p[3], p.lineno(4)) - + def p_mixin_args_arguments(self, p): """ mixin_args_list : less_arguments """ p[0] = [p[1]] - + def p_mixin_args_list_aux(self, p): """ mixin_args_list : mixin_args_list ',' mixin_args """ p[1].extend([p[3]]) p[0] = p[1] - + def p_mixin_args_list(self, p): """ mixin_args_list : mixin_args """ @@ -316,31 +322,31 @@ class LessParser(object): | mixin_kwarg """ p[0] = [p[1]] - + def p_mixin_args_empty(self, p): """ mixin_args : empty """ p[0] = None - + def p_mixin_kwarg(self, p): """ mixin_kwarg : variable ':' mixin_kwarg_arg_list """ p[0] = Variable(list(p)[1:], p.lineno(2)) - + def p_margument_list_aux(self, p): """ mixin_kwarg_arg_list : mixin_kwarg_arg_list argument """ p[1].extend(list(p)[2:]) p[0] = p[1] - + def p_margument_list(self, p): """ mixin_kwarg_arg_list : argument """ p[0] = [p[1]] - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_declaration_list(self, p): """ declaration_list : declaration_list declaration @@ -350,7 +356,7 @@ class LessParser(object): if len(p) > 2: p[1].extend(p[2]) p[0] = p[1] - + def p_declaration(self, p): """ declaration : variable_decl | property_decl @@ -359,11 +365,11 @@ class LessParser(object): | call_mixin | import_statement """ - p[0] = p[1] if type(p[1]) is list else [p[1]] - + p[0] = p[1] if isinstance(p[1], list) else [p[1]] + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_variable_decl(self, p): """ variable_decl : variable ':' style_list ';' @@ -371,10 +377,10 @@ class LessParser(object): p[0] = Variable(list(p)[1:-1], p.lineno(4)) p[0].parse(self.scope) self.scope.add_variable(p[0]) - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_property_decl(self, p): """ property_decl : prop_open style_list ';' @@ -382,29 +388,29 @@ class LessParser(object): | prop_open empty ';' """ l = len(p) - p[0] = Property(list(p)[1:-1], p.lineno(l-1)) - + p[0] = Property(list(p)[1:-1], p.lineno(l - 1)) + def p_property_decl_arguments(self, p): """ property_decl : prop_open less_arguments ';' """ p[0] = Property([p[1], [p[2]]], p.lineno(3)) - + def p_prop_open_ie_hack(self, p): """ prop_open : '*' prop_open """ p[0] = (p[1][0], p[2][0]) - + def p_prop_open(self, p): """ prop_open : property ':' | vendor_property ':' | word ':' """ p[0] = (p[1][0], '') - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# - +# + def p_style_list_aux(self, p): """ style_list : style_list style | style_list ',' style @@ -412,12 +418,12 @@ class LessParser(object): """ p[1].extend(list(p)[2:]) p[0] = p[1] - + def p_style_list(self, p): """ style_list : style """ p[0] = [p[1]] - + def p_style(self, p): """ style : expression | css_string @@ -429,10 +435,10 @@ class LessParser(object): | css_ms_filter """ p[0] = p[1] - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_identifier(self, p): """ identifier : identifier_list @@ -440,7 +446,7 @@ class LessParser(object): | page filter """ p[0] = Identifier(p[1], 0) - + def p_identifier_istr(self, p): """ identifier : t_popen '~' istring t_pclose """ @@ -452,18 +458,18 @@ class LessParser(object): p[1].extend([p[2]]) p[1].extend(p[3]) p[0] = p[1] - + def p_identifier_list(self, p): """ identifier_list : identifier_group """ p[0] = p[1] - + def p_identifier_list_keyframe(self, p): """ identifier_list : css_keyframes t_ws css_ident | css_keyframes t_ws css_ident t_ws """ p[0] = list(p)[1:] - + def p_identifier_group_op(self, p): """ identifier_group : identifier_group child_selector ident_parts | identifier_group '+' ident_parts @@ -471,38 +477,40 @@ class LessParser(object): | identifier_group '*' """ p[1].extend([p[2]]) - if len(p) > 3: p[1].extend(p[3]) + if len(p) > 3: + p[1].extend(p[3]) p[0] = p[1] - + def p_identifier_group(self, p): """ identifier_group : ident_parts """ p[0] = p[1] - + def p_ident_parts_aux(self, p): """ ident_parts : ident_parts ident_part | ident_parts filter_group """ - if type(p[2]) is list: + if isinstance(p[2], list): p[1].extend(p[2]) - else: p[1].append(p[2]) + else: + p[1].append(p[2]) p[0] = p[1] - + def p_ident_parts(self, p): """ ident_parts : ident_part | selector | filter_group """ - if type(p[1]) is not list: + if not isinstance(p[1], list): p[1] = [p[1]] p[0] = p[1] - + def p_ident_media(self, p): """ ident_parts : css_media t_ws | css_media t_ws t_popen word ':' number t_pclose """ p[0] = list(p)[1:] - + def p_selector(self, p): """ selector : '*' | '+' @@ -510,7 +518,7 @@ class LessParser(object): | general_sibling_selector """ p[0] = p[1] - + def p_ident_part(self, p): """ ident_part : class | id @@ -519,22 +527,22 @@ class LessParser(object): | color """ p[0] = p[1] - + def p_ident_part_aux(self, p): """ ident_part : combinator vendor_property """ p[0] = [p[1], p[2]] - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_filter_group_aux(self, p): """ filter_group : filter_group filter """ p[1].extend(p[2]) p[0] = p[1] - + def p_filter_group(self, p): """ filter_group : filter """ @@ -552,11 +560,11 @@ class LessParser(object): | ':' ':' vendor_property """ p[0] = list(p)[1:] - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# - +# + def p_fcall(self, p): """ fcall : word t_popen argument_list t_pclose | property t_popen argument_list t_pclose @@ -566,28 +574,28 @@ class LessParser(object): | '~' css_string """ p[0] = Call(list(p)[1:], 0) - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_argument_list_empty(self, p): """ argument_list : empty """ p[0] = '' - + def p_argument_list_aux(self, p): """ argument_list : argument_list argument | argument_list ',' argument """ p[1].extend(list(p)[2:]) p[0] = p[1] - + def p_argument_list(self, p): """ argument_list : argument """ p[0] = [p[1]] - + def p_argument(self, p): """ argument : expression | css_string @@ -599,10 +607,10 @@ class LessParser(object): | fcall """ p[0] = p[1] - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_expression_aux(self, p): """ expression : expression '+' expression @@ -612,160 +620,161 @@ class LessParser(object): | word '/' expression """ p[0] = Expression(list(p)[1:], 0) - + def p_expression_p_neg(self, p): """ expression : '-' t_popen expression t_pclose """ p[0] = [p[1], p[3]] - + def p_expression_p(self, p): """ expression : t_popen expression t_pclose """ p[0] = p[2] - + def p_expression(self, p): """ expression : factor """ p[0] = p[1] - - def p_factor(self, p): + + def p_factor(self, p): """ factor : color | number | variable | css_dom """ p[0] = p[1] - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_interpolated_str(self, p): """ istring : less_string """ p[0] = String(p[1], p.lineno(1)) - + def p_variable_neg(self, p): """ variable : '-' variable """ - p[0] = ['-', p[2]] - + p[0] = ['-', p[2]] + def p_variable_strange(self, p): """ variable : t_popen variable t_pclose """ p[0] = p[2] - + def p_variable(self, p): """ variable : less_variable | less_variable t_ws """ # p[0] = p[1] - p[0] = tuple(list(p)[1:]) + p[0] = tuple(list(p)[1:]) def p_color(self, p): """ color : css_color | css_color t_ws """ try: - p[0] = Color().fmt(p[1]) - if len(p) > 2: p[0] = [p[0], p[2]] + p[0] = Color().fmt(p[1]) + if len(p) > 2: + p[0] = [p[0], p[2]] except ValueError: - self.handle_error('Illegal color value `%s`' % p[1], p.lineno(1), 'W') + self.handle_error( + 'Illegal color value `%s`' % p[1], p.lineno(1), 'W') p[0] = p[1] - + def p_number(self, p): """ number : css_number | css_number t_ws - """ - p[0] = tuple(list(p)[1:]) - + """ + p[0] = tuple(list(p)[1:]) + def p_dom(self, p): """ dom : css_dom | css_dom t_ws """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_word(self, p): """ word : css_ident | css_ident t_ws """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_class(self, p): """ class : css_class | css_class t_ws """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_id(self, p): """ id : css_id | css_id t_ws """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_property(self, p): """ property : css_property | css_property t_ws """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_page(self, p): """ page : css_page | css_page t_ws """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_vendor_property(self, p): """ vendor_property : css_vendor_property | css_vendor_property t_ws """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_combinator(self, p): """ combinator : '&' t_ws | '&' """ - p[0] = tuple(list(p)[1:]) - - + p[0] = tuple(list(p)[1:]) + def p_child_selector(self, p): """ child_selector : '>' t_ws | '>' """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_general_sibling_selector(self, p): """ general_sibling_selector : '~' t_ws | '~' """ - p[0] = tuple(list(p)[1:]) - + p[0] = tuple(list(p)[1:]) + def p_scope_open(self, p): """ brace_open : '{' """ self.scope.push() p[0] = p[1] - + def p_scope_close(self, p): """ brace_close : '}' """ p[0] = p[1] - + def p_empty(self, p): 'empty :' pass - + # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# def p_error(self, t): """ Internal error handler args: - t (Lex token): Error token + t (Lex token): Error token """ - if t: - print("\x1b[31mE: %s line: %d, Syntax Error, token: `%s`, `%s`\x1b[0m" + if t: + print("\x1b[31mE: %s line: %d, Syntax Error, token: `%s`, `%s`\x1b[0m" % (self.target, t.lineno, t.type, t.value), file=sys.stderr) while True: t = self.lex.token() @@ -775,7 +784,7 @@ class LessParser(object): break self.parser.restart() return t - + def handle_error(self, e, line, t='E'): """ Custom error handler args: @@ -785,5 +794,5 @@ class LessParser(object): """ # print(e.trace()) color = '\x1b[31m' if t == 'E' else '\x1b[33m' - print("%s%s: line: %d: %s\n" % (color, t, line, e), end='\x1b[0m', file=sys.stderr) - + print("%s%s: line: %d: %s\n" % + (color, t, line, e), end='\x1b[0m', file=sys.stderr) diff --git a/lesscpy/lessc/scope.py b/lesscpy/lessc/scope.py index 04b4566..f674fa4 100644 --- a/lesscpy/lessc/scope.py +++ b/lesscpy/lessc/scope.py @@ -1,17 +1,20 @@ """ .. module:: lesscpy.lessc.scope :synopsis: Scope class. - - + + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from . import utility + class Scope(list): + """ Scope class. A stack implementation. """ + def __init__(self, init=False): """Scope Args: @@ -19,39 +22,39 @@ class Scope(list): """ super(Scope, self).__init__() self._mixins = {} - if init: self.push() + if init: + self.push() self.deferred = False self.real = [] - + def push(self): """Push level on scope """ self.append({ - '__variables__' : {}, - '__blocks__': [], + '__variables__': {}, + '__blocks__': [], '__names__': [], '__current__': None }) - + @property def current(self): return self[-1]['__current__'] - + @current.setter def current(self, value): self[-1]['__current__'] = value - + @property def scopename(self): """Current scope name as list Returns: list """ - return [r['__current__'] - for r in self + return [r['__current__'] + for r in self if r['__current__']] - def add_block(self, block): """Add block element to scope Args: @@ -59,7 +62,7 @@ class Scope(list): """ self[-1]['__blocks__'].append(block) self[-1]['__names__'].append(block.raw()) - + def add_mixin(self, mixin): """Add mixin to scope Args: @@ -70,14 +73,14 @@ class Scope(list): self._mixins[raw].append(mixin) else: self._mixins[raw] = [mixin] - + def add_variable(self, variable): """Add variable to scope Args: variable (Variable): Variable object """ self[-1]['__variables__'][variable.name] = variable - + def variables(self, name): """Search for variable by name. Searches scope top down Args: @@ -85,7 +88,7 @@ class Scope(list): Returns: Variable object OR False """ - if type(name) is tuple: + if isinstance(name, tuple): name = name[0] i = len(self) while i >= 0: @@ -93,7 +96,7 @@ class Scope(list): if name in self[i]['__variables__']: return self[i]['__variables__'][name] return False - + def mixins(self, name): """ Search mixins for name. Allow '>' to be ignored. '.a .b()' == '.a > .b()' @@ -103,16 +106,17 @@ class Scope(list): Mixin object list OR False """ m = self._smixins(name) - if m: return m + if m: + return m return self._smixins(name.replace('?>?', ' ')) - + def _smixins(self, name): """Inner wrapper to search for mixins by name. """ - return (self._mixins[name] + return (self._mixins[name] if name in self._mixins else False) - + def blocks(self, name): """ Search for defined blocks recursively. @@ -123,9 +127,10 @@ class Scope(list): Block object OR False """ b = self._blocks(name) - if b: return b + if b: + return b return self._blocks(name.replace('?>?', ' ')) - + def _blocks(self, name): """Inner wrapper to search for blocks by name. """ @@ -142,9 +147,10 @@ class Scope(list): r = b.raw() if r and name.startswith(r): b = utility.blocksearch(b, name) - if b: return b + if b: + return b return False - + def update(self, scope, at=0): """Update scope. Add another scope to this one. Args: @@ -157,7 +163,7 @@ class Scope(list): self[at]['__variables__'].update(scope[at]['__variables__']) self[at]['__blocks__'].extend(scope[at]['__blocks__']) self[at]['__names__'].extend(scope[at]['__names__']) - + def swap(self, name): """ Swap variable name for variable value Args: @@ -167,11 +173,10 @@ class Scope(list): """ if name.startswith('@@'): var = self.variables(name[1:]) - if var is False: + if var is False: raise SyntaxError('Unknown variable %s' % name) name = '@' + utility.destring(var.value[0]) var = self.variables(name) - if var is False: + if var is False: raise SyntaxError('Unknown variable %s' % name) return var.value - diff --git a/lesscpy/lessc/utility.py b/lesscpy/lessc/utility.py index 4d15bfb..b74cb7f 100644 --- a/lesscpy/lessc/utility.py +++ b/lesscpy/lessc/utility.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.lessc.utility :synopsis: various utility functions - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -10,11 +10,12 @@ import collections import re + def flatten(lst): """Flatten list. Args: lst (list): List to flatten - Returns: + Returns: generator """ for elm in lst: @@ -23,7 +24,8 @@ def flatten(lst): yield sub else: yield elm - + + def pairwise(lst): """ yield item i and item i+1 in lst. e.g. (lst[0], lst[1]), (lst[1], lst[2]), ..., (lst[-1], None) @@ -32,29 +34,31 @@ def pairwise(lst): Returns: list """ - if not lst: + if not lst: return length = len(lst) - for i in range(length-1): - yield lst[i], lst[i+1] + for i in range(length - 1): + yield lst[i], lst[i + 1] yield lst[-1], None - + + def rename(blocks, scope, stype): - """ Rename all sub-blocks moved under another + """ Rename all sub-blocks moved under another block. (mixins) Args: lst (list): block list scope (object): Scope object """ for p in blocks: - if type(p) is stype: + if isinstance(p, stype): p.tokens[0].parse(scope) - if p.tokens[1]: + if p.tokens[1]: scope.push() scope.current = p.tokens[0] rename(p.tokens[1], scope, stype) scope.pop() - + + def blocksearch(block, name): """ Recursive search for name in block (inner blocks) Args: @@ -64,13 +68,15 @@ def blocksearch(block, name): """ if hasattr(block, 'tokens'): for b in block.tokens[1]: - b = (b if hasattr(b, 'raw') and b.raw() == name + b = (b if hasattr(b, 'raw') and b.raw() == name else blocksearch(b, name)) - if b: return b + if b: + return b return False + def reverse_guard(lst): - """ Reverse guard expression. not + """ Reverse guard expression. not (@a > 5) -> (@a <= 5) Args: lst (list): Expression @@ -87,6 +93,7 @@ def reverse_guard(lst): } return [rev[l] if l in rev else l for l in lst] + def debug_print(lst, lvl=0): """ Print scope tree args: @@ -99,8 +106,9 @@ def debug_print(lst, lvl=0): for p in lst: debug_print(p, lvl) elif hasattr(lst, 'tokens'): - print(pad, t) - debug_print(list(flatten(lst.tokens)), lvl+1) + print(pad, t) + debug_print(list(flatten(lst.tokens)), lvl + 1) + def destring(value): """ Strip quotes from string @@ -111,6 +119,7 @@ def destring(value): """ return value.strip('"\'') + def analyze_number(var, err=''): """ Analyse number for type and split from unit 1px -> (q, 'px') @@ -124,7 +133,7 @@ def analyze_number(var, err=''): tuple """ n, u = split_unit(var) - if type(var) is not str: + if not isinstance(var, str): return (var, u) if is_color(var): return (var, 'color') @@ -136,6 +145,7 @@ def analyze_number(var, err=''): raise SyntaxError('%s ´%s´' % (err, var)) return (n, u) + def with_unit(number, unit=None): """ Return number with unit args: @@ -144,17 +154,18 @@ def with_unit(number, unit=None): returns: str """ - if type(number) is tuple: + if isinstance(number, tuple): number, unit = number - if number == 0: + if number == 0: return '0' if unit: number = str(number) if number.startswith('.'): number = '0' + number return "%s%s" % (number, unit) - return number if type(number) is str else str(number) - + return number if isinstance(number, str) else str(number) + + def is_color(value): """ Is string CSS color args: @@ -162,7 +173,7 @@ def is_color(value): returns: bool """ - if not value or type(value) is not str: + if not value or not isinstance(value, str): return False if value[0] == '#' and len(value) in [4, 5, 7, 9]: try: @@ -171,7 +182,8 @@ def is_color(value): except ValueError: pass return False - + + def is_variable(value): """ Check if string is LESS variable args: @@ -179,13 +191,14 @@ def is_variable(value): returns: bool """ - if type(value) is str: + if isinstance(value, str): return (value.startswith('@') or value.startswith('-@')) - elif type(value) is tuple: + elif isinstance(value, tuple): value = ''.join(value) return (value.startswith('@') or value.startswith('-@')) return False + def is_int(value): """ Is value integer args: @@ -200,6 +213,7 @@ def is_int(value): pass return False + def is_float(value): """ Is value float args: @@ -215,6 +229,7 @@ def is_float(value): pass return False + def split_unit(value): """ Split a number from its unit 1px -> (q, 'px') @@ -224,7 +239,4 @@ def split_unit(value): tuple """ r = re.search('^(\-?[\d\.]+)(.*)$', str(value)) - return r.groups() if r else ('','') - - - \ No newline at end of file + return r.groups() if r else ('', '') diff --git a/lesscpy/lib/colors.py b/lesscpy/lib/colors.py index a087a6b..4266e07 100644 --- a/lesscpy/lib/colors.py +++ b/lesscpy/lib/colors.py @@ -1,151 +1,151 @@ """ """ lessColors = { - 'aliceblue':'#f0f8ff', - 'antiquewhite':'#faebd7', - 'aqua':'#00ffff', - 'aquamarine':'#7fffd4', - 'azure':'#f0ffff', - 'beige':'#f5f5dc', - 'bisque':'#ffe4c4', - 'black':'#000000', - 'blanchedalmond':'#ffebcd', - 'blue':'#0000ff', - 'blueviolet':'#8a2be2', - 'brown':'#a52a2a', - 'burlywood':'#deb887', - 'cadetblue':'#5f9ea0', - 'chartreuse':'#7fff00', - 'chocolate':'#d2691e', - 'coral':'#ff7f50', - 'cornflowerblue':'#6495ed', - 'cornsilk':'#fff8dc', - 'crimson':'#dc143c', - 'cyan':'#00ffff', - 'darkblue':'#00008b', - 'darkcyan':'#008b8b', - 'darkgoldenrod':'#b8860b', - 'darkgray':'#a9a9a9', - 'darkgrey':'#a9a9a9', - 'darkgreen':'#006400', - 'darkkhaki':'#bdb76b', - 'darkmagenta':'#8b008b', - 'darkolivegreen':'#556b2f', - 'darkorange':'#ff8c00', - 'darkorchid':'#9932cc', - 'darkred':'#8b0000', - 'darksalmon':'#e9967a', - 'darkseagreen':'#8fbc8f', - 'darkslateblue':'#483d8b', - 'darkslategray':'#2f4f4f', - 'darkslategrey':'#2f4f4f', - 'darkturquoise':'#00ced1', - 'darkviolet':'#9400d3', - 'deeppink':'#ff1493', - 'deepskyblue':'#00bfff', - 'dimgray':'#696969', - 'dimgrey':'#696969', - 'dodgerblue':'#1e90ff', - 'firebrick':'#b22222', - 'floralwhite':'#fffaf0', - 'forestgreen':'#228b22', - 'fuchsia':'#ff00ff', - 'gainsboro':'#dcdcdc', - 'ghostwhite':'#f8f8ff', - 'gold':'#ffd700', - 'goldenrod':'#daa520', - 'gray':'#808080', - 'grey':'#808080', - 'green':'#008000', - 'greenyellow':'#adff2f', - 'honeydew':'#f0fff0', - 'hotpink':'#ff69b4', - 'indianred':'#cd5c5c', - 'indigo':'#4b0082', - 'ivory':'#fffff0', - 'khaki':'#f0e68c', - 'lavender':'#e6e6fa', - 'lavenderblush':'#fff0f5', - 'lawngreen':'#7cfc00', - 'lemonchiffon':'#fffacd', - 'lightblue':'#add8e6', - 'lightcoral':'#f08080', - 'lightcyan':'#e0ffff', - 'lightgoldenrodyellow':'#fafad2', - 'lightgray':'#d3d3d3', - 'lightgrey':'#d3d3d3', - 'lightgreen':'#90ee90', - 'lightpink':'#ffb6c1', - 'lightsalmon':'#ffa07a', - 'lightseagreen':'#20b2aa', - 'lightskyblue':'#87cefa', - 'lightslategray':'#778899', - 'lightslategrey':'#778899', - 'lightsteelblue':'#b0c4de', - 'lightyellow':'#ffffe0', - 'lime':'#00ff00', - 'limegreen':'#32cd32', - 'linen':'#faf0e6', - 'magenta':'#ff00ff', - 'maroon':'#800000', - 'mediumaquamarine':'#66cdaa', - 'mediumblue':'#0000cd', - 'mediumorchid':'#ba55d3', - 'mediumpurple':'#9370d8', - 'mediumseagreen':'#3cb371', - 'mediumslateblue':'#7b68ee', - 'mediumspringgreen':'#00fa9a', - 'mediumturquoise':'#48d1cc', - 'mediumvioletred':'#c71585', - 'midnightblue':'#191970', - 'mintcream':'#f5fffa', - 'mistyrose':'#ffe4e1', - 'moccasin':'#ffe4b5', - 'navajowhite':'#ffdead', - 'navy':'#000080', - 'oldlace':'#fdf5e6', - 'olive':'#808000', - 'olivedrab':'#6b8e23', - 'orange':'#ffa500', - 'orangered':'#ff4500', - 'orchid':'#da70d6', - 'palegoldenrod':'#eee8aa', - 'palegreen':'#98fb98', - 'paleturquoise':'#afeeee', - 'palevioletred':'#d87093', - 'papayawhip':'#ffefd5', - 'peachpuff':'#ffdab9', - 'peru':'#cd853f', - 'pink':'#ffc0cb', - 'plum':'#dda0dd', - 'powderblue':'#b0e0e6', - 'purple':'#800080', - 'red':'#ff0000', - 'rosybrown':'#bc8f8f', - 'royalblue':'#4169e1', - 'saddlebrown':'#8b4513', - 'salmon':'#fa8072', - 'sandybrown':'#f4a460', - 'seagreen':'#2e8b57', - 'seashell':'#fff5ee', - 'sienna':'#a0522d', - 'silver':'#c0c0c0', - 'skyblue':'#87ceeb', - 'slateblue':'#6a5acd', - 'slategray':'#708090', - 'slategrey':'#708090', - 'snow':'#fffafa', - 'springgreen':'#00ff7f', - 'steelblue':'#4682b4', - 'tan':'#d2b48c', - 'teal':'#008080', - 'thistle':'#d8bfd8', - 'tomato':'#ff6347', - 'turquoise':'#40e0d0', - 'violet':'#ee82ee', - 'wheat':'#f5deb3', - 'white':'#ffffff', - 'whitesmoke':'#f5f5f5', - 'yellow':'#ffff00', - 'yellowgreen':'#9acd32' -} \ No newline at end of file + 'aliceblue': '#f0f8ff', + 'antiquewhite': '#faebd7', + 'aqua': '#00ffff', + 'aquamarine': '#7fffd4', + 'azure': '#f0ffff', + 'beige': '#f5f5dc', + 'bisque': '#ffe4c4', + 'black': '#000000', + 'blanchedalmond': '#ffebcd', + 'blue': '#0000ff', + 'blueviolet': '#8a2be2', + 'brown': '#a52a2a', + 'burlywood': '#deb887', + 'cadetblue': '#5f9ea0', + 'chartreuse': '#7fff00', + 'chocolate': '#d2691e', + 'coral': '#ff7f50', + 'cornflowerblue': '#6495ed', + 'cornsilk': '#fff8dc', + 'crimson': '#dc143c', + 'cyan': '#00ffff', + 'darkblue': '#00008b', + 'darkcyan': '#008b8b', + 'darkgoldenrod': '#b8860b', + 'darkgray': '#a9a9a9', + 'darkgrey': '#a9a9a9', + 'darkgreen': '#006400', + 'darkkhaki': '#bdb76b', + 'darkmagenta': '#8b008b', + 'darkolivegreen': '#556b2f', + 'darkorange': '#ff8c00', + 'darkorchid': '#9932cc', + 'darkred': '#8b0000', + 'darksalmon': '#e9967a', + 'darkseagreen': '#8fbc8f', + 'darkslateblue': '#483d8b', + 'darkslategray': '#2f4f4f', + 'darkslategrey': '#2f4f4f', + 'darkturquoise': '#00ced1', + 'darkviolet': '#9400d3', + 'deeppink': '#ff1493', + 'deepskyblue': '#00bfff', + 'dimgray': '#696969', + 'dimgrey': '#696969', + 'dodgerblue': '#1e90ff', + 'firebrick': '#b22222', + 'floralwhite': '#fffaf0', + 'forestgreen': '#228b22', + 'fuchsia': '#ff00ff', + 'gainsboro': '#dcdcdc', + 'ghostwhite': '#f8f8ff', + 'gold': '#ffd700', + 'goldenrod': '#daa520', + 'gray': '#808080', + 'grey': '#808080', + 'green': '#008000', + 'greenyellow': '#adff2f', + 'honeydew': '#f0fff0', + 'hotpink': '#ff69b4', + 'indianred': '#cd5c5c', + 'indigo': '#4b0082', + 'ivory': '#fffff0', + 'khaki': '#f0e68c', + 'lavender': '#e6e6fa', + 'lavenderblush': '#fff0f5', + 'lawngreen': '#7cfc00', + 'lemonchiffon': '#fffacd', + 'lightblue': '#add8e6', + 'lightcoral': '#f08080', + 'lightcyan': '#e0ffff', + 'lightgoldenrodyellow': '#fafad2', + 'lightgray': '#d3d3d3', + 'lightgrey': '#d3d3d3', + 'lightgreen': '#90ee90', + 'lightpink': '#ffb6c1', + 'lightsalmon': '#ffa07a', + 'lightseagreen': '#20b2aa', + 'lightskyblue': '#87cefa', + 'lightslategray': '#778899', + 'lightslategrey': '#778899', + 'lightsteelblue': '#b0c4de', + 'lightyellow': '#ffffe0', + 'lime': '#00ff00', + 'limegreen': '#32cd32', + 'linen': '#faf0e6', + 'magenta': '#ff00ff', + 'maroon': '#800000', + 'mediumaquamarine': '#66cdaa', + 'mediumblue': '#0000cd', + 'mediumorchid': '#ba55d3', + 'mediumpurple': '#9370d8', + 'mediumseagreen': '#3cb371', + 'mediumslateblue': '#7b68ee', + 'mediumspringgreen': '#00fa9a', + 'mediumturquoise': '#48d1cc', + 'mediumvioletred': '#c71585', + 'midnightblue': '#191970', + 'mintcream': '#f5fffa', + 'mistyrose': '#ffe4e1', + 'moccasin': '#ffe4b5', + 'navajowhite': '#ffdead', + 'navy': '#000080', + 'oldlace': '#fdf5e6', + 'olive': '#808000', + 'olivedrab': '#6b8e23', + 'orange': '#ffa500', + 'orangered': '#ff4500', + 'orchid': '#da70d6', + 'palegoldenrod': '#eee8aa', + 'palegreen': '#98fb98', + 'paleturquoise': '#afeeee', + 'palevioletred': '#d87093', + 'papayawhip': '#ffefd5', + 'peachpuff': '#ffdab9', + 'peru': '#cd853f', + 'pink': '#ffc0cb', + 'plum': '#dda0dd', + 'powderblue': '#b0e0e6', + 'purple': '#800080', + 'red': '#ff0000', + 'rosybrown': '#bc8f8f', + 'royalblue': '#4169e1', + 'saddlebrown': '#8b4513', + 'salmon': '#fa8072', + 'sandybrown': '#f4a460', + 'seagreen': '#2e8b57', + 'seashell': '#fff5ee', + 'sienna': '#a0522d', + 'silver': '#c0c0c0', + 'skyblue': '#87ceeb', + 'slateblue': '#6a5acd', + 'slategray': '#708090', + 'slategrey': '#708090', + 'snow': '#fffafa', + 'springgreen': '#00ff7f', + 'steelblue': '#4682b4', + 'tan': '#d2b48c', + 'teal': '#008080', + 'thistle': '#d8bfd8', + 'tomato': '#ff6347', + 'turquoise': '#40e0d0', + 'violet': '#ee82ee', + 'wheat': '#f5deb3', + 'white': '#ffffff', + 'whitesmoke': '#f5f5f5', + 'yellow': '#ffff00', + 'yellowgreen': '#9acd32' +} diff --git a/lesscpy/lib/css.py b/lesscpy/lib/css.py index 6938f56..cf95d07 100644 --- a/lesscpy/lib/css.py +++ b/lesscpy/lib/css.py @@ -1,6 +1,6 @@ """ CSS syntax names. - + Copyright (c) See LICENSE for details. @@ -123,51 +123,51 @@ css2 = [ 'z-index', ] css3 = [ - 'alignment-adjust', - 'alignment-baseline', - 'animation', - 'animation-delay', - 'animation-direction', - 'animation-duration', - 'animation-iteration-count', - 'animation-name', - 'animation-play-state', - 'animation-timing-function', + 'alignment-adjust', + 'alignment-baseline', + 'animation', + 'animation-delay', + 'animation-direction', + 'animation-duration', + 'animation-iteration-count', + 'animation-name', + 'animation-play-state', + 'animation-timing-function', 'appearance', - 'backface-visibility', - 'background-clip', - 'background-origin', - 'background-size', - 'baseline-shift', - 'bookmark-label', - 'bookmark-level', - 'bookmark-target', - 'border-bottom-left-radius', - 'border-bottom-right-radius', - 'border-image', - 'border-image-outset', - 'border-image-repeat', - 'border-image-slice', - 'border-image-source', - 'border-image-width', - 'border-radius', - 'border-top-left-radius', - 'border-top-right-radius', - 'box-align', - 'box-decoration-break', - 'box-direction', - 'box-flex', - 'box-flex-group', - 'box-lines', - 'box-ordinal-group', - 'box-orient', - 'box-pack', - 'box-shadow', - 'box-sizing', - 'color-profile', - 'column-count', - 'column-fill', - 'column-gap', + 'backface-visibility', + 'background-clip', + 'background-origin', + 'background-size', + 'baseline-shift', + 'bookmark-label', + 'bookmark-level', + 'bookmark-target', + 'border-bottom-left-radius', + 'border-bottom-right-radius', + 'border-image', + 'border-image-outset', + 'border-image-repeat', + 'border-image-slice', + 'border-image-source', + 'border-image-width', + 'border-radius', + 'border-top-left-radius', + 'border-top-right-radius', + 'box-align', + 'box-decoration-break', + 'box-direction', + 'box-flex', + 'box-flex-group', + 'box-lines', + 'box-ordinal-group', + 'box-orient', + 'box-pack', + 'box-shadow', + 'box-sizing', + 'color-profile', + 'column-count', + 'column-fill', + 'column-gap', 'column-rule', 'column-rule-color', 'column-rule-style', @@ -205,7 +205,7 @@ css3 = [ 'line-stacking-ruby', 'line-stacking-shift', 'line-stacking-strategy', -# 'mark', + # 'mark', 'mark-after', 'mark-before', 'marks', diff --git a/lesscpy/lib/dom.py b/lesscpy/lib/dom.py index 3ad30e6..2569d84 100644 --- a/lesscpy/lib/dom.py +++ b/lesscpy/lib/dom.py @@ -1,6 +1,6 @@ """ HTML DOM names - + Copyright (c) See LICENSE for details. @@ -58,7 +58,7 @@ html4 = [ 'label', 'legend', 'li', -# 'link', + # 'link', 'map', 'mark', 'menu', @@ -97,7 +97,7 @@ html4 = [ 'u', 'ul', 'var', - + 'print', 'screen', 'all', @@ -127,7 +127,7 @@ html5 = [ 'nav', 'output', 'progress', - ' progress-bar-stripes', + ' progress-bar-stripes', 'rp', 'rt', 'ruby', @@ -141,4 +141,4 @@ html5 = [ 'wbr', ] html = html4 -html.extend(html5) \ No newline at end of file +html.extend(html5) diff --git a/lesscpy/plib/__init__.py b/lesscpy/plib/__init__.py index 4b22cd7..fe6d5fe 100644 --- a/lesscpy/plib/__init__.py +++ b/lesscpy/plib/__init__.py @@ -2,23 +2,23 @@ """ .. module:: lesscpy.plib :synopsis: Parse Nodes for Lesscpy - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ __all__ = [ - 'Block', - 'Call', - 'Deferred', - 'Expression', - 'Identifier', - 'Mixin', - 'Node', - 'Property', - 'Statement', - 'String', - 'Variable' + 'Block', + 'Call', + 'Deferred', + 'Expression', + 'Identifier', + 'Mixin', + 'Node', + 'Property', + 'Statement', + 'String', + 'Variable' ] from .block import Block from .call import Call @@ -30,4 +30,4 @@ from .node import Node from .property import Property from .statement import Statement from .string import String -from .variable import Variable \ No newline at end of file +from .variable import Variable diff --git a/lesscpy/plib/block.py b/lesscpy/plib/block.py index 653cf06..9b0b916 100644 --- a/lesscpy/plib/block.py +++ b/lesscpy/plib/block.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.plib.block :synopsis: Block parse node. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -11,7 +11,9 @@ import re from .node import Node from lesscpy.lessc import utility + class Block(Node): + """ Block node. Represents one parse-block. Can contain property nodes or other block nodes. identifier { @@ -19,6 +21,7 @@ class Block(Node): inner blocks } """ + def parse(self, scope): """Parse block node. args: @@ -35,14 +38,15 @@ class Block(Node): scope.real.append(self.name) if not self.name.parsed: self.name.parse(scope) - if not inner: inner = [] + if not inner: + inner = [] inner = list(utility.flatten([p.parse(scope) for p in inner if p])) - self.parsed = [p for p in inner if p and type(p) is not Block] - self.inner = [p for p in inner if p and type(p) is Block] + self.parsed = [p for p in inner if p and not isinstance(p, Block)] + self.inner = [p for p in inner if p and isinstance(p, Block)] scope.real.pop() scope.pop() return self - + def raw(self, clean=False): """Raw block name args: @@ -54,7 +58,7 @@ class Block(Node): return self.tokens[0].raw(clean) except (AttributeError, TypeError): pass - + def fmt(self, fills): """Format block (CSS) args: @@ -72,9 +76,9 @@ class Block(Node): }) out.append(f % fills) if hasattr(self, 'inner'): - if self.name.subparse: # @media + if self.name.subparse: # @media inner = ''.join([p.fmt(fills) for p in self.inner]) - inner = inner.replace(fills['nl'], + inner = inner.replace(fills['nl'], fills['nl'] + fills['tab']).rstrip(fills['tab']) if not fills['nl']: inner = inner.strip() @@ -86,7 +90,7 @@ class Block(Node): else: out.append(''.join([p.fmt(fills) for p in self.inner])) return ''.join(out) - + def copy(self): """ Return a full copy of self returns: Block object @@ -98,12 +102,12 @@ class Block(Node): if name: name = name.copy() return Block([name, inner], 0) - + def copy_inner(self, scope): - """Copy block contents (properties, inner blocks). + """Copy block contents (properties, inner blocks). Renames inner block from current scope. Used for mixins. - args: + args: scope (Scope): Current scope returns: list (block contents) diff --git a/lesscpy/plib/call.py b/lesscpy/plib/call.py index e6485ad..0fb4137 100644 --- a/lesscpy/plib/call.py +++ b/lesscpy/plib/call.py @@ -2,12 +2,13 @@ """ .. module:: lesscpy.plib.call :synopsis: Call parse node - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ -import re, math +import re +import math try: from urllib.parse import quote as urlquote, urlparse except ImportError: @@ -17,7 +18,9 @@ import lesscpy.lessc.utility as utility import lesscpy.lessc.color as Color from lesscpy.lib.colors import lessColors + class Call(Node): + """Call node. Node represents a function call. All builtin none-color functions are in this node. This node attempts calls on built-ins and lets non-builtins @@ -25,7 +28,7 @@ class Call(Node): increment(3px) --> 4px unknown(3px) --> unknown(3px) """ - + def parse(self, scope): """Parse Node within scope. the functions ~( and e( map to self.escape @@ -41,14 +44,14 @@ class Call(Node): elif name in ('~', 'e'): name = 'escape' color = Color.Color() - args = [t for t in parsed - if type(t) is not str or t not in '(),'] + args = [t for t in parsed + if not isinstance(t, str) or t not in '(),'] if hasattr(self, name): try: return getattr(self, name)(*args) except ValueError: pass - + if hasattr(color, name): try: result = getattr(color, name)(*args) @@ -59,7 +62,7 @@ class Call(Node): except ValueError: pass return name + ''.join([p for p in parsed]) - + def escape(self, string, *args): """Less Escape. args: @@ -68,7 +71,7 @@ class Call(Node): str """ return utility.destring(string.strip('~')) - + def sformat(self, string, *args): """ String format. args: @@ -85,15 +88,15 @@ class Call(Node): i = 0 for n in m: v = { - '%A' : urlquote, - '%s' : utility.destring, + '%A': urlquote, + '%s': utility.destring, }.get(n, str)(args[i]) items.append(v) i += 1 format = format.replace('%A', '%s') format = format.replace('%d', '%s') return format % tuple(items) - + def isnumber(self, string, *args): """Is number args: @@ -106,7 +109,7 @@ class Call(Node): except SyntaxError as e: return False return True - + def iscolor(self, string, *args): """Is color args: @@ -115,7 +118,7 @@ class Call(Node): bool """ return (string in lessColors) - + def isurl(self, string, *args): """Is url args: @@ -126,13 +129,15 @@ class Call(Node): arg = utility.destring(string) regex = re.compile(r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' - r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... - r'localhost|' #localhost... + r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + # localhost... + r'localhost|' r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?' # optional port + # optional port + r'(?::\d+)?' r'(?:/?|[/?]\S+)$', re.IGNORECASE) return regex.match(arg) - + def isstring(self, string, *args): """Is string args: @@ -142,7 +147,7 @@ class Call(Node): """ regex = re.compile(r'\'[^\']*\'|"[^"]*"') return regex.match(string) - + def iskeyword(self, string, *args): """Is less keyword args: @@ -151,7 +156,7 @@ class Call(Node): bool """ return (string in ('when', 'and', 'not')) - + def increment(self, value, *args): """ Increment function args: @@ -160,8 +165,8 @@ class Call(Node): str """ n, u = utility.analyze_number(value) - return utility.with_unit(n+1, u) - + return utility.with_unit(n + 1, u) + def decrement(self, value, *args): """ Decrement function args: @@ -170,8 +175,8 @@ class Call(Node): str """ n, u = utility.analyze_number(value) - return utility.with_unit(n-1, u) - + return utility.with_unit(n - 1, u) + def add(self, *args): """ Add integers args: @@ -179,9 +184,10 @@ class Call(Node): returns: str """ - if(len(args) <= 1): return 0 + if(len(args) <= 1): + return 0 return sum([int(v) for v in args]) - + def round(self, value, *args): """ Round number args: @@ -191,7 +197,7 @@ class Call(Node): """ n, u = utility.analyze_number(value) return utility.with_unit(round(float(n)), u) - + def ceil(self, value, *args): """ Ceil number args: @@ -201,7 +207,7 @@ class Call(Node): """ n, u = utility.analyze_number(value) return utility.with_unit(math.ceil(n), u) - + def floor(self, value, *args): """ Floor number args: @@ -211,7 +217,7 @@ class Call(Node): """ n, u = utility.analyze_number(value) return utility.with_unit(math.floor(n), u) - + def percentage(self, value, *args): """ Return percentage value args: diff --git a/lesscpy/plib/deferred.py b/lesscpy/plib/deferred.py index 52203d0..dfdf58d 100644 --- a/lesscpy/plib/deferred.py +++ b/lesscpy/plib/deferred.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.plib.deferred :synopsis: Deferred mixin call. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -10,11 +10,13 @@ from .node import Node from lesscpy.lessc import utility + class Deferred(Node): + def __init__(self, mixin, args, lineno=0): """This node represents mixin calls. The calls - to these mixins are deferred until the second - parse cycle. lessc.js allows calls to mixins not + to these mixins are deferred until the second + parse cycle. lessc.js allows calls to mixins not yet defined or known. args: mixin (Mixin): Mixin object @@ -22,18 +24,18 @@ class Deferred(Node): """ self.tokens = [mixin, args] self.lineno = lineno - + def parse(self, scope, error=False): """ Parse function. We search for mixins first within current scope then fallback to global scope. The special scope.deferred - is used when local scope mixins are called - within parent mixins. + is used when local scope mixins are called + within parent mixins. If nothing is found we fallback to block-mixin as lessc.js allows calls to blocks and mixins to be inter-changable. - clx: This method is a HACK that stems from - poor design elsewhere. I will fix it + clx: This method is a HACK that stems from + poor design elsewhere. I will fix it when I have more time. args: scope (Scope): Current scope @@ -44,11 +46,11 @@ class Deferred(Node): ident, args = self.tokens ident.parse(scope) mixins = scope.mixins(ident.raw()) - + if not mixins: ident.parse(None) mixins = scope.mixins(ident.raw()) - + if not mixins: if scope.deferred: store = [t for t in scope.deferred.parsed[-1]] @@ -61,7 +63,7 @@ class Deferred(Node): break scope.deferred.parsed[-1].pop() scope.deferred.parsed[-1] = store - + if not mixins: # Fallback to blocks block = scope.blocks(ident.raw()) @@ -72,34 +74,34 @@ class Deferred(Node): scope.current = scope.real[-1] if scope.real else None res = block.copy_inner(scope) scope.current = None - + if mixins: for mixin in mixins: scope.current = scope.real[-1] if scope.real else None res = mixin.call(scope, args) - if res: + if res: # Add variables to scope to support # closures [scope.add_variable(v) for v in mixin.vars] scope.deferred = ident break - + if res: - store = [t for t in scope.deferred.parsed[-1]] if scope.deferred else False + store = [t for t in scope.deferred.parsed[ + -1]] if scope.deferred else False res = [p.parse(scope) for p in res if p] - while(any(t for t in res if type(t) is Deferred)): + while(any(t for t in res if isinstance(t, Deferred))): res = [p.parse(scope) for p in res if p] - if store: scope.deferred.parsed[-1] = store - + if store: + scope.deferred.parsed[-1] = store + if error and not res: raise SyntaxError('NameError `%s`' % ident.raw(True)) return res - + def copy(self): """ Returns self (used when Block objects are copy'd) returns: self """ return self - - diff --git a/lesscpy/plib/expression.py b/lesscpy/plib/expression.py index 28cf08a..6bd27ce 100644 --- a/lesscpy/plib/expression.py +++ b/lesscpy/plib/expression.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.plib.expression :synopsis: Expression node. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -12,11 +12,13 @@ from .node import Node from lesscpy.lessc import utility from lesscpy.lessc import color + class Expression(Node): + """Expression node. Parses all expression except color expressions, (handled in the color class) """ - + def parse(self, scope): """ Parse Node args: @@ -29,9 +31,9 @@ class Expression(Node): assert(len(self.tokens) == 3) expr = self.process(self.tokens, scope) expr = [self.neg(t, scope) for t in expr] - A, O, B = [e[0] - if type(e) is tuple - else e + A, O, B = [e[0] + if isinstance(e, tuple) + else e for e in expr if str(e).strip()] try: @@ -44,10 +46,10 @@ class Expression(Node): if ua == 'color' or ub == 'color': return color.Color().process((A, O, B)) out = self.operate(a, b, O) - if type(out) is bool: + if isinstance(out, bool): return out return self.with_units(out, ua, ub) - + def neg(self, value, scope): """Apply negativity. args: @@ -58,17 +60,17 @@ class Expression(Node): returns: str """ - if value and type(value) is list and value[0] == '-': + if value and isinstance(value, list) and value[0] == '-': val = value[1] if len(value) > 1 and hasattr(value[1], 'parse'): val = value[1].parse(scope) - if type(val) is str: + if isinstance(val, str): return '-' + val return -val return value - + def with_units(self, val, ua, ub): - """Return value with unit. + """Return value with unit. args: val (mixed): result ua (str): 1st unit @@ -78,13 +80,14 @@ class Expression(Node): returns: str """ - if not val: return str(val) + if not val: + return str(val) if ua or ub: if ua and ub: if ua == ub: return str(val) + ua else: - # Nodejs version does not seem to mind mismatched + # Nodejs version does not seem to mind mismatched # units within expressions. So we choose the first # as they do # raise SyntaxError("Error in expression %s != %s" % (ua, ub)) @@ -94,7 +97,7 @@ class Expression(Node): elif ub: return str(val) + ub return str(val) - + def operate(self, vala, valb, oper): """Perform operation args: @@ -130,11 +133,11 @@ class Expression(Node): except ValueError: pass return ret - + def py2op(self, vala, operation, valb): """ Python2 operators """ - if operation == '__lt__': + if operation == '__lt__': ret = (vala < valb) elif operation == '__gt__': ret = (vala > valb) @@ -149,7 +152,7 @@ class Expression(Node): else: ret = getattr(vala, operation)(valb) return ret - + def expression(self): """Return str representation of expression returns: diff --git a/lesscpy/plib/identifier.py b/lesscpy/plib/identifier.py index 3ed0015..c09c26f 100644 --- a/lesscpy/plib/identifier.py +++ b/lesscpy/plib/identifier.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.plib.identifier :synopsis: Identifier node. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -10,10 +10,13 @@ import re from .node import Node from lesscpy.lessc import utility + + class Identifier(Node): + """Identifier node. Represents block identifier. """ - + def parse(self, scope): """Parse node. Block identifiers are stored as strings with spaces replaced with ? @@ -24,15 +27,15 @@ class Identifier(Node): returns: self """ - names = [] - name = [] - self._subp = ( - '@media', '@keyframes', + names = [] + name = [] + self._subp = ( + '@media', '@keyframes', '@-moz-keyframes', '@-webkit-keyframes', '@-ms-keyframes' ) if self.tokens and hasattr(self.tokens, 'parse'): - self.tokens = list(utility.flatten([id.split() + [','] + self.tokens = list(utility.flatten([id.split() + [','] for id in self.tokens.parse(scope).split(',')])) self.tokens.pop() if self.tokens and self.tokens[0] in self._subp: @@ -54,11 +57,11 @@ class Identifier(Node): name.append(n) names.append(name) parsed = self.root(scope, names) if scope else names - self.parsed = [[i for i, j in utility.pairwise(part) - if i != ' ' or (j and '?' not in j)] + self.parsed = [[i for i, j in utility.pairwise(part) + if i != ' ' or (j and '?' not in j)] for part in parsed] return self - + def root(self, scope, names): """Find root of identifier, from scope args: @@ -68,16 +71,16 @@ class Identifier(Node): list """ parent = scope.scopename - if parent: + if parent: parent = parent[-1] if parent.parsed: - return [self._pscn(part, n) + return [self._pscn(part, n) if part and part[0] not in self._subp else n for part in parent.parsed for n in names] return names - + def _pscn(self, parent, name): """ """ @@ -96,28 +99,28 @@ class Identifier(Node): parsed.append(' ') parsed.extend(name) return parsed - + def raw(self, clean=False): """Raw identifier. - args: + args: clean (bool): clean name returns: str """ - if clean: return ''.join(''.join(p) for p in self.parsed).replace('?', ' ') + if clean: + return ''.join(''.join(p) for p in self.parsed).replace('?', ' ') return '%'.join('%'.join(p) for p in self.parsed).strip().strip('%') - + def copy(self): """ Return copy of self Returns: Identifier object """ - tokens = ([t for t in self.tokens] - if type(self.tokens) is list + tokens = ([t for t in self.tokens] + if isinstance(self.tokens, list) else self.tokens) return Identifier(tokens, 0) - - + def fmt(self, fills): """Format identifier args: @@ -125,11 +128,9 @@ class Identifier(Node): returns: str (CSS) """ - name = ',$$'.join(''.join(p).strip() + name = ',$$'.join(''.join(p).strip() for p in self.parsed) name = re.sub('\?(.)\?', '%(ws)s\\1%(ws)s', name) % fills - return (name.replace('$$', fills['nl']) - if len(name) > 85 + return (name.replace('$$', fills['nl']) + if len(name) > 85 else name.replace('$$', fills['ws'])).replace(' ', ' ') - - diff --git a/lesscpy/plib/mixin.py b/lesscpy/plib/mixin.py index 0b37fa9..aec9d73 100644 --- a/lesscpy/plib/mixin.py +++ b/lesscpy/plib/mixin.py @@ -2,22 +2,26 @@ """ .. module:: lesscpy.plib.mixin :synopsis: Mixin node. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ -import sys, copy, itertools +import sys +import copy +import itertools from .node import Node from .block import Block from .expression import Expression from .variable import Variable from lesscpy.lessc import utility + class Mixin(Node): + """ Mixin Node. Represents callable mixin types. """ - + def parse(self, scope): """Parse node args: @@ -30,18 +34,18 @@ class Mixin(Node): self.name, args, self.guards = self.tokens[0] self.args = [a for a in utility.flatten(args) if a] self.body = Block([None, self.tokens[1]], 0) - self.vars = list(utility.flatten([list(v.values()) - for v in [s['__variables__'] + self.vars = list(utility.flatten([list(v.values()) + for v in [s['__variables__'] for s in scope]])) return self - + def raw(self): """Raw mixin name returns: str """ return self.name.raw() - + def parse_args(self, args, scope): """Parse arguments to mixin. Add them to scope as variables. Sets upp special variable @arguments @@ -53,21 +57,23 @@ class Mixin(Node): SyntaxError """ arguments = zip(args, [' '] * len(args)) if args and args[0] else None - zl = itertools.zip_longest if sys.version_info[0] == 3 else itertools.izip_longest + zl = itertools.zip_longest if sys.version_info[ + 0] == 3 else itertools.izip_longest if self.args: parsed = [v if hasattr(v, 'parse') else v for v in copy.copy(self.args)] - args = args if type(args) is list else [args] - vars = [self._parse_arg(var, arg, scope) + args = args if isinstance(args, list) else [args] + vars = [self._parse_arg(var, arg, scope) for arg, var in zl([a for a in args], parsed)] - for var in vars: - if var: var.parse(scope) + for var in vars: + if var: + var.parse(scope) if not arguments: arguments = [v.value for v in vars if v] if not arguments: arguments = '' Variable(['@arguments', None, arguments]).parse(scope) - + def _parse_arg(self, var, arg, scope): """ Parse a single argument to mixin. args: @@ -77,24 +83,26 @@ class Mixin(Node): returns: Variable object or None """ - if type(var) is Variable: + if isinstance(var, Variable): # kwarg if arg: if utility.is_variable(arg[0]): tmp = scope.variables(arg[0]) - if not tmp: return None + if not tmp: + return None val = tmp.value else: val = arg var = Variable(var.tokens[:-1] + [val]) else: - #arg + # arg if utility.is_variable(var): if arg is None: raise SyntaxError('Missing argument to mixin') elif utility.is_variable(arg[0]): tmp = scope.variables(arg[0]) - if not tmp: return None + if not tmp: + return None val = tmp.value else: val = arg @@ -102,7 +110,7 @@ class Mixin(Node): else: return None return var - + def parse_guards(self, scope): """Parse guards on mixin. args: @@ -115,16 +123,17 @@ class Mixin(Node): if self.guards: cor = True if ',' in self.guards else False for g in self.guards: - if type(g) is list: - res = (g[0].parse(scope) - if len(g) == 1 + if isinstance(g, list): + res = (g[0].parse(scope) + if len(g) == 1 else Expression(g).parse(scope)) if cor: - if res: return True + if res: + return True elif not res: return False return True - + def call(self, scope, args=[]): """Call mixin. Parses a copy of the mixins body in the current scope and returns it. @@ -138,8 +147,8 @@ class Mixin(Node): """ ret = False if args: - args = [[a.parse(scope) - if type(a) is Expression + args = [[a.parse(scope) + if isinstance(a, Expression) else a for a in arg] if arg else arg for arg in args] @@ -151,5 +160,6 @@ class Mixin(Node): if self.parse_guards(scope): body = self.body.copy() ret = body.tokens[1] - if ret: utility.rename(ret, scope, Block) + if ret: + utility.rename(ret, scope, Block) return ret diff --git a/lesscpy/plib/node.py b/lesscpy/plib/node.py index bda0d6b..b85f057 100644 --- a/lesscpy/plib/node.py +++ b/lesscpy/plib/node.py @@ -2,14 +2,16 @@ """ .. module:: lesscpy.plib.node :synopsis: Base Node - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from lesscpy.lessc import utility + class Node(object): + def __init__(self, tokens, lineno=0): """ Base Node args: @@ -19,7 +21,7 @@ class Node(object): self.tokens = tokens self.lineno = lineno self.parsed = False - + def parse(self, scope): """ Base parse function args: @@ -28,7 +30,7 @@ class Node(object): self """ return self - + def process(self, tokens, scope): """ Process tokenslist, flattening and parsing it args: @@ -41,17 +43,18 @@ class Node(object): tokens = list(utility.flatten(tokens)) done = True if any(t for t in tokens if hasattr(t, 'parse')): - tokens = [t.parse(scope) - if hasattr(t, 'parse') + tokens = [t.parse(scope) + if hasattr(t, 'parse') else t for t in tokens] done = False if any(t for t in tokens if utility.is_variable(t)): tokens = self.replace_variables(tokens, scope) done = False - if done: break + if done: + break return tokens - + def replace_variables(self, tokens, scope): """ Replace variables in tokenlist args: @@ -62,7 +65,7 @@ class Node(object): """ return [scope.swap(t) if utility.is_variable(t) - else t + else t for t in tokens] def fmt(self, fills): diff --git a/lesscpy/plib/property.py b/lesscpy/plib/property.py index 757f02a..20be2a4 100644 --- a/lesscpy/plib/property.py +++ b/lesscpy/plib/property.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.plib.property :synopsis: Property node. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -11,10 +11,12 @@ import re from .node import Node from lesscpy.lessc import utility + class Property(Node): + """Represents CSS property declaration. """ - + def parse(self, scope): """Parse node args: @@ -37,17 +39,17 @@ class Property(Node): style = self.preprocess(style) self.parsed = self.process(style, scope) return self - + def preprocess(self, style): """Hackish preprocessing from font shorthand tags. Skips expression parse on certain tags. - args: + args: style (list): . returns: list """ if self.property == 'font': - style = [''.join(u.expression()) + style = [''.join(u.expression()) if hasattr(u, 'expression') else u for u in style] @@ -57,7 +59,7 @@ class Property(Node): else u for u in style] return style - + def fmt(self, fills): """ Format node args: @@ -70,10 +72,10 @@ class Property(Node): if fills['nl']: self.parsed = [',%s' % fills['ws'] if p == ',' - else p + else p for p in self.parsed] - style = ''.join([p.fmt(fills) - if hasattr(p, 'fmt') + style = ''.join([p.fmt(fills) + if hasattr(p, 'fmt') else str(p) for p in self.parsed]) # IE cannot handle no space after url() @@ -84,7 +86,7 @@ class Property(Node): 'important': imp }) return f % fills - + def copy(self): """ Return a full copy of self Returns: diff --git a/lesscpy/plib/statement.py b/lesscpy/plib/statement.py index 507d148..dcb67e9 100644 --- a/lesscpy/plib/statement.py +++ b/lesscpy/plib/statement.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.plib.statement :synopsis: Statement node. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -10,10 +10,12 @@ from .node import Node from lesscpy.lessc import utility + class Statement(Node): + """Represents CSS statement (@import, @charset...) """ - + def parse(self, scope): """Parse node args: @@ -29,7 +31,7 @@ class Statement(Node): # Media @import self.parsed.insert(3, ' ') return self - + def fmt(self, fills): """ Format node args: @@ -38,4 +40,3 @@ class Statement(Node): str """ return ''.join(self.parsed) + fills['eb'] - \ No newline at end of file diff --git a/lesscpy/plib/string.py b/lesscpy/plib/string.py index d0c9de8..7037712 100644 --- a/lesscpy/plib/string.py +++ b/lesscpy/plib/string.py @@ -2,7 +2,7 @@ """ .. module:: lesscpy.plib.string :synopsis: Less interpolated string node. - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson @@ -11,7 +11,9 @@ import re from .node import Node from lesscpy.lessc import utility + class String(Node): + def parse(self, scope): """Parse node args: @@ -23,7 +25,7 @@ class String(Node): """ self.scope = scope return re.sub(r'@\{([^\}]+)\}', lambda m: self.swap(m.group(1)), self.tokens) - + def swap(self, var): """ Replace variable args: @@ -36,4 +38,3 @@ class String(Node): var = self.scope.swap('@' + var) var = ''.join(utility.flatten(var)) return var.strip("\"'") - \ No newline at end of file diff --git a/lesscpy/plib/variable.py b/lesscpy/plib/variable.py index 34dfd3a..b5ac11b 100644 --- a/lesscpy/plib/variable.py +++ b/lesscpy/plib/variable.py @@ -2,14 +2,16 @@ """ .. module:: lesscpy.plib.variable :synopsis: Variable declaration - + Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from .node import Node + class Variable(Node): + def parse(self, scope): """ Parse function args: @@ -18,21 +20,20 @@ class Variable(Node): self """ self.name, _, self.value = self.tokens - if type(self.name) is tuple: + if isinstance(self.name, tuple): if len(self.name) > 1: self.name, pad = self.name self.value.append(pad) else: self.name = self.name[0] scope.add_variable(self) - + def copy(self): """ Return a copy of self Returns: Variable object """ return Variable([t for t in self.tokens]) - + def fmt(self, fills): return '' - diff --git a/lesscpy/scripts/compiler.py b/lesscpy/scripts/compiler.py index 8ad328c..d3db996 100644 --- a/lesscpy/scripts/compiler.py +++ b/lesscpy/scripts/compiler.py @@ -5,7 +5,7 @@ from __future__ import print_function CSS/LESSCSS run script http://lesscss.org/#docs - + Copyright (c) See LICENSE for details .. moduleauthor:: Johann T. Mariusson @@ -22,6 +22,7 @@ from lesscpy.lessc import formatter VERSION_STR = 'Lesscpy compiler 0.9h' + def ldirectory(inpath, outpath, args, scope): """Compile all *.less files in directory Args: @@ -44,10 +45,10 @@ def ldirectory(inpath, outpath, args, scope): for lf in less: outf = os.path.splitext(os.path.basename(lf)) minx = '.min' if args.min_ending else '' - outf = "%s/%s%s.css" % (outpath, outf[0], minx) + outf = "%s/%s%s.css" % (outpath, outf[0], minx) if not args.force and os.path.exists(outf): recompile = os.path.getmtime(outf) < os.path.getmtime(lf) - else: + else: recompile = True if recompile: print('%s -> %s' % (lf, outf)) @@ -62,71 +63,78 @@ def ldirectory(inpath, outpath, args, scope): if not args.dry_run: with open(outf, 'w') as outfile: outfile.write(css) - elif args.verbose: print('skipping %s, not modified' % lf, file=sys.stderr) + elif args.verbose: + print('skipping %s, not modified' % lf, file=sys.stderr) sys.stdout.flush() if args.recurse: - [ldirectory(os.path.join(inpath, name), os.path.join(outpath, name), args, scope) - for name in os.listdir(inpath) + [ldirectory(os.path.join(inpath, name), os.path.join(outpath, name), args, scope) + for name in os.listdir(inpath) if os.path.isdir(os.path.join(inpath, name)) and not name.startswith('.') and not name == outpath] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# + + def run(): """Run compiler """ - aparse = argparse.ArgumentParser(description='LessCss Compiler', - epilog='<< jtm@robot.is @_o >>', + aparse = argparse.ArgumentParser(description='LessCss Compiler', + epilog='<< jtm@robot.is @_o >>', version=VERSION_STR) aparse.add_argument('-I', '--include', action="store", type=str, help="Included less-files (comma separated)") - aparse.add_argument('-V', '--verbose', action="store_true", + aparse.add_argument('-V', '--verbose', action="store_true", default=False, help="Verbose mode") fgroup = aparse.add_argument_group('Formatting options') - fgroup.add_argument('-x', '--minify', action="store_true", + fgroup.add_argument('-x', '--minify', action="store_true", default=False, help="Minify output") - fgroup.add_argument('-X', '--xminify', action="store_true", + fgroup.add_argument('-X', '--xminify', action="store_true", default=False, help="Minify output, no end of block newlines") fgroup.add_argument('-t', '--tabs', help="Use tabs", action="store_true") - fgroup.add_argument('-s', '--spaces', help="Number of startline spaces (default 2)", default=2) - dgroup = aparse.add_argument_group('Directory options', + fgroup.add_argument( + '-s', '--spaces', help="Number of startline spaces (default 2)", default=2) + dgroup = aparse.add_argument_group('Directory options', 'Compiles all *.less files in directory that ' 'have a newer timestamp than it\'s css file.') dgroup.add_argument('-o', '--out', action="store", help="Output directory") - dgroup.add_argument('-r', '--recurse', action="store_true", help="Recursive into subdirectorys") - dgroup.add_argument('-f', '--force', action="store_true", help="Force recompile on all files") - dgroup.add_argument('-m', '--min-ending', action="store_true", + dgroup.add_argument( + '-r', '--recurse', action="store_true", help="Recursive into subdirectorys") + dgroup.add_argument( + '-f', '--force', action="store_true", help="Force recompile on all files") + dgroup.add_argument('-m', '--min-ending', action="store_true", default=False, help="Add '.min' into output filename. eg, name.min.css") - dgroup.add_argument('-D', '--dry-run', action="store_true", + dgroup.add_argument('-D', '--dry-run', action="store_true", default=False, help="Dry run, do not write files") group = aparse.add_argument_group('Debugging') - group.add_argument('-g', '--debug', action="store_true", - default=False, help="Debugging information") - group.add_argument('-S', '--scopemap', action="store_true", - default=False, help="Scopemap") - group.add_argument('-L', '--lex-only', action="store_true", - default=False, help="Run lexer on target") - group.add_argument('-N', '--no-css', action="store_true", - default=False, help="No css output") + group.add_argument('-g', '--debug', action="store_true", + default=False, help="Debugging information") + group.add_argument('-S', '--scopemap', action="store_true", + default=False, help="Scopemap") + group.add_argument('-L', '--lex-only', action="store_true", + default=False, help="Run lexer on target") + group.add_argument('-N', '--no-css', action="store_true", + default=False, help="No css output") aparse.add_argument('target', help="less file or directory") args = aparse.parse_args() try: # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # + # if args.lex_only: lex = lexer.LessLexer() ll = lex.file(args.target) while True: tok = ll.token() - if not tok: break + if not tok: + break print(tok) print('EOF') sys.exit() # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # + # yacctab = 'yacctab' if args.debug else None scope = None if args.include: @@ -150,9 +158,9 @@ def run(): if not os.path.exists(args.target): sys.exit("Target not found '%s' ..." % args.target) if os.path.isdir(args.target): - ldirectory(args.target, args.out, args, scope) + ldirectory(args.target, args.out, args, scope) if args.dry_run: - print('Dry run, nothing done.', file=sys.stderr) + print('Dry run, nothing done.', file=sys.stderr) else: p = parser.LessParser(yacc_debug=(args.debug), lex_optimize=True, @@ -168,4 +176,3 @@ def run(): print(out) except (KeyboardInterrupt, SystemExit, IOError): sys.exit('\nAborting...') - diff --git a/lesscpy/test/__main__.py b/lesscpy/test/__main__.py index 67d806d..1dde8d7 100644 --- a/lesscpy/test/__main__.py +++ b/lesscpy/test/__main__.py @@ -7,6 +7,7 @@ import re import bootstrap + def find(): svn = re.compile('\.svn') test = re.compile('test.+\.py$') @@ -22,4 +23,3 @@ def find(): if __name__ == '__main__': unittest.main(defaultTest='find') - \ No newline at end of file diff --git a/lesscpy/test/bootstrap.py b/lesscpy/test/bootstrap.py index b186c18..ae5a807 100644 --- a/lesscpy/test/bootstrap.py +++ b/lesscpy/test/bootstrap.py @@ -10,4 +10,4 @@ while os.path.dirname(path) != path: if os.path.exists(os.path.join(path, 'lesscpy', '__init__.py')): sys.path.insert(0, path) break - path = os.path.dirname(path) \ No newline at end of file + path = os.path.dirname(path) diff --git a/lesscpy/test/testcolor.py b/lesscpy/test/testcolor.py index 7b10e60..a0b959f 100644 --- a/lesscpy/test/testcolor.py +++ b/lesscpy/test/testcolor.py @@ -8,49 +8,50 @@ from lesscpy.lessc import color class TestLessColor(unittest.TestCase): + def setUp(self): self.color = color.Color() - + def test_rgb(self): test = self.color.rgb for r, g, b, v in [ - (255,255,255,'#ffffff'), - (100,100,100,'#646464'), - (0,0,0,'#000000'), - ('70%','70%','70%', '#b2b2b2'), - ('1%','1%','1%', '#020202'), - ('100%','100%','100%', '#ffffff'), - ('0%','0%','0%', '#000000'), + (255, 255, 255, '#ffffff'), + (100, 100, 100, '#646464'), + (0, 0, 0, '#000000'), + ('70%', '70%', '70%', '#b2b2b2'), + ('1%', '1%', '1%', '#020202'), + ('100%', '100%', '100%', '#ffffff'), + ('0%', '0%', '0%', '#000000'), ]: self.assertEqual(test(r, g, b), v) for args in [ - (255,255,256), - (0,-1,0), + (255, 255, 256), + (0, -1, 0), ('100%', '100%', 200), ('100%', '100%', '200%'), ]: - self.assertRaises(ValueError, test, args) - + self.assertRaises(ValueError, test, args) + def test_rgba(self): test = self.color.rgba for r, g, b, a, v in [ - (255,255,255,255,'#ffffffff'), - (100,100,100,100,'#64646464'), - (0,0,0,0,'#00000000'), - ('70%','70%','70%', '70%', '#b2b2b2b2'), - ('1%','1%','1%', '1%', '#02020202'), - ('100%','100%','100%','100%', '#ffffffff'), - ('0%','0%','0%','0%', '#00000000'), + (255, 255, 255, 255, '#ffffffff'), + (100, 100, 100, 100, '#64646464'), + (0, 0, 0, 0, '#00000000'), + ('70%', '70%', '70%', '70%', '#b2b2b2b2'), + ('1%', '1%', '1%', '1%', '#02020202'), + ('100%', '100%', '100%', '100%', '#ffffffff'), + ('0%', '0%', '0%', '0%', '#00000000'), ]: self.assertEqual(test(r, g, b, a), v) for args in [ - (255,255,255,256), - (0,0,0,-1), + (255, 255, 255, 256), + (0, 0, 0, -1), ('100%', '100%', '100%', 200), ('100%', '100%', '100%', '200%'), ]: - self.assertRaises(ValueError, test, args) - + self.assertRaises(ValueError, test, args) + def test_hsl(self): """ """ @@ -63,7 +64,7 @@ class TestLessColor(unittest.TestCase): (100, '0%', '0%', '#000000'), ]: self.assertEqual(test(h, s, l), v) - + def test_hsla(self): test = self.color.hsla for h, s, l, a, v in [ @@ -74,7 +75,7 @@ class TestLessColor(unittest.TestCase): (31, '100%', '4%', '100%', 'rgba(20.0,11.0,0.0,1.0)'), ]: self.assertEqual(test(h, s, l, a), v) - + def test_fmt(self): test = self.color.fmt self.assertEqual(test('#000'), '#000000') @@ -88,7 +89,7 @@ class TestLessColor(unittest.TestCase): self.assertRaises(ValueError, test, None) self.assertRaises(ValueError, test, 'aabbcc') self.assertRaises(ValueError, test, '#4aabbcc') - + def test_saturate(self): test = self.color.saturate for c, p, v in [ @@ -108,10 +109,10 @@ class TestLessColor(unittest.TestCase): ('#29332f', '40%', '#174533'), ('#29332f', '60%', '#0d4f35'), ('#29332f', '100%', '#005c37'), - + ]: self.assertEqual(test(c, p), v, v) - + def test_desaturate(self): test = self.color.desaturate for c, p, v in [ @@ -131,10 +132,10 @@ class TestLessColor(unittest.TestCase): ('#29332f', '40%', '#2e2e2e'), ('#29332f', '60%', '#2e2e2e'), ('#29332f', '100%', '#2e2e2e'), - + ]: self.assertEqual(test(c, p), v, v) - + def test_spin(self): test = self.color.spin for c, p, v in [ @@ -154,9 +155,9 @@ class TestLessColor(unittest.TestCase): ('#29332f', '40%', '#293033'), ('#29332f', '60%', '#292d33'), ('#29332f', '100%', '#2c2933'), - + ]: self.assertEqual(test(c, p), v, v) - + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/lesscpy/test/testexpression.py b/lesscpy/test/testexpression.py index af5d48b..29ad58a 100644 --- a/lesscpy/test/testexpression.py +++ b/lesscpy/test/testexpression.py @@ -3,7 +3,9 @@ if __name__ == '__main__': import bootstrap from lesscpy.plib.expression import Expression -class TestExpression(unittest.TestCase): + +class TestExpression(unittest.TestCase): + def test_basic(self): for test in [ ['0', '+', '0', '0'], @@ -18,10 +20,10 @@ class TestExpression(unittest.TestCase): ['2.0px', '+', '2', '4px'], [('2px', ' '), '+', '2.0', '4px'], ['2.0px', '+', '2.0', '4px'], - ]: + ]: e = Expression(test[:3]) self.assertEqual(test[3], e.parse(None), str(test)) - + def test_neg(self): for test in [ ['-0', '+', '0', '0'], @@ -42,7 +44,7 @@ class TestExpression(unittest.TestCase): ]: e = Expression(test[:3]) self.assertEqual(test[3], e.parse(None), str(test)) - + def testop(self): for test in [ ['0', '=', '0', True], @@ -55,7 +57,7 @@ class TestExpression(unittest.TestCase): ]: e = Expression(test[:3]) self.assertEqual(test[3], e.parse(None), test) - + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/lesscpy/test/testidentifier.py b/lesscpy/test/testidentifier.py index e26cab9..475d43e 100644 --- a/lesscpy/test/testidentifier.py +++ b/lesscpy/test/testidentifier.py @@ -4,7 +4,9 @@ if __name__ == '__main__': from lesscpy.lessc.scope import Scope from lesscpy.plib.identifier import Identifier -class TestIdentifier(unittest.TestCase): + +class TestIdentifier(unittest.TestCase): + def test_basic(self): fl = {'ws': ' '} for i in [ @@ -19,7 +21,7 @@ class TestIdentifier(unittest.TestCase): t, r = i id = Identifier(t, 0) self.assertEqual(id.parse(None).fmt(fl), r, i) - + def test_scope(self): fl = {'ws': ' '} sc = Scope() @@ -36,7 +38,7 @@ class TestIdentifier(unittest.TestCase): t, r = i id = Identifier(t, 0) self.assertEqual(id.parse(sc).fmt(fl), r, i) - + def test_combinators(self): fl = {'ws': ' '} sc = Scope() @@ -46,8 +48,9 @@ class TestIdentifier(unittest.TestCase): (['&', '.scope', ' ', 'a'], '.current.scope a'), (['.scope', '&', ' ', 'a'], '.scope.current a'), (['.scope', ' ', 'a', '&'], '.scope a.current'), - (['&', '>' ,'.scope', ' ', 'a'], '.current > .scope a'), - (['.span', '&', '.scope', ' ', 'a', '&'], '.span.current.scope a.current'), + (['&', '>', '.scope', ' ', 'a'], '.current > .scope a'), + (['.span', '&', '.scope', ' ', 'a', '&'], + '.span.current.scope a.current'), ]: t, r = i id = Identifier(t, 0) @@ -56,17 +59,17 @@ class TestIdentifier(unittest.TestCase): sc.current = Identifier(['&', '.next'], 0).parse(sc) id = Identifier(['&', '.top'], 0) self.assertEqual(id.parse(sc).fmt(fl), '.current.next.top') - + def test_groups(self): fl = {'ws': ' '} sc = Scope() sc.push() - sc.current = Identifier(['.a', ',', '.b'], 0).parse(sc) + sc.current = Identifier(['.a', ',', '.b'], 0).parse(sc) for i in [ (['&', '.scope', ' ', 'a'], '.a.scope a, .b.scope a'), (['.scope', '&', ' ', 'a'], '.scope.a a, .scope.b a'), (['.scope', ' ', 'a', '&'], '.scope a.a, .scope a.b'), - (['>' ,'&', '.scope', ' ', 'a'], ' > .a.scope a, > .b.scope a'), + (['>', '&', '.scope', ' ', 'a'], ' > .a.scope a, > .b.scope a'), ]: t, r = i id = Identifier(t, 0) @@ -76,18 +79,19 @@ class TestIdentifier(unittest.TestCase): sc.current = Identifier(['.c', ',', '.d'], 0).parse(sc) id = Identifier(['.deep'], 0) self.assertEqual(id.parse(sc).fmt(fl), '.a .next .c .deep, ' - '.a .next .d .deep, ' - '.b .next .c .deep, ' - '.b .next .d .deep') + '.a .next .d .deep, ' + '.b .next .c .deep, ' + '.b .next .d .deep') self.assertEqual(id.raw(), '.a% %.next% %.c% %.deep%.a%' ' %.next% %.d% %.deep%.b% %.next%' ' %.c% %.deep%.b% %.next% %.d% %.deep') - + def test_media(self): fl = {'ws': ' '} sc = Scope() sc.push() - sc.current = Identifier(['@media', ' ', 'screen', ',', 'projection'], 0).parse(sc) + sc.current = Identifier( + ['@media', ' ', 'screen', ',', 'projection'], 0).parse(sc) self.assertEqual(sc.current.fmt(fl), '@media screen,projection') for i in [ (['html'], 'html'), @@ -95,7 +99,6 @@ class TestIdentifier(unittest.TestCase): t, r = i id = Identifier(t, 0) self.assertEqual(id.parse(sc).fmt(fl), r, i) - + if __name__ == '__main__': unittest.main() - \ No newline at end of file diff --git a/lesscpy/test/testissues.py b/lesscpy/test/testissues.py index 3bc2b42..4956991 100644 --- a/lesscpy/test/testissues.py +++ b/lesscpy/test/testissues.py @@ -9,16 +9,20 @@ import bootstrap from lesscpy.lessc import parser from lesscpy.lessc import formatter + class TestCase(unittest.TestCase): pass + class Opt(object): + def __init__(self): self.minify = False self.xminify = False self.tabs = True -def create_test (args): + +def create_test(args): def do_test_expected(self): lessf, cssf, minf = args if os.path.exists(cssf): @@ -31,18 +35,22 @@ def create_test (args): with open(cssf) as cssf: for line in cssf.readlines(): if i >= pl: - self.fail("%s: result has less lines (%d < %d)" % (cssf, i, pl)) + self.fail( + "%s: result has less lines (%d < %d)" % (cssf, i, pl)) line = line.rstrip() - if not line: continue - self.assertEqual(line, pout[i], '%s: Line %d' % (cssf, i+1)) + if not line: + continue + self.assertEqual( + line, pout[i], '%s: Line %d' % (cssf, i + 1)) i += 1 if pl > i and i: - self.fail("%s: result has more lines (%d > %d)" % (cssf, i, pl)) + self.fail( + "%s: result has more lines (%d > %d)" % (cssf, i, pl)) else: self.fail("%s not found..." % cssf) return do_test_expected -LESS = glob.glob( os.path.join('less/issues', '*.less')) +LESS = glob.glob(os.path.join('less/issues', '*.less')) for less in LESS: lessf = less.split('.')[0].split('/')[-1] css = 'css/issues/' + lessf + '.css' @@ -51,5 +59,5 @@ for less in LESS: test_method.__name__ = 'test_%s' % less.replace('./-', '_') setattr(TestCase, test_method.__name__, test_method) -if __name__=="__main__": - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/lesscpy/test/testless.py b/lesscpy/test/testless.py index 14c61f5..bed0815 100644 --- a/lesscpy/test/testless.py +++ b/lesscpy/test/testless.py @@ -9,16 +9,20 @@ import bootstrap from lesscpy.lessc import parser from lesscpy.lessc import formatter + class TestCase(unittest.TestCase): pass + class Opt(object): + def __init__(self): self.minify = False self.xminify = False self.tabs = True -def create_test (args): + +def create_test(args): def do_test_expected(self): lessf, cssf, minf = args if os.path.exists(cssf): @@ -31,13 +35,17 @@ def create_test (args): with open(cssf) as cssf: for line in cssf.readlines(): if i >= pl: - self.fail("%s: result has less lines (%d < %d)" % (cssf, i, pl)) + self.fail( + "%s: result has less lines (%d < %d)" % (cssf, i, pl)) line = line.rstrip() - if not line: continue - self.assertEqual(line, pout[i], '%s: Line %d' % (cssf, i+1)) + if not line: + continue + self.assertEqual( + line, pout[i], '%s: Line %d' % (cssf, i + 1)) i += 1 if pl > i and i: - self.fail("%s: result has more lines (%d > %d)" % (cssf, i, pl)) + self.fail( + "%s: result has more lines (%d > %d)" % (cssf, i, pl)) else: self.fail("%s not found..." % cssf) if os.path.exists(minf): @@ -52,16 +60,19 @@ def create_test (args): with open(minf) as cssf: for line in cssf.readlines(): if i >= ml: - self.fail("%s: result has less lines (%d < %d)" % (minf, i, ml)) - self.assertEqual(line.rstrip(), mout[i], '%s: Line %d' % (minf, i+1)) + self.fail( + "%s: result has less lines (%d < %d)" % (minf, i, ml)) + self.assertEqual( + line.rstrip(), mout[i], '%s: Line %d' % (minf, i + 1)) i += 1 if ml > i and i: - self.fail("%s: result has more lines (%d > %d)" % (minf, i, ml)) + self.fail( + "%s: result has more lines (%d > %d)" % (minf, i, ml)) else: self.fail("%s not found..." % minf) return do_test_expected -LESS = glob.glob( os.path.join('less/', '*.less')) +LESS = glob.glob(os.path.join('less/', '*.less')) for less in LESS: lessf = less.split('.')[0].split('/')[-1] css = 'css/' + lessf + '.css' @@ -70,5 +81,5 @@ for less in LESS: test_method.__name__ = 'test_%s' % less.replace('./-', '_') setattr(TestCase, test_method.__name__, test_method) -if __name__=="__main__": +if __name__ == "__main__": unittest.main() diff --git a/lesscpy/test/testscope.py b/lesscpy/test/testscope.py index 1e3fd83..7e7965e 100644 --- a/lesscpy/test/testscope.py +++ b/lesscpy/test/testscope.py @@ -4,8 +4,9 @@ if __name__ == '__main__': from lesscpy.lessc.scope import Scope from lesscpy.plib.identifier import Identifier -class TestIdentifier(unittest.TestCase): + +class TestIdentifier(unittest.TestCase): pass if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/lesscpy/test/testutility.py b/lesscpy/test/testutility.py index bb41b0c..3bf8ea1 100644 --- a/lesscpy/test/testutility.py +++ b/lesscpy/test/testutility.py @@ -3,7 +3,9 @@ if __name__ == '__main__': import bootstrap import lesscpy.lessc.utility as utility -class TestUtility(unittest.TestCase): + +class TestUtility(unittest.TestCase): + def testanalyze(self): test = utility.analyze_number self.assertEqual((0, ''), test('0')) @@ -23,7 +25,7 @@ class TestUtility(unittest.TestCase): 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)) @@ -33,7 +35,7 @@ class TestUtility(unittest.TestCase): 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)) @@ -43,7 +45,7 @@ class TestUtility(unittest.TestCase): 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)) @@ -54,7 +56,7 @@ class TestUtility(unittest.TestCase): 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')) @@ -69,7 +71,7 @@ class TestUtility(unittest.TestCase): self.assertFalse(test('.925')) self.assertFalse(test(False)) self.assertFalse(test([])) - + def testis_variable(self): test = utility.is_variable self.assertTrue(test('@var')) @@ -78,7 +80,7 @@ class TestUtility(unittest.TestCase): self.assertFalse(test('')) self.assertFalse(test(False)) self.assertFalse(test([])) - + def testwith_unit(self): test = utility.with_unit self.assertEqual('1px', test((1, 'px'))) @@ -89,8 +91,7 @@ class TestUtility(unittest.TestCase): 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 + unittest.main() From a54d89288756ccbd6144971f8d4dfd498978294a Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Fri, 19 Jul 2013 11:39:27 +0200 Subject: [PATCH 05/23] Use reStructuredText for README Automatic highlighting on Github and PyPI, much better readibility. Replace references to py3 with py2.6/2.7, it's not py3 code. --- MANIFEST.in | 2 +- README | 136 ----------------------------------------------- README.rst | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 4 +- 4 files changed, 153 insertions(+), 139 deletions(-) delete mode 100644 README create mode 100644 README.rst diff --git a/MANIFEST.in b/MANIFEST.in index 9575a1c..9d5d250 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include LICENSE -include README +include README.rst diff --git a/README b/README deleted file mode 100644 index b2238ca..0000000 --- a/README +++ /dev/null @@ -1,136 +0,0 @@ -* -* LESSCPY * -* -python LessCss Compiler. -v0.9h - -A compiler written in python 3 for the lesscss language. -For those of us not willing/able to have node.js installed in our environment. -Not all features of lesscss are supported (yet). -Some features wil probably never be supported (JavaScript evaluation). -This program uses PLY (Python Lex-Yacc) to tokenize/parse the input and is -considerably slower than the nodejs compiler. The plan is to utilize this -to build in proper syntax checking and perhaps YUI compressing. - -This is an early version, so you are likly to find bugs. - -For more information on lesscss: - * http://lesscss.org/ - * https://github.com/cloudhead/less.js - -Development files - * https://github.com/robotis/Lesscpy - -Requirements -============ - -* python 3+ -* ply (Python Lex-Yacc) python 3 version - -For more information on ply: -* http://www.dabeaz.com/ply/ - -Installation -============ - -python3 setup.py install - -or simply place the package into your python path. - -Compiler script Usage -===================== - -usage: lesscpy [-h] [-v] [-I INCLUDE] [-V] [-x] [-X] [-t] [-s SPACES] [-o OUT] - [-r] [-f] [-m] [-D] [-g] [-S] [-L] [-N] - target - -LessCss Compiler - -positional arguments: - target less file or directory - -optional arguments: - -h, --help show this help message and exit - -v, --version show program's version number and exit - -I INCLUDE, --include INCLUDE - Included less-files (comma separated) - -V, --verbose Verbose mode - -Formatting options: - -x, --minify Minify output - -X, --xminify Minify output, no end of block newlines - -t, --tabs Use tabs - -s SPACES, --spaces SPACES - Number of startline spaces (default 2) - -Directory options: - Compiles all *.less files in directory that have a newer timestamp than - it's css file. - - -o OUT, --out OUT Output directory - -r, --recurse Recursive into subdirectorys - -f, --force Force recompile on all files - -m, --min-ending Add '.min' into output filename. eg, name.min.css - -D, --dry-run Dry run, do not write files - -Debugging: - -g, --debug Debugging information - -S, --scopemap Scopemap - -L, --lex-only Run lexer on target - -N, --no-css No css output - -Supported features -================== -* Variables -* String interpolation -* Mixins -* mixins (Nested) -* mixins (Nested (Calls)) -* mixins (closures) -* mixins (recursive) -* Guard expressions -* Parametered mixins (class) -* Parametered mixins (id) -* @arguments -* Nesting -* Escapes ~/e() -* Expressions -* Keyframe blocks -* Color functions: -** lighten -** darken -** saturate -** desaturate -** spin -** hue -** mix -** saturation -** lightness -* Other functions: -** round -** increment -** decrement -** format '%(' -** add -** iscolor -** isnumber -** isurl -** isstring -** iskeyword -* Keyframe blocks - -Differences from lessc.js -========================= -* All MS filters and other strange vendor constructs must be escaped -* All colors are auto-formatted to #nnnnnn. eg, #f7e923 -* Does not preserve css comments - -Not supported -=================== -* JavaScript evaluation - -License -======= -See the LICENSE file - - diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..43306bb --- /dev/null +++ b/README.rst @@ -0,0 +1,150 @@ +LESSCPY +======= + +Python LESS Compiler. + +A compiler written in Python for the LESS language. For those of us not willing +or able to have node.js installed in our environment. Not all features of LESS +are supported (yet). Some features wil probably never be supported (JavaScript +evaluation). This program uses PLY (Python Lex-Yacc) to tokenize / parse the +input and is considerably slower than the NodeJS compiler. The plan is to +utilize this to build in proper syntax checking and perhaps YUI compressing. + +This is an early version, so you are likly to find bugs. + +For more information on LESS: + http://lesscss.org/ or https://github.com/cloudhead/less.js + +Development files: + https://github.com/robotis/Lesscpy + + +Requirements +------------ + +- Python 2.6 or 2.7 +- ply (Python Lex-Yacc) + +For more information on ply: + http://www.dabeaz.com/ply/ + + +Installation +------------ + +.. code-block:: bash + + python setup.py install + +Or simply place the package into your Python path. + + +Compiler script Usage +--------------------- + +.. code-block:: text + + usage: lesscpy [-h] [-v] [-I INCLUDE] [-V] [-x] [-X] [-t] [-s SPACES] [-o OUT] + [-r] [-f] [-m] [-D] [-g] [-S] [-L] [-N] + target + + LessCss Compiler + + positional arguments: + target less file or directory + + optional arguments: + -h, --help show this help message and exit + -v, --version show program's version number and exit + -I INCLUDE, --include INCLUDE + Included less-files (comma separated) + -V, --verbose Verbose mode + + Formatting options: + -x, --minify Minify output + -X, --xminify Minify output, no end of block newlines + -t, --tabs Use tabs + -s SPACES, --spaces SPACES + Number of startline spaces (default 2) + + Directory options: + Compiles all \*.less files in directory that have a newer timestamp than + it's css file. + + -o OUT, --out OUT Output directory + -r, --recurse Recursive into subdirectorys + -f, --force Force recompile on all files + -m, --min-ending Add '.min' into output filename. eg, name.min.css + -D, --dry-run Dry run, do not write files + + Debugging: + -g, --debug Debugging information + -S, --scopemap Scopemap + -L, --lex-only Run lexer on target + -N, --no-css No css output + + +Supported features +------------------ + +- Variables +- String interpolation +- Mixins +- mixins (Nested) +- mixins (Nested (Calls)) +- mixins (closures) +- mixins (recursive) +- Guard expressions +- Parametered mixins (class) +- Parametered mixins (id) +- @arguments +- Nesting +- Escapes ~/e() +- Expressions +- Keyframe blocks +- Color functions: + + - lighten + - darken + - saturate + - desaturate + - spin + - hue + - mix + - saturation + - lightness + +- Other functions: + + - round + - increment + - decrement + - format '%(' + - add + - iscolor + - isnumber + - isurl + - isstring + - iskeyword + +- Keyframe blocks + + +Differences from less.js +------------------------ + +- All MS filters and other strange vendor constructs must be escaped +- All colors are auto-formatted to #nnnnnn. eg, #f7e923 +- Does not preserve css comments + + +Not supported +------------- + +- JavaScript evaluation + + +License +------- + +See the LICENSE file diff --git a/setup.py b/setup.py index 6a33f96..6bce3ba 100755 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ setup( 'lesscpy/test/less/*.less', 'lesscpy/test/less/issues/*.less',]}, license=open('LICENSE').read(), - long_description=open('README').read(), -) \ No newline at end of file + long_description=open('README.rst').read(), +) From d668b9dafe67d2b6da82c8a75e06ba593bffb58f Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Fri, 19 Jul 2013 11:53:00 +0200 Subject: [PATCH 06/23] Some Python-3 compatibility fixes --- .travis.yml | 2 +- lesscpy/lessc/color.py | 4 ++-- lesscpy/lessc/lexer.py | 10 +++++----- lesscpy/lessc/parser.py | 5 ++++- lesscpy/lessc/utility.py | 3 +++ lesscpy/plib/mixin.py | 2 +- lesscpy/scripts/compiler.py | 5 ++++- tox.ini | 2 +- 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c84fed..84f25ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python python: - "2.6" - "2.7" - #- "3.3" + - "3.3" # command to install dependencies install: "pip install -r requirements.txt --use-mirrors" # command to run tests diff --git a/lesscpy/lessc/color.py b/lesscpy/lessc/color.py index f10683e..c0da7fa 100644 --- a/lesscpy/lessc/color.py +++ b/lesscpy/lessc/color.py @@ -62,7 +62,7 @@ class Color(): return self.rgba(*args) elif len(args) == 3: try: - return self._rgbatohex(map(int, args)) + return self._rgbatohex(list(map(int, args))) except ValueError: if all((a for a in args if a[-1] == '%' @@ -80,7 +80,7 @@ class Color(): """ if len(args) == 4: try: - return self._rgbatohex(map(int, args)) + return self._rgbatohex(list(map(int, args))) except ValueError: if all((a for a in args if a[-1] == '%' diff --git a/lesscpy/lessc/lexer.py b/lesscpy/lessc/lexer.py index 662f776..e03d86a 100644 --- a/lesscpy/lessc/lexer.py +++ b/lesscpy/lessc/lexer.py @@ -84,7 +84,7 @@ class LessLexer: def __init__(self): self.build(reflags=re.UNICODE | re.IGNORECASE) self.last = None - self.next = None + self.next_ = None self.pretok = True def t_css_filter(self, t): @@ -256,9 +256,9 @@ class LessLexer: 2. Strips out whitespace from nonsignificant locations to ease parsing. """ - if self.next: - t = self.next - self.next = None + if self.next_: + t = self.next_ + self.next_ = None return t while True: t = self.lexer.token() @@ -270,7 +270,7 @@ class LessLexer: continue self.pretok = False if t.type == '}' and self.last and self.last.type not in '{;}': - self.next = t + self.next_ = t tok = lex.LexToken() tok.type = ';' tok.value = ';' diff --git a/lesscpy/lessc/parser.py b/lesscpy/lessc/parser.py index a744679..d45c15f 100644 --- a/lesscpy/lessc/parser.py +++ b/lesscpy/lessc/parser.py @@ -1,4 +1,3 @@ -from __future__ import print_function # -*- coding: utf8 -*- """ .. module:: lesscpy.lessc.parser @@ -12,9 +11,13 @@ from __future__ import print_function See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ + +from __future__ import print_function + import os import sys import ply.yacc + from . import lexer from . import utility from .scope import Scope diff --git a/lesscpy/lessc/utility.py b/lesscpy/lessc/utility.py index b74cb7f..33977a4 100644 --- a/lesscpy/lessc/utility.py +++ b/lesscpy/lessc/utility.py @@ -7,6 +7,9 @@ See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ + +from __future__ import print_function + import collections import re diff --git a/lesscpy/plib/mixin.py b/lesscpy/plib/mixin.py index aec9d73..75ead7e 100644 --- a/lesscpy/plib/mixin.py +++ b/lesscpy/plib/mixin.py @@ -56,7 +56,7 @@ class Mixin(Node): raises: SyntaxError """ - arguments = zip(args, [' '] * len(args)) if args and args[0] else None + arguments = list(zip(args, [' '] * len(args))) if args and args[0] else None zl = itertools.zip_longest if sys.version_info[ 0] == 3 else itertools.izip_longest if self.args: diff --git a/lesscpy/scripts/compiler.py b/lesscpy/scripts/compiler.py index d3db996..f6b92c7 100644 --- a/lesscpy/scripts/compiler.py +++ b/lesscpy/scripts/compiler.py @@ -1,4 +1,3 @@ -from __future__ import print_function # -*- coding: utf8 -*- """ .. module:: lesscpy.scripts.compiler @@ -10,11 +9,15 @@ from __future__ import print_function See LICENSE for details .. moduleauthor:: Johann T. Mariusson """ + +from __future__ import print_function + import os import sys import glob import copy import argparse + sys.path.append(os.path.abspath(os.path.dirname(__file__))) from lesscpy.lessc import parser from lesscpy.lessc import lexer diff --git a/tox.ini b/tox.ini index 1d001e0..e1744d8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,pep8 +envlist = py26,py27,py33,pep8 [testenv] deps = -r{toxinidir}/requirements.txt From 5aa3b8437521449bfcd0b9df67ece9591c7958c9 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Fri, 19 Jul 2013 11:56:22 +0200 Subject: [PATCH 07/23] Add tox.ini and requirements.txt to MANIFEST.in --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 9d5d250..ed77be9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ include LICENSE include README.rst +include requirements.txt +include tox.ini From 1831a46fcc776a58016f242429343f6ce464f491 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Wed, 7 Aug 2013 16:47:02 +0200 Subject: [PATCH 08/23] Fix test path issues. Find fixtures when invoke via "python lesscpy/tests/__main__.py" and generate better names for 'less' and 'issues' tests. --- lesscpy/test/testissues.py | 10 +++++++--- lesscpy/test/testless.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lesscpy/test/testissues.py b/lesscpy/test/testissues.py index 4956991..e7d4610 100644 --- a/lesscpy/test/testissues.py +++ b/lesscpy/test/testissues.py @@ -51,12 +51,16 @@ def create_test(args): return do_test_expected LESS = glob.glob(os.path.join('less/issues', '*.less')) +_less_path = os.path.join(os.path.dirname(__file__), 'less', 'issues') +_css_path = os.path.join(os.path.dirname(__file__), 'css', 'issues') + +LESS = glob.glob(os.path.join(_less_path, '*.less')) for less in LESS: lessf = less.split('.')[0].split('/')[-1] - css = 'css/issues/' + lessf + '.css' - mincss = 'css/issues/' + lessf + '.min.css' + css = os.path.join(_css_path, lessf + '.css') + mincss = os.path.join(_css_path, lessf + '.min.css') test_method = create_test((less, css, mincss)) - test_method.__name__ = 'test_%s' % less.replace('./-', '_') + test_method.__name__ = 'test_%s' % "_".join(reversed(os.path.basename(less).split('.'))) setattr(TestCase, test_method.__name__, test_method) if __name__ == "__main__": diff --git a/lesscpy/test/testless.py b/lesscpy/test/testless.py index bed0815..51c7602 100644 --- a/lesscpy/test/testless.py +++ b/lesscpy/test/testless.py @@ -72,13 +72,17 @@ def create_test(args): self.fail("%s not found..." % minf) return do_test_expected -LESS = glob.glob(os.path.join('less/', '*.less')) + +_less_path = os.path.join(os.path.dirname(__file__), 'less') +_css_path = os.path.join(os.path.dirname(__file__), 'css') + +LESS = glob.glob(os.path.join(_less_path, '*.less')) for less in LESS: lessf = less.split('.')[0].split('/')[-1] - css = 'css/' + lessf + '.css' - mincss = 'css/' + lessf + '.min.css' + css = os.path.join(_css_path, lessf + '.css') + mincss = os.path.join(_css_path, lessf + '.min.css') test_method = create_test((less, css, mincss)) - test_method.__name__ = 'test_%s' % less.replace('./-', '_') + test_method.__name__ = 'test_%s' % "_".join(reversed(os.path.basename(less).split('.'))) setattr(TestCase, test_method.__name__, test_method) if __name__ == "__main__": From cea4f297c72db533760b4839772519aee5110605 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 09:39:30 +0200 Subject: [PATCH 09/23] Verbose testsuite runs --- .travis.yml | 3 +-- tox.ini | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 84f25ff..f210315 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,5 +6,4 @@ python: # command to install dependencies install: "pip install -r requirements.txt --use-mirrors" # command to run tests -script: python lesscpy/test/__main__.py - +script: python lesscpy/test/__main__.py -v diff --git a/tox.ini b/tox.ini index e1744d8..7b9e3da 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py26,py27,py33,pep8 [testenv] deps = -r{toxinidir}/requirements.txt -commands = python lesscpy/test/__main__.py +commands = python lesscpy/test/__main__.py -v [testenv:pep8] deps = pep8 From 9044b43eb58d175490cb128b6161f10e962b8388 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 09:16:01 +0200 Subject: [PATCH 10/23] Convert to int when using round(), ceil() or floor() round(1.2px) is expected return '1px', not '1.0px' --- lesscpy/plib/call.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lesscpy/plib/call.py b/lesscpy/plib/call.py index 0fb4137..d2fe8c7 100644 --- a/lesscpy/plib/call.py +++ b/lesscpy/plib/call.py @@ -196,7 +196,7 @@ class Call(Node): str """ n, u = utility.analyze_number(value) - return utility.with_unit(round(float(n)), u) + return utility.with_unit(int(round(float(n))), u) def ceil(self, value, *args): """ Ceil number @@ -206,7 +206,7 @@ class Call(Node): str """ n, u = utility.analyze_number(value) - return utility.with_unit(math.ceil(n), u) + return utility.with_unit(int(math.ceil(n)), u) def floor(self, value, *args): """ Floor number @@ -216,7 +216,7 @@ class Call(Node): str """ n, u = utility.analyze_number(value) - return utility.with_unit(math.floor(n), u) + return utility.with_unit(int(math.floor(n)), u) def percentage(self, value, *args): """ Return percentage value From 59b243a181349f82b2c66bd3c87fd2b3919e167d Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 09:19:20 +0200 Subject: [PATCH 11/23] lighten(#555, 10%) should return #6f6f6f, not #6e6e6e Matches lessc's output. --- lesscpy/test/css/colors.css | 2 +- lesscpy/test/css/colors.min.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lesscpy/test/css/colors.css b/lesscpy/test/css/colors.css index 0ddc16e..1f0d87d 100644 --- a/lesscpy/test/css/colors.css +++ b/lesscpy/test/css/colors.css @@ -75,7 +75,7 @@ } .lighten { color: #585858; - color: #6e6e6e; + color: #6f6f6f; color: #888888; color: #bbbbbb; color: #eeeeee; diff --git a/lesscpy/test/css/colors.min.css b/lesscpy/test/css/colors.min.css index 60bee80..839529d 100644 --- a/lesscpy/test/css/colors.min.css +++ b/lesscpy/test/css/colors.min.css @@ -12,7 +12,7 @@ .hsl{color:#ffffff;color:#ffffff;color:#000000;color:#000000;} .saturate{color:#565454;color:#5e4c4c;color:#664444;color:#773333;color:#882222;color:#aa0000;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#243830;color:#203c31;color:#174533;color:#0d4f35;color:#005c37;} .desaturate{color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;} -.lighten{color:#585858;color:#6e6e6e;color:#888888;color:#bbbbbb;color:#eeeeee;color:#ffffff;color:#ffffff;color:#000000;color:#ffffff;color:#ffffff;color:#2b3632;color:#404f49;color:#566c63;color:#88a096;color:#c1cdc8;color:#ffffff;} +.lighten{color:#585858;color:#6f6f6f;color:#888888;color:#bbbbbb;color:#eeeeee;color:#ffffff;color:#ffffff;color:#000000;color:#ffffff;color:#ffffff;color:#2b3632;color:#404f49;color:#566c63;color:#88a096;color:#c1cdc8;color:#ffffff;} .lighten{color:#525252;color:#3b3b3b;color:#222222;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#ffffff;color:#27302c;color:#121715;color:#000000;color:#000000;color:#000000;color:#000000;} .spin{color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#29332d;color:#293332;color:#2a3329;color:#292d33;color:#2c2933;} .grayscale{color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;} From 2b2a41751e97fa14f6c37ab5aea6598645e2eb4a Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 10:07:43 +0200 Subject: [PATCH 12/23] Use precise grid margin values This is a bit controversial since it's different from lessc output. Still, it's correct since 87.8+27+1.7 == 116.5 (offset3_margin-left + span1_width + span_margin-left == offset4_margin-left) --- lesscpy/test/css/grid.css | 4 ++-- lesscpy/test/css/grid.min.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lesscpy/test/css/grid.css b/lesscpy/test/css/grid.css index 67674c2..ba266f1 100644 --- a/lesscpy/test/css/grid.css +++ b/lesscpy/test/css/grid.css @@ -124,13 +124,13 @@ margin-left: 59.1; } .offset3 { - margin-left: 87.80000000000001; + margin-left: 87.8; } .offset4 { margin-left: 116.5; } .offset5 { - margin-left: 145.20000000000002; + margin-left: 145.2; } .offset6 { margin-left: 173.9; diff --git a/lesscpy/test/css/grid.min.css b/lesscpy/test/css/grid.min.css index 06c58e4..215ccc7 100644 --- a/lesscpy/test/css/grid.min.css +++ b/lesscpy/test/css/grid.min.css @@ -39,9 +39,9 @@ .span12,.container{width:342.7;} .offset1{margin-left:30.4;} .offset2{margin-left:59.1;} -.offset3{margin-left:87.80000000000001;} +.offset3{margin-left:87.8;} .offset4{margin-left:116.5;} -.offset5{margin-left:145.20000000000002;} +.offset5{margin-left:145.2;} .offset6{margin-left:173.9;} .offset7{margin-left:202.6;} .offset8{margin-left:231.3;} From 81c4e765d6ffc171750c6922430209eae31a07f4 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Wed, 7 Aug 2013 17:47:44 +0200 Subject: [PATCH 13/23] Implement SVG stylesheets. Including SVG-specific elements and attributes and an initial testsuite. --- lesscpy/lib/css.py | 50 ++++++++++++++++++++++++++++++++-- lesscpy/lib/dom.py | 19 +++++++++++++ lesscpy/test/css/svg.css | 53 ++++++++++++++++++++++++++++++++++++ lesscpy/test/css/svg.min.css | 15 ++++++++++ lesscpy/test/less/svg.less | 39 ++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 lesscpy/test/css/svg.css create mode 100644 lesscpy/test/css/svg.min.css create mode 100644 lesscpy/test/less/svg.less diff --git a/lesscpy/lib/css.py b/lesscpy/lib/css.py index cf95d07..14cb58a 100644 --- a/lesscpy/lib/css.py +++ b/lesscpy/lib/css.py @@ -272,6 +272,52 @@ css3 = [ 'word-break', 'word-wrap' ] +# SVG only includes style not present in either css2 or css3: +svg = [ + # clipping / masking / compositing: + 'clip-path', + 'clip-rule', + 'mask', + # filter effects: + 'enable-background', + 'filter', + 'flood-color', + 'flood-opacity', + 'lightning-color', + # gradient: + 'stop-color', + 'stop-opacity', + # interactivity: + 'pointer-events', + # color / painting: + 'color-interpolation', + 'color-interpolation-filters', + 'color-rendering', + 'fill', + 'fill-opacity', + 'fill-rule', + 'image-rendering', + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + 'shape-rendering', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'text-rendering', + # text: + 'glyph-orientation-horizontal', + 'glyph-orientation-vertical', + 'kerning', + 'text-anchor', + 'writing-mode', +] vendor_prefix = [ '-ms-', '-moz-', @@ -284,10 +330,8 @@ vendor_prefix = [ 'mso-', ] vendor_ugly = [ - 'filter', 'accelerator', 'behavior', - 'filter', 'zoom', ] -propertys = css2 + css3 + vendor_ugly +propertys = css2 + css3 + svg + vendor_ugly diff --git a/lesscpy/lib/dom.py b/lesscpy/lib/dom.py index 2569d84..8f662af 100644 --- a/lesscpy/lib/dom.py +++ b/lesscpy/lib/dom.py @@ -140,5 +140,24 @@ html5 = [ 'video', 'wbr', ] +svg = [ + 'altGlyph', + 'altGlyphDef', + 'altGlyphItem', + 'circle', + 'desc', + 'ellipse', + 'glyphRef', + 'line', + 'path', + 'polygon', + 'polyline', + 'rect', + 'text', + 'textPath', + 'tref', + 'tspan', +] html = html4 html.extend(html5) +html.extend(svg) diff --git a/lesscpy/test/css/svg.css b/lesscpy/test/css/svg.css new file mode 100644 index 0000000..afabf9d --- /dev/null +++ b/lesscpy/test/css/svg.css @@ -0,0 +1,53 @@ +svg { + color: red; +} +circle { + stroke: #006600; + fill: #00cc00; +} +circle .green { + stroke: #006600; + stroke-width: 9; + fill: #00cc00; +} +rect { + fill: blue; + fill-opacity: 0.1; + stroke: green; + opacity: 0.5; +} +ellipse { + fill: yellow; +} +line { + stroke: red; +} +path { + stroke: blue; +} +polygon { + fill: lime; + fill-rule: nonzero; +} +polygon { + fill: lime; + fill-rule: evenodd; +} +polyline { + fill: none; +} +.node text { + font: 12px sans-serif; +} +tref { + font-weight: bold; +} +text tref { + font-weight: bold; +} +tspan { + font-weight: bold; +} +text tspan { + font-weight: bold; +} diff --git a/lesscpy/test/css/svg.min.css b/lesscpy/test/css/svg.min.css new file mode 100644 index 0000000..6f96b1b --- /dev/null +++ b/lesscpy/test/css/svg.min.css @@ -0,0 +1,15 @@ +svg{color:red;} +circle{stroke:#006600;fill:#00cc00;} +circle .green{stroke:#006600;stroke-width:9;fill:#00cc00;} +rect{fill:blue;fill-opacity:0.1;stroke:green;opacity:0.5;} +ellipse{fill:yellow;} +line{stroke:red;} +path{stroke:blue;} +polygon{fill:lime;fill-rule:nonzero;} +polygon{fill:lime;fill-rule:evenodd;} +polyline{fill:none;} +.node text{font:12px sans-serif;} +tref{font-weight:bold;} +text tref{font-weight:bold;} +tspan{font-weight:bold;} +text tspan{font-weight:bold;} diff --git a/lesscpy/test/less/svg.less b/lesscpy/test/less/svg.less new file mode 100644 index 0000000..fe1443d --- /dev/null +++ b/lesscpy/test/less/svg.less @@ -0,0 +1,39 @@ +svg {color: red;} + +circle { + stroke: #006600; + fill: #00cc00; +} +circle .green { + stroke: #006600; + stroke-width: 9; + fill: #00cc00; +} +rect { + fill: blue; + fill-opacity: 0.1; + stroke: green; + opacity: 0.5; +} +ellipse { fill: yellow; } +line { stroke: red; } +path { stroke: blue; } +polygon { + fill: lime; + fill-rule: nonzero; +} +polygon { + fill: lime; + fill-rule: evenodd; +} +polyline { fill: none; } + +.node text { font: 12px sans-serif; } +tref { font-weight: bold; } +text tref { font-weight: bold; } +tspan { font-weight: bold; } +text tspan { font-weight: bold; } +/* NOTE(saschpe): TODO +textPath { font-weight: bold; } +text textPath { font-weight: bold; } +svg text textPath { font-weight: bold; } */ From a80cb8942a0d95b6b1d9ed5733bd1f706aa31a46 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 10:53:46 +0200 Subject: [PATCH 14/23] Allow camel-case DOM elements defined in SVG-1.1 The SVG standard defines several DOM elements in camel-case (namely textPath, altGlyph, altGlyphDef, altGlyphItem and glyphRef). Even though most browser also accept lower-case, the lexer should take this into account. --- lesscpy/lessc/lexer.py | 2 +- lesscpy/test/css/svg.css | 9 +++++++++ lesscpy/test/css/svg.min.css | 3 +++ lesscpy/test/less/svg.less | 3 +-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lesscpy/lessc/lexer.py b/lesscpy/lessc/lexer.py index e03d86a..d6ad3f1 100644 --- a/lesscpy/lessc/lexer.py +++ b/lesscpy/lessc/lexer.py @@ -129,7 +129,7 @@ class LessLexer: elif v in css.propertys: t.type = 'css_property' t.value = t.value.strip() - elif v.lower() in dom.html: + elif v in dom.html or v.lower() in dom.html: t.type = 'css_dom' elif c == '@': v = v.lower() diff --git a/lesscpy/test/css/svg.css b/lesscpy/test/css/svg.css index afabf9d..398ecea 100644 --- a/lesscpy/test/css/svg.css +++ b/lesscpy/test/css/svg.css @@ -51,3 +51,12 @@ tspan { text tspan { font-weight: bold; } +textPath { + font-weight: bold; +} +text textPath { + font-weight: bold; +} +svg text textPath { + font-weight: bold; +} diff --git a/lesscpy/test/css/svg.min.css b/lesscpy/test/css/svg.min.css index 6f96b1b..c79a416 100644 --- a/lesscpy/test/css/svg.min.css +++ b/lesscpy/test/css/svg.min.css @@ -13,3 +13,6 @@ tref{font-weight:bold;} text tref{font-weight:bold;} tspan{font-weight:bold;} text tspan{font-weight:bold;} +textPath{font-weight:bold;} +text textPath{font-weight:bold;} +svg text textPath{font-weight:bold;} diff --git a/lesscpy/test/less/svg.less b/lesscpy/test/less/svg.less index fe1443d..95fe8f6 100644 --- a/lesscpy/test/less/svg.less +++ b/lesscpy/test/less/svg.less @@ -33,7 +33,6 @@ tref { font-weight: bold; } text tref { font-weight: bold; } tspan { font-weight: bold; } text tspan { font-weight: bold; } -/* NOTE(saschpe): TODO textPath { font-weight: bold; } text textPath { font-weight: bold; } -svg text textPath { font-weight: bold; } */ +svg text textPath { font-weight: bold; } From 92c0321b3a704274a02bfadb769713d6f14496cb Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 10:57:16 +0200 Subject: [PATCH 15/23] Bump version to 0.9i --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6bce3ba..dc0e9ae 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from distutils.core import setup setup( name='lesscpy', - version='0.9h', + version='0.9i', description='Lesscss compiler.', author='Jóhann T Maríusson', author_email='jtm@robot.is', From d8cd1fa96869d573f9cfc6b8a294ae7c0c567c0c Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 12:47:37 +0200 Subject: [PATCH 16/23] Don't switch operands for non-implemented operators. Bot __truediv__(int, float) and __add__(int, float) raise NotImplemented, but switching only helps in the case of addition. Division isn't commutative so simply cast the int to float. Due to this, lesscpy now generates the same result as lessc in the modified test case. --- lesscpy/plib/expression.py | 4 +++- lesscpy/test/css/calls.css | 2 +- lesscpy/test/css/calls.min.css | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lesscpy/plib/expression.py b/lesscpy/plib/expression.py index 6bd27ce..5b45374 100644 --- a/lesscpy/plib/expression.py +++ b/lesscpy/plib/expression.py @@ -125,7 +125,9 @@ class Expression(Node): else: ret = getattr(vala, operation)(valb) if ret is NotImplemented: - ret = getattr(valb, operation)(vala) + # __truediv__(int, float) isn't implemented, but __truediv__(float, float) is. + # __add__(int, float) is similar. Simply cast vala to float: + ret = getattr(float(vala), operation)(valb) if oper in '+-*/': try: if int(ret) == ret: diff --git a/lesscpy/test/css/calls.css b/lesscpy/test/css/calls.css index d0f6aef..d0f88b0 100644 --- a/lesscpy/test/css/calls.css +++ b/lesscpy/test/css/calls.css @@ -19,7 +19,7 @@ format-url-encode: 'red is %23ff0000'; } #more { - width: 1px; + width: 2px; height: 1px; top: 50%; } diff --git a/lesscpy/test/css/calls.min.css b/lesscpy/test/css/calls.min.css index f5cb8c2..0dc07ba 100644 --- a/lesscpy/test/css/calls.min.css +++ b/lesscpy/test/css/calls.min.css @@ -1,6 +1,6 @@ #standard{width:16;height:undefined("self");border-width:5;variable:11;decrement:9;rounded:11;roundedpx:3px;} #escapes{escaped:-Some::weird(#thing, y);escaped1:-Some::weird(#thing, z);eformat:rgb(32, 128, 64);} #format{format:"rgb(32, 128, 64)";format-string:"hello world";format-multiple:"hello earth 2";format-url-encode:'red is %23ff0000';} -#more{width:1px;height:1px;top:50%;} +#more{width:2px;height:1px;top:50%;} #colors{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#333333', GradientType=1);} a{background-image:linear-gradient(top,#ffffff,#333333);background-image:linear-gradient(top,#a6a6a6,#000000);} From 57a9891d77d6480e4bbe39ec1f84ea13ca802efe Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 13:22:30 +0200 Subject: [PATCH 17/23] Revert "Use precise grid margin values" This reverts commit 2b2a41751e97fa14f6c37ab5aea6598645e2eb4a. --- lesscpy/test/css/grid.css | 4 ++-- lesscpy/test/css/grid.min.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lesscpy/test/css/grid.css b/lesscpy/test/css/grid.css index ba266f1..67674c2 100644 --- a/lesscpy/test/css/grid.css +++ b/lesscpy/test/css/grid.css @@ -124,13 +124,13 @@ margin-left: 59.1; } .offset3 { - margin-left: 87.8; + margin-left: 87.80000000000001; } .offset4 { margin-left: 116.5; } .offset5 { - margin-left: 145.2; + margin-left: 145.20000000000002; } .offset6 { margin-left: 173.9; diff --git a/lesscpy/test/css/grid.min.css b/lesscpy/test/css/grid.min.css index 215ccc7..06c58e4 100644 --- a/lesscpy/test/css/grid.min.css +++ b/lesscpy/test/css/grid.min.css @@ -39,9 +39,9 @@ .span12,.container{width:342.7;} .offset1{margin-left:30.4;} .offset2{margin-left:59.1;} -.offset3{margin-left:87.8;} +.offset3{margin-left:87.80000000000001;} .offset4{margin-left:116.5;} -.offset5{margin-left:145.2;} +.offset5{margin-left:145.20000000000002;} .offset6{margin-left:173.9;} .offset7{margin-left:202.6;} .offset8{margin-left:231.3;} From 833d887789e48219e00bdf8019cbbd68b7eb6bc3 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 15:12:03 +0200 Subject: [PATCH 18/23] Really fix rounding errors The result of str(float) was changed with Python3: % python Python 2.7.3 (default, Apr 14 2012, 08:58:41) [GCC] on linux2 >>> repr(1.1*1.1) '1.2100000000000002' >>> str(1.1*1.1) '1.21' % python3 Python 3.3.0 (default, Oct 01 2012, 09:13:30) [GCC] on linux >>> repr(1.1*1.1) '1.2100000000000002' >>> str(1.1*1.1) '1.2100000000000002' Thus, instead of rounding the resulting CSS, don't use str() but rather repr() to return the correct value in with_unit(). --- lesscpy/plib/expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lesscpy/plib/expression.py b/lesscpy/plib/expression.py index 5b45374..d758db1 100644 --- a/lesscpy/plib/expression.py +++ b/lesscpy/plib/expression.py @@ -96,7 +96,7 @@ class Expression(Node): return str(val) + ua elif ub: return str(val) + ub - return str(val) + return repr(val) def operate(self, vala, valb, oper): """Perform operation From 3e8c4b71df6515856d69507f9ec286b41dfe8a04 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 17:43:35 +0200 Subject: [PATCH 19/23] Use CSS class '.darken' for darken(), not '.lighten' --- lesscpy/test/css/colors.css | 2 +- lesscpy/test/css/colors.min.css | 2 +- lesscpy/test/less/colors.less | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lesscpy/test/css/colors.css b/lesscpy/test/css/colors.css index 1f0d87d..78030de 100644 --- a/lesscpy/test/css/colors.css +++ b/lesscpy/test/css/colors.css @@ -91,7 +91,7 @@ color: #c1cdc8; color: #ffffff; } -.lighten { +.darken { color: #525252; color: #3b3b3b; color: #222222; diff --git a/lesscpy/test/css/colors.min.css b/lesscpy/test/css/colors.min.css index 839529d..b9ea18f 100644 --- a/lesscpy/test/css/colors.min.css +++ b/lesscpy/test/css/colors.min.css @@ -13,7 +13,7 @@ .saturate{color:#565454;color:#5e4c4c;color:#664444;color:#773333;color:#882222;color:#aa0000;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#243830;color:#203c31;color:#174533;color:#0d4f35;color:#005c37;} .desaturate{color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;} .lighten{color:#585858;color:#6f6f6f;color:#888888;color:#bbbbbb;color:#eeeeee;color:#ffffff;color:#ffffff;color:#000000;color:#ffffff;color:#ffffff;color:#2b3632;color:#404f49;color:#566c63;color:#88a096;color:#c1cdc8;color:#ffffff;} -.lighten{color:#525252;color:#3b3b3b;color:#222222;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#ffffff;color:#27302c;color:#121715;color:#000000;color:#000000;color:#000000;color:#000000;} +.darken{color:#525252;color:#3b3b3b;color:#222222;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#ffffff;color:#27302c;color:#121715;color:#000000;color:#000000;color:#000000;color:#000000;} .spin{color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#29332d;color:#293332;color:#2a3329;color:#292d33;color:#2c2933;} .grayscale{color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;} .mix{color:#7f007f;color:#7f7f7f;color:#7f9055;color:#3f00bf;color:#ff0000;color:#0000ff;} diff --git a/lesscpy/test/less/colors.less b/lesscpy/test/less/colors.less index 89b0fa2..14dccf2 100644 --- a/lesscpy/test/less/colors.less +++ b/lesscpy/test/less/colors.less @@ -98,7 +98,7 @@ color: lighten(#29332f, 60%); color: lighten(#29332f, 100%); } -.lighten { +.darken { color: darken(#555, 1%); color: darken(#555, 10%); color: darken(#555, 20%); From 7a65e81fa7b9ce0b0997b5fd8b956be36faaf3a7 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 17:44:31 +0200 Subject: [PATCH 20/23] Add utility function convergent_round Mimics Python3's changed rounding behavior: "The round() function rounding strategy and return type have changed. Exact halfway cases are now rounded to the nearest even result instead of away from zero. (For example, round(2.5) now returns 2 rather than 3.) round(x[, n]) now delegates to x.__round__([n]) instead of always returning a float. It generally returns an integer when called with a single argument and a value of the same type as x when called with two arguments." --- lesscpy/lessc/utility.py | 22 ++++++++++++++++++++++ lesscpy/test/testutility.py | 14 ++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lesscpy/lessc/utility.py b/lesscpy/lessc/utility.py index 33977a4..73f5361 100644 --- a/lesscpy/lessc/utility.py +++ b/lesscpy/lessc/utility.py @@ -11,6 +11,7 @@ from __future__ import print_function import collections +import math import re @@ -243,3 +244,24 @@ def split_unit(value): """ r = re.search('^(\-?[\d\.]+)(.*)$', str(value)) return r.groups() if r else ('', '') + + +def convergent_round(value, ndigits=0): + """Convergent rounding. + + Round to neareas even, similar to Python3's round() method. + """ + if sys.version_info[0] < 3: + if value < 0.0: + return -convergent_round(-value) + + epsilon = 0.0000001 + integral_part, _ = divmod(value, 1) + + if abs(value - (integral_part + 0.5)) < epsilon: + if integral_part % 2.0 < epsilon: + return integral_part + else: + nearest_even = integral_part + 0.5 + return math.ceil(nearest_even) + return round(value, ndigits) diff --git a/lesscpy/test/testutility.py b/lesscpy/test/testutility.py index 3bf8ea1..3a2a992 100644 --- a/lesscpy/test/testutility.py +++ b/lesscpy/test/testutility.py @@ -92,6 +92,20 @@ class TestUtility(unittest.TestCase): self.assertEqual('1', test(1, None)) self.assertEqual('1', test(1,)) + def test_convergent_round(self): + test = utility.convergent_round + self.assertEqual(-4, test(-4.5)) + self.assertEqual(-4, test(-3.5)) + self.assertEqual(-2, test(-2.5)) + self.assertEqual(-2, test(-1.5)) + self.assertEqual(0, test(-0.5)) + self.assertEqual(0, test(0.5)) + self.assertEqual(2, test(1.5)) + self.assertEqual(2, test(2.5)) + self.assertEqual(3.0, test(10.0 / 3, 0)) + self.assertEqual(4, test(3.5)) + self.assertEqual(4, test(4.5)) + if __name__ == '__main__': unittest.main() From 8da243d2b83e9397e4d1a305e2902a09fca37957 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 17:56:36 +0200 Subject: [PATCH 21/23] Use convergent_round everywhere except for round() LESS function. The LESS function round uses away-from-zero rounding. --- lesscpy/lessc/color.py | 12 ++++++------ lesscpy/test/css/calls.css | 2 ++ lesscpy/test/css/calls.min.css | 2 +- lesscpy/test/less/calls.less | 2 ++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lesscpy/lessc/color.py b/lesscpy/lessc/color.py index c0da7fa..1a392ed 100644 --- a/lesscpy/lessc/color.py +++ b/lesscpy/lessc/color.py @@ -105,7 +105,7 @@ class Color(): if isinstance(s, str): s = int(s.strip('%')) rgb = colorsys.hls_to_rgb(int(h) / 360.0, l / 100.0, s / 100.0) - color = (round(c * 255) for c in rgb) + color = (utility.convergent_round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') @@ -123,8 +123,8 @@ class Color(): if isinstance(s, str): s = int(s.strip('%')) rgb = colorsys.hls_to_rgb(int(h) / 360.0, l / 100.0, s / 100.0) - color = [float(round(c * 255)) for c in rgb] - color.append(round(float(a[:-1]) / 100.0, 2)) + color = [float(utility.convergent_round(c * 255)) for c in rgb] + color.append(utility.convergent_round(float(a[:-1]) / 100.0, 2)) return "rgba(%s,%s,%s,%s)" % tuple(color) raise ValueError('Illegal color values') @@ -139,7 +139,7 @@ class Color(): """ if color: h, l, s = self._hextohls(color) - return round(h * 360.0, 3) + return utility.convergent_round(h * 360.0, 3) raise ValueError('Illegal color values') def saturation(self, color, *args): @@ -260,7 +260,7 @@ class Color(): h = ((h * 360.0) + degree) % 360.0 h = 360.0 + h if h < 0 else h rgb = colorsys.hls_to_rgb(h / 360.0, l, s) - color = (round(c * 255) for c in rgb) + color = (utility.convergent_round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') @@ -364,5 +364,5 @@ class Color(): hls = list(self._hextohls(color)) hls[idx] = self._clamp(getattr(hls[idx], op)(diff / 100.0)) rgb = colorsys.hls_to_rgb(*hls) - color = (round(c * 255) for c in rgb) + color = (utility.convergent_round(c * 255) for c in rgb) return self._rgbatohex(color) diff --git a/lesscpy/test/css/calls.css b/lesscpy/test/css/calls.css index d0f88b0..cde5394 100644 --- a/lesscpy/test/css/calls.css +++ b/lesscpy/test/css/calls.css @@ -6,6 +6,8 @@ decrement: 9; rounded: 11; roundedpx: 3px; + round25: 3; + round15: 2; } #escapes { escaped: -Some::weird(#thing, y); diff --git a/lesscpy/test/css/calls.min.css b/lesscpy/test/css/calls.min.css index 0dc07ba..c896d45 100644 --- a/lesscpy/test/css/calls.min.css +++ b/lesscpy/test/css/calls.min.css @@ -1,4 +1,4 @@ -#standard{width:16;height:undefined("self");border-width:5;variable:11;decrement:9;rounded:11;roundedpx:3px;} +#standard{width:16;height:undefined("self");border-width:5;variable:11;decrement:9;rounded:11;roundedpx:3px;round25:3;round15:2;} #escapes{escaped:-Some::weird(#thing, y);escaped1:-Some::weird(#thing, z);eformat:rgb(32, 128, 64);} #format{format:"rgb(32, 128, 64)";format-string:"hello world";format-multiple:"hello earth 2";format-url-encode:'red is %23ff0000';} #more{width:2px;height:1px;top:50%;} diff --git a/lesscpy/test/less/calls.less b/lesscpy/test/less/calls.less index b420bc7..8cd987a 100644 --- a/lesscpy/test/less/calls.less +++ b/lesscpy/test/less/calls.less @@ -11,6 +11,8 @@ decrement: decrement(@var); rounded: round(@r/3); roundedpx: round(10px / 3); + round25: round(2.5); + round15: round(1.5); } #escapes { escaped: e("-Some::weird(#thing, y)"); From e6512edb98e8b80c6a793986aea75afda406ad1a Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 18:06:10 +0200 Subject: [PATCH 22/23] Add utility away_from_zero_round and use it for LESS round() Also _ophsl should use it when running unter Py3k (instead of convergent_round). --- lesscpy/lessc/color.py | 2 +- lesscpy/lessc/utility.py | 13 +++++++++++++ lesscpy/plib/call.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lesscpy/lessc/color.py b/lesscpy/lessc/color.py index 1a392ed..cde21b3 100644 --- a/lesscpy/lessc/color.py +++ b/lesscpy/lessc/color.py @@ -364,5 +364,5 @@ class Color(): hls = list(self._hextohls(color)) hls[idx] = self._clamp(getattr(hls[idx], op)(diff / 100.0)) rgb = colorsys.hls_to_rgb(*hls) - color = (utility.convergent_round(c * 255) for c in rgb) + color = (utility.away_from_zero_round(c * 255) for c in rgb) return self._rgbatohex(color) diff --git a/lesscpy/lessc/utility.py b/lesscpy/lessc/utility.py index 73f5361..02b1d07 100644 --- a/lesscpy/lessc/utility.py +++ b/lesscpy/lessc/utility.py @@ -13,6 +13,7 @@ from __future__ import print_function import collections import math import re +import sys def flatten(lst): @@ -246,6 +247,18 @@ def split_unit(value): return r.groups() if r else ('', '') +def away_from_zero_round(value, ndigits=0): + """Round half-way away from zero. + + Python2's round() method. + """ + if sys.version_info[0] >= 3: + p = 10 ** ndigits + return float(math.floor((value * p) + math.copysign(0.5, value))) / p + else: + return round(value, ndigits) + + def convergent_round(value, ndigits=0): """Convergent rounding. diff --git a/lesscpy/plib/call.py b/lesscpy/plib/call.py index d2fe8c7..2b8bbcb 100644 --- a/lesscpy/plib/call.py +++ b/lesscpy/plib/call.py @@ -196,7 +196,7 @@ class Call(Node): str """ n, u = utility.analyze_number(value) - return utility.with_unit(int(round(float(n))), u) + return utility.with_unit(int(utility.away_from_zero_round(float(n))), u) def ceil(self, value, *args): """ Ceil number From 92c84c495b2bd04e2e985e3cdfcaa00dff09d11b Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 8 Aug 2013 18:26:38 +0200 Subject: [PATCH 23/23] Bump version to 0.9j --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dc0e9ae..c42b00a 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from distutils.core import setup setup( name='lesscpy', - version='0.9i', + version='0.9j', description='Lesscss compiler.', author='Jóhann T Maríusson', author_email='jtm@robot.is',