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