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