1. Victor Kotseruba
  2. python-sass

Commits

Victor Kotseruba  committed 3c2aafd

better expression parser

  • Participants
  • Parent commits ce31a21
  • Branches default

Comments (0)

Files changed (3)

File sass/css.py

View file
  • Ignore whitespace
 # -*- coding: utf-8 -*-
 
 import cStringIO
+import weakref
 
 
 class Sheet:
         out.seek(0)
         return out.read()
     
-    def write_selector(self, out, selector):
+    def write_selector(self, out, selector, level):
         line_length = 0
+        out.write('  ' * level)
         for i in range(len(selector.sels)):
             if i:
                 out.write(', ')
                 line_length += 2
             sel = selector.sels[i]
             if line_length and line_length + len(sel) > 50:
-                out.write('\n')
+                out.write('\n' + '  ' * level)
                 line_length = 0
             out.write(sel)
             line_length += len(sel)
     
     def write_node(self, out, node):
-        self.write_selector(out, node.selector)
-        out.write(' {\n')
+        level = node.level
+        self.write_selector(out, node.selector, level)
+        out.write(' {')
         for prop in node.props:
-            self.write_prop(out, prop)
-        out.write('}\n\n')
+            self.write_prop(out, prop, level)
+        out.write(' }\n')
     
-    def write_prop(self, out, prop):
+    def write_prop(self, out, prop, level):
         name, values = prop
         value = ' '.join([str(val) for val in values])
-        out.write('  %s: %s;\n' % (name, value))
+        out.write('\n' + '  ' * level + '  %s: %s;' % (name, value))
 
 
 class Node:
     
     def __init__(self, selector, parent, sheet):
         selector = Selector(selector)
-        
         if parent:
             selector = parent.selector + selector
         self.selector = selector
         self.props = []
+        self.parent = weakref.proxy(parent) if parent else None
         sheet.nodes.append(self)
 
     def set_prop(self, name, values):
         self.props.append((name, values))        
 
+    @property
+    def level(self):
+        node = self
+        level = 0
+        while node.parent is not None:
+            level += 1
+            node = node.parent
+        return level
+
 
 class Selector:
     

File sass/expr.py

View file
  • Ignore whitespace
