Documentation

This commit is contained in:
jtm 2012-03-30 16:12:24 +00:00
parent b050925ba0
commit ba4fe2ea62
6 changed files with 356 additions and 197 deletions

@ -0,0 +1,4 @@
"""
Main lesscss parse library. Contains lexer and parser, along with
utility classes
"""

@ -1,9 +1,11 @@
# -*- coding: utf8 -*-
"""
LESSCPY Color functions
.. module:: lesscpy.lessc.color
:synopsis: Lesscpy Color functions
Copyright (c)
See LICENSE for details.
<jtm@robot.is>
.. moduleauthor:: Jóhann T. Maríusson <jtm@robot.is>
"""
import colorsys
from . import utility
@ -11,8 +13,10 @@ from . import utility
class Color():
def process(self, expression):
""" Process color expression
@param tuple: color expression
@return: string
args:
expression (tuple): color expression
returns:
str
"""
a, o, b = expression
c1 = self._hextorgb(a)
@ -25,23 +29,29 @@ class Color():
r.append("%02x" % v)
return ''.join(r)
def operate(self, a, b, o):
def operate(self, left, right, operation):
""" Do operation on colors
@param string: color
@param string: color
@param string: operator
args:
left (str): left side
right (str): right side
operation (str): Operation
returns:
str
"""
operation = {
'+': '__add__',
'-': '__sub__',
'*': '__mul__',
'/': '__truediv__'
}.get(o)
v = getattr(a, operation)(b)
return v
}.get(operation)
return getattr(left, operation)(right)
def rgb(self, *args):
"""
""" Translate rgb(...) to color string
raises:
ValueError
returns:
str
"""
if len(args) == 4:
return self.rgba(*args)
@ -57,7 +67,11 @@ class Color():
raise ValueError('Illegal color values')
def rgba(self, *args):
"""
""" Translate rgba(...) to color string
raises:
ValueError
returns:
str
"""
if len(args) == 4:
try:
@ -71,7 +85,11 @@ class Color():
raise ValueError('Illegal color values')
def hsl(self, *args):
"""
""" Translate hsl(...) to color string
raises:
ValueError
returns:
str
"""
if len(args) == 4:
return self.hsla(*args)
@ -85,7 +103,11 @@ class Color():
raise ValueError('Illegal color values')
def hsla(self, *args):
"""
""" Translate hsla(...) to color string
raises:
ValueError
returns:
str
"""
if len(args) == 4:
h, s, l, a = args
@ -97,28 +119,46 @@ class Color():
return "rgba(%s,%s,%s,%s)" % tuple(color)
raise ValueError('Illegal color values')
def hue(self, *args):
def hue(self, color, *args):
""" Return the hue value of a color
args:
color (str): color
raises:
ValueError
returns:
float
"""
"""
if args:
h, l, s = self._hextohls(args[0])
return round(h * 360, 3)
if color:
h, l, s = self._hextohls(color)
return round(h * 360.0, 3)
raise ValueError('Illegal color values')
def saturation(self, *args):
def saturation(self, color, *args):
""" Return the saturation value of a color
args:
color (str): color
raises:
ValueError
returns:
float
"""
"""
if args:
h, l, s = self._hextohls(args[0])
return s * 100
if color:
h, l, s = self._hextohls(color)
return s * 100.0
raise ValueError('Illegal color values')
def lightness(self, *args):
def lightness(self, color, *args):
""" Return the lightness value of a color
args:
color (str): color
raises:
ValueError
returns:
float
"""
"""
if args:
h, l, s = self._hextohls(args[0])
return l * 100
if color:
h, l, s = self._hextohls(color)
return l * 100.0
raise ValueError('Illegal color values')
def opacity(self, *args):
@ -126,74 +166,97 @@ class Color():
"""
pass
def lighten(self, *args):
def lighten(self, color, diff, *args):
""" Lighten a color
args:
color (str): color
diff (str): percentage
returns:
str
"""
"""
if len(args) == 2:
color, diff = args
if color and diff:
return self._ophsl(color, diff, 1, '__add__')
raise ValueError('Illegal color values')
def darken(self, *args):
def darken(self, color, diff, *args):
""" Darken a color
args:
color (str): color
diff (str): percentage
returns:
str
"""
"""
if len(args) == 2:
color, diff = args
if color and diff:
return self._ophsl(color, diff, 1, '__sub__')
raise ValueError('Illegal color values')
def saturate(self, *args):
def saturate(self, color, diff, *args):
""" Saturate a color
args:
color (str): color
diff (str): percentage
returns:
str
"""
"""
if len(args) == 2:
color, diff = args
if color and diff:
return self._ophsl(color, diff, 2, '__add__')
raise ValueError('Illegal color values')
def desaturate(self, *args):
def desaturate(self, color, diff, *args):
""" Desaturate a color
args:
color (str): color
diff (str): percentage
returns:
str
"""
"""
if len(args) == 2:
color, diff = args
if color and diff:
return self._ophsl(color, diff, 2, '__sub__')
raise ValueError('Illegal color values')
def clamp(self, v):
"""
"""
return min(1, max(0, v))
def _clamp(self, value):
# Clamp value
return min(1, max(0, value))
def grayscale(self, *args):
def grayscale(self, color, *args):
""" Simply 100% desaturate.
args:
color (str): color
returns:
str
"""
Simply 100% desaturate.
"""
if len(args) == 2:
return self.desaturate(args[0], 100)
if color:
return self.desaturate(color, 100.0)
raise ValueError('Illegal color values')
def greyscale(self, *args):
def greyscale(self, color, *args):
"""Wrapper for grayscale, other spelling
"""
Wrapper for grayscale
"""
return self.grayscale(*args)
return self.grayscale(color, *args)
def spin(self, *args):
def spin(self, color, degree, *args):
""" Spin color by degree. (Increase / decrease hue)
args:
color (str): color
degree (str): percentage
raises:
ValueError
returns:
str
"""
"""
if len(args) == 2:
color, deg = args
if type(deg) == str: deg = int(deg.strip('%'))
if color and degree:
if type(degree) == str:
degree = int(degree.strip('%'))
h, l, s = self._hextohls(color)
h = ((h * 360) + deg) % 360
h = 360 + h if h < 0 else h
rgb = colorsys.hls_to_rgb(h / 360, l, s)
h = ((h * 360.0) + degree) % 360.0
h = 360.0 + h if h < 0 else h
rgb = colorsys.hls_to_rgb(h / 360.0, l, s)
color = (round(c * 255) for c in rgb)
return self._rgbatohex(color)
raise ValueError('Illegal color values')
def mix(self, *args):
"""
This algorithm factors in both the user-provided weight
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.
@ -216,19 +279,24 @@ class Color():
Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein
http://sass-lang.com
args:
color1 (str): first color
color2 (str): second color
weight (int/str): weight
raises:
ValueError
returns:
str
"""
if len(args) >= 2:
try:
c1, c2, w = args
except ValueError:
c1, c2 = args
w = 50
if type(w) == str: w = int(w.strip('%'))
w = ((w / 100.0) * 2) - 1
rgb1 = self._hextorgb(c1)
rgb2 = self._hextorgb(c2)
a = 0
w1 = (((w if w * a == -1 else w + a) / (1 + w * a)) + 1)
if color1 and color2:
if type(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
else weight + alpha) / (1 + weight * alpha)) + 1)
w1 = w1 / 2.0
w2 = 1 - w1
rgb = [
@ -240,10 +308,14 @@ class Color():
raise ValueError('Illegal color values')
def fmt(self, color):
"""
Format CSS Hex color code.
""" Format CSS Hex color code.
uppercase becomes lowercase, 3 digit codes expand to 6 digit.
@param string: color
args:
color (str): color
raises:
ValueError
returns:
str
"""
if utility.is_color(color):
color = color.lower().strip('#')
@ -253,8 +325,6 @@ class Color():
raise ValueError('Cannot format non-color')
def _rgbatohex(self, rgba):
"""
"""
return '#%s' % ''.join(["%02x" % v for v in
[0xff
if h > 0xff else
@ -262,8 +332,6 @@ class Color():
for h in rgba]
])
def _hextorgb(self, hex):
"""
"""
hex = hex.strip()
if hex[0] == '#':
hex = hex.strip('#').strip(';')
@ -275,17 +343,13 @@ class Color():
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('%'))
hls = list(self._hextohls(color))
hls[idx] = self.clamp(getattr(hls[idx], op)(diff / 100))
hls[idx] = self._clamp(getattr(hls[idx], op)(diff / 100))
rgb = colorsys.hls_to_rgb(*hls)
color = (round(c * 255) for c in rgb)
return self._rgbatohex(color)

