##// END OF EJS Templates
templater: simplify cache and remove filter argument in __call__
Matt Mackall -
r3637:e7639888 default
parent child Browse files
Show More
@@ -1,533 +1,531
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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from demandload import demandload
9 9 from i18n import gettext as _
10 10 from node import *
11 11 demandload(globals(), "cStringIO cgi re sys os time urllib util textwrap")
12 12
13 13 def parsestring(s, quoted=True):
14 14 '''parse a string using simple c-like syntax.
15 15 string must be in quotes if quoted is True.'''
16 16 if quoted:
17 17 first = s[0]
18 18 if len(s) < 2: raise SyntaxError(_('string too short'))
19 19 if first not in "'\"": raise SyntaxError(_('invalid quote'))
20 20 if s[-1] != first: raise SyntaxError(_('unmatched quotes'))
21 21 s = s[1:-1].decode('string_escape')
22 22 if first in s: raise SyntaxError(_('string ends early'))
23 23 return s
24 24
25 25 return s.decode('string_escape')
26 26
27 27 class templater(object):
28 28 '''template expansion engine.
29 29
30 30 template expansion works like this. a map file contains key=value
31 31 pairs. if value is quoted, it is treated as string. otherwise, it
32 32 is treated as name of template file.
33 33
34 34 templater is asked to expand a key in map. it looks up key, and
35 35 looks for atrings like this: {foo}. it expands {foo} by looking up
36 36 foo in map, and substituting it. expansion is recursive: it stops
37 37 when there is no more {foo} to replace.
38 38
39 39 expansion also allows formatting and filtering.
40 40
41 41 format uses key to expand each item in list. syntax is
42 42 {key%format}.
43 43
44 44 filter uses function to transform value. syntax is
45 45 {key|filter1|filter2|...}.'''
46 46
47 47 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
48 48 '''set up template engine.
49 49 mapfile is name of file to read map definitions from.
50 50 filters is dict of functions. each transforms a value into another.
51 51 defaults is dict of default map definitions.'''
52 52 self.mapfile = mapfile or 'template'
53 53 self.cache = cache.copy()
54 54 self.map = {}
55 55 self.base = (mapfile and os.path.dirname(mapfile)) or ''
56 56 self.filters = filters
57 57 self.defaults = defaults
58 58
59 59 if not mapfile:
60 60 return
61 61 i = 0
62 62 for l in file(mapfile):
63 63 l = l.strip()
64 64 i += 1
65 65 if not l or l[0] in '#;': continue
66 66 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
67 67 if m:
68 68 key, val = m.groups()
69 69 if val[0] in "'\"":
70 70 try:
71 71 self.cache[key] = parsestring(val)
72 72 except SyntaxError, inst:
73 73 raise SyntaxError('%s:%s: %s' %
74 74 (mapfile, i, inst.args[0]))
75 75 else:
76 76 self.map[key] = os.path.join(self.base, val)
77 77 else:
78 78 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
79 79
80 80 def __contains__(self, key):
81 return key in self.cache
81 return key in self.cache or key in self.map
82 82
83 83 def __call__(self, t, **map):
84 84 '''perform expansion.
85 85 t is name of map element to expand.
86 86 map is added elements to use during expansion.'''
87 87 m = self.defaults.copy()
88 88 m.update(map)
89 try:
90 tmpl = self.cache[t]
91 except KeyError:
89 if not self.cache.has_key(t):
92 90 try:
93 tmpl = self.cache[t] = file(self.map[t]).read()
91 self.cache[t] = file(self.map[t]).read()
94 92 except IOError, inst:
95 93 raise IOError(inst.args[0], _('template file %s: %s') %
96 94 (self.map[t], inst.args[1]))
97 return self.template(tmpl, self.filters, **m)
95 return self.template(self.cache[t], **m)
98 96
99 97 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
100 98 r"(\w+)((%\w+)*)((\|\w+)*)[#}]")
101 99
102 def template(self, tmpl, filters={}, **map):
100 def template(self, tmpl, **map):
103 101 while tmpl:
104 102 m = self.template_re.search(tmpl)
105 103 if m:
106 104 start, end = m.span(0)
107 105 key = m.group(1)
108 106 format = m.group(2)
109 107 fl = m.group(4)
110 108
111 109 if start:
112 110 yield tmpl[:start]
113 111
114 112 v = map.get(key, "")
115 113 if callable(v):
116 114 v = v(**map)
117 115
118 116 if format:
119 117 if not hasattr(v, '__iter__'):
120 118 raise SyntaxError(_("Error expanding '%s%s'")
121 119 % (key, format))
122 120 lm = map.copy()
123 121 for i in v:
124 122 lm.update(i)
125 123 yield self(format[1:], **lm)
126 124
127 125 v = ""
128 126
129 127 elif fl:
130 128 for f in fl.split("|")[1:]:
131 v = filters[f](v)
129 v = self.filters[f](v)
132 130
133 131 yield v
134 132 tmpl = tmpl[end:]
135 133 else:
136 134 yield tmpl
137 135 break
138 136
139 137 agescales = [("second", 1),
140 138 ("minute", 60),
141 139 ("hour", 3600),
142 140 ("day", 3600 * 24),
143 141 ("week", 3600 * 24 * 7),
144 142 ("month", 3600 * 24 * 30),
145 143 ("year", 3600 * 24 * 365)]
146 144
147 145 agescales.reverse()
148 146
149 147 def age(date):
150 148 '''turn a (timestamp, tzoff) tuple into an age string.'''
151 149
152 150 def plural(t, c):
153 151 if c == 1:
154 152 return t
155 153 return t + "s"
156 154 def fmt(t, c):
157 155 return "%d %s" % (c, plural(t, c))
158 156
159 157 now = time.time()
160 158 then = date[0]
161 159 delta = max(1, int(now - then))
162 160
163 161 for t, s in agescales:
164 162 n = delta / s
165 163 if n >= 2 or s == 1:
166 164 return fmt(t, n)
167 165
168 166 def stringify(thing):
169 167 '''turn nested template iterator into string.'''
170 168 def flatten(thing):
171 169 if hasattr(thing, '__iter__'):
172 170 for t in thing:
173 171 for i in flatten(t):
174 172 yield i
175 173 else:
176 174 yield str(thing)
177 175 return "".join(flatten(thing))
178 176
179 177 para_re = None
180 178 space_re = None
181 179
182 180 def fill(text, width):
183 181 '''fill many paragraphs.'''
184 182 global para_re, space_re
185 183 if para_re is None:
186 184 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
187 185 space_re = re.compile(r' +')
188 186
189 187 def findparas():
190 188 start = 0
191 189 while True:
192 190 m = para_re.search(text, start)
193 191 if not m:
194 192 w = len(text)
195 193 while w > start and text[w-1].isspace(): w -= 1
196 194 yield text[start:w], text[w:]
197 195 break
198 196 yield text[start:m.start(0)], m.group(1)
199 197 start = m.end(1)
200 198
201 199 return "".join([space_re.sub(' ', textwrap.fill(para, width)) + rest
202 200 for para, rest in findparas()])
203 201
204 202 def firstline(text):
205 203 '''return the first line of text'''
206 204 try:
207 205 return text.splitlines(1)[0].rstrip('\r\n')
208 206 except IndexError:
209 207 return ''
210 208
211 209 def isodate(date):
212 210 '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.'''
213 211 return util.datestr(date, format='%Y-%m-%d %H:%M')
214 212
215 213 def hgdate(date):
216 214 '''turn a (timestamp, tzoff) tuple into an hg cset timestamp.'''
217 215 return "%d %d" % date
218 216
219 217 def nl2br(text):
220 218 '''replace raw newlines with xhtml line breaks.'''
221 219 return text.replace('\n', '<br/>\n')
222 220
223 221 def obfuscate(text):
224 222 text = unicode(text, 'utf-8', 'replace')
225 223 return ''.join(['&#%d;' % ord(c) for c in text])
226 224
227 225 def domain(author):
228 226 '''get domain of author, or empty string if none.'''
229 227 f = author.find('@')
230 228 if f == -1: return ''
231 229 author = author[f+1:]
232 230 f = author.find('>')
233 231 if f >= 0: author = author[:f]
234 232 return author
235 233
236 234 def email(author):
237 235 '''get email of author.'''
238 236 r = author.find('>')
239 237 if r == -1: r = None
240 238 return author[author.find('<')+1:r]
241 239
242 240 def person(author):
243 241 '''get name of author, or else username.'''
244 242 f = author.find('<')
245 243 if f == -1: return util.shortuser(author)
246 244 return author[:f].rstrip()
247 245
248 246 def shortdate(date):
249 247 '''turn (timestamp, tzoff) tuple into iso 8631 date.'''
250 248 return util.datestr(date, format='%Y-%m-%d', timezone=False)
251 249
252 250 def indent(text, prefix):
253 251 '''indent each non-empty line of text after first with prefix.'''
254 252 lines = text.splitlines()
255 253 num_lines = len(lines)
256 254 def indenter():
257 255 for i in xrange(num_lines):
258 256 l = lines[i]
259 257 if i and l.strip():
260 258 yield prefix
261 259 yield l
262 260 if i < num_lines - 1 or text.endswith('\n'):
263 261 yield '\n'
264 262 return "".join(indenter())
265 263
266 264 common_filters = {
267 265 "addbreaks": nl2br,
268 266 "basename": os.path.basename,
269 267 "age": age,
270 268 "date": lambda x: util.datestr(x),
271 269 "domain": domain,
272 270 "email": email,
273 271 "escape": lambda x: cgi.escape(x, True),
274 272 "fill68": lambda x: fill(x, width=68),
275 273 "fill76": lambda x: fill(x, width=76),
276 274 "firstline": firstline,
277 275 "tabindent": lambda x: indent(x, '\t'),
278 276 "hgdate": hgdate,
279 277 "isodate": isodate,
280 278 "obfuscate": obfuscate,
281 279 "permissions": lambda x: x and "-rwxr-xr-x" or "-rw-r--r--",
282 280 "person": person,
283 281 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"),
284 282 "short": lambda x: x[:12],
285 283 "shortdate": shortdate,
286 284 "stringify": stringify,
287 285 "strip": lambda x: x.strip(),
288 286 "urlescape": lambda x: urllib.quote(x),
289 287 "user": lambda x: util.shortuser(x),
290 288 "stringescape": lambda x: x.encode('string_escape'),
291 289 }
292 290
293 291 def templatepath(name=None):
294 292 '''return location of template file or directory (if no name).
295 293 returns None if not found.'''
296 294
297 295 # executable version (py2exe) doesn't support __file__
298 296 if hasattr(sys, 'frozen'):
299 297 module = sys.executable
300 298 else:
301 299 module = __file__
302 300 for f in 'templates', '../templates':
303 301 fl = f.split('/')
304 302 if name: fl.append(name)
305 303 p = os.path.join(os.path.dirname(module), *fl)
306 304 if (name and os.path.exists(p)) or os.path.isdir(p):
307 305 return os.path.normpath(p)
308 306
309 307 class changeset_templater(object):
310 308 '''format changeset information.'''
311 309
312 310 def __init__(self, ui, repo, mapfile, dest=None):
313 311 self.t = templater(mapfile, common_filters,
314 312 cache={'parent': '{rev}:{node|short} ',
315 313 'manifest': '{rev}:{node|short}',
316 314 'filecopy': '{name} ({source})'})
317 315 self.ui = ui
318 316 self.dest = dest
319 317 self.repo = repo
320 318
321 319 def use_template(self, t):
322 320 '''set template string to use'''
323 321 self.t.cache['changeset'] = t
324 322
325 323 def write(self, thing, header=False):
326 324 '''write expanded template.
327 325 uses in-order recursive traverse of iterators.'''
328 326 dest = self.dest or self.ui
329 327 for t in thing:
330 328 if hasattr(t, '__iter__'):
331 329 self.write(t, header=header)
332 330 elif header:
333 331 dest.write_header(t)
334 332 else:
335 333 dest.write(t)
336 334
337 335 def write_header(self, thing):
338 336 self.write(thing, header=True)
339 337
340 338 def show(self, rev=0, changenode=None, brinfo=None, changes=None,
341 339 copies=[], **props):
342 340 '''show a single changeset or file revision'''
343 341 log = self.repo.changelog
344 342 if changenode is None:
345 343 changenode = log.node(rev)
346 344 elif not rev:
347 345 rev = log.rev(changenode)
348 346 if changes is None:
349 347 changes = log.read(changenode)
350 348
351 349 def showlist(name, values, plural=None, **args):
352 350 '''expand set of values.
353 351 name is name of key in template map.
354 352 values is list of strings or dicts.
355 353 plural is plural of name, if not simply name + 's'.
356 354
357 355 expansion works like this, given name 'foo'.
358 356
359 357 if values is empty, expand 'no_foos'.
360 358
361 359 if 'foo' not in template map, return values as a string,
362 360 joined by space.
363 361
364 362 expand 'start_foos'.
365 363
366 364 for each value, expand 'foo'. if 'last_foo' in template
367 365 map, expand it instead of 'foo' for last key.
368 366
369 367 expand 'end_foos'.
370 368 '''
371 369 if plural: names = plural
372 370 else: names = name + 's'
373 371 if not values:
374 372 noname = 'no_' + names
375 373 if noname in self.t:
376 374 yield self.t(noname, **args)
377 375 return
378 376 if name not in self.t:
379 377 if isinstance(values[0], str):
380 378 yield ' '.join(values)
381 379 else:
382 380 for v in values:
383 381 yield dict(v, **args)
384 382 return
385 383 startname = 'start_' + names
386 384 if startname in self.t:
387 385 yield self.t(startname, **args)
388 386 vargs = args.copy()
389 387 def one(v, tag=name):
390 388 try:
391 389 vargs.update(v)
392 390 except (AttributeError, ValueError):
393 391 try:
394 392 for a, b in v:
395 393 vargs[a] = b
396 394 except ValueError:
397 395 vargs[name] = v
398 396 return self.t(tag, **vargs)
399 397 lastname = 'last_' + name
400 398 if lastname in self.t:
401 399 last = values.pop()
402 400 else:
403 401 last = None
404 402 for v in values:
405 403 yield one(v)
406 404 if last is not None:
407 405 yield one(last, tag=lastname)
408 406 endname = 'end_' + names
409 407 if endname in self.t:
410 408 yield self.t(endname, **args)
411 409
412 410 def showbranches(**args):
413 411 branch = changes[5].get("branch")
414 412 if branch:
415 413 yield showlist('branch', [branch], plural='branches', **args)
416 414 # add old style branches if requested
417 415 if brinfo and changenode in brinfo:
418 416 for x in showlist('branch', brinfo[changenode],
419 417 plural='branches', **args):
420 418 yield x
421 419
422 420 if self.ui.debugflag:
423 421 def showmanifest(**args):
424 422 args = args.copy()
425 423 args.update(dict(rev=self.repo.manifest.rev(changes[0]),
426 424 node=hex(changes[0])))
427 425 yield self.t('manifest', **args)
428 426 else:
429 427 showmanifest = ''
430 428
431 429 def showparents(**args):
432 430 parents = [[('rev', log.rev(p)), ('node', hex(p))]
433 431 for p in log.parents(changenode)
434 432 if self.ui.debugflag or p != nullid]
435 433 if (not self.ui.debugflag and len(parents) == 1 and
436 434 parents[0][0][1] == rev - 1):
437 435 return
438 436 for x in showlist('parent', parents, **args):
439 437 yield x
440 438
441 439 def showtags(**args):
442 440 for x in showlist('tag', self.repo.nodetags(changenode), **args):
443 441 yield x
444 442
445 443 def showextras(**args):
446 444 extras = changes[5].items()
447 445 extras.sort()
448 446 for key, value in extras:
449 447 args = args.copy()
450 448 args.update(dict(key=key, value=value))
451 449 yield self.t('extra', **args)
452 450
453 451 if self.ui.debugflag:
454 452 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
455 453 def showfiles(**args):
456 454 for x in showlist('file', files[0], **args): yield x
457 455 def showadds(**args):
458 456 for x in showlist('file_add', files[1], **args): yield x
459 457 def showdels(**args):
460 458 for x in showlist('file_del', files[2], **args): yield x
461 459 else:
462 460 def showfiles(**args):
463 461 for x in showlist('file', changes[3], **args): yield x
464 462 showadds = ''
465 463 showdels = ''
466 464
467 465 copies = [{'name': x[0], 'source': x[1]}
468 466 for x in copies]
469 467 def showcopies(**args):
470 468 for x in showlist('file_copy', copies, plural='file_copies',
471 469 **args):
472 470 yield x
473 471
474 472 defprops = {
475 473 'author': changes[1],
476 474 'branches': showbranches,
477 475 'date': changes[2],
478 476 'desc': changes[4],
479 477 'file_adds': showadds,
480 478 'file_dels': showdels,
481 479 'files': showfiles,
482 480 'file_copies': showcopies,
483 481 'manifest': showmanifest,
484 482 'node': hex(changenode),
485 483 'parents': showparents,
486 484 'rev': rev,
487 485 'tags': showtags,
488 486 'extras': showextras,
489 487 }
490 488 props = props.copy()
491 489 props.update(defprops)
492 490
493 491 try:
494 492 if self.ui.debugflag and 'header_debug' in self.t:
495 493 key = 'header_debug'
496 494 elif self.ui.quiet and 'header_quiet' in self.t:
497 495 key = 'header_quiet'
498 496 elif self.ui.verbose and 'header_verbose' in self.t:
499 497 key = 'header_verbose'
500 498 elif 'header' in self.t:
501 499 key = 'header'
502 500 else:
503 501 key = ''
504 502 if key:
505 503 self.write_header(self.t(key, **props))
506 504 if self.ui.debugflag and 'changeset_debug' in self.t:
507 505 key = 'changeset_debug'
508 506 elif self.ui.quiet and 'changeset_quiet' in self.t:
509 507 key = 'changeset_quiet'
510 508 elif self.ui.verbose and 'changeset_verbose' in self.t:
511 509 key = 'changeset_verbose'
512 510 else:
513 511 key = 'changeset'
514 512 self.write(self.t(key, **props))
515 513 except KeyError, inst:
516 514 raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
517 515 inst.args[0]))
518 516 except SyntaxError, inst:
519 517 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
520 518
521 519 class stringio(object):
522 520 '''wrap cStringIO for use by changeset_templater.'''
523 521 def __init__(self):
524 522 self.fp = cStringIO.StringIO()
525 523
526 524 def write(self, *args):
527 525 for a in args:
528 526 self.fp.write(a)
529 527
530 528 write_header = write
531 529
532 530 def __getattr__(self, key):
533 531 return getattr(self.fp, key)
General Comments 0
You need to be logged in to leave comments. Login now