Commit c8a1f2e1 by Chris Jerdonek

Merge branch 'decouple-parser' into 'development':

Parsing a template no longer requires a RenderEngine instance.
parents 7eef0a68 5dd7fa7c
......@@ -38,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding()
# strings that arise from files.
FILE_ENCODING = sys.getdefaultencoding()
# The delimiters to start with when parsing.
DELIMITERS = (u'{{', u'}}')
# How to handle missing tags when rendering a template.
MISSING_TAGS = MissingTags.ignore
......
......@@ -38,18 +38,17 @@ class ParsedTemplate(object):
def add(self, node):
self._parse_tree.append(node)
def render(self, context):
def render(self, engine, context):
"""
Returns: a string of type unicode.
"""
# We avoid use of the ternary operator for Python 2.4 support.
def get_unicode(val):
if callable(val):
return val(context)
if type(val) is unicode:
return val
return val.render(engine, context)
parts = map(get_unicode, self._parse_tree)
s = ''.join(parts)
return unicode(s)
# coding: utf-8
"""
Provides a class for parsing template strings.
This module is only meant for internal use by the renderengine module.
Exposes a parse() function to parse template strings.
"""
import re
from pystache.defaults import DELIMITERS
from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = (u'{{', u'}}')
END_OF_LINE_CHARACTERS = [u'\r', u'\n']
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
def _compile_template_re(delimiters=None):
# TODO: add some unit tests for this.
def parse(template, delimiters=None):
"""
Return a regular expresssion object (re.RegexObject) instance.
Parse a unicode template string and return a ParsedTemplate instance.
Arguments:
template: a unicode template string.
delimiters: a 2-tuple of delimiters. Defaults to the package default.
"""
if delimiters is None:
delimiters = DEFAULT_DELIMITERS
parser = _Parser(delimiters)
return parser.parse(template)
def _compile_template_re(delimiters):
"""
Return a regular expresssion object (re.RegexObject) instance.
"""
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
tag_types = "!>&/#^"
......@@ -52,25 +64,131 @@ class ParsingError(Exception):
pass
class Parser(object):
## Node types
_delimiters = None
_template_re = None
def __init__(self, engine, delimiters=None):
"""
Construct an instance.
class _CommentNode(object):
Arguments:
def render(self, engine, context):
return u''
engine: a RenderEngine instance.
"""
class _ChangeNode(object):
def __init__(self, delimiters):
self.delimiters = delimiters
def render(self, engine, context):
return u''
class _TagNode(object):
def __init__(self, key):
self.key = key
def render(self, engine, context):
s = engine.fetch_string(context, self.key)
return engine.escape(s)
class _LiteralNode(object):
def __init__(self, key):
self.key = key
def render(self, engine, context):
s = engine.fetch_string(context, self.key)
return engine.literal(s)
class _PartialNode(object):
def __init__(self, key, indent):
self.key = key
self.indent = indent
def render(self, engine, context):
template = engine.resolve_partial(self.key)
# Indent before rendering.
template = re.sub(NON_BLANK_RE, self.indent + ur'\1', template)
return engine.render(template, context)
class _InvertedNode(object):
def __init__(self, key, parsed_section):
self.key = key
self.parsed_section = parsed_section
def render(self, engine, context):
# TODO: is there a bug because we are not using the same
# logic as in fetch_string()?
data = engine.resolve_context(context, self.key)
# Note that lambdas are considered truthy for inverted sections
# per the spec.
if data:
return u''
return engine.render_parsed(self.parsed_section, context)
class _SectionNode(object):
# 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 __init__(self, key, parsed_contents, delimiters, template, section_begin_index, section_end_index):
self.delimiters = delimiters
self.key = key
self.parsed_contents = parsed_contents
self.template = template
self.section_begin_index = section_begin_index
self.section_end_index = section_end_index
def render(self, engine, context):
data = engine.fetch_section_data(context, self.key)
parts = []
for val in data:
if callable(val):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
val = val(self.template[self.section_begin_index:self.section_end_index])
val = engine._render_value(val, context, delimiters=self.delimiters)
parts.append(val)
continue
context.push(val)
parts.append(engine.render_parsed(self.parsed_contents, context))
context.pop()
return unicode(''.join(parts))
class _Parser(object):
_delimiters = None
_template_re = None
def __init__(self, delimiters=None):
if delimiters is None:
delimiters = DEFAULT_DELIMITERS
delimiters = DELIMITERS
self._delimiters = delimiters
self.engine = engine
def _compile_delimiters(self):
self._template_re = _compile_template_re(self._delimiters)
......@@ -172,7 +290,8 @@ class Parser(object):
parsed_template.add(node)
# Add the remainder of the template.
# Avoid adding spurious empty strings to the parse tree.
if start_index != len(template):
parsed_template.add(template[start_index:])
return parsed_template
......@@ -184,21 +303,21 @@ class Parser(object):
"""
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if tag_type == '!':
return u''
return _CommentNode()
if tag_type == '=':
delimiters = tag_key.split()
self._change_delimiters(delimiters)
return u''
return _ChangeNode(delimiters)
if tag_type == '':
return self.engine._make_get_escaped(tag_key)
return _TagNode(tag_key)
if tag_type == '&':
return self.engine._make_get_literal(tag_key)
return _LiteralNode(tag_key)
if tag_type == '>':
return self.engine._make_get_partial(tag_key, leading_whitespace)
return _PartialNode(tag_key, leading_whitespace)
raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type))
......@@ -209,10 +328,10 @@ class Parser(object):
"""
if tag_type == '#':
return self.engine._make_get_section(tag_key, parsed_section, self._delimiters,
return _SectionNode(tag_key, parsed_section, self._delimiters,
template, section_start_index, section_end_index)
if tag_type == '^':
return self.engine._make_get_inverse(tag_key, parsed_section)
return _InvertedNode(tag_key, parsed_section)
raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
......@@ -8,10 +8,7 @@ Defines a class responsible for rendering logic.
import re
from pystache.common import is_string
from pystache.parser import Parser
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
from pystache.parser import parse
def context_get(stack, name):
......@@ -90,12 +87,12 @@ class RenderEngine(object):
# The returned value MUST be rendered against the default delimiters,
# then interpolated in place of the lambda.
#
def _get_string_value(self, context, tag_name):
def fetch_string(self, context, name):
"""
Get a value from the given context as a basestring instance.
"""
val = self.resolve_context(context, tag_name)
val = self.resolve_context(context, name)
if callable(val):
# Return because _render_value() is already a string.
......@@ -106,72 +103,7 @@ class RenderEngine(object):
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)
return self.literal(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)
return self.escape(s)
return get_escaped
def _make_get_partial(self, tag_key, leading_whitespace):
template = self.resolve_partial(tag_key)
# Indent before rendering.
template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
def get_partial(context):
"""
Returns: a string of type unicode.
"""
# TODO: can we do the parsing before calling this function?
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.
"""
# TODO: is there a bug because we are not using the same
# logic as in _get_string_value()?
data = self.resolve_context(context, name)
# Per the spec, lambdas in inverted sections are considered truthy.
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, delims,
template, section_start_index, section_end_index):
def get_section_value(context):
"""
Returns: a string of type unicode.
"""
def fetch_section_data(self, context, name):
data = self.resolve_context(context, name)
# From the spec:
......@@ -204,36 +136,7 @@ class RenderEngine(object):
data = [data]
# Otherwise, treat the value as a list.
parts = []
for val in data:
if callable(val):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
val = val(template[section_start_index:section_end_index])
val = self._render_value(val, context, delimiters=delims)
parts.append(val)
continue
context.push(val)
parts.append(parsed_template.render(context))
context.pop()
return unicode(''.join(parts))
return get_section_value
return data
def _render_value(self, val, context, delimiters=None):
"""
......@@ -247,6 +150,9 @@ class RenderEngine(object):
val = self.literal(val)
return self.render(val, context, delimiters)
def render_parsed(self, parsed_template, context_stack):
return parsed_template.render(self, context_stack)
def render(self, template, context_stack, delimiters=None):
"""
Render a unicode template string, and return as unicode.
......@@ -259,7 +165,6 @@ class RenderEngine(object):
context_stack: a ContextStack instance.
"""
parser = Parser(self, delimiters=delimiters)
parsed_template = parser.parse(template)
parsed_template = parse(template, delimiters)
return parsed_template.render(context_stack)
return self.render_parsed(parsed_template, context_stack)
......@@ -7,6 +7,7 @@ Unit tests of parser.py.
import unittest
from pystache.defaults import DELIMITERS
from pystache.parser import _compile_template_re as make_re
......@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase):
Test getting a key from a dictionary.
"""
re = make_re()
re = make_re(DELIMITERS)
match = re.search("b {{test}}")
self.assertEqual(match.start(), 1)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment