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