Commits

Victor Kotseruba committed bb56c70

sass parser with ast builder

  • Participants
  • Parent commits 1ca57c7

Comments (0)

Files changed (13)

+attempt to make python version of SASS (http://sass-lang.com/)
+
+this version should work only with python 2.6,
+because of ugly _ast module in 2.5

color.py

-# -*- coding: utf-8 -*-
-
-import re
-
-class Color(object):
-
-    full_color_re = re.compile(r'^#[\da-f]{6}$', re.I)
-    compact_color_re = re.compile(r'^#[\da-f]{3}$', re.I)
-    rgb_color_re = re.compile(r'^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$', re.I)
-    
-    def __init__(self, color):
-        self.color = color
-        self._full_color = None
-        self._compact_color = None
-        self._rgb_color = None
-        if self.full_color_re.match(color):
-            self._full_color = color.upper()
-        elif self.compact_color_re.match(color):
-            self._compact_color = color.lower()
-        elif self.rgb_color_re.match(color):
-            _parts = []
-            for part in map(int, self.rgb_color_re.findall(self.color)[0]):
-                part = hex(part)[2:].upper()
-                if len(part) == 1:
-                    part = '0' + part
-                _parts.append(part)
-            self._full_color = '#' + ''.join(_parts)
-            self._rgb_color = re.sub('\s+', '', color).replace(',', ', ')
-            self.color = self._full_color
-        else:
-            raise self.UnknownType
-
-    @property
-    def rgb_color(self):
-        if self._rgb_color:
-            return self._rgb_color
-        color = self.full_color
-        return 'rgb(%i, %i, %i)' % (int(color[1:3], 16),
-                                    int(color[3:5], 16),
-                                    int(color[5:7], 16))
-    
-    @property
-    def compact_color(self):
-        if self._compact_color:
-            return self._compact_color
-        color = self.color.lower()
-        if color[1] == color[2] and color[3] == color[4] and color[5] == color[6]:
-            return '#%s%s%s' % (color[1], color[3], color[5])
-        return color
-    
-    @property
-    def full_color(self):
-        if self._full_color:
-            return self._full_color
-        color = self.color.upper()
-        return '#%s%s%s' % (color[1]*2, color[2]*2, color[3]*2)
-    
-    class UnknownType(Exception): pass

context.py

-# -*- coding: utf-8 -*-
-
-
-class Context(object):
-    
-    def __init__(self, *args):
-        self.dicts = args or [{}]
-    
-    def __getitem__(self, name):
-        for d in self.dicts:
-            if name in d:
-                return d[name]
-        raise KeyError(name)
-    
-    def __setitem__(self, name, value):
-        self.dicts[0][name] = value
-    
-    def __contains__(self, name):
-        for d in self.dicts:
-            if name in d:
-                return True
-        return False
-    
-    def extend(self, d):
-        return self.__class__(d, *self.dicts)    
-    
-    def copy(self):
-        return self.__cls__([d.copy() for d in self.dicts])

expression.py

-# -*- coding: utf-8 -*-
-
-import re
-from number import Number
-
-
-class EvalException(Exception): pass
-
-
-class EvaluatedExpression(str):
-            
-    def evaluate(self, context):
-        return str(self)
-
-
-class Expression(object):
-
-    number_re = re.compile(r'(?<!Number\()(\d+(?:\.\d+)?(?:em|px|%|pt))')
-    variable_re = re.compile(r'(?P<name>!\w+)')
-    
-    compact_res = (re.compile(r'\s*(?=\+|-|/|\*|\))'),
-                   re.compile(r'(?<=\+|-|/|\*|\()\s*'))
-    
-    def __init__(self, expr):
-        self.expr = expr        
-    
-    
-    def split(self):
-        expr = self.expr
-        for re in self.compact_res:
-            expr = re.sub('', expr)
-        return expr.split(' ')
-        
-    
-    def evaluate(self, context):
-        #print 'original expr %r' % self.expr
-        exprs = self.split()
-        if len(exprs) > 1:
-            return ' '.join(Expression(e).evaluate(context) for e in exprs)
-        expr = exprs[0]
-        #print 'compact expr %r' % expr
-        for op in '+-/*!':
-            if op in expr:
-                break
-        else:
-            #print 'should not eval'
-            #print 
-            return expr
-        def replace_variable(name):
-            name = name.groupdict()['name']
-            name = name.strip('!')
-            if name in context:
-                value = context[name]
-                #print 'value of !%s is %r' % (name, value)
-                if type(value) in (int, float):
-                    return repr(value)
-                return str(context[name])
-            raise Exception('variable "%s" not found' % name)
-        expr = self.variable_re.sub(replace_variable, expr)
-        expr = self.number_re.sub(r"Number('\1')", expr)
-        #print 'expr to eval %r' % expr
-        try:
-            result = str(eval(expr))
-        except:
-            raise EvalException(expr)
-        #print 'result %r' % result
-        #print
-        return result
-    
-    def __repr__(self):
-        return self.expr

grammar.py

-# -*- coding: utf-8 -*-
-
-""" attempt to use pyparsing """
-
-from pyparsing import *
-import pprint
-
-__all__ = ('parse_sass', )
-
-ParserElement.setDefaultWhitespaceChars(' ')
-
-def get_sass_parser():
-    
-    def parser():
-    
-        def strip_name(s, loc, toks):
-            return toks[0].strip('!:= ')
-    
-        stack = [1]
-    
-        # common
-        NAME = Word(alphas + '-')
-        ENDL = LineEnd().suppress()
-        LPAR = Suppress('(')
-        RPAR = Suppress(')')
-    
-        # expressions
-        NUMBER = Regex(r'-?\d+(?:\.\d+)?(?:px|em|pt|%)?').setResultsName('number')
-        OPERATOR = (Literal('+') | Literal('-') | Literal('/') | Literal('*') | Literal('==')).setResultsName('operator')
-        STRING = (QuotedString(quoteChar='"') | QuotedString(quoteChar="'")).setResultsName('string')
-        VARIABLE = Regex(r'![\w-]+').addParseAction(strip_name).setResultsName('variable')
-        EXPRESSION = Forward()
-        ATOM = (NUMBER | VARIABLE | STRING) | (LPAR + Group(EXPRESSION) + RPAR)
-        EXPRESSION << ATOM + ZeroOrMore(OPERATOR + ATOM)
-    
-        # properties
-        PROP_KEY= Regex(r'(?::[\w-]+|[\w-]+:)').addParseAction(strip_name)
-        PROP = Group(PROP_KEY + White(' ').suppress() + Group(OneOrMore(Word(printables)))) + ENDL
-        PROP_KEY_EXPR = Regex(r':?[\w-]+').addParseAction(strip_name)
-        PROP_EXPR = Group(PROP_KEY_EXPR + White(' ').suppress() + Suppress('=') + OneOrMore(Group(EXPRESSION))) + ENDL
-        PROP_PREFIX = (Suppress(':') + Word(alphas + '-')) | (Word(alphas + '-') + Suppress(':')) + ENDL
-        PREFIX_NODE = Forward()
-        prefix_node_suit = PROP_EXPR | PROP | LineEnd().suppress()
-        PREFIX_NODE << Group(PROP_PREFIX + indentedBlock(prefix_node_suit, stack)).setResultsName('prefix-node')
-    
-        SELECTOR_NODE = Forward()
-        CONTROL = Forward()
-    
-        # mixins
-        ARG = VARIABLE
-        ARG_WITH_DEFAULTS = Group(VARIABLE + Suppress('=') + EXPRESSION)
-        ARGS = ZeroOrMore(ARG + Suppress(',')) + ARG
-        MIXIN_USE = Suppress('+') + NAME + Optional(LPAR + Optional(Group(EXPRESSION) + ZeroOrMore(Suppress(',') + Group(EXPRESSION))) + RPAR) + ENDL
-        MIXIN_DEF = Suppress('=') + NAME + Group(Optional(LPAR + Optional(ARGS) + RPAR)) + ENDL
-        MIXIN_NODE = Forward()
-    
-        # selectors
-        SELECTOR = Regex(r'(?!(?::|[\w-]:[^\w])).+') + ENDL
-        selector_node_suit = LineEnd().suppress() | PROP_EXPR | PROP | PREFIX_NODE | MIXIN_USE | CONTROL | SELECTOR_NODE
-    
-        # control directives
-        FOR = Keyword('@for') + VARIABLE + Keyword('from') + EXPRESSION + (Keyword('to') | Keyword('through')) + EXPRESSION + ENDL
-        FOR_NODE = Group(FOR + indentedBlock(selector_node_suit, stack)).setResultsName('for-node')
-        IF = Keyword('@if') + EXPRESSION + ENDL
-        ELSE = Keyword('@else') + ENDL
-        ELSE_IF = Keyword('@else') + Keyword('if') + EXPRESSION + ENDL
-        ELSE_NODE = Group(ELSE + indentedBlock(selector_node_suit, stack)).setResultsName('else-node')
-        ELSE_IF_NODE = Group(ELSE_IF + indentedBlock(selector_node_suit, stack)).setResultsName('else-if-node')
-        IF_NODE = Group(IF + indentedBlock(selector_node_suit, stack)  + ZeroOrMore(ELSE_IF_NODE) + Optional(ELSE_NODE))
-        CONTROL << (FOR_NODE | IF_NODE)
-    
-        # nodes
-        SELECTOR_NODE << Group(SELECTOR + indentedBlock(selector_node_suit, stack)).setResultsName('selector-node')
-        MIXIN_NODE << Group(MIXIN_DEF + indentedBlock(selector_node_suit, stack)).setResultsName('mixin-node')
-        suite = OneOrMore(ENDL | MIXIN_NODE | CONTROL | SELECTOR_NODE)
-        
-        return suite
-    
-    def parse(string):
-        return parser().parseString(string)
-            
-    return parse
-
-
-parse_sass = get_sass_parser()
-
-
-if __name__ == '__main__':
-
-    tree = parse_sass("""
-
-body
-    margin = 1px 2px 3px + 44px
-
-@for !i from 1 to 2
-    .test-#{!i}
-        color: red
-
-@if !i + 1
-    .test
-        color: red
-@else if !a
-    +empty(1, 2)
-@else
-    @for !k from 1 to 10
-        .test-#{!k}
-            :display none
-
-=empty
-    :display block
-
-=test(!a, !k, !t, !g)
-    pass: pass
-
-.wrap
-    :width = 1px + 2px + (2 * 3px + (!width)) + !k
-    
-    :border
-        color: red
-        :style solid
-    
-    height: 2px
-    :font-weight bold
-    
-    +test((((1)+1)+1)+1)
-    
-    .page.active
-        +test(1, 2, 3, 4)
-        +empty(45px + "123123")
-
-""")
-
-    pprint.PrettyPrinter(indent=2).pprint(tree.asList())

number.py

-# -*- coding: utf-8 -*-
-
-import re, sys
-
-class MathException(Exception): pass
-
-
-class Number(object):
-    
-    number_re = re.compile(r'^(?P<value>\d+(?:\.\d+)?)(?P<unit>em|px|%|pt)$')
-    
-    def __init__(self, expr):
-        data = self.number_re.match(expr).groupdict()
-        if '.' in data['value']:
-            self.number = float(data['value'])
-        else:
-            self.number = int(data['value'])
-        self.unit = data['unit']
-
-    def __str__(self):
-        return '%s%s' % (self.number, self.unit)
-
-    def __add__(self, value):
-        if type(value) is str:
-            return '%s%s' % (self, value)
-        if type(value) in (int, float):
-            self.number += value
-            return self
-        if type(value) is Number and value.unit == self.unit:
-            self.number += value.number
-            return self
-        raise MathException("%s + %s" % (self, value))
-    
-    def __sub__(self, value):
-        if type(value) in (int, float):
-            self.number -= value
-            return self
-        if type(value) is Number and value.unit == self.unit:
-            self.number -= value.number
-            return self
-        raise MathException("%s - %s" % (self, value))
-    
-    def __mul__(self, value):
-        if type(value) in (int, float):
-            self.number *= value
-            return self
-        raise MathException("%s * %s" % (self, value))
-    
-    def __div__(self, value):
-        if type(value) in (int, float):
-            if sys.version.startswith('3') and type(self.number) is int:
-                self.number //= value
-            else:
-                self.number /= value
-            return self
-        raise MathException("%s / %s" % (self, value))
-
-    __truediv__ = __div__
-
-    def __repr__(self):
-        return "Number('%s%s')" % (self.number, self.unit)
 
 =blueprint-reset
   +blueprint-reset-box-model
-  :font
-    :weight inherit
-    :style inherit
-    :size 100%
-    :family inherit
+  :font-weight inherit
+  :font-style inherit
+  :size 100%
+  :font-family inherit
   :vertical-align baseline
 
 =blueprint-reset-quotation
   +blueprint-nested-reset
 
 
-+blueprint-global-reset
++blueprint-global-reset

sass.py

-# -*- coding: utf-8 -*-
-
-import re
-from color import Color
-from expression import Expression, EvaluatedExpression
-from context import Context
-
-
-class Property(object):
-    
-    def __init__(self, key, value):
-        self.key = key
-        self.value = value
-
-
-class Selector(object):
-    
-    def __init__(self, sel):
-        if type(sel) is str:
-            self.sels = map(str.strip, sel.split(','))
-        elif type(sel) in (list, tuple):
-            self.sels = sel
-    
-    def __add__(self, selector):
-        _sels = []
-        for sel in self.sels:
-            for add_sel in selector.sels:
-                if add_sel.startswith('>'):
-                    _sels.append(sel + add_sel)
-                elif add_sel.startswith('&'):
-                    _sels.append(sel + add_sel.lstrip('&'))
-                else:
-                    _sels.append(sel + ' ' + add_sel)
-        return Selector(_sels)
-
-
-class SassNode(object):
-    
-    def __init__(self, selector=None, properties=None, nodes=None, mixins=None):
-        self.selector = selector
-        self.properties = properties or []
-        self.nodes = nodes or []
-        self.mixins = mixins or []
-
-
-class SassMixin(object):
-    
-    def __init__(self, args=None, properties=None, nodes=None, mixins=None):
-        self.args = args or []
-        self.properties = properties or []
-        self.nodes = nodes or []
-        self.mixins = mixins or []
-
-
-class MixinCall(object):
-    
-    def __init__(self, mixin, args):
-        self.mixin = mixin
-        self.args = args
-
-
-class SassParser(object):
-    
-    mixin_def_re = re.compile(r'^=(?P<name>[\.\w-]+)(?:\((?P<args>[^\)]+)\))?')
-    mixin_use_re = re.compile(r'^\s*\+(?P<name>[\.\w-]+)(?:\((?P<args>[^\)]+)\))?')
-    var_def_re = re.compile(r'^!(?P<name>[\.\w-]+)\s*(?P<optional>(?:\|\|)?)=\s*(?P<value>.+?)\s*$')
-    property_re = re.compile(r'^\s+(?P<key>(?::?[\w-]+(?::|\s+=)|:[\w-]+))\s+(?P<value>.+?)\s*$')
-    prop_prefix_re = re.compile(r'^\s+(?P<prefix>(?::[\w-]+|[\w-]+:))\s*$')
-    tabstop = 2
-    
-    def __init__(self, mixins={}, variables={}):
-        self.nodes = []
-        self.mixins = {}
-        self.variables = {}
-    
-    def parse(self, sass_code):
-        mixins = {}
-        context = Context({}, self.variables)
-        parents = [SassNode('')]
-        node = None
-        prop_prefix = None
-        prop_prefix_level = -1
-        while True:
-            
-            line = sass_code.readline()
-            if not line:
-                break
-            
-            if '\t' in line:
-                raise Exception('tabs are not supported, use spaces for indentation')
-            
-            stripped = line.strip()
-            if not stripped or stripped.startswith('//') or stripped.startswith('/*'):
-                continue
-            
-            var_def_match = self.var_def_re.match(line)
-            if var_def_match:
-                data = var_def_match.groupdict()
-                name, value = data['name'], data['value']
-                if data['optional'] and name in context:
-                    continue
-                context[name] = Expression(value)
-                continue
-            
-            level = (len(line) - len(line.lstrip())) // self.tabstop
-            
-            if level <= prop_prefix_level:
-                prop_prefix_level = -1
-                prop_prefix = None
-            
-            prop_prefix_match = self.prop_prefix_re.match(line)
-            if prop_prefix_match:
-                prop_prefix_level = level
-                prop_prefix = prop_prefix_match.groupdict()['prefix'].strip(':')
-                continue
-            
-            property_match = self.property_re.match(line)
-            if property_match:
-                data = property_match.groupdict()
-                value = data['value'].rstrip(';')
-                key = data['key'].strip(': =')
-                if prop_prefix:
-                    key = '%s-%s' % (prop_prefix, key)
-                    level = prop_prefix_level
-                if data['key'].endswith('='):
-                    value = Expression(value)
-                else:
-                    value = EvaluatedExpression(value)
-                prop = Property(key, value)
-                parents[level].properties.append(prop)
-                continue
-            
-            mixin_use_match = self.mixin_use_re.match(line)
-            if mixin_use_match:
-                data = mixin_use_match.groupdict()
-                name, args = data['name'], data['args']
-                #mixin = mixins.get(name, self.mixins.get(name, None))
-                #assert mixin, 'mixin "%s" not found' % name
-                if args:
-                    args = map(Expression, map(str.strip, args.split(',')))
-                    #call_args = dict(zip(mixin.args, args))
-                #else:
-                #    call_args = {}
-                parents[level].mixins.append(MixinCall(name, args))
-            
-            else:
-                
-                mixin_def_match = self.mixin_def_re.match(line)
-                if mixin_def_match:
-                    data = mixin_def_match.groupdict()
-                    name, args = data['name'], data['args']
-                    if args:
-                        args = [x.strip().lstrip('!') for x in args.split(',')]
-                    node = SassMixin(args=args)
-                    mixins[name] = node
-                
-                else:
-                    sel = line.strip()
-                    while sel.endswith(','):
-                        sel += sass_code.readline().strip()
-                    node = SassNode(Selector(sel))
-                    parents[level].nodes.append(node)
-                
-                if level + 1 >= len(parents):
-                    parents.append(node)
-                else:
-                    parents[level + 1] = node
-                    del parents[level + 2:]
-        
-        assert not parents[0].properties
-        return parents[0].nodes, context, mixins
-
-
-class SassExpander(object):
-    
-    def __init__(self):
-        pass
-    
-    def expand(self, nodes, context, mixins):
-        for node in nodes:
-            yield self.expand_node(node, context, mixins)
-    
-    def expand_node(self, node, context, mixins):
-        props = list(self.evaluate_properties(node.properties, context))
-        nodes = list(self.expand(node.nodes, context, mixins))
-        for call in node.mixins:
-            _nodes, _properties = self.expand_mixin(call, context, mixins)
-            nodes += _nodes
-            props += _properties
-        return node.__class__(node.selector, props, nodes)
-    
-    def expand_mixin(self, call, context, mixins):
-        mixin = mixins[call.mixin]
-        call_args = {}
-        for key, value in zip(mixin.args, call.args):
-            call_args[key] = value.evaluate(context)
-        context = context.extend(call_args)
-        props = list(self.evaluate_properties(mixin.properties, context))
-        nodes = list(self.expand(mixin.nodes, context, mixins))
-        for _call in mixin.mixins:
-            _nodes, _properties = self.expand_mixin(_call, context, mixins)
-            props += _properties
-            nodes += _nodes
-        return nodes, props
-    
-    def evaluate_properties(self, properties, context):
-        for prop in properties:
-            yield Property(prop.key, prop.value.evaluate(context))
-
-
-
-
-class SassConvertor(object):
-
-    styles = {
-        'expanded': {
-            'rule_template': '%s {\n%s\n}',
-            'property_template': '  %s: %s;',
-            'properties_separator': '\n',
-            'rules_separator': '\n\n',
-            'selector_separator': ' ',
-            'selector_linebreak': '\n',
-            'colors': 'expanded',
-            'nesting': False,
-            },
-        'nested': {
-            'rule_template': '%s {\n%s }',
-            'property_template': '  %s: %s;',
-            'properties_separator': '\n',
-            'rules_separator': '\n\n',
-            'selector_separator': ' ',
-            'selector_linebreak': '\n',
-            'colors': 'expanded',
-            'nesting': True,
-            },
-        'nested-compact': {
-            'rule_template': '%s { %s }',
-            'property_template': '%s: %s',
-            'properties_separator': '; ',
-            'rules_separator': '\n',
-            'selector_separator': ' ',
-            'selector_linebreak': '',
-            'colors': 'compact',
-            'nesting': True,
-            },
-        'compact': {
-            'rule_template': '%s{%s}',
-            'property_template': '%s:%s',
-            'properties_separator': ';',
-            'rules_separator': '',
-            'selector_separator': '',
-            'selector_linebreak': '',
-            'colors': 'compact',
-            'nesting': False,
-            },
-        }
-
-
-    def __init__(self, style='style'):
-        style = self.styles[style]
-        self.rule_template = style['rule_template']
-        self.properties_separator = style['properties_separator']
-        self.rules_separator = style['rules_separator']
-        self.selector_separator = style['selector_separator']
-        self.property_template = style['property_template']
-        self.colors = style['colors']
-        self.nesting = style['nesting']
-        self.selector_linebreak = style['selector_linebreak']
-    
-    def convert_node(self, node, selector=None, level=0):
-        level_indent = 0
-        if self.nesting:
-            level_indent = level * 2
-        if selector:
-            sel = selector + node.selector
-        else:
-            sel = node.selector
-        sel_text = ''
-        for _sel in sel.sels:
-            sel_text += ',' + self.selector_separator
-            if len((sel_text + _sel).split('\n')[-1]) + level_indent >= 80:
-                sel_text += self.selector_linebreak
-            sel_text += _sel
-        sel_text = sel_text.strip(',').strip()
-        properties = []
-        for prop in node.properties:
-            value = prop.value
-            if self.colors != 'pass':
-                try:
-                    color = Color(value)
-                    if self.colors == 'compact':
-                        value = color.compact_color
-                    elif self.colors == 'expanded':
-                        value = color.full_color
-                    elif self.colors == 'rgb':
-                        value = color.rgb_color
-                except (Color.UnknownType, TypeError):
-                    pass
-            properties.append(self.property_template % (prop.key, value))
-        if properties:
-            props_text = self.properties_separator.join(properties)
-            css_text = self.rule_template % (sel_text, props_text)
-            if self.nesting:
-                css_text = '\n'.join('  ' * level + line for line in css_text.split('\n'))
-            texts = [css_text]
-            next_level = level + 1
-        else:
-            texts = []
-            next_level = level
-        for childnode in node.nodes:
-            texts.append(self.convert_node(childnode, selector=sel, level=next_level))
-        return self.rules_separator.join(texts)
-    
-    def convert(self, nodes):
-        output = map(self.convert_node, nodes)
-        return self.rules_separator.join(output)
-    
-
-def convert(code, style='expanded'):
-    parser = SassParser()
-    convertor = SassConvertor(style=style)
-    expander = SassExpander()
-    nodes, context, mixins = parser.parse(code)
-    expanded_nodes = expander.expand(nodes, context, mixins)
-    css = convertor.convert(expanded_nodes)
-    return css
-
-
-if __name__ == '__main__':
-    style = 'expanded'
-    import sys
-    if len(sys.argv) > 2:
-        filename = sys.argv[1]
-        style = sys.argv[2]
-        print(convert(open(filename), style=style))
-    else:
-        print('python sass.py <filename> <style>')
+# -*- coding: utf-8 -*-
+
+import parser
+import css
+
+__all__ = ('convert', )
+
+
+def convert(data):
+    pr = parser.Parser()
+    mod = pr.parse(data)
+    ns = {'Sheet': css.Sheet,
+          'Node': css.Node,
+          'Number': css.Number}
+    exec compile(mod, '<sass>', 'exec') in ns
+    return ns['_sheet'].to_css()
+# -*- coding: utf-8 -*-
+
+import cStringIO
+
+
+class Sheet:
+    
+    """
+    represents css stylesheet
+    """
+    
+    def __init__(self):
+        self.nodes = []
+    
+    def to_css(self):
+        out = cStringIO.StringIO()
+        for node in self.nodes:
+            self.write_node(out, node)
+        out.seek(0)
+        return out.read()
+    
+    def write_node(self, out, node):
+        out.write(', '.join(node.selector.sels))
+        out.write(' {\n')
+        for prop in node.props:
+            self.write_prop(out, prop)
+        out.write('}\n\n')
+    
+    def write_prop(self, out, prop):
+        name, values = prop
+        value = ' '.join([str(val) for val in values])
+        out.write('  %s: %s;\n' % (name, value))
+
+
+class Node:
+    
+    """
+    represents one css rule (selector with properties)
+    """
+    
+    def __init__(self, selector, parent, sheet):
+        selector = Selector(selector)
+        
+        if parent:
+            selector = parent.selector + selector
+        self.selector = selector
+        self.props = []
+        sheet.nodes.append(self)
+
+    def set_prop(self, name, values):
+        self.props.append((name, values))        
+
+
+class Selector:
+    
+    """
+    css selector with support of "+" operator
+    understands ">" and "&"
+    """
+
+    def __init__(self, sel):
+        self.sels = map(str.strip, sel.split(','))
+
+    def __add__(self, selector):
+        _sels = []
+        for sel in self.sels:
+            for add_sel in selector.sels:
+                if add_sel.startswith('>'):
+                    _sels.append(sel + add_sel)
+                elif add_sel.startswith('&'):
+                    _sels.append(sel + add_sel.lstrip('&'))
+                else:
+                    _sels.append(sel + ' ' + add_sel)
+        return Selector(', '.join(_sels))
+
+
+class MathException(Exception):
+    
+    """
+    happens when somebody make mistake with Numbers
+    """
+    
+    pass
+
+
+class Number:
+    
+    """
+    number with unit supporting basic math operations
+    """
+    
+    def __init__(self, number, unit):
+        self.number = number
+        self.unit = unit
+
+    def __str__(self):
+        return '%s%s' % (self.number, self.unit)
+
+    def __add__(self, value):
+        if isinstance(value, Number) and value.unit == self.unit:
+            return Number(self.number+value.number, self.unit)
+        if value.unit != self.unit:
+            raise MathException('units not match %s + %s' % (self, value))
+        raise MathException('not a number %s + %s' % (self, value))
+
+    def __sub__(self, value):
+        if isinstance(value, Number) and value.unit == self.unit:
+            return Number(self.number-value.number, self.unit)
+        raise MathException("%s - %s" % (self, value))
+
+    def __mul__(self, value):
+        if isinstance(value, (int, float)):
+            return Number(self.number*value, self.unit)
+        raise MathException("%s * %s" % (self, value))
+
+    def __div__(self, value):
+        if isinstance(value, int):
+            return Number(self.number//value, self.unit)
+        elif isinstance(value, float):
+            return Number(self.number/value, self.unit)
+        raise MathException("%s / %s" % (self, value))
+
+    __rmul__ = __mul__
+    __truediv__ = __div__
+    __repr__ = __str__
+# -*- coding: utf-8 -*-
+
+import re
+import weakref
+import ast
+import cStringIO
+import threading
+import functools
+
+
+_num_r = functools.partial(re.sub, r'(\d+(?:\.\d+)?)(px|em|pt)', r'Number(\1, "\2")')
+_var_r = functools.partial(re.sub, r'!(?=\w+)', '')
+
+
+def _convert_s(value):
+    for fn in [_num_r, _var_r]:
+        value = fn(value)
+    return value.strip()
+
+
+class Parser(object):
+    
+    
+    class AstWrap(object):
+        
+        
+        def __init__(self, owner):
+            self.owner = weakref.proxy(owner)
+        
+        
+        def __getattr__(self, name):
+            return functools.partial(getattr(ast, name),
+                                     col_offset=0,
+                                     lineno=self.owner.lineno)
+    
+    
+    def __init__(self, indent=2):
+        self.indent = indent
+        self.ast = self.AstWrap(self)
+        self.reset()
+    
+    
+    def reset(self):
+        self.level = 0
+        self.parents = []
+        self.__id = 0
+        self.lineno = 0
+        self.ctx = self.ast.Module(body=[
+            self.ast.Assign(targets=[self.ast.Name(id='_sheet', ctx=self.ast.Store())],
+                            value=self.ast.Call(
+                                func=self.ast.Name(id='Sheet', ctx=self.ast.Load()),
+                                args=[], keywords=[], starargs=None, kwargs=None
+                            ))
+        ])
+    
+    
+    def _id(self):
+        self.__id += 1
+        return self.__id
+    
+    
+    def parse(self, input):
+        with threading.Lock():
+            self.input = cStringIO.StringIO(input)
+            lines = self.input.readlines()
+            l = len(lines)
+            i = 0
+            while i < l:
+                if not lines[i].strip():
+                    i += 1
+                    continue
+                line = lines[i].rstrip()
+                while (i < l-1) and lines[i].rstrip().endswith(','):
+                    i += 1
+                    line += lines[i].strip()
+                next = ''
+                self.lineno = i + 1
+                if i < l - 1:
+                    next = lines[i + 1]
+                self.process_line(line, next)
+                i += 1
+        while self.parents:
+            self.leave_ctx()
+        ctx = self.ctx
+        self.reset()
+        return ctx
+    
+    
+    def _push(self, expr):
+        self.ctx.body.append(expr)
+    
+    
+    def enter_ctx(self, ctx):
+        self.level += 1
+        self.parents.append(self.ctx)
+        self.ctx = ctx
+    
+    
+    def leave_ctx(self):
+        self.level -= 1
+        self.ctx = self.parents.pop()
+    
+    
+    def get_level(self, line):
+        return (len(line) - len(line.lstrip())) / self.indent
+    
+    
+    def process_line(self, line, next):
+        level = self.get_level(line)
+        assert level - self.level <= 1, 'broken indentation'
+        
+        if level < self.level:
+            for i in range(self.level - level):
+                self.leave_ctx()
+        
+        line = line.strip()
+        
+        if line.startswith('='):
+            self.enter_ctx(self.handle_def(line[1:]))
+        
+        elif line.startswith('+'):
+            self.handle_call(line[1:])
+        
+        elif line.startswith('.') or line.startswith('#') or\
+                            level < self.get_level(next):
+            self.enter_ctx(self.handle_rule(line))
+        
+        elif line.startswith('!'):
+            self.handle_var(line[1:])
+        
+        else:
+            self.handle_prop(line.strip(':'))
+    
+    
+    def parse_prop(self, data):
+        
+        data = data.lstrip(':')
+        if 0 < data.find('=') < data.find(' '):
+            name, tail = data.split('=', 1)
+            name.rstrip(':')
+            return name, tail, True
+        name, tail = data.split(None, 1)
+        name = name.rstrip(':')
+        if tail.startswith('='):
+            return name, tail.lstrip('='), True
+        return name, tail, False
+    
+    
+    def parse_multiexpr(self, inp):
+        exprs = []
+        inp = inp.strip()
+        while inp:
+            try:
+                expr = ast.parse(inp)
+            except SyntaxError, e:
+                space_pos = inp.rfind(' ', 0, e.offset-1)
+                expr = ast.parse(inp[:space_pos])
+                exprs.append(expr)
+                inp = inp[space_pos:].strip()
+            else:
+                exprs.append(expr)
+                break
+        return [mod.body[0].value for mod in exprs]
+    
+    
+    def handle_prop(self, data):
+        name, value, expr = self.parse_prop(data)
+        if expr:
+            value = _convert_s(value)
+            value = self.ast.List(elts=self.parse_multiexpr(value), ctx=self.ast.Load())
+        else:
+            value = self.ast.List(elts=[self.ast.Str(value)], ctx=self.ast.Load())
+
+        self._push(self.ast.Expr(value=self.ast.Call(
+            func=self.ast.Attribute(value=self.ast.Name(id='node', ctx=self.ast.Load()),
+                                    attr='set_prop', ctx=self.ast.Load()),
+            args=[self.ast.Str(s=name), value], keywords=[], starargs=None, kwargs=None
+        )))
+    
+    
+    def handle_rule(self, data):
+        
+        if type(self.ctx) is ast.Module:
+            parent = 'None'
+        else:
+            parent = 'node'
+        
+        parent = self.ast.Name(id=parent, ctx=self.ast.Load())
+        name = '_n_%s' % self._id()
+        
+        func_def = self.ast.FunctionDef(
+            name=name,
+            args=self.ast.arguments(args=[self.ast.Name(id='parent', ctx=self.ast.Param())],
+                                    vararg=None, kwarg=None, defaults=[]),
+            body=[
+                self.ast.Assign(targets=[self.ast.Name(id='node', ctx=self.ast.Store())],
+                                value=self.ast.Call(
+                                    func=self.ast.Name(id='Node', ctx=self.ast.Load()),
+                                    args=[self.ast.Str(s=data),
+                                          self.ast.Name(id='parent', ctx=self.ast.Load()),
+                                          self.ast.Name(id='_sheet', ctx=self.ast.Load()),
+                                    ],
+                                    keywords=[], starargs=None, kwargs=None
+                                ))
+            ], decorator_list=[]
+        )
+        
+        self._push(func_def)
+        
+        self._push(self.ast.Expr(value=self.ast.Call(
+            func=self.ast.Name(id=name, ctx=self.ast.Load()),
+            args=[parent], keywords=[], starargs=None, kwargs=None
+        )))
+        
+        return func_def
+    
+    
+    def handle_var(self, data):
+        data = _convert_s(data)
+        self._push(ast.parse(data).body[0])
+    
+    
+    def convert_def_call(self, data):
+        data = _convert_s(data)
+        if '(' not in data:
+            data += '()'
+        name_end = data.index('(')
+        name, tail = data[:name_end], data[name_end:]
+        name = name.replace('-', '__')
+        return name + tail
+    
+    
+    def handle_def(self, data):
+        decl = self.convert_def_call(data)
+        decl = ast.parse('def _m_%s:pass' % decl).body[0]
+        decl.body = []
+        decl.args.args.insert(0, self.ast.Name(id='node', ctx=self.ast.Param()))
+        self._push(decl)
+        return decl
+    
+    
+    def handle_call(self, data):
+        data = self.convert_def_call(data)
+        if type(self.ctx) is ast.Module:
+            parent = 'None'
+        else:
+            parent = 'node'
+        expr = ast.parse('_m_%s' % (data)).body[0]
+        expr.value.args.insert(0, self.ast.Name(id=parent, ctx=self.ast.Load()))
+        self._push(expr)
+import sass
+data = open('reset.sass').read()
+print sass.convert(data)

test.sass

-
-=square(!width)
-  :width = !width
-  :height = !width
-  :padding = !width / 10
-  :margin = !width / 20 - 1px 0 0 0
-  +box-font(!width)
-
-=box-font(!size)
-  :font-size = !size / 5
-
-
-.box
-  +square(100px)