Source

wheezy.template / src / wheezy / template / ext / determined.py


"""
"""

import re

from wheezy.template.utils import find_balanced


RE_ARGS = re.compile(
    r'\s*(?P<expr>(([\'"]).*?\3|.+?))\s*\,')
RE_KWARGS = re.compile(
    r'\s*(?P<name>\w+)\s*=\s*(?P<expr>([\'"].*?[\'"]|.+?))\s*\,')
RE_STR_VALUE = re.compile(
    r'^[\'"](?P<value>.+)[\'"]$')
RE_INT_VALUE = re.compile(
    r'^(?P<value>(\d+))$')


# region: core extension

class DeterminedExtension(object):
    """ Tranlates function calls between template engines.

        Strictly determined known calls are converted to preprocessor
        calls, e.g.::

            @_('Name:')
            @path_for('default')
            @path_for('static', path='/static/css/site.css')

        Those that are not strictly determined are ignored and processed
        by runtime engine.
    """

    def __init__(self, known_calls, runtime_token_start='@',
                 token_start='#'):
        self.token_start = token_start
        self.pattern = re.compile(r'%s(%s)(?=\()' % (
            runtime_token_start, '|'.join(known_calls)))
        self.preprocessors = [self.preprocess]

    def preprocess(self, source):
        result = []
        start = 0
        for m in self.pattern.finditer(source):
            pstart = m.end()
            pend = find_balanced(source, pstart)
            if determined(source[pstart + 1:pend - 1]):
                name = m.group(1)
                result.append(source[start:m.start()])
                result.append(self.token_start + "ctx['" + name + "']")
                start = pstart
        if start:
            result.append(source[start:])
            return ''.join(result)
        else:
            return source


def determined(expression):
    """ Checks if expresion is strictly determined.

        >>> determined("'default'")
        True
        >>> determined('name')
        False
        >>> determined("'default', id=id")
        False
        >>> determined("'default', lang=100")
        True
        >>> determined('')
        True
    """
    args, kwargs = parse_params(expression)
    for arg in args:
        if not str_or_int(arg):
            return False
    for arg in kwargs.values():
        if not str_or_int(arg):
            return False
    return True


def parse_kwargs(text):
    """ Parses key-value type of parameters.

        >>> parse_kwargs('id=item.id')
        {'id': 'item.id'}
        >>> parse_kwargs('lang="en", id=12')
        {'lang': '"en"', 'id': '12'}
    """
    kwargs = {}
    for m in RE_KWARGS.finditer(text + ','):
        groups = m.groupdict()
        kwargs[groups['name'].rstrip('_')] = groups['expr']
    return kwargs


def parse_args(text):
    """ Parses argument type of parameters.

        >>> parse_args('')
        []
        >>> parse_args('10, "x"')
        ['10', '"x"']
        >>> parse_args("'x', 100")
        ["'x'", '100']
        >>> parse_args('"default"')
        ['"default"']
    """
    args = []
    for m in RE_ARGS.finditer(text + ','):
        args.append(m.group('expr'))
    return args


def parse_params(text):
    """ Parses function parameters.

        >>> parse_params('')
        ([], {})
        >>> parse_params('id=item.id')
        ([], {'id': 'item.id'})
        >>> parse_params('"default"')
        (['"default"'], {})
        >>> parse_params('"default", lang="en"')
        (['"default"'], {'lang': '"en"'})
    """
    if '=' in text:
        args = text.split('=')[0]
        if ',' in args:
            args = args.rsplit(',', 1)[0]
            kwargs = text[len(args):]
            return parse_args(args), parse_kwargs(kwargs)
        else:
            return [], parse_kwargs(text)
    else:
        return parse_args(text), {}


def str_or_int(text):
    """ Ensures ``text`` as string or int expression.

        >>> str_or_int('"default"')
        True
        >>> str_or_int("'Hello'")
        True
        >>> str_or_int('100')
        True
        >>> str_or_int('item.id')
        False
    """
    m = RE_STR_VALUE.match(text)
    if m:
        return True
    else:
        m = RE_INT_VALUE.match(text)
        if m:
            return True
    return False