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