python-sass / sass / parser.py

# -*- coding: utf-8 -*-

import re
import weakref
import ast
import cStringIO
import threading
import functools


_num_r = functools.partial(re.compile(r'(\d+(?:\.\d+)?)(px|em|pt)').sub, r"Number(\1, '\2')")
_var_r = functools.partial(re.compile(r'!(?=\w+)').sub, '')


Load = ast.Load
Store = ast.Store

def _convert_s(value):
    for fn in [_num_r, _var_r]:
        value = fn(value)
    return value.strip()



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)


class AstElse(object):
    
    def __init__(self, if_node):
        self.if_node = if_node
    
    @property
    def body(self):
        return self.if_node.orelse



class RubyStringFormatter(ast.NodeTransformer):
    
    lineno = 0
    
    
    def __init__(self):
        super(RubyStringFormatter, self).__init__()
        self.ast = AstWrap(self)
    
    
    def _expr(self, variables, match):
        s = _convert_s(match.group(1))
        expr = ast.parse(_convert_s(match.group(1))).body[0].value
        variables.append(expr)
        return '%s'
    
    
    def visit_Str(self, node):
        variables = []
        s = re.sub(r'#{([^}]+)}', functools.partial(self._expr, variables),
                   node.s)
        
        if not variables:
            return node

        varl = self.ast.Tuple(elts=variables, ctx=Load())
        
        return ast.copy_location(self.ast.BinOp(left=self.ast.Str(s=s),
                                                op=self.ast.Mod(),
                                                right=varl), node)


class Parser(object):
    
    def __init__(self, indent=2):
        self.indent = indent
        self.ast = AstWrap(self)
        self.reset()
    
    
    def reset(self):
        self.level = 0
        self.parents = []
        self.if_nodes = []
        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()
        mod = RubyStringFormatter().visit(ctx)
        return mod
    
    
    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('@for '):
            self.enter_ctx(self.handle_for(line[5:]))
        
        elif line.startswith('@while '):
            self.enter_ctx(self.handle_while(line[1:]))
        
        elif line.startswith('@if '):
            if_node = self.handle_if(line[4:])
            self.if_nodes += [None] * (self.level - len(self.if_nodes) + 1)
            self.if_nodes[self.level] = if_node
            self.enter_ctx(if_node)
        
        elif line == '@else':
            self.enter_ctx(self.handle_else())
        
        elif line.startswith('@else if '):
            self.enter_ctx(self.handle_elif(line[9:]))
        
        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_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':
            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),
                            body=[], orelse=[])
        self._push(loop)
        return loop
    
    
    def handle_while(self, data):
        data = _convert_s(data)
        loop = ast.parse('%s:pass' % data).body[0]
        loop.body = []
        self._push(loop)
        return loop
    
    
    def handle_if(self, data):
        test = ast.parse(_convert_s(data)).body[0].value
        condition = self.ast.If(test=test, body=[], orelse=[])
        self._push(condition)
        return condition
    
    
    def handle_else(self):
        return AstElse(self.if_nodes[self.level])
    
    
    def handle_elif(self, data):
        test = ast.parse(_convert_s(data)).body[0].value
        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 = _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 filter(lambda p: type(p) is ast.FunctionDef, self.parents + [self.ctx]):
            parent = 'node'
        else:
            parent = 'None'
        
        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(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)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.