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