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