| Index: third_party/pystache/src/parser.py
|
| diff --git a/third_party/pystache/src/parser.py b/third_party/pystache/src/parser.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..9a4fba235b07fa05992bf67e2d8ae56d54dd0842
|
| --- /dev/null
|
| +++ b/third_party/pystache/src/parser.py
|
| @@ -0,0 +1,378 @@
|
| +# coding: utf-8
|
| +
|
| +"""
|
| +Exposes a parse() function to parse template strings.
|
| +
|
| +"""
|
| +
|
| +import re
|
| +
|
| +from pystache import defaults
|
| +from pystache.parsed import ParsedTemplate
|
| +
|
| +
|
| +END_OF_LINE_CHARACTERS = [u'\r', u'\n']
|
| +NON_BLANK_RE = re.compile(ur'^(.)', re.M)
|
| +
|
| +
|
| +# 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):
|
| + """
|
| + 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 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 expression object (re.RegexObject) instance.
|
| +
|
| + """
|
| + # The possible tag type characters following the opening tag,
|
| + # excluding "=" and "{".
|
| + tag_types = "!>&/#^"
|
| +
|
| + # TODO: are we following this in the spec?
|
| + #
|
| + # The tag's content MUST be a non-whitespace character sequence
|
| + # NOT containing the current closing delimiter.
|
| + #
|
| + tag = r"""
|
| + (?P<whitespace>[\ \t]*)
|
| + %(otag)s \s*
|
| + (?:
|
| + (?P<change>=) \s* (?P<delims>.+?) \s* = |
|
| + (?P<raw>{) \s* (?P<raw_name>.+?) \s* } |
|
| + (?P<tag>[%(tag_types)s]?) \s* (?P<tag_key>[\s\S]+?)
|
| + )
|
| + \s* %(ctag)s
|
| + """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])}
|
| +
|
| + return re.compile(tag, re.VERBOSE)
|
| +
|
| +
|
| +class ParsingError(Exception):
|
| +
|
| + pass
|
| +
|
| +
|
| +## Node types
|
| +
|
| +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))
|
| +
|
| +
|
| +class _CommentNode(object):
|
| +
|
| + 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 = defaults.DELIMITERS
|
| +
|
| + self._delimiters = delimiters
|
| +
|
| + def _compile_delimiters(self):
|
| + self._template_re = _compile_template_re(self._delimiters)
|
| +
|
| + def _change_delimiters(self, delimiters):
|
| + self._delimiters = delimiters
|
| + self._compile_delimiters()
|
| +
|
| + def parse(self, template):
|
| + """
|
| + Parse a template string starting at some index.
|
| +
|
| + This method uses the current tag delimiter.
|
| +
|
| + Arguments:
|
| +
|
| + template: a unicode string that is the template to parse.
|
| +
|
| + index: the index at which to start parsing.
|
| +
|
| + Returns:
|
| +
|
| + a ParsedTemplate instance.
|
| +
|
| + """
|
| + 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, start_index)
|
| +
|
| + if match is None:
|
| + break
|
| +
|
| + match_index = match.start()
|
| + end_index = match.end()
|
| +
|
| + matches = match.groupdict()
|
| +
|
| + # Normalize the matches dictionary.
|
| + if matches['change'] is not None:
|
| + matches.update(tag='=', tag_key=matches['delims'])
|
| + elif matches['raw'] is not None:
|
| + matches.update(tag='&', tag_key=matches['raw_name'])
|
| +
|
| + tag_type = matches['tag']
|
| + tag_key = matches['tag_key']
|
| + leading_whitespace = matches['whitespace']
|
| +
|
| + # Standalone (non-interpolation) tags consume the entire line,
|
| + # both leading whitespace and trailing newline.
|
| + did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS
|
| + did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS
|
| + is_tag_interpolating = tag_type in ['', '&']
|
| +
|
| + if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating:
|
| + if end_index < len(template):
|
| + end_index += template[end_index] == '\r' and 1 or 0
|
| + if end_index < len(template):
|
| + end_index += template[end_index] == '\n' and 1 or 0
|
| + elif leading_whitespace:
|
| + match_index += len(leading_whitespace)
|
| + leading_whitespace = ''
|
| +
|
| + # Avoid adding spurious empty strings to the parse tree.
|
| + if start_index != match_index:
|
| + parsed_template.add(template[start_index:match_index])
|
| +
|
| + start_index = end_index
|
| +
|
| + if tag_type in ('#', '^'):
|
| + # Cache current state.
|
| + state = (tag_type, end_index, section_key, parsed_template)
|
| + states.append(state)
|
| +
|
| + # Initialize new state
|
| + section_key, parsed_template = tag_key, ParsedTemplate()
|
| + continue
|
| +
|
| + if tag_type == '/':
|
| + if tag_key != section_key:
|
| + raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
|
| +
|
| + # Restore previous state with newly found section data.
|
| + parsed_section = parsed_template
|
| +
|
| + (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)
|
| +
|
| + else:
|
| + node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace)
|
| +
|
| + parsed_template.add(node)
|
| +
|
| + # Avoid adding spurious empty strings to the parse tree.
|
| + if start_index != len(template):
|
| + parsed_template.add(template[start_index:])
|
| +
|
| + return parsed_template
|
| +
|
| + def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace):
|
| + """
|
| + 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 _CommentNode()
|
| +
|
| + if tag_type == '=':
|
| + delimiters = tag_key.split()
|
| + self._change_delimiters(delimiters)
|
| + return _ChangeNode(delimiters)
|
| +
|
| + if tag_type == '':
|
| + return _EscapeNode(tag_key)
|
| +
|
| + if tag_type == '&':
|
| + return _LiteralNode(tag_key)
|
| +
|
| + if tag_type == '>':
|
| + return _PartialNode(tag_key, leading_whitespace)
|
| +
|
| + raise Exception("Invalid symbol for interpolation tag: %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.
|
| +
|
| + """
|
| + if tag_type == '#':
|
| + return _SectionNode(tag_key, parsed_section, self._delimiters,
|
| + template, section_start_index, section_end_index)
|
| +
|
| + if tag_type == '^':
|
| + return _InvertedNode(tag_key, parsed_section)
|
| +
|
| + raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
|
|
|