##// END OF EJS Templates
templater: unify unwrapvalue() with _unwrapvalue()...
Yuya Nishihara -
r38227:d48b80d5 default
parent child Browse files
Show More
@@ -1,705 +1,706 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 .node import (
14 14 bin,
15 15 wdirid,
16 16 )
17 17 from . import (
18 18 color,
19 19 encoding,
20 20 error,
21 21 minirst,
22 22 obsutil,
23 23 pycompat,
24 24 registrar,
25 25 revset as revsetmod,
26 26 revsetlang,
27 27 scmutil,
28 28 templatefilters,
29 29 templatekw,
30 30 templateutil,
31 31 util,
32 32 )
33 33 from .utils import (
34 34 dateutil,
35 35 stringutil,
36 36 )
37 37
38 38 evalrawexp = templateutil.evalrawexp
39 39 evalfuncarg = templateutil.evalfuncarg
40 40 evalboolean = templateutil.evalboolean
41 41 evaldate = templateutil.evaldate
42 42 evalinteger = templateutil.evalinteger
43 43 evalstring = templateutil.evalstring
44 44 evalstringliteral = templateutil.evalstringliteral
45 45
46 46 # dict of template built-in functions
47 47 funcs = {}
48 48 templatefunc = registrar.templatefunc(funcs)
49 49
50 50 @templatefunc('date(date[, fmt])')
51 51 def date(context, mapping, args):
52 52 """Format a date. See :hg:`help dates` for formatting
53 53 strings. The default is a Unix date format, including the timezone:
54 54 "Mon Sep 04 15:13:13 2006 0700"."""
55 55 if not (1 <= len(args) <= 2):
56 56 # i18n: "date" is a keyword
57 57 raise error.ParseError(_("date expects one or two arguments"))
58 58
59 59 date = evaldate(context, mapping, args[0],
60 60 # i18n: "date" is a keyword
61 61 _("date expects a date information"))
62 62 fmt = None
63 63 if len(args) == 2:
64 64 fmt = evalstring(context, mapping, args[1])
65 65 if fmt is None:
66 66 return dateutil.datestr(date)
67 67 else:
68 68 return dateutil.datestr(date, fmt)
69 69
70 70 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
71 71 def dict_(context, mapping, args):
72 72 """Construct a dict from key-value pairs. A key may be omitted if
73 73 a value expression can provide an unambiguous name."""
74 74 data = util.sortdict()
75 75
76 76 for v in args['args']:
77 77 k = templateutil.findsymbolicname(v)
78 78 if not k:
79 79 raise error.ParseError(_('dict key cannot be inferred'))
80 80 if k in data or k in args['kwargs']:
81 81 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
82 82 data[k] = evalfuncarg(context, mapping, v)
83 83
84 84 data.update((k, evalfuncarg(context, mapping, v))
85 85 for k, v in args['kwargs'].iteritems())
86 86 return templateutil.hybriddict(data)
87 87
88 88 @templatefunc('diff([includepattern [, excludepattern]])')
89 89 def diff(context, mapping, args):
90 90 """Show a diff, optionally
91 91 specifying files to include or exclude."""
92 92 if len(args) > 2:
93 93 # i18n: "diff" is a keyword
94 94 raise error.ParseError(_("diff expects zero, one, or two arguments"))
95 95
96 96 def getpatterns(i):
97 97 if i < len(args):
98 98 s = evalstring(context, mapping, args[i]).strip()
99 99 if s:
100 100 return [s]
101 101 return []
102 102
103 103 ctx = context.resource(mapping, 'ctx')
104 104 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
105 105
106 106 return ''.join(chunks)
107 107
108 108 @templatefunc('extdata(source)', argspec='source')
109 109 def extdata(context, mapping, args):
110 110 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
111 111 if 'source' not in args:
112 112 # i18n: "extdata" is a keyword
113 113 raise error.ParseError(_('extdata expects one argument'))
114 114
115 115 source = evalstring(context, mapping, args['source'])
116 116 if not source:
117 117 sym = templateutil.findsymbolicname(args['source'])
118 118 if sym:
119 119 raise error.ParseError(_('empty data source specified'),
120 120 hint=_("did you mean extdata('%s')?") % sym)
121 121 else:
122 122 raise error.ParseError(_('empty data source specified'))
123 123 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
124 124 ctx = context.resource(mapping, 'ctx')
125 125 if source in cache:
126 126 data = cache[source]
127 127 else:
128 128 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
129 129 return data.get(ctx.rev(), '')
130 130
131 131 @templatefunc('files(pattern)')
132 132 def files(context, mapping, args):
133 133 """All files of the current changeset matching the pattern. See
134 134 :hg:`help patterns`."""
135 135 if not len(args) == 1:
136 136 # i18n: "files" is a keyword
137 137 raise error.ParseError(_("files expects one argument"))
138 138
139 139 raw = evalstring(context, mapping, args[0])
140 140 ctx = context.resource(mapping, 'ctx')
141 141 m = ctx.match([raw])
142 142 files = list(ctx.matches(m))
143 143 return templateutil.compatlist(context, mapping, "file", files)
144 144
145 145 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
146 146 def fill(context, mapping, args):
147 147 """Fill many
148 148 paragraphs with optional indentation. See the "fill" filter."""
149 149 if not (1 <= len(args) <= 4):
150 150 # i18n: "fill" is a keyword
151 151 raise error.ParseError(_("fill expects one to four arguments"))
152 152
153 153 text = evalstring(context, mapping, args[0])
154 154 width = 76
155 155 initindent = ''
156 156 hangindent = ''
157 157 if 2 <= len(args) <= 4:
158 158 width = evalinteger(context, mapping, args[1],
159 159 # i18n: "fill" is a keyword
160 160 _("fill expects an integer width"))
161 161 try:
162 162 initindent = evalstring(context, mapping, args[2])
163 163 hangindent = evalstring(context, mapping, args[3])
164 164 except IndexError:
165 165 pass
166 166
167 167 return templatefilters.fill(text, width, initindent, hangindent)
168 168
169 169 @templatefunc('formatnode(node)')
170 170 def formatnode(context, mapping, args):
171 171 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
172 172 if len(args) != 1:
173 173 # i18n: "formatnode" is a keyword
174 174 raise error.ParseError(_("formatnode expects one argument"))
175 175
176 176 ui = context.resource(mapping, 'ui')
177 177 node = evalstring(context, mapping, args[0])
178 178 if ui.debugflag:
179 179 return node
180 180 return templatefilters.short(node)
181 181
182 182 @templatefunc('mailmap(author)')
183 183 def mailmap(context, mapping, args):
184 184 """Return the author, updated according to the value
185 185 set in the .mailmap file"""
186 186 if len(args) != 1:
187 187 raise error.ParseError(_("mailmap expects one argument"))
188 188
189 189 author = evalstring(context, mapping, args[0])
190 190
191 191 cache = context.resource(mapping, 'cache')
192 192 repo = context.resource(mapping, 'repo')
193 193
194 194 if 'mailmap' not in cache:
195 195 data = repo.wvfs.tryread('.mailmap')
196 196 cache['mailmap'] = stringutil.parsemailmap(data)
197 197
198 198 return stringutil.mapname(cache['mailmap'], author)
199 199
200 200 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
201 201 argspec='text width fillchar left')
202 202 def pad(context, mapping, args):
203 203 """Pad text with a
204 204 fill character."""
205 205 if 'text' not in args or 'width' not in args:
206 206 # i18n: "pad" is a keyword
207 207 raise error.ParseError(_("pad() expects two to four arguments"))
208 208
209 209 width = evalinteger(context, mapping, args['width'],
210 210 # i18n: "pad" is a keyword
211 211 _("pad() expects an integer width"))
212 212
213 213 text = evalstring(context, mapping, args['text'])
214 214
215 215 left = False
216 216 fillchar = ' '
217 217 if 'fillchar' in args:
218 218 fillchar = evalstring(context, mapping, args['fillchar'])
219 219 if len(color.stripeffects(fillchar)) != 1:
220 220 # i18n: "pad" is a keyword
221 221 raise error.ParseError(_("pad() expects a single fill character"))
222 222 if 'left' in args:
223 223 left = evalboolean(context, mapping, args['left'])
224 224
225 225 fillwidth = width - encoding.colwidth(color.stripeffects(text))
226 226 if fillwidth <= 0:
227 227 return text
228 228 if left:
229 229 return fillchar * fillwidth + text
230 230 else:
231 231 return text + fillchar * fillwidth
232 232
233 233 @templatefunc('indent(text, indentchars[, firstline])')
234 234 def indent(context, mapping, args):
235 235 """Indents all non-empty lines
236 236 with the characters given in the indentchars string. An optional
237 237 third parameter will override the indent for the first line only
238 238 if present."""
239 239 if not (2 <= len(args) <= 3):
240 240 # i18n: "indent" is a keyword
241 241 raise error.ParseError(_("indent() expects two or three arguments"))
242 242
243 243 text = evalstring(context, mapping, args[0])
244 244 indent = evalstring(context, mapping, args[1])
245 245
246 246 if len(args) == 3:
247 247 firstline = evalstring(context, mapping, args[2])
248 248 else:
249 249 firstline = indent
250 250
251 251 # the indent function doesn't indent the first line, so we do it here
252 252 return templatefilters.indent(firstline + text, indent)
253 253
254 254 @templatefunc('get(dict, key)')
255 255 def get(context, mapping, args):
256 256 """Get an attribute/key from an object. Some keywords
257 257 are complex types. This function allows you to obtain the value of an
258 258 attribute on these types."""
259 259 if len(args) != 2:
260 260 # i18n: "get" is a keyword
261 261 raise error.ParseError(_("get() expects two arguments"))
262 262
263 263 dictarg = evalfuncarg(context, mapping, args[0])
264 264 if not util.safehasattr(dictarg, 'get'):
265 265 # i18n: "get" is a keyword
266 266 raise error.ParseError(_("get() expects a dict as first argument"))
267 267
268 268 key = evalfuncarg(context, mapping, args[1])
269 269 return templateutil.getdictitem(dictarg, key)
270 270
271 271 @templatefunc('if(expr, then[, else])')
272 272 def if_(context, mapping, args):
273 273 """Conditionally execute based on the result of
274 274 an expression."""
275 275 if not (2 <= len(args) <= 3):
276 276 # i18n: "if" is a keyword
277 277 raise error.ParseError(_("if expects two or three arguments"))
278 278
279 279 test = evalboolean(context, mapping, args[0])
280 280 if test:
281 281 return evalrawexp(context, mapping, args[1])
282 282 elif len(args) == 3:
283 283 return evalrawexp(context, mapping, args[2])
284 284
285 285 @templatefunc('ifcontains(needle, haystack, then[, else])')
286 286 def ifcontains(context, mapping, args):
287 287 """Conditionally execute based
288 288 on whether the item "needle" is in "haystack"."""
289 289 if not (3 <= len(args) <= 4):
290 290 # i18n: "ifcontains" is a keyword
291 291 raise error.ParseError(_("ifcontains expects three or four arguments"))
292 292
293 293 haystack = evalfuncarg(context, mapping, args[1])
294 294 keytype = getattr(haystack, 'keytype', None)
295 295 try:
296 296 needle = evalrawexp(context, mapping, args[0])
297 297 needle = templateutil.unwrapastype(context, mapping, needle,
298 298 keytype or bytes)
299 299 found = (needle in haystack)
300 300 except error.ParseError:
301 301 found = False
302 302
303 303 if found:
304 304 return evalrawexp(context, mapping, args[2])
305 305 elif len(args) == 4:
306 306 return evalrawexp(context, mapping, args[3])
307 307
308 308 @templatefunc('ifeq(expr1, expr2, then[, else])')
309 309 def ifeq(context, mapping, args):
310 310 """Conditionally execute based on
311 311 whether 2 items are equivalent."""
312 312 if not (3 <= len(args) <= 4):
313 313 # i18n: "ifeq" is a keyword
314 314 raise error.ParseError(_("ifeq expects three or four arguments"))
315 315
316 316 test = evalstring(context, mapping, args[0])
317 317 match = evalstring(context, mapping, args[1])
318 318 if test == match:
319 319 return evalrawexp(context, mapping, args[2])
320 320 elif len(args) == 4:
321 321 return evalrawexp(context, mapping, args[3])
322 322
323 323 @templatefunc('join(list, sep)')
324 324 def join(context, mapping, args):
325 325 """Join items in a list with a delimiter."""
326 326 if not (1 <= len(args) <= 2):
327 327 # i18n: "join" is a keyword
328 328 raise error.ParseError(_("join expects one or two arguments"))
329 329
330 330 joinset = evalrawexp(context, mapping, args[0])
331 331 joiner = " "
332 332 if len(args) > 1:
333 333 joiner = evalstring(context, mapping, args[1])
334 334 if isinstance(joinset, templateutil.wrapped):
335 335 return joinset.join(context, mapping, joiner)
336 # TODO: perhaps a generator should be stringify()-ed here, but we can't
337 # because hgweb abuses it as a keyword that returns a list of dicts.
336 # TODO: rethink about join() of a byte string, which had no defined
337 # behavior since a string may be either a bytes or a generator.
338 # TODO: fix type error on join() of non-iterable
338 339 joinset = templateutil.unwrapvalue(context, mapping, joinset)
339 340 return templateutil.joinitems(pycompat.maybebytestr(joinset), joiner)
340 341
341 342 @templatefunc('label(label, expr)')
342 343 def label(context, mapping, args):
343 344 """Apply a label to generated content. Content with
344 345 a label applied can result in additional post-processing, such as
345 346 automatic colorization."""
346 347 if len(args) != 2:
347 348 # i18n: "label" is a keyword
348 349 raise error.ParseError(_("label expects two arguments"))
349 350
350 351 ui = context.resource(mapping, 'ui')
351 352 thing = evalstring(context, mapping, args[1])
352 353 # preserve unknown symbol as literal so effects like 'red', 'bold',
353 354 # etc. don't need to be quoted
354 355 label = evalstringliteral(context, mapping, args[0])
355 356
356 357 return ui.label(thing, label)
357 358
358 359 @templatefunc('latesttag([pattern])')
359 360 def latesttag(context, mapping, args):
360 361 """The global tags matching the given pattern on the
361 362 most recent globally tagged ancestor of this changeset.
362 363 If no such tags exist, the "{tag}" template resolves to
363 364 the string "null". See :hg:`help revisions.patterns` for the pattern
364 365 syntax.
365 366 """
366 367 if len(args) > 1:
367 368 # i18n: "latesttag" is a keyword
368 369 raise error.ParseError(_("latesttag expects at most one argument"))
369 370
370 371 pattern = None
371 372 if len(args) == 1:
372 373 pattern = evalstring(context, mapping, args[0])
373 374 return templatekw.showlatesttags(context, mapping, pattern)
374 375
375 376 @templatefunc('localdate(date[, tz])')
376 377 def localdate(context, mapping, args):
377 378 """Converts a date to the specified timezone.
378 379 The default is local date."""
379 380 if not (1 <= len(args) <= 2):
380 381 # i18n: "localdate" is a keyword
381 382 raise error.ParseError(_("localdate expects one or two arguments"))
382 383
383 384 date = evaldate(context, mapping, args[0],
384 385 # i18n: "localdate" is a keyword
385 386 _("localdate expects a date information"))
386 387 if len(args) >= 2:
387 388 tzoffset = None
388 389 tz = evalfuncarg(context, mapping, args[1])
389 390 if isinstance(tz, bytes):
390 391 tzoffset, remainder = dateutil.parsetimezone(tz)
391 392 if remainder:
392 393 tzoffset = None
393 394 if tzoffset is None:
394 395 try:
395 396 tzoffset = int(tz)
396 397 except (TypeError, ValueError):
397 398 # i18n: "localdate" is a keyword
398 399 raise error.ParseError(_("localdate expects a timezone"))
399 400 else:
400 401 tzoffset = dateutil.makedate()[1]
401 402 return (date[0], tzoffset)
402 403
403 404 @templatefunc('max(iterable)')
404 405 def max_(context, mapping, args, **kwargs):
405 406 """Return the max of an iterable"""
406 407 if len(args) != 1:
407 408 # i18n: "max" is a keyword
408 409 raise error.ParseError(_("max expects one argument"))
409 410
410 411 iterable = evalfuncarg(context, mapping, args[0])
411 412 try:
412 413 x = max(pycompat.maybebytestr(iterable))
413 414 except (TypeError, ValueError):
414 415 # i18n: "max" is a keyword
415 416 raise error.ParseError(_("max first argument should be an iterable"))
416 417 return templateutil.wraphybridvalue(iterable, x, x)
417 418
418 419 @templatefunc('min(iterable)')
419 420 def min_(context, mapping, args, **kwargs):
420 421 """Return the min of an iterable"""
421 422 if len(args) != 1:
422 423 # i18n: "min" is a keyword
423 424 raise error.ParseError(_("min expects one argument"))
424 425
425 426 iterable = evalfuncarg(context, mapping, args[0])
426 427 try:
427 428 x = min(pycompat.maybebytestr(iterable))
428 429 except (TypeError, ValueError):
429 430 # i18n: "min" is a keyword
430 431 raise error.ParseError(_("min first argument should be an iterable"))
431 432 return templateutil.wraphybridvalue(iterable, x, x)
432 433
433 434 @templatefunc('mod(a, b)')
434 435 def mod(context, mapping, args):
435 436 """Calculate a mod b such that a / b + a mod b == a"""
436 437 if not len(args) == 2:
437 438 # i18n: "mod" is a keyword
438 439 raise error.ParseError(_("mod expects two arguments"))
439 440
440 441 func = lambda a, b: a % b
441 442 return templateutil.runarithmetic(context, mapping,
442 443 (func, args[0], args[1]))
443 444
444 445 @templatefunc('obsfateoperations(markers)')
445 446 def obsfateoperations(context, mapping, args):
446 447 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
447 448 if len(args) != 1:
448 449 # i18n: "obsfateoperations" is a keyword
449 450 raise error.ParseError(_("obsfateoperations expects one argument"))
450 451
451 452 markers = evalfuncarg(context, mapping, args[0])
452 453
453 454 try:
454 455 data = obsutil.markersoperations(markers)
455 456 return templateutil.hybridlist(data, name='operation')
456 457 except (TypeError, KeyError):
457 458 # i18n: "obsfateoperations" is a keyword
458 459 errmsg = _("obsfateoperations first argument should be an iterable")
459 460 raise error.ParseError(errmsg)
460 461
461 462 @templatefunc('obsfatedate(markers)')
462 463 def obsfatedate(context, mapping, args):
463 464 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
464 465 if len(args) != 1:
465 466 # i18n: "obsfatedate" is a keyword
466 467 raise error.ParseError(_("obsfatedate expects one argument"))
467 468
468 469 markers = evalfuncarg(context, mapping, args[0])
469 470
470 471 try:
471 472 data = obsutil.markersdates(markers)
472 473 return templateutil.hybridlist(data, name='date', fmt='%d %d')
473 474 except (TypeError, KeyError):
474 475 # i18n: "obsfatedate" is a keyword
475 476 errmsg = _("obsfatedate first argument should be an iterable")
476 477 raise error.ParseError(errmsg)
477 478
478 479 @templatefunc('obsfateusers(markers)')
479 480 def obsfateusers(context, mapping, args):
480 481 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
481 482 if len(args) != 1:
482 483 # i18n: "obsfateusers" is a keyword
483 484 raise error.ParseError(_("obsfateusers expects one argument"))
484 485
485 486 markers = evalfuncarg(context, mapping, args[0])
486 487
487 488 try:
488 489 data = obsutil.markersusers(markers)
489 490 return templateutil.hybridlist(data, name='user')
490 491 except (TypeError, KeyError, ValueError):
491 492 # i18n: "obsfateusers" is a keyword
492 493 msg = _("obsfateusers first argument should be an iterable of "
493 494 "obsmakers")
494 495 raise error.ParseError(msg)
495 496
496 497 @templatefunc('obsfateverb(successors, markers)')
497 498 def obsfateverb(context, mapping, args):
498 499 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
499 500 if len(args) != 2:
500 501 # i18n: "obsfateverb" is a keyword
501 502 raise error.ParseError(_("obsfateverb expects two arguments"))
502 503
503 504 successors = evalfuncarg(context, mapping, args[0])
504 505 markers = evalfuncarg(context, mapping, args[1])
505 506
506 507 try:
507 508 return obsutil.obsfateverb(successors, markers)
508 509 except TypeError:
509 510 # i18n: "obsfateverb" is a keyword
510 511 errmsg = _("obsfateverb first argument should be countable")
511 512 raise error.ParseError(errmsg)
512 513
513 514 @templatefunc('relpath(path)')
514 515 def relpath(context, mapping, args):
515 516 """Convert a repository-absolute path into a filesystem path relative to
516 517 the current working directory."""
517 518 if len(args) != 1:
518 519 # i18n: "relpath" is a keyword
519 520 raise error.ParseError(_("relpath expects one argument"))
520 521
521 522 repo = context.resource(mapping, 'ctx').repo()
522 523 path = evalstring(context, mapping, args[0])
523 524 return repo.pathto(path)
524 525
525 526 @templatefunc('revset(query[, formatargs...])')
526 527 def revset(context, mapping, args):
527 528 """Execute a revision set query. See
528 529 :hg:`help revset`."""
529 530 if not len(args) > 0:
530 531 # i18n: "revset" is a keyword
531 532 raise error.ParseError(_("revset expects one or more arguments"))
532 533
533 534 raw = evalstring(context, mapping, args[0])
534 535 ctx = context.resource(mapping, 'ctx')
535 536 repo = ctx.repo()
536 537
537 538 def query(expr):
538 539 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
539 540 return m(repo)
540 541
541 542 if len(args) > 1:
542 543 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
543 544 revs = query(revsetlang.formatspec(raw, *formatargs))
544 545 revs = list(revs)
545 546 else:
546 547 cache = context.resource(mapping, 'cache')
547 548 revsetcache = cache.setdefault("revsetcache", {})
548 549 if raw in revsetcache:
549 550 revs = revsetcache[raw]
550 551 else:
551 552 revs = query(raw)
552 553 revs = list(revs)
553 554 revsetcache[raw] = revs
554 555 return templatekw.showrevslist(context, mapping, "revision", revs)
555 556
556 557 @templatefunc('rstdoc(text, style)')
557 558 def rstdoc(context, mapping, args):
558 559 """Format reStructuredText."""
559 560 if len(args) != 2:
560 561 # i18n: "rstdoc" is a keyword
561 562 raise error.ParseError(_("rstdoc expects two arguments"))
562 563
563 564 text = evalstring(context, mapping, args[0])
564 565 style = evalstring(context, mapping, args[1])
565 566
566 567 return minirst.format(text, style=style, keep=['verbose'])
567 568
568 569 @templatefunc('separate(sep, args)', argspec='sep *args')
569 570 def separate(context, mapping, args):
570 571 """Add a separator between non-empty arguments."""
571 572 if 'sep' not in args:
572 573 # i18n: "separate" is a keyword
573 574 raise error.ParseError(_("separate expects at least one argument"))
574 575
575 576 sep = evalstring(context, mapping, args['sep'])
576 577 first = True
577 578 for arg in args['args']:
578 579 argstr = evalstring(context, mapping, arg)
579 580 if not argstr:
580 581 continue
581 582 if first:
582 583 first = False
583 584 else:
584 585 yield sep
585 586 yield argstr
586 587
587 588 @templatefunc('shortest(node, minlength=4)')
588 589 def shortest(context, mapping, args):
589 590 """Obtain the shortest representation of
590 591 a node."""
591 592 if not (1 <= len(args) <= 2):
592 593 # i18n: "shortest" is a keyword
593 594 raise error.ParseError(_("shortest() expects one or two arguments"))
594 595
595 596 hexnode = evalstring(context, mapping, args[0])
596 597
597 598 minlength = 4
598 599 if len(args) > 1:
599 600 minlength = evalinteger(context, mapping, args[1],
600 601 # i18n: "shortest" is a keyword
601 602 _("shortest() expects an integer minlength"))
602 603
603 604 repo = context.resource(mapping, 'ctx')._repo
604 605 if len(hexnode) > 40:
605 606 return hexnode
606 607 elif len(hexnode) == 40:
607 608 try:
608 609 node = bin(hexnode)
609 610 except TypeError:
610 611 return hexnode
611 612 else:
612 613 try:
613 614 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
614 615 except error.WdirUnsupported:
615 616 node = wdirid
616 617 except error.LookupError:
617 618 return hexnode
618 619 if not node:
619 620 return hexnode
620 621 try:
621 622 return scmutil.shortesthexnodeidprefix(repo, node, minlength)
622 623 except error.RepoLookupError:
623 624 return hexnode
624 625
625 626 @templatefunc('strip(text[, chars])')
626 627 def strip(context, mapping, args):
627 628 """Strip characters from a string. By default,
628 629 strips all leading and trailing whitespace."""
629 630 if not (1 <= len(args) <= 2):
630 631 # i18n: "strip" is a keyword
631 632 raise error.ParseError(_("strip expects one or two arguments"))
632 633
633 634 text = evalstring(context, mapping, args[0])
634 635 if len(args) == 2:
635 636 chars = evalstring(context, mapping, args[1])
636 637 return text.strip(chars)
637 638 return text.strip()
638 639
639 640 @templatefunc('sub(pattern, replacement, expression)')
640 641 def sub(context, mapping, args):
641 642 """Perform text substitution
642 643 using regular expressions."""
643 644 if len(args) != 3:
644 645 # i18n: "sub" is a keyword
645 646 raise error.ParseError(_("sub expects three arguments"))
646 647
647 648 pat = evalstring(context, mapping, args[0])
648 649 rpl = evalstring(context, mapping, args[1])
649 650 src = evalstring(context, mapping, args[2])
650 651 try:
651 652 patre = re.compile(pat)
652 653 except re.error:
653 654 # i18n: "sub" is a keyword
654 655 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
655 656 try:
656 657 yield patre.sub(rpl, src)
657 658 except re.error:
658 659 # i18n: "sub" is a keyword
659 660 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
660 661
661 662 @templatefunc('startswith(pattern, text)')
662 663 def startswith(context, mapping, args):
663 664 """Returns the value from the "text" argument
664 665 if it begins with the content from the "pattern" argument."""
665 666 if len(args) != 2:
666 667 # i18n: "startswith" is a keyword
667 668 raise error.ParseError(_("startswith expects two arguments"))
668 669
669 670 patn = evalstring(context, mapping, args[0])
670 671 text = evalstring(context, mapping, args[1])
671 672 if text.startswith(patn):
672 673 return text
673 674 return ''
674 675
675 676 @templatefunc('word(number, text[, separator])')
676 677 def word(context, mapping, args):
677 678 """Return the nth word from a string."""
678 679 if not (2 <= len(args) <= 3):
679 680 # i18n: "word" is a keyword
680 681 raise error.ParseError(_("word expects two or three arguments, got %d")
681 682 % len(args))
682 683
683 684 num = evalinteger(context, mapping, args[0],
684 685 # i18n: "word" is a keyword
685 686 _("word expects an integer index"))
686 687 text = evalstring(context, mapping, args[1])
687 688 if len(args) == 3:
688 689 splitter = evalstring(context, mapping, args[2])
689 690 else:
690 691 splitter = None
691 692
692 693 tokens = text.split(splitter)
693 694 if num >= len(tokens) or num < -len(tokens):
694 695 return ''
695 696 else:
696 697 return tokens[num]
697 698
698 699 def loadfunction(ui, extname, registrarobj):
699 700 """Load template function from specified registrarobj
700 701 """
701 702 for name, func in registrarobj._table.iteritems():
702 703 funcs[name] = func
703 704
704 705 # tell hggettext to extract docstrings from these functions:
705 706 i18nfunctions = funcs.values()
@@ -1,924 +1,921 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 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 returns a generator of dicts.)
31
32 29 None
33 30 sometimes represents an empty value, which can be stringified to ''.
34 31
35 32 True, False, int, float
36 33 can be stringified as such.
37 34
38 35 date tuple
39 36 a (unixtime, offset) tuple, which produces no meaningful output by itself.
40 37
41 38 hybrid
42 39 represents a list/dict of printable values, which can also be converted
43 40 to mappings by % operator.
44 41
45 42 mappable
46 43 represents a scalar printable value, also supports % operator.
47 44
48 45 mappinggenerator, mappinglist
49 46 represents mappings (i.e. a list of dicts), which may have default
50 47 output format.
51 48
52 49 mappedgenerator
53 50 a lazily-evaluated list of byte strings, which is e.g. a result of %
54 51 operation.
55 52 """
56 53
57 54 from __future__ import absolute_import, print_function
58 55
59 56 import abc
60 57 import os
61 58
62 59 from .i18n import _
63 60 from . import (
64 61 config,
65 62 encoding,
66 63 error,
67 64 parser,
68 65 pycompat,
69 66 templatefilters,
70 67 templatefuncs,
71 68 templateutil,
72 69 util,
73 70 )
74 71 from .utils import (
75 72 stringutil,
76 73 )
77 74
78 75 # template parsing
79 76
80 77 elements = {
81 78 # token-type: binding-strength, primary, prefix, infix, suffix
82 79 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
83 80 ".": (18, None, None, (".", 18), None),
84 81 "%": (15, None, None, ("%", 15), None),
85 82 "|": (15, None, None, ("|", 15), None),
86 83 "*": (5, None, None, ("*", 5), None),
87 84 "/": (5, None, None, ("/", 5), None),
88 85 "+": (4, None, None, ("+", 4), None),
89 86 "-": (4, None, ("negate", 19), ("-", 4), None),
90 87 "=": (3, None, None, ("keyvalue", 3), None),
91 88 ",": (2, None, None, ("list", 2), None),
92 89 ")": (0, None, None, None, None),
93 90 "integer": (0, "integer", None, None, None),
94 91 "symbol": (0, "symbol", None, None, None),
95 92 "string": (0, "string", None, None, None),
96 93 "template": (0, "template", None, None, None),
97 94 "end": (0, None, None, None, None),
98 95 }
99 96
100 97 def tokenize(program, start, end, term=None):
101 98 """Parse a template expression into a stream of tokens, which must end
102 99 with term if specified"""
103 100 pos = start
104 101 program = pycompat.bytestr(program)
105 102 while pos < end:
106 103 c = program[pos]
107 104 if c.isspace(): # skip inter-token whitespace
108 105 pass
109 106 elif c in "(=,).%|+-*/": # handle simple operators
110 107 yield (c, None, pos)
111 108 elif c in '"\'': # handle quoted templates
112 109 s = pos + 1
113 110 data, pos = _parsetemplate(program, s, end, c)
114 111 yield ('template', data, s)
115 112 pos -= 1
116 113 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
117 114 # handle quoted strings
118 115 c = program[pos + 1]
119 116 s = pos = pos + 2
120 117 while pos < end: # find closing quote
121 118 d = program[pos]
122 119 if d == '\\': # skip over escaped characters
123 120 pos += 2
124 121 continue
125 122 if d == c:
126 123 yield ('string', program[s:pos], s)
127 124 break
128 125 pos += 1
129 126 else:
130 127 raise error.ParseError(_("unterminated string"), s)
131 128 elif c.isdigit():
132 129 s = pos
133 130 while pos < end:
134 131 d = program[pos]
135 132 if not d.isdigit():
136 133 break
137 134 pos += 1
138 135 yield ('integer', program[s:pos], s)
139 136 pos -= 1
140 137 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
141 138 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
142 139 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
143 140 # where some of nested templates were preprocessed as strings and
144 141 # then compiled. therefore, \"...\" was allowed. (issue4733)
145 142 #
146 143 # processing flow of _evalifliteral() at 5ab28a2e9962:
147 144 # outer template string -> stringify() -> compiletemplate()
148 145 # ------------------------ ------------ ------------------
149 146 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
150 147 # ~~~~~~~~
151 148 # escaped quoted string
152 149 if c == 'r':
153 150 pos += 1
154 151 token = 'string'
155 152 else:
156 153 token = 'template'
157 154 quote = program[pos:pos + 2]
158 155 s = pos = pos + 2
159 156 while pos < end: # find closing escaped quote
160 157 if program.startswith('\\\\\\', pos, end):
161 158 pos += 4 # skip over double escaped characters
162 159 continue
163 160 if program.startswith(quote, pos, end):
164 161 # interpret as if it were a part of an outer string
165 162 data = parser.unescapestr(program[s:pos])
166 163 if token == 'template':
167 164 data = _parsetemplate(data, 0, len(data))[0]
168 165 yield (token, data, s)
169 166 pos += 1
170 167 break
171 168 pos += 1
172 169 else:
173 170 raise error.ParseError(_("unterminated string"), s)
174 171 elif c.isalnum() or c in '_':
175 172 s = pos
176 173 pos += 1
177 174 while pos < end: # find end of symbol
178 175 d = program[pos]
179 176 if not (d.isalnum() or d == "_"):
180 177 break
181 178 pos += 1
182 179 sym = program[s:pos]
183 180 yield ('symbol', sym, s)
184 181 pos -= 1
185 182 elif c == term:
186 183 yield ('end', None, pos)
187 184 return
188 185 else:
189 186 raise error.ParseError(_("syntax error"), pos)
190 187 pos += 1
191 188 if term:
192 189 raise error.ParseError(_("unterminated template expansion"), start)
193 190 yield ('end', None, pos)
194 191
195 192 def _parsetemplate(tmpl, start, stop, quote=''):
196 193 r"""
197 194 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
198 195 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
199 196 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
200 197 ([('string', 'foo'), ('symbol', 'bar')], 9)
201 198 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
202 199 ([('string', 'foo')], 4)
203 200 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
204 201 ([('string', 'foo"'), ('string', 'bar')], 9)
205 202 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
206 203 ([('string', 'foo\\')], 6)
207 204 """
208 205 parsed = []
209 206 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
210 207 if typ == 'string':
211 208 parsed.append((typ, val))
212 209 elif typ == 'template':
213 210 parsed.append(val)
214 211 elif typ == 'end':
215 212 return parsed, pos
216 213 else:
217 214 raise error.ProgrammingError('unexpected type: %s' % typ)
218 215 raise error.ProgrammingError('unterminated scanning of template')
219 216
220 217 def scantemplate(tmpl, raw=False):
221 218 r"""Scan (type, start, end) positions of outermost elements in template
222 219
223 220 If raw=True, a backslash is not taken as an escape character just like
224 221 r'' string in Python. Note that this is different from r'' literal in
225 222 template in that no template fragment can appear in r'', e.g. r'{foo}'
226 223 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
227 224 'foo'.
228 225
229 226 >>> list(scantemplate(b'foo{bar}"baz'))
230 227 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
231 228 >>> list(scantemplate(b'outer{"inner"}outer'))
232 229 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
233 230 >>> list(scantemplate(b'foo\\{escaped}'))
234 231 [('string', 0, 5), ('string', 5, 13)]
235 232 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
236 233 [('string', 0, 4), ('template', 4, 13)]
237 234 """
238 235 last = None
239 236 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
240 237 if last:
241 238 yield last + (pos,)
242 239 if typ == 'end':
243 240 return
244 241 else:
245 242 last = (typ, pos)
246 243 raise error.ProgrammingError('unterminated scanning of template')
247 244
248 245 def _scantemplate(tmpl, start, stop, quote='', raw=False):
249 246 """Parse template string into chunks of strings and template expressions"""
250 247 sepchars = '{' + quote
251 248 unescape = [parser.unescapestr, pycompat.identity][raw]
252 249 pos = start
253 250 p = parser.parser(elements)
254 251 try:
255 252 while pos < stop:
256 253 n = min((tmpl.find(c, pos, stop)
257 254 for c in pycompat.bytestr(sepchars)),
258 255 key=lambda n: (n < 0, n))
259 256 if n < 0:
260 257 yield ('string', unescape(tmpl[pos:stop]), pos)
261 258 pos = stop
262 259 break
263 260 c = tmpl[n:n + 1]
264 261 bs = 0 # count leading backslashes
265 262 if not raw:
266 263 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
267 264 if bs % 2 == 1:
268 265 # escaped (e.g. '\{', '\\\{', but not '\\{')
269 266 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
270 267 pos = n + 1
271 268 continue
272 269 if n > pos:
273 270 yield ('string', unescape(tmpl[pos:n]), pos)
274 271 if c == quote:
275 272 yield ('end', None, n + 1)
276 273 return
277 274
278 275 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
279 276 if not tmpl.startswith('}', pos):
280 277 raise error.ParseError(_("invalid token"), pos)
281 278 yield ('template', parseres, n)
282 279 pos += 1
283 280
284 281 if quote:
285 282 raise error.ParseError(_("unterminated string"), start)
286 283 except error.ParseError as inst:
287 284 if len(inst.args) > 1: # has location
288 285 loc = inst.args[1]
289 286 # Offset the caret location by the number of newlines before the
290 287 # location of the error, since we will replace one-char newlines
291 288 # with the two-char literal r'\n'.
292 289 offset = tmpl[:loc].count('\n')
293 290 tmpl = tmpl.replace('\n', br'\n')
294 291 # We want the caret to point to the place in the template that
295 292 # failed to parse, but in a hint we get a open paren at the
296 293 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
297 294 # to line up the caret with the location of the error.
298 295 inst.hint = (tmpl + '\n'
299 296 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
300 297 raise
301 298 yield ('end', None, pos)
302 299
303 300 def _unnesttemplatelist(tree):
304 301 """Expand list of templates to node tuple
305 302
306 303 >>> def f(tree):
307 304 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
308 305 >>> f((b'template', []))
309 306 (string '')
310 307 >>> f((b'template', [(b'string', b'foo')]))
311 308 (string 'foo')
312 309 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
313 310 (template
314 311 (string 'foo')
315 312 (symbol 'rev'))
316 313 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
317 314 (template
318 315 (symbol 'rev'))
319 316 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
320 317 (string 'foo')
321 318 """
322 319 if not isinstance(tree, tuple):
323 320 return tree
324 321 op = tree[0]
325 322 if op != 'template':
326 323 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
327 324
328 325 assert len(tree) == 2
329 326 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
330 327 if not xs:
331 328 return ('string', '') # empty template ""
332 329 elif len(xs) == 1 and xs[0][0] == 'string':
333 330 return xs[0] # fast path for string with no template fragment "x"
334 331 else:
335 332 return (op,) + xs
336 333
337 334 def parse(tmpl):
338 335 """Parse template string into tree"""
339 336 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
340 337 assert pos == len(tmpl), 'unquoted template should be consumed'
341 338 return _unnesttemplatelist(('template', parsed))
342 339
343 340 def _parseexpr(expr):
344 341 """Parse a template expression into tree
345 342
346 343 >>> _parseexpr(b'"foo"')
347 344 ('string', 'foo')
348 345 >>> _parseexpr(b'foo(bar)')
349 346 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
350 347 >>> _parseexpr(b'foo(')
351 348 Traceback (most recent call last):
352 349 ...
353 350 ParseError: ('not a prefix: end', 4)
354 351 >>> _parseexpr(b'"foo" "bar"')
355 352 Traceback (most recent call last):
356 353 ...
357 354 ParseError: ('invalid token', 7)
358 355 """
359 356 p = parser.parser(elements)
360 357 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
361 358 if pos != len(expr):
362 359 raise error.ParseError(_('invalid token'), pos)
363 360 return _unnesttemplatelist(tree)
364 361
365 362 def prettyformat(tree):
366 363 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
367 364
368 365 def compileexp(exp, context, curmethods):
369 366 """Compile parsed template tree to (func, data) pair"""
370 367 if not exp:
371 368 raise error.ParseError(_("missing argument"))
372 369 t = exp[0]
373 370 if t in curmethods:
374 371 return curmethods[t](exp, context)
375 372 raise error.ParseError(_("unknown method '%s'") % t)
376 373
377 374 # template evaluation
378 375
379 376 def getsymbol(exp):
380 377 if exp[0] == 'symbol':
381 378 return exp[1]
382 379 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
383 380
384 381 def getlist(x):
385 382 if not x:
386 383 return []
387 384 if x[0] == 'list':
388 385 return getlist(x[1]) + [x[2]]
389 386 return [x]
390 387
391 388 def gettemplate(exp, context):
392 389 """Compile given template tree or load named template from map file;
393 390 returns (func, data) pair"""
394 391 if exp[0] in ('template', 'string'):
395 392 return compileexp(exp, context, methods)
396 393 if exp[0] == 'symbol':
397 394 # unlike runsymbol(), here 'symbol' is always taken as template name
398 395 # even if it exists in mapping. this allows us to override mapping
399 396 # by web templates, e.g. 'changelogtag' is redefined in map file.
400 397 return context._load(exp[1])
401 398 raise error.ParseError(_("expected template specifier"))
402 399
403 400 def _runrecursivesymbol(context, mapping, key):
404 401 raise error.Abort(_("recursive reference '%s' in template") % key)
405 402
406 403 def buildtemplate(exp, context):
407 404 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
408 405 return (templateutil.runtemplate, ctmpl)
409 406
410 407 def buildfilter(exp, context):
411 408 n = getsymbol(exp[2])
412 409 if n in context._filters:
413 410 filt = context._filters[n]
414 411 arg = compileexp(exp[1], context, methods)
415 412 return (templateutil.runfilter, (arg, filt))
416 413 if n in context._funcs:
417 414 f = context._funcs[n]
418 415 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
419 416 return (f, args)
420 417 raise error.ParseError(_("unknown function '%s'") % n)
421 418
422 419 def buildmap(exp, context):
423 420 darg = compileexp(exp[1], context, methods)
424 421 targ = gettemplate(exp[2], context)
425 422 return (templateutil.runmap, (darg, targ))
426 423
427 424 def buildmember(exp, context):
428 425 darg = compileexp(exp[1], context, methods)
429 426 memb = getsymbol(exp[2])
430 427 return (templateutil.runmember, (darg, memb))
431 428
432 429 def buildnegate(exp, context):
433 430 arg = compileexp(exp[1], context, exprmethods)
434 431 return (templateutil.runnegate, arg)
435 432
436 433 def buildarithmetic(exp, context, func):
437 434 left = compileexp(exp[1], context, exprmethods)
438 435 right = compileexp(exp[2], context, exprmethods)
439 436 return (templateutil.runarithmetic, (func, left, right))
440 437
441 438 def buildfunc(exp, context):
442 439 n = getsymbol(exp[1])
443 440 if n in context._funcs:
444 441 f = context._funcs[n]
445 442 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
446 443 return (f, args)
447 444 if n in context._filters:
448 445 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
449 446 if len(args) != 1:
450 447 raise error.ParseError(_("filter %s expects one argument") % n)
451 448 f = context._filters[n]
452 449 return (templateutil.runfilter, (args[0], f))
453 450 raise error.ParseError(_("unknown function '%s'") % n)
454 451
455 452 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
456 453 """Compile parsed tree of function arguments into list or dict of
457 454 (func, data) pairs
458 455
459 456 >>> context = engine(lambda t: (templateutil.runsymbol, t))
460 457 >>> def fargs(expr, argspec):
461 458 ... x = _parseexpr(expr)
462 459 ... n = getsymbol(x[1])
463 460 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
464 461 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
465 462 ['l', 'k']
466 463 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
467 464 >>> list(args.keys()), list(args[b'opts'].keys())
468 465 (['opts'], ['opts', 'k'])
469 466 """
470 467 def compiledict(xs):
471 468 return util.sortdict((k, compileexp(x, context, curmethods))
472 469 for k, x in xs.iteritems())
473 470 def compilelist(xs):
474 471 return [compileexp(x, context, curmethods) for x in xs]
475 472
476 473 if not argspec:
477 474 # filter or function with no argspec: return list of positional args
478 475 return compilelist(getlist(exp))
479 476
480 477 # function with argspec: return dict of named args
481 478 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
482 479 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
483 480 keyvaluenode='keyvalue', keynode='symbol')
484 481 compargs = util.sortdict()
485 482 if varkey:
486 483 compargs[varkey] = compilelist(treeargs.pop(varkey))
487 484 if optkey:
488 485 compargs[optkey] = compiledict(treeargs.pop(optkey))
489 486 compargs.update(compiledict(treeargs))
490 487 return compargs
491 488
492 489 def buildkeyvaluepair(exp, content):
493 490 raise error.ParseError(_("can't use a key-value pair in this context"))
494 491
495 492 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
496 493 exprmethods = {
497 494 "integer": lambda e, c: (templateutil.runinteger, e[1]),
498 495 "string": lambda e, c: (templateutil.runstring, e[1]),
499 496 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
500 497 "template": buildtemplate,
501 498 "group": lambda e, c: compileexp(e[1], c, exprmethods),
502 499 ".": buildmember,
503 500 "|": buildfilter,
504 501 "%": buildmap,
505 502 "func": buildfunc,
506 503 "keyvalue": buildkeyvaluepair,
507 504 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
508 505 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
509 506 "negate": buildnegate,
510 507 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
511 508 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
512 509 }
513 510
514 511 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
515 512 methods = exprmethods.copy()
516 513 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
517 514
518 515 class _aliasrules(parser.basealiasrules):
519 516 """Parsing and expansion rule set of template aliases"""
520 517 _section = _('template alias')
521 518 _parse = staticmethod(_parseexpr)
522 519
523 520 @staticmethod
524 521 def _trygetfunc(tree):
525 522 """Return (name, args) if tree is func(...) or ...|filter; otherwise
526 523 None"""
527 524 if tree[0] == 'func' and tree[1][0] == 'symbol':
528 525 return tree[1][1], getlist(tree[2])
529 526 if tree[0] == '|' and tree[2][0] == 'symbol':
530 527 return tree[2][1], [tree[1]]
531 528
532 529 def expandaliases(tree, aliases):
533 530 """Return new tree of aliases are expanded"""
534 531 aliasmap = _aliasrules.buildmap(aliases)
535 532 return _aliasrules.expand(aliasmap, tree)
536 533
537 534 # template engine
538 535
539 536 def unquotestring(s):
540 537 '''unwrap quotes if any; otherwise returns unmodified string'''
541 538 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
542 539 return s
543 540 return s[1:-1]
544 541
545 542 class resourcemapper(object):
546 543 """Mapper of internal template resources"""
547 544
548 545 __metaclass__ = abc.ABCMeta
549 546
550 547 @abc.abstractmethod
551 548 def availablekeys(self, context, mapping):
552 549 """Return a set of available resource keys based on the given mapping"""
553 550
554 551 @abc.abstractmethod
555 552 def knownkeys(self):
556 553 """Return a set of supported resource keys"""
557 554
558 555 @abc.abstractmethod
559 556 def lookup(self, context, mapping, key):
560 557 """Return a resource for the key if available; otherwise None"""
561 558
562 559 @abc.abstractmethod
563 560 def populatemap(self, context, origmapping, newmapping):
564 561 """Return a dict of additional mapping items which should be paired
565 562 with the given new mapping"""
566 563
567 564 class nullresourcemapper(resourcemapper):
568 565 def availablekeys(self, context, mapping):
569 566 return set()
570 567
571 568 def knownkeys(self):
572 569 return set()
573 570
574 571 def lookup(self, context, mapping, key):
575 572 return None
576 573
577 574 def populatemap(self, context, origmapping, newmapping):
578 575 return {}
579 576
580 577 class engine(object):
581 578 '''template expansion engine.
582 579
583 580 template expansion works like this. a map file contains key=value
584 581 pairs. if value is quoted, it is treated as string. otherwise, it
585 582 is treated as name of template file.
586 583
587 584 templater is asked to expand a key in map. it looks up key, and
588 585 looks for strings like this: {foo}. it expands {foo} by looking up
589 586 foo in map, and substituting it. expansion is recursive: it stops
590 587 when there is no more {foo} to replace.
591 588
592 589 expansion also allows formatting and filtering.
593 590
594 591 format uses key to expand each item in list. syntax is
595 592 {key%format}.
596 593
597 594 filter uses function to transform value. syntax is
598 595 {key|filter1|filter2|...}.'''
599 596
600 597 def __init__(self, loader, filters=None, defaults=None, resources=None,
601 598 aliases=()):
602 599 self._loader = loader
603 600 if filters is None:
604 601 filters = {}
605 602 self._filters = filters
606 603 self._funcs = templatefuncs.funcs # make this a parameter if needed
607 604 if defaults is None:
608 605 defaults = {}
609 606 if resources is None:
610 607 resources = nullresourcemapper()
611 608 self._defaults = defaults
612 609 self._resources = resources
613 610 self._aliasmap = _aliasrules.buildmap(aliases)
614 611 self._cache = {} # key: (func, data)
615 612 self._tmplcache = {} # literal template: (func, data)
616 613
617 614 def overlaymap(self, origmapping, newmapping):
618 615 """Create combined mapping from the original mapping and partial
619 616 mapping to override the original"""
620 617 # do not copy symbols which overrides the defaults depending on
621 618 # new resources, so the defaults will be re-evaluated (issue5612)
622 619 knownres = self._resources.knownkeys()
623 620 newres = self._resources.availablekeys(self, newmapping)
624 621 mapping = {k: v for k, v in origmapping.iteritems()
625 622 if (k in knownres # not a symbol per self.symbol()
626 623 or newres.isdisjoint(self._defaultrequires(k)))}
627 624 mapping.update(newmapping)
628 625 mapping.update(
629 626 self._resources.populatemap(self, origmapping, newmapping))
630 627 return mapping
631 628
632 629 def _defaultrequires(self, key):
633 630 """Resource keys required by the specified default symbol function"""
634 631 v = self._defaults.get(key)
635 632 if v is None or not callable(v):
636 633 return ()
637 634 return getattr(v, '_requires', ())
638 635
639 636 def symbol(self, mapping, key):
640 637 """Resolve symbol to value or function; None if nothing found"""
641 638 v = None
642 639 if key not in self._resources.knownkeys():
643 640 v = mapping.get(key)
644 641 if v is None:
645 642 v = self._defaults.get(key)
646 643 return v
647 644
648 645 def availableresourcekeys(self, mapping):
649 646 """Return a set of available resource keys based on the given mapping"""
650 647 return self._resources.availablekeys(self, mapping)
651 648
652 649 def knownresourcekeys(self):
653 650 """Return a set of supported resource keys"""
654 651 return self._resources.knownkeys()
655 652
656 653 def resource(self, mapping, key):
657 654 """Return internal data (e.g. cache) used for keyword/function
658 655 evaluation"""
659 656 v = self._resources.lookup(self, mapping, key)
660 657 if v is None:
661 658 raise templateutil.ResourceUnavailable(
662 659 _('template resource not available: %s') % key)
663 660 return v
664 661
665 662 def _load(self, t):
666 663 '''load, parse, and cache a template'''
667 664 if t not in self._cache:
668 665 # put poison to cut recursion while compiling 't'
669 666 self._cache[t] = (_runrecursivesymbol, t)
670 667 try:
671 668 x = parse(self._loader(t))
672 669 if self._aliasmap:
673 670 x = _aliasrules.expand(self._aliasmap, x)
674 671 self._cache[t] = compileexp(x, self, methods)
675 672 except: # re-raises
676 673 del self._cache[t]
677 674 raise
678 675 return self._cache[t]
679 676
680 677 def _parse(self, tmpl):
681 678 """Parse and cache a literal template"""
682 679 if tmpl not in self._tmplcache:
683 680 x = parse(tmpl)
684 681 self._tmplcache[tmpl] = compileexp(x, self, methods)
685 682 return self._tmplcache[tmpl]
686 683
687 684 def preload(self, t):
688 685 """Load, parse, and cache the specified template if available"""
689 686 try:
690 687 self._load(t)
691 688 return True
692 689 except templateutil.TemplateNotFound:
693 690 return False
694 691
695 692 def process(self, t, mapping):
696 693 '''Perform expansion. t is name of map element to expand.
697 694 mapping contains added elements for use during expansion. Is a
698 695 generator.'''
699 696 func, data = self._load(t)
700 697 return self._expand(func, data, mapping)
701 698
702 699 def expand(self, tmpl, mapping):
703 700 """Perform expansion over a literal template
704 701
705 702 No user aliases will be expanded since this is supposed to be called
706 703 with an internal template string.
707 704 """
708 705 func, data = self._parse(tmpl)
709 706 return self._expand(func, data, mapping)
710 707
711 708 def _expand(self, func, data, mapping):
712 709 # populate additional items only if they don't exist in the given
713 710 # mapping. this is slightly different from overlaymap() because the
714 711 # initial 'revcache' may contain pre-computed items.
715 712 extramapping = self._resources.populatemap(self, {}, mapping)
716 713 if extramapping:
717 714 extramapping.update(mapping)
718 715 mapping = extramapping
719 716 return templateutil.flatten(self, mapping, func(self, mapping, data))
720 717
721 718 engines = {'default': engine}
722 719
723 720 def stylelist():
724 721 paths = templatepaths()
725 722 if not paths:
726 723 return _('no templates found, try `hg debuginstall` for more info')
727 724 dirlist = os.listdir(paths[0])
728 725 stylelist = []
729 726 for file in dirlist:
730 727 split = file.split(".")
731 728 if split[-1] in ('orig', 'rej'):
732 729 continue
733 730 if split[0] == "map-cmdline":
734 731 stylelist.append(split[1])
735 732 return ", ".join(sorted(stylelist))
736 733
737 734 def _readmapfile(mapfile):
738 735 """Load template elements from the given map file"""
739 736 if not os.path.exists(mapfile):
740 737 raise error.Abort(_("style '%s' not found") % mapfile,
741 738 hint=_("available styles: %s") % stylelist())
742 739
743 740 base = os.path.dirname(mapfile)
744 741 conf = config.config(includepaths=templatepaths())
745 742 conf.read(mapfile, remap={'': 'templates'})
746 743
747 744 cache = {}
748 745 tmap = {}
749 746 aliases = []
750 747
751 748 val = conf.get('templates', '__base__')
752 749 if val and val[0] not in "'\"":
753 750 # treat as a pointer to a base class for this style
754 751 path = util.normpath(os.path.join(base, val))
755 752
756 753 # fallback check in template paths
757 754 if not os.path.exists(path):
758 755 for p in templatepaths():
759 756 p2 = util.normpath(os.path.join(p, val))
760 757 if os.path.isfile(p2):
761 758 path = p2
762 759 break
763 760 p3 = util.normpath(os.path.join(p2, "map"))
764 761 if os.path.isfile(p3):
765 762 path = p3
766 763 break
767 764
768 765 cache, tmap, aliases = _readmapfile(path)
769 766
770 767 for key, val in conf['templates'].items():
771 768 if not val:
772 769 raise error.ParseError(_('missing value'),
773 770 conf.source('templates', key))
774 771 if val[0] in "'\"":
775 772 if val[0] != val[-1]:
776 773 raise error.ParseError(_('unmatched quotes'),
777 774 conf.source('templates', key))
778 775 cache[key] = unquotestring(val)
779 776 elif key != '__base__':
780 777 val = 'default', val
781 778 if ':' in val[1]:
782 779 val = val[1].split(':', 1)
783 780 tmap[key] = val[0], os.path.join(base, val[1])
784 781 aliases.extend(conf['templatealias'].items())
785 782 return cache, tmap, aliases
786 783
787 784 class templater(object):
788 785
789 786 def __init__(self, filters=None, defaults=None, resources=None,
790 787 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
791 788 """Create template engine optionally with preloaded template fragments
792 789
793 790 - ``filters``: a dict of functions to transform a value into another.
794 791 - ``defaults``: a dict of symbol values/functions; may be overridden
795 792 by a ``mapping`` dict.
796 793 - ``resources``: a resourcemapper object to look up internal data
797 794 (e.g. cache), inaccessible from user template.
798 795 - ``cache``: a dict of preloaded template fragments.
799 796 - ``aliases``: a list of alias (name, replacement) pairs.
800 797
801 798 self.cache may be updated later to register additional template
802 799 fragments.
803 800 """
804 801 if filters is None:
805 802 filters = {}
806 803 if defaults is None:
807 804 defaults = {}
808 805 if cache is None:
809 806 cache = {}
810 807 self.cache = cache.copy()
811 808 self.map = {}
812 809 self.filters = templatefilters.filters.copy()
813 810 self.filters.update(filters)
814 811 self.defaults = defaults
815 812 self._resources = resources
816 813 self._aliases = aliases
817 814 self.minchunk, self.maxchunk = minchunk, maxchunk
818 815 self.ecache = {}
819 816
820 817 @classmethod
821 818 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
822 819 cache=None, minchunk=1024, maxchunk=65536):
823 820 """Create templater from the specified map file"""
824 821 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
825 822 cache, tmap, aliases = _readmapfile(mapfile)
826 823 t.cache.update(cache)
827 824 t.map = tmap
828 825 t._aliases = aliases
829 826 return t
830 827
831 828 def __contains__(self, key):
832 829 return key in self.cache or key in self.map
833 830
834 831 def load(self, t):
835 832 '''Get the template for the given template name. Use a local cache.'''
836 833 if t not in self.cache:
837 834 try:
838 835 self.cache[t] = util.readfile(self.map[t][1])
839 836 except KeyError as inst:
840 837 raise templateutil.TemplateNotFound(
841 838 _('"%s" not in template map') % inst.args[0])
842 839 except IOError as inst:
843 840 reason = (_('template file %s: %s')
844 841 % (self.map[t][1],
845 842 stringutil.forcebytestr(inst.args[1])))
846 843 raise IOError(inst.args[0], encoding.strfromlocal(reason))
847 844 return self.cache[t]
848 845
849 846 def renderdefault(self, mapping):
850 847 """Render the default unnamed template and return result as string"""
851 848 return self.render('', mapping)
852 849
853 850 def render(self, t, mapping):
854 851 """Render the specified named template and return result as string"""
855 852 return b''.join(self.generate(t, mapping))
856 853
857 854 def generate(self, t, mapping):
858 855 """Return a generator that renders the specified named template and
859 856 yields chunks"""
860 857 ttype = t in self.map and self.map[t][0] or 'default'
861 858 if ttype not in self.ecache:
862 859 try:
863 860 ecls = engines[ttype]
864 861 except KeyError:
865 862 raise error.Abort(_('invalid template engine: %s') % ttype)
866 863 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
867 864 self._resources, self._aliases)
868 865 proc = self.ecache[ttype]
869 866
870 867 stream = proc.process(t, mapping)
871 868 if self.minchunk:
872 869 stream = util.increasingchunks(stream, min=self.minchunk,
873 870 max=self.maxchunk)
874 871 return stream
875 872
876 873 def templatepaths():
877 874 '''return locations used for template files.'''
878 875 pathsrel = ['templates']
879 876 paths = [os.path.normpath(os.path.join(util.datapath, f))
880 877 for f in pathsrel]
881 878 return [p for p in paths if os.path.isdir(p)]
882 879
883 880 def templatepath(name):
884 881 '''return location of template file. returns None if not found.'''
885 882 for p in templatepaths():
886 883 f = os.path.join(p, name)
887 884 if os.path.exists(f):
888 885 return f
889 886 return None
890 887
891 888 def stylemap(styles, paths=None):
892 889 """Return path to mapfile for a given style.
893 890
894 891 Searches mapfile in the following locations:
895 892 1. templatepath/style/map
896 893 2. templatepath/map-style
897 894 3. templatepath/map
898 895 """
899 896
900 897 if paths is None:
901 898 paths = templatepaths()
902 899 elif isinstance(paths, bytes):
903 900 paths = [paths]
904 901
905 902 if isinstance(styles, bytes):
906 903 styles = [styles]
907 904
908 905 for style in styles:
909 906 # only plain name is allowed to honor template paths
910 907 if (not style
911 908 or style in (pycompat.oscurdir, pycompat.ospardir)
912 909 or pycompat.ossep in style
913 910 or pycompat.osaltsep and pycompat.osaltsep in style):
914 911 continue
915 912 locations = [os.path.join(style, 'map'), 'map-' + style]
916 913 locations.append('map')
917 914
918 915 for path in paths:
919 916 for location in locations:
920 917 mapfile = os.path.join(path, location)
921 918 if os.path.isfile(mapfile):
922 919 return style, mapfile
923 920
924 921 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,691 +1,683 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 Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain the inner
35 35 object.
36 36 """
37 37
38 38 __metaclass__ = abc.ABCMeta
39 39
40 40 @abc.abstractmethod
41 41 def itermaps(self, context):
42 42 """Yield each template mapping"""
43 43
44 44 @abc.abstractmethod
45 45 def join(self, context, mapping, sep):
46 46 """Join items with the separator; Returns a bytes or (possibly nested)
47 47 generator of bytes
48 48
49 49 A pre-configured template may be rendered per item if this container
50 50 holds unprintable items.
51 51 """
52 52
53 53 @abc.abstractmethod
54 54 def show(self, context, mapping):
55 55 """Return a bytes or (possibly nested) generator of bytes representing
56 56 the underlying object
57 57
58 58 A pre-configured template may be rendered if the underlying object is
59 59 not printable.
60 60 """
61 61
62 62 @abc.abstractmethod
63 63 def tovalue(self, context, mapping):
64 64 """Move the inner value object out or create a value representation
65 65
66 66 A returned value must be serializable by templaterfilters.json().
67 67 """
68 68
69 69 # stub for representing a date type; may be a real date type that can
70 70 # provide a readable string value
71 71 class date(object):
72 72 pass
73 73
74 74 class hybrid(wrapped):
75 75 """Wrapper for list or dict to support legacy template
76 76
77 77 This class allows us to handle both:
78 78 - "{files}" (legacy command-line-specific list hack) and
79 79 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
80 80 and to access raw values:
81 81 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
82 82 - "{get(extras, key)}"
83 83 - "{files|json}"
84 84 """
85 85
86 86 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
87 87 self._gen = gen # generator or function returning generator
88 88 self._values = values
89 89 self._makemap = makemap
90 90 self._joinfmt = joinfmt
91 91 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
92 92
93 93 def itermaps(self, context):
94 94 makemap = self._makemap
95 95 for x in self._values:
96 96 yield makemap(x)
97 97
98 98 def join(self, context, mapping, sep):
99 99 # TODO: switch gen to (context, mapping) API?
100 100 return joinitems((self._joinfmt(x) for x in self._values), sep)
101 101
102 102 def show(self, context, mapping):
103 103 # TODO: switch gen to (context, mapping) API?
104 104 gen = self._gen
105 105 if gen is None:
106 106 return self.join(context, mapping, ' ')
107 107 if callable(gen):
108 108 return gen()
109 109 return gen
110 110
111 111 def tovalue(self, context, mapping):
112 112 # TODO: return self._values and get rid of proxy methods
113 113 return self
114 114
115 115 def __contains__(self, x):
116 116 return x in self._values
117 117 def __getitem__(self, key):
118 118 return self._values[key]
119 119 def __len__(self):
120 120 return len(self._values)
121 121 def __iter__(self):
122 122 return iter(self._values)
123 123 def __getattr__(self, name):
124 124 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
125 125 r'itervalues', r'keys', r'values'):
126 126 raise AttributeError(name)
127 127 return getattr(self._values, name)
128 128
129 129 class mappable(wrapped):
130 130 """Wrapper for non-list/dict object to support map operation
131 131
132 132 This class allows us to handle both:
133 133 - "{manifest}"
134 134 - "{manifest % '{rev}:{node}'}"
135 135 - "{manifest.rev}"
136 136
137 137 Unlike a hybrid, this does not simulate the behavior of the underling
138 138 value.
139 139 """
140 140
141 141 def __init__(self, gen, key, value, makemap):
142 142 self._gen = gen # generator or function returning generator
143 143 self._key = key
144 144 self._value = value # may be generator of strings
145 145 self._makemap = makemap
146 146
147 147 def tomap(self):
148 148 return self._makemap(self._key)
149 149
150 150 def itermaps(self, context):
151 151 yield self.tomap()
152 152
153 153 def join(self, context, mapping, sep):
154 154 # TODO: just copies the old behavior where a value was a generator
155 155 # yielding one item, but reconsider about it. join() over a string
156 156 # has no consistent result because a string may be a bytes, or a
157 157 # generator yielding an item, or a generator yielding multiple items.
158 158 # Preserving all of the current behaviors wouldn't make any sense.
159 159 return self.show(context, mapping)
160 160
161 161 def show(self, context, mapping):
162 162 # TODO: switch gen to (context, mapping) API?
163 163 gen = self._gen
164 164 if gen is None:
165 165 return pycompat.bytestr(self._value)
166 166 if callable(gen):
167 167 return gen()
168 168 return gen
169 169
170 170 def tovalue(self, context, mapping):
171 171 return _unthunk(context, mapping, self._value)
172 172
173 173 class _mappingsequence(wrapped):
174 174 """Wrapper for sequence of template mappings
175 175
176 176 This represents an inner template structure (i.e. a list of dicts),
177 177 which can also be rendered by the specified named/literal template.
178 178
179 179 Template mappings may be nested.
180 180 """
181 181
182 182 def __init__(self, name=None, tmpl=None, sep=''):
183 183 if name is not None and tmpl is not None:
184 184 raise error.ProgrammingError('name and tmpl are mutually exclusive')
185 185 self._name = name
186 186 self._tmpl = tmpl
187 187 self._defaultsep = sep
188 188
189 189 def join(self, context, mapping, sep):
190 190 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
191 191 if self._name:
192 192 itemiter = (context.process(self._name, m) for m in mapsiter)
193 193 elif self._tmpl:
194 194 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
195 195 else:
196 196 raise error.ParseError(_('not displayable without template'))
197 197 return joinitems(itemiter, sep)
198 198
199 199 def show(self, context, mapping):
200 200 return self.join(context, mapping, self._defaultsep)
201 201
202 202 def tovalue(self, context, mapping):
203 203 knownres = context.knownresourcekeys()
204 204 items = []
205 205 for nm in self.itermaps(context):
206 206 # drop internal resources (recursively) which shouldn't be displayed
207 207 lm = context.overlaymap(mapping, nm)
208 208 items.append({k: unwrapvalue(context, lm, v)
209 209 for k, v in nm.iteritems() if k not in knownres})
210 210 return items
211 211
212 212 class mappinggenerator(_mappingsequence):
213 213 """Wrapper for generator of template mappings
214 214
215 215 The function ``make(context, *args)`` should return a generator of
216 216 mapping dicts.
217 217 """
218 218
219 219 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
220 220 super(mappinggenerator, self).__init__(name, tmpl, sep)
221 221 self._make = make
222 222 self._args = args
223 223
224 224 def itermaps(self, context):
225 225 return self._make(context, *self._args)
226 226
227 227 class mappinglist(_mappingsequence):
228 228 """Wrapper for list of template mappings"""
229 229
230 230 def __init__(self, mappings, name=None, tmpl=None, sep=''):
231 231 super(mappinglist, self).__init__(name, tmpl, sep)
232 232 self._mappings = mappings
233 233
234 234 def itermaps(self, context):
235 235 return iter(self._mappings)
236 236
237 237 class mappedgenerator(wrapped):
238 238 """Wrapper for generator of strings which acts as a list
239 239
240 240 The function ``make(context, *args)`` should return a generator of
241 241 byte strings, or a generator of (possibly nested) generators of byte
242 242 strings (i.e. a generator for a list of byte strings.)
243 243 """
244 244
245 245 def __init__(self, make, args=()):
246 246 self._make = make
247 247 self._args = args
248 248
249 249 def _gen(self, context):
250 250 return self._make(context, *self._args)
251 251
252 252 def itermaps(self, context):
253 253 raise error.ParseError(_('list of strings is not mappable'))
254 254
255 255 def join(self, context, mapping, sep):
256 256 return joinitems(self._gen(context), sep)
257 257
258 258 def show(self, context, mapping):
259 259 return self.join(context, mapping, '')
260 260
261 261 def tovalue(self, context, mapping):
262 262 return [stringify(context, mapping, x) for x in self._gen(context)]
263 263
264 264 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
265 265 """Wrap data to support both dict-like and string-like operations"""
266 266 prefmt = pycompat.identity
267 267 if fmt is None:
268 268 fmt = '%s=%s'
269 269 prefmt = pycompat.bytestr
270 270 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
271 271 lambda k: fmt % (prefmt(k), prefmt(data[k])))
272 272
273 273 def hybridlist(data, name, fmt=None, gen=None):
274 274 """Wrap data to support both list-like and string-like operations"""
275 275 prefmt = pycompat.identity
276 276 if fmt is None:
277 277 fmt = '%s'
278 278 prefmt = pycompat.bytestr
279 279 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
280 280
281 281 def unwraphybrid(context, mapping, thing):
282 282 """Return an object which can be stringified possibly by using a legacy
283 283 template"""
284 284 if not isinstance(thing, wrapped):
285 285 return thing
286 286 return thing.show(context, mapping)
287 287
288 def unwrapvalue(context, mapping, thing):
289 """Move the inner value object out of the wrapper"""
290 if not isinstance(thing, wrapped):
291 return thing
292 return thing.tovalue(context, mapping)
293
294 288 def wraphybridvalue(container, key, value):
295 289 """Wrap an element of hybrid container to be mappable
296 290
297 291 The key is passed to the makemap function of the given container, which
298 292 should be an item generated by iter(container).
299 293 """
300 294 makemap = getattr(container, '_makemap', None)
301 295 if makemap is None:
302 296 return value
303 297 if util.safehasattr(value, '_makemap'):
304 298 # a nested hybrid list/dict, which has its own way of map operation
305 299 return value
306 300 return mappable(None, key, value, makemap)
307 301
308 302 def compatdict(context, mapping, name, data, key='key', value='value',
309 303 fmt=None, plural=None, separator=' '):
310 304 """Wrap data like hybriddict(), but also supports old-style list template
311 305
312 306 This exists for backward compatibility with the old-style template. Use
313 307 hybriddict() for new template keywords.
314 308 """
315 309 c = [{key: k, value: v} for k, v in data.iteritems()]
316 310 f = _showcompatlist(context, mapping, name, c, plural, separator)
317 311 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
318 312
319 313 def compatlist(context, mapping, name, data, element=None, fmt=None,
320 314 plural=None, separator=' '):
321 315 """Wrap data like hybridlist(), but also supports old-style list template
322 316
323 317 This exists for backward compatibility with the old-style template. Use
324 318 hybridlist() for new template keywords.
325 319 """
326 320 f = _showcompatlist(context, mapping, name, data, plural, separator)
327 321 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
328 322
329 323 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
330 324 """Return a generator that renders old-style list template
331 325
332 326 name is name of key in template map.
333 327 values is list of strings or dicts.
334 328 plural is plural of name, if not simply name + 's'.
335 329 separator is used to join values as a string
336 330
337 331 expansion works like this, given name 'foo'.
338 332
339 333 if values is empty, expand 'no_foos'.
340 334
341 335 if 'foo' not in template map, return values as a string,
342 336 joined by 'separator'.
343 337
344 338 expand 'start_foos'.
345 339
346 340 for each value, expand 'foo'. if 'last_foo' in template
347 341 map, expand it instead of 'foo' for last key.
348 342
349 343 expand 'end_foos'.
350 344 """
351 345 if not plural:
352 346 plural = name + 's'
353 347 if not values:
354 348 noname = 'no_' + plural
355 349 if context.preload(noname):
356 350 yield context.process(noname, mapping)
357 351 return
358 352 if not context.preload(name):
359 353 if isinstance(values[0], bytes):
360 354 yield separator.join(values)
361 355 else:
362 356 for v in values:
363 357 r = dict(v)
364 358 r.update(mapping)
365 359 yield r
366 360 return
367 361 startname = 'start_' + plural
368 362 if context.preload(startname):
369 363 yield context.process(startname, mapping)
370 364 def one(v, tag=name):
371 365 vmapping = {}
372 366 try:
373 367 vmapping.update(v)
374 368 # Python 2 raises ValueError if the type of v is wrong. Python
375 369 # 3 raises TypeError.
376 370 except (AttributeError, TypeError, ValueError):
377 371 try:
378 372 # Python 2 raises ValueError trying to destructure an e.g.
379 373 # bytes. Python 3 raises TypeError.
380 374 for a, b in v:
381 375 vmapping[a] = b
382 376 except (TypeError, ValueError):
383 377 vmapping[name] = v
384 378 vmapping = context.overlaymap(mapping, vmapping)
385 379 return context.process(tag, vmapping)
386 380 lastname = 'last_' + name
387 381 if context.preload(lastname):
388 382 last = values.pop()
389 383 else:
390 384 last = None
391 385 for v in values:
392 386 yield one(v)
393 387 if last is not None:
394 388 yield one(last, tag=lastname)
395 389 endname = 'end_' + plural
396 390 if context.preload(endname):
397 391 yield context.process(endname, mapping)
398 392
399 393 def flatten(context, mapping, thing):
400 394 """Yield a single stream from a possibly nested set of iterators"""
401 395 thing = unwraphybrid(context, mapping, thing)
402 396 if isinstance(thing, bytes):
403 397 yield thing
404 398 elif isinstance(thing, str):
405 399 # We can only hit this on Python 3, and it's here to guard
406 400 # against infinite recursion.
407 401 raise error.ProgrammingError('Mercurial IO including templates is done'
408 402 ' with bytes, not strings, got %r' % thing)
409 403 elif thing is None:
410 404 pass
411 405 elif not util.safehasattr(thing, '__iter__'):
412 406 yield pycompat.bytestr(thing)
413 407 else:
414 408 for i in thing:
415 409 i = unwraphybrid(context, mapping, i)
416 410 if isinstance(i, bytes):
417 411 yield i
418 412 elif i is None:
419 413 pass
420 414 elif not util.safehasattr(i, '__iter__'):
421 415 yield pycompat.bytestr(i)
422 416 else:
423 417 for j in flatten(context, mapping, i):
424 418 yield j
425 419
426 420 def stringify(context, mapping, thing):
427 421 """Turn values into bytes by converting into text and concatenating them"""
428 422 if isinstance(thing, bytes):
429 423 return thing # retain localstr to be round-tripped
430 424 return b''.join(flatten(context, mapping, thing))
431 425
432 426 def findsymbolicname(arg):
433 427 """Find symbolic name for the given compiled expression; returns None
434 428 if nothing found reliably"""
435 429 while True:
436 430 func, data = arg
437 431 if func is runsymbol:
438 432 return data
439 433 elif func is runfilter:
440 434 arg = data[0]
441 435 else:
442 436 return None
443 437
444 438 def _unthunk(context, mapping, thing):
445 439 """Evaluate a lazy byte string into value"""
446 440 if not isinstance(thing, types.GeneratorType):
447 441 return thing
448 442 return stringify(context, mapping, thing)
449 443
450 444 def evalrawexp(context, mapping, arg):
451 445 """Evaluate given argument as a bare template object which may require
452 446 further processing (such as folding generator of strings)"""
453 447 func, data = arg
454 448 return func(context, mapping, data)
455 449
456 450 def evalfuncarg(context, mapping, arg):
457 451 """Evaluate given argument as value type"""
458 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
452 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
459 453
460 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
461 # is fixed. we can't do that right now because join() has to take a generator
462 # of byte strings as it is, not a lazy byte string.
463 def _unwrapvalue(context, mapping, thing):
454 def unwrapvalue(context, mapping, thing):
455 """Move the inner value object out of the wrapper"""
464 456 if isinstance(thing, wrapped):
465 457 return thing.tovalue(context, mapping)
466 458 # evalrawexp() may return string, generator of strings or arbitrary object
467 459 # such as date tuple, but filter does not want generator.
468 460 return _unthunk(context, mapping, thing)
469 461
470 462 def evalboolean(context, mapping, arg):
471 463 """Evaluate given argument as boolean, but also takes boolean literals"""
472 464 func, data = arg
473 465 if func is runsymbol:
474 466 thing = func(context, mapping, data, default=None)
475 467 if thing is None:
476 468 # not a template keyword, takes as a boolean literal
477 469 thing = stringutil.parsebool(data)
478 470 else:
479 471 thing = func(context, mapping, data)
480 472 if isinstance(thing, wrapped):
481 473 thing = thing.tovalue(context, mapping)
482 474 if isinstance(thing, bool):
483 475 return thing
484 476 # other objects are evaluated as strings, which means 0 is True, but
485 477 # empty dict/list should be False as they are expected to be ''
486 478 return bool(stringify(context, mapping, thing))
487 479
488 480 def evaldate(context, mapping, arg, err=None):
489 481 """Evaluate given argument as a date tuple or a date string; returns
490 482 a (unixtime, offset) tuple"""
491 483 thing = evalrawexp(context, mapping, arg)
492 484 return unwrapdate(context, mapping, thing, err)
493 485
494 486 def unwrapdate(context, mapping, thing, err=None):
495 thing = _unwrapvalue(context, mapping, thing)
487 thing = unwrapvalue(context, mapping, thing)
496 488 try:
497 489 return dateutil.parsedate(thing)
498 490 except AttributeError:
499 491 raise error.ParseError(err or _('not a date tuple nor a string'))
500 492 except error.ParseError:
501 493 if not err:
502 494 raise
503 495 raise error.ParseError(err)
504 496
505 497 def evalinteger(context, mapping, arg, err=None):
506 498 thing = evalrawexp(context, mapping, arg)
507 499 return unwrapinteger(context, mapping, thing, err)
508 500
509 501 def unwrapinteger(context, mapping, thing, err=None):
510 thing = _unwrapvalue(context, mapping, thing)
502 thing = unwrapvalue(context, mapping, thing)
511 503 try:
512 504 return int(thing)
513 505 except (TypeError, ValueError):
514 506 raise error.ParseError(err or _('not an integer'))
515 507
516 508 def evalstring(context, mapping, arg):
517 509 return stringify(context, mapping, evalrawexp(context, mapping, arg))
518 510
519 511 def evalstringliteral(context, mapping, arg):
520 512 """Evaluate given argument as string template, but returns symbol name
521 513 if it is unknown"""
522 514 func, data = arg
523 515 if func is runsymbol:
524 516 thing = func(context, mapping, data, default=data)
525 517 else:
526 518 thing = func(context, mapping, data)
527 519 return stringify(context, mapping, thing)
528 520
529 521 _unwrapfuncbytype = {
530 None: _unwrapvalue,
522 None: unwrapvalue,
531 523 bytes: stringify,
532 524 date: unwrapdate,
533 525 int: unwrapinteger,
534 526 }
535 527
536 528 def unwrapastype(context, mapping, thing, typ):
537 529 """Move the inner value object out of the wrapper and coerce its type"""
538 530 try:
539 531 f = _unwrapfuncbytype[typ]
540 532 except KeyError:
541 533 raise error.ProgrammingError('invalid type specified: %r' % typ)
542 534 return f(context, mapping, thing)
543 535
544 536 def runinteger(context, mapping, data):
545 537 return int(data)
546 538
547 539 def runstring(context, mapping, data):
548 540 return data
549 541
550 542 def _recursivesymbolblocker(key):
551 543 def showrecursion(**args):
552 544 raise error.Abort(_("recursive reference '%s' in template") % key)
553 545 return showrecursion
554 546
555 547 def runsymbol(context, mapping, key, default=''):
556 548 v = context.symbol(mapping, key)
557 549 if v is None:
558 550 # put poison to cut recursion. we can't move this to parsing phase
559 551 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
560 552 safemapping = mapping.copy()
561 553 safemapping[key] = _recursivesymbolblocker(key)
562 554 try:
563 555 v = context.process(key, safemapping)
564 556 except TemplateNotFound:
565 557 v = default
566 558 if callable(v) and getattr(v, '_requires', None) is None:
567 559 # old templatekw: expand all keywords and resources
568 560 # (TODO: deprecate this after porting web template keywords to new API)
569 561 props = {k: context._resources.lookup(context, mapping, k)
570 562 for k in context._resources.knownkeys()}
571 563 # pass context to _showcompatlist() through templatekw._showlist()
572 564 props['templ'] = context
573 565 props.update(mapping)
574 566 return v(**pycompat.strkwargs(props))
575 567 if callable(v):
576 568 # new templatekw
577 569 try:
578 570 return v(context, mapping)
579 571 except ResourceUnavailable:
580 572 # unsupported keyword is mapped to empty just like unknown keyword
581 573 return None
582 574 return v
583 575
584 576 def runtemplate(context, mapping, template):
585 577 for arg in template:
586 578 yield evalrawexp(context, mapping, arg)
587 579
588 580 def runfilter(context, mapping, data):
589 581 arg, filt = data
590 582 thing = evalrawexp(context, mapping, arg)
591 583 intype = getattr(filt, '_intype', None)
592 584 try:
593 585 thing = unwrapastype(context, mapping, thing, intype)
594 586 return filt(thing)
595 587 except error.ParseError as e:
596 588 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
597 589
598 590 def _formatfiltererror(arg, filt):
599 591 fn = pycompat.sysbytes(filt.__name__)
600 592 sym = findsymbolicname(arg)
601 593 if not sym:
602 594 return _("incompatible use of template filter '%s'") % fn
603 595 return (_("template filter '%s' is not compatible with keyword '%s'")
604 596 % (fn, sym))
605 597
606 598 def _checkeditermaps(darg, d):
607 599 try:
608 600 for v in d:
609 601 if not isinstance(v, dict):
610 602 raise TypeError
611 603 yield v
612 604 except TypeError:
613 605 sym = findsymbolicname(darg)
614 606 if sym:
615 607 raise error.ParseError(_("keyword '%s' is not iterable of mappings")
616 608 % sym)
617 609 else:
618 610 raise error.ParseError(_("%r is not iterable of mappings") % d)
619 611
620 612 def _iteroverlaymaps(context, origmapping, newmappings):
621 613 """Generate combined mappings from the original mapping and an iterable
622 614 of partial mappings to override the original"""
623 615 for i, nm in enumerate(newmappings):
624 616 lm = context.overlaymap(origmapping, nm)
625 617 lm['index'] = i
626 618 yield lm
627 619
628 620 def _applymap(context, mapping, diter, targ):
629 621 for lm in _iteroverlaymaps(context, mapping, diter):
630 622 yield evalrawexp(context, lm, targ)
631 623
632 624 def runmap(context, mapping, data):
633 625 darg, targ = data
634 626 d = evalrawexp(context, mapping, darg)
635 627 # TODO: a generator should be rejected because it is a thunk of lazy
636 628 # string, but we can't because hgweb abuses generator as a keyword
637 629 # that returns a list of dicts.
638 630 # TODO: drop _checkeditermaps() and pass 'd' to mappedgenerator so it
639 631 # can be restarted.
640 632 if isinstance(d, wrapped):
641 633 diter = d.itermaps(context)
642 634 else:
643 635 diter = _checkeditermaps(darg, d)
644 636 return mappedgenerator(_applymap, args=(mapping, diter, targ))
645 637
646 638 def runmember(context, mapping, data):
647 639 darg, memb = data
648 640 d = evalrawexp(context, mapping, darg)
649 641 if util.safehasattr(d, 'tomap'):
650 642 lm = context.overlaymap(mapping, d.tomap())
651 643 return runsymbol(context, lm, memb)
652 644 if util.safehasattr(d, 'get'):
653 645 return getdictitem(d, memb)
654 646
655 647 sym = findsymbolicname(darg)
656 648 if sym:
657 649 raise error.ParseError(_("keyword '%s' has no member") % sym)
658 650 else:
659 651 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
660 652
661 653 def runnegate(context, mapping, data):
662 654 data = evalinteger(context, mapping, data,
663 655 _('negation needs an integer argument'))
664 656 return -data
665 657
666 658 def runarithmetic(context, mapping, data):
667 659 func, left, right = data
668 660 left = evalinteger(context, mapping, left,
669 661 _('arithmetic only defined on integers'))
670 662 right = evalinteger(context, mapping, right,
671 663 _('arithmetic only defined on integers'))
672 664 try:
673 665 return func(left, right)
674 666 except ZeroDivisionError:
675 667 raise error.Abort(_('division by zero is not defined'))
676 668
677 669 def getdictitem(dictarg, key):
678 670 val = dictarg.get(key)
679 671 if val is None:
680 672 return
681 673 return wraphybridvalue(dictarg, key, val)
682 674
683 675 def joinitems(itemiter, sep):
684 676 """Join items with the separator; Returns generator of bytes"""
685 677 first = True
686 678 for x in itemiter:
687 679 if first:
688 680 first = False
689 681 elif sep:
690 682 yield sep
691 683 yield x
General Comments 0
You need to be logged in to leave comments. Login now