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