@ -1,6 +1,6 @@
# -*- coding: utf8 -*-
"""
.. module:: parser
.. module:: lesscpy.lessc.parser
:synopsis: Lesscss parser.
http://www.dabeaz.com/ply/ply.html
@ -33,7 +33,8 @@ class LessParser(object):
scope=None,
outputdir='/tmp',
importlvl=0,
verbose=False):
verbose=False
):
""" Parser object
Kwargs:

@ -1,16 +1,29 @@
"""
.. module:: lesscpy.lessc.scope
:synopsis: Scope class.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Jóhann T. Maríusson <jtm@robot.is>
"""
from . import utility
class Scope(list):
""" Scope class. A stack implementation.
"""
def __init__(self, init=False):
"""Scope
Args:
init (bool): Initiate scope
"""
super().__init__()
self._mixins = {}
if init: self.push()
self.in_mixin = False
def push(self):
"""
"""Push level on scope
"""
self.append({
'__variables__' : {},
@ -29,7 +42,9 @@ class Scope(list):
@property
def scopename(self):
"""
"""Current scope name as list
Returns:
list
"""
return [r['__current__']
for r in self
@ -37,13 +52,17 @@ class Scope(list):
def add_block(self, block):
"""
"""Add block element to scope
Args:
block (Block): Block object
"""
self[-1]['__blocks__'].append(block)
self[-1]['__names__'].append(block.raw())
def add_mixin(self, mixin):
"""
"""Add mixin to scope
Args:
mixin (Mixin): Mixin object
"""
raw = mixin.name.raw()
if raw in self._mixins:
@ -52,13 +71,18 @@ class Scope(list):
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
"""Search for variable by name. Searches scope top down
Args:
name (string): Search term
Returns:
Variable object OR False
"""
if type(name) is tuple:
name = name[0]
@ -70,17 +94,19 @@ class Scope(list):
return False
def mixins(self, name):
"""
Search mixins for name.
Allow '>' to be ignored.
""" Search mixins for name.
Allow '>' to be ignored. '.a .b()' == '.a > .b()'
Args:
name (string): Search term
Returns:
Mixin object list OR False
"""
m = self._smixins(name)
if m: return m
return self._smixins(name.replace('?>?', ' '))
def _smixins(self, name):
"""
Inner wrapper to search for mixins by name.
"""Inner wrapper to search for mixins by name.
"""
return (self._mixins[name]
if name in self._mixins
@ -89,15 +115,18 @@ class Scope(list):
def blocks(self, name):
"""
Search for defined blocks recursively.
Allow '>' to be ignored.
Allow '>' to be ignored. '.a .b' == '.a > .b'
Args:
name (string): Search term
Returns:
Block object OR False
"""
b = self._blocks(name)
if b: return b
return self._blocks(name.replace('?>?', ' '))
def _blocks(self, name):
"""
Inner wrapper to search for blocks by name.
"""Inner wrapper to search for blocks by name.
"""
i = len(self)
while i >= 0:
@ -116,7 +145,11 @@ class Scope(list):
return False
def update(self, scope, at=0):
"""
"""Update scope. Add another scope to this one.
Args:
scope (Scope): Scope object
Kwargs:
at (int): Level to update
"""
if hasattr(scope, '_mixins') and not at:
self._mixins.update(scope._mixins)
@ -125,13 +158,19 @@ class Scope(list):
self[at]['__names__'].extend(scope[at]['__names__'])
def swap(self, name):
"""
""" Swap variable name for variable value
Args:
name (str): Variable name
Returns:
Variable value (Mixed)
"""
if name.startswith('@@'):
var = self.variables(name[1:])
if var is False: raise SyntaxError('Unknown variable %s' % name)
if var is False:
raise SyntaxError('Unknown variable %s' % name)
name = '@' + utility.destring(var.value[0])
var = self.variables(name)
if var is False: raise SyntaxError('Unknown variable %s' % name)
if var is False:
raise SyntaxError('Unknown variable %s' % name)
return var.value

@ -1,41 +1,52 @@
# -*- coding: utf8 -*-
"""
Utility functions
.. module:: lesscpy.lessc.utility
:synopsis: various utility functions
Copyright (c)
See LICENSE for details.
<jtm@robot.is>
.. moduleauthor:: Jóhann T. Maríusson <jtm@robot.is>
"""
import collections
import re
def flatten(ll):
def flatten(lst):
"""Flatten list.
Args:
lst (list): List to flatten
Returns:
generator
"""
Flatten list.
@param ll: list
@return: generator
"""
for el in ll:
if isinstance(el, collections.Iterable) and not isinstance(el, str):
for sub in flatten(el):
for elm in lst:
if isinstance(elm, collections.Iterable) and not isinstance(elm, str):
for sub in flatten(elm):
yield sub
else:
yield el
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)
Args:
lst (list): List to process
Returns:
list
"""
if not lst: return
l = len(lst)
for i in range(l-1):
if not lst:
return
length = len(lst)
for i in range(length-1):
yield lst[i], lst[i+1]
yield lst[-1], None
def rename(ll, scope):
def rename(blocks, scope):
""" Rename all sub-blocks moved under another
block. (mixins)
Args:
lst (list): block list
scope (object): Scope object
"""
for p in ll:
for p in blocks:
if hasattr(p, 'inner'):
p.name.parse(scope)
if p.inner:
@ -45,7 +56,11 @@ def rename(ll, scope):
scope.pop()
def blocksearch(block, name):
""" Recursive search for name in block
""" Recursive search for name in block (inner blocks)
Args:
name (str): search term
Returns:
Block OR False
"""
for b in block.inner:
b = (b if b.raw() == name
@ -53,43 +68,59 @@ def blocksearch(block, name):
if b: return b
return False
def reverse_guard(ll):
"""
def reverse_guard(lst):
""" Reverse guard expression. not
(@a > 5) -> (@a <= 5)
Args:
lst (list): Expression
returns:
list
"""
rev = {
'<': '>',
'>': '<',
'<': '>=',
'>': '<=',
'=': '!=',
'!=': '=',
'>=': '<=',
'<=': '>='
'>=': '<',
'<=': '>'
}
return [rev[l] if l in rev else l for l in ll]
return [rev[l] if l in rev else l for l in lst]
def debug_print(ll, lvl=0):
"""
def debug_print(lst, lvl=0):
""" Print scope tree
args:
lst (list): parse result
lvl (int): current nesting level
"""
pad = ''.join(['\t.'] * lvl)
t = type(ll)
t = type(lst)
if t is list:
for p in ll:
for p in lst:
debug_print(p, lvl)
elif hasattr(ll, 'tokens'):
elif hasattr(lst, 'tokens'):
print(pad, t)
debug_print(list(flatten(ll.tokens)), lvl+1)
debug_print(list(flatten(lst.tokens)), lvl+1)
def destring(v):
""" Strip quotes
@param string: value
@return: string
def destring(value):
""" Strip quotes from string
args:
value (str)
returns:
str
"""
return v.strip('"\'')
return value.strip('"\'')
def analyze_number(var, err=''):
""" Analyse number for type and split from unit
@param str: value
@raises: SyntaxError
@return: tuple (number, unit)
1px -> (q, 'px')
args:
var (str): number string
kwargs:
err (str): Error message
raises:
SyntaxError
returns:
tuple
"""
n, u = split_unit(var)
if type(var) is not str:
@ -104,74 +135,94 @@ def analyze_number(var, err=''):
raise SyntaxError('%s ´%s´' % (err, var))
return (n, u)
def with_unit(n, u=None):
def with_unit(number, unit=None):
""" Return number with unit
@param int/float: value
@param str: unit
@return: mixed
args:
number (mixed): Number
unit (str): Unit
returns:
str
"""
if type(n) is tuple:
n, u = n
if n == 0: return 0
if u:
n = str(n)
if n.startswith('.'):
n = '0' + n
return "%s%s" % (n, u)
return n
if type(number) is tuple:
number, unit = number
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)
def is_color(v):
""" Is CSS color
@param mixed: value
@return: bool
def is_color(value):
""" Is string CSS color
args:
value (str): string
returns:
bool
"""
if not v or type(v) is not str:
if not value or type(value) is not str:
return False
if v[0] == '#' and len(v) in [4, 5, 7, 9]:
if value[0] == '#' and len(value) in [4, 5, 7, 9]:
try:
int(v[1:], 16)
int(value[1:], 16)
return True
except Exception:
except ValueError:
pass
return False
def is_variable(v):
def is_variable(value):
""" Check if string is LESS variable
@param string: check
@return: bool
args:
value (str): string
returns:
bool
"""
if type(v) is str:
return (v.startswith('@') or v.startswith('-@'))
elif type(v) is tuple:
v = ''.join(v)
return (v.startswith('@') or v.startswith('-@'))
if type(value) is str:
return (value.startswith('@') or value.startswith('-@'))
elif type(value) is tuple:
value = ''.join(value)
return (value.startswith('@') or value.startswith('-@'))
return False
def is_int(v):
def is_int(value):
""" Is value integer
args:
value (str): string
returns:
bool
"""
try:
int(str(v))
int(str(value))
return True
except (ValueError, TypeError):
pass
return False
def is_float(v):
def is_float(value):
""" Is value float
args:
value (str): string
returns:
bool
"""
if not is_int(v):
if not is_int(value):
try:
float(str(v))
float(str(value))
return True
except (ValueError, TypeError):
pass
return False
def split_unit(v):
def split_unit(value):
""" Split a number from its unit
1px -> (q, 'px')
Args:
value (str): input
returns:
tuple
"""
r = re.search('^(\-?[\d\.]+)(.*)$', str(v))
r = re.search('^(\-?[\d\.]+)(.*)$', str(value))
return r.groups() if r else ('','')

@ -86,9 +86,9 @@ class TestUtility(unittest.TestCase):
self.assertEqual('1.0px', test(1.0, 'px'))
self.assertEqual('0.0px', test('.0', 'px'))
self.assertEqual('0.6px', test(.6, 'px'))
self.assertEqual(1, test(1))
self.assertEqual(1, test(1, None))
self.assertEqual(1, test(1,))
self.assertEqual('1', test(1))
self.assertEqual('1', test(1, None))
self.assertEqual('1', test(1,))