+# -*- coding: utf-8 -*-
+
+import threading
+import string
+import ast
+
+
+SEP_CHARS = '()+*/-,[]=: '
+
+
+class ExprParser:
+    
+    
+    def __init__(self):
+        pass
+    
+    
+    def parse(self, data):
+        with threading.Lock():
+            self.data = data
+            self.state = 'space'
+            self.quote = None
+            out = self._cleanup()
+            node = ast.parse(out).body[0]
+            if type(node) is ast.Expr:
+                return node.value
+            return node
+    
+    
+    def cleanup(self, data):
+        with threading.Lock():
+            self.data = data
+            self.state = 'space'
+            self.quote = None
+            return self._cleanup()
+    
+    
+    def _cleanup(self):
+        out = ''
+        token = ''
+        tokens = []
+        for i in range(len(self.data)):
+            next = self.data[i+1] if len(self.data) > i+1 else None
+            state = self.state
+            char = self.handle(out[:-1], self.data[i], next, token)
+            out += char
+            if state != self.state:
+                if token:
+                    tokens.append((token, state))
+                token = ''
+            token += char
+        if token:
+            if tokens and tokens[-1][1] == self.state:
+                tokens[-1] = (tokens[-1][0] + token, self.state)
+            else:
+                tokens.append((token, self.state))
+        out = ''
+        for token, state in tokens:
+            if state == 'name':
+                out += token.lstrip('!')
+            elif state == 'color':
+                out += 'Color("%s")' % token
+            elif state == 'number':
+                if token.isdigit():
+                    out += token
+                else:
+                    for i in range(len(token)):
+                        if token[i] not in string.digits + '.':
+                            break
+                    out += 'Number(%s, "%s")' % (token[:i], token[i:])
+            else:
+                out += token
+        
+        return out.strip()
+    
+    
+    def handle(self, prev, char, next, token):
+        
+        if self.state == 'space':
+            if char in SEP_CHARS:
+                return char
+            elif char == '!':
+                if next not in string.ascii_letters:
+                    raise SyntaxError
+                self.state = 'name'
+                return '!'
+            elif char in '"\'':
+                self.state = 'string'
+                self.quote = char
+                return char
+            elif char in string.digits:
+                self.state = 'number'
+                return char
+            elif char == '#':
+                self.state = 'color'
+                return char
+            elif char in string.ascii_letters + '_':
+                self.state = 'name'
+                return char
+            else:
+                raise SyntaxError
+        
+        elif self.state == 'name':
+            if char in string.ascii_letters + string.digits + '_':
+                return char
+            elif char in SEP_CHARS:
+                self.state = 'space'
+                return char
+            else:
+                raise SyntaxError
+        
+        elif self.state == 'number':
+            if char in string.digits + '.':
+                return char
+            elif char in string.ascii_letters + '%':
+                return char
+            elif char in SEP_CHARS:
+                self.state = 'space'
+                return char
+            else:
+                raise SyntaxError
+        
+        elif self.state == 'color':
+            if char in string.digits + 'abcdefABCDEF':
+                return char
+            elif char in SEP_CHARS:
+                self.state = 'space'
+                return char
+            else:
+                raise SyntaxError
+        
+        elif self.state == 'string':
+            if char == self.quote and prev != '\\':
+                self.state = 'space'
+            return char
+        
+        else:
+            raise SyntaxError
+
+
+def sass_expr_parse(data):
+    parser = ExprParser()
+    return parser.parse(data)
+
+
+def sass_expr_cleanup(data):
+    parse = ExprParser()
+    return parser.cleanup(data)

File sass/parser.py

View file
  • Ignore whitespace
 import threading
 import functools
 
+import expr
+
 
 _num_r = re.compile(r'(\d+(?:\.\d+)?)(px|em|pt)')
 _num_placeholder_r = re.compile('_num_\d+')
 Store = ast.Store
 
 
-
-def sass_expr_parse(data):
-    initial_data = data
-    data = data.strip()
-    numbers = []
-    while True:
-        try:
-            tree = ast.parse(data)
-            transformer = NumberTransformer(numbers)
-            tree = transformer.visit(tree)
-            expr = tree.body[0]
-            if type(expr) == ast.Expr:
-                return expr.value
-            return expr
-        except SyntaxError, e:
-            pos = e.offset - 1
-            if data[pos] == '!':
-                data = data[:pos] + data[pos+1:]
-            elif pos and len(data) > pos and _num_r.match(data[pos-2:pos+1]):
-                start = pos - 2
-                while start  and data[start].isdigit():
-                    start -= 1
-                if start and data[start].isdigit():
-                    raise RuntimeError('parse error: %r' % initial_data)
-                numbers.append(data[start:pos+1])
-                data = data[:start] + '_num_%i' % (len(numbers) - 1) + data[pos+1:]
-            else:
-                raise
-
-
 class AstWrap(object):    
     
     
                                  col_offset=0,
                                  lineno=self.owner.lineno)
 
+    
+    def _name(self, id, ctx='load'):
+        if ctx == 'load':
+            ctx = Load()
+        elif ctx == 'store':
+            ctx = Store()
+        else:
+            raise RuntimeError('unknown ctx %r' % ctx)
+        return self.Name(id=id, ctx=ctx)
+    
+    
+    def _call(self, fn, args=None, kwargs=None):
+        if not args:
+            args = []
+        return self.Call(func=fn, args=args, keywords=[],
+                         starargs=None, kwargs=kwargs)
+
 
 class AstElse(object):
     
     
     
     def _expr(self, variables, match):
