##// END OF EJS Templates
templater: drop 'templ' from resources dict...
Yuya Nishihara -
r37088:1101d674 default
parent child Browse files
Show More
@@ -1,809 +1,798 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 __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from .node import (
12 12 hex,
13 13 nullid,
14 14 )
15 15
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 hbisect,
20 20 i18n,
21 21 obsutil,
22 22 patch,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 templateutil,
27 27 util,
28 28 )
29 29
30 30 _hybrid = templateutil.hybrid
31 31 _mappable = templateutil.mappable
32 32 hybriddict = templateutil.hybriddict
33 33 hybridlist = templateutil.hybridlist
34 34 compatdict = templateutil.compatdict
35 35 compatlist = templateutil.compatlist
36 36 _showcompatlist = templateutil._showcompatlist
37 37
38 # TODO: temporary hack for porting; will be removed soon
39 class _fakecontextwrapper(object):
40 def __init__(self, templ):
41 self._templ = templ
42
43 def preload(self, t):
44 return t in self._templ
45
46 def process(self, t, mapping):
47 return self._templ.generatenamed(t, mapping)
48
49 38 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
50 context = _fakecontextwrapper(templ)
39 context = templ # this is actually a template context, not a templater
51 40 return _showcompatlist(context, mapping, name, values, plural, separator)
52 41
53 42 def showdict(name, data, mapping, plural=None, key='key', value='value',
54 43 fmt=None, separator=' '):
55 44 ui = mapping.get('ui')
56 45 if ui:
57 46 ui.deprecwarn("templatekw.showdict() is deprecated, use "
58 47 "templateutil.compatdict()", '4.6')
59 48 c = [{key: k, value: v} for k, v in data.iteritems()]
60 49 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
61 50 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
62 51
63 52 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
64 53 ui = mapping.get('ui')
65 54 if ui:
66 55 ui.deprecwarn("templatekw.showlist() is deprecated, use "
67 56 "templateutil.compatlist()", '4.6')
68 57 if not element:
69 58 element = name
70 59 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
71 60 return hybridlist(values, name=element, gen=f)
72 61
73 62 def getlatesttags(context, mapping, pattern=None):
74 63 '''return date, distance and name for the latest tag of rev'''
75 64 repo = context.resource(mapping, 'repo')
76 65 ctx = context.resource(mapping, 'ctx')
77 66 cache = context.resource(mapping, 'cache')
78 67
79 68 cachename = 'latesttags'
80 69 if pattern is not None:
81 70 cachename += '-' + pattern
82 71 match = util.stringmatcher(pattern)[2]
83 72 else:
84 73 match = util.always
85 74
86 75 if cachename not in cache:
87 76 # Cache mapping from rev to a tuple with tag date, tag
88 77 # distance and tag name
89 78 cache[cachename] = {-1: (0, 0, ['null'])}
90 79 latesttags = cache[cachename]
91 80
92 81 rev = ctx.rev()
93 82 todo = [rev]
94 83 while todo:
95 84 rev = todo.pop()
96 85 if rev in latesttags:
97 86 continue
98 87 ctx = repo[rev]
99 88 tags = [t for t in ctx.tags()
100 89 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
101 90 and match(t))]
102 91 if tags:
103 92 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
104 93 continue
105 94 try:
106 95 ptags = [latesttags[p.rev()] for p in ctx.parents()]
107 96 if len(ptags) > 1:
108 97 if ptags[0][2] == ptags[1][2]:
109 98 # The tuples are laid out so the right one can be found by
110 99 # comparison in this case.
111 100 pdate, pdist, ptag = max(ptags)
112 101 else:
113 102 def key(x):
114 103 changessincetag = len(repo.revs('only(%d, %s)',
115 104 ctx.rev(), x[2][0]))
116 105 # Smallest number of changes since tag wins. Date is
117 106 # used as tiebreaker.
118 107 return [-changessincetag, x[0]]
119 108 pdate, pdist, ptag = max(ptags, key=key)
120 109 else:
121 110 pdate, pdist, ptag = ptags[0]
122 111 except KeyError:
123 112 # Cache miss - recurse
124 113 todo.append(rev)
125 114 todo.extend(p.rev() for p in ctx.parents())
126 115 continue
127 116 latesttags[rev] = pdate, pdist + 1, ptag
128 117 return latesttags[rev]
129 118
130 119 def getrenamedfn(repo, endrev=None):
131 120 rcache = {}
132 121 if endrev is None:
133 122 endrev = len(repo)
134 123
135 124 def getrenamed(fn, rev):
136 125 '''looks up all renames for a file (up to endrev) the first
137 126 time the file is given. It indexes on the changerev and only
138 127 parses the manifest if linkrev != changerev.
139 128 Returns rename info for fn at changerev rev.'''
140 129 if fn not in rcache:
141 130 rcache[fn] = {}
142 131 fl = repo.file(fn)
143 132 for i in fl:
144 133 lr = fl.linkrev(i)
145 134 renamed = fl.renamed(fl.node(i))
146 135 rcache[fn][lr] = renamed
147 136 if lr >= endrev:
148 137 break
149 138 if rev in rcache[fn]:
150 139 return rcache[fn][rev]
151 140
152 141 # If linkrev != rev (i.e. rev not found in rcache) fallback to
153 142 # filectx logic.
154 143 try:
155 144 return repo[rev][fn].renamed()
156 145 except error.LookupError:
157 146 return None
158 147
159 148 return getrenamed
160 149
161 150 def getlogcolumns():
162 151 """Return a dict of log column labels"""
163 152 _ = pycompat.identity # temporarily disable gettext
164 153 # i18n: column positioning for "hg log"
165 154 columns = _('bookmark: %s\n'
166 155 'branch: %s\n'
167 156 'changeset: %s\n'
168 157 'copies: %s\n'
169 158 'date: %s\n'
170 159 'extra: %s=%s\n'
171 160 'files+: %s\n'
172 161 'files-: %s\n'
173 162 'files: %s\n'
174 163 'instability: %s\n'
175 164 'manifest: %s\n'
176 165 'obsolete: %s\n'
177 166 'parent: %s\n'
178 167 'phase: %s\n'
179 168 'summary: %s\n'
180 169 'tag: %s\n'
181 170 'user: %s\n')
182 171 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
183 172 i18n._(columns).splitlines(True)))
184 173
185 174 # default templates internally used for rendering of lists
186 175 defaulttempl = {
187 176 'parent': '{rev}:{node|formatnode} ',
188 177 'manifest': '{rev}:{node|formatnode}',
189 178 'file_copy': '{name} ({source})',
190 179 'envvar': '{key}={value}',
191 180 'extra': '{key}={value|stringescape}'
192 181 }
193 182 # filecopy is preserved for compatibility reasons
194 183 defaulttempl['filecopy'] = defaulttempl['file_copy']
195 184
196 185 # keywords are callables (see registrar.templatekeyword for details)
197 186 keywords = {}
198 187 templatekeyword = registrar.templatekeyword(keywords)
199 188
200 189 @templatekeyword('author', requires={'ctx'})
201 190 def showauthor(context, mapping):
202 191 """String. The unmodified author of the changeset."""
203 192 ctx = context.resource(mapping, 'ctx')
204 193 return ctx.user()
205 194
206 195 @templatekeyword('bisect', requires={'repo', 'ctx'})
207 196 def showbisect(context, mapping):
208 197 """String. The changeset bisection status."""
209 198 repo = context.resource(mapping, 'repo')
210 199 ctx = context.resource(mapping, 'ctx')
211 200 return hbisect.label(repo, ctx.node())
212 201
213 202 @templatekeyword('branch', requires={'ctx'})
214 203 def showbranch(context, mapping):
215 204 """String. The name of the branch on which the changeset was
216 205 committed.
217 206 """
218 207 ctx = context.resource(mapping, 'ctx')
219 208 return ctx.branch()
220 209
221 210 @templatekeyword('branches', requires={'ctx'})
222 211 def showbranches(context, mapping):
223 212 """List of strings. The name of the branch on which the
224 213 changeset was committed. Will be empty if the branch name was
225 214 default. (DEPRECATED)
226 215 """
227 216 ctx = context.resource(mapping, 'ctx')
228 217 branch = ctx.branch()
229 218 if branch != 'default':
230 219 return compatlist(context, mapping, 'branch', [branch],
231 220 plural='branches')
232 221 return compatlist(context, mapping, 'branch', [], plural='branches')
233 222
234 223 @templatekeyword('bookmarks', requires={'repo', 'ctx'})
235 224 def showbookmarks(context, mapping):
236 225 """List of strings. Any bookmarks associated with the
237 226 changeset. Also sets 'active', the name of the active bookmark.
238 227 """
239 228 repo = context.resource(mapping, 'repo')
240 229 ctx = context.resource(mapping, 'ctx')
241 230 bookmarks = ctx.bookmarks()
242 231 active = repo._activebookmark
243 232 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
244 233 f = _showcompatlist(context, mapping, 'bookmark', bookmarks)
245 234 return _hybrid(f, bookmarks, makemap, pycompat.identity)
246 235
247 236 @templatekeyword('children', requires={'ctx'})
248 237 def showchildren(context, mapping):
249 238 """List of strings. The children of the changeset."""
250 239 ctx = context.resource(mapping, 'ctx')
251 240 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
252 241 return compatlist(context, mapping, 'children', childrevs, element='child')
253 242
254 243 # Deprecated, but kept alive for help generation a purpose.
255 244 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
256 245 def showcurrentbookmark(context, mapping):
257 246 """String. The active bookmark, if it is associated with the changeset.
258 247 (DEPRECATED)"""
259 248 return showactivebookmark(context, mapping)
260 249
261 250 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
262 251 def showactivebookmark(context, mapping):
263 252 """String. The active bookmark, if it is associated with the changeset."""
264 253 repo = context.resource(mapping, 'repo')
265 254 ctx = context.resource(mapping, 'ctx')
266 255 active = repo._activebookmark
267 256 if active and active in ctx.bookmarks():
268 257 return active
269 258 return ''
270 259
271 260 @templatekeyword('date', requires={'ctx'})
272 261 def showdate(context, mapping):
273 262 """Date information. The date when the changeset was committed."""
274 263 ctx = context.resource(mapping, 'ctx')
275 264 return ctx.date()
276 265
277 266 @templatekeyword('desc', requires={'ctx'})
278 267 def showdescription(context, mapping):
279 268 """String. The text of the changeset description."""
280 269 ctx = context.resource(mapping, 'ctx')
281 270 s = ctx.description()
282 271 if isinstance(s, encoding.localstr):
283 272 # try hard to preserve utf-8 bytes
284 273 return encoding.tolocal(encoding.fromlocal(s).strip())
285 274 else:
286 275 return s.strip()
287 276
288 277 @templatekeyword('diffstat', requires={'ctx'})
289 278 def showdiffstat(context, mapping):
290 279 """String. Statistics of changes with the following format:
291 280 "modified files: +added/-removed lines"
292 281 """
293 282 ctx = context.resource(mapping, 'ctx')
294 283 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
295 284 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
296 285 return '%d: +%d/-%d' % (len(stats), adds, removes)
297 286
298 287 @templatekeyword('envvars', requires={'ui'})
299 288 def showenvvars(context, mapping):
300 289 """A dictionary of environment variables. (EXPERIMENTAL)"""
301 290 ui = context.resource(mapping, 'ui')
302 291 env = ui.exportableenviron()
303 292 env = util.sortdict((k, env[k]) for k in sorted(env))
304 293 return compatdict(context, mapping, 'envvar', env, plural='envvars')
305 294
306 295 @templatekeyword('extras', requires={'ctx'})
307 296 def showextras(context, mapping):
308 297 """List of dicts with key, value entries of the 'extras'
309 298 field of this changeset."""
310 299 ctx = context.resource(mapping, 'ctx')
311 300 extras = ctx.extra()
312 301 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
313 302 makemap = lambda k: {'key': k, 'value': extras[k]}
314 303 c = [makemap(k) for k in extras]
315 304 f = _showcompatlist(context, mapping, 'extra', c, plural='extras')
316 305 return _hybrid(f, extras, makemap,
317 306 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
318 307
319 308 def _showfilesbystat(context, mapping, name, index):
320 309 repo = context.resource(mapping, 'repo')
321 310 ctx = context.resource(mapping, 'ctx')
322 311 revcache = context.resource(mapping, 'revcache')
323 312 if 'files' not in revcache:
324 313 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
325 314 files = revcache['files'][index]
326 315 return compatlist(context, mapping, name, files, element='file')
327 316
328 317 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache'})
329 318 def showfileadds(context, mapping):
330 319 """List of strings. Files added by this changeset."""
331 320 return _showfilesbystat(context, mapping, 'file_add', 1)
332 321
333 322 @templatekeyword('file_copies',
334 323 requires={'repo', 'ctx', 'cache', 'revcache'})
335 324 def showfilecopies(context, mapping):
336 325 """List of strings. Files copied in this changeset with
337 326 their sources.
338 327 """
339 328 repo = context.resource(mapping, 'repo')
340 329 ctx = context.resource(mapping, 'ctx')
341 330 cache = context.resource(mapping, 'cache')
342 331 copies = context.resource(mapping, 'revcache').get('copies')
343 332 if copies is None:
344 333 if 'getrenamed' not in cache:
345 334 cache['getrenamed'] = getrenamedfn(repo)
346 335 copies = []
347 336 getrenamed = cache['getrenamed']
348 337 for fn in ctx.files():
349 338 rename = getrenamed(fn, ctx.rev())
350 339 if rename:
351 340 copies.append((fn, rename[0]))
352 341
353 342 copies = util.sortdict(copies)
354 343 return compatdict(context, mapping, 'file_copy', copies,
355 344 key='name', value='source', fmt='%s (%s)',
356 345 plural='file_copies')
357 346
358 347 # showfilecopiesswitch() displays file copies only if copy records are
359 348 # provided before calling the templater, usually with a --copies
360 349 # command line switch.
361 350 @templatekeyword('file_copies_switch', requires={'revcache'})
362 351 def showfilecopiesswitch(context, mapping):
363 352 """List of strings. Like "file_copies" but displayed
364 353 only if the --copied switch is set.
365 354 """
366 355 copies = context.resource(mapping, 'revcache').get('copies') or []
367 356 copies = util.sortdict(copies)
368 357 return compatdict(context, mapping, 'file_copy', copies,
369 358 key='name', value='source', fmt='%s (%s)',
370 359 plural='file_copies')
371 360
372 361 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache'})
373 362 def showfiledels(context, mapping):
374 363 """List of strings. Files removed by this changeset."""
375 364 return _showfilesbystat(context, mapping, 'file_del', 2)
376 365
377 366 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache'})
378 367 def showfilemods(context, mapping):
379 368 """List of strings. Files modified by this changeset."""
380 369 return _showfilesbystat(context, mapping, 'file_mod', 0)
381 370
382 371 @templatekeyword('files', requires={'ctx'})
383 372 def showfiles(context, mapping):
384 373 """List of strings. All files modified, added, or removed by this
385 374 changeset.
386 375 """
387 376 ctx = context.resource(mapping, 'ctx')
388 377 return compatlist(context, mapping, 'file', ctx.files())
389 378
390 379 @templatekeyword('graphnode', requires={'repo', 'ctx'})
391 380 def showgraphnode(context, mapping):
392 381 """String. The character representing the changeset node in an ASCII
393 382 revision graph."""
394 383 repo = context.resource(mapping, 'repo')
395 384 ctx = context.resource(mapping, 'ctx')
396 385 return getgraphnode(repo, ctx)
397 386
398 387 def getgraphnode(repo, ctx):
399 388 wpnodes = repo.dirstate.parents()
400 389 if wpnodes[1] == nullid:
401 390 wpnodes = wpnodes[:1]
402 391 if ctx.node() in wpnodes:
403 392 return '@'
404 393 elif ctx.obsolete():
405 394 return 'x'
406 395 elif ctx.isunstable():
407 396 return '*'
408 397 elif ctx.closesbranch():
409 398 return '_'
410 399 else:
411 400 return 'o'
412 401
413 402 @templatekeyword('graphwidth', requires=())
414 403 def showgraphwidth(context, mapping):
415 404 """Integer. The width of the graph drawn by 'log --graph' or zero."""
416 405 # just hosts documentation; should be overridden by template mapping
417 406 return 0
418 407
419 408 @templatekeyword('index', requires=())
420 409 def showindex(context, mapping):
421 410 """Integer. The current iteration of the loop. (0 indexed)"""
422 411 # just hosts documentation; should be overridden by template mapping
423 412 raise error.Abort(_("can't use index in this context"))
424 413
425 414 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache'})
426 415 def showlatesttag(context, mapping):
427 416 """List of strings. The global tags on the most recent globally
428 417 tagged ancestor of this changeset. If no such tags exist, the list
429 418 consists of the single string "null".
430 419 """
431 420 return showlatesttags(context, mapping, None)
432 421
433 422 def showlatesttags(context, mapping, pattern):
434 423 """helper method for the latesttag keyword and function"""
435 424 latesttags = getlatesttags(context, mapping, pattern)
436 425
437 426 # latesttag[0] is an implementation detail for sorting csets on different
438 427 # branches in a stable manner- it is the date the tagged cset was created,
439 428 # not the date the tag was created. Therefore it isn't made visible here.
440 429 makemap = lambda v: {
441 430 'changes': _showchangessincetag,
442 431 'distance': latesttags[1],
443 432 'latesttag': v, # BC with {latesttag % '{latesttag}'}
444 433 'tag': v
445 434 }
446 435
447 436 tags = latesttags[2]
448 437 f = _showcompatlist(context, mapping, 'latesttag', tags, separator=':')
449 438 return _hybrid(f, tags, makemap, pycompat.identity)
450 439
451 440 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
452 441 def showlatesttagdistance(context, mapping):
453 442 """Integer. Longest path to the latest tag."""
454 443 return getlatesttags(context, mapping)[1]
455 444
456 445 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
457 446 def showchangessincelatesttag(context, mapping):
458 447 """Integer. All ancestors not in the latest tag."""
459 448 mapping = mapping.copy()
460 449 mapping['tag'] = getlatesttags(context, mapping)[2][0]
461 450 return _showchangessincetag(context, mapping)
462 451
463 452 def _showchangessincetag(context, mapping):
464 453 repo = context.resource(mapping, 'repo')
465 454 ctx = context.resource(mapping, 'ctx')
466 455 offset = 0
467 456 revs = [ctx.rev()]
468 457 tag = context.symbol(mapping, 'tag')
469 458
470 459 # The only() revset doesn't currently support wdir()
471 460 if ctx.rev() is None:
472 461 offset = 1
473 462 revs = [p.rev() for p in ctx.parents()]
474 463
475 464 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
476 465
477 466 # teach templater latesttags.changes is switched to (context, mapping) API
478 467 _showchangessincetag._requires = {'repo', 'ctx'}
479 468
480 469 @templatekeyword('manifest', requires={'repo', 'ctx'})
481 470 def showmanifest(context, mapping):
482 471 repo = context.resource(mapping, 'repo')
483 472 ctx = context.resource(mapping, 'ctx')
484 473 mnode = ctx.manifestnode()
485 474 if mnode is None:
486 475 # just avoid crash, we might want to use the 'ff...' hash in future
487 476 return
488 477 mrev = repo.manifestlog._revlog.rev(mnode)
489 478 mhex = hex(mnode)
490 479 mapping = mapping.copy()
491 480 mapping.update({'rev': mrev, 'node': mhex})
492 481 f = context.process('manifest', mapping)
493 482 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
494 483 # rev and node are completely different from changeset's.
495 484 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
496 485
497 486 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx'})
498 487 def showobsfate(context, mapping):
499 488 # this function returns a list containing pre-formatted obsfate strings.
500 489 #
501 490 # This function will be replaced by templates fragments when we will have
502 491 # the verbosity templatekw available.
503 492 succsandmarkers = showsuccsandmarkers(context, mapping)
504 493
505 494 ui = context.resource(mapping, 'ui')
506 495 values = []
507 496
508 497 for x in succsandmarkers:
509 498 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
510 499
511 500 return compatlist(context, mapping, "fate", values)
512 501
513 502 def shownames(context, mapping, namespace):
514 503 """helper method to generate a template keyword for a namespace"""
515 504 repo = context.resource(mapping, 'repo')
516 505 ctx = context.resource(mapping, 'ctx')
517 506 ns = repo.names[namespace]
518 507 names = ns.names(repo, ctx.node())
519 508 return compatlist(context, mapping, ns.templatename, names,
520 509 plural=namespace)
521 510
522 511 @templatekeyword('namespaces', requires={'repo', 'ctx'})
523 512 def shownamespaces(context, mapping):
524 513 """Dict of lists. Names attached to this changeset per
525 514 namespace."""
526 515 repo = context.resource(mapping, 'repo')
527 516 ctx = context.resource(mapping, 'ctx')
528 517
529 518 namespaces = util.sortdict()
530 519 def makensmapfn(ns):
531 520 # 'name' for iterating over namespaces, templatename for local reference
532 521 return lambda v: {'name': v, ns.templatename: v}
533 522
534 523 for k, ns in repo.names.iteritems():
535 524 names = ns.names(repo, ctx.node())
536 525 f = _showcompatlist(context, mapping, 'name', names)
537 526 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
538 527
539 528 f = _showcompatlist(context, mapping, 'namespace', list(namespaces))
540 529
541 530 def makemap(ns):
542 531 return {
543 532 'namespace': ns,
544 533 'names': namespaces[ns],
545 534 'builtin': repo.names[ns].builtin,
546 535 'colorname': repo.names[ns].colorname,
547 536 }
548 537
549 538 return _hybrid(f, namespaces, makemap, pycompat.identity)
550 539
551 540 @templatekeyword('node', requires={'ctx'})
552 541 def shownode(context, mapping):
553 542 """String. The changeset identification hash, as a 40 hexadecimal
554 543 digit string.
555 544 """
556 545 ctx = context.resource(mapping, 'ctx')
557 546 return ctx.hex()
558 547
559 548 @templatekeyword('obsolete', requires={'ctx'})
560 549 def showobsolete(context, mapping):
561 550 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
562 551 ctx = context.resource(mapping, 'ctx')
563 552 if ctx.obsolete():
564 553 return 'obsolete'
565 554 return ''
566 555
567 556 @templatekeyword('peerurls', requires={'repo'})
568 557 def showpeerurls(context, mapping):
569 558 """A dictionary of repository locations defined in the [paths] section
570 559 of your configuration file."""
571 560 repo = context.resource(mapping, 'repo')
572 561 # see commands.paths() for naming of dictionary keys
573 562 paths = repo.ui.paths
574 563 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
575 564 def makemap(k):
576 565 p = paths[k]
577 566 d = {'name': k, 'url': p.rawloc}
578 567 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
579 568 return d
580 569 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
581 570
582 571 @templatekeyword("predecessors", requires={'repo', 'ctx'})
583 572 def showpredecessors(context, mapping):
584 573 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
585 574 repo = context.resource(mapping, 'repo')
586 575 ctx = context.resource(mapping, 'ctx')
587 576 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
588 577 predecessors = map(hex, predecessors)
589 578
590 579 return _hybrid(None, predecessors,
591 580 lambda x: {'ctx': repo[x], 'revcache': {}},
592 581 lambda x: scmutil.formatchangeid(repo[x]))
593 582
594 583 @templatekeyword('reporoot', requires={'repo'})
595 584 def showreporoot(context, mapping):
596 585 """String. The root directory of the current repository."""
597 586 repo = context.resource(mapping, 'repo')
598 587 return repo.root
599 588
600 589 @templatekeyword("successorssets", requires={'repo', 'ctx'})
601 590 def showsuccessorssets(context, mapping):
602 591 """Returns a string of sets of successors for a changectx. Format used
603 592 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
604 593 while also diverged into ctx3. (EXPERIMENTAL)"""
605 594 repo = context.resource(mapping, 'repo')
606 595 ctx = context.resource(mapping, 'ctx')
607 596 if not ctx.obsolete():
608 597 return ''
609 598
610 599 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
611 600 ssets = [[hex(n) for n in ss] for ss in ssets]
612 601
613 602 data = []
614 603 for ss in ssets:
615 604 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
616 605 lambda x: scmutil.formatchangeid(repo[x]))
617 606 data.append(h)
618 607
619 608 # Format the successorssets
620 609 def render(d):
621 610 t = []
622 611 for i in d.gen():
623 612 t.append(i)
624 613 return "".join(t)
625 614
626 615 def gen(data):
627 616 yield "; ".join(render(d) for d in data)
628 617
629 618 return _hybrid(gen(data), data, lambda x: {'successorset': x},
630 619 pycompat.identity)
631 620
632 621 @templatekeyword("succsandmarkers", requires={'repo', 'ctx'})
633 622 def showsuccsandmarkers(context, mapping):
634 623 """Returns a list of dict for each final successor of ctx. The dict
635 624 contains successors node id in "successors" keys and the list of
636 625 obs-markers from ctx to the set of successors in "markers".
637 626 (EXPERIMENTAL)
638 627 """
639 628 repo = context.resource(mapping, 'repo')
640 629 ctx = context.resource(mapping, 'ctx')
641 630
642 631 values = obsutil.successorsandmarkers(repo, ctx)
643 632
644 633 if values is None:
645 634 values = []
646 635
647 636 # Format successors and markers to avoid exposing binary to templates
648 637 data = []
649 638 for i in values:
650 639 # Format successors
651 640 successors = i['successors']
652 641
653 642 successors = [hex(n) for n in successors]
654 643 successors = _hybrid(None, successors,
655 644 lambda x: {'ctx': repo[x], 'revcache': {}},
656 645 lambda x: scmutil.formatchangeid(repo[x]))
657 646
658 647 # Format markers
659 648 finalmarkers = []
660 649 for m in i['markers']:
661 650 hexprec = hex(m[0])
662 651 hexsucs = tuple(hex(n) for n in m[1])
663 652 hexparents = None
664 653 if m[5] is not None:
665 654 hexparents = tuple(hex(n) for n in m[5])
666 655 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
667 656 finalmarkers.append(newmarker)
668 657
669 658 data.append({'successors': successors, 'markers': finalmarkers})
670 659
671 660 f = _showcompatlist(context, mapping, 'succsandmarkers', data)
672 661 return _hybrid(f, data, lambda x: x, pycompat.identity)
673 662
674 663 @templatekeyword('p1rev', requires={'ctx'})
675 664 def showp1rev(context, mapping):
676 665 """Integer. The repository-local revision number of the changeset's
677 666 first parent, or -1 if the changeset has no parents."""
678 667 ctx = context.resource(mapping, 'ctx')
679 668 return ctx.p1().rev()
680 669
681 670 @templatekeyword('p2rev', requires={'ctx'})
682 671 def showp2rev(context, mapping):
683 672 """Integer. The repository-local revision number of the changeset's
684 673 second parent, or -1 if the changeset has no second parent."""
685 674 ctx = context.resource(mapping, 'ctx')
686 675 return ctx.p2().rev()
687 676
688 677 @templatekeyword('p1node', requires={'ctx'})
689 678 def showp1node(context, mapping):
690 679 """String. The identification hash of the changeset's first parent,
691 680 as a 40 digit hexadecimal string. If the changeset has no parents, all
692 681 digits are 0."""
693 682 ctx = context.resource(mapping, 'ctx')
694 683 return ctx.p1().hex()
695 684
696 685 @templatekeyword('p2node', requires={'ctx'})
697 686 def showp2node(context, mapping):
698 687 """String. The identification hash of the changeset's second
699 688 parent, as a 40 digit hexadecimal string. If the changeset has no second
700 689 parent, all digits are 0."""
701 690 ctx = context.resource(mapping, 'ctx')
702 691 return ctx.p2().hex()
703 692
704 693 @templatekeyword('parents', requires={'repo', 'ctx'})
705 694 def showparents(context, mapping):
706 695 """List of strings. The parents of the changeset in "rev:node"
707 696 format. If the changeset has only one "natural" parent (the predecessor
708 697 revision) nothing is shown."""
709 698 repo = context.resource(mapping, 'repo')
710 699 ctx = context.resource(mapping, 'ctx')
711 700 pctxs = scmutil.meaningfulparents(repo, ctx)
712 701 prevs = [p.rev() for p in pctxs]
713 702 parents = [[('rev', p.rev()),
714 703 ('node', p.hex()),
715 704 ('phase', p.phasestr())]
716 705 for p in pctxs]
717 706 f = _showcompatlist(context, mapping, 'parent', parents)
718 707 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
719 708 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
720 709
721 710 @templatekeyword('phase', requires={'ctx'})
722 711 def showphase(context, mapping):
723 712 """String. The changeset phase name."""
724 713 ctx = context.resource(mapping, 'ctx')
725 714 return ctx.phasestr()
726 715
727 716 @templatekeyword('phaseidx', requires={'ctx'})
728 717 def showphaseidx(context, mapping):
729 718 """Integer. The changeset phase index. (ADVANCED)"""
730 719 ctx = context.resource(mapping, 'ctx')
731 720 return ctx.phase()
732 721
733 722 @templatekeyword('rev', requires={'ctx'})
734 723 def showrev(context, mapping):
735 724 """Integer. The repository-local changeset revision number."""
736 725 ctx = context.resource(mapping, 'ctx')
737 726 return scmutil.intrev(ctx)
738 727
739 728 def showrevslist(context, mapping, name, revs):
740 729 """helper to generate a list of revisions in which a mapped template will
741 730 be evaluated"""
742 731 repo = context.resource(mapping, 'repo')
743 732 f = _showcompatlist(context, mapping, name, ['%d' % r for r in revs])
744 733 return _hybrid(f, revs,
745 734 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
746 735 pycompat.identity, keytype=int)
747 736
748 737 @templatekeyword('subrepos', requires={'ctx'})
749 738 def showsubrepos(context, mapping):
750 739 """List of strings. Updated subrepositories in the changeset."""
751 740 ctx = context.resource(mapping, 'ctx')
752 741 substate = ctx.substate
753 742 if not substate:
754 743 return compatlist(context, mapping, 'subrepo', [])
755 744 psubstate = ctx.parents()[0].substate or {}
756 745 subrepos = []
757 746 for sub in substate:
758 747 if sub not in psubstate or substate[sub] != psubstate[sub]:
759 748 subrepos.append(sub) # modified or newly added in ctx
760 749 for sub in psubstate:
761 750 if sub not in substate:
762 751 subrepos.append(sub) # removed in ctx
763 752 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
764 753
765 754 # don't remove "showtags" definition, even though namespaces will put
766 755 # a helper function for "tags" keyword into "keywords" map automatically,
767 756 # because online help text is built without namespaces initialization
768 757 @templatekeyword('tags', requires={'repo', 'ctx'})
769 758 def showtags(context, mapping):
770 759 """List of strings. Any tags associated with the changeset."""
771 760 return shownames(context, mapping, 'tags')
772 761
773 762 @templatekeyword('termwidth', requires={'ui'})
774 763 def showtermwidth(context, mapping):
775 764 """Integer. The width of the current terminal."""
776 765 ui = context.resource(mapping, 'ui')
777 766 return ui.termwidth()
778 767
779 768 @templatekeyword('instabilities', requires={'ctx'})
780 769 def showinstabilities(context, mapping):
781 770 """List of strings. Evolution instabilities affecting the changeset.
782 771 (EXPERIMENTAL)
783 772 """
784 773 ctx = context.resource(mapping, 'ctx')
785 774 return compatlist(context, mapping, 'instability', ctx.instabilities(),
786 775 plural='instabilities')
787 776
788 777 @templatekeyword('verbosity', requires={'ui'})
789 778 def showverbosity(context, mapping):
790 779 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
791 780 or ''."""
792 781 ui = context.resource(mapping, 'ui')
793 782 # see logcmdutil.changesettemplater for priority of these flags
794 783 if ui.debugflag:
795 784 return 'debug'
796 785 elif ui.quiet:
797 786 return 'quiet'
798 787 elif ui.verbose:
799 788 return 'verbose'
800 789 return ''
801 790
802 791 def loadkeyword(ui, extname, registrarobj):
803 792 """Load template keyword from specified registrarobj
804 793 """
805 794 for name, func in registrarobj._table.iteritems():
806 795 keywords[name] = func
807 796
808 797 # tell hggettext to extract docstrings from these functions:
809 798 i18nfunctions = keywords.values()
@@ -1,850 +1,849 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 """Slightly complicated template engine for commands and hgweb
9 9
10 10 This module provides low-level interface to the template engine. See the
11 11 formatter and cmdutil modules if you are looking for high-level functions
12 12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13 13
14 14 Internal Data Types
15 15 -------------------
16 16
17 17 Template keywords and functions take a dictionary of current symbols and
18 18 resources (a "mapping") and return result. Inputs and outputs must be one
19 19 of the following data types:
20 20
21 21 bytes
22 22 a byte string, which is generally a human-readable text in local encoding.
23 23
24 24 generator
25 25 a lazily-evaluated byte string, which is a possibly nested generator of
26 26 values of any printable types, and will be folded by ``stringify()``
27 27 or ``flatten()``.
28 28
29 29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 30 returns a generator of dicts.)
31 31
32 32 None
33 33 sometimes represents an empty value, which can be stringified to ''.
34 34
35 35 True, False, int, float
36 36 can be stringified as such.
37 37
38 38 date tuple
39 39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
40 40
41 41 hybrid
42 42 represents a list/dict of printable values, which can also be converted
43 43 to mappings by % operator.
44 44
45 45 mappable
46 46 represents a scalar printable value, also supports % operator.
47 47 """
48 48
49 49 from __future__ import absolute_import, print_function
50 50
51 51 import os
52 52
53 53 from .i18n import _
54 54 from . import (
55 55 config,
56 56 encoding,
57 57 error,
58 58 parser,
59 59 pycompat,
60 60 templatefilters,
61 61 templatefuncs,
62 62 templateutil,
63 63 util,
64 64 )
65 65
66 66 # template parsing
67 67
68 68 elements = {
69 69 # token-type: binding-strength, primary, prefix, infix, suffix
70 70 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
71 71 ".": (18, None, None, (".", 18), None),
72 72 "%": (15, None, None, ("%", 15), None),
73 73 "|": (15, None, None, ("|", 15), None),
74 74 "*": (5, None, None, ("*", 5), None),
75 75 "/": (5, None, None, ("/", 5), None),
76 76 "+": (4, None, None, ("+", 4), None),
77 77 "-": (4, None, ("negate", 19), ("-", 4), None),
78 78 "=": (3, None, None, ("keyvalue", 3), None),
79 79 ",": (2, None, None, ("list", 2), None),
80 80 ")": (0, None, None, None, None),
81 81 "integer": (0, "integer", None, None, None),
82 82 "symbol": (0, "symbol", None, None, None),
83 83 "string": (0, "string", None, None, None),
84 84 "template": (0, "template", None, None, None),
85 85 "end": (0, None, None, None, None),
86 86 }
87 87
88 88 def tokenize(program, start, end, term=None):
89 89 """Parse a template expression into a stream of tokens, which must end
90 90 with term if specified"""
91 91 pos = start
92 92 program = pycompat.bytestr(program)
93 93 while pos < end:
94 94 c = program[pos]
95 95 if c.isspace(): # skip inter-token whitespace
96 96 pass
97 97 elif c in "(=,).%|+-*/": # handle simple operators
98 98 yield (c, None, pos)
99 99 elif c in '"\'': # handle quoted templates
100 100 s = pos + 1
101 101 data, pos = _parsetemplate(program, s, end, c)
102 102 yield ('template', data, s)
103 103 pos -= 1
104 104 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
105 105 # handle quoted strings
106 106 c = program[pos + 1]
107 107 s = pos = pos + 2
108 108 while pos < end: # find closing quote
109 109 d = program[pos]
110 110 if d == '\\': # skip over escaped characters
111 111 pos += 2
112 112 continue
113 113 if d == c:
114 114 yield ('string', program[s:pos], s)
115 115 break
116 116 pos += 1
117 117 else:
118 118 raise error.ParseError(_("unterminated string"), s)
119 119 elif c.isdigit():
120 120 s = pos
121 121 while pos < end:
122 122 d = program[pos]
123 123 if not d.isdigit():
124 124 break
125 125 pos += 1
126 126 yield ('integer', program[s:pos], s)
127 127 pos -= 1
128 128 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
129 129 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
130 130 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
131 131 # where some of nested templates were preprocessed as strings and
132 132 # then compiled. therefore, \"...\" was allowed. (issue4733)
133 133 #
134 134 # processing flow of _evalifliteral() at 5ab28a2e9962:
135 135 # outer template string -> stringify() -> compiletemplate()
136 136 # ------------------------ ------------ ------------------
137 137 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
138 138 # ~~~~~~~~
139 139 # escaped quoted string
140 140 if c == 'r':
141 141 pos += 1
142 142 token = 'string'
143 143 else:
144 144 token = 'template'
145 145 quote = program[pos:pos + 2]
146 146 s = pos = pos + 2
147 147 while pos < end: # find closing escaped quote
148 148 if program.startswith('\\\\\\', pos, end):
149 149 pos += 4 # skip over double escaped characters
150 150 continue
151 151 if program.startswith(quote, pos, end):
152 152 # interpret as if it were a part of an outer string
153 153 data = parser.unescapestr(program[s:pos])
154 154 if token == 'template':
155 155 data = _parsetemplate(data, 0, len(data))[0]
156 156 yield (token, data, s)
157 157 pos += 1
158 158 break
159 159 pos += 1
160 160 else:
161 161 raise error.ParseError(_("unterminated string"), s)
162 162 elif c.isalnum() or c in '_':
163 163 s = pos
164 164 pos += 1
165 165 while pos < end: # find end of symbol
166 166 d = program[pos]
167 167 if not (d.isalnum() or d == "_"):
168 168 break
169 169 pos += 1
170 170 sym = program[s:pos]
171 171 yield ('symbol', sym, s)
172 172 pos -= 1
173 173 elif c == term:
174 174 yield ('end', None, pos)
175 175 return
176 176 else:
177 177 raise error.ParseError(_("syntax error"), pos)
178 178 pos += 1
179 179 if term:
180 180 raise error.ParseError(_("unterminated template expansion"), start)
181 181 yield ('end', None, pos)
182 182
183 183 def _parsetemplate(tmpl, start, stop, quote=''):
184 184 r"""
185 185 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
186 186 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
187 187 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
188 188 ([('string', 'foo'), ('symbol', 'bar')], 9)
189 189 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
190 190 ([('string', 'foo')], 4)
191 191 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
192 192 ([('string', 'foo"'), ('string', 'bar')], 9)
193 193 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
194 194 ([('string', 'foo\\')], 6)
195 195 """
196 196 parsed = []
197 197 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
198 198 if typ == 'string':
199 199 parsed.append((typ, val))
200 200 elif typ == 'template':
201 201 parsed.append(val)
202 202 elif typ == 'end':
203 203 return parsed, pos
204 204 else:
205 205 raise error.ProgrammingError('unexpected type: %s' % typ)
206 206 raise error.ProgrammingError('unterminated scanning of template')
207 207
208 208 def scantemplate(tmpl, raw=False):
209 209 r"""Scan (type, start, end) positions of outermost elements in template
210 210
211 211 If raw=True, a backslash is not taken as an escape character just like
212 212 r'' string in Python. Note that this is different from r'' literal in
213 213 template in that no template fragment can appear in r'', e.g. r'{foo}'
214 214 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
215 215 'foo'.
216 216
217 217 >>> list(scantemplate(b'foo{bar}"baz'))
218 218 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
219 219 >>> list(scantemplate(b'outer{"inner"}outer'))
220 220 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
221 221 >>> list(scantemplate(b'foo\\{escaped}'))
222 222 [('string', 0, 5), ('string', 5, 13)]
223 223 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
224 224 [('string', 0, 4), ('template', 4, 13)]
225 225 """
226 226 last = None
227 227 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
228 228 if last:
229 229 yield last + (pos,)
230 230 if typ == 'end':
231 231 return
232 232 else:
233 233 last = (typ, pos)
234 234 raise error.ProgrammingError('unterminated scanning of template')
235 235
236 236 def _scantemplate(tmpl, start, stop, quote='', raw=False):
237 237 """Parse template string into chunks of strings and template expressions"""
238 238 sepchars = '{' + quote
239 239 unescape = [parser.unescapestr, pycompat.identity][raw]
240 240 pos = start
241 241 p = parser.parser(elements)
242 242 try:
243 243 while pos < stop:
244 244 n = min((tmpl.find(c, pos, stop) for c in sepchars),
245 245 key=lambda n: (n < 0, n))
246 246 if n < 0:
247 247 yield ('string', unescape(tmpl[pos:stop]), pos)
248 248 pos = stop
249 249 break
250 250 c = tmpl[n:n + 1]
251 251 bs = 0 # count leading backslashes
252 252 if not raw:
253 253 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
254 254 if bs % 2 == 1:
255 255 # escaped (e.g. '\{', '\\\{', but not '\\{')
256 256 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
257 257 pos = n + 1
258 258 continue
259 259 if n > pos:
260 260 yield ('string', unescape(tmpl[pos:n]), pos)
261 261 if c == quote:
262 262 yield ('end', None, n + 1)
263 263 return
264 264
265 265 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
266 266 if not tmpl.startswith('}', pos):
267 267 raise error.ParseError(_("invalid token"), pos)
268 268 yield ('template', parseres, n)
269 269 pos += 1
270 270
271 271 if quote:
272 272 raise error.ParseError(_("unterminated string"), start)
273 273 except error.ParseError as inst:
274 274 if len(inst.args) > 1: # has location
275 275 loc = inst.args[1]
276 276 # Offset the caret location by the number of newlines before the
277 277 # location of the error, since we will replace one-char newlines
278 278 # with the two-char literal r'\n'.
279 279 offset = tmpl[:loc].count('\n')
280 280 tmpl = tmpl.replace('\n', br'\n')
281 281 # We want the caret to point to the place in the template that
282 282 # failed to parse, but in a hint we get a open paren at the
283 283 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
284 284 # to line up the caret with the location of the error.
285 285 inst.hint = (tmpl + '\n'
286 286 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
287 287 raise
288 288 yield ('end', None, pos)
289 289
290 290 def _unnesttemplatelist(tree):
291 291 """Expand list of templates to node tuple
292 292
293 293 >>> def f(tree):
294 294 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
295 295 >>> f((b'template', []))
296 296 (string '')
297 297 >>> f((b'template', [(b'string', b'foo')]))
298 298 (string 'foo')
299 299 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
300 300 (template
301 301 (string 'foo')
302 302 (symbol 'rev'))
303 303 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
304 304 (template
305 305 (symbol 'rev'))
306 306 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
307 307 (string 'foo')
308 308 """
309 309 if not isinstance(tree, tuple):
310 310 return tree
311 311 op = tree[0]
312 312 if op != 'template':
313 313 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
314 314
315 315 assert len(tree) == 2
316 316 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
317 317 if not xs:
318 318 return ('string', '') # empty template ""
319 319 elif len(xs) == 1 and xs[0][0] == 'string':
320 320 return xs[0] # fast path for string with no template fragment "x"
321 321 else:
322 322 return (op,) + xs
323 323
324 324 def parse(tmpl):
325 325 """Parse template string into tree"""
326 326 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
327 327 assert pos == len(tmpl), 'unquoted template should be consumed'
328 328 return _unnesttemplatelist(('template', parsed))
329 329
330 330 def _parseexpr(expr):
331 331 """Parse a template expression into tree
332 332
333 333 >>> _parseexpr(b'"foo"')
334 334 ('string', 'foo')
335 335 >>> _parseexpr(b'foo(bar)')
336 336 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
337 337 >>> _parseexpr(b'foo(')
338 338 Traceback (most recent call last):
339 339 ...
340 340 ParseError: ('not a prefix: end', 4)
341 341 >>> _parseexpr(b'"foo" "bar"')
342 342 Traceback (most recent call last):
343 343 ...
344 344 ParseError: ('invalid token', 7)
345 345 """
346 346 p = parser.parser(elements)
347 347 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
348 348 if pos != len(expr):
349 349 raise error.ParseError(_('invalid token'), pos)
350 350 return _unnesttemplatelist(tree)
351 351
352 352 def prettyformat(tree):
353 353 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
354 354
355 355 def compileexp(exp, context, curmethods):
356 356 """Compile parsed template tree to (func, data) pair"""
357 357 if not exp:
358 358 raise error.ParseError(_("missing argument"))
359 359 t = exp[0]
360 360 if t in curmethods:
361 361 return curmethods[t](exp, context)
362 362 raise error.ParseError(_("unknown method '%s'") % t)
363 363
364 364 # template evaluation
365 365
366 366 def getsymbol(exp):
367 367 if exp[0] == 'symbol':
368 368 return exp[1]
369 369 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
370 370
371 371 def getlist(x):
372 372 if not x:
373 373 return []
374 374 if x[0] == 'list':
375 375 return getlist(x[1]) + [x[2]]
376 376 return [x]
377 377
378 378 def gettemplate(exp, context):
379 379 """Compile given template tree or load named template from map file;
380 380 returns (func, data) pair"""
381 381 if exp[0] in ('template', 'string'):
382 382 return compileexp(exp, context, methods)
383 383 if exp[0] == 'symbol':
384 384 # unlike runsymbol(), here 'symbol' is always taken as template name
385 385 # even if it exists in mapping. this allows us to override mapping
386 386 # by web templates, e.g. 'changelogtag' is redefined in map file.
387 387 return context._load(exp[1])
388 388 raise error.ParseError(_("expected template specifier"))
389 389
390 390 def _runrecursivesymbol(context, mapping, key):
391 391 raise error.Abort(_("recursive reference '%s' in template") % key)
392 392
393 393 def buildtemplate(exp, context):
394 394 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
395 395 return (templateutil.runtemplate, ctmpl)
396 396
397 397 def buildfilter(exp, context):
398 398 n = getsymbol(exp[2])
399 399 if n in context._filters:
400 400 filt = context._filters[n]
401 401 arg = compileexp(exp[1], context, methods)
402 402 return (templateutil.runfilter, (arg, filt))
403 403 if n in context._funcs:
404 404 f = context._funcs[n]
405 405 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
406 406 return (f, args)
407 407 raise error.ParseError(_("unknown function '%s'") % n)
408 408
409 409 def buildmap(exp, context):
410 410 darg = compileexp(exp[1], context, methods)
411 411 targ = gettemplate(exp[2], context)
412 412 return (templateutil.runmap, (darg, targ))
413 413
414 414 def buildmember(exp, context):
415 415 darg = compileexp(exp[1], context, methods)
416 416 memb = getsymbol(exp[2])
417 417 return (templateutil.runmember, (darg, memb))
418 418
419 419 def buildnegate(exp, context):
420 420 arg = compileexp(exp[1], context, exprmethods)
421 421 return (templateutil.runnegate, arg)
422 422
423 423 def buildarithmetic(exp, context, func):
424 424 left = compileexp(exp[1], context, exprmethods)
425 425 right = compileexp(exp[2], context, exprmethods)
426 426 return (templateutil.runarithmetic, (func, left, right))
427 427
428 428 def buildfunc(exp, context):
429 429 n = getsymbol(exp[1])
430 430 if n in context._funcs:
431 431 f = context._funcs[n]
432 432 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
433 433 return (f, args)
434 434 if n in context._filters:
435 435 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
436 436 if len(args) != 1:
437 437 raise error.ParseError(_("filter %s expects one argument") % n)
438 438 f = context._filters[n]
439 439 return (templateutil.runfilter, (args[0], f))
440 440 raise error.ParseError(_("unknown function '%s'") % n)
441 441
442 442 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
443 443 """Compile parsed tree of function arguments into list or dict of
444 444 (func, data) pairs
445 445
446 446 >>> context = engine(lambda t: (templateutil.runsymbol, t))
447 447 >>> def fargs(expr, argspec):
448 448 ... x = _parseexpr(expr)
449 449 ... n = getsymbol(x[1])
450 450 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
451 451 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
452 452 ['l', 'k']
453 453 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
454 454 >>> list(args.keys()), list(args[b'opts'].keys())
455 455 (['opts'], ['opts', 'k'])
456 456 """
457 457 def compiledict(xs):
458 458 return util.sortdict((k, compileexp(x, context, curmethods))
459 459 for k, x in xs.iteritems())
460 460 def compilelist(xs):
461 461 return [compileexp(x, context, curmethods) for x in xs]
462 462
463 463 if not argspec:
464 464 # filter or function with no argspec: return list of positional args
465 465 return compilelist(getlist(exp))
466 466
467 467 # function with argspec: return dict of named args
468 468 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
469 469 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
470 470 keyvaluenode='keyvalue', keynode='symbol')
471 471 compargs = util.sortdict()
472 472 if varkey:
473 473 compargs[varkey] = compilelist(treeargs.pop(varkey))
474 474 if optkey:
475 475 compargs[optkey] = compiledict(treeargs.pop(optkey))
476 476 compargs.update(compiledict(treeargs))
477 477 return compargs
478 478
479 479 def buildkeyvaluepair(exp, content):
480 480 raise error.ParseError(_("can't use a key-value pair in this context"))
481 481
482 482 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
483 483 exprmethods = {
484 484 "integer": lambda e, c: (templateutil.runinteger, e[1]),
485 485 "string": lambda e, c: (templateutil.runstring, e[1]),
486 486 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
487 487 "template": buildtemplate,
488 488 "group": lambda e, c: compileexp(e[1], c, exprmethods),
489 489 ".": buildmember,
490 490 "|": buildfilter,
491 491 "%": buildmap,
492 492 "func": buildfunc,
493 493 "keyvalue": buildkeyvaluepair,
494 494 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
495 495 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
496 496 "negate": buildnegate,
497 497 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
498 498 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
499 499 }
500 500
501 501 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
502 502 methods = exprmethods.copy()
503 503 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
504 504
505 505 class _aliasrules(parser.basealiasrules):
506 506 """Parsing and expansion rule set of template aliases"""
507 507 _section = _('template alias')
508 508 _parse = staticmethod(_parseexpr)
509 509
510 510 @staticmethod
511 511 def _trygetfunc(tree):
512 512 """Return (name, args) if tree is func(...) or ...|filter; otherwise
513 513 None"""
514 514 if tree[0] == 'func' and tree[1][0] == 'symbol':
515 515 return tree[1][1], getlist(tree[2])
516 516 if tree[0] == '|' and tree[2][0] == 'symbol':
517 517 return tree[2][1], [tree[1]]
518 518
519 519 def expandaliases(tree, aliases):
520 520 """Return new tree of aliases are expanded"""
521 521 aliasmap = _aliasrules.buildmap(aliases)
522 522 return _aliasrules.expand(aliasmap, tree)
523 523
524 524 # template engine
525 525
526 526 def _flatten(thing):
527 527 '''yield a single stream from a possibly nested set of iterators'''
528 528 thing = templateutil.unwraphybrid(thing)
529 529 if isinstance(thing, bytes):
530 530 yield thing
531 531 elif isinstance(thing, str):
532 532 # We can only hit this on Python 3, and it's here to guard
533 533 # against infinite recursion.
534 534 raise error.ProgrammingError('Mercurial IO including templates is done'
535 535 ' with bytes, not strings, got %r' % thing)
536 536 elif thing is None:
537 537 pass
538 538 elif not util.safehasattr(thing, '__iter__'):
539 539 yield pycompat.bytestr(thing)
540 540 else:
541 541 for i in thing:
542 542 i = templateutil.unwraphybrid(i)
543 543 if isinstance(i, bytes):
544 544 yield i
545 545 elif i is None:
546 546 pass
547 547 elif not util.safehasattr(i, '__iter__'):
548 548 yield pycompat.bytestr(i)
549 549 else:
550 550 for j in _flatten(i):
551 551 yield j
552 552
553 553 def unquotestring(s):
554 554 '''unwrap quotes if any; otherwise returns unmodified string'''
555 555 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
556 556 return s
557 557 return s[1:-1]
558 558
559 559 class engine(object):
560 560 '''template expansion engine.
561 561
562 562 template expansion works like this. a map file contains key=value
563 563 pairs. if value is quoted, it is treated as string. otherwise, it
564 564 is treated as name of template file.
565 565
566 566 templater is asked to expand a key in map. it looks up key, and
567 567 looks for strings like this: {foo}. it expands {foo} by looking up
568 568 foo in map, and substituting it. expansion is recursive: it stops
569 569 when there is no more {foo} to replace.
570 570
571 571 expansion also allows formatting and filtering.
572 572
573 573 format uses key to expand each item in list. syntax is
574 574 {key%format}.
575 575
576 576 filter uses function to transform value. syntax is
577 577 {key|filter1|filter2|...}.'''
578 578
579 579 def __init__(self, loader, filters=None, defaults=None, resources=None,
580 580 aliases=()):
581 581 self._loader = loader
582 582 if filters is None:
583 583 filters = {}
584 584 self._filters = filters
585 585 self._funcs = templatefuncs.funcs # make this a parameter if needed
586 586 if defaults is None:
587 587 defaults = {}
588 588 if resources is None:
589 589 resources = {}
590 590 self._defaults = defaults
591 591 self._resources = resources
592 592 self._aliasmap = _aliasrules.buildmap(aliases)
593 593 self._cache = {} # key: (func, data)
594 594
595 595 def symbol(self, mapping, key):
596 596 """Resolve symbol to value or function; None if nothing found"""
597 597 v = None
598 598 if key not in self._resources:
599 599 v = mapping.get(key)
600 600 if v is None:
601 601 v = self._defaults.get(key)
602 602 return v
603 603
604 604 def resource(self, mapping, key):
605 605 """Return internal data (e.g. cache) used for keyword/function
606 606 evaluation"""
607 607 v = None
608 608 if key in self._resources:
609 609 v = self._resources[key](self, mapping, key)
610 610 if v is None:
611 611 raise templateutil.ResourceUnavailable(
612 612 _('template resource not available: %s') % key)
613 613 return v
614 614
615 615 def _load(self, t):
616 616 '''load, parse, and cache a template'''
617 617 if t not in self._cache:
618 618 # put poison to cut recursion while compiling 't'
619 619 self._cache[t] = (_runrecursivesymbol, t)
620 620 try:
621 621 x = parse(self._loader(t))
622 622 if self._aliasmap:
623 623 x = _aliasrules.expand(self._aliasmap, x)
624 624 self._cache[t] = compileexp(x, self, methods)
625 625 except: # re-raises
626 626 del self._cache[t]
627 627 raise
628 628 return self._cache[t]
629 629
630 630 def preload(self, t):
631 631 """Load, parse, and cache the specified template if available"""
632 632 try:
633 633 self._load(t)
634 634 return True
635 635 except templateutil.TemplateNotFound:
636 636 return False
637 637
638 638 def process(self, t, mapping):
639 639 '''Perform expansion. t is name of map element to expand.
640 640 mapping contains added elements for use during expansion. Is a
641 641 generator.'''
642 642 func, data = self._load(t)
643 643 return _flatten(func(self, mapping, data))
644 644
645 645 engines = {'default': engine}
646 646
647 647 def stylelist():
648 648 paths = templatepaths()
649 649 if not paths:
650 650 return _('no templates found, try `hg debuginstall` for more info')
651 651 dirlist = os.listdir(paths[0])
652 652 stylelist = []
653 653 for file in dirlist:
654 654 split = file.split(".")
655 655 if split[-1] in ('orig', 'rej'):
656 656 continue
657 657 if split[0] == "map-cmdline":
658 658 stylelist.append(split[1])
659 659 return ", ".join(sorted(stylelist))
660 660
661 661 def _readmapfile(mapfile):
662 662 """Load template elements from the given map file"""
663 663 if not os.path.exists(mapfile):
664 664 raise error.Abort(_("style '%s' not found") % mapfile,
665 665 hint=_("available styles: %s") % stylelist())
666 666
667 667 base = os.path.dirname(mapfile)
668 668 conf = config.config(includepaths=templatepaths())
669 669 conf.read(mapfile, remap={'': 'templates'})
670 670
671 671 cache = {}
672 672 tmap = {}
673 673 aliases = []
674 674
675 675 val = conf.get('templates', '__base__')
676 676 if val and val[0] not in "'\"":
677 677 # treat as a pointer to a base class for this style
678 678 path = util.normpath(os.path.join(base, val))
679 679
680 680 # fallback check in template paths
681 681 if not os.path.exists(path):
682 682 for p in templatepaths():
683 683 p2 = util.normpath(os.path.join(p, val))
684 684 if os.path.isfile(p2):
685 685 path = p2
686 686 break
687 687 p3 = util.normpath(os.path.join(p2, "map"))
688 688 if os.path.isfile(p3):
689 689 path = p3
690 690 break
691 691
692 692 cache, tmap, aliases = _readmapfile(path)
693 693
694 694 for key, val in conf['templates'].items():
695 695 if not val:
696 696 raise error.ParseError(_('missing value'),
697 697 conf.source('templates', key))
698 698 if val[0] in "'\"":
699 699 if val[0] != val[-1]:
700 700 raise error.ParseError(_('unmatched quotes'),
701 701 conf.source('templates', key))
702 702 cache[key] = unquotestring(val)
703 703 elif key != '__base__':
704 704 val = 'default', val
705 705 if ':' in val[1]:
706 706 val = val[1].split(':', 1)
707 707 tmap[key] = val[0], os.path.join(base, val[1])
708 708 aliases.extend(conf['templatealias'].items())
709 709 return cache, tmap, aliases
710 710
711 711 class templater(object):
712 712
713 713 def __init__(self, filters=None, defaults=None, resources=None,
714 714 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
715 715 """Create template engine optionally with preloaded template fragments
716 716
717 717 - ``filters``: a dict of functions to transform a value into another.
718 718 - ``defaults``: a dict of symbol values/functions; may be overridden
719 719 by a ``mapping`` dict.
720 720 - ``resources``: a dict of functions returning internal data
721 721 (e.g. cache), inaccessible from user template.
722 722 - ``cache``: a dict of preloaded template fragments.
723 723 - ``aliases``: a list of alias (name, replacement) pairs.
724 724
725 725 self.cache may be updated later to register additional template
726 726 fragments.
727 727 """
728 728 if filters is None:
729 729 filters = {}
730 730 if defaults is None:
731 731 defaults = {}
732 732 if resources is None:
733 733 resources = {}
734 734 if cache is None:
735 735 cache = {}
736 736 self.cache = cache.copy()
737 737 self.map = {}
738 738 self.filters = templatefilters.filters.copy()
739 739 self.filters.update(filters)
740 740 self.defaults = defaults
741 self._resources = {'templ': lambda context, mapping, key: self}
742 self._resources.update(resources)
741 self._resources = resources
743 742 self._aliases = aliases
744 743 self.minchunk, self.maxchunk = minchunk, maxchunk
745 744 self.ecache = {}
746 745
747 746 @classmethod
748 747 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
749 748 cache=None, minchunk=1024, maxchunk=65536):
750 749 """Create templater from the specified map file"""
751 750 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
752 751 cache, tmap, aliases = _readmapfile(mapfile)
753 752 t.cache.update(cache)
754 753 t.map = tmap
755 754 t._aliases = aliases
756 755 return t
757 756
758 757 def __contains__(self, key):
759 758 return key in self.cache or key in self.map
760 759
761 760 def load(self, t):
762 761 '''Get the template for the given template name. Use a local cache.'''
763 762 if t not in self.cache:
764 763 try:
765 764 self.cache[t] = util.readfile(self.map[t][1])
766 765 except KeyError as inst:
767 766 raise templateutil.TemplateNotFound(
768 767 _('"%s" not in template map') % inst.args[0])
769 768 except IOError as inst:
770 769 reason = (_('template file %s: %s')
771 770 % (self.map[t][1], util.forcebytestr(inst.args[1])))
772 771 raise IOError(inst.args[0], encoding.strfromlocal(reason))
773 772 return self.cache[t]
774 773
775 774 def renderdefault(self, mapping):
776 775 """Render the default unnamed template and return result as string"""
777 776 return self.render('', mapping)
778 777
779 778 def render(self, t, mapping):
780 779 """Render the specified named template and return result as string"""
781 780 return templateutil.stringify(self.generate(t, mapping))
782 781
783 782 def generate(self, t, mapping):
784 783 """Return a generator that renders the specified named template and
785 784 yields chunks"""
786 785 ttype = t in self.map and self.map[t][0] or 'default'
787 786 if ttype not in self.ecache:
788 787 try:
789 788 ecls = engines[ttype]
790 789 except KeyError:
791 790 raise error.Abort(_('invalid template engine: %s') % ttype)
792 791 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
793 792 self._resources, self._aliases)
794 793 proc = self.ecache[ttype]
795 794
796 795 stream = proc.process(t, mapping)
797 796 if self.minchunk:
798 797 stream = util.increasingchunks(stream, min=self.minchunk,
799 798 max=self.maxchunk)
800 799 return stream
801 800
802 801 def templatepaths():
803 802 '''return locations used for template files.'''
804 803 pathsrel = ['templates']
805 804 paths = [os.path.normpath(os.path.join(util.datapath, f))
806 805 for f in pathsrel]
807 806 return [p for p in paths if os.path.isdir(p)]
808 807
809 808 def templatepath(name):
810 809 '''return location of template file. returns None if not found.'''
811 810 for p in templatepaths():
812 811 f = os.path.join(p, name)
813 812 if os.path.exists(f):
814 813 return f
815 814 return None
816 815
817 816 def stylemap(styles, paths=None):
818 817 """Return path to mapfile for a given style.
819 818
820 819 Searches mapfile in the following locations:
821 820 1. templatepath/style/map
822 821 2. templatepath/map-style
823 822 3. templatepath/map
824 823 """
825 824
826 825 if paths is None:
827 826 paths = templatepaths()
828 827 elif isinstance(paths, bytes):
829 828 paths = [paths]
830 829
831 830 if isinstance(styles, bytes):
832 831 styles = [styles]
833 832
834 833 for style in styles:
835 834 # only plain name is allowed to honor template paths
836 835 if (not style
837 836 or style in (pycompat.oscurdir, pycompat.ospardir)
838 837 or pycompat.ossep in style
839 838 or pycompat.osaltsep and pycompat.osaltsep in style):
840 839 continue
841 840 locations = [os.path.join(style, 'map'), 'map-' + style]
842 841 locations.append('map')
843 842
844 843 for path in paths:
845 844 for location in locations:
846 845 mapfile = os.path.join(path, location)
847 846 if os.path.isfile(mapfile):
848 847 return style, mapfile
849 848
850 849 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,447 +1,450 b''
1 1 # templateutil.py - utility for template evaluation
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 __future__ import absolute_import
9 9
10 10 import types
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 pycompat,
16 16 util,
17 17 )
18 18
19 19 class ResourceUnavailable(error.Abort):
20 20 pass
21 21
22 22 class TemplateNotFound(error.Abort):
23 23 pass
24 24
25 25 class hybrid(object):
26 26 """Wrapper for list or dict to support legacy template
27 27
28 28 This class allows us to handle both:
29 29 - "{files}" (legacy command-line-specific list hack) and
30 30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
31 31 and to access raw values:
32 32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
33 33 - "{get(extras, key)}"
34 34 - "{files|json}"
35 35 """
36 36
37 37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
38 38 if gen is not None:
39 39 self.gen = gen # generator or function returning generator
40 40 self._values = values
41 41 self._makemap = makemap
42 42 self.joinfmt = joinfmt
43 43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
44 44 def gen(self):
45 45 """Default generator to stringify this as {join(self, ' ')}"""
46 46 for i, x in enumerate(self._values):
47 47 if i > 0:
48 48 yield ' '
49 49 yield self.joinfmt(x)
50 50 def itermaps(self):
51 51 makemap = self._makemap
52 52 for x in self._values:
53 53 yield makemap(x)
54 54 def __contains__(self, x):
55 55 return x in self._values
56 56 def __getitem__(self, key):
57 57 return self._values[key]
58 58 def __len__(self):
59 59 return len(self._values)
60 60 def __iter__(self):
61 61 return iter(self._values)
62 62 def __getattr__(self, name):
63 63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
64 64 r'itervalues', r'keys', r'values'):
65 65 raise AttributeError(name)
66 66 return getattr(self._values, name)
67 67
68 68 class mappable(object):
69 69 """Wrapper for non-list/dict object to support map operation
70 70
71 71 This class allows us to handle both:
72 72 - "{manifest}"
73 73 - "{manifest % '{rev}:{node}'}"
74 74 - "{manifest.rev}"
75 75
76 76 Unlike a hybrid, this does not simulate the behavior of the underling
77 77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
78 78 """
79 79
80 80 def __init__(self, gen, key, value, makemap):
81 81 if gen is not None:
82 82 self.gen = gen # generator or function returning generator
83 83 self._key = key
84 84 self._value = value # may be generator of strings
85 85 self._makemap = makemap
86 86
87 87 def gen(self):
88 88 yield pycompat.bytestr(self._value)
89 89
90 90 def tomap(self):
91 91 return self._makemap(self._key)
92 92
93 93 def itermaps(self):
94 94 yield self.tomap()
95 95
96 96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
97 97 """Wrap data to support both dict-like and string-like operations"""
98 98 prefmt = pycompat.identity
99 99 if fmt is None:
100 100 fmt = '%s=%s'
101 101 prefmt = pycompat.bytestr
102 102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
104 104
105 105 def hybridlist(data, name, fmt=None, gen=None):
106 106 """Wrap data to support both list-like and string-like operations"""
107 107 prefmt = pycompat.identity
108 108 if fmt is None:
109 109 fmt = '%s'
110 110 prefmt = pycompat.bytestr
111 111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
112 112
113 113 def unwraphybrid(thing):
114 114 """Return an object which can be stringified possibly by using a legacy
115 115 template"""
116 116 gen = getattr(thing, 'gen', None)
117 117 if gen is None:
118 118 return thing
119 119 if callable(gen):
120 120 return gen()
121 121 return gen
122 122
123 123 def unwrapvalue(thing):
124 124 """Move the inner value object out of the wrapper"""
125 125 if not util.safehasattr(thing, '_value'):
126 126 return thing
127 127 return thing._value
128 128
129 129 def wraphybridvalue(container, key, value):
130 130 """Wrap an element of hybrid container to be mappable
131 131
132 132 The key is passed to the makemap function of the given container, which
133 133 should be an item generated by iter(container).
134 134 """
135 135 makemap = getattr(container, '_makemap', None)
136 136 if makemap is None:
137 137 return value
138 138 if util.safehasattr(value, '_makemap'):
139 139 # a nested hybrid list/dict, which has its own way of map operation
140 140 return value
141 141 return mappable(None, key, value, makemap)
142 142
143 143 def compatdict(context, mapping, name, data, key='key', value='value',
144 144 fmt=None, plural=None, separator=' '):
145 145 """Wrap data like hybriddict(), but also supports old-style list template
146 146
147 147 This exists for backward compatibility with the old-style template. Use
148 148 hybriddict() for new template keywords.
149 149 """
150 150 c = [{key: k, value: v} for k, v in data.iteritems()]
151 151 f = _showcompatlist(context, mapping, name, c, plural, separator)
152 152 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
153 153
154 154 def compatlist(context, mapping, name, data, element=None, fmt=None,
155 155 plural=None, separator=' '):
156 156 """Wrap data like hybridlist(), but also supports old-style list template
157 157
158 158 This exists for backward compatibility with the old-style template. Use
159 159 hybridlist() for new template keywords.
160 160 """
161 161 f = _showcompatlist(context, mapping, name, data, plural, separator)
162 162 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
163 163
164 164 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
165 165 """Return a generator that renders old-style list template
166 166
167 167 name is name of key in template map.
168 168 values is list of strings or dicts.
169 169 plural is plural of name, if not simply name + 's'.
170 170 separator is used to join values as a string
171 171
172 172 expansion works like this, given name 'foo'.
173 173
174 174 if values is empty, expand 'no_foos'.
175 175
176 176 if 'foo' not in template map, return values as a string,
177 177 joined by 'separator'.
178 178
179 179 expand 'start_foos'.
180 180
181 181 for each value, expand 'foo'. if 'last_foo' in template
182 182 map, expand it instead of 'foo' for last key.
183 183
184 184 expand 'end_foos'.
185 185 """
186 186 if not plural:
187 187 plural = name + 's'
188 188 if not values:
189 189 noname = 'no_' + plural
190 190 if context.preload(noname):
191 191 yield context.process(noname, mapping)
192 192 return
193 193 if not context.preload(name):
194 194 if isinstance(values[0], bytes):
195 195 yield separator.join(values)
196 196 else:
197 197 for v in values:
198 198 r = dict(v)
199 199 r.update(mapping)
200 200 yield r
201 201 return
202 202 startname = 'start_' + plural
203 203 if context.preload(startname):
204 204 yield context.process(startname, mapping)
205 205 vmapping = mapping.copy()
206 206 def one(v, tag=name):
207 207 try:
208 208 vmapping.update(v)
209 209 # Python 2 raises ValueError if the type of v is wrong. Python
210 210 # 3 raises TypeError.
211 211 except (AttributeError, TypeError, ValueError):
212 212 try:
213 213 # Python 2 raises ValueError trying to destructure an e.g.
214 214 # bytes. Python 3 raises TypeError.
215 215 for a, b in v:
216 216 vmapping[a] = b
217 217 except (TypeError, ValueError):
218 218 vmapping[name] = v
219 219 return context.process(tag, vmapping)
220 220 lastname = 'last_' + name
221 221 if context.preload(lastname):
222 222 last = values.pop()
223 223 else:
224 224 last = None
225 225 for v in values:
226 226 yield one(v)
227 227 if last is not None:
228 228 yield one(last, tag=lastname)
229 229 endname = 'end_' + plural
230 230 if context.preload(endname):
231 231 yield context.process(endname, mapping)
232 232
233 233 def stringify(thing):
234 234 """Turn values into bytes by converting into text and concatenating them"""
235 235 thing = unwraphybrid(thing)
236 236 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
237 237 if isinstance(thing, str):
238 238 # This is only reachable on Python 3 (otherwise
239 239 # isinstance(thing, bytes) would have been true), and is
240 240 # here to prevent infinite recursion bugs on Python 3.
241 241 raise error.ProgrammingError(
242 242 'stringify got unexpected unicode string: %r' % thing)
243 243 return "".join([stringify(t) for t in thing if t is not None])
244 244 if thing is None:
245 245 return ""
246 246 return pycompat.bytestr(thing)
247 247
248 248 def findsymbolicname(arg):
249 249 """Find symbolic name for the given compiled expression; returns None
250 250 if nothing found reliably"""
251 251 while True:
252 252 func, data = arg
253 253 if func is runsymbol:
254 254 return data
255 255 elif func is runfilter:
256 256 arg = data[0]
257 257 else:
258 258 return None
259 259
260 260 def evalrawexp(context, mapping, arg):
261 261 """Evaluate given argument as a bare template object which may require
262 262 further processing (such as folding generator of strings)"""
263 263 func, data = arg
264 264 return func(context, mapping, data)
265 265
266 266 def evalfuncarg(context, mapping, arg):
267 267 """Evaluate given argument as value type"""
268 268 thing = evalrawexp(context, mapping, arg)
269 269 thing = unwrapvalue(thing)
270 270 # evalrawexp() may return string, generator of strings or arbitrary object
271 271 # such as date tuple, but filter does not want generator.
272 272 if isinstance(thing, types.GeneratorType):
273 273 thing = stringify(thing)
274 274 return thing
275 275
276 276 def evalboolean(context, mapping, arg):
277 277 """Evaluate given argument as boolean, but also takes boolean literals"""
278 278 func, data = arg
279 279 if func is runsymbol:
280 280 thing = func(context, mapping, data, default=None)
281 281 if thing is None:
282 282 # not a template keyword, takes as a boolean literal
283 283 thing = util.parsebool(data)
284 284 else:
285 285 thing = func(context, mapping, data)
286 286 thing = unwrapvalue(thing)
287 287 if isinstance(thing, bool):
288 288 return thing
289 289 # other objects are evaluated as strings, which means 0 is True, but
290 290 # empty dict/list should be False as they are expected to be ''
291 291 return bool(stringify(thing))
292 292
293 293 def evalinteger(context, mapping, arg, err=None):
294 294 v = evalfuncarg(context, mapping, arg)
295 295 try:
296 296 return int(v)
297 297 except (TypeError, ValueError):
298 298 raise error.ParseError(err or _('not an integer'))
299 299
300 300 def evalstring(context, mapping, arg):
301 301 return stringify(evalrawexp(context, mapping, arg))
302 302
303 303 def evalstringliteral(context, mapping, arg):
304 304 """Evaluate given argument as string template, but returns symbol name
305 305 if it is unknown"""
306 306 func, data = arg
307 307 if func is runsymbol:
308 308 thing = func(context, mapping, data, default=data)
309 309 else:
310 310 thing = func(context, mapping, data)
311 311 return stringify(thing)
312 312
313 313 _evalfuncbytype = {
314 314 bool: evalboolean,
315 315 bytes: evalstring,
316 316 int: evalinteger,
317 317 }
318 318
319 319 def evalastype(context, mapping, arg, typ):
320 320 """Evaluate given argument and coerce its type"""
321 321 try:
322 322 f = _evalfuncbytype[typ]
323 323 except KeyError:
324 324 raise error.ProgrammingError('invalid type specified: %r' % typ)
325 325 return f(context, mapping, arg)
326 326
327 327 def runinteger(context, mapping, data):
328 328 return int(data)
329 329
330 330 def runstring(context, mapping, data):
331 331 return data
332 332
333 333 def _recursivesymbolblocker(key):
334 334 def showrecursion(**args):
335 335 raise error.Abort(_("recursive reference '%s' in template") % key)
336 336 return showrecursion
337 337
338 338 def runsymbol(context, mapping, key, default=''):
339 339 v = context.symbol(mapping, key)
340 340 if v is None:
341 341 # put poison to cut recursion. we can't move this to parsing phase
342 342 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
343 343 safemapping = mapping.copy()
344 344 safemapping[key] = _recursivesymbolblocker(key)
345 345 try:
346 346 v = context.process(key, safemapping)
347 347 except TemplateNotFound:
348 348 v = default
349 349 if callable(v) and getattr(v, '_requires', None) is None:
350 350 # old templatekw: expand all keywords and resources
351 # (TODO: deprecate this after porting web template keywords to new API)
351 352 props = {k: f(context, mapping, k)
352 353 for k, f in context._resources.items()}
354 # pass context to _showcompatlist() through templatekw._showlist()
355 props['templ'] = context
353 356 props.update(mapping)
354 357 return v(**pycompat.strkwargs(props))
355 358 if callable(v):
356 359 # new templatekw
357 360 try:
358 361 return v(context, mapping)
359 362 except ResourceUnavailable:
360 363 # unsupported keyword is mapped to empty just like unknown keyword
361 364 return None
362 365 return v
363 366
364 367 def runtemplate(context, mapping, template):
365 368 for arg in template:
366 369 yield evalrawexp(context, mapping, arg)
367 370
368 371 def runfilter(context, mapping, data):
369 372 arg, filt = data
370 373 thing = evalfuncarg(context, mapping, arg)
371 374 try:
372 375 return filt(thing)
373 376 except (ValueError, AttributeError, TypeError):
374 377 sym = findsymbolicname(arg)
375 378 if sym:
376 379 msg = (_("template filter '%s' is not compatible with keyword '%s'")
377 380 % (pycompat.sysbytes(filt.__name__), sym))
378 381 else:
379 382 msg = (_("incompatible use of template filter '%s'")
380 383 % pycompat.sysbytes(filt.__name__))
381 384 raise error.Abort(msg)
382 385
383 386 def runmap(context, mapping, data):
384 387 darg, targ = data
385 388 d = evalrawexp(context, mapping, darg)
386 389 if util.safehasattr(d, 'itermaps'):
387 390 diter = d.itermaps()
388 391 else:
389 392 try:
390 393 diter = iter(d)
391 394 except TypeError:
392 395 sym = findsymbolicname(darg)
393 396 if sym:
394 397 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
395 398 else:
396 399 raise error.ParseError(_("%r is not iterable") % d)
397 400
398 401 for i, v in enumerate(diter):
399 402 lm = mapping.copy()
400 403 lm['index'] = i
401 404 if isinstance(v, dict):
402 405 lm.update(v)
403 406 lm['originalnode'] = mapping.get('node')
404 407 yield evalrawexp(context, lm, targ)
405 408 else:
406 409 # v is not an iterable of dicts, this happen when 'key'
407 410 # has been fully expanded already and format is useless.
408 411 # If so, return the expanded value.
409 412 yield v
410 413
411 414 def runmember(context, mapping, data):
412 415 darg, memb = data
413 416 d = evalrawexp(context, mapping, darg)
414 417 if util.safehasattr(d, 'tomap'):
415 418 lm = mapping.copy()
416 419 lm.update(d.tomap())
417 420 return runsymbol(context, lm, memb)
418 421 if util.safehasattr(d, 'get'):
419 422 return getdictitem(d, memb)
420 423
421 424 sym = findsymbolicname(darg)
422 425 if sym:
423 426 raise error.ParseError(_("keyword '%s' has no member") % sym)
424 427 else:
425 428 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
426 429
427 430 def runnegate(context, mapping, data):
428 431 data = evalinteger(context, mapping, data,
429 432 _('negation needs an integer argument'))
430 433 return -data
431 434
432 435 def runarithmetic(context, mapping, data):
433 436 func, left, right = data
434 437 left = evalinteger(context, mapping, left,
435 438 _('arithmetic only defined on integers'))
436 439 right = evalinteger(context, mapping, right,
437 440 _('arithmetic only defined on integers'))
438 441 try:
439 442 return func(left, right)
440 443 except ZeroDivisionError:
441 444 raise error.Abort(_('division by zero is not defined'))
442 445
443 446 def getdictitem(dictarg, key):
444 447 val = dictarg.get(key)
445 448 if val is None:
446 449 return
447 450 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now