944 lines
31 KiB
Python
944 lines
31 KiB
Python
#
|
|
# Copyright (c) 2015 Will Bond, Mjumbe Wawatu Ukweli, 2012 Canonical Ltd
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation, version 3 only.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
# GNU Lesser General Public License version 3 (see the file LICENSE).
|
|
|
|
"""The compiler for pybars."""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from types import ModuleType
|
|
import linecache
|
|
|
|
# The following code allows importing pybars from the current dir
|
|
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if sys.path[0] != prev_dir:
|
|
try:
|
|
sys.path.remove(prev_dir)
|
|
except ValueError:
|
|
pass
|
|
sys.path.insert(0, prev_dir)
|
|
|
|
import pybars
|
|
from pybars import _templates
|
|
from .pymeta.grammar import OMeta
|
|
|
|
__all__ = [
|
|
'Compiler',
|
|
'strlist',
|
|
'Scope'
|
|
]
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
# This allows the code to run on Python 2 and 3 by
|
|
# creating a consistent reference for the appropriate
|
|
# string class
|
|
try:
|
|
str_class = unicode
|
|
except NameError:
|
|
# Python 3 support
|
|
str_class = str
|
|
|
|
# backward compatibility for basestring for python < 2.3
|
|
try:
|
|
basestring
|
|
except NameError:
|
|
basestring = str
|
|
|
|
# Flag for testing
|
|
debug = False
|
|
|
|
|
|
# Note that unless we presume handlebars is only generating valid html, we have
|
|
# to accept anything - so a broken template won't be all that visible - it will
|
|
# just render literally (because the anything rule matches it).
|
|
|
|
# this grammar generates a tokenised tree
|
|
handlebars_grammar = r"""
|
|
|
|
template ::= (<text> | <templatecommand>)*:body => ['template'] + body
|
|
text ::= <newline_text> | <whitespace_text> | <other_text>
|
|
newline_text ::= (~(<start>) ('\r'?'\n'):char) => ('newline', u'' + char)
|
|
whitespace_text ::= (~(<start>) (' '|'\t'))+:text => ('whitespace', u''.join(text))
|
|
other_text ::= (~(<start>) <anything>)+:text => ('literal', u''.join(text))
|
|
other ::= <anything>:char => ('literal', u'' + char)
|
|
templatecommand ::= <blockrule>
|
|
| <comment>
|
|
| <escapedexpression>
|
|
| <expression>
|
|
| <partial>
|
|
| <rawblock>
|
|
start ::= '{' '{'
|
|
finish ::= '}' '}'
|
|
comment ::= <start> '!' (~(<finish>) <anything>)* <finish> => ('comment', )
|
|
space ::= ' '|'\t'|'\r'|'\n'
|
|
arguments ::= (<space>+ (<kwliteral>|<literal>|<path>|<subexpression>))*:arguments => arguments
|
|
subexpression ::= '(' <spaces> <path>:p (<space>+ (<kwliteral>|<literal>|<path>|<subexpression>))*:arguments <spaces> ')' => ('subexpr', p, arguments)
|
|
expression_inner ::= <spaces> <path>:p <arguments>:arguments <spaces> <finish> => (p, arguments)
|
|
expression ::= <start> '{' <expression_inner>:e '}' => ('expand', ) + e
|
|
| <start> '&' <expression_inner>:e => ('expand', ) + e
|
|
escapedexpression ::= <start> <expression_inner>:e => ('escapedexpand', ) + e
|
|
block_inner ::= <spaces> <symbol>:s <arguments>:args <spaces> <finish>
|
|
=> (u''.join(s), args)
|
|
partial_inner ::= <spaces> <partialname>:s <arguments>:args <spaces> <finish>
|
|
=> (s, args)
|
|
alt_inner ::= <spaces> ('^' | 'e' 'l' 's' 'e') <spaces> <finish>
|
|
partial ::= <start> '>' <partial_inner>:i => ('partial',) + i
|
|
path ::= ~('/') <pathseg>+:segments => ('path', segments)
|
|
kwliteral ::= <safesymbol>:s '=' (<literal>|<path>|<subexpression>):v => ('kwparam', s, v)
|
|
literal ::= (<string>|<integer>|<boolean>|<null>|<undefined>):thing => ('literalparam', thing)
|
|
string ::= '"' <notdquote>*:ls '"' => u'"' + u''.join(ls) + u'"'
|
|
| "'" <notsquote>*:ls "'" => u"'" + u''.join(ls) + u"'"
|
|
integer ::= '-'?:sign <digit>+:ds => int((sign if sign else '') + ''.join(ds))
|
|
boolean ::= <false>|<true>
|
|
false ::= 'f' 'a' 'l' 's' 'e' => False
|
|
true ::= 't' 'r' 'u' 'e' => True
|
|
null ::= ('n' 'u' 'l' 'l') => None
|
|
undefined ::= ('u' 'n' 'd' 'e' 'f' 'i' 'n' 'e' 'd') => None
|
|
notdquote ::= <escapedquote>
|
|
| '\n' => '\\n'
|
|
| '\r' => '\\r'
|
|
| '\\' => '\\\\'
|
|
| (~('"') <anything>)
|
|
notsquote ::= <escapedquote>
|
|
| '\n' => '\\n'
|
|
| '\r' => '\\r'
|
|
| '\\' => '\\\\'
|
|
| (~("'") <anything>)
|
|
escapedquote ::= '\\' '"' => '\\"'
|
|
| "\\" "'" => "\\'"
|
|
notclosebracket ::= (~(']') <anything>)
|
|
safesymbol ::= ~<alt_inner> '['? (<letter>|'_'):start (<letterOrDigit>|'_')+:symbol ']'? => start + u''.join(symbol)
|
|
symbol ::= ~<alt_inner> '['? (<letterOrDigit>|'-'|'@')+:symbol ']'? => u''.join(symbol)
|
|
partialname ::= <subexpression>:s => s
|
|
| ~<alt_inner> ('['|'"')? (~(<space>|<finish>|']'|'"' ) <anything>)+:symbol (']'|'"')? => ('literalparam', '"' + u''.join(symbol) + '"')
|
|
pathseg ::= '[' <notclosebracket>+:symbol ']' => u''.join(symbol)
|
|
| ('@' '.' '.' '/') => u'@@_parent'
|
|
| <symbol>
|
|
| '/' => u''
|
|
| ('.' '.' '/') => u'@_parent'
|
|
| '.' => u''
|
|
pathfinish :expected ::= <start> '/' <path>:found ?(found == expected) <finish>
|
|
symbolfinish :expected ::= <start> '/' <symbol>:found ?(found == expected) <finish>
|
|
blockrule ::= <start> '#' <block_inner>:i
|
|
<template>:t <alttemplate>:alt_t <symbolfinish i[0]> => ('block',) + i + (t, alt_t)
|
|
| <start> '^' <block_inner>:i
|
|
<template>:t <alttemplate>:alt_t <symbolfinish i[0]> => ('invertedblock',) + i + (t, alt_t)
|
|
alttemplate ::= (<start> <alt_inner> <template>)?:alt_t => alt_t or []
|
|
rawblockstart ::= <start> <start> <block_inner>:i <finish> => i
|
|
rawblockfinish :expected ::= <start> <symbolfinish expected> <finish>
|
|
rawblock ::= <rawblockstart>:i (~(<rawblockfinish i[0]>) <anything>)*:r <rawblockfinish i[0]>
|
|
=> ('rawblock',) + i + (''.join(r),)
|
|
"""
|
|
|
|
# this grammar compiles the template to python
|
|
compile_grammar = """
|
|
compile ::= <prolog> <rule>* => builder.finish()
|
|
prolog ::= "template" => builder.start()
|
|
rule ::= <literal>
|
|
| <expand>
|
|
| <escapedexpand>
|
|
| <comment>
|
|
| <block>
|
|
| <invertedblock>
|
|
| <rawblock>
|
|
| <partial>
|
|
block ::= [ "block" <anything>:symbol [<arg>*:arguments] [<compile>:t] [<compile>?:alt_t] ] => builder.add_block(symbol, arguments, t, alt_t)
|
|
comment ::= [ "comment" ]
|
|
literal ::= [ ( "literal" | "newline" | "whitespace" ) :value ] => builder.add_literal(value)
|
|
expand ::= [ "expand" <path>:value [<arg>*:arguments]] => builder.add_expand(value, arguments)
|
|
escapedexpand ::= [ "escapedexpand" <path>:value [<arg>*:arguments]] => builder.add_escaped_expand(value, arguments)
|
|
invertedblock ::= [ "invertedblock" <anything>:symbol [<arg>*:arguments] [<compile>:t] [<compile>?:alt_t] ] => builder.add_invertedblock(symbol, arguments, t, alt_t)
|
|
rawblock ::= [ "rawblock" <anything>:symbol [<arg>*:arguments] <anything>:raw ] => builder.add_rawblock(symbol, arguments, raw)
|
|
partial ::= ["partial" <complexarg>:symbol [<arg>*:arguments]] => builder.add_partial(symbol, arguments)
|
|
path ::= [ "path" [<pathseg>:segment]] => ("simple", segment)
|
|
| [ "path" [<pathseg>+:segments] ] => ("complex", u"resolve(context, u'" + u"', u'".join(segments) + u"')" )
|
|
complexarg ::= [ "path" [<pathseg>+:segments] ] => u"resolve(context, u'" + u"', u'".join(segments) + u"')"
|
|
| [ "subexpr" ["path" <pathseg>:name] [<arg>*:arguments] ] => u'resolve_subexpr(helpers, "' + name + '", context' + (u', ' + u', '.join(arguments) if arguments else u'') + u')'
|
|
| [ "literalparam" <anything>:value ] => {str_class}(value)
|
|
arg ::= [ "kwparam" <anything>:symbol <complexarg>:a ] => {str_class}(symbol) + '=' + a
|
|
| <complexarg>
|
|
pathseg ::= "/" => ''
|
|
| "." => ''
|
|
| "" => ''
|
|
| "this" => ''
|
|
pathseg ::= <anything>:symbol => u''.join(symbol)
|
|
""" # noqa: E501
|
|
compile_grammar = compile_grammar.format(str_class=str_class.__name__)
|
|
|
|
|
|
class PybarsError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
class strlist(list):
|
|
|
|
"""A quasi-list to let the template code avoid special casing."""
|
|
|
|
def __str__(self): # Python 3
|
|
return ''.join(self)
|
|
|
|
def __unicode__(self): # Python 2
|
|
return u''.join(self)
|
|
|
|
def grow(self, thing):
|
|
"""Make the list longer, appending for unicode, extending otherwise."""
|
|
if type(thing) == str_class:
|
|
self.append(thing)
|
|
|
|
# This will only ever match in Python 2 since str_class is str in
|
|
# Python 3.
|
|
elif type(thing) == str:
|
|
self.append(unicode(thing)) # noqa: F821 undefined name 'unicode'
|
|
|
|
else:
|
|
# Recursively expand to a flat list; may deserve a C accelerator at
|
|
# some point.
|
|
for element in thing:
|
|
self.grow(element)
|
|
|
|
|
|
_map = {
|
|
'&': '&',
|
|
'"': '"',
|
|
"'": ''',
|
|
'`': '`',
|
|
'<': '<',
|
|
'>': '>',
|
|
}
|
|
|
|
|
|
def substitute(match, _map=_map):
|
|
return _map[match.group(0)]
|
|
|
|
|
|
_escape_re = re.compile(r"&|\"|'|`|<|>")
|
|
|
|
|
|
def escape(something, _escape_re=_escape_re, substitute=substitute):
|
|
return _escape_re.sub(substitute, something)
|
|
|
|
|
|
def pick(context, name, default=None):
|
|
try:
|
|
return context[name]
|
|
except (KeyError, TypeError, AttributeError):
|
|
if isinstance(name, basestring):
|
|
try:
|
|
exists = hasattr(context, name)
|
|
except UnicodeEncodeError:
|
|
# Python 2 raises UnicodeEncodeError on non-ASCII strings
|
|
pass
|
|
else:
|
|
if exists:
|
|
return getattr(context, name)
|
|
if hasattr(context, 'get'):
|
|
return context.get(name)
|
|
return default
|
|
|
|
|
|
sentinel = object()
|
|
|
|
|
|
class Scope:
|
|
|
|
def __init__(self, context, parent, root, overrides=None, index=None, key=None, first=None, last=None):
|
|
self.context = context
|
|
self.parent = parent
|
|
self.root = root
|
|
# Must be dict of keys and values
|
|
self.overrides = overrides
|
|
self.index = index
|
|
self.key = key
|
|
self.first = first
|
|
self.last = last
|
|
|
|
def get(self, name, default=None):
|
|
if name == '@root':
|
|
return self.root
|
|
if name == '@_parent':
|
|
return self.parent
|
|
if name == '@index' and self.index is not None:
|
|
return self.index
|
|
if name == '@key' and self.key is not None:
|
|
return self.key
|
|
if name == '@first' and self.first is not None:
|
|
return self.first
|
|
if name == '@last' and self.last is not None:
|
|
return self.last
|
|
if name == 'this':
|
|
return self.context
|
|
if self.overrides and name in self.overrides:
|
|
return self.overrides[name]
|
|
return pick(self.context, name, default)
|
|
__getitem__ = get
|
|
|
|
def __len__(self):
|
|
return len(self.context)
|
|
|
|
# Added for Python 3
|
|
def __str__(self):
|
|
return str(self.context)
|
|
|
|
# Only called in Python 2
|
|
def __unicode__(self):
|
|
return unicode(self.context) # noqa: F821 undefined name 'unicode'
|
|
|
|
|
|
def resolve(context, *segments):
|
|
carryover_data = False
|
|
|
|
# This makes sure that bare "this" paths don't return a Scope object
|
|
if segments == ('',) and isinstance(context, Scope):
|
|
return context.get('this')
|
|
|
|
for segment in segments:
|
|
|
|
# Handle @../index syntax by popping the extra @ along the segment path
|
|
if carryover_data:
|
|
carryover_data = False
|
|
segment = u'@%s' % segment
|
|
if len(segment) > 1 and segment[0:2] == '@@':
|
|
segment = segment[1:]
|
|
carryover_data = True
|
|
|
|
if context is None:
|
|
return None
|
|
if segment in (None, ""):
|
|
continue
|
|
if type(context) in (list, tuple):
|
|
if segment == 'length':
|
|
return len(context)
|
|
offset = int(segment)
|
|
context = context[offset]
|
|
elif isinstance(context, Scope):
|
|
context = context.get(segment)
|
|
else:
|
|
context = pick(context, segment)
|
|
return context
|
|
|
|
|
|
def resolve_subexpr(helpers, name, context, *args, **kwargs):
|
|
if name not in helpers:
|
|
raise PybarsError(u"Could not find property %s" % (name,))
|
|
return helpers[name](context, *args, **kwargs)
|
|
|
|
|
|
def prepare(value, should_escape):
|
|
"""
|
|
Prepares a value to be added to the result
|
|
|
|
:param value:
|
|
The value to add to the result
|
|
|
|
:param should_escape:
|
|
If the string should be HTML-escaped
|
|
|
|
:return:
|
|
A unicode string or strlist
|
|
"""
|
|
|
|
if value is None:
|
|
return u''
|
|
type_ = type(value)
|
|
if type_ is not strlist:
|
|
if type_ is not str_class:
|
|
if type_ is bool:
|
|
value = u'true' if value else u'false'
|
|
else:
|
|
value = str_class(value)
|
|
if should_escape:
|
|
value = escape(value)
|
|
return value
|
|
|
|
|
|
def ensure_scope(context, root):
|
|
return context if isinstance(context, Scope) else Scope(context, context, root)
|
|
|
|
|
|
def _each(this, options, context):
|
|
result = strlist()
|
|
|
|
# All sequences in python have a length
|
|
try:
|
|
last_index = len(context) - 1
|
|
|
|
# If there are no items, we want to trigger the else clause
|
|
if last_index < 0:
|
|
raise IndexError()
|
|
|
|
except (TypeError, IndexError):
|
|
return options['inverse'](this)
|
|
|
|
# We use the presence of a keys method to determine if the
|
|
# key attribute should be passed to the block handler
|
|
has_keys = hasattr(context, 'keys')
|
|
|
|
index = 0
|
|
for value in context:
|
|
kwargs = {
|
|
'index': index,
|
|
'first': index == 0,
|
|
'last': index == last_index
|
|
}
|
|
|
|
if has_keys:
|
|
kwargs['key'] = value
|
|
value = context[value]
|
|
|
|
scope = Scope(value, this, options['root'], **kwargs)
|
|
|
|
# Necessary because of cases such as {{^each things}}test{{/each}}.
|
|
try:
|
|
result.grow(options['fn'](scope))
|
|
except TypeError:
|
|
pass
|
|
index += 1
|
|
|
|
return result
|
|
|
|
|
|
def _if(this, options, context):
|
|
if hasattr(context, '__call__'):
|
|
context = context(this)
|
|
if context:
|
|
return options['fn'](this)
|
|
else:
|
|
return options['inverse'](this)
|
|
|
|
|
|
def _log(this, context):
|
|
pybars.log(context)
|
|
|
|
|
|
def _unless(this, options, context):
|
|
if not context:
|
|
return options['fn'](this)
|
|
|
|
|
|
def _lookup(this, context, key):
|
|
try:
|
|
return context[key]
|
|
except (KeyError, IndexError, TypeError):
|
|
return
|
|
|
|
|
|
def _blockHelperMissing(this, options, context):
|
|
if hasattr(context, '__call__'):
|
|
context = context(this)
|
|
if context != u"" and not context:
|
|
return options['inverse'](this)
|
|
if type(context) in (list, strlist, tuple):
|
|
return _each(this, options, context)
|
|
if context is True:
|
|
callwith = this
|
|
else:
|
|
callwith = context
|
|
return options['fn'](callwith)
|
|
|
|
|
|
def _helperMissing(scope, name, *args):
|
|
if not args:
|
|
return None
|
|
raise PybarsError(u"Could not find property %s" % (name,))
|
|
|
|
|
|
def _with(this, options, context):
|
|
return options['fn'](context)
|
|
|
|
|
|
# scope for the compiled code to reuse globals
|
|
_pybars_ = {
|
|
'helpers': {
|
|
'blockHelperMissing': _blockHelperMissing,
|
|
'each': _each,
|
|
'if': _if,
|
|
'helperMissing': _helperMissing,
|
|
'log': _log,
|
|
'unless': _unless,
|
|
'with': _with,
|
|
'lookup': _lookup,
|
|
},
|
|
}
|
|
|
|
|
|
class FunctionContainer:
|
|
|
|
"""
|
|
Used as a container for functions by the CodeBuidler
|
|
"""
|
|
|
|
def __init__(self, name, code):
|
|
self.name = name
|
|
self.code = code
|
|
|
|
@property
|
|
def full_code(self):
|
|
headers = (
|
|
u'import pybars\n'
|
|
u'\n'
|
|
u'if pybars.__version__ != %s:\n'
|
|
u' raise pybars.PybarsError("This template was precompiled with pybars3 version %s, running version %%s" %% pybars.__version__)\n'
|
|
u'\n'
|
|
u'from pybars import strlist, Scope, PybarsError\n'
|
|
u'from pybars._compiler import _pybars_, escape, resolve, resolve_subexpr, prepare, ensure_scope\n'
|
|
u'\n'
|
|
u'from functools import partial\n'
|
|
u'\n'
|
|
u'\n'
|
|
) % (repr(pybars.__version__), pybars.__version__)
|
|
|
|
return headers + self.code
|
|
|
|
|
|
class CodeBuilder:
|
|
|
|
"""Builds code for a template."""
|
|
|
|
def __init__(self):
|
|
self._reset()
|
|
|
|
def _reset(self):
|
|
self.stack = []
|
|
self.var_counter = 1
|
|
self.render_counter = 0
|
|
|
|
def start(self):
|
|
function_name = 'render' if self.render_counter == 0 else 'block_%s' % self.render_counter
|
|
self.render_counter += 1
|
|
|
|
self.stack.append((strlist(), {}, function_name))
|
|
self._result, self._locals, _ = self.stack[-1]
|
|
# Context may be a user hash or a Scope (which injects '@_parent' to
|
|
# implement .. lookups). The JS implementation uses a vector of scopes
|
|
# and then interprets a linear walk-up, which is why there is a
|
|
# disabled test showing arbitrary complex path manipulation: the scope
|
|
# approach used here will probably DTRT but may be slower: reevaluate
|
|
# when profiling.
|
|
if len(self.stack) == 1:
|
|
self._result.grow([
|
|
u"def render(context, helpers=None, partials=None, root=None):\n"
|
|
u" _helpers = dict(_pybars_['helpers'])\n"
|
|
u" if helpers is not None:\n"
|
|
u" _helpers.update(helpers)\n"
|
|
u" helpers = _helpers\n"
|
|
u" if partials is None:\n"
|
|
u" partials = {}\n"
|
|
u" called = root is None\n"
|
|
u" if called:\n"
|
|
u" root = context\n"
|
|
])
|
|
else:
|
|
self._result.grow(u"def %s(context, helpers, partials, root):\n" % function_name)
|
|
self._result.grow(u" result = strlist()\n")
|
|
self._result.grow(u" context = ensure_scope(context, root)\n")
|
|
|
|
def finish(self):
|
|
lines, ns, function_name = self.stack.pop(-1)
|
|
|
|
# Ensure the result is a string and not a strlist
|
|
if len(self.stack) == 0:
|
|
self._result.grow(u" if called:\n")
|
|
self._result.grow(u" result = %s(result)\n" % str_class.__name__)
|
|
self._result.grow(u" return result\n")
|
|
|
|
source = str_class(u"".join(lines))
|
|
|
|
self._result = self.stack and self.stack[-1][0]
|
|
self._locals = self.stack and self.stack[-1][1]
|
|
|
|
code = ''
|
|
for key in ns:
|
|
if isinstance(ns[key], FunctionContainer):
|
|
code += ns[key].code + '\n'
|
|
else:
|
|
code += '%s = %s\n' % (key, repr(ns[key]))
|
|
code += source
|
|
|
|
result = FunctionContainer(function_name, code)
|
|
if debug and len(self.stack) == 0:
|
|
print('Compiled Python')
|
|
print('---------------')
|
|
print(result.full_code)
|
|
|
|
return result
|
|
|
|
def _wrap_nested(self, name):
|
|
return u"partial(%s, helpers=helpers, partials=partials, root=root)" % name
|
|
|
|
def add_block(self, symbol, arguments, nested, alt_nested):
|
|
name = nested.name
|
|
self._locals[name] = nested
|
|
|
|
if alt_nested:
|
|
alt_name = alt_nested.name
|
|
self._locals[alt_name] = alt_nested
|
|
|
|
call = self.arguments_to_call(arguments)
|
|
self._result.grow([
|
|
u" options = {'fn': %s}\n" % self._wrap_nested(name),
|
|
u" options['helpers'] = helpers\n"
|
|
u" options['partials'] = partials\n"
|
|
u" options['root'] = root\n"
|
|
])
|
|
if alt_nested:
|
|
self._result.grow([
|
|
u" options['inverse'] = ",
|
|
self._wrap_nested(alt_name),
|
|
u"\n"
|
|
])
|
|
else:
|
|
self._result.grow([
|
|
u" options['inverse'] = lambda this: None\n"
|
|
])
|
|
self._result.grow([
|
|
u" value = helper = helpers.get(u'%s')\n" % symbol,
|
|
u" if value is None:\n"
|
|
u" value = resolve(context, u'%s')\n" % symbol,
|
|
u" if helper and hasattr(helper, '__call__'):\n"
|
|
u" value = helper(context, options%s\n" % call,
|
|
u" else:\n"
|
|
u" value = helpers['blockHelperMissing'](context, options, value)\n"
|
|
u" result.grow(value or '')\n"
|
|
])
|
|
|
|
def add_literal(self, value):
|
|
self._result.grow(u" result.append(%s)\n" % repr(value))
|
|
|
|
def _lookup_arg(self, arg):
|
|
if not arg:
|
|
return u"context"
|
|
return arg
|
|
|
|
def arguments_to_call(self, arguments):
|
|
params = list(map(self._lookup_arg, arguments))
|
|
output = u', '.join(params) + u')'
|
|
if len(params) > 0:
|
|
output = u', ' + output
|
|
return output
|
|
|
|
def find_lookup(self, path, path_type, call):
|
|
if path_type == "simple": # simple names can reference helpers.
|
|
# TODO: compile this whole expression in the grammar; for now,
|
|
# fugly but only a compile time overhead.
|
|
# XXX: just rm.
|
|
realname = path.replace('.get("', '').replace('")', '')
|
|
self._result.grow([
|
|
u" value = helpers.get(u'%s')\n" % realname,
|
|
u" if value is None:\n"
|
|
u" value = resolve(context, u'%s')\n" % path,
|
|
])
|
|
else:
|
|
realname = None
|
|
self._result.grow(u" value = %s\n" % path)
|
|
self._result.grow([
|
|
u" if hasattr(value, '__call__'):\n"
|
|
u" value = value(context%s\n" % call,
|
|
])
|
|
if realname:
|
|
self._result.grow(
|
|
u" elif value is None:\n"
|
|
u" value = helpers['helperMissing'](context, u'%s'%s\n"
|
|
% (realname, call)
|
|
)
|
|
|
|
def add_escaped_expand(self, path_type_path, arguments):
|
|
(path_type, path) = path_type_path
|
|
call = self.arguments_to_call(arguments)
|
|
self.find_lookup(path, path_type, call)
|
|
self._result.grow([
|
|
u" result.grow(prepare(value, True))\n"
|
|
])
|
|
|
|
def add_expand(self, path_type_path, arguments):
|
|
(path_type, path) = path_type_path
|
|
call = self.arguments_to_call(arguments)
|
|
self.find_lookup(path, path_type, call)
|
|
self._result.grow([
|
|
u" result.grow(prepare(value, False))\n"
|
|
])
|
|
|
|
def _debug(self):
|
|
self._result.grow(u" import pdb;pdb.set_trace()\n")
|
|
|
|
def add_invertedblock(self, symbol, arguments, nested, alt_nested):
|
|
name = nested.name
|
|
self._locals[name] = nested
|
|
|
|
if alt_nested:
|
|
alt_name = alt_nested.name
|
|
self._locals[alt_name] = alt_nested
|
|
|
|
call = self.arguments_to_call(arguments)
|
|
self._result.grow([
|
|
u" options = {'inverse': %s}\n" % self._wrap_nested(name),
|
|
u" options['helpers'] = helpers\n"
|
|
u" options['partials'] = partials\n"
|
|
u" options['root'] = root\n"
|
|
])
|
|
if alt_nested:
|
|
self._result.grow([
|
|
u" options['fn'] = ",
|
|
self._wrap_nested(alt_name),
|
|
u"\n"
|
|
])
|
|
else:
|
|
self._result.grow([
|
|
u" options['fn'] = lambda this: None\n"
|
|
])
|
|
self._result.grow([
|
|
u" value = helper = helpers.get(u'%s')\n" % symbol,
|
|
u" if value is None:\n"
|
|
u" value = resolve(context, u'%s')\n" % symbol,
|
|
u" if helper and hasattr(helper, '__call__'):\n"
|
|
u" value = helper(context, options%s\n" % call,
|
|
u" else:\n"
|
|
u" value = helpers['blockHelperMissing'](context, options, value)\n"
|
|
u" result.grow(value or '')\n"
|
|
])
|
|
|
|
def add_rawblock(self, symbol, arguments, raw):
|
|
call = self.arguments_to_call(arguments)
|
|
self._result.grow([
|
|
u" options = {'fn': lambda this: %s}\n" % repr(raw),
|
|
u" options['helpers'] = helpers\n"
|
|
u" options['partials'] = partials\n"
|
|
u" options['root'] = root\n"
|
|
u" options['inverse'] = lambda this: None\n"
|
|
u" helper = helpers.get(u'%s')\n" % symbol,
|
|
u" if helper and hasattr(helper, '__call__'):\n"
|
|
u" value = helper(context, options%s\n" % call,
|
|
u" else:\n"
|
|
u" value = %s\n" % repr(raw),
|
|
u" result.grow(value or '')\n"
|
|
])
|
|
|
|
def _invoke_template(self, fn_name, this_name):
|
|
self._result.grow([
|
|
u" result.grow(",
|
|
fn_name,
|
|
u"(",
|
|
this_name,
|
|
u", helpers=helpers, partials=partials, root=root))\n"
|
|
])
|
|
|
|
def add_partial(self, symbol, arguments):
|
|
arg = ""
|
|
|
|
overrides = None
|
|
positional_args = 0
|
|
if arguments:
|
|
for argument in arguments:
|
|
kwmatch = re.match(r'(\w+)=(.+)$', argument)
|
|
if kwmatch:
|
|
if not overrides:
|
|
overrides = {}
|
|
overrides[kwmatch.group(1)] = kwmatch.group(2)
|
|
else:
|
|
if positional_args != 0:
|
|
raise PybarsError("An extra positional argument was passed to a partial")
|
|
positional_args += 1
|
|
arg = argument
|
|
|
|
overrides_literal = 'None'
|
|
if overrides:
|
|
overrides_literal = u'{'
|
|
for key in overrides:
|
|
overrides_literal += u'"%s": %s, ' % (key, overrides[key])
|
|
overrides_literal += u'}'
|
|
self._result.grow([u" overrides = %s\n" % overrides_literal])
|
|
|
|
self._result.grow([
|
|
u" partialName = %s\n" % symbol,
|
|
u" if partialName not in partials:\n",
|
|
u" raise PybarsError('The partial %s could not be found' % partialName)\n",
|
|
u" inner = partials[partialName]\n",
|
|
u" scope = Scope(%s, context, root, overrides=overrides)\n" % self._lookup_arg(arg)])
|
|
self._invoke_template("inner", "scope")
|
|
|
|
|
|
class Compiler:
|
|
|
|
"""A handlebars template compiler.
|
|
|
|
The compiler is not threadsafe: you need one per thread because of the
|
|
state in CodeBuilder.
|
|
"""
|
|
|
|
_handlebars = OMeta.makeGrammar(handlebars_grammar, {}, 'handlebars')
|
|
_builder = CodeBuilder()
|
|
_compiler = OMeta.makeGrammar(compile_grammar, {'builder': _builder})
|
|
|
|
def __init__(self):
|
|
self._helpers = {}
|
|
self.template_counter = 1
|
|
|
|
def _extract_word(self, source, position):
|
|
"""
|
|
Extracts the word that falls at or around a specific position
|
|
|
|
:param source:
|
|
The template source as a unicode string
|
|
:param position:
|
|
The position where the word falls
|
|
|
|
:return:
|
|
The word
|
|
"""
|
|
boundry = re.search(r'{{|{|\s|$', source[:position][::-1])
|
|
start_offset = boundry.end() if boundry.group(0).startswith('{') else boundry.start()
|
|
|
|
boundry = re.search(r'}}|}|\s|$', source[position:])
|
|
end_offset = boundry.end() if boundry.group(0).startswith('}') else boundry.start()
|
|
|
|
return source[position - start_offset:position + end_offset]
|
|
|
|
def _generate_code(self, source):
|
|
"""
|
|
Common compilation code shared between precompile() and compile()
|
|
|
|
:param source:
|
|
The template source as a unicode string
|
|
|
|
:return:
|
|
A tuple of (function, source_code)
|
|
"""
|
|
|
|
if not isinstance(source, str_class):
|
|
raise PybarsError("Template source must be a unicode string")
|
|
|
|
source = self.whitespace_control(source)
|
|
|
|
tree, (position, _) = self._handlebars(source).apply('template')
|
|
|
|
if debug:
|
|
print('\nAST')
|
|
print('---')
|
|
print(tree)
|
|
print('')
|
|
|
|
if position < len(source):
|
|
line_num = source.count('\n') + 1
|
|
beginning_of_line = source.rfind('\n', 0, position)
|
|
if beginning_of_line == -1:
|
|
char_num = position
|
|
else:
|
|
char_num = position - beginning_of_line
|
|
word = self._extract_word(source, position)
|
|
raise PybarsError("Error at character %s of line %s near %s" % (char_num, line_num, word))
|
|
|
|
# Ensure the builder is in a clean state - kinda gross
|
|
self._compiler.globals['builder']._reset()
|
|
|
|
output = self._compiler(tree).apply('compile')[0]
|
|
return output
|
|
|
|
def whitespace_control(self, source):
|
|
"""
|
|
Preprocess source to handle whitespace control and remove extra block
|
|
whitespaces.
|
|
|
|
:param source:
|
|
The template source as a unicode string
|
|
:return:
|
|
The processed template source as a unicode string
|
|
"""
|
|
cleanup_sub = re.compile(
|
|
# Clean-up whitespace control marks and spaces between blocks tags
|
|
r'(?<={{)~|~(?=}})|(?<=}})[ \t]+(?={{)').sub
|
|
|
|
return re.sub(
|
|
# Whitespace control using "~" mark
|
|
r'~}}\s*|\s*{{~|'
|
|
|
|
# Whitespace around alone blocks tags that in a line
|
|
r'(?<=\n)([ \t]*{{(#[^{}]+|/[^{}]+|![^{}]+|else|else if [^{}]+)}}[ \t]*)+\r?\n|'
|
|
|
|
# Whitespace aroud alone blocks tag on the first line
|
|
r'^([ \t]*{{(#[^{}]+|![^{}]+)}}[ \t]*)+\r?\n|'
|
|
|
|
# Whitespace aroud alone blocks tag on the last line
|
|
r'\r?\n([ \t]*{{(/[^{}]+|![^{}]+)}}[ \t]*)+$',
|
|
lambda match: cleanup_sub('', match.group(0).strip()), source)
|
|
|
|
def precompile(self, source):
|
|
"""
|
|
Generates python source code that can be saved to a file for caching
|
|
|
|
:param source:
|
|
The template to generate source for - should be a unicode string
|
|
|
|
:return:
|
|
Python code as a unicode string
|
|
"""
|
|
|
|
return self._generate_code(source).full_code
|
|
|
|
def compile(self, source, path=None):
|
|
"""Compile source to a ready to run template.
|
|
|
|
:param source:
|
|
The template to compile - should be a unicode string
|
|
|
|
:return:
|
|
A template function ready to execute
|
|
"""
|
|
|
|
container = self._generate_code(source)
|
|
|
|
def make_module_name(name, suffix=None):
|
|
output = 'pybars._templates.%s' % name
|
|
if suffix:
|
|
output += '_%s' % suffix
|
|
return output
|
|
|
|
if not path:
|
|
path = '_template'
|
|
generate_name = True
|
|
else:
|
|
path = path.replace('\\', '/')
|
|
path = path.replace('/', '_')
|
|
mod_name = make_module_name(path)
|
|
generate_name = mod_name in sys.modules
|
|
|
|
if generate_name:
|
|
mod_name = make_module_name(path, self.template_counter)
|
|
while mod_name in sys.modules:
|
|
self.template_counter += 1
|
|
mod_name = make_module_name(path, self.template_counter)
|
|
|
|
mod = ModuleType(mod_name)
|
|
filename = '%s.py' % mod_name.replace('pybars.', '').replace('.', '/')
|
|
exec(compile(container.full_code, filename, 'exec', dont_inherit=True), mod.__dict__)
|
|
sys.modules[mod_name] = mod
|
|
linecache.getlines(filename, mod.__dict__)
|
|
|
|
return mod.__dict__[container.name]
|
|
|
|
def template(self, code):
|
|
def _render(context, helpers=None, partials=None, root=None):
|
|
ns = {
|
|
'context': context,
|
|
'helpers': helpers,
|
|
'partials': partials,
|
|
'root': root
|
|
}
|
|
exec(code + '\nresult = render(context, helpers=helpers, partials=partials, root=root)', ns)
|
|
return ns['result']
|
|
return _render
|