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