##// END OF EJS Templates
templater: add get() function to access dict element (e.g. extra)
Benoit Boissinot -
r18582:ef78450c default
parent child Browse files
Show More
@@ -1,98 +1,100 b''
1 1 Mercurial allows you to customize output of commands through
2 2 templates. You can either pass in a template from the command
3 3 line, via the --template option, or select an existing
4 4 template-style (--style).
5 5
6 6 You can customize output for any "log-like" command: log,
7 7 outgoing, incoming, tip, parents, heads and glog.
8 8
9 9 Four styles are packaged with Mercurial: default (the style used
10 10 when no explicit preference is passed), compact, changelog,
11 11 and xml.
12 12 Usage::
13 13
14 14 $ hg log -r1 --style changelog
15 15
16 16 A template is a piece of text, with markup to invoke variable
17 17 expansion::
18 18
19 19 $ hg log -r1 --template "{node}\n"
20 20 b56ce7b07c52de7d5fd79fb89701ea538af65746
21 21
22 22 Strings in curly braces are called keywords. The availability of
23 23 keywords depends on the exact context of the templater. These
24 24 keywords are usually available for templating a log-like command:
25 25
26 26 .. keywordsmarker
27 27
28 28 The "date" keyword does not produce human-readable output. If you
29 29 want to use a date in your output, you can use a filter to process
30 30 it. Filters are functions which return a string based on the input
31 31 variable. Be sure to use the stringify filter first when you're
32 32 applying a string-input filter to a list-like input variable.
33 33 You can also use a chain of filters to get the desired output::
34 34
35 35 $ hg tip --template "{date|isodate}\n"
36 36 2008-08-21 18:22 +0000
37 37
38 38 List of filters:
39 39
40 40 .. filtersmarker
41 41
42 42 Note that a filter is nothing more than a function call, i.e.
43 43 ``expr|filter`` is equivalent to ``filter(expr)``.
44 44
45 45 In addition to filters, there are some basic built-in functions:
46 46
47 - date(date[, fmt])
48
49 - fill(text[, width])
50
51 - get(dict, key)
52
47 53 - if(expr, then[, else])
48 54
49 55 - ifeq(expr, expr, then[, else])
50 56
51 - sub(pat, repl, expr)
52
53 57 - join(list, sep)
54 58
55 59 - label(label, expr)
56 60
57 - date(date[, fmt])
58
59 - fill(text[, width])
61 - sub(pat, repl, expr)
60 62
61 63 Also, for any expression that returns a list, there is a list operator:
62 64
63 65 - expr % "{template}"
64 66
65 67 Some sample command line templates:
66 68
67 69 - Format lists, e.g. files::
68 70
69 71 $ hg log -r 0 --template "files:\n{files % ' {file}\n'}"
70 72
71 73 - Join the list of files with a ", "::
72 74
73 75 $ hg log -r 0 --template "files: {join(files, ', ')}\n"
74 76
75 77 - Format date::
76 78
77 79 $ hg log -r 0 --template "{date(date, '%Y')}\n"
78 80
79 81 - Output the description set to a fill-width of 30::
80 82
81 83 $ hg log -r 0 --template "{fill(desc, '30')}"
82 84
83 85 - Use a conditional to test for the default branch::
84 86
85 87 $ hg log -r 0 --template "{ifeq(branch, 'default', 'on the main branch',
86 88 'on branch {branch}')}\n"
87 89
88 90 - Append a newline if not empty::
89 91
90 92 $ hg tip --template "{if(author, '{author}\n')}"
91 93
92 94 - Label the output for use with the color extension::
93 95
94 96 $ hg log -r 0 --template "{label('changeset.{phase}', node|short)}\n"
95 97
96 98 - Invert the firstline filter, i.e. everything but the first line::
97 99
98 100 $ hg log -r 0 --template "{sub(r'^.*\n?\n?', '', desc)}\n"
@@ -1,491 +1,505 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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import sys, os, re
10 10 import util, config, templatefilters, parser, error
11 11 import types
12 12
13 13 # template parsing
14 14
15 15 elements = {
16 16 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
17 17 ",": (2, None, ("list", 2)),
18 18 "|": (5, None, ("|", 5)),
19 19 "%": (6, None, ("%", 6)),
20 20 ")": (0, None, None),
21 21 "symbol": (0, ("symbol",), None),
22 22 "string": (0, ("string",), None),
23 23 "end": (0, None, None),
24 24 }
25 25
26 26 def tokenizer(data):
27 27 program, start, end = data
28 28 pos = start
29 29 while pos < end:
30 30 c = program[pos]
31 31 if c.isspace(): # skip inter-token whitespace
32 32 pass
33 33 elif c in "(,)%|": # handle simple operators
34 34 yield (c, None, pos)
35 35 elif (c in '"\'' or c == 'r' and
36 36 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
37 37 if c == 'r':
38 38 pos += 1
39 39 c = program[pos]
40 40 decode = False
41 41 else:
42 42 decode = True
43 43 pos += 1
44 44 s = pos
45 45 while pos < end: # find closing quote
46 46 d = program[pos]
47 47 if decode and d == '\\': # skip over escaped characters
48 48 pos += 2
49 49 continue
50 50 if d == c:
51 51 if not decode:
52 52 yield ('string', program[s:pos].replace('\\', r'\\'), s)
53 53 break
54 54 yield ('string', program[s:pos].decode('string-escape'), s)
55 55 break
56 56 pos += 1
57 57 else:
58 58 raise error.ParseError(_("unterminated string"), s)
59 59 elif c.isalnum() or c in '_':
60 60 s = pos
61 61 pos += 1
62 62 while pos < end: # find end of symbol
63 63 d = program[pos]
64 64 if not (d.isalnum() or d == "_"):
65 65 break
66 66 pos += 1
67 67 sym = program[s:pos]
68 68 yield ('symbol', sym, s)
69 69 pos -= 1
70 70 elif c == '}':
71 71 pos += 1
72 72 break
73 73 else:
74 74 raise error.ParseError(_("syntax error"), pos)
75 75 pos += 1
76 76 yield ('end', None, pos)
77 77
78 78 def compiletemplate(tmpl, context):
79 79 parsed = []
80 80 pos, stop = 0, len(tmpl)
81 81 p = parser.parser(tokenizer, elements)
82 82
83 83 while pos < stop:
84 84 n = tmpl.find('{', pos)
85 85 if n < 0:
86 86 parsed.append(("string", tmpl[pos:]))
87 87 break
88 88 if n > 0 and tmpl[n - 1] == '\\':
89 89 # escaped
90 90 parsed.append(("string", tmpl[pos:n - 1] + "{"))
91 91 pos = n + 1
92 92 continue
93 93 if n > pos:
94 94 parsed.append(("string", tmpl[pos:n]))
95 95
96 96 pd = [tmpl, n + 1, stop]
97 97 parseres, pos = p.parse(pd)
98 98 parsed.append(parseres)
99 99
100 100 return [compileexp(e, context) for e in parsed]
101 101
102 102 def compileexp(exp, context):
103 103 t = exp[0]
104 104 if t in methods:
105 105 return methods[t](exp, context)
106 106 raise error.ParseError(_("unknown method '%s'") % t)
107 107
108 108 # template evaluation
109 109
110 110 def getsymbol(exp):
111 111 if exp[0] == 'symbol':
112 112 return exp[1]
113 113 raise error.ParseError(_("expected a symbol"))
114 114
115 115 def getlist(x):
116 116 if not x:
117 117 return []
118 118 if x[0] == 'list':
119 119 return getlist(x[1]) + [x[2]]
120 120 return [x]
121 121
122 122 def getfilter(exp, context):
123 123 f = getsymbol(exp)
124 124 if f not in context._filters:
125 125 raise error.ParseError(_("unknown function '%s'") % f)
126 126 return context._filters[f]
127 127
128 128 def gettemplate(exp, context):
129 129 if exp[0] == 'string':
130 130 return compiletemplate(exp[1], context)
131 131 if exp[0] == 'symbol':
132 132 return context._load(exp[1])
133 133 raise error.ParseError(_("expected template specifier"))
134 134
135 135 def runstring(context, mapping, data):
136 136 return data
137 137
138 138 def runsymbol(context, mapping, key):
139 139 v = mapping.get(key)
140 140 if v is None:
141 141 v = context._defaults.get(key, '')
142 142 if util.safehasattr(v, '__call__'):
143 143 return v(**mapping)
144 144 if isinstance(v, types.GeneratorType):
145 145 v = list(v)
146 146 mapping[key] = v
147 147 return v
148 148 return v
149 149
150 150 def buildfilter(exp, context):
151 151 func, data = compileexp(exp[1], context)
152 152 filt = getfilter(exp[2], context)
153 153 return (runfilter, (func, data, filt))
154 154
155 155 def runfilter(context, mapping, data):
156 156 func, data, filt = data
157 157 try:
158 158 return filt(func(context, mapping, data))
159 159 except (ValueError, AttributeError, TypeError):
160 160 if isinstance(data, tuple):
161 161 dt = data[1]
162 162 else:
163 163 dt = data
164 164 raise util.Abort(_("template filter '%s' is not compatible with "
165 165 "keyword '%s'") % (filt.func_name, dt))
166 166
167 167 def buildmap(exp, context):
168 168 func, data = compileexp(exp[1], context)
169 169 ctmpl = gettemplate(exp[2], context)
170 170 return (runmap, (func, data, ctmpl))
171 171
172 172 def runtemplate(context, mapping, template):
173 173 for func, data in template:
174 174 yield func(context, mapping, data)
175 175
176 176 def runmap(context, mapping, data):
177 177 func, data, ctmpl = data
178 178 d = func(context, mapping, data)
179 179 if util.safehasattr(d, '__call__'):
180 180 d = d()
181 181
182 182 lm = mapping.copy()
183 183
184 184 for i in d:
185 185 if isinstance(i, dict):
186 186 lm.update(i)
187 187 lm['originalnode'] = mapping.get('node')
188 188 yield runtemplate(context, lm, ctmpl)
189 189 else:
190 190 # v is not an iterable of dicts, this happen when 'key'
191 191 # has been fully expanded already and format is useless.
192 192 # If so, return the expanded value.
193 193 yield i
194 194
195 195 def buildfunc(exp, context):
196 196 n = getsymbol(exp[1])
197 197 args = [compileexp(x, context) for x in getlist(exp[2])]
198 198 if n in funcs:
199 199 f = funcs[n]
200 200 return (f, args)
201 201 if n in templatefilters.funcs:
202 202 f = templatefilters.funcs[n]
203 203 return (f, args)
204 204 if n in context._filters:
205 205 if len(args) != 1:
206 206 raise error.ParseError(_("filter %s expects one argument") % n)
207 207 f = context._filters[n]
208 208 return (runfilter, (args[0][0], args[0][1], f))
209 209
210 def get(context, mapping, args):
211 if len(args) != 2:
212 # i18n: "get" is a keyword
213 raise error.ParseError(_("get() expects two arguments"))
214
215 dictarg = args[0][0](context, mapping, args[0][1])
216 if not util.safehasattr(dictarg, 'get'):
217 # i18n: "get" is a keyword
218 raise error.ParseError(_("get() expects a dict as first argument"))
219
220 key = args[1][0](context, mapping, args[1][1])
221 yield dictarg.get(key)
222
210 223 def join(context, mapping, args):
211 224 if not (1 <= len(args) <= 2):
212 225 # i18n: "join" is a keyword
213 226 raise error.ParseError(_("join expects one or two arguments"))
214 227
215 228 joinset = args[0][0](context, mapping, args[0][1])
216 229 if util.safehasattr(joinset, '__call__'):
217 230 joinset = [x.values()[0] for x in joinset()]
218 231
219 232 joiner = " "
220 233 if len(args) > 1:
221 234 joiner = args[1][0](context, mapping, args[1][1])
222 235
223 236 first = True
224 237 for x in joinset:
225 238 if first:
226 239 first = False
227 240 else:
228 241 yield joiner
229 242 yield x
230 243
231 244 def sub(context, mapping, args):
232 245 if len(args) != 3:
233 246 # i18n: "sub" is a keyword
234 247 raise error.ParseError(_("sub expects three arguments"))
235 248
236 249 pat = stringify(args[0][0](context, mapping, args[0][1]))
237 250 rpl = stringify(args[1][0](context, mapping, args[1][1]))
238 251 src = stringify(args[2][0](context, mapping, args[2][1]))
239 252 yield re.sub(pat, rpl, src)
240 253
241 254 def if_(context, mapping, args):
242 255 if not (2 <= len(args) <= 3):
243 256 # i18n: "if" is a keyword
244 257 raise error.ParseError(_("if expects two or three arguments"))
245 258
246 259 test = stringify(args[0][0](context, mapping, args[0][1]))
247 260 if test:
248 261 t = stringify(args[1][0](context, mapping, args[1][1]))
249 262 yield runtemplate(context, mapping, compiletemplate(t, context))
250 263 elif len(args) == 3:
251 264 t = stringify(args[2][0](context, mapping, args[2][1]))
252 265 yield runtemplate(context, mapping, compiletemplate(t, context))
253 266
254 267 def ifeq(context, mapping, args):
255 268 if not (3 <= len(args) <= 4):
256 269 # i18n: "ifeq" is a keyword
257 270 raise error.ParseError(_("ifeq expects three or four arguments"))
258 271
259 272 test = stringify(args[0][0](context, mapping, args[0][1]))
260 273 match = stringify(args[1][0](context, mapping, args[1][1]))
261 274 if test == match:
262 275 t = stringify(args[2][0](context, mapping, args[2][1]))
263 276 yield runtemplate(context, mapping, compiletemplate(t, context))
264 277 elif len(args) == 4:
265 278 t = stringify(args[3][0](context, mapping, args[3][1]))
266 279 yield runtemplate(context, mapping, compiletemplate(t, context))
267 280
268 281 def label(context, mapping, args):
269 282 if len(args) != 2:
270 283 # i18n: "label" is a keyword
271 284 raise error.ParseError(_("label expects two arguments"))
272 285
273 286 # ignore args[0] (the label string) since this is supposed to be a a no-op
274 287 t = stringify(args[1][0](context, mapping, args[1][1]))
275 288 yield runtemplate(context, mapping, compiletemplate(t, context))
276 289
277 290 methods = {
278 291 "string": lambda e, c: (runstring, e[1]),
279 292 "symbol": lambda e, c: (runsymbol, e[1]),
280 293 "group": lambda e, c: compileexp(e[1], c),
281 294 # ".": buildmember,
282 295 "|": buildfilter,
283 296 "%": buildmap,
284 297 "func": buildfunc,
285 298 }
286 299
287 300 funcs = {
301 "get": get,
288 302 "if": if_,
289 303 "ifeq": ifeq,
290 304 "join": join,
305 "label": label,
291 306 "sub": sub,
292 "label": label,
293 307 }
294 308
295 309 # template engine
296 310
297 311 path = ['templates', '../templates']
298 312 stringify = templatefilters.stringify
299 313
300 314 def _flatten(thing):
301 315 '''yield a single stream from a possibly nested set of iterators'''
302 316 if isinstance(thing, str):
303 317 yield thing
304 318 elif not util.safehasattr(thing, '__iter__'):
305 319 if thing is not None:
306 320 yield str(thing)
307 321 else:
308 322 for i in thing:
309 323 if isinstance(i, str):
310 324 yield i
311 325 elif not util.safehasattr(i, '__iter__'):
312 326 if i is not None:
313 327 yield str(i)
314 328 elif i is not None:
315 329 for j in _flatten(i):
316 330 yield j
317 331
318 332 def parsestring(s, quoted=True):
319 333 '''parse a string using simple c-like syntax.
320 334 string must be in quotes if quoted is True.'''
321 335 if quoted:
322 336 if len(s) < 2 or s[0] != s[-1]:
323 337 raise SyntaxError(_('unmatched quotes'))
324 338 return s[1:-1].decode('string_escape')
325 339
326 340 return s.decode('string_escape')
327 341
328 342 class engine(object):
329 343 '''template expansion engine.
330 344
331 345 template expansion works like this. a map file contains key=value
332 346 pairs. if value is quoted, it is treated as string. otherwise, it
333 347 is treated as name of template file.
334 348
335 349 templater is asked to expand a key in map. it looks up key, and
336 350 looks for strings like this: {foo}. it expands {foo} by looking up
337 351 foo in map, and substituting it. expansion is recursive: it stops
338 352 when there is no more {foo} to replace.
339 353
340 354 expansion also allows formatting and filtering.
341 355
342 356 format uses key to expand each item in list. syntax is
343 357 {key%format}.
344 358
345 359 filter uses function to transform value. syntax is
346 360 {key|filter1|filter2|...}.'''
347 361
348 362 def __init__(self, loader, filters={}, defaults={}):
349 363 self._loader = loader
350 364 self._filters = filters
351 365 self._defaults = defaults
352 366 self._cache = {}
353 367
354 368 def _load(self, t):
355 369 '''load, parse, and cache a template'''
356 370 if t not in self._cache:
357 371 self._cache[t] = compiletemplate(self._loader(t), self)
358 372 return self._cache[t]
359 373
360 374 def process(self, t, mapping):
361 375 '''Perform expansion. t is name of map element to expand.
362 376 mapping contains added elements for use during expansion. Is a
363 377 generator.'''
364 378 return _flatten(runtemplate(self, mapping, self._load(t)))
365 379
366 380 engines = {'default': engine}
367 381
368 382 class templater(object):
369 383
370 384 def __init__(self, mapfile, filters={}, defaults={}, cache={},
371 385 minchunk=1024, maxchunk=65536):
372 386 '''set up template engine.
373 387 mapfile is name of file to read map definitions from.
374 388 filters is dict of functions. each transforms a value into another.
375 389 defaults is dict of default map definitions.'''
376 390 self.mapfile = mapfile or 'template'
377 391 self.cache = cache.copy()
378 392 self.map = {}
379 393 self.base = (mapfile and os.path.dirname(mapfile)) or ''
380 394 self.filters = templatefilters.filters.copy()
381 395 self.filters.update(filters)
382 396 self.defaults = defaults
383 397 self.minchunk, self.maxchunk = minchunk, maxchunk
384 398 self.ecache = {}
385 399
386 400 if not mapfile:
387 401 return
388 402 if not os.path.exists(mapfile):
389 403 raise util.Abort(_('style not found: %s') % mapfile)
390 404
391 405 conf = config.config()
392 406 conf.read(mapfile)
393 407
394 408 for key, val in conf[''].items():
395 409 if not val:
396 410 raise SyntaxError(_('%s: missing value') % conf.source('', key))
397 411 if val[0] in "'\"":
398 412 try:
399 413 self.cache[key] = parsestring(val)
400 414 except SyntaxError, inst:
401 415 raise SyntaxError('%s: %s' %
402 416 (conf.source('', key), inst.args[0]))
403 417 else:
404 418 val = 'default', val
405 419 if ':' in val[1]:
406 420 val = val[1].split(':', 1)
407 421 self.map[key] = val[0], os.path.join(self.base, val[1])
408 422
409 423 def __contains__(self, key):
410 424 return key in self.cache or key in self.map
411 425
412 426 def load(self, t):
413 427 '''Get the template for the given template name. Use a local cache.'''
414 428 if t not in self.cache:
415 429 try:
416 430 self.cache[t] = util.readfile(self.map[t][1])
417 431 except KeyError, inst:
418 432 raise util.Abort(_('"%s" not in template map') % inst.args[0])
419 433 except IOError, inst:
420 434 raise IOError(inst.args[0], _('template file %s: %s') %
421 435 (self.map[t][1], inst.args[1]))
422 436 return self.cache[t]
423 437
424 438 def __call__(self, t, **mapping):
425 439 ttype = t in self.map and self.map[t][0] or 'default'
426 440 if ttype not in self.ecache:
427 441 self.ecache[ttype] = engines[ttype](self.load,
428 442 self.filters, self.defaults)
429 443 proc = self.ecache[ttype]
430 444
431 445 stream = proc.process(t, mapping)
432 446 if self.minchunk:
433 447 stream = util.increasingchunks(stream, min=self.minchunk,
434 448 max=self.maxchunk)
435 449 return stream
436 450
437 451 def templatepath(name=None):
438 452 '''return location of template file or directory (if no name).
439 453 returns None if not found.'''
440 454 normpaths = []
441 455
442 456 # executable version (py2exe) doesn't support __file__
443 457 if util.mainfrozen():
444 458 module = sys.executable
445 459 else:
446 460 module = __file__
447 461 for f in path:
448 462 if f.startswith('/'):
449 463 p = f
450 464 else:
451 465 fl = f.split('/')
452 466 p = os.path.join(os.path.dirname(module), *fl)
453 467 if name:
454 468 p = os.path.join(p, name)
455 469 if name and os.path.exists(p):
456 470 return os.path.normpath(p)
457 471 elif os.path.isdir(p):
458 472 normpaths.append(os.path.normpath(p))
459 473
460 474 return normpaths
461 475
462 476 def stylemap(styles, paths=None):
463 477 """Return path to mapfile for a given style.
464 478
465 479 Searches mapfile in the following locations:
466 480 1. templatepath/style/map
467 481 2. templatepath/map-style
468 482 3. templatepath/map
469 483 """
470 484
471 485 if paths is None:
472 486 paths = templatepath()
473 487 elif isinstance(paths, str):
474 488 paths = [paths]
475 489
476 490 if isinstance(styles, str):
477 491 styles = [styles]
478 492
479 493 for style in styles:
480 494 if not style:
481 495 continue
482 496 locations = [os.path.join(style, 'map'), 'map-' + style]
483 497 locations.append('map')
484 498
485 499 for path in paths:
486 500 for location in locations:
487 501 mapfile = os.path.join(path, location)
488 502 if os.path.isfile(mapfile):
489 503 return style, mapfile
490 504
491 505 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now