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