##// END OF EJS Templates
templater: factor out runtemplate method...
Matt Mackall -
r17632:523625e4 default
parent child Browse files
Show More
@@ -1,405 +1,409 b''
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import sys, os
10 10 import util, config, templatefilters, parser, error
11 11
12 12 # template parsing
13 13
14 14 elements = {
15 15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 16 ",": (2, None, ("list", 2)),
17 17 "|": (5, None, ("|", 5)),
18 18 "%": (6, None, ("%", 6)),
19 19 ")": (0, None, None),
20 20 "symbol": (0, ("symbol",), None),
21 21 "string": (0, ("string",), None),
22 22 "end": (0, None, None),
23 23 }
24 24
25 25 def tokenizer(data):
26 26 program, start, end = data
27 27 pos = start
28 28 while pos < end:
29 29 c = program[pos]
30 30 if c.isspace(): # skip inter-token whitespace
31 31 pass
32 32 elif c in "(,)%|": # handle simple operators
33 33 yield (c, None, pos)
34 34 elif (c in '"\'' or c == 'r' and
35 35 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
36 36 if c == 'r':
37 37 pos += 1
38 38 c = program[pos]
39 39 decode = lambda x: x
40 40 else:
41 41 decode = lambda x: x.decode('string-escape')
42 42 pos += 1
43 43 s = pos
44 44 while pos < end: # find closing quote
45 45 d = program[pos]
46 46 if d == '\\': # skip over escaped characters
47 47 pos += 2
48 48 continue
49 49 if d == c:
50 50 yield ('string', decode(program[s:pos]), s)
51 51 break
52 52 pos += 1
53 53 else:
54 54 raise error.ParseError(_("unterminated string"), s)
55 55 elif c.isalnum() or c in '_':
56 56 s = pos
57 57 pos += 1
58 58 while pos < end: # find end of symbol
59 59 d = program[pos]
60 60 if not (d.isalnum() or d == "_"):
61 61 break
62 62 pos += 1
63 63 sym = program[s:pos]
64 64 yield ('symbol', sym, s)
65 65 pos -= 1
66 66 elif c == '}':
67 67 pos += 1
68 68 break
69 69 else:
70 70 raise error.ParseError(_("syntax error"), pos)
71 71 pos += 1
72 72 yield ('end', None, pos)
73 73
74 74 def compiletemplate(tmpl, context):
75 75 parsed = []
76 76 pos, stop = 0, len(tmpl)
77 77 p = parser.parser(tokenizer, elements)
78 78
79 79 while pos < stop:
80 80 n = tmpl.find('{', pos)
81 81 if n < 0:
82 82 parsed.append(("string", tmpl[pos:]))
83 83 break
84 84 if n > 0 and tmpl[n - 1] == '\\':
85 85 # escaped
86 86 parsed.append(("string", tmpl[pos:n - 1] + "{"))
87 87 pos = n + 1
88 88 continue
89 89 if n > pos:
90 90 parsed.append(("string", tmpl[pos:n]))
91 91
92 92 pd = [tmpl, n + 1, stop]
93 93 parseres, pos = p.parse(pd)
94 94 parsed.append(parseres)
95 95
96 96 return [compileexp(e, context) for e in parsed]
97 97
98 98 def compileexp(exp, context):
99 99 t = exp[0]
100 100 if t in methods:
101 101 return methods[t](exp, context)
102 102 raise error.ParseError(_("unknown method '%s'") % t)
103 103
104 104 # template evaluation
105 105
106 106 def getsymbol(exp):
107 107 if exp[0] == 'symbol':
108 108 return exp[1]
109 109 raise error.ParseError(_("expected a symbol"))
110 110
111 111 def getlist(x):
112 112 if not x:
113 113 return []
114 114 if x[0] == 'list':
115 115 return getlist(x[1]) + [x[2]]
116 116 return [x]
117 117
118 118 def getfilter(exp, context):
119 119 f = getsymbol(exp)
120 120 if f not in context._filters:
121 121 raise error.ParseError(_("unknown function '%s'") % f)
122 122 return context._filters[f]
123 123
124 124 def gettemplate(exp, context):
125 125 if exp[0] == 'string':
126 126 return compiletemplate(exp[1], context)
127 127 if exp[0] == 'symbol':
128 128 return context._load(exp[1])
129 129 raise error.ParseError(_("expected template specifier"))
130 130
131 131 def runstring(context, mapping, data):
132 132 return data
133 133
134 134 def runsymbol(context, mapping, key):
135 135 v = mapping.get(key)
136 136 if v is None:
137 137 v = context._defaults.get(key, '')
138 138 if util.safehasattr(v, '__call__'):
139 139 return v(**mapping)
140 140 return v
141 141
142 142 def buildfilter(exp, context):
143 143 func, data = compileexp(exp[1], context)
144 144 filt = getfilter(exp[2], context)
145 145 return (runfilter, (func, data, filt))
146 146
147 147 def runfilter(context, mapping, data):
148 148 func, data, filt = data
149 149 try:
150 150 return filt(func(context, mapping, data))
151 151 except (ValueError, AttributeError, TypeError):
152 152 if isinstance(data, tuple):
153 153 dt = data[1]
154 154 else:
155 155 dt = data
156 156 raise util.Abort(_("template filter '%s' is not compatible with "
157 157 "keyword '%s'") % (filt.func_name, dt))
158 158
159 159 def buildmap(exp, context):
160 160 func, data = compileexp(exp[1], context)
161 161 ctmpl = gettemplate(exp[2], context)
162 162 return (runmap, (func, data, ctmpl))
163 163
164 def runtemplate(context, mapping, template):
165 for func, data in template:
166 yield func(context, mapping, data)
167
164 168 def runmap(context, mapping, data):
165 169 func, data, ctmpl = data
166 170 d = func(context, mapping, data)
167 171 if util.safehasattr(d, '__call__'):
168 172 d = d()
169 173
170 174 lm = mapping.copy()
171 175
172 176 for i in d:
173 177 if isinstance(i, dict):
174 178 lm.update(i)
175 for f, d in ctmpl:
176 yield f(context, lm, d)
179 yield runtemplate(context, lm, ctmpl)
177 180 else:
178 181 # v is not an iterable of dicts, this happen when 'key'
179 182 # has been fully expanded already and format is useless.
180 183 # If so, return the expanded value.
181 184 yield i
182 185
183 186 def buildfunc(exp, context):
184 187 n = getsymbol(exp[1])
185 188 args = [compileexp(x, context) for x in getlist(exp[2])]
186 189 if n in funcs:
187 190 f = funcs[n]
188 191 return (f, args)
189 192 if n in context._filters:
190 193 if len(args) != 1:
191 194 raise error.ParseError(_("filter %s expects one argument") % n)
192 195 f = context._filters[n]
193 196 return (runfilter, (args[0][0], args[0][1], f))
194 197
195 198 methods = {
196 199 "string": lambda e, c: (runstring, e[1]),
197 200 "symbol": lambda e, c: (runsymbol, e[1]),
198 201 "group": lambda e, c: compileexp(e[1], c),
199 202 # ".": buildmember,
200 203 "|": buildfilter,
201 204 "%": buildmap,
202 205 "func": buildfunc,
203 206 }
204 207
205 208 funcs = {
206 209 }
207 210
208 211 # template engine
209 212
210 213 path = ['templates', '../templates']
211 214 stringify = templatefilters.stringify
212 215
213 216 def _flatten(thing):
214 217 '''yield a single stream from a possibly nested set of iterators'''
215 218 if isinstance(thing, str):
216 219 yield thing
217 220 elif not util.safehasattr(thing, '__iter__'):
218 221 if thing is not None:
219 222 yield str(thing)
220 223 else:
221 224 for i in thing:
222 225 if isinstance(i, str):
223 226 yield i
224 227 elif not util.safehasattr(i, '__iter__'):
225 228 if i is not None:
226 229 yield str(i)
227 230 elif i is not None:
228 231 for j in _flatten(i):
229 232 yield j
230 233
231 234 def parsestring(s, quoted=True):
232 235 '''parse a string using simple c-like syntax.
233 236 string must be in quotes if quoted is True.'''
234 237 if quoted:
235 238 if len(s) < 2 or s[0] != s[-1]:
236 239 raise SyntaxError(_('unmatched quotes'))
237 240 return s[1:-1].decode('string_escape')
238 241
239 242 return s.decode('string_escape')
240 243
241 244 class engine(object):
242 245 '''template expansion engine.
243 246
244 247 template expansion works like this. a map file contains key=value
245 248 pairs. if value is quoted, it is treated as string. otherwise, it
246 249 is treated as name of template file.
247 250
248 251 templater is asked to expand a key in map. it looks up key, and
249 252 looks for strings like this: {foo}. it expands {foo} by looking up
250 253 foo in map, and substituting it. expansion is recursive: it stops
251 254 when there is no more {foo} to replace.
252 255
253 256 expansion also allows formatting and filtering.
254 257
255 258 format uses key to expand each item in list. syntax is
256 259 {key%format}.
257 260
258 261 filter uses function to transform value. syntax is
259 262 {key|filter1|filter2|...}.'''
260 263
261 264 def __init__(self, loader, filters={}, defaults={}):
262 265 self._loader = loader
263 266 self._filters = filters
264 267 self._defaults = defaults
265 268 self._cache = {}
266 269
267 270 def _load(self, t):
268 271 '''load, parse, and cache a template'''
269 272 if t not in self._cache:
270 273 self._cache[t] = compiletemplate(self._loader(t), self)
271 274 return self._cache[t]
272 275
273 276 def process(self, t, mapping):
274 277 '''Perform expansion. t is name of map element to expand.
275 278 mapping contains added elements for use during expansion. Is a
276 279 generator.'''
277 280 return _flatten(func(self, mapping, data) for func, data in
278 281 self._load(t))
282 return _flatten(runtemplate(self, mapping, self._load(t)))
279 283
280 284 engines = {'default': engine}
281 285
282 286 class templater(object):
283 287
284 288 def __init__(self, mapfile, filters={}, defaults={}, cache={},
285 289 minchunk=1024, maxchunk=65536):
286 290 '''set up template engine.
287 291 mapfile is name of file to read map definitions from.
288 292 filters is dict of functions. each transforms a value into another.
289 293 defaults is dict of default map definitions.'''
290 294 self.mapfile = mapfile or 'template'
291 295 self.cache = cache.copy()
292 296 self.map = {}
293 297 self.base = (mapfile and os.path.dirname(mapfile)) or ''
294 298 self.filters = templatefilters.filters.copy()
295 299 self.filters.update(filters)
296 300 self.defaults = defaults
297 301 self.minchunk, self.maxchunk = minchunk, maxchunk
298 302 self.ecache = {}
299 303
300 304 if not mapfile:
301 305 return
302 306 if not os.path.exists(mapfile):
303 307 raise util.Abort(_('style not found: %s') % mapfile)
304 308
305 309 conf = config.config()
306 310 conf.read(mapfile)
307 311
308 312 for key, val in conf[''].items():
309 313 if not val:
310 314 raise SyntaxError(_('%s: missing value') % conf.source('', key))
311 315 if val[0] in "'\"":
312 316 try:
313 317 self.cache[key] = parsestring(val)
314 318 except SyntaxError, inst:
315 319 raise SyntaxError('%s: %s' %
316 320 (conf.source('', key), inst.args[0]))
317 321 else:
318 322 val = 'default', val
319 323 if ':' in val[1]:
320 324 val = val[1].split(':', 1)
321 325 self.map[key] = val[0], os.path.join(self.base, val[1])
322 326
323 327 def __contains__(self, key):
324 328 return key in self.cache or key in self.map
325 329
326 330 def load(self, t):
327 331 '''Get the template for the given template name. Use a local cache.'''
328 332 if t not in self.cache:
329 333 try:
330 334 self.cache[t] = util.readfile(self.map[t][1])
331 335 except KeyError, inst:
332 336 raise util.Abort(_('"%s" not in template map') % inst.args[0])
333 337 except IOError, inst:
334 338 raise IOError(inst.args[0], _('template file %s: %s') %
335 339 (self.map[t][1], inst.args[1]))
336 340 return self.cache[t]
337 341
338 342 def __call__(self, t, **mapping):
339 343 ttype = t in self.map and self.map[t][0] or 'default'
340 344 if ttype not in self.ecache:
341 345 self.ecache[ttype] = engines[ttype](self.load,
342 346 self.filters, self.defaults)
343 347 proc = self.ecache[ttype]
344 348
345 349 stream = proc.process(t, mapping)
346 350 if self.minchunk:
347 351 stream = util.increasingchunks(stream, min=self.minchunk,
348 352 max=self.maxchunk)
349 353 return stream
350 354
351 355 def templatepath(name=None):
352 356 '''return location of template file or directory (if no name).
353 357 returns None if not found.'''
354 358 normpaths = []
355 359
356 360 # executable version (py2exe) doesn't support __file__
357 361 if util.mainfrozen():
358 362 module = sys.executable
359 363 else:
360 364 module = __file__
361 365 for f in path:
362 366 if f.startswith('/'):
363 367 p = f
364 368 else:
365 369 fl = f.split('/')
366 370 p = os.path.join(os.path.dirname(module), *fl)
367 371 if name:
368 372 p = os.path.join(p, name)
369 373 if name and os.path.exists(p):
370 374 return os.path.normpath(p)
371 375 elif os.path.isdir(p):
372 376 normpaths.append(os.path.normpath(p))
373 377
374 378 return normpaths
375 379
376 380 def stylemap(styles, paths=None):
377 381 """Return path to mapfile for a given style.
378 382
379 383 Searches mapfile in the following locations:
380 384 1. templatepath/style/map
381 385 2. templatepath/map-style
382 386 3. templatepath/map
383 387 """
384 388
385 389 if paths is None:
386 390 paths = templatepath()
387 391 elif isinstance(paths, str):
388 392 paths = [paths]
389 393
390 394 if isinstance(styles, str):
391 395 styles = [styles]
392 396
393 397 for style in styles:
394 398 if not style:
395 399 continue
396 400 locations = [os.path.join(style, 'map'), 'map-' + style]
397 401 locations.append('map')
398 402
399 403 for path in paths:
400 404 for location in locations:
401 405 mapfile = os.path.join(path, location)
402 406 if os.path.isfile(mapfile):
403 407 return style, mapfile
404 408
405 409 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now