summaryrefslogtreecommitdiff
path: root/pystache/parser.py
diff options
context:
space:
mode:
Diffstat (limited to 'pystache/parser.py')
-rw-r--r--pystache/parser.py333
1 files changed, 237 insertions, 96 deletions
diff --git a/pystache/parser.py b/pystache/parser.py
index 4e05f3b..c6a171f 100644
--- a/pystache/parser.py
+++ b/pystache/parser.py
@@ -1,31 +1,51 @@
# 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.common import TemplateNotFoundError
+from pystache import defaults
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.
+# TODO: add a test case that checks for spurious spaces.
+# TODO: add test cases for delimiters.
+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.
+
+ Examples:
+
+ >>> parsed = parse(u"Hey {{#who}}{{name}}!{{/who}}")
+ >>> print str(parsed).replace('u', '') # This is a hack to get the test to pass both in Python 2 and 3.
+ ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_EscapeNode(key='name'), '!'])]
"""
- if delimiters is None:
- delimiters = DEFAULT_DELIMITERS
+ if type(template) is not unicode:
+ raise Exception("Template is not unicode: %s" % type(template))
+ 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 = "!>&/#^"
@@ -54,34 +74,172 @@ class ParsingError(Exception):
pass
-class Parser(object):
+## Node types
- _delimiters = None
- _template_re = None
+def _format(obj, exclude=None):
+ if exclude is None:
+ exclude = []
+ exclude.append('key')
+ attrs = obj.__dict__
+ names = list(set(attrs.keys()) - set(exclude))
+ names.sort()
+ names.insert(0, 'key')
+ args = ["%s=%s" % (name, repr(attrs[name])) for name in names]
+ return "%s(%s)" % (obj.__class__.__name__, ", ".join(args))
- def __init__(self, engine, delimiters=None):
- """
- Construct an instance.
- Arguments:
+class _CommentNode(object):
- engine: a RenderEngine instance.
+ def __repr__(self):
+ return _format(self)
- """
+ def render(self, engine, context):
+ return u''
+
+
+class _ChangeNode(object):
+
+ def __init__(self, delimiters):
+ self.delimiters = delimiters
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ return u''
+
+
+class _EscapeNode(object):
+
+ def __init__(self, key):
+ self.key = key
+
+ def __repr__(self):
+ return _format(self)
+
+ 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 __repr__(self):
+ return _format(self)
+
+ 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 __repr__(self):
+ return _format(self)
+
+ 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 __repr__(self):
+ return _format(self)
+
+ 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 self.parsed_section.render(engine, 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, delimiters, template, index_begin, index_end):
+ self.delimiters = delimiters
+ self.key = key
+ self.parsed = parsed
+ self.template = template
+ self.index_begin = index_begin
+ self.index_end = index_end
+
+ def __repr__(self):
+ return _format(self, exclude=['delimiters', 'template'])
+
+ def render(self, engine, context):
+ values = engine.fetch_section_data(context, self.key)
+
+ parts = []
+ for val in values:
+ 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.index_begin:self.index_end])
+ val = engine._render_value(val, context, delimiters=self.delimiters)
+ parts.append(val)
+ continue
+
+ context.push(val)
+ parts.append(self.parsed.render(engine, 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 = defaults.DELIMITERS
self._delimiters = delimiters
- self.engine = engine
- def compile_template_re(self):
+ def _compile_delimiters(self):
self._template_re = _compile_template_re(self._delimiters)
def _change_delimiters(self, delimiters):
self._delimiters = delimiters
- self.compile_template_re()
+ self._compile_delimiters()
- def parse(self, template, start_index=0, section_key=None):
+ def parse(self, template):
"""
Parse a template string starting at some index.
@@ -98,11 +256,16 @@ class Parser(object):
a ParsedTemplate instance.
"""
- parse_tree = []
- index = start_index
+ self._compile_delimiters()
+
+ start_index = 0
+ content_end_index, parsed_section, section_key = None, None, None
+ parsed_template = ParsedTemplate()
+
+ states = []
while True:
- match = self._template_re.search(template, index)
+ match = self._template_re.search(template, start_index)
if match is None:
break
@@ -110,10 +273,6 @@ class Parser(object):
match_index = match.start()
end_index = match.end()
- before_tag = template[index : match_index]
-
- parse_tree.append(before_tag)
-
matches = match.groupdict()
# Normalize the matches dictionary.
@@ -138,100 +297,82 @@ class Parser(object):
if end_index < len(template):
end_index += template[end_index] == '\n' and 1 or 0
elif leading_whitespace:
- parse_tree.append(leading_whitespace)
match_index += len(leading_whitespace)
leading_whitespace = ''
- if tag_type == '/':
- if tag_key != section_key:
- raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != match_index:
+ parsed_template.add(template[start_index:match_index])
- return ParsedTemplate(parse_tree), match_index, end_index
+ start_index = end_index
- index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
+ if tag_type in ('#', '^'):
+ # Cache current state.
+ state = (tag_type, end_index, section_key, parsed_template)
+ states.append(state)
- # Save the rest of the template.
- parse_tree.append(template[index:])
+ # Initialize new state
+ section_key, parsed_template = tag_key, ParsedTemplate()
+ continue
- return ParsedTemplate(parse_tree)
-
- def _parse_section(self, template, start_index, section_key):
- """
- Parse the contents of a template section.
-
- Arguments:
-
- template: a unicode template string.
+ if tag_type == '/':
+ if tag_key != section_key:
+ raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
- start_index: the string index at which the section contents begin.
+ # Restore previous state with newly found section data.
+ parsed_section = parsed_template
- section_key: the tag key of the section.
+ (tag_type, section_start_index, section_key, parsed_template) = states.pop()
+ node = self._make_section_node(template, tag_type, tag_key, parsed_section,
+ section_start_index, match_index)
- Returns: a 3-tuple:
+ else:
+ node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace)
- parsed_section: the section contents parsed as a ParsedTemplate
- instance.
+ parsed_template.add(node)
- content_end_index: the string index after the section contents.
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != len(template):
+ parsed_template.add(template[start_index:])
- end_index: the string index after the closing section tag (and
- including any trailing newlines).
+ return parsed_template
+ def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace):
"""
- parsed_section, content_end_index, end_index = \
- self.parse(template=template, start_index=start_index, section_key=section_key)
-
- return parsed_section, template[start_index:content_end_index], end_index
-
- def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
+ Create and return a non-section node for the parse tree.
+ """
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if tag_type == '!':
- return end_index
+ return _CommentNode()
if tag_type == '=':
delimiters = tag_key.split()
self._change_delimiters(delimiters)
- return end_index
-
- engine = self.engine
+ return _ChangeNode(delimiters)
if tag_type == '':
+ return _EscapeNode(tag_key)
- func = engine._make_get_escaped(tag_key)
-
- elif tag_type == '&':
+ if tag_type == '&':
+ return _LiteralNode(tag_key)
- func = engine._make_get_literal(tag_key)
+ if tag_type == '>':
+ return _PartialNode(tag_key, leading_whitespace)
- elif tag_type == '#':
+ raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type))
- parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
- func = engine._make_get_section(tag_key, parsed_section, section_contents, self._delimiters)
-
- elif tag_type == '^':
-
- parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
- func = engine._make_get_inverse(tag_key, parsed_section)
-
- elif tag_type == '>':
-
- try:
- # TODO: make engine.load() and test it separately.
- template = engine.load_partial(tag_key)
- except TemplateNotFoundError:
- template = u''
-
- # Indent before rendering.
- template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
-
- func = engine._make_get_partial(template)
-
- else:
-
- raise Exception("Unrecognized tag type: %s" % repr(tag_type))
+ def _make_section_node(self, template, tag_type, tag_key, parsed_section,
+ section_start_index, section_end_index):
+ """
+ Create and return a section node for the parse tree.
- parse_tree.append(func)
+ """
+ if tag_type == '#':
+ return _SectionNode(tag_key, parsed_section, self._delimiters,
+ template, section_start_index, section_end_index)
- return end_index
+ if tag_type == '^':
+ return _InvertedNode(tag_key, parsed_section)
+ raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))