blob: c797b1765a5ae09ca1c6bd9cd3adeb6a219ea913 [file] [log] [blame]
# coding: utf-8
"""
Defines a class responsible for rendering logic.
"""
import re
from pystache.common import is_string
from pystache.parser import parse
def context_get(stack, name):
"""
Find and return a name from a ContextStack instance.
"""
return stack.get(name)
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).
"""
# TODO: it would probably be better for the constructor to accept
# and set as an attribute a single RenderResolver instance
# that encapsulates the customizable aspects of converting
# strings and resolving partials and names from context.
def __init__(self, literal=None, escape=None, resolve_context=None,
resolve_partial=None, to_str=None):
"""
Arguments:
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 markupsafe.Markup differently
from plain unicode strings.
resolve_context: the function to call to resolve a name against
a context stack. The function should accept two positional
arguments: a ContextStack instance and a name to resolve.
resolve_partial: the function to call when loading a partial.
The function should accept a template name string and return a
template string of type unicode (not a subclass).
to_str: a function that accepts an object and returns a string (e.g.
the built-in function str). This function is used for string
coercion whenever a string is required (e.g. for converting None
or 0 to a string).
"""
self.escape = escape
self.literal = literal
self.resolve_context = resolve_context
self.resolve_partial = resolve_partial
self.to_str = to_str
# TODO: Rename context to stack throughout this module.
# From 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.
#
def fetch_string(self, context, name):
"""
Get a value from the given context as a basestring instance.
"""
val = self.resolve_context(context, name)
if callable(val):
# Return because _render_value() is already a string.
return self._render_value(val(), context)
if not is_string(val):
return self.to_str(val)
return val
def fetch_section_data(self, context, name):
"""
Fetch the value of a section as a list.
"""
data = self.resolve_context(context, name)
# From the spec:
#
# If the data is not of a list type, it is coerced into a list
# as follows: if the data is truthy (e.g. `!!data == true`),
# use a single-element list containing the data, otherwise use
# an empty list.
#
if not data:
data = []
else:
# The least brittle way to determine whether something
# supports iteration is by trying to call iter() on it:
#
# http://docs.python.org/library/functions.html#iter
#
# It is not sufficient, for example, to check whether the item
# implements __iter__ () (the iteration protocol). There is
# also __getitem__() (the sequence protocol). In Python 2,
# strings do not implement __iter__(), but in Python 3 they do.
try:
iter(data)
except TypeError:
# Then the value does not support iteration.
data = [data]
else:
if is_string(data) or isinstance(data, dict):
# Do not treat strings and dicts (which are iterable) as lists.
data = [data]
# Otherwise, treat the value as a list.
return data
def _render_value(self, val, context, delimiters=None):
"""
Render an arbitrary value.
"""
if not is_string(val):
# In case the template is an integer, for example.
val = self.to_str(val)
if type(val) is not unicode:
val = self.literal(val)
return self.render(val, context, delimiters)
def render(self, template, context_stack, delimiters=None):
"""
Render a unicode template string, and return as unicode.
Arguments:
template: a template string of type unicode (but not a proper
subclass of unicode).
context_stack: a ContextStack instance.
"""
parsed_template = parse(template, delimiters)
return parsed_template.render(self, context_stack)