-        expr = sass_expr_parse(match.group(1))
-        variables.append(expr)
+        ex = expr.sass_expr_parse(match.group(1))
+        variables.append(ex)
         return '%s'
     
     
                                                 right=varl), node)
 
 
-class NumberTransformer(ast.NodeTransformer):
-
-    lineno = 0
-
-    def __init__(self, numbers):
-        self.numbers = numbers
-        self.ast = AstWrap(self)
-
-    def visit_Name(self, node):
-        if _num_placeholder_r.match(node.id):
-            num = self.numbers[int(node.id[len(node.id)-1:])]
-            value, units = _num_r.findall(num)[0]
-            value = float(value) if '.' in value else int(value)
-            return ast.copy_location(self.ast.Call(func=self.ast.Name(id='Number', ctx=Load()),
-                                                   args=[self.ast.Num(n=value), self.ast.Str(s=units)],
-                                                   keywords=[], starargs=None, kwargs=None),
-                                     node)
-        return node
-
 
 class Parser(object):
     
         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
-                            ))
+            self.ast.Assign(targets=[self.ast._name('_sheet', 'store')],
+                            value=self.ast._call(self.ast._name('Sheet')))
         ])
     
     
         self.__id += 1
         return self.__id
     
+    def _import(self, inp):
+        pass
     
-    def parse(self, input):
+    def _get_contents(self, filename):
+        with open(filename) as fp:
+            inp = fp.read()
+        lines = inp.split('\n')
+        imported = [line for line in lines if line.startswith('@import')]
+        content = []
+        for import_line in imported:
+            rel_path = import_line[7:].strip(' "\'')
+            content.append(self._get_contents(rel_path))
+        content.append(inp)
+        return '\n\n'.join(content)
+        
+    
+    def parse(self, filename):
+        inp = self._get_contents(filename)
         with threading.Lock():
-            self.input = cStringIO.StringIO(input)
+            self.input = cStringIO.StringIO(inp)
             lines = self.input.readlines()
             l = len(lines)
-            i = 0
+            i = 0            
             while i < l:
                 if not lines[i].strip():
                     i += 1
         
         line = line.strip()
         
-        if line.startswith('//'):
+        if line.startswith('@import'):
+            pass
+        
+        elif line.startswith('//'):
             return
         
-        if line.startswith('='):
+        elif line.startswith('='):
             self.enter_ctx(self.handle_def(line[1:]))
                 
         elif line.startswith('+'):
             self.handle_call(line[1:])
-        
+                
         elif line.startswith('@for '):
             self.enter_ctx(self.handle_for(line[5:]))
         
     
     
     def handle_for(self, data):
+        
         var, _, start, through, end = data.split()
-        var, start, end = var.lstrip('!'), int(start), int(end)
-        if end < start:
-            start, end = end, start
-        if through == 'through':
-            end += 1
-        elif through != 'to':
+        var = var.lstrip('!')
+        if through == 'to':
+            end += ' + 1'
+        elif through != 'through':
             raise RuntimeError('syntax error: @for %s' % data)
