Merge pull request #16 from saschpe/master

Several fixes
This commit is contained in:
robotis
2013-07-19 04:45:15 -07:00
40 changed files with 1120 additions and 969 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ parser.out
build
dist
*.pyc
MANIFEST

10
.travis.yml Normal file
View File

@@ -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

View File

@@ -1,2 +1,4 @@
include LICENSE
include README
include README.rst
include requirements.txt
include tox.ini

136
README
View File

@@ -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
<jtm@robot.is>

150
README.rst Normal file
View File

@@ -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

View File

@@ -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
"""
"""

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.lessc.color
:synopsis: Lesscpy Color functions
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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:
@@ -58,15 +62,15 @@ 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] == '%'
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:
@@ -76,15 +80,15 @@ 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] == '%'
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)

View File

@@ -2,21 +2,24 @@
"""
.. module:: lesscpy.lessc.formatter
:synopsis: CSS Formatter class.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
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()

View File

@@ -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.
<jtm@robot.is>
@@ -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.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:
@@ -254,20 +256,21 @@ 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()
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 '{;}':
self.next = t
self.next_ = t
tok = lex.LexToken()
tok.type = ';'
tok.value = ';'

View File

@@ -1,43 +1,48 @@
from __future__ import print_function
# -*- coding: utf8 -*-
"""
.. 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 <jtm@robot.is>
"""
from __future__ import print_function
import os
import sys
import ply.yacc
from . import lexer
from . import utility
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 +53,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 +76,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 +104,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 +142,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 +190,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 +201,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 +212,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 +222,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 +230,7 @@ class LessParser(object):
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
#
def p_mixin(self, p):
""" mixin_decl : open_mixin declaration_list brace_close
@@ -241,12 +250,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 +263,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 +291,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 +325,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 +359,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 +368,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 +380,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 +391,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 +421,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 +438,10 @@ class LessParser(object):
| css_ms_filter
"""
p[0] = p[1]
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
#
def p_identifier(self, p):
""" identifier : identifier_list
@@ -440,7 +449,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 +461,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 +480,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 +521,7 @@ class LessParser(object):
| general_sibling_selector
"""
p[0] = p[1]
def p_ident_part(self, p):
""" ident_part : class
| id
@@ -519,22 +530,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 +563,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 +577,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 +610,10 @@ class LessParser(object):
| fcall
"""
p[0] = p[1]
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
#
def p_expression_aux(self, p):
""" expression : expression '+' expression
@@ -612,160 +623,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 +787,7 @@ class LessParser(object):
break
self.parser.restart()
return t
def handle_error(self, e, line, t='E'):
""" Custom error handler
args:
@@ -785,5 +797,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)

View File

@@ -1,17 +1,20 @@
"""
.. module:: lesscpy.lessc.scope
:synopsis: Scope class.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
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

View File

@@ -2,19 +2,23 @@
"""
.. module:: lesscpy.lessc.utility
:synopsis: various utility functions
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
from __future__ import print_function
import collections
import re
def flatten(lst):
"""Flatten list.
Args:
lst (list): List to flatten
Returns:
Returns:
generator
"""
for elm in lst:
@@ -23,7 +27,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 +37,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 +71,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 +96,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 +109,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 +122,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 +136,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 +148,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 +157,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 +176,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 +185,8 @@ def is_color(value):
except ValueError:
pass
return False
def is_variable(value):
""" Check if string is LESS variable
args:
@@ -179,13 +194,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 +216,7 @@ def is_int(value):
pass
return False
def is_float(value):
""" Is value float
args:
@@ -215,6 +232,7 @@ def is_float(value):
pass
return False
def split_unit(value):
""" Split a number from its unit
1px -> (q, 'px')
@@ -224,7 +242,4 @@ def split_unit(value):
tuple
"""
r = re.search('^(\-?[\d\.]+)(.*)$', str(value))
return r.groups() if r else ('','')
return r.groups() if r else ('', '')

View File

@@ -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'
}
'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'
}

View File

@@ -1,6 +1,6 @@
"""
CSS syntax names.
Copyright (c)
See LICENSE for details.
<jtm@robot.is>
@@ -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',

View File

@@ -1,6 +1,6 @@
"""
HTML DOM names
Copyright (c)
See LICENSE for details.
<jtm@robot.is>
@@ -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)
html.extend(html5)

View File

@@ -2,23 +2,23 @@
"""
.. module:: lesscpy.plib
:synopsis: Parse Nodes for Lesscpy
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
__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
from .variable import Variable

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.plib.block
:synopsis: Block parse node.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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)

View File

@@ -2,12 +2,13 @@
"""
.. module:: lesscpy.plib.call
:synopsis: Call parse node
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
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:

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.plib.deferred
:synopsis: Deferred mixin call.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.plib.expression
:synopsis: Expression node.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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:

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.plib.identifier
:synopsis: Identifier node.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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(' ', ' ')

View File

@@ -2,22 +2,26 @@
"""
.. module:: lesscpy.plib.mixin
:synopsis: Mixin node.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
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
@@ -52,22 +56,24 @@ class Mixin(Node):
raises:
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
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:
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

View File

@@ -2,14 +2,16 @@
"""
.. module:: lesscpy.plib.node
:synopsis: Base Node
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
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):

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.plib.property
:synopsis: Property node.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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:

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.plib.statement
:synopsis: Statement node.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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']

View File

@@ -2,7 +2,7 @@
"""
.. module:: lesscpy.plib.string
:synopsis: Less interpolated string node.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
@@ -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("\"'")

View File

@@ -2,14 +2,16 @@
"""
.. module:: lesscpy.plib.variable
:synopsis: Variable declaration
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
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 ''

View File

@@ -1,20 +1,23 @@
from __future__ import print_function
# -*- coding: utf8 -*-
"""
.. module:: lesscpy.scripts.compiler
CSS/LESSCSS run script
http://lesscss.org/#docs
Copyright (c)
See LICENSE for details
.. moduleauthor:: Johann T. Mariusson <jtm@robot.is>
"""
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
@@ -22,6 +25,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 +48,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 +66,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 +161,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 +179,3 @@ def run():
print(out)
except (KeyboardInterrupt, SystemExit, IOError):
sys.exit('\nAborting...')

View File

@@ -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')

View File

@@ -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)
path = os.path.dirname(path)

View File

@@ -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()
unittest.main()

View File

@@ -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()
unittest.main()

View File

@@ -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()

View File

@@ -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()
if __name__ == "__main__":
unittest.main()

View File

@@ -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()

View File

@@ -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()
unittest.main()

View File

@@ -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()
unittest.main()

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
ply

View File

@@ -25,5 +25,5 @@ setup(
'lesscpy/test/less/*.less',
'lesscpy/test/less/issues/*.less',]},
license=open('LICENSE').read(),
long_description=open('README').read(),
)
long_description=open('README.rst').read(),
)

10
tox.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py26,py27,py33,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