summaryrefslogtreecommitdiff
path: root/pystache
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2012-05-05 17:21:21 -0700
committerChris Jerdonek <chris.jerdonek@gmail.com>2012-05-05 17:21:21 -0700
commitc8a1f2e13c6e483e992cdf5c78e6c9940331e791 (patch)
tree1ef5eb072129f7c2637f3bc78584bcec995e5c1f /pystache
parent7eef0a68507d1f01464d9ee980483ba2fca10c28 (diff)
parent5dd7fa7c8df7f186abda7ac463c44fc70953cc27 (diff)
downloadpystache-c8a1f2e13c6e483e992cdf5c78e6c9940331e791.tar.gz
Merge branch 'decouple-parser' into 'development':
Parsing a template no longer requires a RenderEngine instance.
Diffstat (limited to 'pystache')
-rw-r--r--pystache/defaults.py3
-rw-r--r--pystache/parsed.py9
-rw-r--r--pystache/parser.py177
-rw-r--r--pystache/renderengine.py171
-rw-r--r--pystache/tests/test_parser.py3
5 files changed, 195 insertions, 168 deletions
diff --git a/pystache/defaults.py b/pystache/defaults.py
index a510cfe..0a20328 100644
--- a/pystache/defaults.py
+++ b/pystache/defaults.py
@@ -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
diff --git a/pystache/parsed.py b/pystache/parsed.py
index d791be4..eb138cd 100644
--- a/pystache/parsed.py
+++ b/pystache/parsed.py
@@ -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)
- return val
+ 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)
-
diff --git a/pystache/parser.py b/pystache/parser.py
index 99f8d13..9bfc30c 100644
--- a/pystache/parser.py
+++ b/pystache/parser.py
@@ -1,29 +1,41 @@
# 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.
+
+ """
+ parser = _Parser(delimiters)
+ return parser.parse(template)
+
+def _compile_template_re(delimiters):
"""
- if delimiters is None:
- delimiters = DEFAULT_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,8 +290,9 @@ class Parser(object):
parsed_template.add(node)
- # Add the remainder of the template.
- parsed_template.add(template[start_index:])
+ # 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,
- template, section_start_index, section_end_index)
+ 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))
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index 16cd940..9259544 100644
--- a/pystache/renderengine.py
+++ b/pystache/renderengine.py
@@ -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,134 +103,40 @@ 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.
-
- """
- data = self.resolve_context(context, name)
-
- # From the spec:
+ def fetch_section_data(self, context, name):
+ 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:
#
- # 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.
+ # http://docs.python.org/library/functions.html#iter
#
- if not data:
- data = []
+ # 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:
- # 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.
+ if is_string(data) or isinstance(data, dict):
+ # Do not treat strings and dicts (which are iterable) as lists.
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.
-
- 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
+ # Otherwise, treat the value as a list.
+
+ 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)
diff --git a/pystache/tests/test_parser.py b/pystache/tests/test_parser.py
index 4aa0959..92248ea 100644
--- a/pystache/tests/test_parser.py
+++ b/pystache/tests/test_parser.py
@@ -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)