##// END OF EJS Templates
templater: unify unwrapvalue() with _unwrapvalue()...
Yuya Nishihara -
r38227:d48b80d5 default
parent child Browse files
Show More
@@ -1,705 +1,706 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 evalfuncarg = templateutil.evalfuncarg
39 evalfuncarg = templateutil.evalfuncarg
40 evalboolean = templateutil.evalboolean
40 evalboolean = templateutil.evalboolean
41 evaldate = templateutil.evaldate
41 evaldate = templateutil.evaldate
42 evalinteger = templateutil.evalinteger
42 evalinteger = templateutil.evalinteger
43 evalstring = templateutil.evalstring
43 evalstring = templateutil.evalstring
44 evalstringliteral = templateutil.evalstringliteral
44 evalstringliteral = templateutil.evalstringliteral
45
45
46 # dict of template built-in functions
46 # dict of template built-in functions
47 funcs = {}
47 funcs = {}
48 templatefunc = registrar.templatefunc(funcs)
48 templatefunc = registrar.templatefunc(funcs)
49
49
50 @templatefunc('date(date[, fmt])')
50 @templatefunc('date(date[, fmt])')
51 def date(context, mapping, args):
51 def date(context, mapping, args):
52 """Format a date. See :hg:`help dates` for formatting
52 """Format a date. See :hg:`help dates` for formatting
53 strings. The default is a Unix date format, including the timezone:
53 strings. The default is a Unix date format, including the timezone:
54 "Mon Sep 04 15:13:13 2006 0700"."""
54 "Mon Sep 04 15:13:13 2006 0700"."""
55 if not (1 <= len(args) <= 2):
55 if not (1 <= len(args) <= 2):
56 # i18n: "date" is a keyword
56 # i18n: "date" is a keyword
57 raise error.ParseError(_("date expects one or two arguments"))
57 raise error.ParseError(_("date expects one or two arguments"))
58
58
59 date = evaldate(context, mapping, args[0],
59 date = evaldate(context, mapping, args[0],
60 # i18n: "date" is a keyword
60 # i18n: "date" is a keyword
61 _("date expects a date information"))
61 _("date expects a date information"))
62 fmt = None
62 fmt = None
63 if len(args) == 2:
63 if len(args) == 2:
64 fmt = evalstring(context, mapping, args[1])
64 fmt = evalstring(context, mapping, args[1])
65 if fmt is None:
65 if fmt is None:
66 return dateutil.datestr(date)
66 return dateutil.datestr(date)
67 else:
67 else:
68 return dateutil.datestr(date, fmt)
68 return dateutil.datestr(date, fmt)
69
69
70 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
70 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
71 def dict_(context, mapping, args):
71 def dict_(context, mapping, args):
72 """Construct a dict from key-value pairs. A key may be omitted if
72 """Construct a dict from key-value pairs. A key may be omitted if
73 a value expression can provide an unambiguous name."""
73 a value expression can provide an unambiguous name."""
74 data = util.sortdict()
74 data = util.sortdict()
75
75
76 for v in args['args']:
76 for v in args['args']:
77 k = templateutil.findsymbolicname(v)
77 k = templateutil.findsymbolicname(v)
78 if not k:
78 if not k:
79 raise error.ParseError(_('dict key cannot be inferred'))
79 raise error.ParseError(_('dict key cannot be inferred'))
80 if k in data or k in args['kwargs']:
80 if k in data or k in args['kwargs']:
81 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
81 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
82 data[k] = evalfuncarg(context, mapping, v)
82 data[k] = evalfuncarg(context, mapping, v)
83
83
84 data.update((k, evalfuncarg(context, mapping, v))
84 data.update((k, evalfuncarg(context, mapping, v))
85 for k, v in args['kwargs'].iteritems())
85 for k, v in args['kwargs'].iteritems())
86 return templateutil.hybriddict(data)
86 return templateutil.hybriddict(data)
87
87
88 @templatefunc('diff([includepattern [, excludepattern]])')
88 @templatefunc('diff([includepattern [, excludepattern]])')
89 def diff(context, mapping, args):
89 def diff(context, mapping, args):
90 """Show a diff, optionally
90 """Show a diff, optionally
91 specifying files to include or exclude."""
91 specifying files to include or exclude."""
92 if len(args) > 2:
92 if len(args) > 2:
93 # i18n: "diff" is a keyword
93 # i18n: "diff" is a keyword
94 raise error.ParseError(_("diff expects zero, one, or two arguments"))
94 raise error.ParseError(_("diff expects zero, one, or two arguments"))
95
95
96 def getpatterns(i):
96 def getpatterns(i):
97 if i < len(args):
97 if i < len(args):
98 s = evalstring(context, mapping, args[i]).strip()
98 s = evalstring(context, mapping, args[i]).strip()
99 if s:
99 if s:
100 return [s]
100 return [s]
101 return []
101 return []
102
102
103 ctx = context.resource(mapping, 'ctx')
103 ctx = context.resource(mapping, 'ctx')
104 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
104 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
105
105
106 return ''.join(chunks)
106 return ''.join(chunks)
107
107
108 @templatefunc('extdata(source)', argspec='source')
108 @templatefunc('extdata(source)', argspec='source')
109 def extdata(context, mapping, args):
109 def extdata(context, mapping, args):
110 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
110 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
111 if 'source' not in args:
111 if 'source' not in args:
112 # i18n: "extdata" is a keyword
112 # i18n: "extdata" is a keyword
113 raise error.ParseError(_('extdata expects one argument'))
113 raise error.ParseError(_('extdata expects one argument'))
114
114
115 source = evalstring(context, mapping, args['source'])
115 source = evalstring(context, mapping, args['source'])
116 if not source:
116 if not source:
117 sym = templateutil.findsymbolicname(args['source'])
117 sym = templateutil.findsymbolicname(args['source'])
118 if sym:
118 if sym:
119 raise error.ParseError(_('empty data source specified'),
119 raise error.ParseError(_('empty data source specified'),
120 hint=_("did you mean extdata('%s')?") % sym)
120 hint=_("did you mean extdata('%s')?") % sym)
121 else:
121 else:
122 raise error.ParseError(_('empty data source specified'))
122 raise error.ParseError(_('empty data source specified'))
123 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
123 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
124 ctx = context.resource(mapping, 'ctx')
124 ctx = context.resource(mapping, 'ctx')
125 if source in cache:
125 if source in cache:
126 data = cache[source]
126 data = cache[source]
127 else:
127 else:
128 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
128 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
129 return data.get(ctx.rev(), '')
129 return data.get(ctx.rev(), '')
130
130
131 @templatefunc('files(pattern)')
131 @templatefunc('files(pattern)')
132 def files(context, mapping, args):
132 def files(context, mapping, args):
133 """All files of the current changeset matching the pattern. See
133 """All files of the current changeset matching the pattern. See
134 :hg:`help patterns`."""
134 :hg:`help patterns`."""
135 if not len(args) == 1:
135 if not len(args) == 1:
136 # i18n: "files" is a keyword
136 # i18n: "files" is a keyword
137 raise error.ParseError(_("files expects one argument"))
137 raise error.ParseError(_("files expects one argument"))
138
138
139 raw = evalstring(context, mapping, args[0])
139 raw = evalstring(context, mapping, args[0])
140 ctx = context.resource(mapping, 'ctx')
140 ctx = context.resource(mapping, 'ctx')
141 m = ctx.match([raw])
141 m = ctx.match([raw])
142 files = list(ctx.matches(m))
142 files = list(ctx.matches(m))
143 return templateutil.compatlist(context, mapping, "file", files)
143 return templateutil.compatlist(context, mapping, "file", files)
144
144
145 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
145 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
146 def fill(context, mapping, args):
146 def fill(context, mapping, args):
147 """Fill many
147 """Fill many
148 paragraphs with optional indentation. See the "fill" filter."""
148 paragraphs with optional indentation. See the "fill" filter."""
149 if not (1 <= len(args) <= 4):
149 if not (1 <= len(args) <= 4):
150 # i18n: "fill" is a keyword
150 # i18n: "fill" is a keyword
151 raise error.ParseError(_("fill expects one to four arguments"))
151 raise error.ParseError(_("fill expects one to four arguments"))
152
152
153 text = evalstring(context, mapping, args[0])
153 text = evalstring(context, mapping, args[0])
154 width = 76
154 width = 76
155 initindent = ''
155 initindent = ''
156 hangindent = ''
156 hangindent = ''
157 if 2 <= len(args) <= 4:
157 if 2 <= len(args) <= 4:
158 width = evalinteger(context, mapping, args[1],
158 width = evalinteger(context, mapping, args[1],
159 # i18n: "fill" is a keyword
159 # i18n: "fill" is a keyword
160 _("fill expects an integer width"))
160 _("fill expects an integer width"))
161 try:
161 try:
162 initindent = evalstring(context, mapping, args[2])
162 initindent = evalstring(context, mapping, args[2])
163 hangindent = evalstring(context, mapping, args[3])
163 hangindent = evalstring(context, mapping, args[3])
164 except IndexError:
164 except IndexError:
165 pass
165 pass
166
166
167 return templatefilters.fill(text, width, initindent, hangindent)
167 return templatefilters.fill(text, width, initindent, hangindent)
168
168
169 @templatefunc('formatnode(node)')
169 @templatefunc('formatnode(node)')
170 def formatnode(context, mapping, args):
170 def formatnode(context, mapping, args):
171 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
171 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
172 if len(args) != 1:
172 if len(args) != 1:
173 # i18n: "formatnode" is a keyword
173 # i18n: "formatnode" is a keyword
174 raise error.ParseError(_("formatnode expects one argument"))
174 raise error.ParseError(_("formatnode expects one argument"))
175
175
176 ui = context.resource(mapping, 'ui')
176 ui = context.resource(mapping, 'ui')
177 node = evalstring(context, mapping, args[0])
177 node = evalstring(context, mapping, args[0])
178 if ui.debugflag:
178 if ui.debugflag:
179 return node
179 return node
180 return templatefilters.short(node)
180 return templatefilters.short(node)
181
181
182 @templatefunc('mailmap(author)')
182 @templatefunc('mailmap(author)')
183 def mailmap(context, mapping, args):
183 def mailmap(context, mapping, args):
184 """Return the author, updated according to the value
184 """Return the author, updated according to the value
185 set in the .mailmap file"""
185 set in the .mailmap file"""
186 if len(args) != 1:
186 if len(args) != 1:
187 raise error.ParseError(_("mailmap expects one argument"))
187 raise error.ParseError(_("mailmap expects one argument"))
188
188
189 author = evalstring(context, mapping, args[0])
189 author = evalstring(context, mapping, args[0])
190
190
191 cache = context.resource(mapping, 'cache')
191 cache = context.resource(mapping, 'cache')
192 repo = context.resource(mapping, 'repo')
192 repo = context.resource(mapping, 'repo')
193
193
194 if 'mailmap' not in cache:
194 if 'mailmap' not in cache:
195 data = repo.wvfs.tryread('.mailmap')
195 data = repo.wvfs.tryread('.mailmap')
196 cache['mailmap'] = stringutil.parsemailmap(data)
196 cache['mailmap'] = stringutil.parsemailmap(data)
197
197
198 return stringutil.mapname(cache['mailmap'], author)
198 return stringutil.mapname(cache['mailmap'], author)
199
199
200 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
200 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
201 argspec='text width fillchar left')
201 argspec='text width fillchar left')
202 def pad(context, mapping, args):
202 def pad(context, mapping, args):
203 """Pad text with a
203 """Pad text with a
204 fill character."""
204 fill character."""
205 if 'text' not in args or 'width' not in args:
205 if 'text' not in args or 'width' not in args:
206 # i18n: "pad" is a keyword
206 # i18n: "pad" is a keyword
207 raise error.ParseError(_("pad() expects two to four arguments"))
207 raise error.ParseError(_("pad() expects two to four arguments"))
208
208
209 width = evalinteger(context, mapping, args['width'],
209 width = evalinteger(context, mapping, args['width'],
210 # i18n: "pad" is a keyword
210 # i18n: "pad" is a keyword
211 _("pad() expects an integer width"))
211 _("pad() expects an integer width"))
212
212
213 text = evalstring(context, mapping, args['text'])
213 text = evalstring(context, mapping, args['text'])
214
214
215 left = False
215 left = False
216 fillchar = ' '
216 fillchar = ' '
217 if 'fillchar' in args:
217 if 'fillchar' in args:
218 fillchar = evalstring(context, mapping, args['fillchar'])
218 fillchar = evalstring(context, mapping, args['fillchar'])
219 if len(color.stripeffects(fillchar)) != 1:
219 if len(color.stripeffects(fillchar)) != 1:
220 # i18n: "pad" is a keyword
220 # i18n: "pad" is a keyword
221 raise error.ParseError(_("pad() expects a single fill character"))
221 raise error.ParseError(_("pad() expects a single fill character"))
222 if 'left' in args:
222 if 'left' in args:
223 left = evalboolean(context, mapping, args['left'])
223 left = evalboolean(context, mapping, args['left'])
224
224
225 fillwidth = width - encoding.colwidth(color.stripeffects(text))
225 fillwidth = width - encoding.colwidth(color.stripeffects(text))
226 if fillwidth <= 0:
226 if fillwidth <= 0:
227 return text
227 return text
228 if left:
228 if left:
229 return fillchar * fillwidth + text
229 return fillchar * fillwidth + text
230 else:
230 else:
231 return text + fillchar * fillwidth
231 return text + fillchar * fillwidth
232
232
233 @templatefunc('indent(text, indentchars[, firstline])')
233 @templatefunc('indent(text, indentchars[, firstline])')
234 def indent(context, mapping, args):
234 def indent(context, mapping, args):
235 """Indents all non-empty lines
235 """Indents all non-empty lines
236 with the characters given in the indentchars string. An optional
236 with the characters given in the indentchars string. An optional
237 third parameter will override the indent for the first line only
237 third parameter will override the indent for the first line only
238 if present."""
238 if present."""
239 if not (2 <= len(args) <= 3):
239 if not (2 <= len(args) <= 3):
240 # i18n: "indent" is a keyword
240 # i18n: "indent" is a keyword
241 raise error.ParseError(_("indent() expects two or three arguments"))
241 raise error.ParseError(_("indent() expects two or three arguments"))
242
242
243 text = evalstring(context, mapping, args[0])
243 text = evalstring(context, mapping, args[0])
244 indent = evalstring(context, mapping, args[1])
244 indent = evalstring(context, mapping, args[1])
245
245
246 if len(args) == 3:
246 if len(args) == 3:
247 firstline = evalstring(context, mapping, args[2])
247 firstline = evalstring(context, mapping, args[2])
248 else:
248 else:
249 firstline = indent
249 firstline = indent
250
250
251 # the indent function doesn't indent the first line, so we do it here
251 # the indent function doesn't indent the first line, so we do it here
252 return templatefilters.indent(firstline + text, indent)
252 return templatefilters.indent(firstline + text, indent)
253
253
254 @templatefunc('get(dict, key)')
254 @templatefunc('get(dict, key)')
255 def get(context, mapping, args):
255 def get(context, mapping, args):
256 """Get an attribute/key from an object. Some keywords
256 """Get an attribute/key from an object. Some keywords
257 are complex types. This function allows you to obtain the value of an
257 are complex types. This function allows you to obtain the value of an
258 attribute on these types."""
258 attribute on these types."""
259 if len(args) != 2:
259 if len(args) != 2:
260 # i18n: "get" is a keyword
260 # i18n: "get" is a keyword
261 raise error.ParseError(_("get() expects two arguments"))
261 raise error.ParseError(_("get() expects two arguments"))
262
262
263 dictarg = evalfuncarg(context, mapping, args[0])
263 dictarg = evalfuncarg(context, mapping, args[0])
264 if not util.safehasattr(dictarg, 'get'):
264 if not util.safehasattr(dictarg, 'get'):
265 # i18n: "get" is a keyword
265 # i18n: "get" is a keyword
266 raise error.ParseError(_("get() expects a dict as first argument"))
266 raise error.ParseError(_("get() expects a dict as first argument"))
267
267
268 key = evalfuncarg(context, mapping, args[1])
268 key = evalfuncarg(context, mapping, args[1])
269 return templateutil.getdictitem(dictarg, key)
269 return templateutil.getdictitem(dictarg, key)
270
270
271 @templatefunc('if(expr, then[, else])')
271 @templatefunc('if(expr, then[, else])')
272 def if_(context, mapping, args):
272 def if_(context, mapping, args):
273 """Conditionally execute based on the result of
273 """Conditionally execute based on the result of
274 an expression."""
274 an expression."""
275 if not (2 <= len(args) <= 3):
275 if not (2 <= len(args) <= 3):
276 # i18n: "if" is a keyword
276 # i18n: "if" is a keyword
277 raise error.ParseError(_("if expects two or three arguments"))
277 raise error.ParseError(_("if expects two or three arguments"))
278
278
279 test = evalboolean(context, mapping, args[0])
279 test = evalboolean(context, mapping, args[0])
280 if test:
280 if test:
281 return evalrawexp(context, mapping, args[1])
281 return evalrawexp(context, mapping, args[1])
282 elif len(args) == 3:
282 elif len(args) == 3:
283 return evalrawexp(context, mapping, args[2])
283 return evalrawexp(context, mapping, args[2])
284
284
285 @templatefunc('ifcontains(needle, haystack, then[, else])')
285 @templatefunc('ifcontains(needle, haystack, then[, else])')
286 def ifcontains(context, mapping, args):
286 def ifcontains(context, mapping, args):
287 """Conditionally execute based
287 """Conditionally execute based
288 on whether the item "needle" is in "haystack"."""
288 on whether the item "needle" is in "haystack"."""
289 if not (3 <= len(args) <= 4):
289 if not (3 <= len(args) <= 4):
290 # i18n: "ifcontains" is a keyword
290 # i18n: "ifcontains" is a keyword
291 raise error.ParseError(_("ifcontains expects three or four arguments"))
291 raise error.ParseError(_("ifcontains expects three or four arguments"))
292
292
293 haystack = evalfuncarg(context, mapping, args[1])
293 haystack = evalfuncarg(context, mapping, args[1])
294 keytype = getattr(haystack, 'keytype', None)
294 keytype = getattr(haystack, 'keytype', None)
295 try:
295 try:
296 needle = evalrawexp(context, mapping, args[0])
296 needle = evalrawexp(context, mapping, args[0])
297 needle = templateutil.unwrapastype(context, mapping, needle,
297 needle = templateutil.unwrapastype(context, mapping, needle,
298 keytype or bytes)
298 keytype or bytes)
299 found = (needle in haystack)
299 found = (needle in haystack)
300 except error.ParseError:
300 except error.ParseError:
301 found = False
301 found = False
302
302
303 if found:
303 if found:
304 return evalrawexp(context, mapping, args[2])
304 return evalrawexp(context, mapping, args[2])
305 elif len(args) == 4:
305 elif len(args) == 4:
306 return evalrawexp(context, mapping, args[3])
306 return evalrawexp(context, mapping, args[3])
307
307
308 @templatefunc('ifeq(expr1, expr2, then[, else])')
308 @templatefunc('ifeq(expr1, expr2, then[, else])')
309 def ifeq(context, mapping, args):
309 def ifeq(context, mapping, args):
310 """Conditionally execute based on
310 """Conditionally execute based on
311 whether 2 items are equivalent."""
311 whether 2 items are equivalent."""
312 if not (3 <= len(args) <= 4):
312 if not (3 <= len(args) <= 4):
313 # i18n: "ifeq" is a keyword
313 # i18n: "ifeq" is a keyword
314 raise error.ParseError(_("ifeq expects three or four arguments"))
314 raise error.ParseError(_("ifeq expects three or four arguments"))
315
315
316 test = evalstring(context, mapping, args[0])
316 test = evalstring(context, mapping, args[0])
317 match = evalstring(context, mapping, args[1])
317 match = evalstring(context, mapping, args[1])
318 if test == match:
318 if test == match:
319 return evalrawexp(context, mapping, args[2])
319 return evalrawexp(context, mapping, args[2])
320 elif len(args) == 4:
320 elif len(args) == 4:
321 return evalrawexp(context, mapping, args[3])
321 return evalrawexp(context, mapping, args[3])
322
322
323 @templatefunc('join(list, sep)')
323 @templatefunc('join(list, sep)')
324 def join(context, mapping, args):
324 def join(context, mapping, args):
325 """Join items in a list with a delimiter."""
325 """Join items in a list with a delimiter."""
326 if not (1 <= len(args) <= 2):
326 if not (1 <= len(args) <= 2):
327 # i18n: "join" is a keyword
327 # i18n: "join" is a keyword
328 raise error.ParseError(_("join expects one or two arguments"))
328 raise error.ParseError(_("join expects one or two arguments"))
329
329
330 joinset = evalrawexp(context, mapping, args[0])
330 joinset = evalrawexp(context, mapping, args[0])
331 joiner = " "
331 joiner = " "
332 if len(args) > 1:
332 if len(args) > 1:
333 joiner = evalstring(context, mapping, args[1])
333 joiner = evalstring(context, mapping, args[1])
334 if isinstance(joinset, templateutil.wrapped):
334 if isinstance(joinset, templateutil.wrapped):
335 return joinset.join(context, mapping, joiner)
335 return joinset.join(context, mapping, joiner)
336 # TODO: perhaps a generator should be stringify()-ed here, but we can't
336 # TODO: rethink about join() of a byte string, which had no defined
337 # because hgweb abuses it as a keyword that returns a list of dicts.
337 # behavior since a string may be either a bytes or a generator.
338 # TODO: fix type error on join() of non-iterable
338 joinset = templateutil.unwrapvalue(context, mapping, joinset)
339 joinset = templateutil.unwrapvalue(context, mapping, joinset)
339 return templateutil.joinitems(pycompat.maybebytestr(joinset), joiner)
340 return templateutil.joinitems(pycompat.maybebytestr(joinset), joiner)
340
341
341 @templatefunc('label(label, expr)')
342 @templatefunc('label(label, expr)')
342 def label(context, mapping, args):
343 def label(context, mapping, args):
343 """Apply a label to generated content. Content with
344 """Apply a label to generated content. Content with
344 a label applied can result in additional post-processing, such as
345 a label applied can result in additional post-processing, such as
345 automatic colorization."""
346 automatic colorization."""
346 if len(args) != 2:
347 if len(args) != 2:
347 # i18n: "label" is a keyword
348 # i18n: "label" is a keyword
348 raise error.ParseError(_("label expects two arguments"))
349 raise error.ParseError(_("label expects two arguments"))
349
350
350 ui = context.resource(mapping, 'ui')
351 ui = context.resource(mapping, 'ui')
351 thing = evalstring(context, mapping, args[1])
352 thing = evalstring(context, mapping, args[1])
352 # preserve unknown symbol as literal so effects like 'red', 'bold',
353 # preserve unknown symbol as literal so effects like 'red', 'bold',
353 # etc. don't need to be quoted
354 # etc. don't need to be quoted
354 label = evalstringliteral(context, mapping, args[0])
355 label = evalstringliteral(context, mapping, args[0])
355
356
356 return ui.label(thing, label)
357 return ui.label(thing, label)
357
358
358 @templatefunc('latesttag([pattern])')
359 @templatefunc('latesttag([pattern])')
359 def latesttag(context, mapping, args):
360 def latesttag(context, mapping, args):
360 """The global tags matching the given pattern on the
361 """The global tags matching the given pattern on the
361 most recent globally tagged ancestor of this changeset.
362 most recent globally tagged ancestor of this changeset.
362 If no such tags exist, the "{tag}" template resolves to
363 If no such tags exist, the "{tag}" template resolves to
363 the string "null". See :hg:`help revisions.patterns` for the pattern
364 the string "null". See :hg:`help revisions.patterns` for the pattern
364 syntax.
365 syntax.
365 """
366 """
366 if len(args) > 1:
367 if len(args) > 1:
367 # i18n: "latesttag" is a keyword
368 # i18n: "latesttag" is a keyword
368 raise error.ParseError(_("latesttag expects at most one argument"))
369 raise error.ParseError(_("latesttag expects at most one argument"))
369
370
370 pattern = None
371 pattern = None
371 if len(args) == 1:
372 if len(args) == 1:
372 pattern = evalstring(context, mapping, args[0])
373 pattern = evalstring(context, mapping, args[0])
373 return templatekw.showlatesttags(context, mapping, pattern)
374 return templatekw.showlatesttags(context, mapping, pattern)
374
375
375 @templatefunc('localdate(date[, tz])')
376 @templatefunc('localdate(date[, tz])')
376 def localdate(context, mapping, args):
377 def localdate(context, mapping, args):
377 """Converts a date to the specified timezone.
378 """Converts a date to the specified timezone.
378 The default is local date."""
379 The default is local date."""
379 if not (1 <= len(args) <= 2):
380 if not (1 <= len(args) <= 2):
380 # i18n: "localdate" is a keyword
381 # i18n: "localdate" is a keyword
381 raise error.ParseError(_("localdate expects one or two arguments"))
382 raise error.ParseError(_("localdate expects one or two arguments"))
382
383
383 date = evaldate(context, mapping, args[0],
384 date = evaldate(context, mapping, args[0],
384 # i18n: "localdate" is a keyword
385 # i18n: "localdate" is a keyword
385 _("localdate expects a date information"))
386 _("localdate expects a date information"))
386 if len(args) >= 2:
387 if len(args) >= 2:
387 tzoffset = None
388 tzoffset = None
388 tz = evalfuncarg(context, mapping, args[1])
389 tz = evalfuncarg(context, mapping, args[1])
389 if isinstance(tz, bytes):
390 if isinstance(tz, bytes):
390 tzoffset, remainder = dateutil.parsetimezone(tz)
391 tzoffset, remainder = dateutil.parsetimezone(tz)
391 if remainder:
392 if remainder:
392 tzoffset = None
393 tzoffset = None
393 if tzoffset is None:
394 if tzoffset is None:
394 try:
395 try:
395 tzoffset = int(tz)
396 tzoffset = int(tz)
396 except (TypeError, ValueError):
397 except (TypeError, ValueError):
397 # i18n: "localdate" is a keyword
398 # i18n: "localdate" is a keyword
398 raise error.ParseError(_("localdate expects a timezone"))
399 raise error.ParseError(_("localdate expects a timezone"))
399 else:
400 else:
400 tzoffset = dateutil.makedate()[1]
401 tzoffset = dateutil.makedate()[1]
401 return (date[0], tzoffset)
402 return (date[0], tzoffset)
402
403
403 @templatefunc('max(iterable)')
404 @templatefunc('max(iterable)')
404 def max_(context, mapping, args, **kwargs):
405 def max_(context, mapping, args, **kwargs):
405 """Return the max of an iterable"""
406 """Return the max of an iterable"""
406 if len(args) != 1:
407 if len(args) != 1:
407 # i18n: "max" is a keyword
408 # i18n: "max" is a keyword
408 raise error.ParseError(_("max expects one argument"))
409 raise error.ParseError(_("max expects one argument"))
409
410
410 iterable = evalfuncarg(context, mapping, args[0])
411 iterable = evalfuncarg(context, mapping, args[0])
411 try:
412 try:
412 x = max(pycompat.maybebytestr(iterable))
413 x = max(pycompat.maybebytestr(iterable))
413 except (TypeError, ValueError):
414 except (TypeError, ValueError):
414 # i18n: "max" is a keyword
415 # i18n: "max" is a keyword
415 raise error.ParseError(_("max first argument should be an iterable"))
416 raise error.ParseError(_("max first argument should be an iterable"))
416 return templateutil.wraphybridvalue(iterable, x, x)
417 return templateutil.wraphybridvalue(iterable, x, x)
417
418
418 @templatefunc('min(iterable)')
419 @templatefunc('min(iterable)')
419 def min_(context, mapping, args, **kwargs):
420 def min_(context, mapping, args, **kwargs):
420 """Return the min of an iterable"""
421 """Return the min of an iterable"""
421 if len(args) != 1:
422 if len(args) != 1:
422 # i18n: "min" is a keyword
423 # i18n: "min" is a keyword
423 raise error.ParseError(_("min expects one argument"))
424 raise error.ParseError(_("min expects one argument"))
424
425
425 iterable = evalfuncarg(context, mapping, args[0])
426 iterable = evalfuncarg(context, mapping, args[0])
426 try:
427 try:
427 x = min(pycompat.maybebytestr(iterable))
428 x = min(pycompat.maybebytestr(iterable))
428 except (TypeError, ValueError):
429 except (TypeError, ValueError):
429 # i18n: "min" is a keyword
430 # i18n: "min" is a keyword
430 raise error.ParseError(_("min first argument should be an iterable"))
431 raise error.ParseError(_("min first argument should be an iterable"))
431 return templateutil.wraphybridvalue(iterable, x, x)
432 return templateutil.wraphybridvalue(iterable, x, x)
432
433
433 @templatefunc('mod(a, b)')
434 @templatefunc('mod(a, b)')
434 def mod(context, mapping, args):
435 def mod(context, mapping, args):
435 """Calculate a mod b such that a / b + a mod b == a"""
436 """Calculate a mod b such that a / b + a mod b == a"""
436 if not len(args) == 2:
437 if not len(args) == 2:
437 # i18n: "mod" is a keyword
438 # i18n: "mod" is a keyword
438 raise error.ParseError(_("mod expects two arguments"))
439 raise error.ParseError(_("mod expects two arguments"))
439
440
440 func = lambda a, b: a % b
441 func = lambda a, b: a % b
441 return templateutil.runarithmetic(context, mapping,
442 return templateutil.runarithmetic(context, mapping,
442 (func, args[0], args[1]))
443 (func, args[0], args[1]))
443
444
444 @templatefunc('obsfateoperations(markers)')
445 @templatefunc('obsfateoperations(markers)')
445 def obsfateoperations(context, mapping, args):
446 def obsfateoperations(context, mapping, args):
446 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
447 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
447 if len(args) != 1:
448 if len(args) != 1:
448 # i18n: "obsfateoperations" is a keyword
449 # i18n: "obsfateoperations" is a keyword
449 raise error.ParseError(_("obsfateoperations expects one argument"))
450 raise error.ParseError(_("obsfateoperations expects one argument"))
450
451
451 markers = evalfuncarg(context, mapping, args[0])
452 markers = evalfuncarg(context, mapping, args[0])
452
453
453 try:
454 try:
454 data = obsutil.markersoperations(markers)
455 data = obsutil.markersoperations(markers)
455 return templateutil.hybridlist(data, name='operation')
456 return templateutil.hybridlist(data, name='operation')
456 except (TypeError, KeyError):
457 except (TypeError, KeyError):
457 # i18n: "obsfateoperations" is a keyword
458 # i18n: "obsfateoperations" is a keyword
458 errmsg = _("obsfateoperations first argument should be an iterable")
459 errmsg = _("obsfateoperations first argument should be an iterable")
459 raise error.ParseError(errmsg)
460 raise error.ParseError(errmsg)
460
461
461 @templatefunc('obsfatedate(markers)')
462 @templatefunc('obsfatedate(markers)')
462 def obsfatedate(context, mapping, args):
463 def obsfatedate(context, mapping, args):
463 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
464 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
464 if len(args) != 1:
465 if len(args) != 1:
465 # i18n: "obsfatedate" is a keyword
466 # i18n: "obsfatedate" is a keyword
466 raise error.ParseError(_("obsfatedate expects one argument"))
467 raise error.ParseError(_("obsfatedate expects one argument"))
467
468
468 markers = evalfuncarg(context, mapping, args[0])
469 markers = evalfuncarg(context, mapping, args[0])
469
470
470 try:
471 try:
471 data = obsutil.markersdates(markers)
472 data = obsutil.markersdates(markers)
472 return templateutil.hybridlist(data, name='date', fmt='%d %d')
473 return templateutil.hybridlist(data, name='date', fmt='%d %d')
473 except (TypeError, KeyError):
474 except (TypeError, KeyError):
474 # i18n: "obsfatedate" is a keyword
475 # i18n: "obsfatedate" is a keyword
475 errmsg = _("obsfatedate first argument should be an iterable")
476 errmsg = _("obsfatedate first argument should be an iterable")
476 raise error.ParseError(errmsg)
477 raise error.ParseError(errmsg)
477
478
478 @templatefunc('obsfateusers(markers)')
479 @templatefunc('obsfateusers(markers)')
479 def obsfateusers(context, mapping, args):
480 def obsfateusers(context, mapping, args):
480 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
481 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
481 if len(args) != 1:
482 if len(args) != 1:
482 # i18n: "obsfateusers" is a keyword
483 # i18n: "obsfateusers" is a keyword
483 raise error.ParseError(_("obsfateusers expects one argument"))
484 raise error.ParseError(_("obsfateusers expects one argument"))
484
485
485 markers = evalfuncarg(context, mapping, args[0])
486 markers = evalfuncarg(context, mapping, args[0])
486
487
487 try:
488 try:
488 data = obsutil.markersusers(markers)
489 data = obsutil.markersusers(markers)
489 return templateutil.hybridlist(data, name='user')
490 return templateutil.hybridlist(data, name='user')
490 except (TypeError, KeyError, ValueError):
491 except (TypeError, KeyError, ValueError):
491 # i18n: "obsfateusers" is a keyword
492 # i18n: "obsfateusers" is a keyword
492 msg = _("obsfateusers first argument should be an iterable of "
493 msg = _("obsfateusers first argument should be an iterable of "
493 "obsmakers")
494 "obsmakers")
494 raise error.ParseError(msg)
495 raise error.ParseError(msg)
495
496
496 @templatefunc('obsfateverb(successors, markers)')
497 @templatefunc('obsfateverb(successors, markers)')
497 def obsfateverb(context, mapping, args):
498 def obsfateverb(context, mapping, args):
498 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
499 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
499 if len(args) != 2:
500 if len(args) != 2:
500 # i18n: "obsfateverb" is a keyword
501 # i18n: "obsfateverb" is a keyword
501 raise error.ParseError(_("obsfateverb expects two arguments"))
502 raise error.ParseError(_("obsfateverb expects two arguments"))
502
503
503 successors = evalfuncarg(context, mapping, args[0])
504 successors = evalfuncarg(context, mapping, args[0])
504 markers = evalfuncarg(context, mapping, args[1])
505 markers = evalfuncarg(context, mapping, args[1])
505
506
506 try:
507 try:
507 return obsutil.obsfateverb(successors, markers)
508 return obsutil.obsfateverb(successors, markers)
508 except TypeError:
509 except TypeError:
509 # i18n: "obsfateverb" is a keyword
510 # i18n: "obsfateverb" is a keyword
510 errmsg = _("obsfateverb first argument should be countable")
511 errmsg = _("obsfateverb first argument should be countable")
511 raise error.ParseError(errmsg)
512 raise error.ParseError(errmsg)
512
513
513 @templatefunc('relpath(path)')
514 @templatefunc('relpath(path)')
514 def relpath(context, mapping, args):
515 def relpath(context, mapping, args):
515 """Convert a repository-absolute path into a filesystem path relative to
516 """Convert a repository-absolute path into a filesystem path relative to
516 the current working directory."""
517 the current working directory."""
517 if len(args) != 1:
518 if len(args) != 1:
518 # i18n: "relpath" is a keyword
519 # i18n: "relpath" is a keyword
519 raise error.ParseError(_("relpath expects one argument"))
520 raise error.ParseError(_("relpath expects one argument"))
520
521
521 repo = context.resource(mapping, 'ctx').repo()
522 repo = context.resource(mapping, 'ctx').repo()
522 path = evalstring(context, mapping, args[0])
523 path = evalstring(context, mapping, args[0])
523 return repo.pathto(path)
524 return repo.pathto(path)
524
525
525 @templatefunc('revset(query[, formatargs...])')
526 @templatefunc('revset(query[, formatargs...])')
526 def revset(context, mapping, args):
527 def revset(context, mapping, args):
527 """Execute a revision set query. See
528 """Execute a revision set query. See
528 :hg:`help revset`."""
529 :hg:`help revset`."""
529 if not len(args) > 0:
530 if not len(args) > 0:
530 # i18n: "revset" is a keyword
531 # i18n: "revset" is a keyword
531 raise error.ParseError(_("revset expects one or more arguments"))
532 raise error.ParseError(_("revset expects one or more arguments"))
532
533
533 raw = evalstring(context, mapping, args[0])
534 raw = evalstring(context, mapping, args[0])
534 ctx = context.resource(mapping, 'ctx')
535 ctx = context.resource(mapping, 'ctx')
535 repo = ctx.repo()
536 repo = ctx.repo()
536
537
537 def query(expr):
538 def query(expr):
538 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
539 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
539 return m(repo)
540 return m(repo)
540
541
541 if len(args) > 1:
542 if len(args) > 1:
542 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
543 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
543 revs = query(revsetlang.formatspec(raw, *formatargs))
544 revs = query(revsetlang.formatspec(raw, *formatargs))
544 revs = list(revs)
545 revs = list(revs)
545 else:
546 else:
546 cache = context.resource(mapping, 'cache')
547 cache = context.resource(mapping, 'cache')
547 revsetcache = cache.setdefault("revsetcache", {})
548 revsetcache = cache.setdefault("revsetcache", {})
548 if raw in revsetcache:
549 if raw in revsetcache:
549 revs = revsetcache[raw]
550 revs = revsetcache[raw]
550 else:
551 else:
551 revs = query(raw)
552 revs = query(raw)
552 revs = list(revs)
553 revs = list(revs)
553 revsetcache[raw] = revs
554 revsetcache[raw] = revs
554 return templatekw.showrevslist(context, mapping, "revision", revs)
555 return templatekw.showrevslist(context, mapping, "revision", revs)
555
556
556 @templatefunc('rstdoc(text, style)')
557 @templatefunc('rstdoc(text, style)')
557 def rstdoc(context, mapping, args):
558 def rstdoc(context, mapping, args):
558 """Format reStructuredText."""
559 """Format reStructuredText."""
559 if len(args) != 2:
560 if len(args) != 2:
560 # i18n: "rstdoc" is a keyword
561 # i18n: "rstdoc" is a keyword
561 raise error.ParseError(_("rstdoc expects two arguments"))
562 raise error.ParseError(_("rstdoc expects two arguments"))
562
563
563 text = evalstring(context, mapping, args[0])
564 text = evalstring(context, mapping, args[0])
564 style = evalstring(context, mapping, args[1])
565 style = evalstring(context, mapping, args[1])
565
566
566 return minirst.format(text, style=style, keep=['verbose'])
567 return minirst.format(text, style=style, keep=['verbose'])
567
568
568 @templatefunc('separate(sep, args)', argspec='sep *args')
569 @templatefunc('separate(sep, args)', argspec='sep *args')
569 def separate(context, mapping, args):
570 def separate(context, mapping, args):
570 """Add a separator between non-empty arguments."""
571 """Add a separator between non-empty arguments."""
571 if 'sep' not in args:
572 if 'sep' not in args:
572 # i18n: "separate" is a keyword
573 # i18n: "separate" is a keyword
573 raise error.ParseError(_("separate expects at least one argument"))
574 raise error.ParseError(_("separate expects at least one argument"))
574
575
575 sep = evalstring(context, mapping, args['sep'])
576 sep = evalstring(context, mapping, args['sep'])
576 first = True
577 first = True
577 for arg in args['args']:
578 for arg in args['args']:
578 argstr = evalstring(context, mapping, arg)
579 argstr = evalstring(context, mapping, arg)
579 if not argstr:
580 if not argstr:
580 continue
581 continue
581 if first:
582 if first:
582 first = False
583 first = False
583 else:
584 else:
584 yield sep
585 yield sep
585 yield argstr
586 yield argstr
586
587
587 @templatefunc('shortest(node, minlength=4)')
588 @templatefunc('shortest(node, minlength=4)')
588 def shortest(context, mapping, args):
589 def shortest(context, mapping, args):
589 """Obtain the shortest representation of
590 """Obtain the shortest representation of
590 a node."""
591 a node."""
591 if not (1 <= len(args) <= 2):
592 if not (1 <= len(args) <= 2):
592 # i18n: "shortest" is a keyword
593 # i18n: "shortest" is a keyword
593 raise error.ParseError(_("shortest() expects one or two arguments"))
594 raise error.ParseError(_("shortest() expects one or two arguments"))
594
595
595 hexnode = evalstring(context, mapping, args[0])
596 hexnode = evalstring(context, mapping, args[0])
596
597
597 minlength = 4
598 minlength = 4
598 if len(args) > 1:
599 if len(args) > 1:
599 minlength = evalinteger(context, mapping, args[1],
600 minlength = evalinteger(context, mapping, args[1],
600 # i18n: "shortest" is a keyword
601 # i18n: "shortest" is a keyword
601 _("shortest() expects an integer minlength"))
602 _("shortest() expects an integer minlength"))
602
603
603 repo = context.resource(mapping, 'ctx')._repo
604 repo = context.resource(mapping, 'ctx')._repo
604 if len(hexnode) > 40:
605 if len(hexnode) > 40:
605 return hexnode
606 return hexnode
606 elif len(hexnode) == 40:
607 elif len(hexnode) == 40:
607 try:
608 try:
608 node = bin(hexnode)
609 node = bin(hexnode)
609 except TypeError:
610 except TypeError:
610 return hexnode
611 return hexnode
611 else:
612 else:
612 try:
613 try:
613 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
614 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
614 except error.WdirUnsupported:
615 except error.WdirUnsupported:
615 node = wdirid
616 node = wdirid
616 except error.LookupError:
617 except error.LookupError:
617 return hexnode
618 return hexnode
618 if not node:
619 if not node:
619 return hexnode
620 return hexnode
620 try:
621 try:
621 return scmutil.shortesthexnodeidprefix(repo, node, minlength)
622 return scmutil.shortesthexnodeidprefix(repo, node, minlength)
622 except error.RepoLookupError:
623 except error.RepoLookupError:
623 return hexnode
624 return hexnode
624
625
625 @templatefunc('strip(text[, chars])')
626 @templatefunc('strip(text[, chars])')
626 def strip(context, mapping, args):
627 def strip(context, mapping, args):
627 """Strip characters from a string. By default,
628 """Strip characters from a string. By default,
628 strips all leading and trailing whitespace."""
629 strips all leading and trailing whitespace."""
629 if not (1 <= len(args) <= 2):
630 if not (1 <= len(args) <= 2):
630 # i18n: "strip" is a keyword
631 # i18n: "strip" is a keyword
631 raise error.ParseError(_("strip expects one or two arguments"))
632 raise error.ParseError(_("strip expects one or two arguments"))
632
633
633 text = evalstring(context, mapping, args[0])
634 text = evalstring(context, mapping, args[0])
634 if len(args) == 2:
635 if len(args) == 2:
635 chars = evalstring(context, mapping, args[1])
636 chars = evalstring(context, mapping, args[1])
636 return text.strip(chars)
637 return text.strip(chars)
637 return text.strip()
638 return text.strip()
638
639
639 @templatefunc('sub(pattern, replacement, expression)')
640 @templatefunc('sub(pattern, replacement, expression)')
640 def sub(context, mapping, args):
641 def sub(context, mapping, args):
641 """Perform text substitution
642 """Perform text substitution
642 using regular expressions."""
643 using regular expressions."""
643 if len(args) != 3:
644 if len(args) != 3:
644 # i18n: "sub" is a keyword
645 # i18n: "sub" is a keyword
645 raise error.ParseError(_("sub expects three arguments"))
646 raise error.ParseError(_("sub expects three arguments"))
646
647
647 pat = evalstring(context, mapping, args[0])
648 pat = evalstring(context, mapping, args[0])
648 rpl = evalstring(context, mapping, args[1])
649 rpl = evalstring(context, mapping, args[1])
649 src = evalstring(context, mapping, args[2])
650 src = evalstring(context, mapping, args[2])
650 try:
651 try:
651 patre = re.compile(pat)
652 patre = re.compile(pat)
652 except re.error:
653 except re.error:
653 # i18n: "sub" is a keyword
654 # i18n: "sub" is a keyword
654 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
655 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
655 try:
656 try:
656 yield patre.sub(rpl, src)
657 yield patre.sub(rpl, src)
657 except re.error:
658 except re.error:
658 # i18n: "sub" is a keyword
659 # i18n: "sub" is a keyword
659 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
660 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
660
661
661 @templatefunc('startswith(pattern, text)')
662 @templatefunc('startswith(pattern, text)')
662 def startswith(context, mapping, args):
663 def startswith(context, mapping, args):
663 """Returns the value from the "text" argument
664 """Returns the value from the "text" argument
664 if it begins with the content from the "pattern" argument."""
665 if it begins with the content from the "pattern" argument."""
665 if len(args) != 2:
666 if len(args) != 2:
666 # i18n: "startswith" is a keyword
667 # i18n: "startswith" is a keyword
667 raise error.ParseError(_("startswith expects two arguments"))
668 raise error.ParseError(_("startswith expects two arguments"))
668
669
669 patn = evalstring(context, mapping, args[0])
670 patn = evalstring(context, mapping, args[0])
670 text = evalstring(context, mapping, args[1])
671 text = evalstring(context, mapping, args[1])
671 if text.startswith(patn):
672 if text.startswith(patn):
672 return text
673 return text
673 return ''
674 return ''
674
675
675 @templatefunc('word(number, text[, separator])')
676 @templatefunc('word(number, text[, separator])')
676 def word(context, mapping, args):
677 def word(context, mapping, args):
677 """Return the nth word from a string."""
678 """Return the nth word from a string."""
678 if not (2 <= len(args) <= 3):
679 if not (2 <= len(args) <= 3):
679 # i18n: "word" is a keyword
680 # i18n: "word" is a keyword
680 raise error.ParseError(_("word expects two or three arguments, got %d")
681 raise error.ParseError(_("word expects two or three arguments, got %d")
681 % len(args))
682 % len(args))
682
683
683 num = evalinteger(context, mapping, args[0],
684 num = evalinteger(context, mapping, args[0],
684 # i18n: "word" is a keyword
685 # i18n: "word" is a keyword
685 _("word expects an integer index"))
686 _("word expects an integer index"))
686 text = evalstring(context, mapping, args[1])
687 text = evalstring(context, mapping, args[1])
687 if len(args) == 3:
688 if len(args) == 3:
688 splitter = evalstring(context, mapping, args[2])
689 splitter = evalstring(context, mapping, args[2])
689 else:
690 else:
690 splitter = None
691 splitter = None
691
692
692 tokens = text.split(splitter)
693 tokens = text.split(splitter)
693 if num >= len(tokens) or num < -len(tokens):
694 if num >= len(tokens) or num < -len(tokens):
694 return ''
695 return ''
695 else:
696 else:
696 return tokens[num]
697 return tokens[num]
697
698
698 def loadfunction(ui, extname, registrarobj):
699 def loadfunction(ui, extname, registrarobj):
699 """Load template function from specified registrarobj
700 """Load template function from specified registrarobj
700 """
701 """
701 for name, func in registrarobj._table.iteritems():
702 for name, func in registrarobj._table.iteritems():
702 funcs[name] = func
703 funcs[name] = func
703
704
704 # tell hggettext to extract docstrings from these functions:
705 # tell hggettext to extract docstrings from these functions:
705 i18nfunctions = funcs.values()
706 i18nfunctions = funcs.values()
@@ -1,924 +1,921 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
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 """Slightly complicated template engine for commands and hgweb
8 """Slightly complicated template engine for commands and hgweb
9
9
10 This module provides low-level interface to the template engine. See the
10 This module provides low-level interface to the template engine. See the
11 formatter and cmdutil modules if you are looking for high-level functions
11 formatter and cmdutil modules if you are looking for high-level functions
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
13
14 Internal Data Types
14 Internal Data Types
15 -------------------
15 -------------------
16
16
17 Template keywords and functions take a dictionary of current symbols and
17 Template keywords and functions take a dictionary of current symbols and
18 resources (a "mapping") and return result. Inputs and outputs must be one
18 resources (a "mapping") and return result. Inputs and outputs must be one
19 of the following data types:
19 of the following data types:
20
20
21 bytes
21 bytes
22 a byte string, which is generally a human-readable text in local encoding.
22 a byte string, which is generally a human-readable text in local encoding.
23
23
24 generator
24 generator
25 a lazily-evaluated byte string, which is a possibly nested generator of
25 a lazily-evaluated byte string, which is a possibly nested generator of
26 values of any printable types, and will be folded by ``stringify()``
26 values of any printable types, and will be folded by ``stringify()``
27 or ``flatten()``.
27 or ``flatten()``.
28
28
29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 returns a generator of dicts.)
31
32 None
29 None
33 sometimes represents an empty value, which can be stringified to ''.
30 sometimes represents an empty value, which can be stringified to ''.
34
31
35 True, False, int, float
32 True, False, int, float
36 can be stringified as such.
33 can be stringified as such.
37
34
38 date tuple
35 date tuple
39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
36 a (unixtime, offset) tuple, which produces no meaningful output by itself.
40
37
41 hybrid
38 hybrid
42 represents a list/dict of printable values, which can also be converted
39 represents a list/dict of printable values, which can also be converted
43 to mappings by % operator.
40 to mappings by % operator.
44
41
45 mappable
42 mappable
46 represents a scalar printable value, also supports % operator.
43 represents a scalar printable value, also supports % operator.
47
44
48 mappinggenerator, mappinglist
45 mappinggenerator, mappinglist
49 represents mappings (i.e. a list of dicts), which may have default
46 represents mappings (i.e. a list of dicts), which may have default
50 output format.
47 output format.
51
48
52 mappedgenerator
49 mappedgenerator
53 a lazily-evaluated list of byte strings, which is e.g. a result of %
50 a lazily-evaluated list of byte strings, which is e.g. a result of %
54 operation.
51 operation.
55 """
52 """
56
53
57 from __future__ import absolute_import, print_function
54 from __future__ import absolute_import, print_function
58
55
59 import abc
56 import abc
60 import os
57 import os
61
58
62 from .i18n import _
59 from .i18n import _
63 from . import (
60 from . import (
64 config,
61 config,
65 encoding,
62 encoding,
66 error,
63 error,
67 parser,
64 parser,
68 pycompat,
65 pycompat,
69 templatefilters,
66 templatefilters,
70 templatefuncs,
67 templatefuncs,
71 templateutil,
68 templateutil,
72 util,
69 util,
73 )
70 )
74 from .utils import (
71 from .utils import (
75 stringutil,
72 stringutil,
76 )
73 )
77
74
78 # template parsing
75 # template parsing
79
76
80 elements = {
77 elements = {
81 # token-type: binding-strength, primary, prefix, infix, suffix
78 # token-type: binding-strength, primary, prefix, infix, suffix
82 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
79 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
83 ".": (18, None, None, (".", 18), None),
80 ".": (18, None, None, (".", 18), None),
84 "%": (15, None, None, ("%", 15), None),
81 "%": (15, None, None, ("%", 15), None),
85 "|": (15, None, None, ("|", 15), None),
82 "|": (15, None, None, ("|", 15), None),
86 "*": (5, None, None, ("*", 5), None),
83 "*": (5, None, None, ("*", 5), None),
87 "/": (5, None, None, ("/", 5), None),
84 "/": (5, None, None, ("/", 5), None),
88 "+": (4, None, None, ("+", 4), None),
85 "+": (4, None, None, ("+", 4), None),
89 "-": (4, None, ("negate", 19), ("-", 4), None),
86 "-": (4, None, ("negate", 19), ("-", 4), None),
90 "=": (3, None, None, ("keyvalue", 3), None),
87 "=": (3, None, None, ("keyvalue", 3), None),
91 ",": (2, None, None, ("list", 2), None),
88 ",": (2, None, None, ("list", 2), None),
92 ")": (0, None, None, None, None),
89 ")": (0, None, None, None, None),
93 "integer": (0, "integer", None, None, None),
90 "integer": (0, "integer", None, None, None),
94 "symbol": (0, "symbol", None, None, None),
91 "symbol": (0, "symbol", None, None, None),
95 "string": (0, "string", None, None, None),
92 "string": (0, "string", None, None, None),
96 "template": (0, "template", None, None, None),
93 "template": (0, "template", None, None, None),
97 "end": (0, None, None, None, None),
94 "end": (0, None, None, None, None),
98 }
95 }
99
96
100 def tokenize(program, start, end, term=None):
97 def tokenize(program, start, end, term=None):
101 """Parse a template expression into a stream of tokens, which must end
98 """Parse a template expression into a stream of tokens, which must end
102 with term if specified"""
99 with term if specified"""
103 pos = start
100 pos = start
104 program = pycompat.bytestr(program)
101 program = pycompat.bytestr(program)
105 while pos < end:
102 while pos < end:
106 c = program[pos]
103 c = program[pos]
107 if c.isspace(): # skip inter-token whitespace
104 if c.isspace(): # skip inter-token whitespace
108 pass
105 pass
109 elif c in "(=,).%|+-*/": # handle simple operators
106 elif c in "(=,).%|+-*/": # handle simple operators
110 yield (c, None, pos)
107 yield (c, None, pos)
111 elif c in '"\'': # handle quoted templates
108 elif c in '"\'': # handle quoted templates
112 s = pos + 1
109 s = pos + 1
113 data, pos = _parsetemplate(program, s, end, c)
110 data, pos = _parsetemplate(program, s, end, c)
114 yield ('template', data, s)
111 yield ('template', data, s)
115 pos -= 1
112 pos -= 1
116 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
113 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
117 # handle quoted strings
114 # handle quoted strings
118 c = program[pos + 1]
115 c = program[pos + 1]
119 s = pos = pos + 2
116 s = pos = pos + 2
120 while pos < end: # find closing quote
117 while pos < end: # find closing quote
121 d = program[pos]
118 d = program[pos]
122 if d == '\\': # skip over escaped characters
119 if d == '\\': # skip over escaped characters
123 pos += 2
120 pos += 2
124 continue
121 continue
125 if d == c:
122 if d == c:
126 yield ('string', program[s:pos], s)
123 yield ('string', program[s:pos], s)
127 break
124 break
128 pos += 1
125 pos += 1
129 else:
126 else:
130 raise error.ParseError(_("unterminated string"), s)
127 raise error.ParseError(_("unterminated string"), s)
131 elif c.isdigit():
128 elif c.isdigit():
132 s = pos
129 s = pos
133 while pos < end:
130 while pos < end:
134 d = program[pos]
131 d = program[pos]
135 if not d.isdigit():
132 if not d.isdigit():
136 break
133 break
137 pos += 1
134 pos += 1
138 yield ('integer', program[s:pos], s)
135 yield ('integer', program[s:pos], s)
139 pos -= 1
136 pos -= 1
140 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
137 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
141 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
138 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
142 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
139 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
143 # where some of nested templates were preprocessed as strings and
140 # where some of nested templates were preprocessed as strings and
144 # then compiled. therefore, \"...\" was allowed. (issue4733)
141 # then compiled. therefore, \"...\" was allowed. (issue4733)
145 #
142 #
146 # processing flow of _evalifliteral() at 5ab28a2e9962:
143 # processing flow of _evalifliteral() at 5ab28a2e9962:
147 # outer template string -> stringify() -> compiletemplate()
144 # outer template string -> stringify() -> compiletemplate()
148 # ------------------------ ------------ ------------------
145 # ------------------------ ------------ ------------------
149 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
146 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
150 # ~~~~~~~~
147 # ~~~~~~~~
151 # escaped quoted string
148 # escaped quoted string
152 if c == 'r':
149 if c == 'r':
153 pos += 1
150 pos += 1
154 token = 'string'
151 token = 'string'
155 else:
152 else:
156 token = 'template'
153 token = 'template'
157 quote = program[pos:pos + 2]
154 quote = program[pos:pos + 2]
158 s = pos = pos + 2
155 s = pos = pos + 2
159 while pos < end: # find closing escaped quote
156 while pos < end: # find closing escaped quote
160 if program.startswith('\\\\\\', pos, end):
157 if program.startswith('\\\\\\', pos, end):
161 pos += 4 # skip over double escaped characters
158 pos += 4 # skip over double escaped characters
162 continue
159 continue
163 if program.startswith(quote, pos, end):
160 if program.startswith(quote, pos, end):
164 # interpret as if it were a part of an outer string
161 # interpret as if it were a part of an outer string
165 data = parser.unescapestr(program[s:pos])
162 data = parser.unescapestr(program[s:pos])
166 if token == 'template':
163 if token == 'template':
167 data = _parsetemplate(data, 0, len(data))[0]
164 data = _parsetemplate(data, 0, len(data))[0]
168 yield (token, data, s)
165 yield (token, data, s)
169 pos += 1
166 pos += 1
170 break
167 break
171 pos += 1
168 pos += 1
172 else:
169 else:
173 raise error.ParseError(_("unterminated string"), s)
170 raise error.ParseError(_("unterminated string"), s)
174 elif c.isalnum() or c in '_':
171 elif c.isalnum() or c in '_':
175 s = pos
172 s = pos
176 pos += 1
173 pos += 1
177 while pos < end: # find end of symbol
174 while pos < end: # find end of symbol
178 d = program[pos]
175 d = program[pos]
179 if not (d.isalnum() or d == "_"):
176 if not (d.isalnum() or d == "_"):
180 break
177 break
181 pos += 1
178 pos += 1
182 sym = program[s:pos]
179 sym = program[s:pos]
183 yield ('symbol', sym, s)
180 yield ('symbol', sym, s)
184 pos -= 1
181 pos -= 1
185 elif c == term:
182 elif c == term:
186 yield ('end', None, pos)
183 yield ('end', None, pos)
187 return
184 return
188 else:
185 else:
189 raise error.ParseError(_("syntax error"), pos)
186 raise error.ParseError(_("syntax error"), pos)
190 pos += 1
187 pos += 1
191 if term:
188 if term:
192 raise error.ParseError(_("unterminated template expansion"), start)
189 raise error.ParseError(_("unterminated template expansion"), start)
193 yield ('end', None, pos)
190 yield ('end', None, pos)
194
191
195 def _parsetemplate(tmpl, start, stop, quote=''):
192 def _parsetemplate(tmpl, start, stop, quote=''):
196 r"""
193 r"""
197 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
194 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
198 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
195 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
199 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
196 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
200 ([('string', 'foo'), ('symbol', 'bar')], 9)
197 ([('string', 'foo'), ('symbol', 'bar')], 9)
201 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
198 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
202 ([('string', 'foo')], 4)
199 ([('string', 'foo')], 4)
203 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
200 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
204 ([('string', 'foo"'), ('string', 'bar')], 9)
201 ([('string', 'foo"'), ('string', 'bar')], 9)
205 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
202 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
206 ([('string', 'foo\\')], 6)
203 ([('string', 'foo\\')], 6)
207 """
204 """
208 parsed = []
205 parsed = []
209 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
206 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
210 if typ == 'string':
207 if typ == 'string':
211 parsed.append((typ, val))
208 parsed.append((typ, val))
212 elif typ == 'template':
209 elif typ == 'template':
213 parsed.append(val)
210 parsed.append(val)
214 elif typ == 'end':
211 elif typ == 'end':
215 return parsed, pos
212 return parsed, pos
216 else:
213 else:
217 raise error.ProgrammingError('unexpected type: %s' % typ)
214 raise error.ProgrammingError('unexpected type: %s' % typ)
218 raise error.ProgrammingError('unterminated scanning of template')
215 raise error.ProgrammingError('unterminated scanning of template')
219
216
220 def scantemplate(tmpl, raw=False):
217 def scantemplate(tmpl, raw=False):
221 r"""Scan (type, start, end) positions of outermost elements in template
218 r"""Scan (type, start, end) positions of outermost elements in template
222
219
223 If raw=True, a backslash is not taken as an escape character just like
220 If raw=True, a backslash is not taken as an escape character just like
224 r'' string in Python. Note that this is different from r'' literal in
221 r'' string in Python. Note that this is different from r'' literal in
225 template in that no template fragment can appear in r'', e.g. r'{foo}'
222 template in that no template fragment can appear in r'', e.g. r'{foo}'
226 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
223 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
227 'foo'.
224 'foo'.
228
225
229 >>> list(scantemplate(b'foo{bar}"baz'))
226 >>> list(scantemplate(b'foo{bar}"baz'))
230 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
227 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
231 >>> list(scantemplate(b'outer{"inner"}outer'))
228 >>> list(scantemplate(b'outer{"inner"}outer'))
232 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
229 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
233 >>> list(scantemplate(b'foo\\{escaped}'))
230 >>> list(scantemplate(b'foo\\{escaped}'))
234 [('string', 0, 5), ('string', 5, 13)]
231 [('string', 0, 5), ('string', 5, 13)]
235 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
232 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
236 [('string', 0, 4), ('template', 4, 13)]
233 [('string', 0, 4), ('template', 4, 13)]
237 """
234 """
238 last = None
235 last = None
239 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
236 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
240 if last:
237 if last:
241 yield last + (pos,)
238 yield last + (pos,)
242 if typ == 'end':
239 if typ == 'end':
243 return
240 return
244 else:
241 else:
245 last = (typ, pos)
242 last = (typ, pos)
246 raise error.ProgrammingError('unterminated scanning of template')
243 raise error.ProgrammingError('unterminated scanning of template')
247
244
248 def _scantemplate(tmpl, start, stop, quote='', raw=False):
245 def _scantemplate(tmpl, start, stop, quote='', raw=False):
249 """Parse template string into chunks of strings and template expressions"""
246 """Parse template string into chunks of strings and template expressions"""
250 sepchars = '{' + quote
247 sepchars = '{' + quote
251 unescape = [parser.unescapestr, pycompat.identity][raw]
248 unescape = [parser.unescapestr, pycompat.identity][raw]
252 pos = start
249 pos = start
253 p = parser.parser(elements)
250 p = parser.parser(elements)
254 try:
251 try:
255 while pos < stop:
252 while pos < stop:
256 n = min((tmpl.find(c, pos, stop)
253 n = min((tmpl.find(c, pos, stop)
257 for c in pycompat.bytestr(sepchars)),
254 for c in pycompat.bytestr(sepchars)),
258 key=lambda n: (n < 0, n))
255 key=lambda n: (n < 0, n))
259 if n < 0:
256 if n < 0:
260 yield ('string', unescape(tmpl[pos:stop]), pos)
257 yield ('string', unescape(tmpl[pos:stop]), pos)
261 pos = stop
258 pos = stop
262 break
259 break
263 c = tmpl[n:n + 1]
260 c = tmpl[n:n + 1]
264 bs = 0 # count leading backslashes
261 bs = 0 # count leading backslashes
265 if not raw:
262 if not raw:
266 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
263 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
267 if bs % 2 == 1:
264 if bs % 2 == 1:
268 # escaped (e.g. '\{', '\\\{', but not '\\{')
265 # escaped (e.g. '\{', '\\\{', but not '\\{')
269 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
266 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
270 pos = n + 1
267 pos = n + 1
271 continue
268 continue
272 if n > pos:
269 if n > pos:
273 yield ('string', unescape(tmpl[pos:n]), pos)
270 yield ('string', unescape(tmpl[pos:n]), pos)
274 if c == quote:
271 if c == quote:
275 yield ('end', None, n + 1)
272 yield ('end', None, n + 1)
276 return
273 return
277
274
278 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
275 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
279 if not tmpl.startswith('}', pos):
276 if not tmpl.startswith('}', pos):
280 raise error.ParseError(_("invalid token"), pos)
277 raise error.ParseError(_("invalid token"), pos)
281 yield ('template', parseres, n)
278 yield ('template', parseres, n)
282 pos += 1
279 pos += 1
283
280
284 if quote:
281 if quote:
285 raise error.ParseError(_("unterminated string"), start)
282 raise error.ParseError(_("unterminated string"), start)
286 except error.ParseError as inst:
283 except error.ParseError as inst:
287 if len(inst.args) > 1: # has location
284 if len(inst.args) > 1: # has location
288 loc = inst.args[1]
285 loc = inst.args[1]
289 # Offset the caret location by the number of newlines before the
286 # Offset the caret location by the number of newlines before the
290 # location of the error, since we will replace one-char newlines
287 # location of the error, since we will replace one-char newlines
291 # with the two-char literal r'\n'.
288 # with the two-char literal r'\n'.
292 offset = tmpl[:loc].count('\n')
289 offset = tmpl[:loc].count('\n')
293 tmpl = tmpl.replace('\n', br'\n')
290 tmpl = tmpl.replace('\n', br'\n')
294 # We want the caret to point to the place in the template that
291 # We want the caret to point to the place in the template that
295 # failed to parse, but in a hint we get a open paren at the
292 # failed to parse, but in a hint we get a open paren at the
296 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
293 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
297 # to line up the caret with the location of the error.
294 # to line up the caret with the location of the error.
298 inst.hint = (tmpl + '\n'
295 inst.hint = (tmpl + '\n'
299 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
296 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
300 raise
297 raise
301 yield ('end', None, pos)
298 yield ('end', None, pos)
302
299
303 def _unnesttemplatelist(tree):
300 def _unnesttemplatelist(tree):
304 """Expand list of templates to node tuple
301 """Expand list of templates to node tuple
305
302
306 >>> def f(tree):
303 >>> def f(tree):
307 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
304 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
308 >>> f((b'template', []))
305 >>> f((b'template', []))
309 (string '')
306 (string '')
310 >>> f((b'template', [(b'string', b'foo')]))
307 >>> f((b'template', [(b'string', b'foo')]))
311 (string 'foo')
308 (string 'foo')
312 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
309 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
313 (template
310 (template
314 (string 'foo')
311 (string 'foo')
315 (symbol 'rev'))
312 (symbol 'rev'))
316 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
313 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
317 (template
314 (template
318 (symbol 'rev'))
315 (symbol 'rev'))
319 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
316 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
320 (string 'foo')
317 (string 'foo')
321 """
318 """
322 if not isinstance(tree, tuple):
319 if not isinstance(tree, tuple):
323 return tree
320 return tree
324 op = tree[0]
321 op = tree[0]
325 if op != 'template':
322 if op != 'template':
326 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
323 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
327
324
328 assert len(tree) == 2
325 assert len(tree) == 2
329 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
326 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
330 if not xs:
327 if not xs:
331 return ('string', '') # empty template ""
328 return ('string', '') # empty template ""
332 elif len(xs) == 1 and xs[0][0] == 'string':
329 elif len(xs) == 1 and xs[0][0] == 'string':
333 return xs[0] # fast path for string with no template fragment "x"
330 return xs[0] # fast path for string with no template fragment "x"
334 else:
331 else:
335 return (op,) + xs
332 return (op,) + xs
336
333
337 def parse(tmpl):
334 def parse(tmpl):
338 """Parse template string into tree"""
335 """Parse template string into tree"""
339 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
336 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
340 assert pos == len(tmpl), 'unquoted template should be consumed'
337 assert pos == len(tmpl), 'unquoted template should be consumed'
341 return _unnesttemplatelist(('template', parsed))
338 return _unnesttemplatelist(('template', parsed))
342
339
343 def _parseexpr(expr):
340 def _parseexpr(expr):
344 """Parse a template expression into tree
341 """Parse a template expression into tree
345
342
346 >>> _parseexpr(b'"foo"')
343 >>> _parseexpr(b'"foo"')
347 ('string', 'foo')
344 ('string', 'foo')
348 >>> _parseexpr(b'foo(bar)')
345 >>> _parseexpr(b'foo(bar)')
349 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
346 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
350 >>> _parseexpr(b'foo(')
347 >>> _parseexpr(b'foo(')
351 Traceback (most recent call last):
348 Traceback (most recent call last):
352 ...
349 ...
353 ParseError: ('not a prefix: end', 4)
350 ParseError: ('not a prefix: end', 4)
354 >>> _parseexpr(b'"foo" "bar"')
351 >>> _parseexpr(b'"foo" "bar"')
355 Traceback (most recent call last):
352 Traceback (most recent call last):
356 ...
353 ...
357 ParseError: ('invalid token', 7)
354 ParseError: ('invalid token', 7)
358 """
355 """
359 p = parser.parser(elements)
356 p = parser.parser(elements)
360 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
357 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
361 if pos != len(expr):
358 if pos != len(expr):
362 raise error.ParseError(_('invalid token'), pos)
359 raise error.ParseError(_('invalid token'), pos)
363 return _unnesttemplatelist(tree)
360 return _unnesttemplatelist(tree)
364
361
365 def prettyformat(tree):
362 def prettyformat(tree):
366 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
363 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
367
364
368 def compileexp(exp, context, curmethods):
365 def compileexp(exp, context, curmethods):
369 """Compile parsed template tree to (func, data) pair"""
366 """Compile parsed template tree to (func, data) pair"""
370 if not exp:
367 if not exp:
371 raise error.ParseError(_("missing argument"))
368 raise error.ParseError(_("missing argument"))
372 t = exp[0]
369 t = exp[0]
373 if t in curmethods:
370 if t in curmethods:
374 return curmethods[t](exp, context)
371 return curmethods[t](exp, context)
375 raise error.ParseError(_("unknown method '%s'") % t)
372 raise error.ParseError(_("unknown method '%s'") % t)
376
373
377 # template evaluation
374 # template evaluation
378
375
379 def getsymbol(exp):
376 def getsymbol(exp):
380 if exp[0] == 'symbol':
377 if exp[0] == 'symbol':
381 return exp[1]
378 return exp[1]
382 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
379 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
383
380
384 def getlist(x):
381 def getlist(x):
385 if not x:
382 if not x:
386 return []
383 return []
387 if x[0] == 'list':
384 if x[0] == 'list':
388 return getlist(x[1]) + [x[2]]
385 return getlist(x[1]) + [x[2]]
389 return [x]
386 return [x]
390
387
391 def gettemplate(exp, context):
388 def gettemplate(exp, context):
392 """Compile given template tree or load named template from map file;
389 """Compile given template tree or load named template from map file;
393 returns (func, data) pair"""
390 returns (func, data) pair"""
394 if exp[0] in ('template', 'string'):
391 if exp[0] in ('template', 'string'):
395 return compileexp(exp, context, methods)
392 return compileexp(exp, context, methods)
396 if exp[0] == 'symbol':
393 if exp[0] == 'symbol':
397 # unlike runsymbol(), here 'symbol' is always taken as template name
394 # unlike runsymbol(), here 'symbol' is always taken as template name
398 # even if it exists in mapping. this allows us to override mapping
395 # even if it exists in mapping. this allows us to override mapping
399 # by web templates, e.g. 'changelogtag' is redefined in map file.
396 # by web templates, e.g. 'changelogtag' is redefined in map file.
400 return context._load(exp[1])
397 return context._load(exp[1])
401 raise error.ParseError(_("expected template specifier"))
398 raise error.ParseError(_("expected template specifier"))
402
399
403 def _runrecursivesymbol(context, mapping, key):
400 def _runrecursivesymbol(context, mapping, key):
404 raise error.Abort(_("recursive reference '%s' in template") % key)
401 raise error.Abort(_("recursive reference '%s' in template") % key)
405
402
406 def buildtemplate(exp, context):
403 def buildtemplate(exp, context):
407 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
404 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
408 return (templateutil.runtemplate, ctmpl)
405 return (templateutil.runtemplate, ctmpl)
409
406
410 def buildfilter(exp, context):
407 def buildfilter(exp, context):
411 n = getsymbol(exp[2])
408 n = getsymbol(exp[2])
412 if n in context._filters:
409 if n in context._filters:
413 filt = context._filters[n]
410 filt = context._filters[n]
414 arg = compileexp(exp[1], context, methods)
411 arg = compileexp(exp[1], context, methods)
415 return (templateutil.runfilter, (arg, filt))
412 return (templateutil.runfilter, (arg, filt))
416 if n in context._funcs:
413 if n in context._funcs:
417 f = context._funcs[n]
414 f = context._funcs[n]
418 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
415 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
419 return (f, args)
416 return (f, args)
420 raise error.ParseError(_("unknown function '%s'") % n)
417 raise error.ParseError(_("unknown function '%s'") % n)
421
418
422 def buildmap(exp, context):
419 def buildmap(exp, context):
423 darg = compileexp(exp[1], context, methods)
420 darg = compileexp(exp[1], context, methods)
424 targ = gettemplate(exp[2], context)
421 targ = gettemplate(exp[2], context)
425 return (templateutil.runmap, (darg, targ))
422 return (templateutil.runmap, (darg, targ))
426
423
427 def buildmember(exp, context):
424 def buildmember(exp, context):
428 darg = compileexp(exp[1], context, methods)
425 darg = compileexp(exp[1], context, methods)
429 memb = getsymbol(exp[2])
426 memb = getsymbol(exp[2])
430 return (templateutil.runmember, (darg, memb))
427 return (templateutil.runmember, (darg, memb))
431
428
432 def buildnegate(exp, context):
429 def buildnegate(exp, context):
433 arg = compileexp(exp[1], context, exprmethods)
430 arg = compileexp(exp[1], context, exprmethods)
434 return (templateutil.runnegate, arg)
431 return (templateutil.runnegate, arg)
435
432
436 def buildarithmetic(exp, context, func):
433 def buildarithmetic(exp, context, func):
437 left = compileexp(exp[1], context, exprmethods)
434 left = compileexp(exp[1], context, exprmethods)
438 right = compileexp(exp[2], context, exprmethods)
435 right = compileexp(exp[2], context, exprmethods)
439 return (templateutil.runarithmetic, (func, left, right))
436 return (templateutil.runarithmetic, (func, left, right))
440
437
441 def buildfunc(exp, context):
438 def buildfunc(exp, context):
442 n = getsymbol(exp[1])
439 n = getsymbol(exp[1])
443 if n in context._funcs:
440 if n in context._funcs:
444 f = context._funcs[n]
441 f = context._funcs[n]
445 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
442 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
446 return (f, args)
443 return (f, args)
447 if n in context._filters:
444 if n in context._filters:
448 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
445 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
449 if len(args) != 1:
446 if len(args) != 1:
450 raise error.ParseError(_("filter %s expects one argument") % n)
447 raise error.ParseError(_("filter %s expects one argument") % n)
451 f = context._filters[n]
448 f = context._filters[n]
452 return (templateutil.runfilter, (args[0], f))
449 return (templateutil.runfilter, (args[0], f))
453 raise error.ParseError(_("unknown function '%s'") % n)
450 raise error.ParseError(_("unknown function '%s'") % n)
454
451
455 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
452 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
456 """Compile parsed tree of function arguments into list or dict of
453 """Compile parsed tree of function arguments into list or dict of
457 (func, data) pairs
454 (func, data) pairs
458
455
459 >>> context = engine(lambda t: (templateutil.runsymbol, t))
456 >>> context = engine(lambda t: (templateutil.runsymbol, t))
460 >>> def fargs(expr, argspec):
457 >>> def fargs(expr, argspec):
461 ... x = _parseexpr(expr)
458 ... x = _parseexpr(expr)
462 ... n = getsymbol(x[1])
459 ... n = getsymbol(x[1])
463 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
460 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
464 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
461 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
465 ['l', 'k']
462 ['l', 'k']
466 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
463 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
467 >>> list(args.keys()), list(args[b'opts'].keys())
464 >>> list(args.keys()), list(args[b'opts'].keys())
468 (['opts'], ['opts', 'k'])
465 (['opts'], ['opts', 'k'])
469 """
466 """
470 def compiledict(xs):
467 def compiledict(xs):
471 return util.sortdict((k, compileexp(x, context, curmethods))
468 return util.sortdict((k, compileexp(x, context, curmethods))
472 for k, x in xs.iteritems())
469 for k, x in xs.iteritems())
473 def compilelist(xs):
470 def compilelist(xs):
474 return [compileexp(x, context, curmethods) for x in xs]
471 return [compileexp(x, context, curmethods) for x in xs]
475
472
476 if not argspec:
473 if not argspec:
477 # filter or function with no argspec: return list of positional args
474 # filter or function with no argspec: return list of positional args
478 return compilelist(getlist(exp))
475 return compilelist(getlist(exp))
479
476
480 # function with argspec: return dict of named args
477 # function with argspec: return dict of named args
481 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
478 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
482 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
479 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
483 keyvaluenode='keyvalue', keynode='symbol')
480 keyvaluenode='keyvalue', keynode='symbol')
484 compargs = util.sortdict()
481 compargs = util.sortdict()
485 if varkey:
482 if varkey:
486 compargs[varkey] = compilelist(treeargs.pop(varkey))
483 compargs[varkey] = compilelist(treeargs.pop(varkey))
487 if optkey:
484 if optkey:
488 compargs[optkey] = compiledict(treeargs.pop(optkey))
485 compargs[optkey] = compiledict(treeargs.pop(optkey))
489 compargs.update(compiledict(treeargs))
486 compargs.update(compiledict(treeargs))
490 return compargs
487 return compargs
491
488
492 def buildkeyvaluepair(exp, content):
489 def buildkeyvaluepair(exp, content):
493 raise error.ParseError(_("can't use a key-value pair in this context"))
490 raise error.ParseError(_("can't use a key-value pair in this context"))
494
491
495 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
492 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
496 exprmethods = {
493 exprmethods = {
497 "integer": lambda e, c: (templateutil.runinteger, e[1]),
494 "integer": lambda e, c: (templateutil.runinteger, e[1]),
498 "string": lambda e, c: (templateutil.runstring, e[1]),
495 "string": lambda e, c: (templateutil.runstring, e[1]),
499 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
496 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
500 "template": buildtemplate,
497 "template": buildtemplate,
501 "group": lambda e, c: compileexp(e[1], c, exprmethods),
498 "group": lambda e, c: compileexp(e[1], c, exprmethods),
502 ".": buildmember,
499 ".": buildmember,
503 "|": buildfilter,
500 "|": buildfilter,
504 "%": buildmap,
501 "%": buildmap,
505 "func": buildfunc,
502 "func": buildfunc,
506 "keyvalue": buildkeyvaluepair,
503 "keyvalue": buildkeyvaluepair,
507 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
504 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
508 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
505 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
509 "negate": buildnegate,
506 "negate": buildnegate,
510 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
507 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
511 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
508 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
512 }
509 }
513
510
514 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
511 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
515 methods = exprmethods.copy()
512 methods = exprmethods.copy()
516 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
513 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
517
514
518 class _aliasrules(parser.basealiasrules):
515 class _aliasrules(parser.basealiasrules):
519 """Parsing and expansion rule set of template aliases"""
516 """Parsing and expansion rule set of template aliases"""
520 _section = _('template alias')
517 _section = _('template alias')
521 _parse = staticmethod(_parseexpr)
518 _parse = staticmethod(_parseexpr)
522
519
523 @staticmethod
520 @staticmethod
524 def _trygetfunc(tree):
521 def _trygetfunc(tree):
525 """Return (name, args) if tree is func(...) or ...|filter; otherwise
522 """Return (name, args) if tree is func(...) or ...|filter; otherwise
526 None"""
523 None"""
527 if tree[0] == 'func' and tree[1][0] == 'symbol':
524 if tree[0] == 'func' and tree[1][0] == 'symbol':
528 return tree[1][1], getlist(tree[2])
525 return tree[1][1], getlist(tree[2])
529 if tree[0] == '|' and tree[2][0] == 'symbol':
526 if tree[0] == '|' and tree[2][0] == 'symbol':
530 return tree[2][1], [tree[1]]
527 return tree[2][1], [tree[1]]
531
528
532 def expandaliases(tree, aliases):
529 def expandaliases(tree, aliases):
533 """Return new tree of aliases are expanded"""
530 """Return new tree of aliases are expanded"""
534 aliasmap = _aliasrules.buildmap(aliases)
531 aliasmap = _aliasrules.buildmap(aliases)
535 return _aliasrules.expand(aliasmap, tree)
532 return _aliasrules.expand(aliasmap, tree)
536
533
537 # template engine
534 # template engine
538
535
539 def unquotestring(s):
536 def unquotestring(s):
540 '''unwrap quotes if any; otherwise returns unmodified string'''
537 '''unwrap quotes if any; otherwise returns unmodified string'''
541 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
538 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
542 return s
539 return s
543 return s[1:-1]
540 return s[1:-1]
544
541
545 class resourcemapper(object):
542 class resourcemapper(object):
546 """Mapper of internal template resources"""
543 """Mapper of internal template resources"""
547
544
548 __metaclass__ = abc.ABCMeta
545 __metaclass__ = abc.ABCMeta
549
546
550 @abc.abstractmethod
547 @abc.abstractmethod
551 def availablekeys(self, context, mapping):
548 def availablekeys(self, context, mapping):
552 """Return a set of available resource keys based on the given mapping"""
549 """Return a set of available resource keys based on the given mapping"""
553
550
554 @abc.abstractmethod
551 @abc.abstractmethod
555 def knownkeys(self):
552 def knownkeys(self):
556 """Return a set of supported resource keys"""
553 """Return a set of supported resource keys"""
557
554
558 @abc.abstractmethod
555 @abc.abstractmethod
559 def lookup(self, context, mapping, key):
556 def lookup(self, context, mapping, key):
560 """Return a resource for the key if available; otherwise None"""
557 """Return a resource for the key if available; otherwise None"""
561
558
562 @abc.abstractmethod
559 @abc.abstractmethod
563 def populatemap(self, context, origmapping, newmapping):
560 def populatemap(self, context, origmapping, newmapping):
564 """Return a dict of additional mapping items which should be paired
561 """Return a dict of additional mapping items which should be paired
565 with the given new mapping"""
562 with the given new mapping"""
566
563
567 class nullresourcemapper(resourcemapper):
564 class nullresourcemapper(resourcemapper):
568 def availablekeys(self, context, mapping):
565 def availablekeys(self, context, mapping):
569 return set()
566 return set()
570
567
571 def knownkeys(self):
568 def knownkeys(self):
572 return set()
569 return set()
573
570
574 def lookup(self, context, mapping, key):
571 def lookup(self, context, mapping, key):
575 return None
572 return None
576
573
577 def populatemap(self, context, origmapping, newmapping):
574 def populatemap(self, context, origmapping, newmapping):
578 return {}
575 return {}
579
576
580 class engine(object):
577 class engine(object):
581 '''template expansion engine.
578 '''template expansion engine.
582
579
583 template expansion works like this. a map file contains key=value
580 template expansion works like this. a map file contains key=value
584 pairs. if value is quoted, it is treated as string. otherwise, it
581 pairs. if value is quoted, it is treated as string. otherwise, it
585 is treated as name of template file.
582 is treated as name of template file.
586
583
587 templater is asked to expand a key in map. it looks up key, and
584 templater is asked to expand a key in map. it looks up key, and
588 looks for strings like this: {foo}. it expands {foo} by looking up
585 looks for strings like this: {foo}. it expands {foo} by looking up
589 foo in map, and substituting it. expansion is recursive: it stops
586 foo in map, and substituting it. expansion is recursive: it stops
590 when there is no more {foo} to replace.
587 when there is no more {foo} to replace.
591
588
592 expansion also allows formatting and filtering.
589 expansion also allows formatting and filtering.
593
590
594 format uses key to expand each item in list. syntax is
591 format uses key to expand each item in list. syntax is
595 {key%format}.
592 {key%format}.
596
593
597 filter uses function to transform value. syntax is
594 filter uses function to transform value. syntax is
598 {key|filter1|filter2|...}.'''
595 {key|filter1|filter2|...}.'''
599
596
600 def __init__(self, loader, filters=None, defaults=None, resources=None,
597 def __init__(self, loader, filters=None, defaults=None, resources=None,
601 aliases=()):
598 aliases=()):
602 self._loader = loader
599 self._loader = loader
603 if filters is None:
600 if filters is None:
604 filters = {}
601 filters = {}
605 self._filters = filters
602 self._filters = filters
606 self._funcs = templatefuncs.funcs # make this a parameter if needed
603 self._funcs = templatefuncs.funcs # make this a parameter if needed
607 if defaults is None:
604 if defaults is None:
608 defaults = {}
605 defaults = {}
609 if resources is None:
606 if resources is None:
610 resources = nullresourcemapper()
607 resources = nullresourcemapper()
611 self._defaults = defaults
608 self._defaults = defaults
612 self._resources = resources
609 self._resources = resources
613 self._aliasmap = _aliasrules.buildmap(aliases)
610 self._aliasmap = _aliasrules.buildmap(aliases)
614 self._cache = {} # key: (func, data)
611 self._cache = {} # key: (func, data)
615 self._tmplcache = {} # literal template: (func, data)
612 self._tmplcache = {} # literal template: (func, data)
616
613
617 def overlaymap(self, origmapping, newmapping):
614 def overlaymap(self, origmapping, newmapping):
618 """Create combined mapping from the original mapping and partial
615 """Create combined mapping from the original mapping and partial
619 mapping to override the original"""
616 mapping to override the original"""
620 # do not copy symbols which overrides the defaults depending on
617 # do not copy symbols which overrides the defaults depending on
621 # new resources, so the defaults will be re-evaluated (issue5612)
618 # new resources, so the defaults will be re-evaluated (issue5612)
622 knownres = self._resources.knownkeys()
619 knownres = self._resources.knownkeys()
623 newres = self._resources.availablekeys(self, newmapping)
620 newres = self._resources.availablekeys(self, newmapping)
624 mapping = {k: v for k, v in origmapping.iteritems()
621 mapping = {k: v for k, v in origmapping.iteritems()
625 if (k in knownres # not a symbol per self.symbol()
622 if (k in knownres # not a symbol per self.symbol()
626 or newres.isdisjoint(self._defaultrequires(k)))}
623 or newres.isdisjoint(self._defaultrequires(k)))}
627 mapping.update(newmapping)
624 mapping.update(newmapping)
628 mapping.update(
625 mapping.update(
629 self._resources.populatemap(self, origmapping, newmapping))
626 self._resources.populatemap(self, origmapping, newmapping))
630 return mapping
627 return mapping
631
628
632 def _defaultrequires(self, key):
629 def _defaultrequires(self, key):
633 """Resource keys required by the specified default symbol function"""
630 """Resource keys required by the specified default symbol function"""
634 v = self._defaults.get(key)
631 v = self._defaults.get(key)
635 if v is None or not callable(v):
632 if v is None or not callable(v):
636 return ()
633 return ()
637 return getattr(v, '_requires', ())
634 return getattr(v, '_requires', ())
638
635
639 def symbol(self, mapping, key):
636 def symbol(self, mapping, key):
640 """Resolve symbol to value or function; None if nothing found"""
637 """Resolve symbol to value or function; None if nothing found"""
641 v = None
638 v = None
642 if key not in self._resources.knownkeys():
639 if key not in self._resources.knownkeys():
643 v = mapping.get(key)
640 v = mapping.get(key)
644 if v is None:
641 if v is None:
645 v = self._defaults.get(key)
642 v = self._defaults.get(key)
646 return v
643 return v
647
644
648 def availableresourcekeys(self, mapping):
645 def availableresourcekeys(self, mapping):
649 """Return a set of available resource keys based on the given mapping"""
646 """Return a set of available resource keys based on the given mapping"""
650 return self._resources.availablekeys(self, mapping)
647 return self._resources.availablekeys(self, mapping)
651
648
652 def knownresourcekeys(self):
649 def knownresourcekeys(self):
653 """Return a set of supported resource keys"""
650 """Return a set of supported resource keys"""
654 return self._resources.knownkeys()
651 return self._resources.knownkeys()
655
652
656 def resource(self, mapping, key):
653 def resource(self, mapping, key):
657 """Return internal data (e.g. cache) used for keyword/function
654 """Return internal data (e.g. cache) used for keyword/function
658 evaluation"""
655 evaluation"""
659 v = self._resources.lookup(self, mapping, key)
656 v = self._resources.lookup(self, mapping, key)
660 if v is None:
657 if v is None:
661 raise templateutil.ResourceUnavailable(
658 raise templateutil.ResourceUnavailable(
662 _('template resource not available: %s') % key)
659 _('template resource not available: %s') % key)
663 return v
660 return v
664
661
665 def _load(self, t):
662 def _load(self, t):
666 '''load, parse, and cache a template'''
663 '''load, parse, and cache a template'''
667 if t not in self._cache:
664 if t not in self._cache:
668 # put poison to cut recursion while compiling 't'
665 # put poison to cut recursion while compiling 't'
669 self._cache[t] = (_runrecursivesymbol, t)
666 self._cache[t] = (_runrecursivesymbol, t)
670 try:
667 try:
671 x = parse(self._loader(t))
668 x = parse(self._loader(t))
672 if self._aliasmap:
669 if self._aliasmap:
673 x = _aliasrules.expand(self._aliasmap, x)
670 x = _aliasrules.expand(self._aliasmap, x)
674 self._cache[t] = compileexp(x, self, methods)
671 self._cache[t] = compileexp(x, self, methods)
675 except: # re-raises
672 except: # re-raises
676 del self._cache[t]
673 del self._cache[t]
677 raise
674 raise
678 return self._cache[t]
675 return self._cache[t]
679
676
680 def _parse(self, tmpl):
677 def _parse(self, tmpl):
681 """Parse and cache a literal template"""
678 """Parse and cache a literal template"""
682 if tmpl not in self._tmplcache:
679 if tmpl not in self._tmplcache:
683 x = parse(tmpl)
680 x = parse(tmpl)
684 self._tmplcache[tmpl] = compileexp(x, self, methods)
681 self._tmplcache[tmpl] = compileexp(x, self, methods)
685 return self._tmplcache[tmpl]
682 return self._tmplcache[tmpl]
686
683
687 def preload(self, t):
684 def preload(self, t):
688 """Load, parse, and cache the specified template if available"""
685 """Load, parse, and cache the specified template if available"""
689 try:
686 try:
690 self._load(t)
687 self._load(t)
691 return True
688 return True
692 except templateutil.TemplateNotFound:
689 except templateutil.TemplateNotFound:
693 return False
690 return False
694
691
695 def process(self, t, mapping):
692 def process(self, t, mapping):
696 '''Perform expansion. t is name of map element to expand.
693 '''Perform expansion. t is name of map element to expand.
697 mapping contains added elements for use during expansion. Is a
694 mapping contains added elements for use during expansion. Is a
698 generator.'''
695 generator.'''
699 func, data = self._load(t)
696 func, data = self._load(t)
700 return self._expand(func, data, mapping)
697 return self._expand(func, data, mapping)
701
698
702 def expand(self, tmpl, mapping):
699 def expand(self, tmpl, mapping):
703 """Perform expansion over a literal template
700 """Perform expansion over a literal template
704
701
705 No user aliases will be expanded since this is supposed to be called
702 No user aliases will be expanded since this is supposed to be called
706 with an internal template string.
703 with an internal template string.
707 """
704 """
708 func, data = self._parse(tmpl)
705 func, data = self._parse(tmpl)
709 return self._expand(func, data, mapping)
706 return self._expand(func, data, mapping)
710
707
711 def _expand(self, func, data, mapping):
708 def _expand(self, func, data, mapping):
712 # populate additional items only if they don't exist in the given
709 # populate additional items only if they don't exist in the given
713 # mapping. this is slightly different from overlaymap() because the
710 # mapping. this is slightly different from overlaymap() because the
714 # initial 'revcache' may contain pre-computed items.
711 # initial 'revcache' may contain pre-computed items.
715 extramapping = self._resources.populatemap(self, {}, mapping)
712 extramapping = self._resources.populatemap(self, {}, mapping)
716 if extramapping:
713 if extramapping:
717 extramapping.update(mapping)
714 extramapping.update(mapping)
718 mapping = extramapping
715 mapping = extramapping
719 return templateutil.flatten(self, mapping, func(self, mapping, data))
716 return templateutil.flatten(self, mapping, func(self, mapping, data))
720
717
721 engines = {'default': engine}
718 engines = {'default': engine}
722
719
723 def stylelist():
720 def stylelist():
724 paths = templatepaths()
721 paths = templatepaths()
725 if not paths:
722 if not paths:
726 return _('no templates found, try `hg debuginstall` for more info')
723 return _('no templates found, try `hg debuginstall` for more info')
727 dirlist = os.listdir(paths[0])
724 dirlist = os.listdir(paths[0])
728 stylelist = []
725 stylelist = []
729 for file in dirlist:
726 for file in dirlist:
730 split = file.split(".")
727 split = file.split(".")
731 if split[-1] in ('orig', 'rej'):
728 if split[-1] in ('orig', 'rej'):
732 continue
729 continue
733 if split[0] == "map-cmdline":
730 if split[0] == "map-cmdline":
734 stylelist.append(split[1])
731 stylelist.append(split[1])
735 return ", ".join(sorted(stylelist))
732 return ", ".join(sorted(stylelist))
736
733
737 def _readmapfile(mapfile):
734 def _readmapfile(mapfile):
738 """Load template elements from the given map file"""
735 """Load template elements from the given map file"""
739 if not os.path.exists(mapfile):
736 if not os.path.exists(mapfile):
740 raise error.Abort(_("style '%s' not found") % mapfile,
737 raise error.Abort(_("style '%s' not found") % mapfile,
741 hint=_("available styles: %s") % stylelist())
738 hint=_("available styles: %s") % stylelist())
742
739
743 base = os.path.dirname(mapfile)
740 base = os.path.dirname(mapfile)
744 conf = config.config(includepaths=templatepaths())
741 conf = config.config(includepaths=templatepaths())
745 conf.read(mapfile, remap={'': 'templates'})
742 conf.read(mapfile, remap={'': 'templates'})
746
743
747 cache = {}
744 cache = {}
748 tmap = {}
745 tmap = {}
749 aliases = []
746 aliases = []
750
747
751 val = conf.get('templates', '__base__')
748 val = conf.get('templates', '__base__')
752 if val and val[0] not in "'\"":
749 if val and val[0] not in "'\"":
753 # treat as a pointer to a base class for this style
750 # treat as a pointer to a base class for this style
754 path = util.normpath(os.path.join(base, val))
751 path = util.normpath(os.path.join(base, val))
755
752
756 # fallback check in template paths
753 # fallback check in template paths
757 if not os.path.exists(path):
754 if not os.path.exists(path):
758 for p in templatepaths():
755 for p in templatepaths():
759 p2 = util.normpath(os.path.join(p, val))
756 p2 = util.normpath(os.path.join(p, val))
760 if os.path.isfile(p2):
757 if os.path.isfile(p2):
761 path = p2
758 path = p2
762 break
759 break
763 p3 = util.normpath(os.path.join(p2, "map"))
760 p3 = util.normpath(os.path.join(p2, "map"))
764 if os.path.isfile(p3):
761 if os.path.isfile(p3):
765 path = p3
762 path = p3
766 break
763 break
767
764
768 cache, tmap, aliases = _readmapfile(path)
765 cache, tmap, aliases = _readmapfile(path)
769
766
770 for key, val in conf['templates'].items():
767 for key, val in conf['templates'].items():
771 if not val:
768 if not val:
772 raise error.ParseError(_('missing value'),
769 raise error.ParseError(_('missing value'),
773 conf.source('templates', key))
770 conf.source('templates', key))
774 if val[0] in "'\"":
771 if val[0] in "'\"":
775 if val[0] != val[-1]:
772 if val[0] != val[-1]:
776 raise error.ParseError(_('unmatched quotes'),
773 raise error.ParseError(_('unmatched quotes'),
777 conf.source('templates', key))
774 conf.source('templates', key))
778 cache[key] = unquotestring(val)
775 cache[key] = unquotestring(val)
779 elif key != '__base__':
776 elif key != '__base__':
780 val = 'default', val
777 val = 'default', val
781 if ':' in val[1]:
778 if ':' in val[1]:
782 val = val[1].split(':', 1)
779 val = val[1].split(':', 1)
783 tmap[key] = val[0], os.path.join(base, val[1])
780 tmap[key] = val[0], os.path.join(base, val[1])
784 aliases.extend(conf['templatealias'].items())
781 aliases.extend(conf['templatealias'].items())
785 return cache, tmap, aliases
782 return cache, tmap, aliases
786
783
787 class templater(object):
784 class templater(object):
788
785
789 def __init__(self, filters=None, defaults=None, resources=None,
786 def __init__(self, filters=None, defaults=None, resources=None,
790 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
787 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
791 """Create template engine optionally with preloaded template fragments
788 """Create template engine optionally with preloaded template fragments
792
789
793 - ``filters``: a dict of functions to transform a value into another.
790 - ``filters``: a dict of functions to transform a value into another.
794 - ``defaults``: a dict of symbol values/functions; may be overridden
791 - ``defaults``: a dict of symbol values/functions; may be overridden
795 by a ``mapping`` dict.
792 by a ``mapping`` dict.
796 - ``resources``: a resourcemapper object to look up internal data
793 - ``resources``: a resourcemapper object to look up internal data
797 (e.g. cache), inaccessible from user template.
794 (e.g. cache), inaccessible from user template.
798 - ``cache``: a dict of preloaded template fragments.
795 - ``cache``: a dict of preloaded template fragments.
799 - ``aliases``: a list of alias (name, replacement) pairs.
796 - ``aliases``: a list of alias (name, replacement) pairs.
800
797
801 self.cache may be updated later to register additional template
798 self.cache may be updated later to register additional template
802 fragments.
799 fragments.
803 """
800 """
804 if filters is None:
801 if filters is None:
805 filters = {}
802 filters = {}
806 if defaults is None:
803 if defaults is None:
807 defaults = {}
804 defaults = {}
808 if cache is None:
805 if cache is None:
809 cache = {}
806 cache = {}
810 self.cache = cache.copy()
807 self.cache = cache.copy()
811 self.map = {}
808 self.map = {}
812 self.filters = templatefilters.filters.copy()
809 self.filters = templatefilters.filters.copy()
813 self.filters.update(filters)
810 self.filters.update(filters)
814 self.defaults = defaults
811 self.defaults = defaults
815 self._resources = resources
812 self._resources = resources
816 self._aliases = aliases
813 self._aliases = aliases
817 self.minchunk, self.maxchunk = minchunk, maxchunk
814 self.minchunk, self.maxchunk = minchunk, maxchunk
818 self.ecache = {}
815 self.ecache = {}
819
816
820 @classmethod
817 @classmethod
821 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
818 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
822 cache=None, minchunk=1024, maxchunk=65536):
819 cache=None, minchunk=1024, maxchunk=65536):
823 """Create templater from the specified map file"""
820 """Create templater from the specified map file"""
824 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
821 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
825 cache, tmap, aliases = _readmapfile(mapfile)
822 cache, tmap, aliases = _readmapfile(mapfile)
826 t.cache.update(cache)
823 t.cache.update(cache)
827 t.map = tmap
824 t.map = tmap
828 t._aliases = aliases
825 t._aliases = aliases
829 return t
826 return t
830
827
831 def __contains__(self, key):
828 def __contains__(self, key):
832 return key in self.cache or key in self.map
829 return key in self.cache or key in self.map
833
830
834 def load(self, t):
831 def load(self, t):
835 '''Get the template for the given template name. Use a local cache.'''
832 '''Get the template for the given template name. Use a local cache.'''
836 if t not in self.cache:
833 if t not in self.cache:
837 try:
834 try:
838 self.cache[t] = util.readfile(self.map[t][1])
835 self.cache[t] = util.readfile(self.map[t][1])
839 except KeyError as inst:
836 except KeyError as inst:
840 raise templateutil.TemplateNotFound(
837 raise templateutil.TemplateNotFound(
841 _('"%s" not in template map') % inst.args[0])
838 _('"%s" not in template map') % inst.args[0])
842 except IOError as inst:
839 except IOError as inst:
843 reason = (_('template file %s: %s')
840 reason = (_('template file %s: %s')
844 % (self.map[t][1],
841 % (self.map[t][1],
845 stringutil.forcebytestr(inst.args[1])))
842 stringutil.forcebytestr(inst.args[1])))
846 raise IOError(inst.args[0], encoding.strfromlocal(reason))
843 raise IOError(inst.args[0], encoding.strfromlocal(reason))
847 return self.cache[t]
844 return self.cache[t]
848
845
849 def renderdefault(self, mapping):
846 def renderdefault(self, mapping):
850 """Render the default unnamed template and return result as string"""
847 """Render the default unnamed template and return result as string"""
851 return self.render('', mapping)
848 return self.render('', mapping)
852
849
853 def render(self, t, mapping):
850 def render(self, t, mapping):
854 """Render the specified named template and return result as string"""
851 """Render the specified named template and return result as string"""
855 return b''.join(self.generate(t, mapping))
852 return b''.join(self.generate(t, mapping))
856
853
857 def generate(self, t, mapping):
854 def generate(self, t, mapping):
858 """Return a generator that renders the specified named template and
855 """Return a generator that renders the specified named template and
859 yields chunks"""
856 yields chunks"""
860 ttype = t in self.map and self.map[t][0] or 'default'
857 ttype = t in self.map and self.map[t][0] or 'default'
861 if ttype not in self.ecache:
858 if ttype not in self.ecache:
862 try:
859 try:
863 ecls = engines[ttype]
860 ecls = engines[ttype]
864 except KeyError:
861 except KeyError:
865 raise error.Abort(_('invalid template engine: %s') % ttype)
862 raise error.Abort(_('invalid template engine: %s') % ttype)
866 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
863 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
867 self._resources, self._aliases)
864 self._resources, self._aliases)
868 proc = self.ecache[ttype]
865 proc = self.ecache[ttype]
869
866
870 stream = proc.process(t, mapping)
867 stream = proc.process(t, mapping)
871 if self.minchunk:
868 if self.minchunk:
872 stream = util.increasingchunks(stream, min=self.minchunk,
869 stream = util.increasingchunks(stream, min=self.minchunk,
873 max=self.maxchunk)
870 max=self.maxchunk)
874 return stream
871 return stream
875
872
876 def templatepaths():
873 def templatepaths():
877 '''return locations used for template files.'''
874 '''return locations used for template files.'''
878 pathsrel = ['templates']
875 pathsrel = ['templates']
879 paths = [os.path.normpath(os.path.join(util.datapath, f))
876 paths = [os.path.normpath(os.path.join(util.datapath, f))
880 for f in pathsrel]
877 for f in pathsrel]
881 return [p for p in paths if os.path.isdir(p)]
878 return [p for p in paths if os.path.isdir(p)]
882
879
883 def templatepath(name):
880 def templatepath(name):
884 '''return location of template file. returns None if not found.'''
881 '''return location of template file. returns None if not found.'''
885 for p in templatepaths():
882 for p in templatepaths():
886 f = os.path.join(p, name)
883 f = os.path.join(p, name)
887 if os.path.exists(f):
884 if os.path.exists(f):
888 return f
885 return f
889 return None
886 return None
890
887
891 def stylemap(styles, paths=None):
888 def stylemap(styles, paths=None):
892 """Return path to mapfile for a given style.
889 """Return path to mapfile for a given style.
893
890
894 Searches mapfile in the following locations:
891 Searches mapfile in the following locations:
895 1. templatepath/style/map
892 1. templatepath/style/map
896 2. templatepath/map-style
893 2. templatepath/map-style
897 3. templatepath/map
894 3. templatepath/map
898 """
895 """
899
896
900 if paths is None:
897 if paths is None:
901 paths = templatepaths()
898 paths = templatepaths()
902 elif isinstance(paths, bytes):
899 elif isinstance(paths, bytes):
903 paths = [paths]
900 paths = [paths]
904
901
905 if isinstance(styles, bytes):
902 if isinstance(styles, bytes):
906 styles = [styles]
903 styles = [styles]
907
904
908 for style in styles:
905 for style in styles:
909 # only plain name is allowed to honor template paths
906 # only plain name is allowed to honor template paths
910 if (not style
907 if (not style
911 or style in (pycompat.oscurdir, pycompat.ospardir)
908 or style in (pycompat.oscurdir, pycompat.ospardir)
912 or pycompat.ossep in style
909 or pycompat.ossep in style
913 or pycompat.osaltsep and pycompat.osaltsep in style):
910 or pycompat.osaltsep and pycompat.osaltsep in style):
914 continue
911 continue
915 locations = [os.path.join(style, 'map'), 'map-' + style]
912 locations = [os.path.join(style, 'map'), 'map-' + style]
916 locations.append('map')
913 locations.append('map')
917
914
918 for path in paths:
915 for path in paths:
919 for location in locations:
916 for location in locations:
920 mapfile = os.path.join(path, location)
917 mapfile = os.path.join(path, location)
921 if os.path.isfile(mapfile):
918 if os.path.isfile(mapfile):
922 return style, mapfile
919 return style, mapfile
923
920
924 raise RuntimeError("No hgweb templates found in %r" % paths)
921 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,691 +1,683 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 # stub for representing a date type; may be a real date type that can
69 # stub for representing a date type; may be a real date type that can
70 # provide a readable string value
70 # provide a readable string value
71 class date(object):
71 class date(object):
72 pass
72 pass
73
73
74 class hybrid(wrapped):
74 class hybrid(wrapped):
75 """Wrapper for list or dict to support legacy template
75 """Wrapper for list or dict to support legacy template
76
76
77 This class allows us to handle both:
77 This class allows us to handle both:
78 - "{files}" (legacy command-line-specific list hack) and
78 - "{files}" (legacy command-line-specific list hack) and
79 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
79 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
80 and to access raw values:
80 and to access raw values:
81 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
81 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
82 - "{get(extras, key)}"
82 - "{get(extras, key)}"
83 - "{files|json}"
83 - "{files|json}"
84 """
84 """
85
85
86 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
86 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
87 self._gen = gen # generator or function returning generator
87 self._gen = gen # generator or function returning generator
88 self._values = values
88 self._values = values
89 self._makemap = makemap
89 self._makemap = makemap
90 self._joinfmt = joinfmt
90 self._joinfmt = joinfmt
91 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
91 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
92
92
93 def itermaps(self, context):
93 def itermaps(self, context):
94 makemap = self._makemap
94 makemap = self._makemap
95 for x in self._values:
95 for x in self._values:
96 yield makemap(x)
96 yield makemap(x)
97
97
98 def join(self, context, mapping, sep):
98 def join(self, context, mapping, sep):
99 # TODO: switch gen to (context, mapping) API?
99 # TODO: switch gen to (context, mapping) API?
100 return joinitems((self._joinfmt(x) for x in self._values), sep)
100 return joinitems((self._joinfmt(x) for x in self._values), sep)
101
101
102 def show(self, context, mapping):
102 def show(self, context, mapping):
103 # TODO: switch gen to (context, mapping) API?
103 # TODO: switch gen to (context, mapping) API?
104 gen = self._gen
104 gen = self._gen
105 if gen is None:
105 if gen is None:
106 return self.join(context, mapping, ' ')
106 return self.join(context, mapping, ' ')
107 if callable(gen):
107 if callable(gen):
108 return gen()
108 return gen()
109 return gen
109 return gen
110
110
111 def tovalue(self, context, mapping):
111 def tovalue(self, context, mapping):
112 # TODO: return self._values and get rid of proxy methods
112 # TODO: return self._values and get rid of proxy methods
113 return self
113 return self
114
114
115 def __contains__(self, x):
115 def __contains__(self, x):
116 return x in self._values
116 return x in self._values
117 def __getitem__(self, key):
117 def __getitem__(self, key):
118 return self._values[key]
118 return self._values[key]
119 def __len__(self):
119 def __len__(self):
120 return len(self._values)
120 return len(self._values)
121 def __iter__(self):
121 def __iter__(self):
122 return iter(self._values)
122 return iter(self._values)
123 def __getattr__(self, name):
123 def __getattr__(self, name):
124 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
124 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
125 r'itervalues', r'keys', r'values'):
125 r'itervalues', r'keys', r'values'):
126 raise AttributeError(name)
126 raise AttributeError(name)
127 return getattr(self._values, name)
127 return getattr(self._values, name)
128
128
129 class mappable(wrapped):
129 class mappable(wrapped):
130 """Wrapper for non-list/dict object to support map operation
130 """Wrapper for non-list/dict object to support map operation
131
131
132 This class allows us to handle both:
132 This class allows us to handle both:
133 - "{manifest}"
133 - "{manifest}"
134 - "{manifest % '{rev}:{node}'}"
134 - "{manifest % '{rev}:{node}'}"
135 - "{manifest.rev}"
135 - "{manifest.rev}"
136
136
137 Unlike a hybrid, this does not simulate the behavior of the underling
137 Unlike a hybrid, this does not simulate the behavior of the underling
138 value.
138 value.
139 """
139 """
140
140
141 def __init__(self, gen, key, value, makemap):
141 def __init__(self, gen, key, value, makemap):
142 self._gen = gen # generator or function returning generator
142 self._gen = gen # generator or function returning generator
143 self._key = key
143 self._key = key
144 self._value = value # may be generator of strings
144 self._value = value # may be generator of strings
145 self._makemap = makemap
145 self._makemap = makemap
146
146
147 def tomap(self):
147 def tomap(self):
148 return self._makemap(self._key)
148 return self._makemap(self._key)
149
149
150 def itermaps(self, context):
150 def itermaps(self, context):
151 yield self.tomap()
151 yield self.tomap()
152
152
153 def join(self, context, mapping, sep):
153 def join(self, context, mapping, sep):
154 # TODO: just copies the old behavior where a value was a generator
154 # TODO: just copies the old behavior where a value was a generator
155 # yielding one item, but reconsider about it. join() over a string
155 # yielding one item, but reconsider about it. join() over a string
156 # has no consistent result because a string may be a bytes, or a
156 # has no consistent result because a string may be a bytes, or a
157 # generator yielding an item, or a generator yielding multiple items.
157 # generator yielding an item, or a generator yielding multiple items.
158 # Preserving all of the current behaviors wouldn't make any sense.
158 # Preserving all of the current behaviors wouldn't make any sense.
159 return self.show(context, mapping)
159 return self.show(context, mapping)
160
160
161 def show(self, context, mapping):
161 def show(self, context, mapping):
162 # TODO: switch gen to (context, mapping) API?
162 # TODO: switch gen to (context, mapping) API?
163 gen = self._gen
163 gen = self._gen
164 if gen is None:
164 if gen is None:
165 return pycompat.bytestr(self._value)
165 return pycompat.bytestr(self._value)
166 if callable(gen):
166 if callable(gen):
167 return gen()
167 return gen()
168 return gen
168 return gen
169
169
170 def tovalue(self, context, mapping):
170 def tovalue(self, context, mapping):
171 return _unthunk(context, mapping, self._value)
171 return _unthunk(context, mapping, self._value)
172
172
173 class _mappingsequence(wrapped):
173 class _mappingsequence(wrapped):
174 """Wrapper for sequence of template mappings
174 """Wrapper for sequence of template mappings
175
175
176 This represents an inner template structure (i.e. a list of dicts),
176 This represents an inner template structure (i.e. a list of dicts),
177 which can also be rendered by the specified named/literal template.
177 which can also be rendered by the specified named/literal template.
178
178
179 Template mappings may be nested.
179 Template mappings may be nested.
180 """
180 """
181
181
182 def __init__(self, name=None, tmpl=None, sep=''):
182 def __init__(self, name=None, tmpl=None, sep=''):
183 if name is not None and tmpl is not None:
183 if name is not None and tmpl is not None:
184 raise error.ProgrammingError('name and tmpl are mutually exclusive')
184 raise error.ProgrammingError('name and tmpl are mutually exclusive')
185 self._name = name
185 self._name = name
186 self._tmpl = tmpl
186 self._tmpl = tmpl
187 self._defaultsep = sep
187 self._defaultsep = sep
188
188
189 def join(self, context, mapping, sep):
189 def join(self, context, mapping, sep):
190 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
190 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
191 if self._name:
191 if self._name:
192 itemiter = (context.process(self._name, m) for m in mapsiter)
192 itemiter = (context.process(self._name, m) for m in mapsiter)
193 elif self._tmpl:
193 elif self._tmpl:
194 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
194 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
195 else:
195 else:
196 raise error.ParseError(_('not displayable without template'))
196 raise error.ParseError(_('not displayable without template'))
197 return joinitems(itemiter, sep)
197 return joinitems(itemiter, sep)
198
198
199 def show(self, context, mapping):
199 def show(self, context, mapping):
200 return self.join(context, mapping, self._defaultsep)
200 return self.join(context, mapping, self._defaultsep)
201
201
202 def tovalue(self, context, mapping):
202 def tovalue(self, context, mapping):
203 knownres = context.knownresourcekeys()
203 knownres = context.knownresourcekeys()
204 items = []
204 items = []
205 for nm in self.itermaps(context):
205 for nm in self.itermaps(context):
206 # drop internal resources (recursively) which shouldn't be displayed
206 # drop internal resources (recursively) which shouldn't be displayed
207 lm = context.overlaymap(mapping, nm)
207 lm = context.overlaymap(mapping, nm)
208 items.append({k: unwrapvalue(context, lm, v)
208 items.append({k: unwrapvalue(context, lm, v)
209 for k, v in nm.iteritems() if k not in knownres})
209 for k, v in nm.iteritems() if k not in knownres})
210 return items
210 return items
211
211
212 class mappinggenerator(_mappingsequence):
212 class mappinggenerator(_mappingsequence):
213 """Wrapper for generator of template mappings
213 """Wrapper for generator of template mappings
214
214
215 The function ``make(context, *args)`` should return a generator of
215 The function ``make(context, *args)`` should return a generator of
216 mapping dicts.
216 mapping dicts.
217 """
217 """
218
218
219 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
219 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
220 super(mappinggenerator, self).__init__(name, tmpl, sep)
220 super(mappinggenerator, self).__init__(name, tmpl, sep)
221 self._make = make
221 self._make = make
222 self._args = args
222 self._args = args
223
223
224 def itermaps(self, context):
224 def itermaps(self, context):
225 return self._make(context, *self._args)
225 return self._make(context, *self._args)
226
226
227 class mappinglist(_mappingsequence):
227 class mappinglist(_mappingsequence):
228 """Wrapper for list of template mappings"""
228 """Wrapper for list of template mappings"""
229
229
230 def __init__(self, mappings, name=None, tmpl=None, sep=''):
230 def __init__(self, mappings, name=None, tmpl=None, sep=''):
231 super(mappinglist, self).__init__(name, tmpl, sep)
231 super(mappinglist, self).__init__(name, tmpl, sep)
232 self._mappings = mappings
232 self._mappings = mappings
233
233
234 def itermaps(self, context):
234 def itermaps(self, context):
235 return iter(self._mappings)
235 return iter(self._mappings)
236
236
237 class mappedgenerator(wrapped):
237 class mappedgenerator(wrapped):
238 """Wrapper for generator of strings which acts as a list
238 """Wrapper for generator of strings which acts as a list
239
239
240 The function ``make(context, *args)`` should return a generator of
240 The function ``make(context, *args)`` should return a generator of
241 byte strings, or a generator of (possibly nested) generators of byte
241 byte strings, or a generator of (possibly nested) generators of byte
242 strings (i.e. a generator for a list of byte strings.)
242 strings (i.e. a generator for a list of byte strings.)
243 """
243 """
244
244
245 def __init__(self, make, args=()):
245 def __init__(self, make, args=()):
246 self._make = make
246 self._make = make
247 self._args = args
247 self._args = args
248
248
249 def _gen(self, context):
249 def _gen(self, context):
250 return self._make(context, *self._args)
250 return self._make(context, *self._args)
251
251
252 def itermaps(self, context):
252 def itermaps(self, context):
253 raise error.ParseError(_('list of strings is not mappable'))
253 raise error.ParseError(_('list of strings is not mappable'))
254
254
255 def join(self, context, mapping, sep):
255 def join(self, context, mapping, sep):
256 return joinitems(self._gen(context), sep)
256 return joinitems(self._gen(context), sep)
257
257
258 def show(self, context, mapping):
258 def show(self, context, mapping):
259 return self.join(context, mapping, '')
259 return self.join(context, mapping, '')
260
260
261 def tovalue(self, context, mapping):
261 def tovalue(self, context, mapping):
262 return [stringify(context, mapping, x) for x in self._gen(context)]
262 return [stringify(context, mapping, x) for x in self._gen(context)]
263
263
264 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
264 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
265 """Wrap data to support both dict-like and string-like operations"""
265 """Wrap data to support both dict-like and string-like operations"""
266 prefmt = pycompat.identity
266 prefmt = pycompat.identity
267 if fmt is None:
267 if fmt is None:
268 fmt = '%s=%s'
268 fmt = '%s=%s'
269 prefmt = pycompat.bytestr
269 prefmt = pycompat.bytestr
270 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
270 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
271 lambda k: fmt % (prefmt(k), prefmt(data[k])))
271 lambda k: fmt % (prefmt(k), prefmt(data[k])))
272
272
273 def hybridlist(data, name, fmt=None, gen=None):
273 def hybridlist(data, name, fmt=None, gen=None):
274 """Wrap data to support both list-like and string-like operations"""
274 """Wrap data to support both list-like and string-like operations"""
275 prefmt = pycompat.identity
275 prefmt = pycompat.identity
276 if fmt is None:
276 if fmt is None:
277 fmt = '%s'
277 fmt = '%s'
278 prefmt = pycompat.bytestr
278 prefmt = pycompat.bytestr
279 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
279 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
280
280
281 def unwraphybrid(context, mapping, thing):
281 def unwraphybrid(context, mapping, thing):
282 """Return an object which can be stringified possibly by using a legacy
282 """Return an object which can be stringified possibly by using a legacy
283 template"""
283 template"""
284 if not isinstance(thing, wrapped):
284 if not isinstance(thing, wrapped):
285 return thing
285 return thing
286 return thing.show(context, mapping)
286 return thing.show(context, mapping)
287
287
288 def unwrapvalue(context, mapping, thing):
289 """Move the inner value object out of the wrapper"""
290 if not isinstance(thing, wrapped):
291 return thing
292 return thing.tovalue(context, mapping)
293
294 def wraphybridvalue(container, key, value):
288 def wraphybridvalue(container, key, value):
295 """Wrap an element of hybrid container to be mappable
289 """Wrap an element of hybrid container to be mappable
296
290
297 The key is passed to the makemap function of the given container, which
291 The key is passed to the makemap function of the given container, which
298 should be an item generated by iter(container).
292 should be an item generated by iter(container).
299 """
293 """
300 makemap = getattr(container, '_makemap', None)
294 makemap = getattr(container, '_makemap', None)
301 if makemap is None:
295 if makemap is None:
302 return value
296 return value
303 if util.safehasattr(value, '_makemap'):
297 if util.safehasattr(value, '_makemap'):
304 # a nested hybrid list/dict, which has its own way of map operation
298 # a nested hybrid list/dict, which has its own way of map operation
305 return value
299 return value
306 return mappable(None, key, value, makemap)
300 return mappable(None, key, value, makemap)
307
301
308 def compatdict(context, mapping, name, data, key='key', value='value',
302 def compatdict(context, mapping, name, data, key='key', value='value',
309 fmt=None, plural=None, separator=' '):
303 fmt=None, plural=None, separator=' '):
310 """Wrap data like hybriddict(), but also supports old-style list template
304 """Wrap data like hybriddict(), but also supports old-style list template
311
305
312 This exists for backward compatibility with the old-style template. Use
306 This exists for backward compatibility with the old-style template. Use
313 hybriddict() for new template keywords.
307 hybriddict() for new template keywords.
314 """
308 """
315 c = [{key: k, value: v} for k, v in data.iteritems()]
309 c = [{key: k, value: v} for k, v in data.iteritems()]
316 f = _showcompatlist(context, mapping, name, c, plural, separator)
310 f = _showcompatlist(context, mapping, name, c, plural, separator)
317 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
311 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
318
312
319 def compatlist(context, mapping, name, data, element=None, fmt=None,
313 def compatlist(context, mapping, name, data, element=None, fmt=None,
320 plural=None, separator=' '):
314 plural=None, separator=' '):
321 """Wrap data like hybridlist(), but also supports old-style list template
315 """Wrap data like hybridlist(), but also supports old-style list template
322
316
323 This exists for backward compatibility with the old-style template. Use
317 This exists for backward compatibility with the old-style template. Use
324 hybridlist() for new template keywords.
318 hybridlist() for new template keywords.
325 """
319 """
326 f = _showcompatlist(context, mapping, name, data, plural, separator)
320 f = _showcompatlist(context, mapping, name, data, plural, separator)
327 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
321 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
328
322
329 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
323 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
330 """Return a generator that renders old-style list template
324 """Return a generator that renders old-style list template
331
325
332 name is name of key in template map.
326 name is name of key in template map.
333 values is list of strings or dicts.
327 values is list of strings or dicts.
334 plural is plural of name, if not simply name + 's'.
328 plural is plural of name, if not simply name + 's'.
335 separator is used to join values as a string
329 separator is used to join values as a string
336
330
337 expansion works like this, given name 'foo'.
331 expansion works like this, given name 'foo'.
338
332
339 if values is empty, expand 'no_foos'.
333 if values is empty, expand 'no_foos'.
340
334
341 if 'foo' not in template map, return values as a string,
335 if 'foo' not in template map, return values as a string,
342 joined by 'separator'.
336 joined by 'separator'.
343
337
344 expand 'start_foos'.
338 expand 'start_foos'.
345
339
346 for each value, expand 'foo'. if 'last_foo' in template
340 for each value, expand 'foo'. if 'last_foo' in template
347 map, expand it instead of 'foo' for last key.
341 map, expand it instead of 'foo' for last key.
348
342
349 expand 'end_foos'.
343 expand 'end_foos'.
350 """
344 """
351 if not plural:
345 if not plural:
352 plural = name + 's'
346 plural = name + 's'
353 if not values:
347 if not values:
354 noname = 'no_' + plural
348 noname = 'no_' + plural
355 if context.preload(noname):
349 if context.preload(noname):
356 yield context.process(noname, mapping)
350 yield context.process(noname, mapping)
357 return
351 return
358 if not context.preload(name):
352 if not context.preload(name):
359 if isinstance(values[0], bytes):
353 if isinstance(values[0], bytes):
360 yield separator.join(values)
354 yield separator.join(values)
361 else:
355 else:
362 for v in values:
356 for v in values:
363 r = dict(v)
357 r = dict(v)
364 r.update(mapping)
358 r.update(mapping)
365 yield r
359 yield r
366 return
360 return
367 startname = 'start_' + plural
361 startname = 'start_' + plural
368 if context.preload(startname):
362 if context.preload(startname):
369 yield context.process(startname, mapping)
363 yield context.process(startname, mapping)
370 def one(v, tag=name):
364 def one(v, tag=name):
371 vmapping = {}
365 vmapping = {}
372 try:
366 try:
373 vmapping.update(v)
367 vmapping.update(v)
374 # Python 2 raises ValueError if the type of v is wrong. Python
368 # Python 2 raises ValueError if the type of v is wrong. Python
375 # 3 raises TypeError.
369 # 3 raises TypeError.
376 except (AttributeError, TypeError, ValueError):
370 except (AttributeError, TypeError, ValueError):
377 try:
371 try:
378 # Python 2 raises ValueError trying to destructure an e.g.
372 # Python 2 raises ValueError trying to destructure an e.g.
379 # bytes. Python 3 raises TypeError.
373 # bytes. Python 3 raises TypeError.
380 for a, b in v:
374 for a, b in v:
381 vmapping[a] = b
375 vmapping[a] = b
382 except (TypeError, ValueError):
376 except (TypeError, ValueError):
383 vmapping[name] = v
377 vmapping[name] = v
384 vmapping = context.overlaymap(mapping, vmapping)
378 vmapping = context.overlaymap(mapping, vmapping)
385 return context.process(tag, vmapping)
379 return context.process(tag, vmapping)
386 lastname = 'last_' + name
380 lastname = 'last_' + name
387 if context.preload(lastname):
381 if context.preload(lastname):
388 last = values.pop()
382 last = values.pop()
389 else:
383 else:
390 last = None
384 last = None
391 for v in values:
385 for v in values:
392 yield one(v)
386 yield one(v)
393 if last is not None:
387 if last is not None:
394 yield one(last, tag=lastname)
388 yield one(last, tag=lastname)
395 endname = 'end_' + plural
389 endname = 'end_' + plural
396 if context.preload(endname):
390 if context.preload(endname):
397 yield context.process(endname, mapping)
391 yield context.process(endname, mapping)
398
392
399 def flatten(context, mapping, thing):
393 def flatten(context, mapping, thing):
400 """Yield a single stream from a possibly nested set of iterators"""
394 """Yield a single stream from a possibly nested set of iterators"""
401 thing = unwraphybrid(context, mapping, thing)
395 thing = unwraphybrid(context, mapping, thing)
402 if isinstance(thing, bytes):
396 if isinstance(thing, bytes):
403 yield thing
397 yield thing
404 elif isinstance(thing, str):
398 elif isinstance(thing, str):
405 # We can only hit this on Python 3, and it's here to guard
399 # We can only hit this on Python 3, and it's here to guard
406 # against infinite recursion.
400 # against infinite recursion.
407 raise error.ProgrammingError('Mercurial IO including templates is done'
401 raise error.ProgrammingError('Mercurial IO including templates is done'
408 ' with bytes, not strings, got %r' % thing)
402 ' with bytes, not strings, got %r' % thing)
409 elif thing is None:
403 elif thing is None:
410 pass
404 pass
411 elif not util.safehasattr(thing, '__iter__'):
405 elif not util.safehasattr(thing, '__iter__'):
412 yield pycompat.bytestr(thing)
406 yield pycompat.bytestr(thing)
413 else:
407 else:
414 for i in thing:
408 for i in thing:
415 i = unwraphybrid(context, mapping, i)
409 i = unwraphybrid(context, mapping, i)
416 if isinstance(i, bytes):
410 if isinstance(i, bytes):
417 yield i
411 yield i
418 elif i is None:
412 elif i is None:
419 pass
413 pass
420 elif not util.safehasattr(i, '__iter__'):
414 elif not util.safehasattr(i, '__iter__'):
421 yield pycompat.bytestr(i)
415 yield pycompat.bytestr(i)
422 else:
416 else:
423 for j in flatten(context, mapping, i):
417 for j in flatten(context, mapping, i):
424 yield j
418 yield j
425
419
426 def stringify(context, mapping, thing):
420 def stringify(context, mapping, thing):
427 """Turn values into bytes by converting into text and concatenating them"""
421 """Turn values into bytes by converting into text and concatenating them"""
428 if isinstance(thing, bytes):
422 if isinstance(thing, bytes):
429 return thing # retain localstr to be round-tripped
423 return thing # retain localstr to be round-tripped
430 return b''.join(flatten(context, mapping, thing))
424 return b''.join(flatten(context, mapping, thing))
431
425
432 def findsymbolicname(arg):
426 def findsymbolicname(arg):
433 """Find symbolic name for the given compiled expression; returns None
427 """Find symbolic name for the given compiled expression; returns None
434 if nothing found reliably"""
428 if nothing found reliably"""
435 while True:
429 while True:
436 func, data = arg
430 func, data = arg
437 if func is runsymbol:
431 if func is runsymbol:
438 return data
432 return data
439 elif func is runfilter:
433 elif func is runfilter:
440 arg = data[0]
434 arg = data[0]
441 else:
435 else:
442 return None
436 return None
443
437
444 def _unthunk(context, mapping, thing):
438 def _unthunk(context, mapping, thing):
445 """Evaluate a lazy byte string into value"""
439 """Evaluate a lazy byte string into value"""
446 if not isinstance(thing, types.GeneratorType):
440 if not isinstance(thing, types.GeneratorType):
447 return thing
441 return thing
448 return stringify(context, mapping, thing)
442 return stringify(context, mapping, thing)
449
443
450 def evalrawexp(context, mapping, arg):
444 def evalrawexp(context, mapping, arg):
451 """Evaluate given argument as a bare template object which may require
445 """Evaluate given argument as a bare template object which may require
452 further processing (such as folding generator of strings)"""
446 further processing (such as folding generator of strings)"""
453 func, data = arg
447 func, data = arg
454 return func(context, mapping, data)
448 return func(context, mapping, data)
455
449
456 def evalfuncarg(context, mapping, arg):
450 def evalfuncarg(context, mapping, arg):
457 """Evaluate given argument as value type"""
451 """Evaluate given argument as value type"""
458 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
452 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
459
453
460 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
454 def unwrapvalue(context, mapping, thing):
461 # is fixed. we can't do that right now because join() has to take a generator
455 """Move the inner value object out of the wrapper"""
462 # of byte strings as it is, not a lazy byte string.
463 def _unwrapvalue(context, mapping, thing):
464 if isinstance(thing, wrapped):
456 if isinstance(thing, wrapped):
465 return thing.tovalue(context, mapping)
457 return thing.tovalue(context, mapping)
466 # evalrawexp() may return string, generator of strings or arbitrary object
458 # evalrawexp() may return string, generator of strings or arbitrary object
467 # such as date tuple, but filter does not want generator.
459 # such as date tuple, but filter does not want generator.
468 return _unthunk(context, mapping, thing)
460 return _unthunk(context, mapping, thing)
469
461
470 def evalboolean(context, mapping, arg):
462 def evalboolean(context, mapping, arg):
471 """Evaluate given argument as boolean, but also takes boolean literals"""
463 """Evaluate given argument as boolean, but also takes boolean literals"""
472 func, data = arg
464 func, data = arg
473 if func is runsymbol:
465 if func is runsymbol:
474 thing = func(context, mapping, data, default=None)
466 thing = func(context, mapping, data, default=None)
475 if thing is None:
467 if thing is None:
476 # not a template keyword, takes as a boolean literal
468 # not a template keyword, takes as a boolean literal
477 thing = stringutil.parsebool(data)
469 thing = stringutil.parsebool(data)
478 else:
470 else:
479 thing = func(context, mapping, data)
471 thing = func(context, mapping, data)
480 if isinstance(thing, wrapped):
472 if isinstance(thing, wrapped):
481 thing = thing.tovalue(context, mapping)
473 thing = thing.tovalue(context, mapping)
482 if isinstance(thing, bool):
474 if isinstance(thing, bool):
483 return thing
475 return thing
484 # other objects are evaluated as strings, which means 0 is True, but
476 # other objects are evaluated as strings, which means 0 is True, but
485 # empty dict/list should be False as they are expected to be ''
477 # empty dict/list should be False as they are expected to be ''
486 return bool(stringify(context, mapping, thing))
478 return bool(stringify(context, mapping, thing))
487
479
488 def evaldate(context, mapping, arg, err=None):
480 def evaldate(context, mapping, arg, err=None):
489 """Evaluate given argument as a date tuple or a date string; returns
481 """Evaluate given argument as a date tuple or a date string; returns
490 a (unixtime, offset) tuple"""
482 a (unixtime, offset) tuple"""
491 thing = evalrawexp(context, mapping, arg)
483 thing = evalrawexp(context, mapping, arg)
492 return unwrapdate(context, mapping, thing, err)
484 return unwrapdate(context, mapping, thing, err)
493
485
494 def unwrapdate(context, mapping, thing, err=None):
486 def unwrapdate(context, mapping, thing, err=None):
495 thing = _unwrapvalue(context, mapping, thing)
487 thing = unwrapvalue(context, mapping, thing)
496 try:
488 try:
497 return dateutil.parsedate(thing)
489 return dateutil.parsedate(thing)
498 except AttributeError:
490 except AttributeError:
499 raise error.ParseError(err or _('not a date tuple nor a string'))
491 raise error.ParseError(err or _('not a date tuple nor a string'))
500 except error.ParseError:
492 except error.ParseError:
501 if not err:
493 if not err:
502 raise
494 raise
503 raise error.ParseError(err)
495 raise error.ParseError(err)
504
496
505 def evalinteger(context, mapping, arg, err=None):
497 def evalinteger(context, mapping, arg, err=None):
506 thing = evalrawexp(context, mapping, arg)
498 thing = evalrawexp(context, mapping, arg)
507 return unwrapinteger(context, mapping, thing, err)
499 return unwrapinteger(context, mapping, thing, err)
508
500
509 def unwrapinteger(context, mapping, thing, err=None):
501 def unwrapinteger(context, mapping, thing, err=None):
510 thing = _unwrapvalue(context, mapping, thing)
502 thing = unwrapvalue(context, mapping, thing)
511 try:
503 try:
512 return int(thing)
504 return int(thing)
513 except (TypeError, ValueError):
505 except (TypeError, ValueError):
514 raise error.ParseError(err or _('not an integer'))
506 raise error.ParseError(err or _('not an integer'))
515
507
516 def evalstring(context, mapping, arg):
508 def evalstring(context, mapping, arg):
517 return stringify(context, mapping, evalrawexp(context, mapping, arg))
509 return stringify(context, mapping, evalrawexp(context, mapping, arg))
518
510
519 def evalstringliteral(context, mapping, arg):
511 def evalstringliteral(context, mapping, arg):
520 """Evaluate given argument as string template, but returns symbol name
512 """Evaluate given argument as string template, but returns symbol name
521 if it is unknown"""
513 if it is unknown"""
522 func, data = arg
514 func, data = arg
523 if func is runsymbol:
515 if func is runsymbol:
524 thing = func(context, mapping, data, default=data)
516 thing = func(context, mapping, data, default=data)
525 else:
517 else:
526 thing = func(context, mapping, data)
518 thing = func(context, mapping, data)
527 return stringify(context, mapping, thing)
519 return stringify(context, mapping, thing)
528
520
529 _unwrapfuncbytype = {
521 _unwrapfuncbytype = {
530 None: _unwrapvalue,
522 None: unwrapvalue,
531 bytes: stringify,
523 bytes: stringify,
532 date: unwrapdate,
524 date: unwrapdate,
533 int: unwrapinteger,
525 int: unwrapinteger,
534 }
526 }
535
527
536 def unwrapastype(context, mapping, thing, typ):
528 def unwrapastype(context, mapping, thing, typ):
537 """Move the inner value object out of the wrapper and coerce its type"""
529 """Move the inner value object out of the wrapper and coerce its type"""
538 try:
530 try:
539 f = _unwrapfuncbytype[typ]
531 f = _unwrapfuncbytype[typ]
540 except KeyError:
532 except KeyError:
541 raise error.ProgrammingError('invalid type specified: %r' % typ)
533 raise error.ProgrammingError('invalid type specified: %r' % typ)
542 return f(context, mapping, thing)
534 return f(context, mapping, thing)
543
535
544 def runinteger(context, mapping, data):
536 def runinteger(context, mapping, data):
545 return int(data)
537 return int(data)
546
538
547 def runstring(context, mapping, data):
539 def runstring(context, mapping, data):
548 return data
540 return data
549
541
550 def _recursivesymbolblocker(key):
542 def _recursivesymbolblocker(key):
551 def showrecursion(**args):
543 def showrecursion(**args):
552 raise error.Abort(_("recursive reference '%s' in template") % key)
544 raise error.Abort(_("recursive reference '%s' in template") % key)
553 return showrecursion
545 return showrecursion
554
546
555 def runsymbol(context, mapping, key, default=''):
547 def runsymbol(context, mapping, key, default=''):
556 v = context.symbol(mapping, key)
548 v = context.symbol(mapping, key)
557 if v is None:
549 if v is None:
558 # put poison to cut recursion. we can't move this to parsing phase
550 # put poison to cut recursion. we can't move this to parsing phase
559 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
551 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
560 safemapping = mapping.copy()
552 safemapping = mapping.copy()
561 safemapping[key] = _recursivesymbolblocker(key)
553 safemapping[key] = _recursivesymbolblocker(key)
562 try:
554 try:
563 v = context.process(key, safemapping)
555 v = context.process(key, safemapping)
564 except TemplateNotFound:
556 except TemplateNotFound:
565 v = default
557 v = default
566 if callable(v) and getattr(v, '_requires', None) is None:
558 if callable(v) and getattr(v, '_requires', None) is None:
567 # old templatekw: expand all keywords and resources
559 # old templatekw: expand all keywords and resources
568 # (TODO: deprecate this after porting web template keywords to new API)
560 # (TODO: deprecate this after porting web template keywords to new API)
569 props = {k: context._resources.lookup(context, mapping, k)
561 props = {k: context._resources.lookup(context, mapping, k)
570 for k in context._resources.knownkeys()}
562 for k in context._resources.knownkeys()}
571 # pass context to _showcompatlist() through templatekw._showlist()
563 # pass context to _showcompatlist() through templatekw._showlist()
572 props['templ'] = context
564 props['templ'] = context
573 props.update(mapping)
565 props.update(mapping)
574 return v(**pycompat.strkwargs(props))
566 return v(**pycompat.strkwargs(props))
575 if callable(v):
567 if callable(v):
576 # new templatekw
568 # new templatekw
577 try:
569 try:
578 return v(context, mapping)
570 return v(context, mapping)
579 except ResourceUnavailable:
571 except ResourceUnavailable:
580 # unsupported keyword is mapped to empty just like unknown keyword
572 # unsupported keyword is mapped to empty just like unknown keyword
581 return None
573 return None
582 return v
574 return v
583
575
584 def runtemplate(context, mapping, template):
576 def runtemplate(context, mapping, template):
585 for arg in template:
577 for arg in template:
586 yield evalrawexp(context, mapping, arg)
578 yield evalrawexp(context, mapping, arg)
587
579
588 def runfilter(context, mapping, data):
580 def runfilter(context, mapping, data):
589 arg, filt = data
581 arg, filt = data
590 thing = evalrawexp(context, mapping, arg)
582 thing = evalrawexp(context, mapping, arg)
591 intype = getattr(filt, '_intype', None)
583 intype = getattr(filt, '_intype', None)
592 try:
584 try:
593 thing = unwrapastype(context, mapping, thing, intype)
585 thing = unwrapastype(context, mapping, thing, intype)
594 return filt(thing)
586 return filt(thing)
595 except error.ParseError as e:
587 except error.ParseError as e:
596 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
588 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
597
589
598 def _formatfiltererror(arg, filt):
590 def _formatfiltererror(arg, filt):
599 fn = pycompat.sysbytes(filt.__name__)
591 fn = pycompat.sysbytes(filt.__name__)
600 sym = findsymbolicname(arg)
592 sym = findsymbolicname(arg)
601 if not sym:
593 if not sym:
602 return _("incompatible use of template filter '%s'") % fn
594 return _("incompatible use of template filter '%s'") % fn
603 return (_("template filter '%s' is not compatible with keyword '%s'")
595 return (_("template filter '%s' is not compatible with keyword '%s'")
604 % (fn, sym))
596 % (fn, sym))
605
597
606 def _checkeditermaps(darg, d):
598 def _checkeditermaps(darg, d):
607 try:
599 try:
608 for v in d:
600 for v in d:
609 if not isinstance(v, dict):
601 if not isinstance(v, dict):
610 raise TypeError
602 raise TypeError
611 yield v
603 yield v
612 except TypeError:
604 except TypeError:
613 sym = findsymbolicname(darg)
605 sym = findsymbolicname(darg)
614 if sym:
606 if sym:
615 raise error.ParseError(_("keyword '%s' is not iterable of mappings")
607 raise error.ParseError(_("keyword '%s' is not iterable of mappings")
616 % sym)
608 % sym)
617 else:
609 else:
618 raise error.ParseError(_("%r is not iterable of mappings") % d)
610 raise error.ParseError(_("%r is not iterable of mappings") % d)
619
611
620 def _iteroverlaymaps(context, origmapping, newmappings):
612 def _iteroverlaymaps(context, origmapping, newmappings):
621 """Generate combined mappings from the original mapping and an iterable
613 """Generate combined mappings from the original mapping and an iterable
622 of partial mappings to override the original"""
614 of partial mappings to override the original"""
623 for i, nm in enumerate(newmappings):
615 for i, nm in enumerate(newmappings):
624 lm = context.overlaymap(origmapping, nm)
616 lm = context.overlaymap(origmapping, nm)
625 lm['index'] = i
617 lm['index'] = i
626 yield lm
618 yield lm
627
619
628 def _applymap(context, mapping, diter, targ):
620 def _applymap(context, mapping, diter, targ):
629 for lm in _iteroverlaymaps(context, mapping, diter):
621 for lm in _iteroverlaymaps(context, mapping, diter):
630 yield evalrawexp(context, lm, targ)
622 yield evalrawexp(context, lm, targ)
631
623
632 def runmap(context, mapping, data):
624 def runmap(context, mapping, data):
633 darg, targ = data
625 darg, targ = data
634 d = evalrawexp(context, mapping, darg)
626 d = evalrawexp(context, mapping, darg)
635 # TODO: a generator should be rejected because it is a thunk of lazy
627 # TODO: a generator should be rejected because it is a thunk of lazy
636 # string, but we can't because hgweb abuses generator as a keyword
628 # string, but we can't because hgweb abuses generator as a keyword
637 # that returns a list of dicts.
629 # that returns a list of dicts.
638 # TODO: drop _checkeditermaps() and pass 'd' to mappedgenerator so it
630 # TODO: drop _checkeditermaps() and pass 'd' to mappedgenerator so it
639 # can be restarted.
631 # can be restarted.
640 if isinstance(d, wrapped):
632 if isinstance(d, wrapped):
641 diter = d.itermaps(context)
633 diter = d.itermaps(context)
642 else:
634 else:
643 diter = _checkeditermaps(darg, d)
635 diter = _checkeditermaps(darg, d)
644 return mappedgenerator(_applymap, args=(mapping, diter, targ))
636 return mappedgenerator(_applymap, args=(mapping, diter, targ))
645
637
646 def runmember(context, mapping, data):
638 def runmember(context, mapping, data):
647 darg, memb = data
639 darg, memb = data
648 d = evalrawexp(context, mapping, darg)
640 d = evalrawexp(context, mapping, darg)
649 if util.safehasattr(d, 'tomap'):
641 if util.safehasattr(d, 'tomap'):
650 lm = context.overlaymap(mapping, d.tomap())
642 lm = context.overlaymap(mapping, d.tomap())
651 return runsymbol(context, lm, memb)
643 return runsymbol(context, lm, memb)
652 if util.safehasattr(d, 'get'):
644 if util.safehasattr(d, 'get'):
653 return getdictitem(d, memb)
645 return getdictitem(d, memb)
654
646
655 sym = findsymbolicname(darg)
647 sym = findsymbolicname(darg)
656 if sym:
648 if sym:
657 raise error.ParseError(_("keyword '%s' has no member") % sym)
649 raise error.ParseError(_("keyword '%s' has no member") % sym)
658 else:
650 else:
659 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
651 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
660
652
661 def runnegate(context, mapping, data):
653 def runnegate(context, mapping, data):
662 data = evalinteger(context, mapping, data,
654 data = evalinteger(context, mapping, data,
663 _('negation needs an integer argument'))
655 _('negation needs an integer argument'))
664 return -data
656 return -data
665
657
666 def runarithmetic(context, mapping, data):
658 def runarithmetic(context, mapping, data):
667 func, left, right = data
659 func, left, right = data
668 left = evalinteger(context, mapping, left,
660 left = evalinteger(context, mapping, left,
669 _('arithmetic only defined on integers'))
661 _('arithmetic only defined on integers'))
670 right = evalinteger(context, mapping, right,
662 right = evalinteger(context, mapping, right,
671 _('arithmetic only defined on integers'))
663 _('arithmetic only defined on integers'))
672 try:
664 try:
673 return func(left, right)
665 return func(left, right)
674 except ZeroDivisionError:
666 except ZeroDivisionError:
675 raise error.Abort(_('division by zero is not defined'))
667 raise error.Abort(_('division by zero is not defined'))
676
668
677 def getdictitem(dictarg, key):
669 def getdictitem(dictarg, key):
678 val = dictarg.get(key)
670 val = dictarg.get(key)
679 if val is None:
671 if val is None:
680 return
672 return
681 return wraphybridvalue(dictarg, key, val)
673 return wraphybridvalue(dictarg, key, val)
682
674
683 def joinitems(itemiter, sep):
675 def joinitems(itemiter, sep):
684 """Join items with the separator; Returns generator of bytes"""
676 """Join items with the separator; Returns generator of bytes"""
685 first = True
677 first = True
686 for x in itemiter:
678 for x in itemiter:
687 if first:
679 if first:
688 first = False
680 first = False
689 elif sep:
681 elif sep:
690 yield sep
682 yield sep
691 yield x
683 yield x
General Comments 0
You need to be logged in to leave comments. Login now