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