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