diff options
Diffstat (limited to 'pystache/parser.py')
-rw-r--r-- | pystache/parser.py | 333 |
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)) |