##// END OF EJS Templates
templater: replace 'ctx._repo' with 'ctx.repo()'
Matt Harbison -
r24337:696ab1a2 default
parent child Browse files
Show More
@@ -1,466 +1,466 b''
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 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 node import hex
9 9 import patch, util, error
10 10 import hbisect
11 11
12 12 # This helper class allows us to handle both:
13 13 # "{files}" (legacy command-line-specific list hack) and
14 14 # "{files % '{file}\n'}" (hgweb-style with inlining and function support)
15 15 # and to access raw values:
16 16 # "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
17 17 # "{get(extras, key)}"
18 18
19 19 class _hybrid(object):
20 20 def __init__(self, gen, values, makemap, joinfmt=None):
21 21 self.gen = gen
22 22 self.values = values
23 23 self._makemap = makemap
24 24 if joinfmt:
25 25 self.joinfmt = joinfmt
26 26 else:
27 27 self.joinfmt = lambda x: x.values()[0]
28 28 def __iter__(self):
29 29 return self.gen
30 30 def __call__(self):
31 31 makemap = self._makemap
32 32 for x in self.values:
33 33 yield makemap(x)
34 34 def __contains__(self, x):
35 35 return x in self.values
36 36 def __len__(self):
37 37 return len(self.values)
38 38 def __getattr__(self, name):
39 39 if name != 'get':
40 40 raise AttributeError(name)
41 41 return getattr(self.values, name)
42 42
43 43 def showlist(name, values, plural=None, element=None, **args):
44 44 if not element:
45 45 element = name
46 46 f = _showlist(name, values, plural, **args)
47 47 return _hybrid(f, values, lambda x: {element: x})
48 48
49 49 def _showlist(name, values, plural=None, **args):
50 50 '''expand set of values.
51 51 name is name of key in template map.
52 52 values is list of strings or dicts.
53 53 plural is plural of name, if not simply name + 's'.
54 54
55 55 expansion works like this, given name 'foo'.
56 56
57 57 if values is empty, expand 'no_foos'.
58 58
59 59 if 'foo' not in template map, return values as a string,
60 60 joined by space.
61 61
62 62 expand 'start_foos'.
63 63
64 64 for each value, expand 'foo'. if 'last_foo' in template
65 65 map, expand it instead of 'foo' for last key.
66 66
67 67 expand 'end_foos'.
68 68 '''
69 69 templ = args['templ']
70 70 if plural:
71 71 names = plural
72 72 else: names = name + 's'
73 73 if not values:
74 74 noname = 'no_' + names
75 75 if noname in templ:
76 76 yield templ(noname, **args)
77 77 return
78 78 if name not in templ:
79 79 if isinstance(values[0], str):
80 80 yield ' '.join(values)
81 81 else:
82 82 for v in values:
83 83 yield dict(v, **args)
84 84 return
85 85 startname = 'start_' + names
86 86 if startname in templ:
87 87 yield templ(startname, **args)
88 88 vargs = args.copy()
89 89 def one(v, tag=name):
90 90 try:
91 91 vargs.update(v)
92 92 except (AttributeError, ValueError):
93 93 try:
94 94 for a, b in v:
95 95 vargs[a] = b
96 96 except ValueError:
97 97 vargs[name] = v
98 98 return templ(tag, **vargs)
99 99 lastname = 'last_' + name
100 100 if lastname in templ:
101 101 last = values.pop()
102 102 else:
103 103 last = None
104 104 for v in values:
105 105 yield one(v)
106 106 if last is not None:
107 107 yield one(last, tag=lastname)
108 108 endname = 'end_' + names
109 109 if endname in templ:
110 110 yield templ(endname, **args)
111 111
112 112 def getfiles(repo, ctx, revcache):
113 113 if 'files' not in revcache:
114 114 revcache['files'] = repo.status(ctx.p1().node(), ctx.node())[:3]
115 115 return revcache['files']
116 116
117 117 def getlatesttags(repo, ctx, cache):
118 118 '''return date, distance and name for the latest tag of rev'''
119 119
120 120 if 'latesttags' not in cache:
121 121 # Cache mapping from rev to a tuple with tag date, tag
122 122 # distance and tag name
123 123 cache['latesttags'] = {-1: (0, 0, 'null')}
124 124 latesttags = cache['latesttags']
125 125
126 126 rev = ctx.rev()
127 127 todo = [rev]
128 128 while todo:
129 129 rev = todo.pop()
130 130 if rev in latesttags:
131 131 continue
132 132 ctx = repo[rev]
133 133 tags = [t for t in ctx.tags()
134 134 if (repo.tagtype(t) and repo.tagtype(t) != 'local')]
135 135 if tags:
136 136 latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
137 137 continue
138 138 try:
139 139 # The tuples are laid out so the right one can be found by
140 140 # comparison.
141 141 pdate, pdist, ptag = max(
142 142 latesttags[p.rev()] for p in ctx.parents())
143 143 except KeyError:
144 144 # Cache miss - recurse
145 145 todo.append(rev)
146 146 todo.extend(p.rev() for p in ctx.parents())
147 147 continue
148 148 latesttags[rev] = pdate, pdist + 1, ptag
149 149 return latesttags[rev]
150 150
151 151 def getrenamedfn(repo, endrev=None):
152 152 rcache = {}
153 153 if endrev is None:
154 154 endrev = len(repo)
155 155
156 156 def getrenamed(fn, rev):
157 157 '''looks up all renames for a file (up to endrev) the first
158 158 time the file is given. It indexes on the changerev and only
159 159 parses the manifest if linkrev != changerev.
160 160 Returns rename info for fn at changerev rev.'''
161 161 if fn not in rcache:
162 162 rcache[fn] = {}
163 163 fl = repo.file(fn)
164 164 for i in fl:
165 165 lr = fl.linkrev(i)
166 166 renamed = fl.renamed(fl.node(i))
167 167 rcache[fn][lr] = renamed
168 168 if lr >= endrev:
169 169 break
170 170 if rev in rcache[fn]:
171 171 return rcache[fn][rev]
172 172
173 173 # If linkrev != rev (i.e. rev not found in rcache) fallback to
174 174 # filectx logic.
175 175 try:
176 176 return repo[rev][fn].renamed()
177 177 except error.LookupError:
178 178 return None
179 179
180 180 return getrenamed
181 181
182 182
183 183 def showauthor(repo, ctx, templ, **args):
184 184 """:author: String. The unmodified author of the changeset."""
185 185 return ctx.user()
186 186
187 187 def showbisect(repo, ctx, templ, **args):
188 188 """:bisect: String. The changeset bisection status."""
189 189 return hbisect.label(repo, ctx.node())
190 190
191 191 def showbranch(**args):
192 192 """:branch: String. The name of the branch on which the changeset was
193 193 committed.
194 194 """
195 195 return args['ctx'].branch()
196 196
197 197 def showbranches(**args):
198 198 """:branches: List of strings. The name of the branch on which the
199 199 changeset was committed. Will be empty if the branch name was
200 200 default.
201 201 """
202 202 branch = args['ctx'].branch()
203 203 if branch != 'default':
204 204 return showlist('branch', [branch], plural='branches', **args)
205 205 return showlist('branch', [], plural='branches', **args)
206 206
207 207 def showbookmarks(**args):
208 208 """:bookmarks: List of strings. Any bookmarks associated with the
209 209 changeset.
210 210 """
211 211 repo = args['ctx']._repo
212 212 bookmarks = args['ctx'].bookmarks()
213 213 current = repo._bookmarkcurrent
214 214 makemap = lambda v: {'bookmark': v, 'current': current}
215 215 f = _showlist('bookmark', bookmarks, **args)
216 216 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
217 217
218 218 def showchildren(**args):
219 219 """:children: List of strings. The children of the changeset."""
220 220 ctx = args['ctx']
221 221 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
222 222 return showlist('children', childrevs, element='child', **args)
223 223
224 224 def showcurrentbookmark(**args):
225 225 """:currentbookmark: String. The active bookmark, if it is
226 226 associated with the changeset"""
227 227 import bookmarks as bookmarks # to avoid circular import issues
228 228 repo = args['repo']
229 229 if bookmarks.iscurrent(repo):
230 230 current = repo._bookmarkcurrent
231 231 if current in args['ctx'].bookmarks():
232 232 return current
233 233 return ''
234 234
235 235 def showdate(repo, ctx, templ, **args):
236 236 """:date: Date information. The date when the changeset was committed."""
237 237 return ctx.date()
238 238
239 239 def showdescription(repo, ctx, templ, **args):
240 240 """:desc: String. The text of the changeset description."""
241 241 return ctx.description().strip()
242 242
243 243 def showdiffstat(repo, ctx, templ, **args):
244 244 """:diffstat: String. Statistics of changes with the following format:
245 245 "modified files: +added/-removed lines"
246 246 """
247 247 stats = patch.diffstatdata(util.iterlines(ctx.diff()))
248 248 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
249 249 return '%s: +%s/-%s' % (len(stats), adds, removes)
250 250
251 251 def showextras(**args):
252 252 """:extras: List of dicts with key, value entries of the 'extras'
253 253 field of this changeset."""
254 254 extras = args['ctx'].extra()
255 255 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
256 256 makemap = lambda k: {'key': k, 'value': extras[k]}
257 257 c = [makemap(k) for k in extras]
258 258 f = _showlist('extra', c, plural='extras', **args)
259 259 return _hybrid(f, extras, makemap,
260 260 lambda x: '%s=%s' % (x['key'], x['value']))
261 261
262 262 def showfileadds(**args):
263 263 """:file_adds: List of strings. Files added by this changeset."""
264 264 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
265 265 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
266 266 element='file', **args)
267 267
268 268 def showfilecopies(**args):
269 269 """:file_copies: List of strings. Files copied in this changeset with
270 270 their sources.
271 271 """
272 272 cache, ctx = args['cache'], args['ctx']
273 273 copies = args['revcache'].get('copies')
274 274 if copies is None:
275 275 if 'getrenamed' not in cache:
276 276 cache['getrenamed'] = getrenamedfn(args['repo'])
277 277 copies = []
278 278 getrenamed = cache['getrenamed']
279 279 for fn in ctx.files():
280 280 rename = getrenamed(fn, ctx.rev())
281 281 if rename:
282 282 copies.append((fn, rename[0]))
283 283
284 284 copies = util.sortdict(copies)
285 285 makemap = lambda k: {'name': k, 'source': copies[k]}
286 286 c = [makemap(k) for k in copies]
287 287 f = _showlist('file_copy', c, plural='file_copies', **args)
288 288 return _hybrid(f, copies, makemap,
289 289 lambda x: '%s (%s)' % (x['name'], x['source']))
290 290
291 291 # showfilecopiesswitch() displays file copies only if copy records are
292 292 # provided before calling the templater, usually with a --copies
293 293 # command line switch.
294 294 def showfilecopiesswitch(**args):
295 295 """:file_copies_switch: List of strings. Like "file_copies" but displayed
296 296 only if the --copied switch is set.
297 297 """
298 298 copies = args['revcache'].get('copies') or []
299 299 copies = util.sortdict(copies)
300 300 makemap = lambda k: {'name': k, 'source': copies[k]}
301 301 c = [makemap(k) for k in copies]
302 302 f = _showlist('file_copy', c, plural='file_copies', **args)
303 303 return _hybrid(f, copies, makemap,
304 304 lambda x: '%s (%s)' % (x['name'], x['source']))
305 305
306 306 def showfiledels(**args):
307 307 """:file_dels: List of strings. Files removed by this changeset."""
308 308 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
309 309 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
310 310 element='file', **args)
311 311
312 312 def showfilemods(**args):
313 313 """:file_mods: List of strings. Files modified by this changeset."""
314 314 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
315 315 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
316 316 element='file', **args)
317 317
318 318 def showfiles(**args):
319 319 """:files: List of strings. All files modified, added, or removed by this
320 320 changeset.
321 321 """
322 322 return showlist('file', args['ctx'].files(), **args)
323 323
324 324 def showlatesttag(repo, ctx, templ, cache, **args):
325 325 """:latesttag: String. Most recent global tag in the ancestors of this
326 326 changeset.
327 327 """
328 328 return getlatesttags(repo, ctx, cache)[2]
329 329
330 330 def showlatesttagdistance(repo, ctx, templ, cache, **args):
331 331 """:latesttagdistance: Integer. Longest path to the latest tag."""
332 332 return getlatesttags(repo, ctx, cache)[1]
333 333
334 334 def showmanifest(**args):
335 335 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
336 336 args = args.copy()
337 337 args.update({'rev': repo.manifest.rev(ctx.changeset()[0]),
338 338 'node': hex(ctx.changeset()[0])})
339 339 return templ('manifest', **args)
340 340
341 341 def shownode(repo, ctx, templ, **args):
342 342 """:node: String. The changeset identification hash, as a 40 hexadecimal
343 343 digit string.
344 344 """
345 345 return ctx.hex()
346 346
347 347 def showp1rev(repo, ctx, templ, **args):
348 348 """:p1rev: Integer. The repository-local revision number of the changeset's
349 349 first parent, or -1 if the changeset has no parents."""
350 350 return ctx.p1().rev()
351 351
352 352 def showp2rev(repo, ctx, templ, **args):
353 353 """:p2rev: Integer. The repository-local revision number of the changeset's
354 354 second parent, or -1 if the changeset has no second parent."""
355 355 return ctx.p2().rev()
356 356
357 357 def showp1node(repo, ctx, templ, **args):
358 358 """:p1node: String. The identification hash of the changeset's first parent,
359 359 as a 40 digit hexadecimal string. If the changeset has no parents, all
360 360 digits are 0."""
361 361 return ctx.p1().hex()
362 362
363 363 def showp2node(repo, ctx, templ, **args):
364 364 """:p2node: String. The identification hash of the changeset's second
365 365 parent, as a 40 digit hexadecimal string. If the changeset has no second
366 366 parent, all digits are 0."""
367 367 return ctx.p2().hex()
368 368
369 369 def showphase(repo, ctx, templ, **args):
370 370 """:phase: String. The changeset phase name."""
371 371 return ctx.phasestr()
372 372
373 373 def showphaseidx(repo, ctx, templ, **args):
374 374 """:phaseidx: Integer. The changeset phase index."""
375 375 return ctx.phase()
376 376
377 377 def showrev(repo, ctx, templ, **args):
378 378 """:rev: Integer. The repository-local changeset revision number."""
379 379 return ctx.rev()
380 380
381 381 def showsubrepos(**args):
382 382 """:subrepos: List of strings. Updated subrepositories in the changeset."""
383 383 ctx = args['ctx']
384 384 substate = ctx.substate
385 385 if not substate:
386 386 return showlist('subrepo', [], **args)
387 387 psubstate = ctx.parents()[0].substate or {}
388 388 subrepos = []
389 389 for sub in substate:
390 390 if sub not in psubstate or substate[sub] != psubstate[sub]:
391 391 subrepos.append(sub) # modified or newly added in ctx
392 392 for sub in psubstate:
393 393 if sub not in substate:
394 394 subrepos.append(sub) # removed in ctx
395 395 return showlist('subrepo', sorted(subrepos), **args)
396 396
397 397 def shownames(namespace, **args):
398 398 """helper method to generate a template keyword for a namespace"""
399 399 ctx = args['ctx']
400 repo = ctx._repo
400 repo = ctx.repo()
401 401 ns = repo.names[namespace]
402 402 names = ns.names(repo, ctx.node())
403 403 return showlist(ns.templatename, names, plural=namespace, **args)
404 404
405 405 # don't remove "showtags" definition, even though namespaces will put
406 406 # a helper function for "tags" keyword into "keywords" map automatically,
407 407 # because online help text is built without namespaces initialization
408 408 def showtags(**args):
409 409 """:tags: List of strings. Any tags associated with the changeset."""
410 410 return shownames('tags', **args)
411 411
412 412 # keywords are callables like:
413 413 # fn(repo, ctx, templ, cache, revcache, **args)
414 414 # with:
415 415 # repo - current repository instance
416 416 # ctx - the changectx being displayed
417 417 # templ - the templater instance
418 418 # cache - a cache dictionary for the whole templater run
419 419 # revcache - a cache dictionary for the current revision
420 420 keywords = {
421 421 'author': showauthor,
422 422 'bisect': showbisect,
423 423 'branch': showbranch,
424 424 'branches': showbranches,
425 425 'bookmarks': showbookmarks,
426 426 'children': showchildren,
427 427 'currentbookmark': showcurrentbookmark,
428 428 'date': showdate,
429 429 'desc': showdescription,
430 430 'diffstat': showdiffstat,
431 431 'extras': showextras,
432 432 'file_adds': showfileadds,
433 433 'file_copies': showfilecopies,
434 434 'file_copies_switch': showfilecopiesswitch,
435 435 'file_dels': showfiledels,
436 436 'file_mods': showfilemods,
437 437 'files': showfiles,
438 438 'latesttag': showlatesttag,
439 439 'latesttagdistance': showlatesttagdistance,
440 440 'manifest': showmanifest,
441 441 'node': shownode,
442 442 'p1rev': showp1rev,
443 443 'p1node': showp1node,
444 444 'p2rev': showp2rev,
445 445 'p2node': showp2node,
446 446 'phase': showphase,
447 447 'phaseidx': showphaseidx,
448 448 'rev': showrev,
449 449 'subrepos': showsubrepos,
450 450 'tags': showtags,
451 451 }
452 452
453 453 def _showparents(**args):
454 454 """:parents: List of strings. The parents of the changeset in "rev:node"
455 455 format. If the changeset has only one "natural" parent (the predecessor
456 456 revision) nothing is shown."""
457 457 pass
458 458
459 459 dockeywords = {
460 460 'parents': _showparents,
461 461 }
462 462 dockeywords.update(keywords)
463 463 del dockeywords['branches']
464 464
465 465 # tell hggettext to extract docstrings from these functions:
466 466 i18nfunctions = dockeywords.values()
@@ -1,770 +1,770 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 os, re
10 10 import util, config, templatefilters, templatekw, parser, error
11 11 import revset as revsetmod
12 12 import types
13 13 import minirst
14 14
15 15 # template parsing
16 16
17 17 elements = {
18 18 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
19 19 ",": (2, None, ("list", 2)),
20 20 "|": (5, None, ("|", 5)),
21 21 "%": (6, None, ("%", 6)),
22 22 ")": (0, None, None),
23 23 "symbol": (0, ("symbol",), None),
24 24 "string": (0, ("string",), None),
25 25 "rawstring": (0, ("rawstring",), None),
26 26 "end": (0, None, None),
27 27 }
28 28
29 29 def tokenizer(data):
30 30 program, start, end = data
31 31 pos = start
32 32 while pos < end:
33 33 c = program[pos]
34 34 if c.isspace(): # skip inter-token whitespace
35 35 pass
36 36 elif c in "(,)%|": # handle simple operators
37 37 yield (c, None, pos)
38 38 elif (c in '"\'' or c == 'r' and
39 39 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
40 40 if c == 'r':
41 41 pos += 1
42 42 c = program[pos]
43 43 decode = False
44 44 else:
45 45 decode = True
46 46 pos += 1
47 47 s = pos
48 48 while pos < end: # find closing quote
49 49 d = program[pos]
50 50 if decode and d == '\\': # skip over escaped characters
51 51 pos += 2
52 52 continue
53 53 if d == c:
54 54 if not decode:
55 55 yield ('rawstring', program[s:pos], s)
56 56 break
57 57 yield ('string', program[s:pos], s)
58 58 break
59 59 pos += 1
60 60 else:
61 61 raise error.ParseError(_("unterminated string"), s)
62 62 elif c.isalnum() or c in '_':
63 63 s = pos
64 64 pos += 1
65 65 while pos < end: # find end of symbol
66 66 d = program[pos]
67 67 if not (d.isalnum() or d == "_"):
68 68 break
69 69 pos += 1
70 70 sym = program[s:pos]
71 71 yield ('symbol', sym, s)
72 72 pos -= 1
73 73 elif c == '}':
74 74 pos += 1
75 75 break
76 76 else:
77 77 raise error.ParseError(_("syntax error"), pos)
78 78 pos += 1
79 79 yield ('end', None, pos)
80 80
81 81 def compiletemplate(tmpl, context, strtoken="string"):
82 82 parsed = []
83 83 pos, stop = 0, len(tmpl)
84 84 p = parser.parser(tokenizer, elements)
85 85 while pos < stop:
86 86 n = tmpl.find('{', pos)
87 87 if n < 0:
88 88 parsed.append((strtoken, tmpl[pos:]))
89 89 break
90 90 if n > 0 and tmpl[n - 1] == '\\':
91 91 # escaped
92 92 parsed.append((strtoken, (tmpl[pos:n - 1] + "{")))
93 93 pos = n + 1
94 94 continue
95 95 if n > pos:
96 96 parsed.append((strtoken, tmpl[pos:n]))
97 97
98 98 pd = [tmpl, n + 1, stop]
99 99 parseres, pos = p.parse(pd)
100 100 parsed.append(parseres)
101 101
102 102 return [compileexp(e, context) for e in parsed]
103 103
104 104 def compileexp(exp, context):
105 105 t = exp[0]
106 106 if t in methods:
107 107 return methods[t](exp, context)
108 108 raise error.ParseError(_("unknown method '%s'") % t)
109 109
110 110 # template evaluation
111 111
112 112 def getsymbol(exp):
113 113 if exp[0] == 'symbol':
114 114 return exp[1]
115 115 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
116 116
117 117 def getlist(x):
118 118 if not x:
119 119 return []
120 120 if x[0] == 'list':
121 121 return getlist(x[1]) + [x[2]]
122 122 return [x]
123 123
124 124 def getfilter(exp, context):
125 125 f = getsymbol(exp)
126 126 if f not in context._filters:
127 127 raise error.ParseError(_("unknown function '%s'") % f)
128 128 return context._filters[f]
129 129
130 130 def gettemplate(exp, context):
131 131 if exp[0] == 'string' or exp[0] == 'rawstring':
132 132 return compiletemplate(exp[1], context, strtoken=exp[0])
133 133 if exp[0] == 'symbol':
134 134 return context._load(exp[1])
135 135 raise error.ParseError(_("expected template specifier"))
136 136
137 137 def runstring(context, mapping, data):
138 138 return data.decode("string-escape")
139 139
140 140 def runrawstring(context, mapping, data):
141 141 return data
142 142
143 143 def runsymbol(context, mapping, key):
144 144 v = mapping.get(key)
145 145 if v is None:
146 146 v = context._defaults.get(key)
147 147 if v is None:
148 148 try:
149 149 v = context.process(key, mapping)
150 150 except TemplateNotFound:
151 151 v = ''
152 152 if callable(v):
153 153 return v(**mapping)
154 154 if isinstance(v, types.GeneratorType):
155 155 v = list(v)
156 156 return v
157 157
158 158 def buildfilter(exp, context):
159 159 func, data = compileexp(exp[1], context)
160 160 filt = getfilter(exp[2], context)
161 161 return (runfilter, (func, data, filt))
162 162
163 163 def runfilter(context, mapping, data):
164 164 func, data, filt = data
165 165 # func() may return string, generator of strings or arbitrary object such
166 166 # as date tuple, but filter does not want generator.
167 167 thing = func(context, mapping, data)
168 168 if isinstance(thing, types.GeneratorType):
169 169 thing = stringify(thing)
170 170 try:
171 171 return filt(thing)
172 172 except (ValueError, AttributeError, TypeError):
173 173 if isinstance(data, tuple):
174 174 dt = data[1]
175 175 else:
176 176 dt = data
177 177 raise util.Abort(_("template filter '%s' is not compatible with "
178 178 "keyword '%s'") % (filt.func_name, dt))
179 179
180 180 def buildmap(exp, context):
181 181 func, data = compileexp(exp[1], context)
182 182 ctmpl = gettemplate(exp[2], context)
183 183 return (runmap, (func, data, ctmpl))
184 184
185 185 def runtemplate(context, mapping, template):
186 186 for func, data in template:
187 187 yield func(context, mapping, data)
188 188
189 189 def runmap(context, mapping, data):
190 190 func, data, ctmpl = data
191 191 d = func(context, mapping, data)
192 192 if callable(d):
193 193 d = d()
194 194
195 195 lm = mapping.copy()
196 196
197 197 for i in d:
198 198 if isinstance(i, dict):
199 199 lm.update(i)
200 200 lm['originalnode'] = mapping.get('node')
201 201 yield runtemplate(context, lm, ctmpl)
202 202 else:
203 203 # v is not an iterable of dicts, this happen when 'key'
204 204 # has been fully expanded already and format is useless.
205 205 # If so, return the expanded value.
206 206 yield i
207 207
208 208 def buildfunc(exp, context):
209 209 n = getsymbol(exp[1])
210 210 args = [compileexp(x, context) for x in getlist(exp[2])]
211 211 if n in funcs:
212 212 f = funcs[n]
213 213 return (f, args)
214 214 if n in context._filters:
215 215 if len(args) != 1:
216 216 raise error.ParseError(_("filter %s expects one argument") % n)
217 217 f = context._filters[n]
218 218 return (runfilter, (args[0][0], args[0][1], f))
219 219 raise error.ParseError(_("unknown function '%s'") % n)
220 220
221 221 def date(context, mapping, args):
222 222 if not (1 <= len(args) <= 2):
223 223 # i18n: "date" is a keyword
224 224 raise error.ParseError(_("date expects one or two arguments"))
225 225
226 226 date = args[0][0](context, mapping, args[0][1])
227 227 if len(args) == 2:
228 228 fmt = stringify(args[1][0](context, mapping, args[1][1]))
229 229 return util.datestr(date, fmt)
230 230 return util.datestr(date)
231 231
232 232 def diff(context, mapping, args):
233 233 if len(args) > 2:
234 234 # i18n: "diff" is a keyword
235 235 raise error.ParseError(_("diff expects one, two or no arguments"))
236 236
237 237 def getpatterns(i):
238 238 if i < len(args):
239 239 s = args[i][1].strip()
240 240 if s:
241 241 return [s]
242 242 return []
243 243
244 244 ctx = mapping['ctx']
245 245 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
246 246
247 247 return ''.join(chunks)
248 248
249 249 def fill(context, mapping, args):
250 250 if not (1 <= len(args) <= 4):
251 251 # i18n: "fill" is a keyword
252 252 raise error.ParseError(_("fill expects one to four arguments"))
253 253
254 254 text = stringify(args[0][0](context, mapping, args[0][1]))
255 255 width = 76
256 256 initindent = ''
257 257 hangindent = ''
258 258 if 2 <= len(args) <= 4:
259 259 try:
260 260 width = int(stringify(args[1][0](context, mapping, args[1][1])))
261 261 except ValueError:
262 262 # i18n: "fill" is a keyword
263 263 raise error.ParseError(_("fill expects an integer width"))
264 264 try:
265 265 initindent = stringify(_evalifliteral(args[2], context, mapping))
266 266 hangindent = stringify(_evalifliteral(args[3], context, mapping))
267 267 except IndexError:
268 268 pass
269 269
270 270 return templatefilters.fill(text, width, initindent, hangindent)
271 271
272 272 def pad(context, mapping, args):
273 273 """usage: pad(text, width, fillchar=' ', right=False)
274 274 """
275 275 if not (2 <= len(args) <= 4):
276 276 # i18n: "pad" is a keyword
277 277 raise error.ParseError(_("pad() expects two to four arguments"))
278 278
279 279 width = int(args[1][1])
280 280
281 281 text = stringify(args[0][0](context, mapping, args[0][1]))
282 282 if args[0][0] == runstring:
283 283 text = stringify(runtemplate(context, mapping,
284 284 compiletemplate(text, context)))
285 285
286 286 right = False
287 287 fillchar = ' '
288 288 if len(args) > 2:
289 289 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
290 290 if len(args) > 3:
291 291 right = util.parsebool(args[3][1])
292 292
293 293 if right:
294 294 return text.rjust(width, fillchar)
295 295 else:
296 296 return text.ljust(width, fillchar)
297 297
298 298 def get(context, mapping, args):
299 299 if len(args) != 2:
300 300 # i18n: "get" is a keyword
301 301 raise error.ParseError(_("get() expects two arguments"))
302 302
303 303 dictarg = args[0][0](context, mapping, args[0][1])
304 304 if not util.safehasattr(dictarg, 'get'):
305 305 # i18n: "get" is a keyword
306 306 raise error.ParseError(_("get() expects a dict as first argument"))
307 307
308 308 key = args[1][0](context, mapping, args[1][1])
309 309 yield dictarg.get(key)
310 310
311 311 def _evalifliteral(arg, context, mapping):
312 312 t = stringify(arg[0](context, mapping, arg[1]))
313 313 if arg[0] == runstring or arg[0] == runrawstring:
314 314 yield runtemplate(context, mapping,
315 315 compiletemplate(t, context, strtoken='rawstring'))
316 316 else:
317 317 yield t
318 318
319 319 def if_(context, mapping, args):
320 320 if not (2 <= len(args) <= 3):
321 321 # i18n: "if" is a keyword
322 322 raise error.ParseError(_("if expects two or three arguments"))
323 323
324 324 test = stringify(args[0][0](context, mapping, args[0][1]))
325 325 if test:
326 326 yield _evalifliteral(args[1], context, mapping)
327 327 elif len(args) == 3:
328 328 yield _evalifliteral(args[2], context, mapping)
329 329
330 330 def ifcontains(context, mapping, args):
331 331 if not (3 <= len(args) <= 4):
332 332 # i18n: "ifcontains" is a keyword
333 333 raise error.ParseError(_("ifcontains expects three or four arguments"))
334 334
335 335 item = stringify(args[0][0](context, mapping, args[0][1]))
336 336 items = args[1][0](context, mapping, args[1][1])
337 337
338 338 if item in items:
339 339 yield _evalifliteral(args[2], context, mapping)
340 340 elif len(args) == 4:
341 341 yield _evalifliteral(args[3], context, mapping)
342 342
343 343 def ifeq(context, mapping, args):
344 344 if not (3 <= len(args) <= 4):
345 345 # i18n: "ifeq" is a keyword
346 346 raise error.ParseError(_("ifeq expects three or four arguments"))
347 347
348 348 test = stringify(args[0][0](context, mapping, args[0][1]))
349 349 match = stringify(args[1][0](context, mapping, args[1][1]))
350 350 if test == match:
351 351 yield _evalifliteral(args[2], context, mapping)
352 352 elif len(args) == 4:
353 353 yield _evalifliteral(args[3], context, mapping)
354 354
355 355 def join(context, mapping, args):
356 356 if not (1 <= len(args) <= 2):
357 357 # i18n: "join" is a keyword
358 358 raise error.ParseError(_("join expects one or two arguments"))
359 359
360 360 joinset = args[0][0](context, mapping, args[0][1])
361 361 if callable(joinset):
362 362 jf = joinset.joinfmt
363 363 joinset = [jf(x) for x in joinset()]
364 364
365 365 joiner = " "
366 366 if len(args) > 1:
367 367 joiner = stringify(args[1][0](context, mapping, args[1][1]))
368 368
369 369 first = True
370 370 for x in joinset:
371 371 if first:
372 372 first = False
373 373 else:
374 374 yield joiner
375 375 yield x
376 376
377 377 def label(context, mapping, args):
378 378 if len(args) != 2:
379 379 # i18n: "label" is a keyword
380 380 raise error.ParseError(_("label expects two arguments"))
381 381
382 382 # ignore args[0] (the label string) since this is supposed to be a a no-op
383 383 yield _evalifliteral(args[1], context, mapping)
384 384
385 385 def revset(context, mapping, args):
386 386 """usage: revset(query[, formatargs...])
387 387 """
388 388 if not len(args) > 0:
389 389 # i18n: "revset" is a keyword
390 390 raise error.ParseError(_("revset expects one or more arguments"))
391 391
392 392 raw = args[0][1]
393 393 ctx = mapping['ctx']
394 repo = ctx._repo
394 repo = ctx.repo()
395 395
396 396 def query(expr):
397 397 m = revsetmod.match(repo.ui, expr)
398 398 return m(repo)
399 399
400 400 if len(args) > 1:
401 401 formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]])
402 402 revs = query(revsetmod.formatspec(raw, *formatargs))
403 403 revs = list([str(r) for r in revs])
404 404 else:
405 405 revsetcache = mapping['cache'].setdefault("revsetcache", {})
406 406 if raw in revsetcache:
407 407 revs = revsetcache[raw]
408 408 else:
409 409 revs = query(raw)
410 410 revs = list([str(r) for r in revs])
411 411 revsetcache[raw] = revs
412 412
413 413 return templatekw.showlist("revision", revs, **mapping)
414 414
415 415 def rstdoc(context, mapping, args):
416 416 if len(args) != 2:
417 417 # i18n: "rstdoc" is a keyword
418 418 raise error.ParseError(_("rstdoc expects two arguments"))
419 419
420 420 text = stringify(args[0][0](context, mapping, args[0][1]))
421 421 style = stringify(args[1][0](context, mapping, args[1][1]))
422 422
423 423 return minirst.format(text, style=style, keep=['verbose'])
424 424
425 425 def shortest(context, mapping, args):
426 426 """usage: shortest(node, minlength=4)
427 427 """
428 428 if not (1 <= len(args) <= 2):
429 429 # i18n: "shortest" is a keyword
430 430 raise error.ParseError(_("shortest() expects one or two arguments"))
431 431
432 432 node = stringify(args[0][0](context, mapping, args[0][1]))
433 433
434 434 minlength = 4
435 435 if len(args) > 1:
436 436 minlength = int(args[1][1])
437 437
438 438 cl = mapping['ctx']._repo.changelog
439 439 def isvalid(test):
440 440 try:
441 441 try:
442 442 cl.index.partialmatch(test)
443 443 except AttributeError:
444 444 # Pure mercurial doesn't support partialmatch on the index.
445 445 # Fallback to the slow way.
446 446 if cl._partialmatch(test) is None:
447 447 return False
448 448
449 449 try:
450 450 i = int(test)
451 451 # if we are a pure int, then starting with zero will not be
452 452 # confused as a rev; or, obviously, if the int is larger than
453 453 # the value of the tip rev
454 454 if test[0] == '0' or i > len(cl):
455 455 return True
456 456 return False
457 457 except ValueError:
458 458 return True
459 459 except error.RevlogError:
460 460 return False
461 461
462 462 shortest = node
463 463 startlength = max(6, minlength)
464 464 length = startlength
465 465 while True:
466 466 test = node[:length]
467 467 if isvalid(test):
468 468 shortest = test
469 469 if length == minlength or length > startlength:
470 470 return shortest
471 471 length -= 1
472 472 else:
473 473 length += 1
474 474 if len(shortest) <= length:
475 475 return shortest
476 476
477 477 def strip(context, mapping, args):
478 478 if not (1 <= len(args) <= 2):
479 479 # i18n: "strip" is a keyword
480 480 raise error.ParseError(_("strip expects one or two arguments"))
481 481
482 482 text = stringify(args[0][0](context, mapping, args[0][1]))
483 483 if len(args) == 2:
484 484 chars = stringify(args[1][0](context, mapping, args[1][1]))
485 485 return text.strip(chars)
486 486 return text.strip()
487 487
488 488 def sub(context, mapping, args):
489 489 if len(args) != 3:
490 490 # i18n: "sub" is a keyword
491 491 raise error.ParseError(_("sub expects three arguments"))
492 492
493 493 pat = stringify(args[0][0](context, mapping, args[0][1]))
494 494 rpl = stringify(args[1][0](context, mapping, args[1][1]))
495 495 src = stringify(_evalifliteral(args[2], context, mapping))
496 496 yield re.sub(pat, rpl, src)
497 497
498 498 def startswith(context, mapping, args):
499 499 if len(args) != 2:
500 500 # i18n: "startswith" is a keyword
501 501 raise error.ParseError(_("startswith expects two arguments"))
502 502
503 503 patn = stringify(args[0][0](context, mapping, args[0][1]))
504 504 text = stringify(args[1][0](context, mapping, args[1][1]))
505 505 if text.startswith(patn):
506 506 return text
507 507 return ''
508 508
509 509
510 510 def word(context, mapping, args):
511 511 """return nth word from a string"""
512 512 if not (2 <= len(args) <= 3):
513 513 # i18n: "word" is a keyword
514 514 raise error.ParseError(_("word expects two or three arguments, got %d")
515 515 % len(args))
516 516
517 517 num = int(stringify(args[0][0](context, mapping, args[0][1])))
518 518 text = stringify(args[1][0](context, mapping, args[1][1]))
519 519 if len(args) == 3:
520 520 splitter = stringify(args[2][0](context, mapping, args[2][1]))
521 521 else:
522 522 splitter = None
523 523
524 524 tokens = text.split(splitter)
525 525 if num >= len(tokens):
526 526 return ''
527 527 else:
528 528 return tokens[num]
529 529
530 530 methods = {
531 531 "string": lambda e, c: (runstring, e[1]),
532 532 "rawstring": lambda e, c: (runrawstring, e[1]),
533 533 "symbol": lambda e, c: (runsymbol, e[1]),
534 534 "group": lambda e, c: compileexp(e[1], c),
535 535 # ".": buildmember,
536 536 "|": buildfilter,
537 537 "%": buildmap,
538 538 "func": buildfunc,
539 539 }
540 540
541 541 funcs = {
542 542 "date": date,
543 543 "diff": diff,
544 544 "fill": fill,
545 545 "get": get,
546 546 "if": if_,
547 547 "ifcontains": ifcontains,
548 548 "ifeq": ifeq,
549 549 "join": join,
550 550 "label": label,
551 551 "pad": pad,
552 552 "revset": revset,
553 553 "rstdoc": rstdoc,
554 554 "shortest": shortest,
555 555 "startswith": startswith,
556 556 "strip": strip,
557 557 "sub": sub,
558 558 "word": word,
559 559 }
560 560
561 561 # template engine
562 562
563 563 stringify = templatefilters.stringify
564 564
565 565 def _flatten(thing):
566 566 '''yield a single stream from a possibly nested set of iterators'''
567 567 if isinstance(thing, str):
568 568 yield thing
569 569 elif not util.safehasattr(thing, '__iter__'):
570 570 if thing is not None:
571 571 yield str(thing)
572 572 else:
573 573 for i in thing:
574 574 if isinstance(i, str):
575 575 yield i
576 576 elif not util.safehasattr(i, '__iter__'):
577 577 if i is not None:
578 578 yield str(i)
579 579 elif i is not None:
580 580 for j in _flatten(i):
581 581 yield j
582 582
583 583 def parsestring(s, quoted=True):
584 584 '''parse a string using simple c-like syntax.
585 585 string must be in quotes if quoted is True.'''
586 586 if quoted:
587 587 if len(s) < 2 or s[0] != s[-1]:
588 588 raise SyntaxError(_('unmatched quotes'))
589 589 return s[1:-1].decode('string_escape')
590 590
591 591 return s.decode('string_escape')
592 592
593 593 class engine(object):
594 594 '''template expansion engine.
595 595
596 596 template expansion works like this. a map file contains key=value
597 597 pairs. if value is quoted, it is treated as string. otherwise, it
598 598 is treated as name of template file.
599 599
600 600 templater is asked to expand a key in map. it looks up key, and
601 601 looks for strings like this: {foo}. it expands {foo} by looking up
602 602 foo in map, and substituting it. expansion is recursive: it stops
603 603 when there is no more {foo} to replace.
604 604
605 605 expansion also allows formatting and filtering.
606 606
607 607 format uses key to expand each item in list. syntax is
608 608 {key%format}.
609 609
610 610 filter uses function to transform value. syntax is
611 611 {key|filter1|filter2|...}.'''
612 612
613 613 def __init__(self, loader, filters={}, defaults={}):
614 614 self._loader = loader
615 615 self._filters = filters
616 616 self._defaults = defaults
617 617 self._cache = {}
618 618
619 619 def _load(self, t):
620 620 '''load, parse, and cache a template'''
621 621 if t not in self._cache:
622 622 self._cache[t] = compiletemplate(self._loader(t), self)
623 623 return self._cache[t]
624 624
625 625 def process(self, t, mapping):
626 626 '''Perform expansion. t is name of map element to expand.
627 627 mapping contains added elements for use during expansion. Is a
628 628 generator.'''
629 629 return _flatten(runtemplate(self, mapping, self._load(t)))
630 630
631 631 engines = {'default': engine}
632 632
633 633 def stylelist():
634 634 paths = templatepaths()
635 635 if not paths:
636 636 return _('no templates found, try `hg debuginstall` for more info')
637 637 dirlist = os.listdir(paths[0])
638 638 stylelist = []
639 639 for file in dirlist:
640 640 split = file.split(".")
641 641 if split[0] == "map-cmdline":
642 642 stylelist.append(split[1])
643 643 return ", ".join(sorted(stylelist))
644 644
645 645 class TemplateNotFound(util.Abort):
646 646 pass
647 647
648 648 class templater(object):
649 649
650 650 def __init__(self, mapfile, filters={}, defaults={}, cache={},
651 651 minchunk=1024, maxchunk=65536):
652 652 '''set up template engine.
653 653 mapfile is name of file to read map definitions from.
654 654 filters is dict of functions. each transforms a value into another.
655 655 defaults is dict of default map definitions.'''
656 656 self.mapfile = mapfile or 'template'
657 657 self.cache = cache.copy()
658 658 self.map = {}
659 659 if mapfile:
660 660 self.base = os.path.dirname(mapfile)
661 661 else:
662 662 self.base = ''
663 663 self.filters = templatefilters.filters.copy()
664 664 self.filters.update(filters)
665 665 self.defaults = defaults
666 666 self.minchunk, self.maxchunk = minchunk, maxchunk
667 667 self.ecache = {}
668 668
669 669 if not mapfile:
670 670 return
671 671 if not os.path.exists(mapfile):
672 672 raise util.Abort(_("style '%s' not found") % mapfile,
673 673 hint=_("available styles: %s") % stylelist())
674 674
675 675 conf = config.config()
676 676 conf.read(mapfile)
677 677
678 678 for key, val in conf[''].items():
679 679 if not val:
680 680 raise SyntaxError(_('%s: missing value') % conf.source('', key))
681 681 if val[0] in "'\"":
682 682 try:
683 683 self.cache[key] = parsestring(val)
684 684 except SyntaxError, inst:
685 685 raise SyntaxError('%s: %s' %
686 686 (conf.source('', key), inst.args[0]))
687 687 else:
688 688 val = 'default', val
689 689 if ':' in val[1]:
690 690 val = val[1].split(':', 1)
691 691 self.map[key] = val[0], os.path.join(self.base, val[1])
692 692
693 693 def __contains__(self, key):
694 694 return key in self.cache or key in self.map
695 695
696 696 def load(self, t):
697 697 '''Get the template for the given template name. Use a local cache.'''
698 698 if t not in self.cache:
699 699 try:
700 700 self.cache[t] = util.readfile(self.map[t][1])
701 701 except KeyError, inst:
702 702 raise TemplateNotFound(_('"%s" not in template map') %
703 703 inst.args[0])
704 704 except IOError, inst:
705 705 raise IOError(inst.args[0], _('template file %s: %s') %
706 706 (self.map[t][1], inst.args[1]))
707 707 return self.cache[t]
708 708
709 709 def __call__(self, t, **mapping):
710 710 ttype = t in self.map and self.map[t][0] or 'default'
711 711 if ttype not in self.ecache:
712 712 self.ecache[ttype] = engines[ttype](self.load,
713 713 self.filters, self.defaults)
714 714 proc = self.ecache[ttype]
715 715
716 716 stream = proc.process(t, mapping)
717 717 if self.minchunk:
718 718 stream = util.increasingchunks(stream, min=self.minchunk,
719 719 max=self.maxchunk)
720 720 return stream
721 721
722 722 def templatepaths():
723 723 '''return locations used for template files.'''
724 724 pathsrel = ['templates']
725 725 paths = [os.path.normpath(os.path.join(util.datapath, f))
726 726 for f in pathsrel]
727 727 return [p for p in paths if os.path.isdir(p)]
728 728
729 729 def templatepath(name):
730 730 '''return location of template file. returns None if not found.'''
731 731 for p in templatepaths():
732 732 f = os.path.join(p, name)
733 733 if os.path.exists(f):
734 734 return f
735 735 return None
736 736
737 737 def stylemap(styles, paths=None):
738 738 """Return path to mapfile for a given style.
739 739
740 740 Searches mapfile in the following locations:
741 741 1. templatepath/style/map
742 742 2. templatepath/map-style
743 743 3. templatepath/map
744 744 """
745 745
746 746 if paths is None:
747 747 paths = templatepaths()
748 748 elif isinstance(paths, str):
749 749 paths = [paths]
750 750
751 751 if isinstance(styles, str):
752 752 styles = [styles]
753 753
754 754 for style in styles:
755 755 # only plain name is allowed to honor template paths
756 756 if (not style
757 757 or style in (os.curdir, os.pardir)
758 758 or os.sep in style
759 759 or os.altsep and os.altsep in style):
760 760 continue
761 761 locations = [os.path.join(style, 'map'), 'map-' + style]
762 762 locations.append('map')
763 763
764 764 for path in paths:
765 765 for location in locations:
766 766 mapfile = os.path.join(path, location)
767 767 if os.path.isfile(mapfile):
768 768 return style, mapfile
769 769
770 770 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now