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