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