##// END OF EJS Templates
templater: pass (context, mapping) down to unwrapvalue()...
Yuya Nishihara -
r37295:9e8128e8 default
parent child Browse files
Show More
@@ -1,682 +1,682 b''
1 1 # templatefuncs.py - common template functions
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 re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 color,
15 15 encoding,
16 16 error,
17 17 minirst,
18 18 obsutil,
19 19 pycompat,
20 20 registrar,
21 21 revset as revsetmod,
22 22 revsetlang,
23 23 scmutil,
24 24 templatefilters,
25 25 templatekw,
26 26 templateutil,
27 27 util,
28 28 )
29 29 from .utils import (
30 30 dateutil,
31 31 stringutil,
32 32 )
33 33
34 34 evalrawexp = templateutil.evalrawexp
35 35 evalfuncarg = templateutil.evalfuncarg
36 36 evalboolean = templateutil.evalboolean
37 37 evaldate = templateutil.evaldate
38 38 evalinteger = templateutil.evalinteger
39 39 evalstring = templateutil.evalstring
40 40 evalstringliteral = templateutil.evalstringliteral
41 41
42 42 # dict of template built-in functions
43 43 funcs = {}
44 44 templatefunc = registrar.templatefunc(funcs)
45 45
46 46 @templatefunc('date(date[, fmt])')
47 47 def date(context, mapping, args):
48 48 """Format a date. See :hg:`help dates` for formatting
49 49 strings. The default is a Unix date format, including the timezone:
50 50 "Mon Sep 04 15:13:13 2006 0700"."""
51 51 if not (1 <= len(args) <= 2):
52 52 # i18n: "date" is a keyword
53 53 raise error.ParseError(_("date expects one or two arguments"))
54 54
55 55 date = evaldate(context, mapping, args[0],
56 56 # i18n: "date" is a keyword
57 57 _("date expects a date information"))
58 58 fmt = None
59 59 if len(args) == 2:
60 60 fmt = evalstring(context, mapping, args[1])
61 61 if fmt is None:
62 62 return dateutil.datestr(date)
63 63 else:
64 64 return dateutil.datestr(date, fmt)
65 65
66 66 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
67 67 def dict_(context, mapping, args):
68 68 """Construct a dict from key-value pairs. A key may be omitted if
69 69 a value expression can provide an unambiguous name."""
70 70 data = util.sortdict()
71 71
72 72 for v in args['args']:
73 73 k = templateutil.findsymbolicname(v)
74 74 if not k:
75 75 raise error.ParseError(_('dict key cannot be inferred'))
76 76 if k in data or k in args['kwargs']:
77 77 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
78 78 data[k] = evalfuncarg(context, mapping, v)
79 79
80 80 data.update((k, evalfuncarg(context, mapping, v))
81 81 for k, v in args['kwargs'].iteritems())
82 82 return templateutil.hybriddict(data)
83 83
84 84 @templatefunc('diff([includepattern [, excludepattern]])')
85 85 def diff(context, mapping, args):
86 86 """Show a diff, optionally
87 87 specifying files to include or exclude."""
88 88 if len(args) > 2:
89 89 # i18n: "diff" is a keyword
90 90 raise error.ParseError(_("diff expects zero, one, or two arguments"))
91 91
92 92 def getpatterns(i):
93 93 if i < len(args):
94 94 s = evalstring(context, mapping, args[i]).strip()
95 95 if s:
96 96 return [s]
97 97 return []
98 98
99 99 ctx = context.resource(mapping, 'ctx')
100 100 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
101 101
102 102 return ''.join(chunks)
103 103
104 104 @templatefunc('extdata(source)', argspec='source')
105 105 def extdata(context, mapping, args):
106 106 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
107 107 if 'source' not in args:
108 108 # i18n: "extdata" is a keyword
109 109 raise error.ParseError(_('extdata expects one argument'))
110 110
111 111 source = evalstring(context, mapping, args['source'])
112 112 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
113 113 ctx = context.resource(mapping, 'ctx')
114 114 if source in cache:
115 115 data = cache[source]
116 116 else:
117 117 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
118 118 return data.get(ctx.rev(), '')
119 119
120 120 @templatefunc('files(pattern)')
121 121 def files(context, mapping, args):
122 122 """All files of the current changeset matching the pattern. See
123 123 :hg:`help patterns`."""
124 124 if not len(args) == 1:
125 125 # i18n: "files" is a keyword
126 126 raise error.ParseError(_("files expects one argument"))
127 127
128 128 raw = evalstring(context, mapping, args[0])
129 129 ctx = context.resource(mapping, 'ctx')
130 130 m = ctx.match([raw])
131 131 files = list(ctx.matches(m))
132 132 return templateutil.compatlist(context, mapping, "file", files)
133 133
134 134 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
135 135 def fill(context, mapping, args):
136 136 """Fill many
137 137 paragraphs with optional indentation. See the "fill" filter."""
138 138 if not (1 <= len(args) <= 4):
139 139 # i18n: "fill" is a keyword
140 140 raise error.ParseError(_("fill expects one to four arguments"))
141 141
142 142 text = evalstring(context, mapping, args[0])
143 143 width = 76
144 144 initindent = ''
145 145 hangindent = ''
146 146 if 2 <= len(args) <= 4:
147 147 width = evalinteger(context, mapping, args[1],
148 148 # i18n: "fill" is a keyword
149 149 _("fill expects an integer width"))
150 150 try:
151 151 initindent = evalstring(context, mapping, args[2])
152 152 hangindent = evalstring(context, mapping, args[3])
153 153 except IndexError:
154 154 pass
155 155
156 156 return templatefilters.fill(text, width, initindent, hangindent)
157 157
158 158 @templatefunc('formatnode(node)')
159 159 def formatnode(context, mapping, args):
160 160 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
161 161 if len(args) != 1:
162 162 # i18n: "formatnode" is a keyword
163 163 raise error.ParseError(_("formatnode expects one argument"))
164 164
165 165 ui = context.resource(mapping, 'ui')
166 166 node = evalstring(context, mapping, args[0])
167 167 if ui.debugflag:
168 168 return node
169 169 return templatefilters.short(node)
170 170
171 171 @templatefunc('mailmap(author)')
172 172 def mailmap(context, mapping, args):
173 173 """Return the author, updated according to the value
174 174 set in the .mailmap file"""
175 175 if len(args) != 1:
176 176 raise error.ParseError(_("mailmap expects one argument"))
177 177
178 178 author = evalstring(context, mapping, args[0])
179 179
180 180 cache = context.resource(mapping, 'cache')
181 181 repo = context.resource(mapping, 'repo')
182 182
183 183 if 'mailmap' not in cache:
184 184 data = repo.wvfs.tryread('.mailmap')
185 185 cache['mailmap'] = stringutil.parsemailmap(data)
186 186
187 187 return stringutil.mapname(cache['mailmap'], author)
188 188
189 189 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
190 190 argspec='text width fillchar left')
191 191 def pad(context, mapping, args):
192 192 """Pad text with a
193 193 fill character."""
194 194 if 'text' not in args or 'width' not in args:
195 195 # i18n: "pad" is a keyword
196 196 raise error.ParseError(_("pad() expects two to four arguments"))
197 197
198 198 width = evalinteger(context, mapping, args['width'],
199 199 # i18n: "pad" is a keyword
200 200 _("pad() expects an integer width"))
201 201
202 202 text = evalstring(context, mapping, args['text'])
203 203
204 204 left = False
205 205 fillchar = ' '
206 206 if 'fillchar' in args:
207 207 fillchar = evalstring(context, mapping, args['fillchar'])
208 208 if len(color.stripeffects(fillchar)) != 1:
209 209 # i18n: "pad" is a keyword
210 210 raise error.ParseError(_("pad() expects a single fill character"))
211 211 if 'left' in args:
212 212 left = evalboolean(context, mapping, args['left'])
213 213
214 214 fillwidth = width - encoding.colwidth(color.stripeffects(text))
215 215 if fillwidth <= 0:
216 216 return text
217 217 if left:
218 218 return fillchar * fillwidth + text
219 219 else:
220 220 return text + fillchar * fillwidth
221 221
222 222 @templatefunc('indent(text, indentchars[, firstline])')
223 223 def indent(context, mapping, args):
224 224 """Indents all non-empty lines
225 225 with the characters given in the indentchars string. An optional
226 226 third parameter will override the indent for the first line only
227 227 if present."""
228 228 if not (2 <= len(args) <= 3):
229 229 # i18n: "indent" is a keyword
230 230 raise error.ParseError(_("indent() expects two or three arguments"))
231 231
232 232 text = evalstring(context, mapping, args[0])
233 233 indent = evalstring(context, mapping, args[1])
234 234
235 235 if len(args) == 3:
236 236 firstline = evalstring(context, mapping, args[2])
237 237 else:
238 238 firstline = indent
239 239
240 240 # the indent function doesn't indent the first line, so we do it here
241 241 return templatefilters.indent(firstline + text, indent)
242 242
243 243 @templatefunc('get(dict, key)')
244 244 def get(context, mapping, args):
245 245 """Get an attribute/key from an object. Some keywords
246 246 are complex types. This function allows you to obtain the value of an
247 247 attribute on these types."""
248 248 if len(args) != 2:
249 249 # i18n: "get" is a keyword
250 250 raise error.ParseError(_("get() expects two arguments"))
251 251
252 252 dictarg = evalfuncarg(context, mapping, args[0])
253 253 if not util.safehasattr(dictarg, 'get'):
254 254 # i18n: "get" is a keyword
255 255 raise error.ParseError(_("get() expects a dict as first argument"))
256 256
257 257 key = evalfuncarg(context, mapping, args[1])
258 258 return templateutil.getdictitem(dictarg, key)
259 259
260 260 @templatefunc('if(expr, then[, else])')
261 261 def if_(context, mapping, args):
262 262 """Conditionally execute based on the result of
263 263 an expression."""
264 264 if not (2 <= len(args) <= 3):
265 265 # i18n: "if" is a keyword
266 266 raise error.ParseError(_("if expects two or three arguments"))
267 267
268 268 test = evalboolean(context, mapping, args[0])
269 269 if test:
270 270 return evalrawexp(context, mapping, args[1])
271 271 elif len(args) == 3:
272 272 return evalrawexp(context, mapping, args[2])
273 273
274 274 @templatefunc('ifcontains(needle, haystack, then[, else])')
275 275 def ifcontains(context, mapping, args):
276 276 """Conditionally execute based
277 277 on whether the item "needle" is in "haystack"."""
278 278 if not (3 <= len(args) <= 4):
279 279 # i18n: "ifcontains" is a keyword
280 280 raise error.ParseError(_("ifcontains expects three or four arguments"))
281 281
282 282 haystack = evalfuncarg(context, mapping, args[1])
283 283 keytype = getattr(haystack, 'keytype', None)
284 284 try:
285 285 needle = evalrawexp(context, mapping, args[0])
286 286 needle = templateutil.unwrapastype(context, mapping, needle,
287 287 keytype or bytes)
288 288 found = (needle in haystack)
289 289 except error.ParseError:
290 290 found = False
291 291
292 292 if found:
293 293 return evalrawexp(context, mapping, args[2])
294 294 elif len(args) == 4:
295 295 return evalrawexp(context, mapping, args[3])
296 296
297 297 @templatefunc('ifeq(expr1, expr2, then[, else])')
298 298 def ifeq(context, mapping, args):
299 299 """Conditionally execute based on
300 300 whether 2 items are equivalent."""
301 301 if not (3 <= len(args) <= 4):
302 302 # i18n: "ifeq" is a keyword
303 303 raise error.ParseError(_("ifeq expects three or four arguments"))
304 304
305 305 test = evalstring(context, mapping, args[0])
306 306 match = evalstring(context, mapping, args[1])
307 307 if test == match:
308 308 return evalrawexp(context, mapping, args[2])
309 309 elif len(args) == 4:
310 310 return evalrawexp(context, mapping, args[3])
311 311
312 312 @templatefunc('join(list, sep)')
313 313 def join(context, mapping, args):
314 314 """Join items in a list with a delimiter."""
315 315 if not (1 <= len(args) <= 2):
316 316 # i18n: "join" is a keyword
317 317 raise error.ParseError(_("join expects one or two arguments"))
318 318
319 319 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
320 320 # abuses generator as a keyword that returns a list of dicts.
321 321 joinset = evalrawexp(context, mapping, args[0])
322 joinset = templateutil.unwrapvalue(joinset)
322 joinset = templateutil.unwrapvalue(context, mapping, joinset)
323 323 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
324 324 joiner = " "
325 325 if len(args) > 1:
326 326 joiner = evalstring(context, mapping, args[1])
327 327
328 328 first = True
329 329 for x in pycompat.maybebytestr(joinset):
330 330 if first:
331 331 first = False
332 332 else:
333 333 yield joiner
334 334 yield joinfmt(x)
335 335
336 336 @templatefunc('label(label, expr)')
337 337 def label(context, mapping, args):
338 338 """Apply a label to generated content. Content with
339 339 a label applied can result in additional post-processing, such as
340 340 automatic colorization."""
341 341 if len(args) != 2:
342 342 # i18n: "label" is a keyword
343 343 raise error.ParseError(_("label expects two arguments"))
344 344
345 345 ui = context.resource(mapping, 'ui')
346 346 thing = evalstring(context, mapping, args[1])
347 347 # preserve unknown symbol as literal so effects like 'red', 'bold',
348 348 # etc. don't need to be quoted
349 349 label = evalstringliteral(context, mapping, args[0])
350 350
351 351 return ui.label(thing, label)
352 352
353 353 @templatefunc('latesttag([pattern])')
354 354 def latesttag(context, mapping, args):
355 355 """The global tags matching the given pattern on the
356 356 most recent globally tagged ancestor of this changeset.
357 357 If no such tags exist, the "{tag}" template resolves to
358 358 the string "null"."""
359 359 if len(args) > 1:
360 360 # i18n: "latesttag" is a keyword
361 361 raise error.ParseError(_("latesttag expects at most one argument"))
362 362
363 363 pattern = None
364 364 if len(args) == 1:
365 365 pattern = evalstring(context, mapping, args[0])
366 366 return templatekw.showlatesttags(context, mapping, pattern)
367 367
368 368 @templatefunc('localdate(date[, tz])')
369 369 def localdate(context, mapping, args):
370 370 """Converts a date to the specified timezone.
371 371 The default is local date."""
372 372 if not (1 <= len(args) <= 2):
373 373 # i18n: "localdate" is a keyword
374 374 raise error.ParseError(_("localdate expects one or two arguments"))
375 375
376 376 date = evaldate(context, mapping, args[0],
377 377 # i18n: "localdate" is a keyword
378 378 _("localdate expects a date information"))
379 379 if len(args) >= 2:
380 380 tzoffset = None
381 381 tz = evalfuncarg(context, mapping, args[1])
382 382 if isinstance(tz, bytes):
383 383 tzoffset, remainder = dateutil.parsetimezone(tz)
384 384 if remainder:
385 385 tzoffset = None
386 386 if tzoffset is None:
387 387 try:
388 388 tzoffset = int(tz)
389 389 except (TypeError, ValueError):
390 390 # i18n: "localdate" is a keyword
391 391 raise error.ParseError(_("localdate expects a timezone"))
392 392 else:
393 393 tzoffset = dateutil.makedate()[1]
394 394 return (date[0], tzoffset)
395 395
396 396 @templatefunc('max(iterable)')
397 397 def max_(context, mapping, args, **kwargs):
398 398 """Return the max of an iterable"""
399 399 if len(args) != 1:
400 400 # i18n: "max" is a keyword
401 401 raise error.ParseError(_("max expects one argument"))
402 402
403 403 iterable = evalfuncarg(context, mapping, args[0])
404 404 try:
405 405 x = max(pycompat.maybebytestr(iterable))
406 406 except (TypeError, ValueError):
407 407 # i18n: "max" is a keyword
408 408 raise error.ParseError(_("max first argument should be an iterable"))
409 409 return templateutil.wraphybridvalue(iterable, x, x)
410 410
411 411 @templatefunc('min(iterable)')
412 412 def min_(context, mapping, args, **kwargs):
413 413 """Return the min of an iterable"""
414 414 if len(args) != 1:
415 415 # i18n: "min" is a keyword
416 416 raise error.ParseError(_("min expects one argument"))
417 417
418 418 iterable = evalfuncarg(context, mapping, args[0])
419 419 try:
420 420 x = min(pycompat.maybebytestr(iterable))
421 421 except (TypeError, ValueError):
422 422 # i18n: "min" is a keyword
423 423 raise error.ParseError(_("min first argument should be an iterable"))
424 424 return templateutil.wraphybridvalue(iterable, x, x)
425 425
426 426 @templatefunc('mod(a, b)')
427 427 def mod(context, mapping, args):
428 428 """Calculate a mod b such that a / b + a mod b == a"""
429 429 if not len(args) == 2:
430 430 # i18n: "mod" is a keyword
431 431 raise error.ParseError(_("mod expects two arguments"))
432 432
433 433 func = lambda a, b: a % b
434 434 return templateutil.runarithmetic(context, mapping,
435 435 (func, args[0], args[1]))
436 436
437 437 @templatefunc('obsfateoperations(markers)')
438 438 def obsfateoperations(context, mapping, args):
439 439 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
440 440 if len(args) != 1:
441 441 # i18n: "obsfateoperations" is a keyword
442 442 raise error.ParseError(_("obsfateoperations expects one argument"))
443 443
444 444 markers = evalfuncarg(context, mapping, args[0])
445 445
446 446 try:
447 447 data = obsutil.markersoperations(markers)
448 448 return templateutil.hybridlist(data, name='operation')
449 449 except (TypeError, KeyError):
450 450 # i18n: "obsfateoperations" is a keyword
451 451 errmsg = _("obsfateoperations first argument should be an iterable")
452 452 raise error.ParseError(errmsg)
453 453
454 454 @templatefunc('obsfatedate(markers)')
455 455 def obsfatedate(context, mapping, args):
456 456 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
457 457 if len(args) != 1:
458 458 # i18n: "obsfatedate" is a keyword
459 459 raise error.ParseError(_("obsfatedate expects one argument"))
460 460
461 461 markers = evalfuncarg(context, mapping, args[0])
462 462
463 463 try:
464 464 data = obsutil.markersdates(markers)
465 465 return templateutil.hybridlist(data, name='date', fmt='%d %d')
466 466 except (TypeError, KeyError):
467 467 # i18n: "obsfatedate" is a keyword
468 468 errmsg = _("obsfatedate first argument should be an iterable")
469 469 raise error.ParseError(errmsg)
470 470
471 471 @templatefunc('obsfateusers(markers)')
472 472 def obsfateusers(context, mapping, args):
473 473 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
474 474 if len(args) != 1:
475 475 # i18n: "obsfateusers" is a keyword
476 476 raise error.ParseError(_("obsfateusers expects one argument"))
477 477
478 478 markers = evalfuncarg(context, mapping, args[0])
479 479
480 480 try:
481 481 data = obsutil.markersusers(markers)
482 482 return templateutil.hybridlist(data, name='user')
483 483 except (TypeError, KeyError, ValueError):
484 484 # i18n: "obsfateusers" is a keyword
485 485 msg = _("obsfateusers first argument should be an iterable of "
486 486 "obsmakers")
487 487 raise error.ParseError(msg)
488 488
489 489 @templatefunc('obsfateverb(successors, markers)')
490 490 def obsfateverb(context, mapping, args):
491 491 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
492 492 if len(args) != 2:
493 493 # i18n: "obsfateverb" is a keyword
494 494 raise error.ParseError(_("obsfateverb expects two arguments"))
495 495
496 496 successors = evalfuncarg(context, mapping, args[0])
497 497 markers = evalfuncarg(context, mapping, args[1])
498 498
499 499 try:
500 500 return obsutil.obsfateverb(successors, markers)
501 501 except TypeError:
502 502 # i18n: "obsfateverb" is a keyword
503 503 errmsg = _("obsfateverb first argument should be countable")
504 504 raise error.ParseError(errmsg)
505 505
506 506 @templatefunc('relpath(path)')
507 507 def relpath(context, mapping, args):
508 508 """Convert a repository-absolute path into a filesystem path relative to
509 509 the current working directory."""
510 510 if len(args) != 1:
511 511 # i18n: "relpath" is a keyword
512 512 raise error.ParseError(_("relpath expects one argument"))
513 513
514 514 repo = context.resource(mapping, 'ctx').repo()
515 515 path = evalstring(context, mapping, args[0])
516 516 return repo.pathto(path)
517 517
518 518 @templatefunc('revset(query[, formatargs...])')
519 519 def revset(context, mapping, args):
520 520 """Execute a revision set query. See
521 521 :hg:`help revset`."""
522 522 if not len(args) > 0:
523 523 # i18n: "revset" is a keyword
524 524 raise error.ParseError(_("revset expects one or more arguments"))
525 525
526 526 raw = evalstring(context, mapping, args[0])
527 527 ctx = context.resource(mapping, 'ctx')
528 528 repo = ctx.repo()
529 529
530 530 def query(expr):
531 531 m = revsetmod.match(repo.ui, expr, repo=repo)
532 532 return m(repo)
533 533
534 534 if len(args) > 1:
535 535 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
536 536 revs = query(revsetlang.formatspec(raw, *formatargs))
537 537 revs = list(revs)
538 538 else:
539 539 cache = context.resource(mapping, 'cache')
540 540 revsetcache = cache.setdefault("revsetcache", {})
541 541 if raw in revsetcache:
542 542 revs = revsetcache[raw]
543 543 else:
544 544 revs = query(raw)
545 545 revs = list(revs)
546 546 revsetcache[raw] = revs
547 547 return templatekw.showrevslist(context, mapping, "revision", revs)
548 548
549 549 @templatefunc('rstdoc(text, style)')
550 550 def rstdoc(context, mapping, args):
551 551 """Format reStructuredText."""
552 552 if len(args) != 2:
553 553 # i18n: "rstdoc" is a keyword
554 554 raise error.ParseError(_("rstdoc expects two arguments"))
555 555
556 556 text = evalstring(context, mapping, args[0])
557 557 style = evalstring(context, mapping, args[1])
558 558
559 559 return minirst.format(text, style=style, keep=['verbose'])
560 560
561 561 @templatefunc('separate(sep, args)', argspec='sep *args')
562 562 def separate(context, mapping, args):
563 563 """Add a separator between non-empty arguments."""
564 564 if 'sep' not in args:
565 565 # i18n: "separate" is a keyword
566 566 raise error.ParseError(_("separate expects at least one argument"))
567 567
568 568 sep = evalstring(context, mapping, args['sep'])
569 569 first = True
570 570 for arg in args['args']:
571 571 argstr = evalstring(context, mapping, arg)
572 572 if not argstr:
573 573 continue
574 574 if first:
575 575 first = False
576 576 else:
577 577 yield sep
578 578 yield argstr
579 579
580 580 @templatefunc('shortest(node, minlength=4)')
581 581 def shortest(context, mapping, args):
582 582 """Obtain the shortest representation of
583 583 a node."""
584 584 if not (1 <= len(args) <= 2):
585 585 # i18n: "shortest" is a keyword
586 586 raise error.ParseError(_("shortest() expects one or two arguments"))
587 587
588 588 node = evalstring(context, mapping, args[0])
589 589
590 590 minlength = 4
591 591 if len(args) > 1:
592 592 minlength = evalinteger(context, mapping, args[1],
593 593 # i18n: "shortest" is a keyword
594 594 _("shortest() expects an integer minlength"))
595 595
596 596 # _partialmatch() of filtered changelog could take O(len(repo)) time,
597 597 # which would be unacceptably slow. so we look for hash collision in
598 598 # unfiltered space, which means some hashes may be slightly longer.
599 599 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
600 600 return cl.shortest(node, minlength)
601 601
602 602 @templatefunc('strip(text[, chars])')
603 603 def strip(context, mapping, args):
604 604 """Strip characters from a string. By default,
605 605 strips all leading and trailing whitespace."""
606 606 if not (1 <= len(args) <= 2):
607 607 # i18n: "strip" is a keyword
608 608 raise error.ParseError(_("strip expects one or two arguments"))
609 609
610 610 text = evalstring(context, mapping, args[0])
611 611 if len(args) == 2:
612 612 chars = evalstring(context, mapping, args[1])
613 613 return text.strip(chars)
614 614 return text.strip()
615 615
616 616 @templatefunc('sub(pattern, replacement, expression)')
617 617 def sub(context, mapping, args):
618 618 """Perform text substitution
619 619 using regular expressions."""
620 620 if len(args) != 3:
621 621 # i18n: "sub" is a keyword
622 622 raise error.ParseError(_("sub expects three arguments"))
623 623
624 624 pat = evalstring(context, mapping, args[0])
625 625 rpl = evalstring(context, mapping, args[1])
626 626 src = evalstring(context, mapping, args[2])
627 627 try:
628 628 patre = re.compile(pat)
629 629 except re.error:
630 630 # i18n: "sub" is a keyword
631 631 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
632 632 try:
633 633 yield patre.sub(rpl, src)
634 634 except re.error:
635 635 # i18n: "sub" is a keyword
636 636 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
637 637
638 638 @templatefunc('startswith(pattern, text)')
639 639 def startswith(context, mapping, args):
640 640 """Returns the value from the "text" argument
641 641 if it begins with the content from the "pattern" argument."""
642 642 if len(args) != 2:
643 643 # i18n: "startswith" is a keyword
644 644 raise error.ParseError(_("startswith expects two arguments"))
645 645
646 646 patn = evalstring(context, mapping, args[0])
647 647 text = evalstring(context, mapping, args[1])
648 648 if text.startswith(patn):
649 649 return text
650 650 return ''
651 651
652 652 @templatefunc('word(number, text[, separator])')
653 653 def word(context, mapping, args):
654 654 """Return the nth word from a string."""
655 655 if not (2 <= len(args) <= 3):
656 656 # i18n: "word" is a keyword
657 657 raise error.ParseError(_("word expects two or three arguments, got %d")
658 658 % len(args))
659 659
660 660 num = evalinteger(context, mapping, args[0],
661 661 # i18n: "word" is a keyword
662 662 _("word expects an integer index"))
663 663 text = evalstring(context, mapping, args[1])
664 664 if len(args) == 3:
665 665 splitter = evalstring(context, mapping, args[2])
666 666 else:
667 667 splitter = None
668 668
669 669 tokens = text.split(splitter)
670 670 if num >= len(tokens) or num < -len(tokens):
671 671 return ''
672 672 else:
673 673 return tokens[num]
674 674
675 675 def loadfunction(ui, extname, registrarobj):
676 676 """Load template function from specified registrarobj
677 677 """
678 678 for name, func in registrarobj._table.iteritems():
679 679 funcs[name] = func
680 680
681 681 # tell hggettext to extract docstrings from these functions:
682 682 i18nfunctions = funcs.values()
@@ -1,533 +1,533 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 abc
11 11 import types
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 pycompat,
17 17 util,
18 18 )
19 19 from .utils import (
20 20 dateutil,
21 21 stringutil,
22 22 )
23 23
24 24 class ResourceUnavailable(error.Abort):
25 25 pass
26 26
27 27 class TemplateNotFound(error.Abort):
28 28 pass
29 29
30 30 class wrapped(object):
31 31 """Object requiring extra conversion prior to displaying or processing
32 32 as value"""
33 33
34 34 __metaclass__ = abc.ABCMeta
35 35
36 36 @abc.abstractmethod
37 37 def show(self, context, mapping):
38 38 """Return a bytes or (possibly nested) generator of bytes representing
39 39 the underlying object
40 40
41 41 A pre-configured template may be rendered if the underlying object is
42 42 not printable.
43 43 """
44 44
45 45 # stub for representing a date type; may be a real date type that can
46 46 # provide a readable string value
47 47 class date(object):
48 48 pass
49 49
50 50 class hybrid(wrapped):
51 51 """Wrapper for list or dict to support legacy template
52 52
53 53 This class allows us to handle both:
54 54 - "{files}" (legacy command-line-specific list hack) and
55 55 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
56 56 and to access raw values:
57 57 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
58 58 - "{get(extras, key)}"
59 59 - "{files|json}"
60 60 """
61 61
62 62 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
63 63 if gen is not None:
64 64 self._gen = gen # generator or function returning generator
65 65 self._values = values
66 66 self._makemap = makemap
67 67 self.joinfmt = joinfmt
68 68 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
69 69
70 70 def _gen(self):
71 71 """Default generator to stringify this as {join(self, ' ')}"""
72 72 for i, x in enumerate(self._values):
73 73 if i > 0:
74 74 yield ' '
75 75 yield self.joinfmt(x)
76 76 def itermaps(self):
77 77 makemap = self._makemap
78 78 for x in self._values:
79 79 yield makemap(x)
80 80
81 81 def show(self, context, mapping):
82 82 # TODO: switch gen to (context, mapping) API?
83 83 gen = self._gen
84 84 if callable(gen):
85 85 return gen()
86 86 return gen
87 87
88 88 def __contains__(self, x):
89 89 return x in self._values
90 90 def __getitem__(self, key):
91 91 return self._values[key]
92 92 def __len__(self):
93 93 return len(self._values)
94 94 def __iter__(self):
95 95 return iter(self._values)
96 96 def __getattr__(self, name):
97 97 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
98 98 r'itervalues', r'keys', r'values'):
99 99 raise AttributeError(name)
100 100 return getattr(self._values, name)
101 101
102 102 class mappable(wrapped):
103 103 """Wrapper for non-list/dict object to support map operation
104 104
105 105 This class allows us to handle both:
106 106 - "{manifest}"
107 107 - "{manifest % '{rev}:{node}'}"
108 108 - "{manifest.rev}"
109 109
110 110 Unlike a hybrid, this does not simulate the behavior of the underling
111 111 value. Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain
112 112 the inner object.
113 113 """
114 114
115 115 def __init__(self, gen, key, value, makemap):
116 116 self._gen = gen # generator or function returning generator
117 117 self._key = key
118 118 self._value = value # may be generator of strings
119 119 self._makemap = makemap
120 120
121 121 def tomap(self):
122 122 return self._makemap(self._key)
123 123
124 124 def itermaps(self):
125 125 yield self.tomap()
126 126
127 127 def show(self, context, mapping):
128 128 # TODO: switch gen to (context, mapping) API?
129 129 gen = self._gen
130 130 if gen is None:
131 131 return pycompat.bytestr(self._value)
132 132 if callable(gen):
133 133 return gen()
134 134 return gen
135 135
136 136 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
137 137 """Wrap data to support both dict-like and string-like operations"""
138 138 prefmt = pycompat.identity
139 139 if fmt is None:
140 140 fmt = '%s=%s'
141 141 prefmt = pycompat.bytestr
142 142 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
143 143 lambda k: fmt % (prefmt(k), prefmt(data[k])))
144 144
145 145 def hybridlist(data, name, fmt=None, gen=None):
146 146 """Wrap data to support both list-like and string-like operations"""
147 147 prefmt = pycompat.identity
148 148 if fmt is None:
149 149 fmt = '%s'
150 150 prefmt = pycompat.bytestr
151 151 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
152 152
153 153 def unwraphybrid(context, mapping, thing):
154 154 """Return an object which can be stringified possibly by using a legacy
155 155 template"""
156 156 if not isinstance(thing, wrapped):
157 157 return thing
158 158 return thing.show(context, mapping)
159 159
160 def unwrapvalue(thing):
160 def unwrapvalue(context, mapping, thing):
161 161 """Move the inner value object out of the wrapper"""
162 162 if not util.safehasattr(thing, '_value'):
163 163 return thing
164 164 return thing._value
165 165
166 166 def wraphybridvalue(container, key, value):
167 167 """Wrap an element of hybrid container to be mappable
168 168
169 169 The key is passed to the makemap function of the given container, which
170 170 should be an item generated by iter(container).
171 171 """
172 172 makemap = getattr(container, '_makemap', None)
173 173 if makemap is None:
174 174 return value
175 175 if util.safehasattr(value, '_makemap'):
176 176 # a nested hybrid list/dict, which has its own way of map operation
177 177 return value
178 178 return mappable(None, key, value, makemap)
179 179
180 180 def compatdict(context, mapping, name, data, key='key', value='value',
181 181 fmt=None, plural=None, separator=' '):
182 182 """Wrap data like hybriddict(), but also supports old-style list template
183 183
184 184 This exists for backward compatibility with the old-style template. Use
185 185 hybriddict() for new template keywords.
186 186 """
187 187 c = [{key: k, value: v} for k, v in data.iteritems()]
188 188 f = _showcompatlist(context, mapping, name, c, plural, separator)
189 189 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
190 190
191 191 def compatlist(context, mapping, name, data, element=None, fmt=None,
192 192 plural=None, separator=' '):
193 193 """Wrap data like hybridlist(), but also supports old-style list template
194 194
195 195 This exists for backward compatibility with the old-style template. Use
196 196 hybridlist() for new template keywords.
197 197 """
198 198 f = _showcompatlist(context, mapping, name, data, plural, separator)
199 199 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
200 200
201 201 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
202 202 """Return a generator that renders old-style list template
203 203
204 204 name is name of key in template map.
205 205 values is list of strings or dicts.
206 206 plural is plural of name, if not simply name + 's'.
207 207 separator is used to join values as a string
208 208
209 209 expansion works like this, given name 'foo'.
210 210
211 211 if values is empty, expand 'no_foos'.
212 212
213 213 if 'foo' not in template map, return values as a string,
214 214 joined by 'separator'.
215 215
216 216 expand 'start_foos'.
217 217
218 218 for each value, expand 'foo'. if 'last_foo' in template
219 219 map, expand it instead of 'foo' for last key.
220 220
221 221 expand 'end_foos'.
222 222 """
223 223 if not plural:
224 224 plural = name + 's'
225 225 if not values:
226 226 noname = 'no_' + plural
227 227 if context.preload(noname):
228 228 yield context.process(noname, mapping)
229 229 return
230 230 if not context.preload(name):
231 231 if isinstance(values[0], bytes):
232 232 yield separator.join(values)
233 233 else:
234 234 for v in values:
235 235 r = dict(v)
236 236 r.update(mapping)
237 237 yield r
238 238 return
239 239 startname = 'start_' + plural
240 240 if context.preload(startname):
241 241 yield context.process(startname, mapping)
242 242 def one(v, tag=name):
243 243 vmapping = {}
244 244 try:
245 245 vmapping.update(v)
246 246 # Python 2 raises ValueError if the type of v is wrong. Python
247 247 # 3 raises TypeError.
248 248 except (AttributeError, TypeError, ValueError):
249 249 try:
250 250 # Python 2 raises ValueError trying to destructure an e.g.
251 251 # bytes. Python 3 raises TypeError.
252 252 for a, b in v:
253 253 vmapping[a] = b
254 254 except (TypeError, ValueError):
255 255 vmapping[name] = v
256 256 vmapping = context.overlaymap(mapping, vmapping)
257 257 return context.process(tag, vmapping)
258 258 lastname = 'last_' + name
259 259 if context.preload(lastname):
260 260 last = values.pop()
261 261 else:
262 262 last = None
263 263 for v in values:
264 264 yield one(v)
265 265 if last is not None:
266 266 yield one(last, tag=lastname)
267 267 endname = 'end_' + plural
268 268 if context.preload(endname):
269 269 yield context.process(endname, mapping)
270 270
271 271 def flatten(context, mapping, thing):
272 272 """Yield a single stream from a possibly nested set of iterators"""
273 273 thing = unwraphybrid(context, mapping, thing)
274 274 if isinstance(thing, bytes):
275 275 yield thing
276 276 elif isinstance(thing, str):
277 277 # We can only hit this on Python 3, and it's here to guard
278 278 # against infinite recursion.
279 279 raise error.ProgrammingError('Mercurial IO including templates is done'
280 280 ' with bytes, not strings, got %r' % thing)
281 281 elif thing is None:
282 282 pass
283 283 elif not util.safehasattr(thing, '__iter__'):
284 284 yield pycompat.bytestr(thing)
285 285 else:
286 286 for i in thing:
287 287 i = unwraphybrid(context, mapping, i)
288 288 if isinstance(i, bytes):
289 289 yield i
290 290 elif i is None:
291 291 pass
292 292 elif not util.safehasattr(i, '__iter__'):
293 293 yield pycompat.bytestr(i)
294 294 else:
295 295 for j in flatten(context, mapping, i):
296 296 yield j
297 297
298 298 def stringify(context, mapping, thing):
299 299 """Turn values into bytes by converting into text and concatenating them"""
300 300 if isinstance(thing, bytes):
301 301 return thing # retain localstr to be round-tripped
302 302 return b''.join(flatten(context, mapping, thing))
303 303
304 304 def findsymbolicname(arg):
305 305 """Find symbolic name for the given compiled expression; returns None
306 306 if nothing found reliably"""
307 307 while True:
308 308 func, data = arg
309 309 if func is runsymbol:
310 310 return data
311 311 elif func is runfilter:
312 312 arg = data[0]
313 313 else:
314 314 return None
315 315
316 316 def evalrawexp(context, mapping, arg):
317 317 """Evaluate given argument as a bare template object which may require
318 318 further processing (such as folding generator of strings)"""
319 319 func, data = arg
320 320 return func(context, mapping, data)
321 321
322 322 def evalfuncarg(context, mapping, arg):
323 323 """Evaluate given argument as value type"""
324 324 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
325 325
326 326 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
327 327 # is fixed. we can't do that right now because join() has to take a generator
328 328 # of byte strings as it is, not a lazy byte string.
329 329 def _unwrapvalue(context, mapping, thing):
330 thing = unwrapvalue(thing)
330 thing = unwrapvalue(context, mapping, thing)
331 331 # evalrawexp() may return string, generator of strings or arbitrary object
332 332 # such as date tuple, but filter does not want generator.
333 333 if isinstance(thing, types.GeneratorType):
334 334 thing = stringify(context, mapping, thing)
335 335 return thing
336 336
337 337 def evalboolean(context, mapping, arg):
338 338 """Evaluate given argument as boolean, but also takes boolean literals"""
339 339 func, data = arg
340 340 if func is runsymbol:
341 341 thing = func(context, mapping, data, default=None)
342 342 if thing is None:
343 343 # not a template keyword, takes as a boolean literal
344 344 thing = stringutil.parsebool(data)
345 345 else:
346 346 thing = func(context, mapping, data)
347 thing = unwrapvalue(thing)
347 thing = unwrapvalue(context, mapping, thing)
348 348 if isinstance(thing, bool):
349 349 return thing
350 350 # other objects are evaluated as strings, which means 0 is True, but
351 351 # empty dict/list should be False as they are expected to be ''
352 352 return bool(stringify(context, mapping, thing))
353 353
354 354 def evaldate(context, mapping, arg, err=None):
355 355 """Evaluate given argument as a date tuple or a date string; returns
356 356 a (unixtime, offset) tuple"""
357 357 thing = evalrawexp(context, mapping, arg)
358 358 return unwrapdate(context, mapping, thing, err)
359 359
360 360 def unwrapdate(context, mapping, thing, err=None):
361 361 thing = _unwrapvalue(context, mapping, thing)
362 362 try:
363 363 return dateutil.parsedate(thing)
364 364 except AttributeError:
365 365 raise error.ParseError(err or _('not a date tuple nor a string'))
366 366 except error.ParseError:
367 367 if not err:
368 368 raise
369 369 raise error.ParseError(err)
370 370
371 371 def evalinteger(context, mapping, arg, err=None):
372 372 thing = evalrawexp(context, mapping, arg)
373 373 return unwrapinteger(context, mapping, thing, err)
374 374
375 375 def unwrapinteger(context, mapping, thing, err=None):
376 376 thing = _unwrapvalue(context, mapping, thing)
377 377 try:
378 378 return int(thing)
379 379 except (TypeError, ValueError):
380 380 raise error.ParseError(err or _('not an integer'))
381 381
382 382 def evalstring(context, mapping, arg):
383 383 return stringify(context, mapping, evalrawexp(context, mapping, arg))
384 384
385 385 def evalstringliteral(context, mapping, arg):
386 386 """Evaluate given argument as string template, but returns symbol name
387 387 if it is unknown"""
388 388 func, data = arg
389 389 if func is runsymbol:
390 390 thing = func(context, mapping, data, default=data)
391 391 else:
392 392 thing = func(context, mapping, data)
393 393 return stringify(context, mapping, thing)
394 394
395 395 _unwrapfuncbytype = {
396 396 None: _unwrapvalue,
397 397 bytes: stringify,
398 398 date: unwrapdate,
399 399 int: unwrapinteger,
400 400 }
401 401
402 402 def unwrapastype(context, mapping, thing, typ):
403 403 """Move the inner value object out of the wrapper and coerce its type"""
404 404 try:
405 405 f = _unwrapfuncbytype[typ]
406 406 except KeyError:
407 407 raise error.ProgrammingError('invalid type specified: %r' % typ)
408 408 return f(context, mapping, thing)
409 409
410 410 def runinteger(context, mapping, data):
411 411 return int(data)
412 412
413 413 def runstring(context, mapping, data):
414 414 return data
415 415
416 416 def _recursivesymbolblocker(key):
417 417 def showrecursion(**args):
418 418 raise error.Abort(_("recursive reference '%s' in template") % key)
419 419 return showrecursion
420 420
421 421 def runsymbol(context, mapping, key, default=''):
422 422 v = context.symbol(mapping, key)
423 423 if v is None:
424 424 # put poison to cut recursion. we can't move this to parsing phase
425 425 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
426 426 safemapping = mapping.copy()
427 427 safemapping[key] = _recursivesymbolblocker(key)
428 428 try:
429 429 v = context.process(key, safemapping)
430 430 except TemplateNotFound:
431 431 v = default
432 432 if callable(v) and getattr(v, '_requires', None) is None:
433 433 # old templatekw: expand all keywords and resources
434 434 # (TODO: deprecate this after porting web template keywords to new API)
435 435 props = {k: context._resources.lookup(context, mapping, k)
436 436 for k in context._resources.knownkeys()}
437 437 # pass context to _showcompatlist() through templatekw._showlist()
438 438 props['templ'] = context
439 439 props.update(mapping)
440 440 return v(**pycompat.strkwargs(props))
441 441 if callable(v):
442 442 # new templatekw
443 443 try:
444 444 return v(context, mapping)
445 445 except ResourceUnavailable:
446 446 # unsupported keyword is mapped to empty just like unknown keyword
447 447 return None
448 448 return v
449 449
450 450 def runtemplate(context, mapping, template):
451 451 for arg in template:
452 452 yield evalrawexp(context, mapping, arg)
453 453
454 454 def runfilter(context, mapping, data):
455 455 arg, filt = data
456 456 thing = evalrawexp(context, mapping, arg)
457 457 intype = getattr(filt, '_intype', None)
458 458 try:
459 459 thing = unwrapastype(context, mapping, thing, intype)
460 460 return filt(thing)
461 461 except error.ParseError as e:
462 462 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
463 463
464 464 def _formatfiltererror(arg, filt):
465 465 fn = pycompat.sysbytes(filt.__name__)
466 466 sym = findsymbolicname(arg)
467 467 if not sym:
468 468 return _("incompatible use of template filter '%s'") % fn
469 469 return (_("template filter '%s' is not compatible with keyword '%s'")
470 470 % (fn, sym))
471 471
472 472 def runmap(context, mapping, data):
473 473 darg, targ = data
474 474 d = evalrawexp(context, mapping, darg)
475 475 if util.safehasattr(d, 'itermaps'):
476 476 diter = d.itermaps()
477 477 else:
478 478 try:
479 479 diter = iter(d)
480 480 except TypeError:
481 481 sym = findsymbolicname(darg)
482 482 if sym:
483 483 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
484 484 else:
485 485 raise error.ParseError(_("%r is not iterable") % d)
486 486
487 487 for i, v in enumerate(diter):
488 488 if isinstance(v, dict):
489 489 lm = context.overlaymap(mapping, v)
490 490 lm['index'] = i
491 491 yield evalrawexp(context, lm, targ)
492 492 else:
493 493 # v is not an iterable of dicts, this happen when 'key'
494 494 # has been fully expanded already and format is useless.
495 495 # If so, return the expanded value.
496 496 yield v
497 497
498 498 def runmember(context, mapping, data):
499 499 darg, memb = data
500 500 d = evalrawexp(context, mapping, darg)
501 501 if util.safehasattr(d, 'tomap'):
502 502 lm = context.overlaymap(mapping, d.tomap())
503 503 return runsymbol(context, lm, memb)
504 504 if util.safehasattr(d, 'get'):
505 505 return getdictitem(d, memb)
506 506
507 507 sym = findsymbolicname(darg)
508 508 if sym:
509 509 raise error.ParseError(_("keyword '%s' has no member") % sym)
510 510 else:
511 511 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
512 512
513 513 def runnegate(context, mapping, data):
514 514 data = evalinteger(context, mapping, data,
515 515 _('negation needs an integer argument'))
516 516 return -data
517 517
518 518 def runarithmetic(context, mapping, data):
519 519 func, left, right = data
520 520 left = evalinteger(context, mapping, left,
521 521 _('arithmetic only defined on integers'))
522 522 right = evalinteger(context, mapping, right,
523 523 _('arithmetic only defined on integers'))
524 524 try:
525 525 return func(left, right)
526 526 except ZeroDivisionError:
527 527 raise error.Abort(_('division by zero is not defined'))
528 528
529 529 def getdictitem(dictarg, key):
530 530 val = dictarg.get(key)
531 531 if val is None:
532 532 return
533 533 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now