diff options
Diffstat (limited to 'pystache/renderengine.py')
-rw-r--r-- | pystache/renderengine.py | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/pystache/renderengine.py b/pystache/renderengine.py new file mode 100644 index 0000000..4361dca --- /dev/null +++ b/pystache/renderengine.py @@ -0,0 +1,236 @@ +# coding: utf-8 + +""" +Defines a class responsible for rendering logic. + +""" + +import re + +from parser import Parser + + +class RenderEngine(object): + + """ + Provides a render() method. + + This class is meant only for internal use. + + As a rule, the code in this class operates on unicode strings where + possible rather than, say, strings of type str or markupsafe.Markup. + This means that strings obtained from "external" sources like partials + and variable tag values are immediately converted to unicode (or + escaped and converted to unicode) before being operated on further. + This makes maintaining, reasoning about, and testing the correctness + of the code much simpler. In particular, it keeps the implementation + of this class independent of the API details of one (or possibly more) + unicode subclasses (e.g. markupsafe.Markup). + + """ + + def __init__(self, load_partial=None, literal=None, escape=None): + """ + Arguments: + + load_partial: the function to call when loading a partial. The + function should accept a string template name and return a + template string of type unicode (not a subclass). + + literal: the function used to convert unescaped variable tag + values to unicode, e.g. the value corresponding to a tag + "{{{name}}}". The function should accept a string of type + str or unicode (or a subclass) and return a string of type + unicode (but not a proper subclass of unicode). + This class will only pass basestring instances to this + function. For example, it will call str() on integer variable + values prior to passing them to this function. + + escape: the function used to escape and convert variable tag + values to unicode, e.g. the value corresponding to a tag + "{{name}}". The function should obey the same properties + described above for the "literal" function argument. + This function should take care to convert any str + arguments to unicode just as the literal function should, as + this class will not pass tag values to literal prior to passing + them to this function. This allows for more flexibility, + for example using a custom escape function that handles + incoming strings of type markupssafe.Markup differently + from plain unicode strings. + + """ + self.escape = escape + self.literal = literal + self.load_partial = load_partial + + def _get_string_value(self, context, tag_name): + """ + Get a value from the given context as a basestring instance. + + """ + val = context.get(tag_name) + + # We use "==" rather than "is" to compare integers, as using "is" + # relies on an implementation detail of CPython. The test about + # rendering zeroes failed while using PyPy when using "is". + # See issue #34: https://github.com/defunkt/pystache/issues/34 + if not val and val != 0: + if tag_name != '.': + return '' + val = context.top() + + if callable(val): + # According to the spec: + # + # When used as the data value for an Interpolation tag, + # the lambda MUST be treatable as an arity 0 function, + # and invoked as such. The returned value MUST be + # rendered against the default delimiters, then + # interpolated in place of the lambda. + template = val() + if not isinstance(template, basestring): + # In case the template is an integer, for example. + template = str(template) + if type(template) is not unicode: + template = self.literal(template) + val = self._render(template, context) + + if not isinstance(val, basestring): + val = str(val) + + return val + + def _make_get_literal(self, name): + def get_literal(context): + """ + Returns: a string of type unicode. + + """ + s = self._get_string_value(context, name) + s = self.literal(s) + return s + + return get_literal + + def _make_get_escaped(self, name): + get_literal = self._make_get_literal(name) + + def get_escaped(context): + """ + Returns: a string of type unicode. + + """ + s = self._get_string_value(context, name) + s = self.escape(s) + return s + + return get_escaped + + def _make_get_partial(self, template): + def get_partial(context): + """ + Returns: a string of type unicode. + + """ + return self._render(template, context) + + return get_partial + + def _make_get_inverse(self, name, parsed_template): + def get_inverse(context): + """ + Returns a string with type unicode. + + """ + data = context.get(name) + if data: + return u'' + return parsed_template.render(context) + + return get_inverse + + # TODO: the template_ and parsed_template_ arguments don't both seem + # to be necessary. Can we remove one of them? For example, if + # callable(data) is True, then the initial parsed_template isn't used. + def _make_get_section(self, name, parsed_template_, template_, delims): + def get_section(context): + """ + Returns: a string of type unicode. + + """ + template = template_ + parsed_template = parsed_template_ + data = context.get(name) + if not data: + data = [] + elif callable(data): + # TODO: should we check the arity? + template = data(template) + parsed_template = self._parse(template, delimiters=delims) + data = [ data ] + elif not hasattr(data, '__iter__') or isinstance(data, dict): + data = [ data ] + + parts = [] + for element in data: + context.push(element) + parts.append(parsed_template.render(context)) + context.pop() + + return unicode(''.join(parts)) + + return get_section + + def _parse(self, template, delimiters=None): + """ + Parse the given template, and return a ParsedTemplate instance. + + Arguments: + + template: a template string of type unicode. + + """ + parser = Parser(self, delimiters=delimiters) + parser.compile_template_re() + + return parser.parse(template=template) + + def _render(self, template, context): + """ + Returns: a string of type unicode. + + Arguments: + + template: a template string of type unicode. + context: a Context instance. + + """ + # We keep this type-check as an added check because this method is + # called with template strings coming from potentially externally- + # supplied functions like self.literal, self.load_partial, etc. + # Beyond this point, we have much better control over the type. + if type(template) is not unicode: + raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) + + parsed_template = self._parse(template) + + return parsed_template.render(context) + + def render(self, template, context): + """ + Return a template rendered as a string with type unicode. + + Arguments: + + template: a template string of type unicode (but not a proper + subclass of unicode). + + context: a Context instance. + + """ + # Be strict but not too strict. In other words, accept str instead + # of unicode, but don't assume anything about the encoding (e.g. + # don't use self.literal). + template = unicode(template) + + return self._render(template, context) |