Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(89)

Side by Side Diff: third_party/pystache/src/parser.py

Issue 2962783004: Adding pystache to third_party (Closed)
Patch Set: Merge branch 'master' into mustache Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « third_party/pystache/src/parsed.py ('k') | third_party/pystache/src/renderengine.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # coding: utf-8
2
3 """
4 Exposes a parse() function to parse template strings.
5
6 """
7
8 import re
9
10 from pystache import defaults
11 from pystache.parsed import ParsedTemplate
12
13
14 END_OF_LINE_CHARACTERS = [u'\r', u'\n']
15 NON_BLANK_RE = re.compile(ur'^(.)', re.M)
16
17
18 # TODO: add some unit tests for this.
19 # TODO: add a test case that checks for spurious spaces.
20 # TODO: add test cases for delimiters.
21 def parse(template, delimiters=None):
22 """
23 Parse a unicode template string and return a ParsedTemplate instance.
24
25 Arguments:
26
27 template: a unicode template string.
28
29 delimiters: a 2-tuple of delimiters. Defaults to the package default.
30
31 Examples:
32
33 >>> parsed = parse(u"Hey {{#who}}{{name}}!{{/who}}")
34 >>> print str(parsed).replace('u', '') # This is a hack to get the test to pass both in Python 2 and 3.
35 ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_Esca peNode(key='name'), '!'])]
36
37 """
38 if type(template) is not unicode:
39 raise Exception("Template is not unicode: %s" % type(template))
40 parser = _Parser(delimiters)
41 return parser.parse(template)
42
43
44 def _compile_template_re(delimiters):
45 """
46 Return a regular expression object (re.RegexObject) instance.
47
48 """
49 # The possible tag type characters following the opening tag,
50 # excluding "=" and "{".
51 tag_types = "!>&/#^"
52
53 # TODO: are we following this in the spec?
54 #
55 # The tag's content MUST be a non-whitespace character sequence
56 # NOT containing the current closing delimiter.
57 #
58 tag = r"""
59 (?P<whitespace>[\ \t]*)
60 %(otag)s \s*
61 (?:
62 (?P<change>=) \s* (?P<delims>.+?) \s* = |
63 (?P<raw>{) \s* (?P<raw_name>.+?) \s* } |
64 (?P<tag>[%(tag_types)s]?) \s* (?P<tag_key>[\s\S]+?)
65 )
66 \s* %(ctag)s
67 """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re. escape(delimiters[1])}
68
69 return re.compile(tag, re.VERBOSE)
70
71
72 class ParsingError(Exception):
73
74 pass
75
76
77 ## Node types
78
79 def _format(obj, exclude=None):
80 if exclude is None:
81 exclude = []
82 exclude.append('key')
83 attrs = obj.__dict__
84 names = list(set(attrs.keys()) - set(exclude))
85 names.sort()
86 names.insert(0, 'key')
87 args = ["%s=%s" % (name, repr(attrs[name])) for name in names]
88 return "%s(%s)" % (obj.__class__.__name__, ", ".join(args))
89
90
91 class _CommentNode(object):
92
93 def __repr__(self):
94 return _format(self)
95
96 def render(self, engine, context):
97 return u''
98
99
100 class _ChangeNode(object):
101
102 def __init__(self, delimiters):
103 self.delimiters = delimiters
104
105 def __repr__(self):
106 return _format(self)
107
108 def render(self, engine, context):
109 return u''
110
111
112 class _EscapeNode(object):
113
114 def __init__(self, key):
115 self.key = key
116
117 def __repr__(self):
118 return _format(self)
119
120 def render(self, engine, context):
121 s = engine.fetch_string(context, self.key)
122 return engine.escape(s)
123
124
125 class _LiteralNode(object):
126
127 def __init__(self, key):
128 self.key = key
129
130 def __repr__(self):
131 return _format(self)
132
133 def render(self, engine, context):
134 s = engine.fetch_string(context, self.key)
135 return engine.literal(s)
136
137
138 class _PartialNode(object):
139
140 def __init__(self, key, indent):
141 self.key = key
142 self.indent = indent
143
144 def __repr__(self):
145 return _format(self)
146
147 def render(self, engine, context):
148 template = engine.resolve_partial(self.key)
149 # Indent before rendering.
150 template = re.sub(NON_BLANK_RE, self.indent + ur'\1', template)
151
152 return engine.render(template, context)
153
154
155 class _InvertedNode(object):
156
157 def __init__(self, key, parsed_section):
158 self.key = key
159 self.parsed_section = parsed_section
160
161 def __repr__(self):
162 return _format(self)
163
164 def render(self, engine, context):
165 # TODO: is there a bug because we are not using the same
166 # logic as in fetch_string()?
167 data = engine.resolve_context(context, self.key)
168 # Note that lambdas are considered truthy for inverted sections
169 # per the spec.
170 if data:
171 return u''
172 return self.parsed_section.render(engine, context)
173
174
175 class _SectionNode(object):
176
177 # TODO: the template_ and parsed_template_ arguments don't both seem
178 # to be necessary. Can we remove one of them? For example, if
179 # callable(data) is True, then the initial parsed_template isn't used.
180 def __init__(self, key, parsed, delimiters, template, index_begin, index_end ):
181 self.delimiters = delimiters
182 self.key = key
183 self.parsed = parsed
184 self.template = template
185 self.index_begin = index_begin
186 self.index_end = index_end
187
188 def __repr__(self):
189 return _format(self, exclude=['delimiters', 'template'])
190
191 def render(self, engine, context):
192 values = engine.fetch_section_data(context, self.key)
193
194 parts = []
195 for val in values:
196 if callable(val):
197 # Lambdas special case section rendering and bypass pushing
198 # the data value onto the context stack. From the spec--
199 #
200 # When used as the data value for a Section tag, the
201 # lambda MUST be treatable as an arity 1 function, and
202 # invoked as such (passing a String containing the
203 # unprocessed section contents). The returned value
204 # MUST be rendered against the current delimiters, then
205 # interpolated in place of the section.
206 #
207 # Also see--
208 #
209 # https://github.com/defunkt/pystache/issues/113
210 #
211 # TODO: should we check the arity?
212 val = val(self.template[self.index_begin:self.index_end])
213 val = engine._render_value(val, context, delimiters=self.delimit ers)
214 parts.append(val)
215 continue
216
217 context.push(val)
218 parts.append(self.parsed.render(engine, context))
219 context.pop()
220
221 return unicode(''.join(parts))
222
223
224 class _Parser(object):
225
226 _delimiters = None
227 _template_re = None
228
229 def __init__(self, delimiters=None):
230 if delimiters is None:
231 delimiters = defaults.DELIMITERS
232
233 self._delimiters = delimiters
234
235 def _compile_delimiters(self):
236 self._template_re = _compile_template_re(self._delimiters)
237
238 def _change_delimiters(self, delimiters):
239 self._delimiters = delimiters
240 self._compile_delimiters()
241
242 def parse(self, template):
243 """
244 Parse a template string starting at some index.
245
246 This method uses the current tag delimiter.
247
248 Arguments:
249
250 template: a unicode string that is the template to parse.
251
252 index: the index at which to start parsing.
253
254 Returns:
255
256 a ParsedTemplate instance.
257
258 """
259 self._compile_delimiters()
260
261 start_index = 0
262 content_end_index, parsed_section, section_key = None, None, None
263 parsed_template = ParsedTemplate()
264
265 states = []
266
267 while True:
268 match = self._template_re.search(template, start_index)
269
270 if match is None:
271 break
272
273 match_index = match.start()
274 end_index = match.end()
275
276 matches = match.groupdict()
277
278 # Normalize the matches dictionary.
279 if matches['change'] is not None:
280 matches.update(tag='=', tag_key=matches['delims'])
281 elif matches['raw'] is not None:
282 matches.update(tag='&', tag_key=matches['raw_name'])
283
284 tag_type = matches['tag']
285 tag_key = matches['tag_key']
286 leading_whitespace = matches['whitespace']
287
288 # Standalone (non-interpolation) tags consume the entire line,
289 # both leading whitespace and trailing newline.
290 did_tag_begin_line = match_index == 0 or template[match_index - 1] i n END_OF_LINE_CHARACTERS
291 did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS
292 is_tag_interpolating = tag_type in ['', '&']
293
294 if did_tag_begin_line and did_tag_end_line and not is_tag_interpolat ing:
295 if end_index < len(template):
296 end_index += template[end_index] == '\r' and 1 or 0
297 if end_index < len(template):
298 end_index += template[end_index] == '\n' and 1 or 0
299 elif leading_whitespace:
300 match_index += len(leading_whitespace)
301 leading_whitespace = ''
302
303 # Avoid adding spurious empty strings to the parse tree.
304 if start_index != match_index:
305 parsed_template.add(template[start_index:match_index])
306
307 start_index = end_index
308
309 if tag_type in ('#', '^'):
310 # Cache current state.
311 state = (tag_type, end_index, section_key, parsed_template)
312 states.append(state)
313
314 # Initialize new state
315 section_key, parsed_template = tag_key, ParsedTemplate()
316 continue
317
318 if tag_type == '/':
319 if tag_key != section_key:
320 raise ParsingError("Section end tag mismatch: %s != %s" % (t ag_key, section_key))
321
322 # Restore previous state with newly found section data.
323 parsed_section = parsed_template
324
325 (tag_type, section_start_index, section_key, parsed_template) = states.pop()
326 node = self._make_section_node(template, tag_type, tag_key, pars ed_section,
327 section_start_index, match_index)
328
329 else:
330 node = self._make_interpolation_node(tag_type, tag_key, leading_ whitespace)
331
332 parsed_template.add(node)
333
334 # Avoid adding spurious empty strings to the parse tree.
335 if start_index != len(template):
336 parsed_template.add(template[start_index:])
337
338 return parsed_template
339
340 def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace):
341 """
342 Create and return a non-section node for the parse tree.
343
344 """
345 # TODO: switch to using a dictionary instead of a bunch of ifs and elifs .
346 if tag_type == '!':
347 return _CommentNode()
348
349 if tag_type == '=':
350 delimiters = tag_key.split()
351 self._change_delimiters(delimiters)
352 return _ChangeNode(delimiters)
353
354 if tag_type == '':
355 return _EscapeNode(tag_key)
356
357 if tag_type == '&':
358 return _LiteralNode(tag_key)
359
360 if tag_type == '>':
361 return _PartialNode(tag_key, leading_whitespace)
362
363 raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_ty pe))
364
365 def _make_section_node(self, template, tag_type, tag_key, parsed_section,
366 section_start_index, section_end_index):
367 """
368 Create and return a section node for the parse tree.
369
370 """
371 if tag_type == '#':
372 return _SectionNode(tag_key, parsed_section, self._delimiters,
373 template, section_start_index, section_end_index)
374
375 if tag_type == '^':
376 return _InvertedNode(tag_key, parsed_section)
377
378 raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
OLDNEW
« no previous file with comments | « third_party/pystache/src/parsed.py ('k') | third_party/pystache/src/renderengine.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698