183 lines
6.6 KiB
Python
183 lines
6.6 KiB
Python
# coding: utf-8
|
|
|
|
"""
|
|
Defines a class responsible for rendering logic.
|
|
|
|
"""
|
|
|
|
import re
|
|
|
|
from pystache.common import is_string
|
|
from pystache.parser import parse
|
|
import collections
|
|
|
|
|
|
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 isinstance(val, collections.Callable):
|
|
# 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 str:
|
|
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)
|