##// END OF EJS Templates
templater: move getdictitem() to hybrid class...
Yuya Nishihara -
r38260:12b6ee9e 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 = evalwrapped(context, mapping, args[0])
264 dictarg = evalwrapped(context, mapping, args[0])
265 if not util.safehasattr(dictarg, 'get'):
265 if not util.safehasattr(dictarg, 'getmember'):
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 dictarg.getmember(context, mapping, 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,710 +1,715 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 getmember(self, context, mapping, key):
132 # TODO: maybe split hybrid list/dict types?
133 if not util.safehasattr(self._values, 'get'):
134 raise error.ParseError(_('not a dictionary'))
135 return self._wrapvalue(key, self._values.get(key))
136
137 def _wrapvalue(self, key, val):
138 if val is None:
139 return
140 return wraphybridvalue(self, key, val)
141
131 def itermaps(self, context):
142 def itermaps(self, context):
132 makemap = self._makemap
143 makemap = self._makemap
133 for x in self._values:
144 for x in self._values:
134 yield makemap(x)
145 yield makemap(x)
135
146
136 def join(self, context, mapping, sep):
147 def join(self, context, mapping, sep):
137 # TODO: switch gen to (context, mapping) API?
148 # TODO: switch gen to (context, mapping) API?
138 return joinitems((self._joinfmt(x) for x in self._values), sep)
149 return joinitems((self._joinfmt(x) for x in self._values), sep)
139
150
140 def show(self, context, mapping):
151 def show(self, context, mapping):
141 # TODO: switch gen to (context, mapping) API?
152 # TODO: switch gen to (context, mapping) API?
142 gen = self._gen
153 gen = self._gen
143 if gen is None:
154 if gen is None:
144 return self.join(context, mapping, ' ')
155 return self.join(context, mapping, ' ')
145 if callable(gen):
156 if callable(gen):
146 return gen()
157 return gen()
147 return gen
158 return gen
148
159
149 def tovalue(self, context, mapping):
160 def tovalue(self, context, mapping):
150 # TODO: return self._values and get rid of proxy methods
161 # TODO: return self._values and get rid of proxy methods
151 return self
162 return self
152
163
153 def __contains__(self, x):
164 def __contains__(self, x):
154 return x in self._values
165 return x in self._values
155 def __getitem__(self, key):
166 def __getitem__(self, key):
156 return self._values[key]
167 return self._values[key]
157 def __len__(self):
168 def __len__(self):
158 return len(self._values)
169 return len(self._values)
159 def __iter__(self):
170 def __iter__(self):
160 return iter(self._values)
171 return iter(self._values)
161 def __getattr__(self, name):
172 def __getattr__(self, name):
162 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
173 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
163 r'itervalues', r'keys', r'values'):
174 r'itervalues', r'keys', r'values'):
164 raise AttributeError(name)
175 raise AttributeError(name)
165 return getattr(self._values, name)
176 return getattr(self._values, name)
166
177
167 class mappable(wrapped):
178 class mappable(wrapped):
168 """Wrapper for non-list/dict object to support map operation
179 """Wrapper for non-list/dict object to support map operation
169
180
170 This class allows us to handle both:
181 This class allows us to handle both:
171 - "{manifest}"
182 - "{manifest}"
172 - "{manifest % '{rev}:{node}'}"
183 - "{manifest % '{rev}:{node}'}"
173 - "{manifest.rev}"
184 - "{manifest.rev}"
174
185
175 Unlike a hybrid, this does not simulate the behavior of the underling
186 Unlike a hybrid, this does not simulate the behavior of the underling
176 value.
187 value.
177 """
188 """
178
189
179 def __init__(self, gen, key, value, makemap):
190 def __init__(self, gen, key, value, makemap):
180 self._gen = gen # generator or function returning generator
191 self._gen = gen # generator or function returning generator
181 self._key = key
192 self._key = key
182 self._value = value # may be generator of strings
193 self._value = value # may be generator of strings
183 self._makemap = makemap
194 self._makemap = makemap
184
195
185 def tomap(self):
196 def tomap(self):
186 return self._makemap(self._key)
197 return self._makemap(self._key)
187
198
188 def itermaps(self, context):
199 def itermaps(self, context):
189 yield self.tomap()
200 yield self.tomap()
190
201
191 def join(self, context, mapping, sep):
202 def join(self, context, mapping, sep):
192 w = makewrapped(context, mapping, self._value)
203 w = makewrapped(context, mapping, self._value)
193 return w.join(context, mapping, sep)
204 return w.join(context, mapping, sep)
194
205
195 def show(self, context, mapping):
206 def show(self, context, mapping):
196 # TODO: switch gen to (context, mapping) API?
207 # TODO: switch gen to (context, mapping) API?
197 gen = self._gen
208 gen = self._gen
198 if gen is None:
209 if gen is None:
199 return pycompat.bytestr(self._value)
210 return pycompat.bytestr(self._value)
200 if callable(gen):
211 if callable(gen):
201 return gen()
212 return gen()
202 return gen
213 return gen
203
214
204 def tovalue(self, context, mapping):
215 def tovalue(self, context, mapping):
205 return _unthunk(context, mapping, self._value)
216 return _unthunk(context, mapping, self._value)
206
217
207 class _mappingsequence(wrapped):
218 class _mappingsequence(wrapped):
208 """Wrapper for sequence of template mappings
219 """Wrapper for sequence of template mappings
209
220
210 This represents an inner template structure (i.e. a list of dicts),
221 This represents an inner template structure (i.e. a list of dicts),
211 which can also be rendered by the specified named/literal template.
222 which can also be rendered by the specified named/literal template.
212
223
213 Template mappings may be nested.
224 Template mappings may be nested.
214 """
225 """
215
226
216 def __init__(self, name=None, tmpl=None, sep=''):
227 def __init__(self, name=None, tmpl=None, sep=''):
217 if name is not None and tmpl is not None:
228 if name is not None and tmpl is not None:
218 raise error.ProgrammingError('name and tmpl are mutually exclusive')
229 raise error.ProgrammingError('name and tmpl are mutually exclusive')
219 self._name = name
230 self._name = name
220 self._tmpl = tmpl
231 self._tmpl = tmpl
221 self._defaultsep = sep
232 self._defaultsep = sep
222
233
223 def join(self, context, mapping, sep):
234 def join(self, context, mapping, sep):
224 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
235 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
225 if self._name:
236 if self._name:
226 itemiter = (context.process(self._name, m) for m in mapsiter)
237 itemiter = (context.process(self._name, m) for m in mapsiter)
227 elif self._tmpl:
238 elif self._tmpl:
228 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
239 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
229 else:
240 else:
230 raise error.ParseError(_('not displayable without template'))
241 raise error.ParseError(_('not displayable without template'))
231 return joinitems(itemiter, sep)
242 return joinitems(itemiter, sep)
232
243
233 def show(self, context, mapping):
244 def show(self, context, mapping):
234 return self.join(context, mapping, self._defaultsep)
245 return self.join(context, mapping, self._defaultsep)
235
246
236 def tovalue(self, context, mapping):
247 def tovalue(self, context, mapping):
237 knownres = context.knownresourcekeys()
248 knownres = context.knownresourcekeys()
238 items = []
249 items = []
239 for nm in self.itermaps(context):
250 for nm in self.itermaps(context):
240 # drop internal resources (recursively) which shouldn't be displayed
251 # drop internal resources (recursively) which shouldn't be displayed
241 lm = context.overlaymap(mapping, nm)
252 lm = context.overlaymap(mapping, nm)
242 items.append({k: unwrapvalue(context, lm, v)
253 items.append({k: unwrapvalue(context, lm, v)
243 for k, v in nm.iteritems() if k not in knownres})
254 for k, v in nm.iteritems() if k not in knownres})
244 return items
255 return items
245
256
246 class mappinggenerator(_mappingsequence):
257 class mappinggenerator(_mappingsequence):
247 """Wrapper for generator of template mappings
258 """Wrapper for generator of template mappings
248
259
249 The function ``make(context, *args)`` should return a generator of
260 The function ``make(context, *args)`` should return a generator of
250 mapping dicts.
261 mapping dicts.
251 """
262 """
252
263
253 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
264 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
254 super(mappinggenerator, self).__init__(name, tmpl, sep)
265 super(mappinggenerator, self).__init__(name, tmpl, sep)
255 self._make = make
266 self._make = make
256 self._args = args
267 self._args = args
257
268
258 def itermaps(self, context):
269 def itermaps(self, context):
259 return self._make(context, *self._args)
270 return self._make(context, *self._args)
260
271
261 class mappinglist(_mappingsequence):
272 class mappinglist(_mappingsequence):
262 """Wrapper for list of template mappings"""
273 """Wrapper for list of template mappings"""
263
274
264 def __init__(self, mappings, name=None, tmpl=None, sep=''):
275 def __init__(self, mappings, name=None, tmpl=None, sep=''):
265 super(mappinglist, self).__init__(name, tmpl, sep)
276 super(mappinglist, self).__init__(name, tmpl, sep)
266 self._mappings = mappings
277 self._mappings = mappings
267
278
268 def itermaps(self, context):
279 def itermaps(self, context):
269 return iter(self._mappings)
280 return iter(self._mappings)
270
281
271 class mappedgenerator(wrapped):
282 class mappedgenerator(wrapped):
272 """Wrapper for generator of strings which acts as a list
283 """Wrapper for generator of strings which acts as a list
273
284
274 The function ``make(context, *args)`` should return a generator of
285 The function ``make(context, *args)`` should return a generator of
275 byte strings, or a generator of (possibly nested) generators of byte
286 byte strings, or a generator of (possibly nested) generators of byte
276 strings (i.e. a generator for a list of byte strings.)
287 strings (i.e. a generator for a list of byte strings.)
277 """
288 """
278
289
279 def __init__(self, make, args=()):
290 def __init__(self, make, args=()):
280 self._make = make
291 self._make = make
281 self._args = args
292 self._args = args
282
293
283 def _gen(self, context):
294 def _gen(self, context):
284 return self._make(context, *self._args)
295 return self._make(context, *self._args)
285
296
286 def itermaps(self, context):
297 def itermaps(self, context):
287 raise error.ParseError(_('list of strings is not mappable'))
298 raise error.ParseError(_('list of strings is not mappable'))
288
299
289 def join(self, context, mapping, sep):
300 def join(self, context, mapping, sep):
290 return joinitems(self._gen(context), sep)
301 return joinitems(self._gen(context), sep)
291
302
292 def show(self, context, mapping):
303 def show(self, context, mapping):
293 return self.join(context, mapping, '')
304 return self.join(context, mapping, '')
294
305
295 def tovalue(self, context, mapping):
306 def tovalue(self, context, mapping):
296 return [stringify(context, mapping, x) for x in self._gen(context)]
307 return [stringify(context, mapping, x) for x in self._gen(context)]
297
308
298 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
309 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
299 """Wrap data to support both dict-like and string-like operations"""
310 """Wrap data to support both dict-like and string-like operations"""
300 prefmt = pycompat.identity
311 prefmt = pycompat.identity
301 if fmt is None:
312 if fmt is None:
302 fmt = '%s=%s'
313 fmt = '%s=%s'
303 prefmt = pycompat.bytestr
314 prefmt = pycompat.bytestr
304 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
315 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
305 lambda k: fmt % (prefmt(k), prefmt(data[k])))
316 lambda k: fmt % (prefmt(k), prefmt(data[k])))
306
317
307 def hybridlist(data, name, fmt=None, gen=None):
318 def hybridlist(data, name, fmt=None, gen=None):
308 """Wrap data to support both list-like and string-like operations"""
319 """Wrap data to support both list-like and string-like operations"""
309 prefmt = pycompat.identity
320 prefmt = pycompat.identity
310 if fmt is None:
321 if fmt is None:
311 fmt = '%s'
322 fmt = '%s'
312 prefmt = pycompat.bytestr
323 prefmt = pycompat.bytestr
313 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
324 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
314
325
315 def unwraphybrid(context, mapping, thing):
326 def unwraphybrid(context, mapping, thing):
316 """Return an object which can be stringified possibly by using a legacy
327 """Return an object which can be stringified possibly by using a legacy
317 template"""
328 template"""
318 if not isinstance(thing, wrapped):
329 if not isinstance(thing, wrapped):
319 return thing
330 return thing
320 return thing.show(context, mapping)
331 return thing.show(context, mapping)
321
332
322 def wraphybridvalue(container, key, value):
333 def wraphybridvalue(container, key, value):
323 """Wrap an element of hybrid container to be mappable
334 """Wrap an element of hybrid container to be mappable
324
335
325 The key is passed to the makemap function of the given container, which
336 The key is passed to the makemap function of the given container, which
326 should be an item generated by iter(container).
337 should be an item generated by iter(container).
327 """
338 """
328 makemap = getattr(container, '_makemap', None)
339 makemap = getattr(container, '_makemap', None)
329 if makemap is None:
340 if makemap is None:
330 return value
341 return value
331 if util.safehasattr(value, '_makemap'):
342 if util.safehasattr(value, '_makemap'):
332 # a nested hybrid list/dict, which has its own way of map operation
343 # a nested hybrid list/dict, which has its own way of map operation
333 return value
344 return value
334 return mappable(None, key, value, makemap)
345 return mappable(None, key, value, makemap)
335
346
336 def compatdict(context, mapping, name, data, key='key', value='value',
347 def compatdict(context, mapping, name, data, key='key', value='value',
337 fmt=None, plural=None, separator=' '):
348 fmt=None, plural=None, separator=' '):
338 """Wrap data like hybriddict(), but also supports old-style list template
349 """Wrap data like hybriddict(), but also supports old-style list template
339
350
340 This exists for backward compatibility with the old-style template. Use
351 This exists for backward compatibility with the old-style template. Use
341 hybriddict() for new template keywords.
352 hybriddict() for new template keywords.
342 """
353 """
343 c = [{key: k, value: v} for k, v in data.iteritems()]
354 c = [{key: k, value: v} for k, v in data.iteritems()]
344 f = _showcompatlist(context, mapping, name, c, plural, separator)
355 f = _showcompatlist(context, mapping, name, c, plural, separator)
345 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
356 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
346
357
347 def compatlist(context, mapping, name, data, element=None, fmt=None,
358 def compatlist(context, mapping, name, data, element=None, fmt=None,
348 plural=None, separator=' '):
359 plural=None, separator=' '):
349 """Wrap data like hybridlist(), but also supports old-style list template
360 """Wrap data like hybridlist(), but also supports old-style list template
350
361
351 This exists for backward compatibility with the old-style template. Use
362 This exists for backward compatibility with the old-style template. Use
352 hybridlist() for new template keywords.
363 hybridlist() for new template keywords.
353 """
364 """
354 f = _showcompatlist(context, mapping, name, data, plural, separator)
365 f = _showcompatlist(context, mapping, name, data, plural, separator)
355 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
366 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
356
367
357 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
368 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
358 """Return a generator that renders old-style list template
369 """Return a generator that renders old-style list template
359
370
360 name is name of key in template map.
371 name is name of key in template map.
361 values is list of strings or dicts.
372 values is list of strings or dicts.
362 plural is plural of name, if not simply name + 's'.
373 plural is plural of name, if not simply name + 's'.
363 separator is used to join values as a string
374 separator is used to join values as a string
364
375
365 expansion works like this, given name 'foo'.
376 expansion works like this, given name 'foo'.
366
377
367 if values is empty, expand 'no_foos'.
378 if values is empty, expand 'no_foos'.
368
379
369 if 'foo' not in template map, return values as a string,
380 if 'foo' not in template map, return values as a string,
370 joined by 'separator'.
381 joined by 'separator'.
371
382
372 expand 'start_foos'.
383 expand 'start_foos'.
373
384
374 for each value, expand 'foo'. if 'last_foo' in template
385 for each value, expand 'foo'. if 'last_foo' in template
375 map, expand it instead of 'foo' for last key.
386 map, expand it instead of 'foo' for last key.
376
387
377 expand 'end_foos'.
388 expand 'end_foos'.
378 """
389 """
379 if not plural:
390 if not plural:
380 plural = name + 's'
391 plural = name + 's'
381 if not values:
392 if not values:
382 noname = 'no_' + plural
393 noname = 'no_' + plural
383 if context.preload(noname):
394 if context.preload(noname):
384 yield context.process(noname, mapping)
395 yield context.process(noname, mapping)
385 return
396 return
386 if not context.preload(name):
397 if not context.preload(name):
387 if isinstance(values[0], bytes):
398 if isinstance(values[0], bytes):
388 yield separator.join(values)
399 yield separator.join(values)
389 else:
400 else:
390 for v in values:
401 for v in values:
391 r = dict(v)
402 r = dict(v)
392 r.update(mapping)
403 r.update(mapping)
393 yield r
404 yield r
394 return
405 return
395 startname = 'start_' + plural
406 startname = 'start_' + plural
396 if context.preload(startname):
407 if context.preload(startname):
397 yield context.process(startname, mapping)
408 yield context.process(startname, mapping)
398 def one(v, tag=name):
409 def one(v, tag=name):
399 vmapping = {}
410 vmapping = {}
400 try:
411 try:
401 vmapping.update(v)
412 vmapping.update(v)
402 # Python 2 raises ValueError if the type of v is wrong. Python
413 # Python 2 raises ValueError if the type of v is wrong. Python
403 # 3 raises TypeError.
414 # 3 raises TypeError.
404 except (AttributeError, TypeError, ValueError):
415 except (AttributeError, TypeError, ValueError):
405 try:
416 try:
406 # Python 2 raises ValueError trying to destructure an e.g.
417 # Python 2 raises ValueError trying to destructure an e.g.
407 # bytes. Python 3 raises TypeError.
418 # bytes. Python 3 raises TypeError.
408 for a, b in v:
419 for a, b in v:
409 vmapping[a] = b
420 vmapping[a] = b
410 except (TypeError, ValueError):
421 except (TypeError, ValueError):
411 vmapping[name] = v
422 vmapping[name] = v
412 vmapping = context.overlaymap(mapping, vmapping)
423 vmapping = context.overlaymap(mapping, vmapping)
413 return context.process(tag, vmapping)
424 return context.process(tag, vmapping)
414 lastname = 'last_' + name
425 lastname = 'last_' + name
415 if context.preload(lastname):
426 if context.preload(lastname):
416 last = values.pop()
427 last = values.pop()
417 else:
428 else:
418 last = None
429 last = None
419 for v in values:
430 for v in values:
420 yield one(v)
431 yield one(v)
421 if last is not None:
432 if last is not None:
422 yield one(last, tag=lastname)
433 yield one(last, tag=lastname)
423 endname = 'end_' + plural
434 endname = 'end_' + plural
424 if context.preload(endname):
435 if context.preload(endname):
425 yield context.process(endname, mapping)
436 yield context.process(endname, mapping)
426
437
427 def flatten(context, mapping, thing):
438 def flatten(context, mapping, thing):
428 """Yield a single stream from a possibly nested set of iterators"""
439 """Yield a single stream from a possibly nested set of iterators"""
429 thing = unwraphybrid(context, mapping, thing)
440 thing = unwraphybrid(context, mapping, thing)
430 if isinstance(thing, bytes):
441 if isinstance(thing, bytes):
431 yield thing
442 yield thing
432 elif isinstance(thing, str):
443 elif isinstance(thing, str):
433 # We can only hit this on Python 3, and it's here to guard
444 # We can only hit this on Python 3, and it's here to guard
434 # against infinite recursion.
445 # against infinite recursion.
435 raise error.ProgrammingError('Mercurial IO including templates is done'
446 raise error.ProgrammingError('Mercurial IO including templates is done'
436 ' with bytes, not strings, got %r' % thing)
447 ' with bytes, not strings, got %r' % thing)
437 elif thing is None:
448 elif thing is None:
438 pass
449 pass
439 elif not util.safehasattr(thing, '__iter__'):
450 elif not util.safehasattr(thing, '__iter__'):
440 yield pycompat.bytestr(thing)
451 yield pycompat.bytestr(thing)
441 else:
452 else:
442 for i in thing:
453 for i in thing:
443 i = unwraphybrid(context, mapping, i)
454 i = unwraphybrid(context, mapping, i)
444 if isinstance(i, bytes):
455 if isinstance(i, bytes):
445 yield i
456 yield i
446 elif i is None:
457 elif i is None:
447 pass
458 pass
448 elif not util.safehasattr(i, '__iter__'):
459 elif not util.safehasattr(i, '__iter__'):
449 yield pycompat.bytestr(i)
460 yield pycompat.bytestr(i)
450 else:
461 else:
451 for j in flatten(context, mapping, i):
462 for j in flatten(context, mapping, i):
452 yield j
463 yield j
453
464
454 def stringify(context, mapping, thing):
465 def stringify(context, mapping, thing):
455 """Turn values into bytes by converting into text and concatenating them"""
466 """Turn values into bytes by converting into text and concatenating them"""
456 if isinstance(thing, bytes):
467 if isinstance(thing, bytes):
457 return thing # retain localstr to be round-tripped
468 return thing # retain localstr to be round-tripped
458 return b''.join(flatten(context, mapping, thing))
469 return b''.join(flatten(context, mapping, thing))
459
470
460 def findsymbolicname(arg):
471 def findsymbolicname(arg):
461 """Find symbolic name for the given compiled expression; returns None
472 """Find symbolic name for the given compiled expression; returns None
462 if nothing found reliably"""
473 if nothing found reliably"""
463 while True:
474 while True:
464 func, data = arg
475 func, data = arg
465 if func is runsymbol:
476 if func is runsymbol:
466 return data
477 return data
467 elif func is runfilter:
478 elif func is runfilter:
468 arg = data[0]
479 arg = data[0]
469 else:
480 else:
470 return None
481 return None
471
482
472 def _unthunk(context, mapping, thing):
483 def _unthunk(context, mapping, thing):
473 """Evaluate a lazy byte string into value"""
484 """Evaluate a lazy byte string into value"""
474 if not isinstance(thing, types.GeneratorType):
485 if not isinstance(thing, types.GeneratorType):
475 return thing
486 return thing
476 return stringify(context, mapping, thing)
487 return stringify(context, mapping, thing)
477
488
478 def evalrawexp(context, mapping, arg):
489 def evalrawexp(context, mapping, arg):
479 """Evaluate given argument as a bare template object which may require
490 """Evaluate given argument as a bare template object which may require
480 further processing (such as folding generator of strings)"""
491 further processing (such as folding generator of strings)"""
481 func, data = arg
492 func, data = arg
482 return func(context, mapping, data)
493 return func(context, mapping, data)
483
494
484 def evalwrapped(context, mapping, arg):
495 def evalwrapped(context, mapping, arg):
485 """Evaluate given argument to wrapped object"""
496 """Evaluate given argument to wrapped object"""
486 thing = evalrawexp(context, mapping, arg)
497 thing = evalrawexp(context, mapping, arg)
487 return makewrapped(context, mapping, thing)
498 return makewrapped(context, mapping, thing)
488
499
489 def makewrapped(context, mapping, thing):
500 def makewrapped(context, mapping, thing):
490 """Lift object to a wrapped type"""
501 """Lift object to a wrapped type"""
491 if isinstance(thing, wrapped):
502 if isinstance(thing, wrapped):
492 return thing
503 return thing
493 thing = _unthunk(context, mapping, thing)
504 thing = _unthunk(context, mapping, thing)
494 if isinstance(thing, bytes):
505 if isinstance(thing, bytes):
495 return wrappedbytes(thing)
506 return wrappedbytes(thing)
496 return wrappedvalue(thing)
507 return wrappedvalue(thing)
497
508
498 def evalfuncarg(context, mapping, arg):
509 def evalfuncarg(context, mapping, arg):
499 """Evaluate given argument as value type"""
510 """Evaluate given argument as value type"""
500 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
511 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
501
512
502 def unwrapvalue(context, mapping, thing):
513 def unwrapvalue(context, mapping, thing):
503 """Move the inner value object out of the wrapper"""
514 """Move the inner value object out of the wrapper"""
504 if isinstance(thing, wrapped):
515 if isinstance(thing, wrapped):
505 return thing.tovalue(context, mapping)
516 return thing.tovalue(context, mapping)
506 # evalrawexp() may return string, generator of strings or arbitrary object
517 # evalrawexp() may return string, generator of strings or arbitrary object
507 # such as date tuple, but filter does not want generator.
518 # such as date tuple, but filter does not want generator.
508 return _unthunk(context, mapping, thing)
519 return _unthunk(context, mapping, thing)
509
520
510 def evalboolean(context, mapping, arg):
521 def evalboolean(context, mapping, arg):
511 """Evaluate given argument as boolean, but also takes boolean literals"""
522 """Evaluate given argument as boolean, but also takes boolean literals"""
512 func, data = arg
523 func, data = arg
513 if func is runsymbol:
524 if func is runsymbol:
514 thing = func(context, mapping, data, default=None)
525 thing = func(context, mapping, data, default=None)
515 if thing is None:
526 if thing is None:
516 # not a template keyword, takes as a boolean literal
527 # not a template keyword, takes as a boolean literal
517 thing = stringutil.parsebool(data)
528 thing = stringutil.parsebool(data)
518 else:
529 else:
519 thing = func(context, mapping, data)
530 thing = func(context, mapping, data)
520 if isinstance(thing, wrapped):
531 if isinstance(thing, wrapped):
521 thing = thing.tovalue(context, mapping)
532 thing = thing.tovalue(context, mapping)
522 if isinstance(thing, bool):
533 if isinstance(thing, bool):
523 return thing
534 return thing
524 # other objects are evaluated as strings, which means 0 is True, but
535 # 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 ''
536 # empty dict/list should be False as they are expected to be ''
526 return bool(stringify(context, mapping, thing))
537 return bool(stringify(context, mapping, thing))
527
538
528 def evaldate(context, mapping, arg, err=None):
539 def evaldate(context, mapping, arg, err=None):
529 """Evaluate given argument as a date tuple or a date string; returns
540 """Evaluate given argument as a date tuple or a date string; returns
530 a (unixtime, offset) tuple"""
541 a (unixtime, offset) tuple"""
531 thing = evalrawexp(context, mapping, arg)
542 thing = evalrawexp(context, mapping, arg)
532 return unwrapdate(context, mapping, thing, err)
543 return unwrapdate(context, mapping, thing, err)
533
544
534 def unwrapdate(context, mapping, thing, err=None):
545 def unwrapdate(context, mapping, thing, err=None):
535 thing = unwrapvalue(context, mapping, thing)
546 thing = unwrapvalue(context, mapping, thing)
536 try:
547 try:
537 return dateutil.parsedate(thing)
548 return dateutil.parsedate(thing)
538 except AttributeError:
549 except AttributeError:
539 raise error.ParseError(err or _('not a date tuple nor a string'))
550 raise error.ParseError(err or _('not a date tuple nor a string'))
540 except error.ParseError:
551 except error.ParseError:
541 if not err:
552 if not err:
542 raise
553 raise
543 raise error.ParseError(err)
554 raise error.ParseError(err)
544
555
545 def evalinteger(context, mapping, arg, err=None):
556 def evalinteger(context, mapping, arg, err=None):
546 thing = evalrawexp(context, mapping, arg)
557 thing = evalrawexp(context, mapping, arg)
547 return unwrapinteger(context, mapping, thing, err)
558 return unwrapinteger(context, mapping, thing, err)
548
559
549 def unwrapinteger(context, mapping, thing, err=None):
560 def unwrapinteger(context, mapping, thing, err=None):
550 thing = unwrapvalue(context, mapping, thing)
561 thing = unwrapvalue(context, mapping, thing)
551 try:
562 try:
552 return int(thing)
563 return int(thing)
553 except (TypeError, ValueError):
564 except (TypeError, ValueError):
554 raise error.ParseError(err or _('not an integer'))
565 raise error.ParseError(err or _('not an integer'))
555
566
556 def evalstring(context, mapping, arg):
567 def evalstring(context, mapping, arg):
557 return stringify(context, mapping, evalrawexp(context, mapping, arg))
568 return stringify(context, mapping, evalrawexp(context, mapping, arg))
558
569
559 def evalstringliteral(context, mapping, arg):
570 def evalstringliteral(context, mapping, arg):
560 """Evaluate given argument as string template, but returns symbol name
571 """Evaluate given argument as string template, but returns symbol name
561 if it is unknown"""
572 if it is unknown"""
562 func, data = arg
573 func, data = arg
563 if func is runsymbol:
574 if func is runsymbol:
564 thing = func(context, mapping, data, default=data)
575 thing = func(context, mapping, data, default=data)
565 else:
576 else:
566 thing = func(context, mapping, data)
577 thing = func(context, mapping, data)
567 return stringify(context, mapping, thing)
578 return stringify(context, mapping, thing)
568
579
569 _unwrapfuncbytype = {
580 _unwrapfuncbytype = {
570 None: unwrapvalue,
581 None: unwrapvalue,
571 bytes: stringify,
582 bytes: stringify,
572 date: unwrapdate,
583 date: unwrapdate,
573 int: unwrapinteger,
584 int: unwrapinteger,
574 }
585 }
575
586
576 def unwrapastype(context, mapping, thing, typ):
587 def unwrapastype(context, mapping, thing, typ):
577 """Move the inner value object out of the wrapper and coerce its type"""
588 """Move the inner value object out of the wrapper and coerce its type"""
578 try:
589 try:
579 f = _unwrapfuncbytype[typ]
590 f = _unwrapfuncbytype[typ]
580 except KeyError:
591 except KeyError:
581 raise error.ProgrammingError('invalid type specified: %r' % typ)
592 raise error.ProgrammingError('invalid type specified: %r' % typ)
582 return f(context, mapping, thing)
593 return f(context, mapping, thing)
583
594
584 def runinteger(context, mapping, data):
595 def runinteger(context, mapping, data):
585 return int(data)
596 return int(data)
586
597
587 def runstring(context, mapping, data):
598 def runstring(context, mapping, data):
588 return data
599 return data
589
600
590 def _recursivesymbolblocker(key):
601 def _recursivesymbolblocker(key):
591 def showrecursion(**args):
602 def showrecursion(**args):
592 raise error.Abort(_("recursive reference '%s' in template") % key)
603 raise error.Abort(_("recursive reference '%s' in template") % key)
593 return showrecursion
604 return showrecursion
594
605
595 def runsymbol(context, mapping, key, default=''):
606 def runsymbol(context, mapping, key, default=''):
596 v = context.symbol(mapping, key)
607 v = context.symbol(mapping, key)
597 if v is None:
608 if v is None:
598 # put poison to cut recursion. we can't move this to parsing phase
609 # 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)
610 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
600 safemapping = mapping.copy()
611 safemapping = mapping.copy()
601 safemapping[key] = _recursivesymbolblocker(key)
612 safemapping[key] = _recursivesymbolblocker(key)
602 try:
613 try:
603 v = context.process(key, safemapping)
614 v = context.process(key, safemapping)
604 except TemplateNotFound:
615 except TemplateNotFound:
605 v = default
616 v = default
606 if callable(v) and getattr(v, '_requires', None) is None:
617 if callable(v) and getattr(v, '_requires', None) is None:
607 # old templatekw: expand all keywords and resources
618 # old templatekw: expand all keywords and resources
608 # (TODO: deprecate this after porting web template keywords to new API)
619 # (TODO: deprecate this after porting web template keywords to new API)
609 props = {k: context._resources.lookup(context, mapping, k)
620 props = {k: context._resources.lookup(context, mapping, k)
610 for k in context._resources.knownkeys()}
621 for k in context._resources.knownkeys()}
611 # pass context to _showcompatlist() through templatekw._showlist()
622 # pass context to _showcompatlist() through templatekw._showlist()
612 props['templ'] = context
623 props['templ'] = context
613 props.update(mapping)
624 props.update(mapping)
614 return v(**pycompat.strkwargs(props))
625 return v(**pycompat.strkwargs(props))
615 if callable(v):
626 if callable(v):
616 # new templatekw
627 # new templatekw
617 try:
628 try:
618 return v(context, mapping)
629 return v(context, mapping)
619 except ResourceUnavailable:
630 except ResourceUnavailable:
620 # unsupported keyword is mapped to empty just like unknown keyword
631 # unsupported keyword is mapped to empty just like unknown keyword
621 return None
632 return None
622 return v
633 return v
623
634
624 def runtemplate(context, mapping, template):
635 def runtemplate(context, mapping, template):
625 for arg in template:
636 for arg in template:
626 yield evalrawexp(context, mapping, arg)
637 yield evalrawexp(context, mapping, arg)
627
638
628 def runfilter(context, mapping, data):
639 def runfilter(context, mapping, data):
629 arg, filt = data
640 arg, filt = data
630 thing = evalrawexp(context, mapping, arg)
641 thing = evalrawexp(context, mapping, arg)
631 intype = getattr(filt, '_intype', None)
642 intype = getattr(filt, '_intype', None)
632 try:
643 try:
633 thing = unwrapastype(context, mapping, thing, intype)
644 thing = unwrapastype(context, mapping, thing, intype)
634 return filt(thing)
645 return filt(thing)
635 except error.ParseError as e:
646 except error.ParseError as e:
636 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
647 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
637
648
638 def _formatfiltererror(arg, filt):
649 def _formatfiltererror(arg, filt):
639 fn = pycompat.sysbytes(filt.__name__)
650 fn = pycompat.sysbytes(filt.__name__)
640 sym = findsymbolicname(arg)
651 sym = findsymbolicname(arg)
641 if not sym:
652 if not sym:
642 return _("incompatible use of template filter '%s'") % fn
653 return _("incompatible use of template filter '%s'") % fn
643 return (_("template filter '%s' is not compatible with keyword '%s'")
654 return (_("template filter '%s' is not compatible with keyword '%s'")
644 % (fn, sym))
655 % (fn, sym))
645
656
646 def _iteroverlaymaps(context, origmapping, newmappings):
657 def _iteroverlaymaps(context, origmapping, newmappings):
647 """Generate combined mappings from the original mapping and an iterable
658 """Generate combined mappings from the original mapping and an iterable
648 of partial mappings to override the original"""
659 of partial mappings to override the original"""
649 for i, nm in enumerate(newmappings):
660 for i, nm in enumerate(newmappings):
650 lm = context.overlaymap(origmapping, nm)
661 lm = context.overlaymap(origmapping, nm)
651 lm['index'] = i
662 lm['index'] = i
652 yield lm
663 yield lm
653
664
654 def _applymap(context, mapping, d, targ):
665 def _applymap(context, mapping, d, targ):
655 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
666 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
656 yield evalrawexp(context, lm, targ)
667 yield evalrawexp(context, lm, targ)
657
668
658 def runmap(context, mapping, data):
669 def runmap(context, mapping, data):
659 darg, targ = data
670 darg, targ = data
660 d = evalwrapped(context, mapping, darg)
671 d = evalwrapped(context, mapping, darg)
661 return mappedgenerator(_applymap, args=(mapping, d, targ))
672 return mappedgenerator(_applymap, args=(mapping, d, targ))
662
673
663 def runmember(context, mapping, data):
674 def runmember(context, mapping, data):
664 darg, memb = data
675 darg, memb = data
665 d = evalwrapped(context, mapping, darg)
676 d = evalwrapped(context, mapping, darg)
666 if util.safehasattr(d, 'tomap'):
677 if util.safehasattr(d, 'tomap'):
667 lm = context.overlaymap(mapping, d.tomap())
678 lm = context.overlaymap(mapping, d.tomap())
668 return runsymbol(context, lm, memb)
679 return runsymbol(context, lm, memb)
669 try:
680 try:
670 if util.safehasattr(d, 'get'):
681 if util.safehasattr(d, 'getmember'):
671 return getdictitem(d, memb)
682 return d.getmember(context, mapping, memb)
672 raise error.ParseError
683 raise error.ParseError
673 except error.ParseError:
684 except error.ParseError:
674 sym = findsymbolicname(darg)
685 sym = findsymbolicname(darg)
675 if sym:
686 if sym:
676 raise error.ParseError(_("keyword '%s' has no member") % sym)
687 raise error.ParseError(_("keyword '%s' has no member") % sym)
677 else:
688 else:
678 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
689 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
679
690
680 def runnegate(context, mapping, data):
691 def runnegate(context, mapping, data):
681 data = evalinteger(context, mapping, data,
692 data = evalinteger(context, mapping, data,
682 _('negation needs an integer argument'))
693 _('negation needs an integer argument'))
683 return -data
694 return -data
684
695
685 def runarithmetic(context, mapping, data):
696 def runarithmetic(context, mapping, data):
686 func, left, right = data
697 func, left, right = data
687 left = evalinteger(context, mapping, left,
698 left = evalinteger(context, mapping, left,
688 _('arithmetic only defined on integers'))
699 _('arithmetic only defined on integers'))
689 right = evalinteger(context, mapping, right,
700 right = evalinteger(context, mapping, right,
690 _('arithmetic only defined on integers'))
701 _('arithmetic only defined on integers'))
691 try:
702 try:
692 return func(left, right)
703 return func(left, right)
693 except ZeroDivisionError:
704 except ZeroDivisionError:
694 raise error.Abort(_('division by zero is not defined'))
705 raise error.Abort(_('division by zero is not defined'))
695
706
696 def getdictitem(dictarg, key):
697 val = dictarg.get(key)
698 if val is None:
699 return
700 return wraphybridvalue(dictarg, key, val)
701
702 def joinitems(itemiter, sep):
707 def joinitems(itemiter, sep):
703 """Join items with the separator; Returns generator of bytes"""
708 """Join items with the separator; Returns generator of bytes"""
704 first = True
709 first = True
705 for x in itemiter:
710 for x in itemiter:
706 if first:
711 if first:
707 first = False
712 first = False
708 elif sep:
713 elif sep:
709 yield sep
714 yield sep
710 yield x
715 yield x
General Comments 0
You need to be logged in to leave comments. Login now