-        loop = self.ast.For(target=self.ast.Name(id=var, ctx=self.ast.Store()),
-                            iter=self.ast.Call(func=self.ast.Name(id='xrange', ctx=self.ast.Load()),
-                                               args=[self.ast.Num(n=start), self.ast.Num(n=end)],
-                                               keywords=[], starargs=None, kwargs=None),
+        loop = self.ast.For(target=self.ast._name(var, 'store'),
+                            iter=self.ast._call(self.ast._name('xrange'),
+                                                args=[expr.sass_expr_parse(start), expr.sass_expr_parse(end)]),
                             body=[], orelse=[])
         self._push(loop)
         return loop
     
     
     def handle_while(self, data):
-        loop = self.ast.While(test=sass_expr_parse(data), body=[], orelse=[])
+        loop = self.ast.While(test=expr.sass_expr_parse(data), body=[], orelse=[])
         self._push(loop)
         return loop
     
     
     def handle_if(self, data):
-        test = sass_expr_parse(data)
+        test = expr.sass_expr_parse(data)
         condition = self.ast.If(test=test, body=[], orelse=[])
         self._push(condition)
         return condition
     
     
     def handle_elif(self, data):
-        test = sass_expr_parse(data)
+        test = expr.sass_expr_parse(data)
         condition = self.ast.If(test=test, body=[], orelse=[])
         self.if_nodes[self.level].orelse.append(condition)
         return condition
     
     
     def handle_prop(self, data):
-        name, value, expr = self.parse_prop(data)
-        if expr:
-            value = self.ast.List(elts=[sass_expr_parse(value)], ctx=Load())
+        name, value, ex = self.parse_prop(data)
+        if ex:
+            value = self.ast.List(elts=[expr.sass_expr_parse(value)], ctx=Load())
         else:
-            value = self.ast.List(elts=[self.ast.Str(value)], ctx=self.ast.Load())
+            value = self.ast.List(elts=[self.ast.Str(value)], ctx=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()),
+            func=self.ast.Attribute(value=self.ast.Name(id='node', ctx=Load()),
+                                    attr='set_prop', ctx=Load()),
             args=[self.ast.Str(s=name), value], keywords=[], starargs=None, kwargs=None
         )))
     
         else:
             parent = 'None'
         
-        parent = self.ast.Name(id=parent, ctx=self.ast.Load())
+        parent = self.ast.Name(id=parent, ctx=Load())
         name = '_n_%s' % self._id()
         
         
             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())],
+                self.ast.Assign(targets=[self.ast.Name(id='node', ctx=Store())],
                                 value=self.ast.Call(
-                                    func=self.ast.Name(id='Node', ctx=self.ast.Load()),
+                                    func=self.ast.Name(id='Node', ctx=Load()),
                                     args=[self.ast.Str(data),
-                                          self.ast.Name(id='parent', ctx=self.ast.Load()),
-                                          self.ast.Name(id='_sheet', ctx=self.ast.Load()),
+                                          self.ast.Name(id='parent', ctx=Load()),
+                                          self.ast.Name(id='_sheet', ctx=Load()),
                                     ],
                                     keywords=[], starargs=None, kwargs=None
                                 ))
         self._push(func_def)
         
         self._push(self.ast.Expr(value=self.ast.Call(
-            func=self.ast.Name(id=name, ctx=self.ast.Load()),
+            func=self.ast.Name(id=name, ctx=Load()),
             args=[parent], keywords=[], starargs=None, kwargs=None
         )))
         return func_def
         if '||=' in data:
             _or = True
             data = data.replace('||=', '=')
-        expr = sass_expr_parse(data)
+        ex = expr.sass_expr_parse(data)
         if _or:
-            test = self.ast.If(test=self.ast.Compare(left=self.ast.Str(expr.targets[0].id),
+            test = self.ast.If(test=self.ast.Compare(left=self.ast.Str(ex.targets[0].id),
                                                      ops=[self.ast.NotIn()],
                                                      comparators=[self._globals_locals()]),
-                               body=[expr], orelse=[])
+                               body=[ex], orelse=[])
             self._push(test)
         else:
-            self._push(expr)
+            self._push(ex)
 
     
     
     
     def handle_def(self, data):
         decl = self.convert_def_call(data)
-        decl = sass_expr_parse('def _m_%s:pass' % decl)
+        decl = expr.sass_expr_parse('def _m_%s:pass' % decl)
         decl.body = []
         decl.args.args.insert(0, self.ast.Name(id='node', ctx=self.ast.Param()))
         self._push(decl)
             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)
+        call = expr.sass_expr_parse('_m_%s' % data)
+        call.args.insert(0, self.ast.Name(id=parent, ctx=Load()))
+        self._push(self.ast.Expr(call))