##// END OF EJS Templates
templater: remove unused context argument from most resourcemapper functions...
Yuya Nishihara -
r39618:28f974d8 default
parent child Browse files
Show More
@@ -1,649 +1,649 b''
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 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 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or template output
16 - fm.data() to provide extra data to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.verbose = verbose
48 ... ui.verbose = verbose
49 ... ui.pushbuffer()
49 ... ui.pushbuffer()
50 ... try:
50 ... try:
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 ... pycompat.byteskwargs(opts)))
52 ... pycompat.byteskwargs(opts)))
53 ... finally:
53 ... finally:
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55
55
56 Basic example:
56 Basic example:
57
57
58 >>> def files(ui, fm):
58 >>> def files(ui, fm):
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 ... for f in files:
60 ... for f in files:
61 ... fm.startitem()
61 ... fm.startitem()
62 ... fm.write(b'path', b'%s', f[0])
62 ... fm.write(b'path', b'%s', f[0])
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 ... fm.data(size=f[1])
65 ... fm.data(size=f[1])
66 ... fm.plain(b'\\n')
66 ... fm.plain(b'\\n')
67 ... fm.end()
67 ... fm.end()
68 >>> show(files)
68 >>> show(files)
69 foo
69 foo
70 bar
70 bar
71 >>> show(files, verbose=True)
71 >>> show(files, verbose=True)
72 foo 1970-01-01 00:00:00
72 foo 1970-01-01 00:00:00
73 bar 1970-01-01 00:00:01
73 bar 1970-01-01 00:00:01
74 >>> show(files, template=b'json')
74 >>> show(files, template=b'json')
75 [
75 [
76 {
76 {
77 "date": [0, 0],
77 "date": [0, 0],
78 "path": "foo",
78 "path": "foo",
79 "size": 123
79 "size": 123
80 },
80 },
81 {
81 {
82 "date": [1, 0],
82 "date": [1, 0],
83 "path": "bar",
83 "path": "bar",
84 "size": 456
84 "size": 456
85 }
85 }
86 ]
86 ]
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 path: foo
88 path: foo
89 date: 1970-01-01T00:00:00+00:00
89 date: 1970-01-01T00:00:00+00:00
90 path: bar
90 path: bar
91 date: 1970-01-01T00:00:01+00:00
91 date: 1970-01-01T00:00:01+00:00
92
92
93 Nested example:
93 Nested example:
94
94
95 >>> def subrepos(ui, fm):
95 >>> def subrepos(ui, fm):
96 ... fm.startitem()
96 ... fm.startitem()
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
99 ... fm.end()
99 ... fm.end()
100 >>> show(subrepos)
100 >>> show(subrepos)
101 [baz]
101 [baz]
102 foo
102 foo
103 bar
103 bar
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 baz: foo, bar
105 baz: foo, bar
106 """
106 """
107
107
108 from __future__ import absolute_import, print_function
108 from __future__ import absolute_import, print_function
109
109
110 import contextlib
110 import contextlib
111 import itertools
111 import itertools
112 import os
112 import os
113
113
114 from .i18n import _
114 from .i18n import _
115 from .node import (
115 from .node import (
116 hex,
116 hex,
117 short,
117 short,
118 )
118 )
119 from .thirdparty import (
119 from .thirdparty import (
120 attr,
120 attr,
121 )
121 )
122
122
123 from . import (
123 from . import (
124 error,
124 error,
125 pycompat,
125 pycompat,
126 templatefilters,
126 templatefilters,
127 templatefuncs,
127 templatefuncs,
128 templatekw,
128 templatekw,
129 templater,
129 templater,
130 templateutil,
130 templateutil,
131 util,
131 util,
132 )
132 )
133 from .utils import dateutil
133 from .utils import dateutil
134
134
135 pickle = util.pickle
135 pickle = util.pickle
136
136
137 class _nullconverter(object):
137 class _nullconverter(object):
138 '''convert non-primitive data types to be processed by formatter'''
138 '''convert non-primitive data types to be processed by formatter'''
139
139
140 # set to True if context object should be stored as item
140 # set to True if context object should be stored as item
141 storecontext = False
141 storecontext = False
142
142
143 @staticmethod
143 @staticmethod
144 def wrapnested(data, tmpl, sep):
144 def wrapnested(data, tmpl, sep):
145 '''wrap nested data by appropriate type'''
145 '''wrap nested data by appropriate type'''
146 return data
146 return data
147 @staticmethod
147 @staticmethod
148 def formatdate(date, fmt):
148 def formatdate(date, fmt):
149 '''convert date tuple to appropriate format'''
149 '''convert date tuple to appropriate format'''
150 # timestamp can be float, but the canonical form should be int
150 # timestamp can be float, but the canonical form should be int
151 ts, tz = date
151 ts, tz = date
152 return (int(ts), tz)
152 return (int(ts), tz)
153 @staticmethod
153 @staticmethod
154 def formatdict(data, key, value, fmt, sep):
154 def formatdict(data, key, value, fmt, sep):
155 '''convert dict or key-value pairs to appropriate dict format'''
155 '''convert dict or key-value pairs to appropriate dict format'''
156 # use plain dict instead of util.sortdict so that data can be
156 # use plain dict instead of util.sortdict so that data can be
157 # serialized as a builtin dict in pickle output
157 # serialized as a builtin dict in pickle output
158 return dict(data)
158 return dict(data)
159 @staticmethod
159 @staticmethod
160 def formatlist(data, name, fmt, sep):
160 def formatlist(data, name, fmt, sep):
161 '''convert iterable to appropriate list format'''
161 '''convert iterable to appropriate list format'''
162 return list(data)
162 return list(data)
163
163
164 class baseformatter(object):
164 class baseformatter(object):
165 def __init__(self, ui, topic, opts, converter):
165 def __init__(self, ui, topic, opts, converter):
166 self._ui = ui
166 self._ui = ui
167 self._topic = topic
167 self._topic = topic
168 self._opts = opts
168 self._opts = opts
169 self._converter = converter
169 self._converter = converter
170 self._item = None
170 self._item = None
171 # function to convert node to string suitable for this output
171 # function to convert node to string suitable for this output
172 self.hexfunc = hex
172 self.hexfunc = hex
173 def __enter__(self):
173 def __enter__(self):
174 return self
174 return self
175 def __exit__(self, exctype, excvalue, traceback):
175 def __exit__(self, exctype, excvalue, traceback):
176 if exctype is None:
176 if exctype is None:
177 self.end()
177 self.end()
178 def _showitem(self):
178 def _showitem(self):
179 '''show a formatted item once all data is collected'''
179 '''show a formatted item once all data is collected'''
180 def startitem(self):
180 def startitem(self):
181 '''begin an item in the format list'''
181 '''begin an item in the format list'''
182 if self._item is not None:
182 if self._item is not None:
183 self._showitem()
183 self._showitem()
184 self._item = {}
184 self._item = {}
185 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
185 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
186 '''convert date tuple to appropriate format'''
186 '''convert date tuple to appropriate format'''
187 return self._converter.formatdate(date, fmt)
187 return self._converter.formatdate(date, fmt)
188 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
188 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
189 '''convert dict or key-value pairs to appropriate dict format'''
189 '''convert dict or key-value pairs to appropriate dict format'''
190 return self._converter.formatdict(data, key, value, fmt, sep)
190 return self._converter.formatdict(data, key, value, fmt, sep)
191 def formatlist(self, data, name, fmt=None, sep=' '):
191 def formatlist(self, data, name, fmt=None, sep=' '):
192 '''convert iterable to appropriate list format'''
192 '''convert iterable to appropriate list format'''
193 # name is mandatory argument for now, but it could be optional if
193 # name is mandatory argument for now, but it could be optional if
194 # we have default template keyword, e.g. {item}
194 # we have default template keyword, e.g. {item}
195 return self._converter.formatlist(data, name, fmt, sep)
195 return self._converter.formatlist(data, name, fmt, sep)
196 def contexthint(self, datafields):
196 def contexthint(self, datafields):
197 '''set of context object keys to be required given datafields set'''
197 '''set of context object keys to be required given datafields set'''
198 return set()
198 return set()
199 def context(self, **ctxs):
199 def context(self, **ctxs):
200 '''insert context objects to be used to render template keywords'''
200 '''insert context objects to be used to render template keywords'''
201 ctxs = pycompat.byteskwargs(ctxs)
201 ctxs = pycompat.byteskwargs(ctxs)
202 assert all(k in {'ctx', 'fctx'} for k in ctxs)
202 assert all(k in {'ctx', 'fctx'} for k in ctxs)
203 if self._converter.storecontext:
203 if self._converter.storecontext:
204 self._item.update(ctxs)
204 self._item.update(ctxs)
205 def datahint(self):
205 def datahint(self):
206 '''set of field names to be referenced'''
206 '''set of field names to be referenced'''
207 return set()
207 return set()
208 def data(self, **data):
208 def data(self, **data):
209 '''insert data into item that's not shown in default output'''
209 '''insert data into item that's not shown in default output'''
210 data = pycompat.byteskwargs(data)
210 data = pycompat.byteskwargs(data)
211 self._item.update(data)
211 self._item.update(data)
212 def write(self, fields, deftext, *fielddata, **opts):
212 def write(self, fields, deftext, *fielddata, **opts):
213 '''do default text output while assigning data to item'''
213 '''do default text output while assigning data to item'''
214 fieldkeys = fields.split()
214 fieldkeys = fields.split()
215 assert len(fieldkeys) == len(fielddata)
215 assert len(fieldkeys) == len(fielddata)
216 self._item.update(zip(fieldkeys, fielddata))
216 self._item.update(zip(fieldkeys, fielddata))
217 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
217 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
218 '''do conditional write (primarily for plain formatter)'''
218 '''do conditional write (primarily for plain formatter)'''
219 fieldkeys = fields.split()
219 fieldkeys = fields.split()
220 assert len(fieldkeys) == len(fielddata)
220 assert len(fieldkeys) == len(fielddata)
221 self._item.update(zip(fieldkeys, fielddata))
221 self._item.update(zip(fieldkeys, fielddata))
222 def plain(self, text, **opts):
222 def plain(self, text, **opts):
223 '''show raw text for non-templated mode'''
223 '''show raw text for non-templated mode'''
224 def isplain(self):
224 def isplain(self):
225 '''check for plain formatter usage'''
225 '''check for plain formatter usage'''
226 return False
226 return False
227 def nested(self, field, tmpl=None, sep=''):
227 def nested(self, field, tmpl=None, sep=''):
228 '''sub formatter to store nested data in the specified field'''
228 '''sub formatter to store nested data in the specified field'''
229 data = []
229 data = []
230 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
230 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
231 return _nestedformatter(self._ui, self._converter, data)
231 return _nestedformatter(self._ui, self._converter, data)
232 def end(self):
232 def end(self):
233 '''end output for the formatter'''
233 '''end output for the formatter'''
234 if self._item is not None:
234 if self._item is not None:
235 self._showitem()
235 self._showitem()
236
236
237 def nullformatter(ui, topic, opts):
237 def nullformatter(ui, topic, opts):
238 '''formatter that prints nothing'''
238 '''formatter that prints nothing'''
239 return baseformatter(ui, topic, opts, converter=_nullconverter)
239 return baseformatter(ui, topic, opts, converter=_nullconverter)
240
240
241 class _nestedformatter(baseformatter):
241 class _nestedformatter(baseformatter):
242 '''build sub items and store them in the parent formatter'''
242 '''build sub items and store them in the parent formatter'''
243 def __init__(self, ui, converter, data):
243 def __init__(self, ui, converter, data):
244 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
244 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
245 self._data = data
245 self._data = data
246 def _showitem(self):
246 def _showitem(self):
247 self._data.append(self._item)
247 self._data.append(self._item)
248
248
249 def _iteritems(data):
249 def _iteritems(data):
250 '''iterate key-value pairs in stable order'''
250 '''iterate key-value pairs in stable order'''
251 if isinstance(data, dict):
251 if isinstance(data, dict):
252 return sorted(data.iteritems())
252 return sorted(data.iteritems())
253 return data
253 return data
254
254
255 class _plainconverter(object):
255 class _plainconverter(object):
256 '''convert non-primitive data types to text'''
256 '''convert non-primitive data types to text'''
257
257
258 storecontext = False
258 storecontext = False
259
259
260 @staticmethod
260 @staticmethod
261 def wrapnested(data, tmpl, sep):
261 def wrapnested(data, tmpl, sep):
262 raise error.ProgrammingError('plainformatter should never be nested')
262 raise error.ProgrammingError('plainformatter should never be nested')
263 @staticmethod
263 @staticmethod
264 def formatdate(date, fmt):
264 def formatdate(date, fmt):
265 '''stringify date tuple in the given format'''
265 '''stringify date tuple in the given format'''
266 return dateutil.datestr(date, fmt)
266 return dateutil.datestr(date, fmt)
267 @staticmethod
267 @staticmethod
268 def formatdict(data, key, value, fmt, sep):
268 def formatdict(data, key, value, fmt, sep):
269 '''stringify key-value pairs separated by sep'''
269 '''stringify key-value pairs separated by sep'''
270 prefmt = pycompat.identity
270 prefmt = pycompat.identity
271 if fmt is None:
271 if fmt is None:
272 fmt = '%s=%s'
272 fmt = '%s=%s'
273 prefmt = pycompat.bytestr
273 prefmt = pycompat.bytestr
274 return sep.join(fmt % (prefmt(k), prefmt(v))
274 return sep.join(fmt % (prefmt(k), prefmt(v))
275 for k, v in _iteritems(data))
275 for k, v in _iteritems(data))
276 @staticmethod
276 @staticmethod
277 def formatlist(data, name, fmt, sep):
277 def formatlist(data, name, fmt, sep):
278 '''stringify iterable separated by sep'''
278 '''stringify iterable separated by sep'''
279 prefmt = pycompat.identity
279 prefmt = pycompat.identity
280 if fmt is None:
280 if fmt is None:
281 fmt = '%s'
281 fmt = '%s'
282 prefmt = pycompat.bytestr
282 prefmt = pycompat.bytestr
283 return sep.join(fmt % prefmt(e) for e in data)
283 return sep.join(fmt % prefmt(e) for e in data)
284
284
285 class plainformatter(baseformatter):
285 class plainformatter(baseformatter):
286 '''the default text output scheme'''
286 '''the default text output scheme'''
287 def __init__(self, ui, out, topic, opts):
287 def __init__(self, ui, out, topic, opts):
288 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
288 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
289 if ui.debugflag:
289 if ui.debugflag:
290 self.hexfunc = hex
290 self.hexfunc = hex
291 else:
291 else:
292 self.hexfunc = short
292 self.hexfunc = short
293 if ui is out:
293 if ui is out:
294 self._write = ui.write
294 self._write = ui.write
295 else:
295 else:
296 self._write = lambda s, **opts: out.write(s)
296 self._write = lambda s, **opts: out.write(s)
297 def startitem(self):
297 def startitem(self):
298 pass
298 pass
299 def data(self, **data):
299 def data(self, **data):
300 pass
300 pass
301 def write(self, fields, deftext, *fielddata, **opts):
301 def write(self, fields, deftext, *fielddata, **opts):
302 self._write(deftext % fielddata, **opts)
302 self._write(deftext % fielddata, **opts)
303 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
303 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
304 '''do conditional write'''
304 '''do conditional write'''
305 if cond:
305 if cond:
306 self._write(deftext % fielddata, **opts)
306 self._write(deftext % fielddata, **opts)
307 def plain(self, text, **opts):
307 def plain(self, text, **opts):
308 self._write(text, **opts)
308 self._write(text, **opts)
309 def isplain(self):
309 def isplain(self):
310 return True
310 return True
311 def nested(self, field, tmpl=None, sep=''):
311 def nested(self, field, tmpl=None, sep=''):
312 # nested data will be directly written to ui
312 # nested data will be directly written to ui
313 return self
313 return self
314 def end(self):
314 def end(self):
315 pass
315 pass
316
316
317 class debugformatter(baseformatter):
317 class debugformatter(baseformatter):
318 def __init__(self, ui, out, topic, opts):
318 def __init__(self, ui, out, topic, opts):
319 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
319 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
320 self._out = out
320 self._out = out
321 self._out.write("%s = [\n" % self._topic)
321 self._out.write("%s = [\n" % self._topic)
322 def _showitem(self):
322 def _showitem(self):
323 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
323 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
324 def end(self):
324 def end(self):
325 baseformatter.end(self)
325 baseformatter.end(self)
326 self._out.write("]\n")
326 self._out.write("]\n")
327
327
328 class pickleformatter(baseformatter):
328 class pickleformatter(baseformatter):
329 def __init__(self, ui, out, topic, opts):
329 def __init__(self, ui, out, topic, opts):
330 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
330 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
331 self._out = out
331 self._out = out
332 self._data = []
332 self._data = []
333 def _showitem(self):
333 def _showitem(self):
334 self._data.append(self._item)
334 self._data.append(self._item)
335 def end(self):
335 def end(self):
336 baseformatter.end(self)
336 baseformatter.end(self)
337 self._out.write(pickle.dumps(self._data))
337 self._out.write(pickle.dumps(self._data))
338
338
339 class jsonformatter(baseformatter):
339 class jsonformatter(baseformatter):
340 def __init__(self, ui, out, topic, opts):
340 def __init__(self, ui, out, topic, opts):
341 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
341 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
342 self._out = out
342 self._out = out
343 self._out.write("[")
343 self._out.write("[")
344 self._first = True
344 self._first = True
345 def _showitem(self):
345 def _showitem(self):
346 if self._first:
346 if self._first:
347 self._first = False
347 self._first = False
348 else:
348 else:
349 self._out.write(",")
349 self._out.write(",")
350
350
351 self._out.write("\n {\n")
351 self._out.write("\n {\n")
352 first = True
352 first = True
353 for k, v in sorted(self._item.items()):
353 for k, v in sorted(self._item.items()):
354 if first:
354 if first:
355 first = False
355 first = False
356 else:
356 else:
357 self._out.write(",\n")
357 self._out.write(",\n")
358 u = templatefilters.json(v, paranoid=False)
358 u = templatefilters.json(v, paranoid=False)
359 self._out.write(' "%s": %s' % (k, u))
359 self._out.write(' "%s": %s' % (k, u))
360 self._out.write("\n }")
360 self._out.write("\n }")
361 def end(self):
361 def end(self):
362 baseformatter.end(self)
362 baseformatter.end(self)
363 self._out.write("\n]\n")
363 self._out.write("\n]\n")
364
364
365 class _templateconverter(object):
365 class _templateconverter(object):
366 '''convert non-primitive data types to be processed by templater'''
366 '''convert non-primitive data types to be processed by templater'''
367
367
368 storecontext = True
368 storecontext = True
369
369
370 @staticmethod
370 @staticmethod
371 def wrapnested(data, tmpl, sep):
371 def wrapnested(data, tmpl, sep):
372 '''wrap nested data by templatable type'''
372 '''wrap nested data by templatable type'''
373 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
373 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
374 @staticmethod
374 @staticmethod
375 def formatdate(date, fmt):
375 def formatdate(date, fmt):
376 '''return date tuple'''
376 '''return date tuple'''
377 return templateutil.date(date)
377 return templateutil.date(date)
378 @staticmethod
378 @staticmethod
379 def formatdict(data, key, value, fmt, sep):
379 def formatdict(data, key, value, fmt, sep):
380 '''build object that can be evaluated as either plain string or dict'''
380 '''build object that can be evaluated as either plain string or dict'''
381 data = util.sortdict(_iteritems(data))
381 data = util.sortdict(_iteritems(data))
382 def f():
382 def f():
383 yield _plainconverter.formatdict(data, key, value, fmt, sep)
383 yield _plainconverter.formatdict(data, key, value, fmt, sep)
384 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
384 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
385 gen=f)
385 gen=f)
386 @staticmethod
386 @staticmethod
387 def formatlist(data, name, fmt, sep):
387 def formatlist(data, name, fmt, sep):
388 '''build object that can be evaluated as either plain string or list'''
388 '''build object that can be evaluated as either plain string or list'''
389 data = list(data)
389 data = list(data)
390 def f():
390 def f():
391 yield _plainconverter.formatlist(data, name, fmt, sep)
391 yield _plainconverter.formatlist(data, name, fmt, sep)
392 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
392 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
393
393
394 class templateformatter(baseformatter):
394 class templateformatter(baseformatter):
395 def __init__(self, ui, out, topic, opts):
395 def __init__(self, ui, out, topic, opts):
396 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
396 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
397 self._out = out
397 self._out = out
398 spec = lookuptemplate(ui, topic, opts.get('template', ''))
398 spec = lookuptemplate(ui, topic, opts.get('template', ''))
399 self._tref = spec.ref
399 self._tref = spec.ref
400 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
400 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
401 resources=templateresources(ui),
401 resources=templateresources(ui),
402 cache=templatekw.defaulttempl)
402 cache=templatekw.defaulttempl)
403 self._parts = templatepartsmap(spec, self._t,
403 self._parts = templatepartsmap(spec, self._t,
404 ['docheader', 'docfooter', 'separator'])
404 ['docheader', 'docfooter', 'separator'])
405 self._counter = itertools.count()
405 self._counter = itertools.count()
406 self._renderitem('docheader', {})
406 self._renderitem('docheader', {})
407
407
408 def _showitem(self):
408 def _showitem(self):
409 item = self._item.copy()
409 item = self._item.copy()
410 item['index'] = index = next(self._counter)
410 item['index'] = index = next(self._counter)
411 if index > 0:
411 if index > 0:
412 self._renderitem('separator', {})
412 self._renderitem('separator', {})
413 self._renderitem(self._tref, item)
413 self._renderitem(self._tref, item)
414
414
415 def _renderitem(self, part, item):
415 def _renderitem(self, part, item):
416 if part not in self._parts:
416 if part not in self._parts:
417 return
417 return
418 ref = self._parts[part]
418 ref = self._parts[part]
419 self._out.write(self._t.render(ref, item))
419 self._out.write(self._t.render(ref, item))
420
420
421 @util.propertycache
421 @util.propertycache
422 def _symbolsused(self):
422 def _symbolsused(self):
423 return self._t.symbolsused(self._tref)
423 return self._t.symbolsused(self._tref)
424
424
425 def contexthint(self, datafields):
425 def contexthint(self, datafields):
426 '''set of context object keys to be required by the template, given
426 '''set of context object keys to be required by the template, given
427 datafields overridden by immediate values'''
427 datafields overridden by immediate values'''
428 requires = set()
428 requires = set()
429 ksyms, fsyms = self._symbolsused
429 ksyms, fsyms = self._symbolsused
430 ksyms = ksyms - set(datafields.split()) # exclude immediate fields
430 ksyms = ksyms - set(datafields.split()) # exclude immediate fields
431 symtables = [(ksyms, templatekw.keywords),
431 symtables = [(ksyms, templatekw.keywords),
432 (fsyms, templatefuncs.funcs)]
432 (fsyms, templatefuncs.funcs)]
433 for syms, table in symtables:
433 for syms, table in symtables:
434 for k in syms:
434 for k in syms:
435 f = table.get(k)
435 f = table.get(k)
436 if not f:
436 if not f:
437 continue
437 continue
438 requires.update(getattr(f, '_requires', ()))
438 requires.update(getattr(f, '_requires', ()))
439 if 'repo' in requires:
439 if 'repo' in requires:
440 requires.add('ctx') # there's no API to pass repo to formatter
440 requires.add('ctx') # there's no API to pass repo to formatter
441 return requires & {'ctx', 'fctx'}
441 return requires & {'ctx', 'fctx'}
442
442
443 def datahint(self):
443 def datahint(self):
444 '''set of field names to be referenced from the template'''
444 '''set of field names to be referenced from the template'''
445 return self._symbolsused[0]
445 return self._symbolsused[0]
446
446
447 def end(self):
447 def end(self):
448 baseformatter.end(self)
448 baseformatter.end(self)
449 self._renderitem('docfooter', {})
449 self._renderitem('docfooter', {})
450
450
451 @attr.s(frozen=True)
451 @attr.s(frozen=True)
452 class templatespec(object):
452 class templatespec(object):
453 ref = attr.ib()
453 ref = attr.ib()
454 tmpl = attr.ib()
454 tmpl = attr.ib()
455 mapfile = attr.ib()
455 mapfile = attr.ib()
456
456
457 def lookuptemplate(ui, topic, tmpl):
457 def lookuptemplate(ui, topic, tmpl):
458 """Find the template matching the given -T/--template spec 'tmpl'
458 """Find the template matching the given -T/--template spec 'tmpl'
459
459
460 'tmpl' can be any of the following:
460 'tmpl' can be any of the following:
461
461
462 - a literal template (e.g. '{rev}')
462 - a literal template (e.g. '{rev}')
463 - a map-file name or path (e.g. 'changelog')
463 - a map-file name or path (e.g. 'changelog')
464 - a reference to [templates] in config file
464 - a reference to [templates] in config file
465 - a path to raw template file
465 - a path to raw template file
466
466
467 A map file defines a stand-alone template environment. If a map file
467 A map file defines a stand-alone template environment. If a map file
468 selected, all templates defined in the file will be loaded, and the
468 selected, all templates defined in the file will be loaded, and the
469 template matching the given topic will be rendered. Aliases won't be
469 template matching the given topic will be rendered. Aliases won't be
470 loaded from user config, but from the map file.
470 loaded from user config, but from the map file.
471
471
472 If no map file selected, all templates in [templates] section will be
472 If no map file selected, all templates in [templates] section will be
473 available as well as aliases in [templatealias].
473 available as well as aliases in [templatealias].
474 """
474 """
475
475
476 # looks like a literal template?
476 # looks like a literal template?
477 if '{' in tmpl:
477 if '{' in tmpl:
478 return templatespec('', tmpl, None)
478 return templatespec('', tmpl, None)
479
479
480 # perhaps a stock style?
480 # perhaps a stock style?
481 if not os.path.split(tmpl)[0]:
481 if not os.path.split(tmpl)[0]:
482 mapname = (templater.templatepath('map-cmdline.' + tmpl)
482 mapname = (templater.templatepath('map-cmdline.' + tmpl)
483 or templater.templatepath(tmpl))
483 or templater.templatepath(tmpl))
484 if mapname and os.path.isfile(mapname):
484 if mapname and os.path.isfile(mapname):
485 return templatespec(topic, None, mapname)
485 return templatespec(topic, None, mapname)
486
486
487 # perhaps it's a reference to [templates]
487 # perhaps it's a reference to [templates]
488 if ui.config('templates', tmpl):
488 if ui.config('templates', tmpl):
489 return templatespec(tmpl, None, None)
489 return templatespec(tmpl, None, None)
490
490
491 if tmpl == 'list':
491 if tmpl == 'list':
492 ui.write(_("available styles: %s\n") % templater.stylelist())
492 ui.write(_("available styles: %s\n") % templater.stylelist())
493 raise error.Abort(_("specify a template"))
493 raise error.Abort(_("specify a template"))
494
494
495 # perhaps it's a path to a map or a template
495 # perhaps it's a path to a map or a template
496 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
496 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
497 # is it a mapfile for a style?
497 # is it a mapfile for a style?
498 if os.path.basename(tmpl).startswith("map-"):
498 if os.path.basename(tmpl).startswith("map-"):
499 return templatespec(topic, None, os.path.realpath(tmpl))
499 return templatespec(topic, None, os.path.realpath(tmpl))
500 with util.posixfile(tmpl, 'rb') as f:
500 with util.posixfile(tmpl, 'rb') as f:
501 tmpl = f.read()
501 tmpl = f.read()
502 return templatespec('', tmpl, None)
502 return templatespec('', tmpl, None)
503
503
504 # constant string?
504 # constant string?
505 return templatespec('', tmpl, None)
505 return templatespec('', tmpl, None)
506
506
507 def templatepartsmap(spec, t, partnames):
507 def templatepartsmap(spec, t, partnames):
508 """Create a mapping of {part: ref}"""
508 """Create a mapping of {part: ref}"""
509 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
509 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
510 if spec.mapfile:
510 if spec.mapfile:
511 partsmap.update((p, p) for p in partnames if p in t)
511 partsmap.update((p, p) for p in partnames if p in t)
512 elif spec.ref:
512 elif spec.ref:
513 for part in partnames:
513 for part in partnames:
514 ref = '%s:%s' % (spec.ref, part) # select config sub-section
514 ref = '%s:%s' % (spec.ref, part) # select config sub-section
515 if ref in t:
515 if ref in t:
516 partsmap[part] = ref
516 partsmap[part] = ref
517 return partsmap
517 return partsmap
518
518
519 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
519 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
520 """Create a templater from either a literal template or loading from
520 """Create a templater from either a literal template or loading from
521 a map file"""
521 a map file"""
522 assert not (spec.tmpl and spec.mapfile)
522 assert not (spec.tmpl and spec.mapfile)
523 if spec.mapfile:
523 if spec.mapfile:
524 frommapfile = templater.templater.frommapfile
524 frommapfile = templater.templater.frommapfile
525 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
525 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
526 cache=cache)
526 cache=cache)
527 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
527 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
528 cache=cache)
528 cache=cache)
529
529
530 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
530 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
531 """Create a templater from a string template 'tmpl'"""
531 """Create a templater from a string template 'tmpl'"""
532 aliases = ui.configitems('templatealias')
532 aliases = ui.configitems('templatealias')
533 t = templater.templater(defaults=defaults, resources=resources,
533 t = templater.templater(defaults=defaults, resources=resources,
534 cache=cache, aliases=aliases)
534 cache=cache, aliases=aliases)
535 t.cache.update((k, templater.unquotestring(v))
535 t.cache.update((k, templater.unquotestring(v))
536 for k, v in ui.configitems('templates'))
536 for k, v in ui.configitems('templates'))
537 if tmpl:
537 if tmpl:
538 t.cache[''] = tmpl
538 t.cache[''] = tmpl
539 return t
539 return t
540
540
541 class templateresources(templater.resourcemapper):
541 class templateresources(templater.resourcemapper):
542 """Resource mapper designed for the default templatekw and function"""
542 """Resource mapper designed for the default templatekw and function"""
543
543
544 def __init__(self, ui, repo=None):
544 def __init__(self, ui, repo=None):
545 self._resmap = {
545 self._resmap = {
546 'cache': {}, # for templatekw/funcs to store reusable data
546 'cache': {}, # for templatekw/funcs to store reusable data
547 'repo': repo,
547 'repo': repo,
548 'ui': ui,
548 'ui': ui,
549 }
549 }
550
550
551 def availablekeys(self, context, mapping):
551 def availablekeys(self, mapping):
552 return {k for k, g in self._gettermap.iteritems()
552 return {k for k, g in self._gettermap.iteritems()
553 if g(self, context, mapping, k) is not None}
553 if g(self, mapping, k) is not None}
554
554
555 def knownkeys(self):
555 def knownkeys(self):
556 return self._knownkeys
556 return self._knownkeys
557
557
558 def lookup(self, context, mapping, key):
558 def lookup(self, mapping, key):
559 get = self._gettermap.get(key)
559 get = self._gettermap.get(key)
560 if not get:
560 if not get:
561 return None
561 return None
562 return get(self, context, mapping, key)
562 return get(self, mapping, key)
563
563
564 def populatemap(self, context, origmapping, newmapping):
564 def populatemap(self, context, origmapping, newmapping):
565 mapping = {}
565 mapping = {}
566 if self._hasctx(newmapping):
566 if self._hasctx(newmapping):
567 mapping['revcache'] = {} # per-ctx cache
567 mapping['revcache'] = {} # per-ctx cache
568 if (('node' in origmapping or self._hasctx(origmapping))
568 if (('node' in origmapping or self._hasctx(origmapping))
569 and ('node' in newmapping or self._hasctx(newmapping))):
569 and ('node' in newmapping or self._hasctx(newmapping))):
570 orignode = templateutil.runsymbol(context, origmapping, 'node')
570 orignode = templateutil.runsymbol(context, origmapping, 'node')
571 mapping['originalnode'] = orignode
571 mapping['originalnode'] = orignode
572 return mapping
572 return mapping
573
573
574 def _getsome(self, context, mapping, key):
574 def _getsome(self, mapping, key):
575 v = mapping.get(key)
575 v = mapping.get(key)
576 if v is not None:
576 if v is not None:
577 return v
577 return v
578 return self._resmap.get(key)
578 return self._resmap.get(key)
579
579
580 def _hasctx(self, mapping):
580 def _hasctx(self, mapping):
581 return 'ctx' in mapping or 'fctx' in mapping
581 return 'ctx' in mapping or 'fctx' in mapping
582
582
583 def _getctx(self, context, mapping, key):
583 def _getctx(self, mapping, key):
584 ctx = mapping.get('ctx')
584 ctx = mapping.get('ctx')
585 if ctx is not None:
585 if ctx is not None:
586 return ctx
586 return ctx
587 fctx = mapping.get('fctx')
587 fctx = mapping.get('fctx')
588 if fctx is not None:
588 if fctx is not None:
589 return fctx.changectx()
589 return fctx.changectx()
590
590
591 def _getrepo(self, context, mapping, key):
591 def _getrepo(self, mapping, key):
592 ctx = self._getctx(context, mapping, 'ctx')
592 ctx = self._getctx(mapping, 'ctx')
593 if ctx is not None:
593 if ctx is not None:
594 return ctx.repo()
594 return ctx.repo()
595 return self._getsome(context, mapping, key)
595 return self._getsome(mapping, key)
596
596
597 _gettermap = {
597 _gettermap = {
598 'cache': _getsome,
598 'cache': _getsome,
599 'ctx': _getctx,
599 'ctx': _getctx,
600 'fctx': _getsome,
600 'fctx': _getsome,
601 'repo': _getrepo,
601 'repo': _getrepo,
602 'revcache': _getsome,
602 'revcache': _getsome,
603 'ui': _getsome,
603 'ui': _getsome,
604 }
604 }
605 _knownkeys = set(_gettermap.keys())
605 _knownkeys = set(_gettermap.keys())
606
606
607 def formatter(ui, out, topic, opts):
607 def formatter(ui, out, topic, opts):
608 template = opts.get("template", "")
608 template = opts.get("template", "")
609 if template == "json":
609 if template == "json":
610 return jsonformatter(ui, out, topic, opts)
610 return jsonformatter(ui, out, topic, opts)
611 elif template == "pickle":
611 elif template == "pickle":
612 return pickleformatter(ui, out, topic, opts)
612 return pickleformatter(ui, out, topic, opts)
613 elif template == "debug":
613 elif template == "debug":
614 return debugformatter(ui, out, topic, opts)
614 return debugformatter(ui, out, topic, opts)
615 elif template != "":
615 elif template != "":
616 return templateformatter(ui, out, topic, opts)
616 return templateformatter(ui, out, topic, opts)
617 # developer config: ui.formatdebug
617 # developer config: ui.formatdebug
618 elif ui.configbool('ui', 'formatdebug'):
618 elif ui.configbool('ui', 'formatdebug'):
619 return debugformatter(ui, out, topic, opts)
619 return debugformatter(ui, out, topic, opts)
620 # deprecated config: ui.formatjson
620 # deprecated config: ui.formatjson
621 elif ui.configbool('ui', 'formatjson'):
621 elif ui.configbool('ui', 'formatjson'):
622 return jsonformatter(ui, out, topic, opts)
622 return jsonformatter(ui, out, topic, opts)
623 return plainformatter(ui, out, topic, opts)
623 return plainformatter(ui, out, topic, opts)
624
624
625 @contextlib.contextmanager
625 @contextlib.contextmanager
626 def openformatter(ui, filename, topic, opts):
626 def openformatter(ui, filename, topic, opts):
627 """Create a formatter that writes outputs to the specified file
627 """Create a formatter that writes outputs to the specified file
628
628
629 Must be invoked using the 'with' statement.
629 Must be invoked using the 'with' statement.
630 """
630 """
631 with util.posixfile(filename, 'wb') as out:
631 with util.posixfile(filename, 'wb') as out:
632 with formatter(ui, out, topic, opts) as fm:
632 with formatter(ui, out, topic, opts) as fm:
633 yield fm
633 yield fm
634
634
635 @contextlib.contextmanager
635 @contextlib.contextmanager
636 def _neverending(fm):
636 def _neverending(fm):
637 yield fm
637 yield fm
638
638
639 def maybereopen(fm, filename):
639 def maybereopen(fm, filename):
640 """Create a formatter backed by file if filename specified, else return
640 """Create a formatter backed by file if filename specified, else return
641 the given formatter
641 the given formatter
642
642
643 Must be invoked using the 'with' statement. This will never call fm.end()
643 Must be invoked using the 'with' statement. This will never call fm.end()
644 of the given formatter.
644 of the given formatter.
645 """
645 """
646 if filename:
646 if filename:
647 return openformatter(fm._ui, filename, fm._topic, fm._opts)
647 return openformatter(fm._ui, filename, fm._topic, fm._opts)
648 else:
648 else:
649 return _neverending(fm)
649 return _neverending(fm)
@@ -1,985 +1,985 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 None
29 None
30 sometimes represents an empty value, which can be stringified to ''.
30 sometimes represents an empty value, which can be stringified to ''.
31
31
32 True, False, int, float
32 True, False, int, float
33 can be stringified as such.
33 can be stringified as such.
34
34
35 wrappedbytes, wrappedvalue
35 wrappedbytes, wrappedvalue
36 a wrapper for the above printable types.
36 a wrapper for the above printable types.
37
37
38 date
38 date
39 represents a (unixtime, offset) tuple.
39 represents a (unixtime, offset) tuple.
40
40
41 hybrid
41 hybrid
42 represents a list/dict of printable values, which can also be converted
42 represents a list/dict of printable values, which can also be converted
43 to mappings by % operator.
43 to mappings by % operator.
44
44
45 hybriditem
45 hybriditem
46 represents a scalar printable value, also supports % operator.
46 represents a scalar printable value, also supports % operator.
47
47
48 mappinggenerator, mappinglist
48 mappinggenerator, mappinglist
49 represents mappings (i.e. a list of dicts), which may have default
49 represents mappings (i.e. a list of dicts), which may have default
50 output format.
50 output format.
51
51
52 mappedgenerator
52 mappedgenerator
53 a lazily-evaluated list of byte strings, which is e.g. a result of %
53 a lazily-evaluated list of byte strings, which is e.g. a result of %
54 operation.
54 operation.
55 """
55 """
56
56
57 from __future__ import absolute_import, print_function
57 from __future__ import absolute_import, print_function
58
58
59 import abc
59 import abc
60 import os
60 import os
61
61
62 from .i18n import _
62 from .i18n import _
63 from . import (
63 from . import (
64 config,
64 config,
65 encoding,
65 encoding,
66 error,
66 error,
67 parser,
67 parser,
68 pycompat,
68 pycompat,
69 templatefilters,
69 templatefilters,
70 templatefuncs,
70 templatefuncs,
71 templateutil,
71 templateutil,
72 util,
72 util,
73 )
73 )
74 from .utils import (
74 from .utils import (
75 stringutil,
75 stringutil,
76 )
76 )
77
77
78 # template parsing
78 # template parsing
79
79
80 elements = {
80 elements = {
81 # token-type: binding-strength, primary, prefix, infix, suffix
81 # token-type: binding-strength, primary, prefix, infix, suffix
82 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
82 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
83 ".": (18, None, None, (".", 18), None),
83 ".": (18, None, None, (".", 18), None),
84 "%": (15, None, None, ("%", 15), None),
84 "%": (15, None, None, ("%", 15), None),
85 "|": (15, None, None, ("|", 15), None),
85 "|": (15, None, None, ("|", 15), None),
86 "*": (5, None, None, ("*", 5), None),
86 "*": (5, None, None, ("*", 5), None),
87 "/": (5, None, None, ("/", 5), None),
87 "/": (5, None, None, ("/", 5), None),
88 "+": (4, None, None, ("+", 4), None),
88 "+": (4, None, None, ("+", 4), None),
89 "-": (4, None, ("negate", 19), ("-", 4), None),
89 "-": (4, None, ("negate", 19), ("-", 4), None),
90 "=": (3, None, None, ("keyvalue", 3), None),
90 "=": (3, None, None, ("keyvalue", 3), None),
91 ",": (2, None, None, ("list", 2), None),
91 ",": (2, None, None, ("list", 2), None),
92 ")": (0, None, None, None, None),
92 ")": (0, None, None, None, None),
93 "integer": (0, "integer", None, None, None),
93 "integer": (0, "integer", None, None, None),
94 "symbol": (0, "symbol", None, None, None),
94 "symbol": (0, "symbol", None, None, None),
95 "string": (0, "string", None, None, None),
95 "string": (0, "string", None, None, None),
96 "template": (0, "template", None, None, None),
96 "template": (0, "template", None, None, None),
97 "end": (0, None, None, None, None),
97 "end": (0, None, None, None, None),
98 }
98 }
99
99
100 def tokenize(program, start, end, term=None):
100 def tokenize(program, start, end, term=None):
101 """Parse a template expression into a stream of tokens, which must end
101 """Parse a template expression into a stream of tokens, which must end
102 with term if specified"""
102 with term if specified"""
103 pos = start
103 pos = start
104 program = pycompat.bytestr(program)
104 program = pycompat.bytestr(program)
105 while pos < end:
105 while pos < end:
106 c = program[pos]
106 c = program[pos]
107 if c.isspace(): # skip inter-token whitespace
107 if c.isspace(): # skip inter-token whitespace
108 pass
108 pass
109 elif c in "(=,).%|+-*/": # handle simple operators
109 elif c in "(=,).%|+-*/": # handle simple operators
110 yield (c, None, pos)
110 yield (c, None, pos)
111 elif c in '"\'': # handle quoted templates
111 elif c in '"\'': # handle quoted templates
112 s = pos + 1
112 s = pos + 1
113 data, pos = _parsetemplate(program, s, end, c)
113 data, pos = _parsetemplate(program, s, end, c)
114 yield ('template', data, s)
114 yield ('template', data, s)
115 pos -= 1
115 pos -= 1
116 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
116 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
117 # handle quoted strings
117 # handle quoted strings
118 c = program[pos + 1]
118 c = program[pos + 1]
119 s = pos = pos + 2
119 s = pos = pos + 2
120 while pos < end: # find closing quote
120 while pos < end: # find closing quote
121 d = program[pos]
121 d = program[pos]
122 if d == '\\': # skip over escaped characters
122 if d == '\\': # skip over escaped characters
123 pos += 2
123 pos += 2
124 continue
124 continue
125 if d == c:
125 if d == c:
126 yield ('string', program[s:pos], s)
126 yield ('string', program[s:pos], s)
127 break
127 break
128 pos += 1
128 pos += 1
129 else:
129 else:
130 raise error.ParseError(_("unterminated string"), s)
130 raise error.ParseError(_("unterminated string"), s)
131 elif c.isdigit():
131 elif c.isdigit():
132 s = pos
132 s = pos
133 while pos < end:
133 while pos < end:
134 d = program[pos]
134 d = program[pos]
135 if not d.isdigit():
135 if not d.isdigit():
136 break
136 break
137 pos += 1
137 pos += 1
138 yield ('integer', program[s:pos], s)
138 yield ('integer', program[s:pos], s)
139 pos -= 1
139 pos -= 1
140 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
140 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
141 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
141 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,
142 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
143 # where some of nested templates were preprocessed as strings and
143 # where some of nested templates were preprocessed as strings and
144 # then compiled. therefore, \"...\" was allowed. (issue4733)
144 # then compiled. therefore, \"...\" was allowed. (issue4733)
145 #
145 #
146 # processing flow of _evalifliteral() at 5ab28a2e9962:
146 # processing flow of _evalifliteral() at 5ab28a2e9962:
147 # outer template string -> stringify() -> compiletemplate()
147 # outer template string -> stringify() -> compiletemplate()
148 # ------------------------ ------------ ------------------
148 # ------------------------ ------------ ------------------
149 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
149 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
150 # ~~~~~~~~
150 # ~~~~~~~~
151 # escaped quoted string
151 # escaped quoted string
152 if c == 'r':
152 if c == 'r':
153 pos += 1
153 pos += 1
154 token = 'string'
154 token = 'string'
155 else:
155 else:
156 token = 'template'
156 token = 'template'
157 quote = program[pos:pos + 2]
157 quote = program[pos:pos + 2]
158 s = pos = pos + 2
158 s = pos = pos + 2
159 while pos < end: # find closing escaped quote
159 while pos < end: # find closing escaped quote
160 if program.startswith('\\\\\\', pos, end):
160 if program.startswith('\\\\\\', pos, end):
161 pos += 4 # skip over double escaped characters
161 pos += 4 # skip over double escaped characters
162 continue
162 continue
163 if program.startswith(quote, pos, end):
163 if program.startswith(quote, pos, end):
164 # interpret as if it were a part of an outer string
164 # interpret as if it were a part of an outer string
165 data = parser.unescapestr(program[s:pos])
165 data = parser.unescapestr(program[s:pos])
166 if token == 'template':
166 if token == 'template':
167 data = _parsetemplate(data, 0, len(data))[0]
167 data = _parsetemplate(data, 0, len(data))[0]
168 yield (token, data, s)
168 yield (token, data, s)
169 pos += 1
169 pos += 1
170 break
170 break
171 pos += 1
171 pos += 1
172 else:
172 else:
173 raise error.ParseError(_("unterminated string"), s)
173 raise error.ParseError(_("unterminated string"), s)
174 elif c.isalnum() or c in '_':
174 elif c.isalnum() or c in '_':
175 s = pos
175 s = pos
176 pos += 1
176 pos += 1
177 while pos < end: # find end of symbol
177 while pos < end: # find end of symbol
178 d = program[pos]
178 d = program[pos]
179 if not (d.isalnum() or d == "_"):
179 if not (d.isalnum() or d == "_"):
180 break
180 break
181 pos += 1
181 pos += 1
182 sym = program[s:pos]
182 sym = program[s:pos]
183 yield ('symbol', sym, s)
183 yield ('symbol', sym, s)
184 pos -= 1
184 pos -= 1
185 elif c == term:
185 elif c == term:
186 yield ('end', None, pos)
186 yield ('end', None, pos)
187 return
187 return
188 else:
188 else:
189 raise error.ParseError(_("syntax error"), pos)
189 raise error.ParseError(_("syntax error"), pos)
190 pos += 1
190 pos += 1
191 if term:
191 if term:
192 raise error.ParseError(_("unterminated template expansion"), start)
192 raise error.ParseError(_("unterminated template expansion"), start)
193 yield ('end', None, pos)
193 yield ('end', None, pos)
194
194
195 def _parsetemplate(tmpl, start, stop, quote=''):
195 def _parsetemplate(tmpl, start, stop, quote=''):
196 r"""
196 r"""
197 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
197 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
198 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
198 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
199 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
199 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
200 ([('string', 'foo'), ('symbol', 'bar')], 9)
200 ([('string', 'foo'), ('symbol', 'bar')], 9)
201 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
201 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
202 ([('string', 'foo')], 4)
202 ([('string', 'foo')], 4)
203 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
203 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
204 ([('string', 'foo"'), ('string', 'bar')], 9)
204 ([('string', 'foo"'), ('string', 'bar')], 9)
205 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
205 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
206 ([('string', 'foo\\')], 6)
206 ([('string', 'foo\\')], 6)
207 """
207 """
208 parsed = []
208 parsed = []
209 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
209 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
210 if typ == 'string':
210 if typ == 'string':
211 parsed.append((typ, val))
211 parsed.append((typ, val))
212 elif typ == 'template':
212 elif typ == 'template':
213 parsed.append(val)
213 parsed.append(val)
214 elif typ == 'end':
214 elif typ == 'end':
215 return parsed, pos
215 return parsed, pos
216 else:
216 else:
217 raise error.ProgrammingError('unexpected type: %s' % typ)
217 raise error.ProgrammingError('unexpected type: %s' % typ)
218 raise error.ProgrammingError('unterminated scanning of template')
218 raise error.ProgrammingError('unterminated scanning of template')
219
219
220 def scantemplate(tmpl, raw=False):
220 def scantemplate(tmpl, raw=False):
221 r"""Scan (type, start, end) positions of outermost elements in template
221 r"""Scan (type, start, end) positions of outermost elements in template
222
222
223 If raw=True, a backslash is not taken as an escape character just like
223 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
224 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}'
225 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
226 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
227 'foo'.
227 'foo'.
228
228
229 >>> list(scantemplate(b'foo{bar}"baz'))
229 >>> list(scantemplate(b'foo{bar}"baz'))
230 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
230 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
231 >>> list(scantemplate(b'outer{"inner"}outer'))
231 >>> list(scantemplate(b'outer{"inner"}outer'))
232 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
232 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
233 >>> list(scantemplate(b'foo\\{escaped}'))
233 >>> list(scantemplate(b'foo\\{escaped}'))
234 [('string', 0, 5), ('string', 5, 13)]
234 [('string', 0, 5), ('string', 5, 13)]
235 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
235 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
236 [('string', 0, 4), ('template', 4, 13)]
236 [('string', 0, 4), ('template', 4, 13)]
237 """
237 """
238 last = None
238 last = None
239 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
239 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
240 if last:
240 if last:
241 yield last + (pos,)
241 yield last + (pos,)
242 if typ == 'end':
242 if typ == 'end':
243 return
243 return
244 else:
244 else:
245 last = (typ, pos)
245 last = (typ, pos)
246 raise error.ProgrammingError('unterminated scanning of template')
246 raise error.ProgrammingError('unterminated scanning of template')
247
247
248 def _scantemplate(tmpl, start, stop, quote='', raw=False):
248 def _scantemplate(tmpl, start, stop, quote='', raw=False):
249 """Parse template string into chunks of strings and template expressions"""
249 """Parse template string into chunks of strings and template expressions"""
250 sepchars = '{' + quote
250 sepchars = '{' + quote
251 unescape = [parser.unescapestr, pycompat.identity][raw]
251 unescape = [parser.unescapestr, pycompat.identity][raw]
252 pos = start
252 pos = start
253 p = parser.parser(elements)
253 p = parser.parser(elements)
254 try:
254 try:
255 while pos < stop:
255 while pos < stop:
256 n = min((tmpl.find(c, pos, stop)
256 n = min((tmpl.find(c, pos, stop)
257 for c in pycompat.bytestr(sepchars)),
257 for c in pycompat.bytestr(sepchars)),
258 key=lambda n: (n < 0, n))
258 key=lambda n: (n < 0, n))
259 if n < 0:
259 if n < 0:
260 yield ('string', unescape(tmpl[pos:stop]), pos)
260 yield ('string', unescape(tmpl[pos:stop]), pos)
261 pos = stop
261 pos = stop
262 break
262 break
263 c = tmpl[n:n + 1]
263 c = tmpl[n:n + 1]
264 bs = 0 # count leading backslashes
264 bs = 0 # count leading backslashes
265 if not raw:
265 if not raw:
266 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
266 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
267 if bs % 2 == 1:
267 if bs % 2 == 1:
268 # escaped (e.g. '\{', '\\\{', but not '\\{')
268 # escaped (e.g. '\{', '\\\{', but not '\\{')
269 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
269 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
270 pos = n + 1
270 pos = n + 1
271 continue
271 continue
272 if n > pos:
272 if n > pos:
273 yield ('string', unescape(tmpl[pos:n]), pos)
273 yield ('string', unescape(tmpl[pos:n]), pos)
274 if c == quote:
274 if c == quote:
275 yield ('end', None, n + 1)
275 yield ('end', None, n + 1)
276 return
276 return
277
277
278 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
278 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
279 if not tmpl.startswith('}', pos):
279 if not tmpl.startswith('}', pos):
280 raise error.ParseError(_("invalid token"), pos)
280 raise error.ParseError(_("invalid token"), pos)
281 yield ('template', parseres, n)
281 yield ('template', parseres, n)
282 pos += 1
282 pos += 1
283
283
284 if quote:
284 if quote:
285 raise error.ParseError(_("unterminated string"), start)
285 raise error.ParseError(_("unterminated string"), start)
286 except error.ParseError as inst:
286 except error.ParseError as inst:
287 if len(inst.args) > 1: # has location
287 if len(inst.args) > 1: # has location
288 loc = inst.args[1]
288 loc = inst.args[1]
289 # Offset the caret location by the number of newlines before the
289 # Offset the caret location by the number of newlines before the
290 # location of the error, since we will replace one-char newlines
290 # location of the error, since we will replace one-char newlines
291 # with the two-char literal r'\n'.
291 # with the two-char literal r'\n'.
292 offset = tmpl[:loc].count('\n')
292 offset = tmpl[:loc].count('\n')
293 tmpl = tmpl.replace('\n', br'\n')
293 tmpl = tmpl.replace('\n', br'\n')
294 # We want the caret to point to the place in the template that
294 # 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
295 # 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")
296 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
297 # to line up the caret with the location of the error.
297 # to line up the caret with the location of the error.
298 inst.hint = (tmpl + '\n'
298 inst.hint = (tmpl + '\n'
299 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
299 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
300 raise
300 raise
301 yield ('end', None, pos)
301 yield ('end', None, pos)
302
302
303 def _unnesttemplatelist(tree):
303 def _unnesttemplatelist(tree):
304 """Expand list of templates to node tuple
304 """Expand list of templates to node tuple
305
305
306 >>> def f(tree):
306 >>> def f(tree):
307 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
307 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
308 >>> f((b'template', []))
308 >>> f((b'template', []))
309 (string '')
309 (string '')
310 >>> f((b'template', [(b'string', b'foo')]))
310 >>> f((b'template', [(b'string', b'foo')]))
311 (string 'foo')
311 (string 'foo')
312 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
312 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
313 (template
313 (template
314 (string 'foo')
314 (string 'foo')
315 (symbol 'rev'))
315 (symbol 'rev'))
316 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
316 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
317 (template
317 (template
318 (symbol 'rev'))
318 (symbol 'rev'))
319 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
319 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
320 (string 'foo')
320 (string 'foo')
321 """
321 """
322 if not isinstance(tree, tuple):
322 if not isinstance(tree, tuple):
323 return tree
323 return tree
324 op = tree[0]
324 op = tree[0]
325 if op != 'template':
325 if op != 'template':
326 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
326 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
327
327
328 assert len(tree) == 2
328 assert len(tree) == 2
329 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
329 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
330 if not xs:
330 if not xs:
331 return ('string', '') # empty template ""
331 return ('string', '') # empty template ""
332 elif len(xs) == 1 and xs[0][0] == 'string':
332 elif len(xs) == 1 and xs[0][0] == 'string':
333 return xs[0] # fast path for string with no template fragment "x"
333 return xs[0] # fast path for string with no template fragment "x"
334 else:
334 else:
335 return (op,) + xs
335 return (op,) + xs
336
336
337 def parse(tmpl):
337 def parse(tmpl):
338 """Parse template string into tree"""
338 """Parse template string into tree"""
339 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
339 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
340 assert pos == len(tmpl), 'unquoted template should be consumed'
340 assert pos == len(tmpl), 'unquoted template should be consumed'
341 return _unnesttemplatelist(('template', parsed))
341 return _unnesttemplatelist(('template', parsed))
342
342
343 def _parseexpr(expr):
343 def _parseexpr(expr):
344 """Parse a template expression into tree
344 """Parse a template expression into tree
345
345
346 >>> _parseexpr(b'"foo"')
346 >>> _parseexpr(b'"foo"')
347 ('string', 'foo')
347 ('string', 'foo')
348 >>> _parseexpr(b'foo(bar)')
348 >>> _parseexpr(b'foo(bar)')
349 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
349 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
350 >>> _parseexpr(b'foo(')
350 >>> _parseexpr(b'foo(')
351 Traceback (most recent call last):
351 Traceback (most recent call last):
352 ...
352 ...
353 ParseError: ('not a prefix: end', 4)
353 ParseError: ('not a prefix: end', 4)
354 >>> _parseexpr(b'"foo" "bar"')
354 >>> _parseexpr(b'"foo" "bar"')
355 Traceback (most recent call last):
355 Traceback (most recent call last):
356 ...
356 ...
357 ParseError: ('invalid token', 7)
357 ParseError: ('invalid token', 7)
358 """
358 """
359 p = parser.parser(elements)
359 p = parser.parser(elements)
360 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
360 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
361 if pos != len(expr):
361 if pos != len(expr):
362 raise error.ParseError(_('invalid token'), pos)
362 raise error.ParseError(_('invalid token'), pos)
363 return _unnesttemplatelist(tree)
363 return _unnesttemplatelist(tree)
364
364
365 def prettyformat(tree):
365 def prettyformat(tree):
366 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
366 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
367
367
368 def compileexp(exp, context, curmethods):
368 def compileexp(exp, context, curmethods):
369 """Compile parsed template tree to (func, data) pair"""
369 """Compile parsed template tree to (func, data) pair"""
370 if not exp:
370 if not exp:
371 raise error.ParseError(_("missing argument"))
371 raise error.ParseError(_("missing argument"))
372 t = exp[0]
372 t = exp[0]
373 if t in curmethods:
373 if t in curmethods:
374 return curmethods[t](exp, context)
374 return curmethods[t](exp, context)
375 raise error.ParseError(_("unknown method '%s'") % t)
375 raise error.ParseError(_("unknown method '%s'") % t)
376
376
377 # template evaluation
377 # template evaluation
378
378
379 def getsymbol(exp):
379 def getsymbol(exp):
380 if exp[0] == 'symbol':
380 if exp[0] == 'symbol':
381 return exp[1]
381 return exp[1]
382 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
382 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
383
383
384 def getlist(x):
384 def getlist(x):
385 if not x:
385 if not x:
386 return []
386 return []
387 if x[0] == 'list':
387 if x[0] == 'list':
388 return getlist(x[1]) + [x[2]]
388 return getlist(x[1]) + [x[2]]
389 return [x]
389 return [x]
390
390
391 def gettemplate(exp, context):
391 def gettemplate(exp, context):
392 """Compile given template tree or load named template from map file;
392 """Compile given template tree or load named template from map file;
393 returns (func, data) pair"""
393 returns (func, data) pair"""
394 if exp[0] in ('template', 'string'):
394 if exp[0] in ('template', 'string'):
395 return compileexp(exp, context, methods)
395 return compileexp(exp, context, methods)
396 if exp[0] == 'symbol':
396 if exp[0] == 'symbol':
397 # unlike runsymbol(), here 'symbol' is always taken as template name
397 # unlike runsymbol(), here 'symbol' is always taken as template name
398 # even if it exists in mapping. this allows us to override mapping
398 # even if it exists in mapping. this allows us to override mapping
399 # by web templates, e.g. 'changelogtag' is redefined in map file.
399 # by web templates, e.g. 'changelogtag' is redefined in map file.
400 return context._load(exp[1])
400 return context._load(exp[1])
401 raise error.ParseError(_("expected template specifier"))
401 raise error.ParseError(_("expected template specifier"))
402
402
403 def _runrecursivesymbol(context, mapping, key):
403 def _runrecursivesymbol(context, mapping, key):
404 raise error.Abort(_("recursive reference '%s' in template") % key)
404 raise error.Abort(_("recursive reference '%s' in template") % key)
405
405
406 def buildtemplate(exp, context):
406 def buildtemplate(exp, context):
407 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
407 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
408 return (templateutil.runtemplate, ctmpl)
408 return (templateutil.runtemplate, ctmpl)
409
409
410 def buildfilter(exp, context):
410 def buildfilter(exp, context):
411 n = getsymbol(exp[2])
411 n = getsymbol(exp[2])
412 if n in context._filters:
412 if n in context._filters:
413 filt = context._filters[n]
413 filt = context._filters[n]
414 arg = compileexp(exp[1], context, methods)
414 arg = compileexp(exp[1], context, methods)
415 return (templateutil.runfilter, (arg, filt))
415 return (templateutil.runfilter, (arg, filt))
416 if n in context._funcs:
416 if n in context._funcs:
417 f = context._funcs[n]
417 f = context._funcs[n]
418 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
418 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
419 return (f, args)
419 return (f, args)
420 raise error.ParseError(_("unknown function '%s'") % n)
420 raise error.ParseError(_("unknown function '%s'") % n)
421
421
422 def buildmap(exp, context):
422 def buildmap(exp, context):
423 darg = compileexp(exp[1], context, methods)
423 darg = compileexp(exp[1], context, methods)
424 targ = gettemplate(exp[2], context)
424 targ = gettemplate(exp[2], context)
425 return (templateutil.runmap, (darg, targ))
425 return (templateutil.runmap, (darg, targ))
426
426
427 def buildmember(exp, context):
427 def buildmember(exp, context):
428 darg = compileexp(exp[1], context, methods)
428 darg = compileexp(exp[1], context, methods)
429 memb = getsymbol(exp[2])
429 memb = getsymbol(exp[2])
430 return (templateutil.runmember, (darg, memb))
430 return (templateutil.runmember, (darg, memb))
431
431
432 def buildnegate(exp, context):
432 def buildnegate(exp, context):
433 arg = compileexp(exp[1], context, exprmethods)
433 arg = compileexp(exp[1], context, exprmethods)
434 return (templateutil.runnegate, arg)
434 return (templateutil.runnegate, arg)
435
435
436 def buildarithmetic(exp, context, func):
436 def buildarithmetic(exp, context, func):
437 left = compileexp(exp[1], context, exprmethods)
437 left = compileexp(exp[1], context, exprmethods)
438 right = compileexp(exp[2], context, exprmethods)
438 right = compileexp(exp[2], context, exprmethods)
439 return (templateutil.runarithmetic, (func, left, right))
439 return (templateutil.runarithmetic, (func, left, right))
440
440
441 def buildfunc(exp, context):
441 def buildfunc(exp, context):
442 n = getsymbol(exp[1])
442 n = getsymbol(exp[1])
443 if n in context._funcs:
443 if n in context._funcs:
444 f = context._funcs[n]
444 f = context._funcs[n]
445 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
445 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
446 return (f, args)
446 return (f, args)
447 if n in context._filters:
447 if n in context._filters:
448 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
448 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
449 if len(args) != 1:
449 if len(args) != 1:
450 raise error.ParseError(_("filter %s expects one argument") % n)
450 raise error.ParseError(_("filter %s expects one argument") % n)
451 f = context._filters[n]
451 f = context._filters[n]
452 return (templateutil.runfilter, (args[0], f))
452 return (templateutil.runfilter, (args[0], f))
453 raise error.ParseError(_("unknown function '%s'") % n)
453 raise error.ParseError(_("unknown function '%s'") % n)
454
454
455 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
455 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
456 """Compile parsed tree of function arguments into list or dict of
456 """Compile parsed tree of function arguments into list or dict of
457 (func, data) pairs
457 (func, data) pairs
458
458
459 >>> context = engine(lambda t: (templateutil.runsymbol, t))
459 >>> context = engine(lambda t: (templateutil.runsymbol, t))
460 >>> def fargs(expr, argspec):
460 >>> def fargs(expr, argspec):
461 ... x = _parseexpr(expr)
461 ... x = _parseexpr(expr)
462 ... n = getsymbol(x[1])
462 ... n = getsymbol(x[1])
463 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
463 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
464 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
464 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
465 ['l', 'k']
465 ['l', 'k']
466 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
466 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
467 >>> list(args.keys()), list(args[b'opts'].keys())
467 >>> list(args.keys()), list(args[b'opts'].keys())
468 (['opts'], ['opts', 'k'])
468 (['opts'], ['opts', 'k'])
469 """
469 """
470 def compiledict(xs):
470 def compiledict(xs):
471 return util.sortdict((k, compileexp(x, context, curmethods))
471 return util.sortdict((k, compileexp(x, context, curmethods))
472 for k, x in xs.iteritems())
472 for k, x in xs.iteritems())
473 def compilelist(xs):
473 def compilelist(xs):
474 return [compileexp(x, context, curmethods) for x in xs]
474 return [compileexp(x, context, curmethods) for x in xs]
475
475
476 if not argspec:
476 if not argspec:
477 # filter or function with no argspec: return list of positional args
477 # filter or function with no argspec: return list of positional args
478 return compilelist(getlist(exp))
478 return compilelist(getlist(exp))
479
479
480 # function with argspec: return dict of named args
480 # function with argspec: return dict of named args
481 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
481 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
482 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
482 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
483 keyvaluenode='keyvalue', keynode='symbol')
483 keyvaluenode='keyvalue', keynode='symbol')
484 compargs = util.sortdict()
484 compargs = util.sortdict()
485 if varkey:
485 if varkey:
486 compargs[varkey] = compilelist(treeargs.pop(varkey))
486 compargs[varkey] = compilelist(treeargs.pop(varkey))
487 if optkey:
487 if optkey:
488 compargs[optkey] = compiledict(treeargs.pop(optkey))
488 compargs[optkey] = compiledict(treeargs.pop(optkey))
489 compargs.update(compiledict(treeargs))
489 compargs.update(compiledict(treeargs))
490 return compargs
490 return compargs
491
491
492 def buildkeyvaluepair(exp, content):
492 def buildkeyvaluepair(exp, content):
493 raise error.ParseError(_("can't use a key-value pair in this context"))
493 raise error.ParseError(_("can't use a key-value pair in this context"))
494
494
495 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
495 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
496 exprmethods = {
496 exprmethods = {
497 "integer": lambda e, c: (templateutil.runinteger, e[1]),
497 "integer": lambda e, c: (templateutil.runinteger, e[1]),
498 "string": lambda e, c: (templateutil.runstring, e[1]),
498 "string": lambda e, c: (templateutil.runstring, e[1]),
499 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
499 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
500 "template": buildtemplate,
500 "template": buildtemplate,
501 "group": lambda e, c: compileexp(e[1], c, exprmethods),
501 "group": lambda e, c: compileexp(e[1], c, exprmethods),
502 ".": buildmember,
502 ".": buildmember,
503 "|": buildfilter,
503 "|": buildfilter,
504 "%": buildmap,
504 "%": buildmap,
505 "func": buildfunc,
505 "func": buildfunc,
506 "keyvalue": buildkeyvaluepair,
506 "keyvalue": buildkeyvaluepair,
507 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
507 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
508 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
508 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
509 "negate": buildnegate,
509 "negate": buildnegate,
510 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
510 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
511 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
511 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
512 }
512 }
513
513
514 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
514 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
515 methods = exprmethods.copy()
515 methods = exprmethods.copy()
516 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
516 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
517
517
518 class _aliasrules(parser.basealiasrules):
518 class _aliasrules(parser.basealiasrules):
519 """Parsing and expansion rule set of template aliases"""
519 """Parsing and expansion rule set of template aliases"""
520 _section = _('template alias')
520 _section = _('template alias')
521 _parse = staticmethod(_parseexpr)
521 _parse = staticmethod(_parseexpr)
522
522
523 @staticmethod
523 @staticmethod
524 def _trygetfunc(tree):
524 def _trygetfunc(tree):
525 """Return (name, args) if tree is func(...) or ...|filter; otherwise
525 """Return (name, args) if tree is func(...) or ...|filter; otherwise
526 None"""
526 None"""
527 if tree[0] == 'func' and tree[1][0] == 'symbol':
527 if tree[0] == 'func' and tree[1][0] == 'symbol':
528 return tree[1][1], getlist(tree[2])
528 return tree[1][1], getlist(tree[2])
529 if tree[0] == '|' and tree[2][0] == 'symbol':
529 if tree[0] == '|' and tree[2][0] == 'symbol':
530 return tree[2][1], [tree[1]]
530 return tree[2][1], [tree[1]]
531
531
532 def expandaliases(tree, aliases):
532 def expandaliases(tree, aliases):
533 """Return new tree of aliases are expanded"""
533 """Return new tree of aliases are expanded"""
534 aliasmap = _aliasrules.buildmap(aliases)
534 aliasmap = _aliasrules.buildmap(aliases)
535 return _aliasrules.expand(aliasmap, tree)
535 return _aliasrules.expand(aliasmap, tree)
536
536
537 # template engine
537 # template engine
538
538
539 def unquotestring(s):
539 def unquotestring(s):
540 '''unwrap quotes if any; otherwise returns unmodified string'''
540 '''unwrap quotes if any; otherwise returns unmodified string'''
541 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
541 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
542 return s
542 return s
543 return s[1:-1]
543 return s[1:-1]
544
544
545 class resourcemapper(object):
545 class resourcemapper(object):
546 """Mapper of internal template resources"""
546 """Mapper of internal template resources"""
547
547
548 __metaclass__ = abc.ABCMeta
548 __metaclass__ = abc.ABCMeta
549
549
550 @abc.abstractmethod
550 @abc.abstractmethod
551 def availablekeys(self, context, mapping):
551 def availablekeys(self, mapping):
552 """Return a set of available resource keys based on the given mapping"""
552 """Return a set of available resource keys based on the given mapping"""
553
553
554 @abc.abstractmethod
554 @abc.abstractmethod
555 def knownkeys(self):
555 def knownkeys(self):
556 """Return a set of supported resource keys"""
556 """Return a set of supported resource keys"""
557
557
558 @abc.abstractmethod
558 @abc.abstractmethod
559 def lookup(self, context, mapping, key):
559 def lookup(self, mapping, key):
560 """Return a resource for the key if available; otherwise None"""
560 """Return a resource for the key if available; otherwise None"""
561
561
562 @abc.abstractmethod
562 @abc.abstractmethod
563 def populatemap(self, context, origmapping, newmapping):
563 def populatemap(self, context, origmapping, newmapping):
564 """Return a dict of additional mapping items which should be paired
564 """Return a dict of additional mapping items which should be paired
565 with the given new mapping"""
565 with the given new mapping"""
566
566
567 class nullresourcemapper(resourcemapper):
567 class nullresourcemapper(resourcemapper):
568 def availablekeys(self, context, mapping):
568 def availablekeys(self, mapping):
569 return set()
569 return set()
570
570
571 def knownkeys(self):
571 def knownkeys(self):
572 return set()
572 return set()
573
573
574 def lookup(self, context, mapping, key):
574 def lookup(self, mapping, key):
575 return None
575 return None
576
576
577 def populatemap(self, context, origmapping, newmapping):
577 def populatemap(self, context, origmapping, newmapping):
578 return {}
578 return {}
579
579
580 class engine(object):
580 class engine(object):
581 '''template expansion engine.
581 '''template expansion engine.
582
582
583 template expansion works like this. a map file contains key=value
583 template expansion works like this. a map file contains key=value
584 pairs. if value is quoted, it is treated as string. otherwise, it
584 pairs. if value is quoted, it is treated as string. otherwise, it
585 is treated as name of template file.
585 is treated as name of template file.
586
586
587 templater is asked to expand a key in map. it looks up key, and
587 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
588 looks for strings like this: {foo}. it expands {foo} by looking up
589 foo in map, and substituting it. expansion is recursive: it stops
589 foo in map, and substituting it. expansion is recursive: it stops
590 when there is no more {foo} to replace.
590 when there is no more {foo} to replace.
591
591
592 expansion also allows formatting and filtering.
592 expansion also allows formatting and filtering.
593
593
594 format uses key to expand each item in list. syntax is
594 format uses key to expand each item in list. syntax is
595 {key%format}.
595 {key%format}.
596
596
597 filter uses function to transform value. syntax is
597 filter uses function to transform value. syntax is
598 {key|filter1|filter2|...}.'''
598 {key|filter1|filter2|...}.'''
599
599
600 def __init__(self, loader, filters=None, defaults=None, resources=None):
600 def __init__(self, loader, filters=None, defaults=None, resources=None):
601 self._loader = loader
601 self._loader = loader
602 if filters is None:
602 if filters is None:
603 filters = {}
603 filters = {}
604 self._filters = filters
604 self._filters = filters
605 self._funcs = templatefuncs.funcs # make this a parameter if needed
605 self._funcs = templatefuncs.funcs # make this a parameter if needed
606 if defaults is None:
606 if defaults is None:
607 defaults = {}
607 defaults = {}
608 if resources is None:
608 if resources is None:
609 resources = nullresourcemapper()
609 resources = nullresourcemapper()
610 self._defaults = defaults
610 self._defaults = defaults
611 self._resources = resources
611 self._resources = resources
612 self._cache = {} # key: (func, data)
612 self._cache = {} # key: (func, data)
613 self._tmplcache = {} # literal template: (func, data)
613 self._tmplcache = {} # literal template: (func, data)
614
614
615 def overlaymap(self, origmapping, newmapping):
615 def overlaymap(self, origmapping, newmapping):
616 """Create combined mapping from the original mapping and partial
616 """Create combined mapping from the original mapping and partial
617 mapping to override the original"""
617 mapping to override the original"""
618 # do not copy symbols which overrides the defaults depending on
618 # do not copy symbols which overrides the defaults depending on
619 # new resources, so the defaults will be re-evaluated (issue5612)
619 # new resources, so the defaults will be re-evaluated (issue5612)
620 knownres = self._resources.knownkeys()
620 knownres = self._resources.knownkeys()
621 newres = self._resources.availablekeys(self, newmapping)
621 newres = self._resources.availablekeys(newmapping)
622 mapping = {k: v for k, v in origmapping.iteritems()
622 mapping = {k: v for k, v in origmapping.iteritems()
623 if (k in knownres # not a symbol per self.symbol()
623 if (k in knownres # not a symbol per self.symbol()
624 or newres.isdisjoint(self._defaultrequires(k)))}
624 or newres.isdisjoint(self._defaultrequires(k)))}
625 mapping.update(newmapping)
625 mapping.update(newmapping)
626 mapping.update(
626 mapping.update(
627 self._resources.populatemap(self, origmapping, newmapping))
627 self._resources.populatemap(self, origmapping, newmapping))
628 return mapping
628 return mapping
629
629
630 def _defaultrequires(self, key):
630 def _defaultrequires(self, key):
631 """Resource keys required by the specified default symbol function"""
631 """Resource keys required by the specified default symbol function"""
632 v = self._defaults.get(key)
632 v = self._defaults.get(key)
633 if v is None or not callable(v):
633 if v is None or not callable(v):
634 return ()
634 return ()
635 return getattr(v, '_requires', ())
635 return getattr(v, '_requires', ())
636
636
637 def symbol(self, mapping, key):
637 def symbol(self, mapping, key):
638 """Resolve symbol to value or function; None if nothing found"""
638 """Resolve symbol to value or function; None if nothing found"""
639 v = None
639 v = None
640 if key not in self._resources.knownkeys():
640 if key not in self._resources.knownkeys():
641 v = mapping.get(key)
641 v = mapping.get(key)
642 if v is None:
642 if v is None:
643 v = self._defaults.get(key)
643 v = self._defaults.get(key)
644 return v
644 return v
645
645
646 def availableresourcekeys(self, mapping):
646 def availableresourcekeys(self, mapping):
647 """Return a set of available resource keys based on the given mapping"""
647 """Return a set of available resource keys based on the given mapping"""
648 return self._resources.availablekeys(self, mapping)
648 return self._resources.availablekeys(mapping)
649
649
650 def knownresourcekeys(self):
650 def knownresourcekeys(self):
651 """Return a set of supported resource keys"""
651 """Return a set of supported resource keys"""
652 return self._resources.knownkeys()
652 return self._resources.knownkeys()
653
653
654 def resource(self, mapping, key):
654 def resource(self, mapping, key):
655 """Return internal data (e.g. cache) used for keyword/function
655 """Return internal data (e.g. cache) used for keyword/function
656 evaluation"""
656 evaluation"""
657 v = self._resources.lookup(self, mapping, key)
657 v = self._resources.lookup(mapping, key)
658 if v is None:
658 if v is None:
659 raise templateutil.ResourceUnavailable(
659 raise templateutil.ResourceUnavailable(
660 _('template resource not available: %s') % key)
660 _('template resource not available: %s') % key)
661 return v
661 return v
662
662
663 def _load(self, t):
663 def _load(self, t):
664 '''load, parse, and cache a template'''
664 '''load, parse, and cache a template'''
665 if t not in self._cache:
665 if t not in self._cache:
666 x = self._loader(t)
666 x = self._loader(t)
667 # put poison to cut recursion while compiling 't'
667 # put poison to cut recursion while compiling 't'
668 self._cache[t] = (_runrecursivesymbol, t)
668 self._cache[t] = (_runrecursivesymbol, t)
669 try:
669 try:
670 self._cache[t] = compileexp(x, self, methods)
670 self._cache[t] = compileexp(x, self, methods)
671 except: # re-raises
671 except: # re-raises
672 del self._cache[t]
672 del self._cache[t]
673 raise
673 raise
674 return self._cache[t]
674 return self._cache[t]
675
675
676 def _parse(self, tmpl):
676 def _parse(self, tmpl):
677 """Parse and cache a literal template"""
677 """Parse and cache a literal template"""
678 if tmpl not in self._tmplcache:
678 if tmpl not in self._tmplcache:
679 x = parse(tmpl)
679 x = parse(tmpl)
680 self._tmplcache[tmpl] = compileexp(x, self, methods)
680 self._tmplcache[tmpl] = compileexp(x, self, methods)
681 return self._tmplcache[tmpl]
681 return self._tmplcache[tmpl]
682
682
683 def preload(self, t):
683 def preload(self, t):
684 """Load, parse, and cache the specified template if available"""
684 """Load, parse, and cache the specified template if available"""
685 try:
685 try:
686 self._load(t)
686 self._load(t)
687 return True
687 return True
688 except templateutil.TemplateNotFound:
688 except templateutil.TemplateNotFound:
689 return False
689 return False
690
690
691 def process(self, t, mapping):
691 def process(self, t, mapping):
692 '''Perform expansion. t is name of map element to expand.
692 '''Perform expansion. t is name of map element to expand.
693 mapping contains added elements for use during expansion. Is a
693 mapping contains added elements for use during expansion. Is a
694 generator.'''
694 generator.'''
695 func, data = self._load(t)
695 func, data = self._load(t)
696 return self._expand(func, data, mapping)
696 return self._expand(func, data, mapping)
697
697
698 def expand(self, tmpl, mapping):
698 def expand(self, tmpl, mapping):
699 """Perform expansion over a literal template
699 """Perform expansion over a literal template
700
700
701 No user aliases will be expanded since this is supposed to be called
701 No user aliases will be expanded since this is supposed to be called
702 with an internal template string.
702 with an internal template string.
703 """
703 """
704 func, data = self._parse(tmpl)
704 func, data = self._parse(tmpl)
705 return self._expand(func, data, mapping)
705 return self._expand(func, data, mapping)
706
706
707 def _expand(self, func, data, mapping):
707 def _expand(self, func, data, mapping):
708 # populate additional items only if they don't exist in the given
708 # populate additional items only if they don't exist in the given
709 # mapping. this is slightly different from overlaymap() because the
709 # mapping. this is slightly different from overlaymap() because the
710 # initial 'revcache' may contain pre-computed items.
710 # initial 'revcache' may contain pre-computed items.
711 extramapping = self._resources.populatemap(self, {}, mapping)
711 extramapping = self._resources.populatemap(self, {}, mapping)
712 if extramapping:
712 if extramapping:
713 extramapping.update(mapping)
713 extramapping.update(mapping)
714 mapping = extramapping
714 mapping = extramapping
715 return templateutil.flatten(self, mapping, func(self, mapping, data))
715 return templateutil.flatten(self, mapping, func(self, mapping, data))
716
716
717 def stylelist():
717 def stylelist():
718 paths = templatepaths()
718 paths = templatepaths()
719 if not paths:
719 if not paths:
720 return _('no templates found, try `hg debuginstall` for more info')
720 return _('no templates found, try `hg debuginstall` for more info')
721 dirlist = os.listdir(paths[0])
721 dirlist = os.listdir(paths[0])
722 stylelist = []
722 stylelist = []
723 for file in dirlist:
723 for file in dirlist:
724 split = file.split(".")
724 split = file.split(".")
725 if split[-1] in ('orig', 'rej'):
725 if split[-1] in ('orig', 'rej'):
726 continue
726 continue
727 if split[0] == "map-cmdline":
727 if split[0] == "map-cmdline":
728 stylelist.append(split[1])
728 stylelist.append(split[1])
729 return ", ".join(sorted(stylelist))
729 return ", ".join(sorted(stylelist))
730
730
731 def _readmapfile(mapfile):
731 def _readmapfile(mapfile):
732 """Load template elements from the given map file"""
732 """Load template elements from the given map file"""
733 if not os.path.exists(mapfile):
733 if not os.path.exists(mapfile):
734 raise error.Abort(_("style '%s' not found") % mapfile,
734 raise error.Abort(_("style '%s' not found") % mapfile,
735 hint=_("available styles: %s") % stylelist())
735 hint=_("available styles: %s") % stylelist())
736
736
737 base = os.path.dirname(mapfile)
737 base = os.path.dirname(mapfile)
738 conf = config.config(includepaths=templatepaths())
738 conf = config.config(includepaths=templatepaths())
739 conf.read(mapfile, remap={'': 'templates'})
739 conf.read(mapfile, remap={'': 'templates'})
740
740
741 cache = {}
741 cache = {}
742 tmap = {}
742 tmap = {}
743 aliases = []
743 aliases = []
744
744
745 val = conf.get('templates', '__base__')
745 val = conf.get('templates', '__base__')
746 if val and val[0] not in "'\"":
746 if val and val[0] not in "'\"":
747 # treat as a pointer to a base class for this style
747 # treat as a pointer to a base class for this style
748 path = util.normpath(os.path.join(base, val))
748 path = util.normpath(os.path.join(base, val))
749
749
750 # fallback check in template paths
750 # fallback check in template paths
751 if not os.path.exists(path):
751 if not os.path.exists(path):
752 for p in templatepaths():
752 for p in templatepaths():
753 p2 = util.normpath(os.path.join(p, val))
753 p2 = util.normpath(os.path.join(p, val))
754 if os.path.isfile(p2):
754 if os.path.isfile(p2):
755 path = p2
755 path = p2
756 break
756 break
757 p3 = util.normpath(os.path.join(p2, "map"))
757 p3 = util.normpath(os.path.join(p2, "map"))
758 if os.path.isfile(p3):
758 if os.path.isfile(p3):
759 path = p3
759 path = p3
760 break
760 break
761
761
762 cache, tmap, aliases = _readmapfile(path)
762 cache, tmap, aliases = _readmapfile(path)
763
763
764 for key, val in conf['templates'].items():
764 for key, val in conf['templates'].items():
765 if not val:
765 if not val:
766 raise error.ParseError(_('missing value'),
766 raise error.ParseError(_('missing value'),
767 conf.source('templates', key))
767 conf.source('templates', key))
768 if val[0] in "'\"":
768 if val[0] in "'\"":
769 if val[0] != val[-1]:
769 if val[0] != val[-1]:
770 raise error.ParseError(_('unmatched quotes'),
770 raise error.ParseError(_('unmatched quotes'),
771 conf.source('templates', key))
771 conf.source('templates', key))
772 cache[key] = unquotestring(val)
772 cache[key] = unquotestring(val)
773 elif key != '__base__':
773 elif key != '__base__':
774 tmap[key] = os.path.join(base, val)
774 tmap[key] = os.path.join(base, val)
775 aliases.extend(conf['templatealias'].items())
775 aliases.extend(conf['templatealias'].items())
776 return cache, tmap, aliases
776 return cache, tmap, aliases
777
777
778 class loader(object):
778 class loader(object):
779 """Load template fragments optionally from a map file"""
779 """Load template fragments optionally from a map file"""
780
780
781 def __init__(self, cache, aliases):
781 def __init__(self, cache, aliases):
782 if cache is None:
782 if cache is None:
783 cache = {}
783 cache = {}
784 self.cache = cache.copy()
784 self.cache = cache.copy()
785 self._map = {}
785 self._map = {}
786 self._aliasmap = _aliasrules.buildmap(aliases)
786 self._aliasmap = _aliasrules.buildmap(aliases)
787
787
788 def __contains__(self, key):
788 def __contains__(self, key):
789 return key in self.cache or key in self._map
789 return key in self.cache or key in self._map
790
790
791 def load(self, t):
791 def load(self, t):
792 """Get parsed tree for the given template name. Use a local cache."""
792 """Get parsed tree for the given template name. Use a local cache."""
793 if t not in self.cache:
793 if t not in self.cache:
794 try:
794 try:
795 self.cache[t] = util.readfile(self._map[t])
795 self.cache[t] = util.readfile(self._map[t])
796 except KeyError as inst:
796 except KeyError as inst:
797 raise templateutil.TemplateNotFound(
797 raise templateutil.TemplateNotFound(
798 _('"%s" not in template map') % inst.args[0])
798 _('"%s" not in template map') % inst.args[0])
799 except IOError as inst:
799 except IOError as inst:
800 reason = (_('template file %s: %s')
800 reason = (_('template file %s: %s')
801 % (self._map[t],
801 % (self._map[t],
802 stringutil.forcebytestr(inst.args[1])))
802 stringutil.forcebytestr(inst.args[1])))
803 raise IOError(inst.args[0], encoding.strfromlocal(reason))
803 raise IOError(inst.args[0], encoding.strfromlocal(reason))
804 return self._parse(self.cache[t])
804 return self._parse(self.cache[t])
805
805
806 def _parse(self, tmpl):
806 def _parse(self, tmpl):
807 x = parse(tmpl)
807 x = parse(tmpl)
808 if self._aliasmap:
808 if self._aliasmap:
809 x = _aliasrules.expand(self._aliasmap, x)
809 x = _aliasrules.expand(self._aliasmap, x)
810 return x
810 return x
811
811
812 def _findsymbolsused(self, tree, syms):
812 def _findsymbolsused(self, tree, syms):
813 if not tree:
813 if not tree:
814 return
814 return
815 op = tree[0]
815 op = tree[0]
816 if op == 'symbol':
816 if op == 'symbol':
817 s = tree[1]
817 s = tree[1]
818 if s in syms[0]:
818 if s in syms[0]:
819 return # avoid recursion: s -> cache[s] -> s
819 return # avoid recursion: s -> cache[s] -> s
820 syms[0].add(s)
820 syms[0].add(s)
821 if s in self.cache or s in self._map:
821 if s in self.cache or s in self._map:
822 # s may be a reference for named template
822 # s may be a reference for named template
823 self._findsymbolsused(self.load(s), syms)
823 self._findsymbolsused(self.load(s), syms)
824 return
824 return
825 if op in {'integer', 'string'}:
825 if op in {'integer', 'string'}:
826 return
826 return
827 # '{arg|func}' == '{func(arg)}'
827 # '{arg|func}' == '{func(arg)}'
828 if op == '|':
828 if op == '|':
829 syms[1].add(getsymbol(tree[2]))
829 syms[1].add(getsymbol(tree[2]))
830 self._findsymbolsused(tree[1], syms)
830 self._findsymbolsused(tree[1], syms)
831 return
831 return
832 if op == 'func':
832 if op == 'func':
833 syms[1].add(getsymbol(tree[1]))
833 syms[1].add(getsymbol(tree[1]))
834 self._findsymbolsused(tree[2], syms)
834 self._findsymbolsused(tree[2], syms)
835 return
835 return
836 for x in tree[1:]:
836 for x in tree[1:]:
837 self._findsymbolsused(x, syms)
837 self._findsymbolsused(x, syms)
838
838
839 def symbolsused(self, t):
839 def symbolsused(self, t):
840 """Look up (keywords, filters/functions) referenced from the name
840 """Look up (keywords, filters/functions) referenced from the name
841 template 't'
841 template 't'
842
842
843 This may load additional templates from the map file.
843 This may load additional templates from the map file.
844 """
844 """
845 syms = (set(), set())
845 syms = (set(), set())
846 self._findsymbolsused(self.load(t), syms)
846 self._findsymbolsused(self.load(t), syms)
847 return syms
847 return syms
848
848
849 class templater(object):
849 class templater(object):
850
850
851 def __init__(self, filters=None, defaults=None, resources=None,
851 def __init__(self, filters=None, defaults=None, resources=None,
852 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
852 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
853 """Create template engine optionally with preloaded template fragments
853 """Create template engine optionally with preloaded template fragments
854
854
855 - ``filters``: a dict of functions to transform a value into another.
855 - ``filters``: a dict of functions to transform a value into another.
856 - ``defaults``: a dict of symbol values/functions; may be overridden
856 - ``defaults``: a dict of symbol values/functions; may be overridden
857 by a ``mapping`` dict.
857 by a ``mapping`` dict.
858 - ``resources``: a resourcemapper object to look up internal data
858 - ``resources``: a resourcemapper object to look up internal data
859 (e.g. cache), inaccessible from user template.
859 (e.g. cache), inaccessible from user template.
860 - ``cache``: a dict of preloaded template fragments.
860 - ``cache``: a dict of preloaded template fragments.
861 - ``aliases``: a list of alias (name, replacement) pairs.
861 - ``aliases``: a list of alias (name, replacement) pairs.
862
862
863 self.cache may be updated later to register additional template
863 self.cache may be updated later to register additional template
864 fragments.
864 fragments.
865 """
865 """
866 allfilters = templatefilters.filters.copy()
866 allfilters = templatefilters.filters.copy()
867 if filters:
867 if filters:
868 allfilters.update(filters)
868 allfilters.update(filters)
869 self._loader = loader(cache, aliases)
869 self._loader = loader(cache, aliases)
870 self._proc = engine(self._loader.load, allfilters, defaults, resources)
870 self._proc = engine(self._loader.load, allfilters, defaults, resources)
871 self._minchunk, self._maxchunk = minchunk, maxchunk
871 self._minchunk, self._maxchunk = minchunk, maxchunk
872
872
873 @classmethod
873 @classmethod
874 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
874 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
875 cache=None, minchunk=1024, maxchunk=65536):
875 cache=None, minchunk=1024, maxchunk=65536):
876 """Create templater from the specified map file"""
876 """Create templater from the specified map file"""
877 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
877 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
878 cache, tmap, aliases = _readmapfile(mapfile)
878 cache, tmap, aliases = _readmapfile(mapfile)
879 t._loader.cache.update(cache)
879 t._loader.cache.update(cache)
880 t._loader._map = tmap
880 t._loader._map = tmap
881 t._loader._aliasmap = _aliasrules.buildmap(aliases)
881 t._loader._aliasmap = _aliasrules.buildmap(aliases)
882 return t
882 return t
883
883
884 def __contains__(self, key):
884 def __contains__(self, key):
885 return key in self._loader
885 return key in self._loader
886
886
887 @property
887 @property
888 def cache(self):
888 def cache(self):
889 return self._loader.cache
889 return self._loader.cache
890
890
891 # for highlight extension to insert one-time 'colorize' filter
891 # for highlight extension to insert one-time 'colorize' filter
892 @property
892 @property
893 def _filters(self):
893 def _filters(self):
894 return self._proc._filters
894 return self._proc._filters
895
895
896 @property
896 @property
897 def defaults(self):
897 def defaults(self):
898 return self._proc._defaults
898 return self._proc._defaults
899
899
900 def load(self, t):
900 def load(self, t):
901 """Get parsed tree for the given template name. Use a local cache."""
901 """Get parsed tree for the given template name. Use a local cache."""
902 return self._loader.load(t)
902 return self._loader.load(t)
903
903
904 def symbolsuseddefault(self):
904 def symbolsuseddefault(self):
905 """Look up (keywords, filters/functions) referenced from the default
905 """Look up (keywords, filters/functions) referenced from the default
906 unnamed template
906 unnamed template
907
907
908 This may load additional templates from the map file.
908 This may load additional templates from the map file.
909 """
909 """
910 return self.symbolsused('')
910 return self.symbolsused('')
911
911
912 def symbolsused(self, t):
912 def symbolsused(self, t):
913 """Look up (keywords, filters/functions) referenced from the name
913 """Look up (keywords, filters/functions) referenced from the name
914 template 't'
914 template 't'
915
915
916 This may load additional templates from the map file.
916 This may load additional templates from the map file.
917 """
917 """
918 return self._loader.symbolsused(t)
918 return self._loader.symbolsused(t)
919
919
920 def renderdefault(self, mapping):
920 def renderdefault(self, mapping):
921 """Render the default unnamed template and return result as string"""
921 """Render the default unnamed template and return result as string"""
922 return self.render('', mapping)
922 return self.render('', mapping)
923
923
924 def render(self, t, mapping):
924 def render(self, t, mapping):
925 """Render the specified named template and return result as string"""
925 """Render the specified named template and return result as string"""
926 return b''.join(self.generate(t, mapping))
926 return b''.join(self.generate(t, mapping))
927
927
928 def generate(self, t, mapping):
928 def generate(self, t, mapping):
929 """Return a generator that renders the specified named template and
929 """Return a generator that renders the specified named template and
930 yields chunks"""
930 yields chunks"""
931 stream = self._proc.process(t, mapping)
931 stream = self._proc.process(t, mapping)
932 if self._minchunk:
932 if self._minchunk:
933 stream = util.increasingchunks(stream, min=self._minchunk,
933 stream = util.increasingchunks(stream, min=self._minchunk,
934 max=self._maxchunk)
934 max=self._maxchunk)
935 return stream
935 return stream
936
936
937 def templatepaths():
937 def templatepaths():
938 '''return locations used for template files.'''
938 '''return locations used for template files.'''
939 pathsrel = ['templates']
939 pathsrel = ['templates']
940 paths = [os.path.normpath(os.path.join(util.datapath, f))
940 paths = [os.path.normpath(os.path.join(util.datapath, f))
941 for f in pathsrel]
941 for f in pathsrel]
942 return [p for p in paths if os.path.isdir(p)]
942 return [p for p in paths if os.path.isdir(p)]
943
943
944 def templatepath(name):
944 def templatepath(name):
945 '''return location of template file. returns None if not found.'''
945 '''return location of template file. returns None if not found.'''
946 for p in templatepaths():
946 for p in templatepaths():
947 f = os.path.join(p, name)
947 f = os.path.join(p, name)
948 if os.path.exists(f):
948 if os.path.exists(f):
949 return f
949 return f
950 return None
950 return None
951
951
952 def stylemap(styles, paths=None):
952 def stylemap(styles, paths=None):
953 """Return path to mapfile for a given style.
953 """Return path to mapfile for a given style.
954
954
955 Searches mapfile in the following locations:
955 Searches mapfile in the following locations:
956 1. templatepath/style/map
956 1. templatepath/style/map
957 2. templatepath/map-style
957 2. templatepath/map-style
958 3. templatepath/map
958 3. templatepath/map
959 """
959 """
960
960
961 if paths is None:
961 if paths is None:
962 paths = templatepaths()
962 paths = templatepaths()
963 elif isinstance(paths, bytes):
963 elif isinstance(paths, bytes):
964 paths = [paths]
964 paths = [paths]
965
965
966 if isinstance(styles, bytes):
966 if isinstance(styles, bytes):
967 styles = [styles]
967 styles = [styles]
968
968
969 for style in styles:
969 for style in styles:
970 # only plain name is allowed to honor template paths
970 # only plain name is allowed to honor template paths
971 if (not style
971 if (not style
972 or style in (pycompat.oscurdir, pycompat.ospardir)
972 or style in (pycompat.oscurdir, pycompat.ospardir)
973 or pycompat.ossep in style
973 or pycompat.ossep in style
974 or pycompat.osaltsep and pycompat.osaltsep in style):
974 or pycompat.osaltsep and pycompat.osaltsep in style):
975 continue
975 continue
976 locations = [os.path.join(style, 'map'), 'map-' + style]
976 locations = [os.path.join(style, 'map'), 'map-' + style]
977 locations.append('map')
977 locations.append('map')
978
978
979 for path in paths:
979 for path in paths:
980 for location in locations:
980 for location in locations:
981 mapfile = os.path.join(path, location)
981 mapfile = os.path.join(path, location)
982 if os.path.isfile(mapfile):
982 if os.path.isfile(mapfile):
983 return style, mapfile
983 return style, mapfile
984
984
985 raise RuntimeError("No hgweb templates found in %r" % paths)
985 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,963 +1,963 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() or unwrapastype() to obtain the inner object.
34 Use unwrapvalue() or unwrapastype() to obtain the inner object.
35 """
35 """
36
36
37 __metaclass__ = abc.ABCMeta
37 __metaclass__ = abc.ABCMeta
38
38
39 @abc.abstractmethod
39 @abc.abstractmethod
40 def contains(self, context, mapping, item):
40 def contains(self, context, mapping, item):
41 """Test if the specified item is in self
41 """Test if the specified item is in self
42
42
43 The item argument may be a wrapped object.
43 The item argument may be a wrapped object.
44 """
44 """
45
45
46 @abc.abstractmethod
46 @abc.abstractmethod
47 def getmember(self, context, mapping, key):
47 def getmember(self, context, mapping, key):
48 """Return a member item for the specified key
48 """Return a member item for the specified key
49
49
50 The key argument may be a wrapped object.
50 The key argument may be a wrapped object.
51 A returned object may be either a wrapped object or a pure value
51 A returned object may be either a wrapped object or a pure value
52 depending on the self type.
52 depending on the self type.
53 """
53 """
54
54
55 @abc.abstractmethod
55 @abc.abstractmethod
56 def getmin(self, context, mapping):
56 def getmin(self, context, mapping):
57 """Return the smallest item, which may be either a wrapped or a pure
57 """Return the smallest item, which may be either a wrapped or a pure
58 value depending on the self type"""
58 value depending on the self type"""
59
59
60 @abc.abstractmethod
60 @abc.abstractmethod
61 def getmax(self, context, mapping):
61 def getmax(self, context, mapping):
62 """Return the largest item, which may be either a wrapped or a pure
62 """Return the largest item, which may be either a wrapped or a pure
63 value depending on the self type"""
63 value depending on the self type"""
64
64
65 @abc.abstractmethod
65 @abc.abstractmethod
66 def filter(self, context, mapping, select):
66 def filter(self, context, mapping, select):
67 """Return new container of the same type which includes only the
67 """Return new container of the same type which includes only the
68 selected elements
68 selected elements
69
69
70 select() takes each item as a wrapped object and returns True/False.
70 select() takes each item as a wrapped object and returns True/False.
71 """
71 """
72
72
73 @abc.abstractmethod
73 @abc.abstractmethod
74 def itermaps(self, context):
74 def itermaps(self, context):
75 """Yield each template mapping"""
75 """Yield each template mapping"""
76
76
77 @abc.abstractmethod
77 @abc.abstractmethod
78 def join(self, context, mapping, sep):
78 def join(self, context, mapping, sep):
79 """Join items with the separator; Returns a bytes or (possibly nested)
79 """Join items with the separator; Returns a bytes or (possibly nested)
80 generator of bytes
80 generator of bytes
81
81
82 A pre-configured template may be rendered per item if this container
82 A pre-configured template may be rendered per item if this container
83 holds unprintable items.
83 holds unprintable items.
84 """
84 """
85
85
86 @abc.abstractmethod
86 @abc.abstractmethod
87 def show(self, context, mapping):
87 def show(self, context, mapping):
88 """Return a bytes or (possibly nested) generator of bytes representing
88 """Return a bytes or (possibly nested) generator of bytes representing
89 the underlying object
89 the underlying object
90
90
91 A pre-configured template may be rendered if the underlying object is
91 A pre-configured template may be rendered if the underlying object is
92 not printable.
92 not printable.
93 """
93 """
94
94
95 @abc.abstractmethod
95 @abc.abstractmethod
96 def tobool(self, context, mapping):
96 def tobool(self, context, mapping):
97 """Return a boolean representation of the inner value"""
97 """Return a boolean representation of the inner value"""
98
98
99 @abc.abstractmethod
99 @abc.abstractmethod
100 def tovalue(self, context, mapping):
100 def tovalue(self, context, mapping):
101 """Move the inner value object out or create a value representation
101 """Move the inner value object out or create a value representation
102
102
103 A returned value must be serializable by templaterfilters.json().
103 A returned value must be serializable by templaterfilters.json().
104 """
104 """
105
105
106 class mappable(object):
106 class mappable(object):
107 """Object which can be converted to a single template mapping"""
107 """Object which can be converted to a single template mapping"""
108
108
109 def itermaps(self, context):
109 def itermaps(self, context):
110 yield self.tomap(context)
110 yield self.tomap(context)
111
111
112 @abc.abstractmethod
112 @abc.abstractmethod
113 def tomap(self, context):
113 def tomap(self, context):
114 """Create a single template mapping representing this"""
114 """Create a single template mapping representing this"""
115
115
116 class wrappedbytes(wrapped):
116 class wrappedbytes(wrapped):
117 """Wrapper for byte string"""
117 """Wrapper for byte string"""
118
118
119 def __init__(self, value):
119 def __init__(self, value):
120 self._value = value
120 self._value = value
121
121
122 def contains(self, context, mapping, item):
122 def contains(self, context, mapping, item):
123 item = stringify(context, mapping, item)
123 item = stringify(context, mapping, item)
124 return item in self._value
124 return item in self._value
125
125
126 def getmember(self, context, mapping, key):
126 def getmember(self, context, mapping, key):
127 raise error.ParseError(_('%r is not a dictionary')
127 raise error.ParseError(_('%r is not a dictionary')
128 % pycompat.bytestr(self._value))
128 % pycompat.bytestr(self._value))
129
129
130 def getmin(self, context, mapping):
130 def getmin(self, context, mapping):
131 return self._getby(context, mapping, min)
131 return self._getby(context, mapping, min)
132
132
133 def getmax(self, context, mapping):
133 def getmax(self, context, mapping):
134 return self._getby(context, mapping, max)
134 return self._getby(context, mapping, max)
135
135
136 def _getby(self, context, mapping, func):
136 def _getby(self, context, mapping, func):
137 if not self._value:
137 if not self._value:
138 raise error.ParseError(_('empty string'))
138 raise error.ParseError(_('empty string'))
139 return func(pycompat.iterbytestr(self._value))
139 return func(pycompat.iterbytestr(self._value))
140
140
141 def filter(self, context, mapping, select):
141 def filter(self, context, mapping, select):
142 raise error.ParseError(_('%r is not filterable')
142 raise error.ParseError(_('%r is not filterable')
143 % pycompat.bytestr(self._value))
143 % pycompat.bytestr(self._value))
144
144
145 def itermaps(self, context):
145 def itermaps(self, context):
146 raise error.ParseError(_('%r is not iterable of mappings')
146 raise error.ParseError(_('%r is not iterable of mappings')
147 % pycompat.bytestr(self._value))
147 % pycompat.bytestr(self._value))
148
148
149 def join(self, context, mapping, sep):
149 def join(self, context, mapping, sep):
150 return joinitems(pycompat.iterbytestr(self._value), sep)
150 return joinitems(pycompat.iterbytestr(self._value), sep)
151
151
152 def show(self, context, mapping):
152 def show(self, context, mapping):
153 return self._value
153 return self._value
154
154
155 def tobool(self, context, mapping):
155 def tobool(self, context, mapping):
156 return bool(self._value)
156 return bool(self._value)
157
157
158 def tovalue(self, context, mapping):
158 def tovalue(self, context, mapping):
159 return self._value
159 return self._value
160
160
161 class wrappedvalue(wrapped):
161 class wrappedvalue(wrapped):
162 """Generic wrapper for pure non-list/dict/bytes value"""
162 """Generic wrapper for pure non-list/dict/bytes value"""
163
163
164 def __init__(self, value):
164 def __init__(self, value):
165 self._value = value
165 self._value = value
166
166
167 def contains(self, context, mapping, item):
167 def contains(self, context, mapping, item):
168 raise error.ParseError(_("%r is not iterable") % self._value)
168 raise error.ParseError(_("%r is not iterable") % self._value)
169
169
170 def getmember(self, context, mapping, key):
170 def getmember(self, context, mapping, key):
171 raise error.ParseError(_('%r is not a dictionary') % self._value)
171 raise error.ParseError(_('%r is not a dictionary') % self._value)
172
172
173 def getmin(self, context, mapping):
173 def getmin(self, context, mapping):
174 raise error.ParseError(_("%r is not iterable") % self._value)
174 raise error.ParseError(_("%r is not iterable") % self._value)
175
175
176 def getmax(self, context, mapping):
176 def getmax(self, context, mapping):
177 raise error.ParseError(_("%r is not iterable") % self._value)
177 raise error.ParseError(_("%r is not iterable") % self._value)
178
178
179 def filter(self, context, mapping, select):
179 def filter(self, context, mapping, select):
180 raise error.ParseError(_("%r is not iterable") % self._value)
180 raise error.ParseError(_("%r is not iterable") % self._value)
181
181
182 def itermaps(self, context):
182 def itermaps(self, context):
183 raise error.ParseError(_('%r is not iterable of mappings')
183 raise error.ParseError(_('%r is not iterable of mappings')
184 % self._value)
184 % self._value)
185
185
186 def join(self, context, mapping, sep):
186 def join(self, context, mapping, sep):
187 raise error.ParseError(_('%r is not iterable') % self._value)
187 raise error.ParseError(_('%r is not iterable') % self._value)
188
188
189 def show(self, context, mapping):
189 def show(self, context, mapping):
190 if self._value is None:
190 if self._value is None:
191 return b''
191 return b''
192 return pycompat.bytestr(self._value)
192 return pycompat.bytestr(self._value)
193
193
194 def tobool(self, context, mapping):
194 def tobool(self, context, mapping):
195 if self._value is None:
195 if self._value is None:
196 return False
196 return False
197 if isinstance(self._value, bool):
197 if isinstance(self._value, bool):
198 return self._value
198 return self._value
199 # otherwise evaluate as string, which means 0 is True
199 # otherwise evaluate as string, which means 0 is True
200 return bool(pycompat.bytestr(self._value))
200 return bool(pycompat.bytestr(self._value))
201
201
202 def tovalue(self, context, mapping):
202 def tovalue(self, context, mapping):
203 return self._value
203 return self._value
204
204
205 class date(mappable, wrapped):
205 class date(mappable, wrapped):
206 """Wrapper for date tuple"""
206 """Wrapper for date tuple"""
207
207
208 def __init__(self, value, showfmt='%d %d'):
208 def __init__(self, value, showfmt='%d %d'):
209 # value may be (float, int), but public interface shouldn't support
209 # value may be (float, int), but public interface shouldn't support
210 # floating-point timestamp
210 # floating-point timestamp
211 self._unixtime, self._tzoffset = map(int, value)
211 self._unixtime, self._tzoffset = map(int, value)
212 self._showfmt = showfmt
212 self._showfmt = showfmt
213
213
214 def contains(self, context, mapping, item):
214 def contains(self, context, mapping, item):
215 raise error.ParseError(_('date is not iterable'))
215 raise error.ParseError(_('date is not iterable'))
216
216
217 def getmember(self, context, mapping, key):
217 def getmember(self, context, mapping, key):
218 raise error.ParseError(_('date is not a dictionary'))
218 raise error.ParseError(_('date is not a dictionary'))
219
219
220 def getmin(self, context, mapping):
220 def getmin(self, context, mapping):
221 raise error.ParseError(_('date is not iterable'))
221 raise error.ParseError(_('date is not iterable'))
222
222
223 def getmax(self, context, mapping):
223 def getmax(self, context, mapping):
224 raise error.ParseError(_('date is not iterable'))
224 raise error.ParseError(_('date is not iterable'))
225
225
226 def filter(self, context, mapping, select):
226 def filter(self, context, mapping, select):
227 raise error.ParseError(_('date is not iterable'))
227 raise error.ParseError(_('date is not iterable'))
228
228
229 def join(self, context, mapping, sep):
229 def join(self, context, mapping, sep):
230 raise error.ParseError(_("date is not iterable"))
230 raise error.ParseError(_("date is not iterable"))
231
231
232 def show(self, context, mapping):
232 def show(self, context, mapping):
233 return self._showfmt % (self._unixtime, self._tzoffset)
233 return self._showfmt % (self._unixtime, self._tzoffset)
234
234
235 def tomap(self, context):
235 def tomap(self, context):
236 return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
236 return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
237
237
238 def tobool(self, context, mapping):
238 def tobool(self, context, mapping):
239 return True
239 return True
240
240
241 def tovalue(self, context, mapping):
241 def tovalue(self, context, mapping):
242 return (self._unixtime, self._tzoffset)
242 return (self._unixtime, self._tzoffset)
243
243
244 class hybrid(wrapped):
244 class hybrid(wrapped):
245 """Wrapper for list or dict to support legacy template
245 """Wrapper for list or dict to support legacy template
246
246
247 This class allows us to handle both:
247 This class allows us to handle both:
248 - "{files}" (legacy command-line-specific list hack) and
248 - "{files}" (legacy command-line-specific list hack) and
249 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
249 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
250 and to access raw values:
250 and to access raw values:
251 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
251 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
252 - "{get(extras, key)}"
252 - "{get(extras, key)}"
253 - "{files|json}"
253 - "{files|json}"
254 """
254 """
255
255
256 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
256 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
257 self._gen = gen # generator or function returning generator
257 self._gen = gen # generator or function returning generator
258 self._values = values
258 self._values = values
259 self._makemap = makemap
259 self._makemap = makemap
260 self._joinfmt = joinfmt
260 self._joinfmt = joinfmt
261 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
261 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
262
262
263 def contains(self, context, mapping, item):
263 def contains(self, context, mapping, item):
264 item = unwrapastype(context, mapping, item, self._keytype)
264 item = unwrapastype(context, mapping, item, self._keytype)
265 return item in self._values
265 return item in self._values
266
266
267 def getmember(self, context, mapping, key):
267 def getmember(self, context, mapping, key):
268 # TODO: maybe split hybrid list/dict types?
268 # TODO: maybe split hybrid list/dict types?
269 if not util.safehasattr(self._values, 'get'):
269 if not util.safehasattr(self._values, 'get'):
270 raise error.ParseError(_('not a dictionary'))
270 raise error.ParseError(_('not a dictionary'))
271 key = unwrapastype(context, mapping, key, self._keytype)
271 key = unwrapastype(context, mapping, key, self._keytype)
272 return self._wrapvalue(key, self._values.get(key))
272 return self._wrapvalue(key, self._values.get(key))
273
273
274 def getmin(self, context, mapping):
274 def getmin(self, context, mapping):
275 return self._getby(context, mapping, min)
275 return self._getby(context, mapping, min)
276
276
277 def getmax(self, context, mapping):
277 def getmax(self, context, mapping):
278 return self._getby(context, mapping, max)
278 return self._getby(context, mapping, max)
279
279
280 def _getby(self, context, mapping, func):
280 def _getby(self, context, mapping, func):
281 if not self._values:
281 if not self._values:
282 raise error.ParseError(_('empty sequence'))
282 raise error.ParseError(_('empty sequence'))
283 val = func(self._values)
283 val = func(self._values)
284 return self._wrapvalue(val, val)
284 return self._wrapvalue(val, val)
285
285
286 def _wrapvalue(self, key, val):
286 def _wrapvalue(self, key, val):
287 if val is None:
287 if val is None:
288 return
288 return
289 if util.safehasattr(val, '_makemap'):
289 if util.safehasattr(val, '_makemap'):
290 # a nested hybrid list/dict, which has its own way of map operation
290 # a nested hybrid list/dict, which has its own way of map operation
291 return val
291 return val
292 return hybriditem(None, key, val, self._makemap)
292 return hybriditem(None, key, val, self._makemap)
293
293
294 def filter(self, context, mapping, select):
294 def filter(self, context, mapping, select):
295 if util.safehasattr(self._values, 'get'):
295 if util.safehasattr(self._values, 'get'):
296 values = {k: v for k, v in self._values.iteritems()
296 values = {k: v for k, v in self._values.iteritems()
297 if select(self._wrapvalue(k, v))}
297 if select(self._wrapvalue(k, v))}
298 else:
298 else:
299 values = [v for v in self._values if select(self._wrapvalue(v, v))]
299 values = [v for v in self._values if select(self._wrapvalue(v, v))]
300 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
300 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
301
301
302 def itermaps(self, context):
302 def itermaps(self, context):
303 makemap = self._makemap
303 makemap = self._makemap
304 for x in self._values:
304 for x in self._values:
305 yield makemap(x)
305 yield makemap(x)
306
306
307 def join(self, context, mapping, sep):
307 def join(self, context, mapping, sep):
308 # TODO: switch gen to (context, mapping) API?
308 # TODO: switch gen to (context, mapping) API?
309 return joinitems((self._joinfmt(x) for x in self._values), sep)
309 return joinitems((self._joinfmt(x) for x in self._values), sep)
310
310
311 def show(self, context, mapping):
311 def show(self, context, mapping):
312 # TODO: switch gen to (context, mapping) API?
312 # TODO: switch gen to (context, mapping) API?
313 gen = self._gen
313 gen = self._gen
314 if gen is None:
314 if gen is None:
315 return self.join(context, mapping, ' ')
315 return self.join(context, mapping, ' ')
316 if callable(gen):
316 if callable(gen):
317 return gen()
317 return gen()
318 return gen
318 return gen
319
319
320 def tobool(self, context, mapping):
320 def tobool(self, context, mapping):
321 return bool(self._values)
321 return bool(self._values)
322
322
323 def tovalue(self, context, mapping):
323 def tovalue(self, context, mapping):
324 # TODO: make it non-recursive for trivial lists/dicts
324 # TODO: make it non-recursive for trivial lists/dicts
325 xs = self._values
325 xs = self._values
326 if util.safehasattr(xs, 'get'):
326 if util.safehasattr(xs, 'get'):
327 return {k: unwrapvalue(context, mapping, v)
327 return {k: unwrapvalue(context, mapping, v)
328 for k, v in xs.iteritems()}
328 for k, v in xs.iteritems()}
329 return [unwrapvalue(context, mapping, x) for x in xs]
329 return [unwrapvalue(context, mapping, x) for x in xs]
330
330
331 class hybriditem(mappable, wrapped):
331 class hybriditem(mappable, wrapped):
332 """Wrapper for non-list/dict object to support map operation
332 """Wrapper for non-list/dict object to support map operation
333
333
334 This class allows us to handle both:
334 This class allows us to handle both:
335 - "{manifest}"
335 - "{manifest}"
336 - "{manifest % '{rev}:{node}'}"
336 - "{manifest % '{rev}:{node}'}"
337 - "{manifest.rev}"
337 - "{manifest.rev}"
338 """
338 """
339
339
340 def __init__(self, gen, key, value, makemap):
340 def __init__(self, gen, key, value, makemap):
341 self._gen = gen # generator or function returning generator
341 self._gen = gen # generator or function returning generator
342 self._key = key
342 self._key = key
343 self._value = value # may be generator of strings
343 self._value = value # may be generator of strings
344 self._makemap = makemap
344 self._makemap = makemap
345
345
346 def tomap(self, context):
346 def tomap(self, context):
347 return self._makemap(self._key)
347 return self._makemap(self._key)
348
348
349 def contains(self, context, mapping, item):
349 def contains(self, context, mapping, item):
350 w = makewrapped(context, mapping, self._value)
350 w = makewrapped(context, mapping, self._value)
351 return w.contains(context, mapping, item)
351 return w.contains(context, mapping, item)
352
352
353 def getmember(self, context, mapping, key):
353 def getmember(self, context, mapping, key):
354 w = makewrapped(context, mapping, self._value)
354 w = makewrapped(context, mapping, self._value)
355 return w.getmember(context, mapping, key)
355 return w.getmember(context, mapping, key)
356
356
357 def getmin(self, context, mapping):
357 def getmin(self, context, mapping):
358 w = makewrapped(context, mapping, self._value)
358 w = makewrapped(context, mapping, self._value)
359 return w.getmin(context, mapping)
359 return w.getmin(context, mapping)
360
360
361 def getmax(self, context, mapping):
361 def getmax(self, context, mapping):
362 w = makewrapped(context, mapping, self._value)
362 w = makewrapped(context, mapping, self._value)
363 return w.getmax(context, mapping)
363 return w.getmax(context, mapping)
364
364
365 def filter(self, context, mapping, select):
365 def filter(self, context, mapping, select):
366 w = makewrapped(context, mapping, self._value)
366 w = makewrapped(context, mapping, self._value)
367 return w.filter(context, mapping, select)
367 return w.filter(context, mapping, select)
368
368
369 def join(self, context, mapping, sep):
369 def join(self, context, mapping, sep):
370 w = makewrapped(context, mapping, self._value)
370 w = makewrapped(context, mapping, self._value)
371 return w.join(context, mapping, sep)
371 return w.join(context, mapping, sep)
372
372
373 def show(self, context, mapping):
373 def show(self, context, mapping):
374 # TODO: switch gen to (context, mapping) API?
374 # TODO: switch gen to (context, mapping) API?
375 gen = self._gen
375 gen = self._gen
376 if gen is None:
376 if gen is None:
377 return pycompat.bytestr(self._value)
377 return pycompat.bytestr(self._value)
378 if callable(gen):
378 if callable(gen):
379 return gen()
379 return gen()
380 return gen
380 return gen
381
381
382 def tobool(self, context, mapping):
382 def tobool(self, context, mapping):
383 w = makewrapped(context, mapping, self._value)
383 w = makewrapped(context, mapping, self._value)
384 return w.tobool(context, mapping)
384 return w.tobool(context, mapping)
385
385
386 def tovalue(self, context, mapping):
386 def tovalue(self, context, mapping):
387 return _unthunk(context, mapping, self._value)
387 return _unthunk(context, mapping, self._value)
388
388
389 class _mappingsequence(wrapped):
389 class _mappingsequence(wrapped):
390 """Wrapper for sequence of template mappings
390 """Wrapper for sequence of template mappings
391
391
392 This represents an inner template structure (i.e. a list of dicts),
392 This represents an inner template structure (i.e. a list of dicts),
393 which can also be rendered by the specified named/literal template.
393 which can also be rendered by the specified named/literal template.
394
394
395 Template mappings may be nested.
395 Template mappings may be nested.
396 """
396 """
397
397
398 def __init__(self, name=None, tmpl=None, sep=''):
398 def __init__(self, name=None, tmpl=None, sep=''):
399 if name is not None and tmpl is not None:
399 if name is not None and tmpl is not None:
400 raise error.ProgrammingError('name and tmpl are mutually exclusive')
400 raise error.ProgrammingError('name and tmpl are mutually exclusive')
401 self._name = name
401 self._name = name
402 self._tmpl = tmpl
402 self._tmpl = tmpl
403 self._defaultsep = sep
403 self._defaultsep = sep
404
404
405 def contains(self, context, mapping, item):
405 def contains(self, context, mapping, item):
406 raise error.ParseError(_('not comparable'))
406 raise error.ParseError(_('not comparable'))
407
407
408 def getmember(self, context, mapping, key):
408 def getmember(self, context, mapping, key):
409 raise error.ParseError(_('not a dictionary'))
409 raise error.ParseError(_('not a dictionary'))
410
410
411 def getmin(self, context, mapping):
411 def getmin(self, context, mapping):
412 raise error.ParseError(_('not comparable'))
412 raise error.ParseError(_('not comparable'))
413
413
414 def getmax(self, context, mapping):
414 def getmax(self, context, mapping):
415 raise error.ParseError(_('not comparable'))
415 raise error.ParseError(_('not comparable'))
416
416
417 def filter(self, context, mapping, select):
417 def filter(self, context, mapping, select):
418 # implement if necessary; we'll need a wrapped type for a mapping dict
418 # implement if necessary; we'll need a wrapped type for a mapping dict
419 raise error.ParseError(_('not filterable without template'))
419 raise error.ParseError(_('not filterable without template'))
420
420
421 def join(self, context, mapping, sep):
421 def join(self, context, mapping, sep):
422 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
422 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
423 if self._name:
423 if self._name:
424 itemiter = (context.process(self._name, m) for m in mapsiter)
424 itemiter = (context.process(self._name, m) for m in mapsiter)
425 elif self._tmpl:
425 elif self._tmpl:
426 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
426 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
427 else:
427 else:
428 raise error.ParseError(_('not displayable without template'))
428 raise error.ParseError(_('not displayable without template'))
429 return joinitems(itemiter, sep)
429 return joinitems(itemiter, sep)
430
430
431 def show(self, context, mapping):
431 def show(self, context, mapping):
432 return self.join(context, mapping, self._defaultsep)
432 return self.join(context, mapping, self._defaultsep)
433
433
434 def tovalue(self, context, mapping):
434 def tovalue(self, context, mapping):
435 knownres = context.knownresourcekeys()
435 knownres = context.knownresourcekeys()
436 items = []
436 items = []
437 for nm in self.itermaps(context):
437 for nm in self.itermaps(context):
438 # drop internal resources (recursively) which shouldn't be displayed
438 # drop internal resources (recursively) which shouldn't be displayed
439 lm = context.overlaymap(mapping, nm)
439 lm = context.overlaymap(mapping, nm)
440 items.append({k: unwrapvalue(context, lm, v)
440 items.append({k: unwrapvalue(context, lm, v)
441 for k, v in nm.iteritems() if k not in knownres})
441 for k, v in nm.iteritems() if k not in knownres})
442 return items
442 return items
443
443
444 class mappinggenerator(_mappingsequence):
444 class mappinggenerator(_mappingsequence):
445 """Wrapper for generator of template mappings
445 """Wrapper for generator of template mappings
446
446
447 The function ``make(context, *args)`` should return a generator of
447 The function ``make(context, *args)`` should return a generator of
448 mapping dicts.
448 mapping dicts.
449 """
449 """
450
450
451 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
451 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
452 super(mappinggenerator, self).__init__(name, tmpl, sep)
452 super(mappinggenerator, self).__init__(name, tmpl, sep)
453 self._make = make
453 self._make = make
454 self._args = args
454 self._args = args
455
455
456 def itermaps(self, context):
456 def itermaps(self, context):
457 return self._make(context, *self._args)
457 return self._make(context, *self._args)
458
458
459 def tobool(self, context, mapping):
459 def tobool(self, context, mapping):
460 return _nonempty(self.itermaps(context))
460 return _nonempty(self.itermaps(context))
461
461
462 class mappinglist(_mappingsequence):
462 class mappinglist(_mappingsequence):
463 """Wrapper for list of template mappings"""
463 """Wrapper for list of template mappings"""
464
464
465 def __init__(self, mappings, name=None, tmpl=None, sep=''):
465 def __init__(self, mappings, name=None, tmpl=None, sep=''):
466 super(mappinglist, self).__init__(name, tmpl, sep)
466 super(mappinglist, self).__init__(name, tmpl, sep)
467 self._mappings = mappings
467 self._mappings = mappings
468
468
469 def itermaps(self, context):
469 def itermaps(self, context):
470 return iter(self._mappings)
470 return iter(self._mappings)
471
471
472 def tobool(self, context, mapping):
472 def tobool(self, context, mapping):
473 return bool(self._mappings)
473 return bool(self._mappings)
474
474
475 class mappedgenerator(wrapped):
475 class mappedgenerator(wrapped):
476 """Wrapper for generator of strings which acts as a list
476 """Wrapper for generator of strings which acts as a list
477
477
478 The function ``make(context, *args)`` should return a generator of
478 The function ``make(context, *args)`` should return a generator of
479 byte strings, or a generator of (possibly nested) generators of byte
479 byte strings, or a generator of (possibly nested) generators of byte
480 strings (i.e. a generator for a list of byte strings.)
480 strings (i.e. a generator for a list of byte strings.)
481 """
481 """
482
482
483 def __init__(self, make, args=()):
483 def __init__(self, make, args=()):
484 self._make = make
484 self._make = make
485 self._args = args
485 self._args = args
486
486
487 def contains(self, context, mapping, item):
487 def contains(self, context, mapping, item):
488 item = stringify(context, mapping, item)
488 item = stringify(context, mapping, item)
489 return item in self.tovalue(context, mapping)
489 return item in self.tovalue(context, mapping)
490
490
491 def _gen(self, context):
491 def _gen(self, context):
492 return self._make(context, *self._args)
492 return self._make(context, *self._args)
493
493
494 def getmember(self, context, mapping, key):
494 def getmember(self, context, mapping, key):
495 raise error.ParseError(_('not a dictionary'))
495 raise error.ParseError(_('not a dictionary'))
496
496
497 def getmin(self, context, mapping):
497 def getmin(self, context, mapping):
498 return self._getby(context, mapping, min)
498 return self._getby(context, mapping, min)
499
499
500 def getmax(self, context, mapping):
500 def getmax(self, context, mapping):
501 return self._getby(context, mapping, max)
501 return self._getby(context, mapping, max)
502
502
503 def _getby(self, context, mapping, func):
503 def _getby(self, context, mapping, func):
504 xs = self.tovalue(context, mapping)
504 xs = self.tovalue(context, mapping)
505 if not xs:
505 if not xs:
506 raise error.ParseError(_('empty sequence'))
506 raise error.ParseError(_('empty sequence'))
507 return func(xs)
507 return func(xs)
508
508
509 @staticmethod
509 @staticmethod
510 def _filteredgen(context, mapping, make, args, select):
510 def _filteredgen(context, mapping, make, args, select):
511 for x in make(context, *args):
511 for x in make(context, *args):
512 s = stringify(context, mapping, x)
512 s = stringify(context, mapping, x)
513 if select(wrappedbytes(s)):
513 if select(wrappedbytes(s)):
514 yield s
514 yield s
515
515
516 def filter(self, context, mapping, select):
516 def filter(self, context, mapping, select):
517 args = (mapping, self._make, self._args, select)
517 args = (mapping, self._make, self._args, select)
518 return mappedgenerator(self._filteredgen, args)
518 return mappedgenerator(self._filteredgen, args)
519
519
520 def itermaps(self, context):
520 def itermaps(self, context):
521 raise error.ParseError(_('list of strings is not mappable'))
521 raise error.ParseError(_('list of strings is not mappable'))
522
522
523 def join(self, context, mapping, sep):
523 def join(self, context, mapping, sep):
524 return joinitems(self._gen(context), sep)
524 return joinitems(self._gen(context), sep)
525
525
526 def show(self, context, mapping):
526 def show(self, context, mapping):
527 return self.join(context, mapping, '')
527 return self.join(context, mapping, '')
528
528
529 def tobool(self, context, mapping):
529 def tobool(self, context, mapping):
530 return _nonempty(self._gen(context))
530 return _nonempty(self._gen(context))
531
531
532 def tovalue(self, context, mapping):
532 def tovalue(self, context, mapping):
533 return [stringify(context, mapping, x) for x in self._gen(context)]
533 return [stringify(context, mapping, x) for x in self._gen(context)]
534
534
535 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
535 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
536 """Wrap data to support both dict-like and string-like operations"""
536 """Wrap data to support both dict-like and string-like operations"""
537 prefmt = pycompat.identity
537 prefmt = pycompat.identity
538 if fmt is None:
538 if fmt is None:
539 fmt = '%s=%s'
539 fmt = '%s=%s'
540 prefmt = pycompat.bytestr
540 prefmt = pycompat.bytestr
541 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
541 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
542 lambda k: fmt % (prefmt(k), prefmt(data[k])))
542 lambda k: fmt % (prefmt(k), prefmt(data[k])))
543
543
544 def hybridlist(data, name, fmt=None, gen=None):
544 def hybridlist(data, name, fmt=None, gen=None):
545 """Wrap data to support both list-like and string-like operations"""
545 """Wrap data to support both list-like and string-like operations"""
546 prefmt = pycompat.identity
546 prefmt = pycompat.identity
547 if fmt is None:
547 if fmt is None:
548 fmt = '%s'
548 fmt = '%s'
549 prefmt = pycompat.bytestr
549 prefmt = pycompat.bytestr
550 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
550 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
551
551
552 def compatdict(context, mapping, name, data, key='key', value='value',
552 def compatdict(context, mapping, name, data, key='key', value='value',
553 fmt=None, plural=None, separator=' '):
553 fmt=None, plural=None, separator=' '):
554 """Wrap data like hybriddict(), but also supports old-style list template
554 """Wrap data like hybriddict(), but also supports old-style list template
555
555
556 This exists for backward compatibility with the old-style template. Use
556 This exists for backward compatibility with the old-style template. Use
557 hybriddict() for new template keywords.
557 hybriddict() for new template keywords.
558 """
558 """
559 c = [{key: k, value: v} for k, v in data.iteritems()]
559 c = [{key: k, value: v} for k, v in data.iteritems()]
560 f = _showcompatlist(context, mapping, name, c, plural, separator)
560 f = _showcompatlist(context, mapping, name, c, plural, separator)
561 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
561 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
562
562
563 def compatlist(context, mapping, name, data, element=None, fmt=None,
563 def compatlist(context, mapping, name, data, element=None, fmt=None,
564 plural=None, separator=' '):
564 plural=None, separator=' '):
565 """Wrap data like hybridlist(), but also supports old-style list template
565 """Wrap data like hybridlist(), but also supports old-style list template
566
566
567 This exists for backward compatibility with the old-style template. Use
567 This exists for backward compatibility with the old-style template. Use
568 hybridlist() for new template keywords.
568 hybridlist() for new template keywords.
569 """
569 """
570 f = _showcompatlist(context, mapping, name, data, plural, separator)
570 f = _showcompatlist(context, mapping, name, data, plural, separator)
571 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
571 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
572
572
573 def compatfilecopiesdict(context, mapping, name, copies):
573 def compatfilecopiesdict(context, mapping, name, copies):
574 """Wrap list of (dest, source) file names to support old-style list
574 """Wrap list of (dest, source) file names to support old-style list
575 template and field names
575 template and field names
576
576
577 This exists for backward compatibility. Use hybriddict for new template
577 This exists for backward compatibility. Use hybriddict for new template
578 keywords.
578 keywords.
579 """
579 """
580 # no need to provide {path} to old-style list template
580 # no need to provide {path} to old-style list template
581 c = [{'name': k, 'source': v} for k, v in copies]
581 c = [{'name': k, 'source': v} for k, v in copies]
582 f = _showcompatlist(context, mapping, name, c, plural='file_copies')
582 f = _showcompatlist(context, mapping, name, c, plural='file_copies')
583 copies = util.sortdict(copies)
583 copies = util.sortdict(copies)
584 return hybrid(f, copies,
584 return hybrid(f, copies,
585 lambda k: {'name': k, 'path': k, 'source': copies[k]},
585 lambda k: {'name': k, 'path': k, 'source': copies[k]},
586 lambda k: '%s (%s)' % (k, copies[k]))
586 lambda k: '%s (%s)' % (k, copies[k]))
587
587
588 def compatfileslist(context, mapping, name, files):
588 def compatfileslist(context, mapping, name, files):
589 """Wrap list of file names to support old-style list template and field
589 """Wrap list of file names to support old-style list template and field
590 names
590 names
591
591
592 This exists for backward compatibility. Use hybridlist for new template
592 This exists for backward compatibility. Use hybridlist for new template
593 keywords.
593 keywords.
594 """
594 """
595 f = _showcompatlist(context, mapping, name, files)
595 f = _showcompatlist(context, mapping, name, files)
596 return hybrid(f, files, lambda x: {'file': x, 'path': x},
596 return hybrid(f, files, lambda x: {'file': x, 'path': x},
597 pycompat.identity)
597 pycompat.identity)
598
598
599 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
599 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
600 """Return a generator that renders old-style list template
600 """Return a generator that renders old-style list template
601
601
602 name is name of key in template map.
602 name is name of key in template map.
603 values is list of strings or dicts.
603 values is list of strings or dicts.
604 plural is plural of name, if not simply name + 's'.
604 plural is plural of name, if not simply name + 's'.
605 separator is used to join values as a string
605 separator is used to join values as a string
606
606
607 expansion works like this, given name 'foo'.
607 expansion works like this, given name 'foo'.
608
608
609 if values is empty, expand 'no_foos'.
609 if values is empty, expand 'no_foos'.
610
610
611 if 'foo' not in template map, return values as a string,
611 if 'foo' not in template map, return values as a string,
612 joined by 'separator'.
612 joined by 'separator'.
613
613
614 expand 'start_foos'.
614 expand 'start_foos'.
615
615
616 for each value, expand 'foo'. if 'last_foo' in template
616 for each value, expand 'foo'. if 'last_foo' in template
617 map, expand it instead of 'foo' for last key.
617 map, expand it instead of 'foo' for last key.
618
618
619 expand 'end_foos'.
619 expand 'end_foos'.
620 """
620 """
621 if not plural:
621 if not plural:
622 plural = name + 's'
622 plural = name + 's'
623 if not values:
623 if not values:
624 noname = 'no_' + plural
624 noname = 'no_' + plural
625 if context.preload(noname):
625 if context.preload(noname):
626 yield context.process(noname, mapping)
626 yield context.process(noname, mapping)
627 return
627 return
628 if not context.preload(name):
628 if not context.preload(name):
629 if isinstance(values[0], bytes):
629 if isinstance(values[0], bytes):
630 yield separator.join(values)
630 yield separator.join(values)
631 else:
631 else:
632 for v in values:
632 for v in values:
633 r = dict(v)
633 r = dict(v)
634 r.update(mapping)
634 r.update(mapping)
635 yield r
635 yield r
636 return
636 return
637 startname = 'start_' + plural
637 startname = 'start_' + plural
638 if context.preload(startname):
638 if context.preload(startname):
639 yield context.process(startname, mapping)
639 yield context.process(startname, mapping)
640 def one(v, tag=name):
640 def one(v, tag=name):
641 vmapping = {}
641 vmapping = {}
642 try:
642 try:
643 vmapping.update(v)
643 vmapping.update(v)
644 # Python 2 raises ValueError if the type of v is wrong. Python
644 # Python 2 raises ValueError if the type of v is wrong. Python
645 # 3 raises TypeError.
645 # 3 raises TypeError.
646 except (AttributeError, TypeError, ValueError):
646 except (AttributeError, TypeError, ValueError):
647 try:
647 try:
648 # Python 2 raises ValueError trying to destructure an e.g.
648 # Python 2 raises ValueError trying to destructure an e.g.
649 # bytes. Python 3 raises TypeError.
649 # bytes. Python 3 raises TypeError.
650 for a, b in v:
650 for a, b in v:
651 vmapping[a] = b
651 vmapping[a] = b
652 except (TypeError, ValueError):
652 except (TypeError, ValueError):
653 vmapping[name] = v
653 vmapping[name] = v
654 vmapping = context.overlaymap(mapping, vmapping)
654 vmapping = context.overlaymap(mapping, vmapping)
655 return context.process(tag, vmapping)
655 return context.process(tag, vmapping)
656 lastname = 'last_' + name
656 lastname = 'last_' + name
657 if context.preload(lastname):
657 if context.preload(lastname):
658 last = values.pop()
658 last = values.pop()
659 else:
659 else:
660 last = None
660 last = None
661 for v in values:
661 for v in values:
662 yield one(v)
662 yield one(v)
663 if last is not None:
663 if last is not None:
664 yield one(last, tag=lastname)
664 yield one(last, tag=lastname)
665 endname = 'end_' + plural
665 endname = 'end_' + plural
666 if context.preload(endname):
666 if context.preload(endname):
667 yield context.process(endname, mapping)
667 yield context.process(endname, mapping)
668
668
669 def flatten(context, mapping, thing):
669 def flatten(context, mapping, thing):
670 """Yield a single stream from a possibly nested set of iterators"""
670 """Yield a single stream from a possibly nested set of iterators"""
671 if isinstance(thing, wrapped):
671 if isinstance(thing, wrapped):
672 thing = thing.show(context, mapping)
672 thing = thing.show(context, mapping)
673 if isinstance(thing, bytes):
673 if isinstance(thing, bytes):
674 yield thing
674 yield thing
675 elif isinstance(thing, str):
675 elif isinstance(thing, str):
676 # We can only hit this on Python 3, and it's here to guard
676 # We can only hit this on Python 3, and it's here to guard
677 # against infinite recursion.
677 # against infinite recursion.
678 raise error.ProgrammingError('Mercurial IO including templates is done'
678 raise error.ProgrammingError('Mercurial IO including templates is done'
679 ' with bytes, not strings, got %r' % thing)
679 ' with bytes, not strings, got %r' % thing)
680 elif thing is None:
680 elif thing is None:
681 pass
681 pass
682 elif not util.safehasattr(thing, '__iter__'):
682 elif not util.safehasattr(thing, '__iter__'):
683 yield pycompat.bytestr(thing)
683 yield pycompat.bytestr(thing)
684 else:
684 else:
685 for i in thing:
685 for i in thing:
686 if isinstance(i, wrapped):
686 if isinstance(i, wrapped):
687 i = i.show(context, mapping)
687 i = i.show(context, mapping)
688 if isinstance(i, bytes):
688 if isinstance(i, bytes):
689 yield i
689 yield i
690 elif i is None:
690 elif i is None:
691 pass
691 pass
692 elif not util.safehasattr(i, '__iter__'):
692 elif not util.safehasattr(i, '__iter__'):
693 yield pycompat.bytestr(i)
693 yield pycompat.bytestr(i)
694 else:
694 else:
695 for j in flatten(context, mapping, i):
695 for j in flatten(context, mapping, i):
696 yield j
696 yield j
697
697
698 def stringify(context, mapping, thing):
698 def stringify(context, mapping, thing):
699 """Turn values into bytes by converting into text and concatenating them"""
699 """Turn values into bytes by converting into text and concatenating them"""
700 if isinstance(thing, bytes):
700 if isinstance(thing, bytes):
701 return thing # retain localstr to be round-tripped
701 return thing # retain localstr to be round-tripped
702 return b''.join(flatten(context, mapping, thing))
702 return b''.join(flatten(context, mapping, thing))
703
703
704 def findsymbolicname(arg):
704 def findsymbolicname(arg):
705 """Find symbolic name for the given compiled expression; returns None
705 """Find symbolic name for the given compiled expression; returns None
706 if nothing found reliably"""
706 if nothing found reliably"""
707 while True:
707 while True:
708 func, data = arg
708 func, data = arg
709 if func is runsymbol:
709 if func is runsymbol:
710 return data
710 return data
711 elif func is runfilter:
711 elif func is runfilter:
712 arg = data[0]
712 arg = data[0]
713 else:
713 else:
714 return None
714 return None
715
715
716 def _nonempty(xiter):
716 def _nonempty(xiter):
717 try:
717 try:
718 next(xiter)
718 next(xiter)
719 return True
719 return True
720 except StopIteration:
720 except StopIteration:
721 return False
721 return False
722
722
723 def _unthunk(context, mapping, thing):
723 def _unthunk(context, mapping, thing):
724 """Evaluate a lazy byte string into value"""
724 """Evaluate a lazy byte string into value"""
725 if not isinstance(thing, types.GeneratorType):
725 if not isinstance(thing, types.GeneratorType):
726 return thing
726 return thing
727 return stringify(context, mapping, thing)
727 return stringify(context, mapping, thing)
728
728
729 def evalrawexp(context, mapping, arg):
729 def evalrawexp(context, mapping, arg):
730 """Evaluate given argument as a bare template object which may require
730 """Evaluate given argument as a bare template object which may require
731 further processing (such as folding generator of strings)"""
731 further processing (such as folding generator of strings)"""
732 func, data = arg
732 func, data = arg
733 return func(context, mapping, data)
733 return func(context, mapping, data)
734
734
735 def evalwrapped(context, mapping, arg):
735 def evalwrapped(context, mapping, arg):
736 """Evaluate given argument to wrapped object"""
736 """Evaluate given argument to wrapped object"""
737 thing = evalrawexp(context, mapping, arg)
737 thing = evalrawexp(context, mapping, arg)
738 return makewrapped(context, mapping, thing)
738 return makewrapped(context, mapping, thing)
739
739
740 def makewrapped(context, mapping, thing):
740 def makewrapped(context, mapping, thing):
741 """Lift object to a wrapped type"""
741 """Lift object to a wrapped type"""
742 if isinstance(thing, wrapped):
742 if isinstance(thing, wrapped):
743 return thing
743 return thing
744 thing = _unthunk(context, mapping, thing)
744 thing = _unthunk(context, mapping, thing)
745 if isinstance(thing, bytes):
745 if isinstance(thing, bytes):
746 return wrappedbytes(thing)
746 return wrappedbytes(thing)
747 return wrappedvalue(thing)
747 return wrappedvalue(thing)
748
748
749 def evalfuncarg(context, mapping, arg):
749 def evalfuncarg(context, mapping, arg):
750 """Evaluate given argument as value type"""
750 """Evaluate given argument as value type"""
751 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
751 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
752
752
753 def unwrapvalue(context, mapping, thing):
753 def unwrapvalue(context, mapping, thing):
754 """Move the inner value object out of the wrapper"""
754 """Move the inner value object out of the wrapper"""
755 if isinstance(thing, wrapped):
755 if isinstance(thing, wrapped):
756 return thing.tovalue(context, mapping)
756 return thing.tovalue(context, mapping)
757 # evalrawexp() may return string, generator of strings or arbitrary object
757 # evalrawexp() may return string, generator of strings or arbitrary object
758 # such as date tuple, but filter does not want generator.
758 # such as date tuple, but filter does not want generator.
759 return _unthunk(context, mapping, thing)
759 return _unthunk(context, mapping, thing)
760
760
761 def evalboolean(context, mapping, arg):
761 def evalboolean(context, mapping, arg):
762 """Evaluate given argument as boolean, but also takes boolean literals"""
762 """Evaluate given argument as boolean, but also takes boolean literals"""
763 func, data = arg
763 func, data = arg
764 if func is runsymbol:
764 if func is runsymbol:
765 thing = func(context, mapping, data, default=None)
765 thing = func(context, mapping, data, default=None)
766 if thing is None:
766 if thing is None:
767 # not a template keyword, takes as a boolean literal
767 # not a template keyword, takes as a boolean literal
768 thing = stringutil.parsebool(data)
768 thing = stringutil.parsebool(data)
769 else:
769 else:
770 thing = func(context, mapping, data)
770 thing = func(context, mapping, data)
771 return makewrapped(context, mapping, thing).tobool(context, mapping)
771 return makewrapped(context, mapping, thing).tobool(context, mapping)
772
772
773 def evaldate(context, mapping, arg, err=None):
773 def evaldate(context, mapping, arg, err=None):
774 """Evaluate given argument as a date tuple or a date string; returns
774 """Evaluate given argument as a date tuple or a date string; returns
775 a (unixtime, offset) tuple"""
775 a (unixtime, offset) tuple"""
776 thing = evalrawexp(context, mapping, arg)
776 thing = evalrawexp(context, mapping, arg)
777 return unwrapdate(context, mapping, thing, err)
777 return unwrapdate(context, mapping, thing, err)
778
778
779 def unwrapdate(context, mapping, thing, err=None):
779 def unwrapdate(context, mapping, thing, err=None):
780 if isinstance(thing, date):
780 if isinstance(thing, date):
781 return thing.tovalue(context, mapping)
781 return thing.tovalue(context, mapping)
782 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
782 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
783 thing = unwrapvalue(context, mapping, thing)
783 thing = unwrapvalue(context, mapping, thing)
784 try:
784 try:
785 return dateutil.parsedate(thing)
785 return dateutil.parsedate(thing)
786 except AttributeError:
786 except AttributeError:
787 raise error.ParseError(err or _('not a date tuple nor a string'))
787 raise error.ParseError(err or _('not a date tuple nor a string'))
788 except error.ParseError:
788 except error.ParseError:
789 if not err:
789 if not err:
790 raise
790 raise
791 raise error.ParseError(err)
791 raise error.ParseError(err)
792
792
793 def evalinteger(context, mapping, arg, err=None):
793 def evalinteger(context, mapping, arg, err=None):
794 thing = evalrawexp(context, mapping, arg)
794 thing = evalrawexp(context, mapping, arg)
795 return unwrapinteger(context, mapping, thing, err)
795 return unwrapinteger(context, mapping, thing, err)
796
796
797 def unwrapinteger(context, mapping, thing, err=None):
797 def unwrapinteger(context, mapping, thing, err=None):
798 thing = unwrapvalue(context, mapping, thing)
798 thing = unwrapvalue(context, mapping, thing)
799 try:
799 try:
800 return int(thing)
800 return int(thing)
801 except (TypeError, ValueError):
801 except (TypeError, ValueError):
802 raise error.ParseError(err or _('not an integer'))
802 raise error.ParseError(err or _('not an integer'))
803
803
804 def evalstring(context, mapping, arg):
804 def evalstring(context, mapping, arg):
805 return stringify(context, mapping, evalrawexp(context, mapping, arg))
805 return stringify(context, mapping, evalrawexp(context, mapping, arg))
806
806
807 def evalstringliteral(context, mapping, arg):
807 def evalstringliteral(context, mapping, arg):
808 """Evaluate given argument as string template, but returns symbol name
808 """Evaluate given argument as string template, but returns symbol name
809 if it is unknown"""
809 if it is unknown"""
810 func, data = arg
810 func, data = arg
811 if func is runsymbol:
811 if func is runsymbol:
812 thing = func(context, mapping, data, default=data)
812 thing = func(context, mapping, data, default=data)
813 else:
813 else:
814 thing = func(context, mapping, data)
814 thing = func(context, mapping, data)
815 return stringify(context, mapping, thing)
815 return stringify(context, mapping, thing)
816
816
817 _unwrapfuncbytype = {
817 _unwrapfuncbytype = {
818 None: unwrapvalue,
818 None: unwrapvalue,
819 bytes: stringify,
819 bytes: stringify,
820 date: unwrapdate,
820 date: unwrapdate,
821 int: unwrapinteger,
821 int: unwrapinteger,
822 }
822 }
823
823
824 def unwrapastype(context, mapping, thing, typ):
824 def unwrapastype(context, mapping, thing, typ):
825 """Move the inner value object out of the wrapper and coerce its type"""
825 """Move the inner value object out of the wrapper and coerce its type"""
826 try:
826 try:
827 f = _unwrapfuncbytype[typ]
827 f = _unwrapfuncbytype[typ]
828 except KeyError:
828 except KeyError:
829 raise error.ProgrammingError('invalid type specified: %r' % typ)
829 raise error.ProgrammingError('invalid type specified: %r' % typ)
830 return f(context, mapping, thing)
830 return f(context, mapping, thing)
831
831
832 def runinteger(context, mapping, data):
832 def runinteger(context, mapping, data):
833 return int(data)
833 return int(data)
834
834
835 def runstring(context, mapping, data):
835 def runstring(context, mapping, data):
836 return data
836 return data
837
837
838 def _recursivesymbolblocker(key):
838 def _recursivesymbolblocker(key):
839 def showrecursion(context, mapping):
839 def showrecursion(context, mapping):
840 raise error.Abort(_("recursive reference '%s' in template") % key)
840 raise error.Abort(_("recursive reference '%s' in template") % key)
841 showrecursion._requires = () # mark as new-style templatekw
841 showrecursion._requires = () # mark as new-style templatekw
842 return showrecursion
842 return showrecursion
843
843
844 def runsymbol(context, mapping, key, default=''):
844 def runsymbol(context, mapping, key, default=''):
845 v = context.symbol(mapping, key)
845 v = context.symbol(mapping, key)
846 if v is None:
846 if v is None:
847 # put poison to cut recursion. we can't move this to parsing phase
847 # put poison to cut recursion. we can't move this to parsing phase
848 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
848 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
849 safemapping = mapping.copy()
849 safemapping = mapping.copy()
850 safemapping[key] = _recursivesymbolblocker(key)
850 safemapping[key] = _recursivesymbolblocker(key)
851 try:
851 try:
852 v = context.process(key, safemapping)
852 v = context.process(key, safemapping)
853 except TemplateNotFound:
853 except TemplateNotFound:
854 v = default
854 v = default
855 if callable(v) and getattr(v, '_requires', None) is None:
855 if callable(v) and getattr(v, '_requires', None) is None:
856 # old templatekw: expand all keywords and resources
856 # old templatekw: expand all keywords and resources
857 # (TODO: drop support for old-style functions. 'f._requires = ()'
857 # (TODO: drop support for old-style functions. 'f._requires = ()'
858 # can be removed.)
858 # can be removed.)
859 props = {k: context._resources.lookup(context, mapping, k)
859 props = {k: context._resources.lookup(mapping, k)
860 for k in context._resources.knownkeys()}
860 for k in context._resources.knownkeys()}
861 # pass context to _showcompatlist() through templatekw._showlist()
861 # pass context to _showcompatlist() through templatekw._showlist()
862 props['templ'] = context
862 props['templ'] = context
863 props.update(mapping)
863 props.update(mapping)
864 ui = props.get('ui')
864 ui = props.get('ui')
865 if ui:
865 if ui:
866 ui.deprecwarn("old-style template keyword '%s'" % key, '4.8')
866 ui.deprecwarn("old-style template keyword '%s'" % key, '4.8')
867 return v(**pycompat.strkwargs(props))
867 return v(**pycompat.strkwargs(props))
868 if callable(v):
868 if callable(v):
869 # new templatekw
869 # new templatekw
870 try:
870 try:
871 return v(context, mapping)
871 return v(context, mapping)
872 except ResourceUnavailable:
872 except ResourceUnavailable:
873 # unsupported keyword is mapped to empty just like unknown keyword
873 # unsupported keyword is mapped to empty just like unknown keyword
874 return None
874 return None
875 return v
875 return v
876
876
877 def runtemplate(context, mapping, template):
877 def runtemplate(context, mapping, template):
878 for arg in template:
878 for arg in template:
879 yield evalrawexp(context, mapping, arg)
879 yield evalrawexp(context, mapping, arg)
880
880
881 def runfilter(context, mapping, data):
881 def runfilter(context, mapping, data):
882 arg, filt = data
882 arg, filt = data
883 thing = evalrawexp(context, mapping, arg)
883 thing = evalrawexp(context, mapping, arg)
884 intype = getattr(filt, '_intype', None)
884 intype = getattr(filt, '_intype', None)
885 try:
885 try:
886 thing = unwrapastype(context, mapping, thing, intype)
886 thing = unwrapastype(context, mapping, thing, intype)
887 return filt(thing)
887 return filt(thing)
888 except error.ParseError as e:
888 except error.ParseError as e:
889 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
889 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
890
890
891 def _formatfiltererror(arg, filt):
891 def _formatfiltererror(arg, filt):
892 fn = pycompat.sysbytes(filt.__name__)
892 fn = pycompat.sysbytes(filt.__name__)
893 sym = findsymbolicname(arg)
893 sym = findsymbolicname(arg)
894 if not sym:
894 if not sym:
895 return _("incompatible use of template filter '%s'") % fn
895 return _("incompatible use of template filter '%s'") % fn
896 return (_("template filter '%s' is not compatible with keyword '%s'")
896 return (_("template filter '%s' is not compatible with keyword '%s'")
897 % (fn, sym))
897 % (fn, sym))
898
898
899 def _iteroverlaymaps(context, origmapping, newmappings):
899 def _iteroverlaymaps(context, origmapping, newmappings):
900 """Generate combined mappings from the original mapping and an iterable
900 """Generate combined mappings from the original mapping and an iterable
901 of partial mappings to override the original"""
901 of partial mappings to override the original"""
902 for i, nm in enumerate(newmappings):
902 for i, nm in enumerate(newmappings):
903 lm = context.overlaymap(origmapping, nm)
903 lm = context.overlaymap(origmapping, nm)
904 lm['index'] = i
904 lm['index'] = i
905 yield lm
905 yield lm
906
906
907 def _applymap(context, mapping, d, darg, targ):
907 def _applymap(context, mapping, d, darg, targ):
908 try:
908 try:
909 diter = d.itermaps(context)
909 diter = d.itermaps(context)
910 except error.ParseError as err:
910 except error.ParseError as err:
911 sym = findsymbolicname(darg)
911 sym = findsymbolicname(darg)
912 if not sym:
912 if not sym:
913 raise
913 raise
914 hint = _("keyword '%s' does not support map operation") % sym
914 hint = _("keyword '%s' does not support map operation") % sym
915 raise error.ParseError(bytes(err), hint=hint)
915 raise error.ParseError(bytes(err), hint=hint)
916 for lm in _iteroverlaymaps(context, mapping, diter):
916 for lm in _iteroverlaymaps(context, mapping, diter):
917 yield evalrawexp(context, lm, targ)
917 yield evalrawexp(context, lm, targ)
918
918
919 def runmap(context, mapping, data):
919 def runmap(context, mapping, data):
920 darg, targ = data
920 darg, targ = data
921 d = evalwrapped(context, mapping, darg)
921 d = evalwrapped(context, mapping, darg)
922 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
922 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
923
923
924 def runmember(context, mapping, data):
924 def runmember(context, mapping, data):
925 darg, memb = data
925 darg, memb = data
926 d = evalwrapped(context, mapping, darg)
926 d = evalwrapped(context, mapping, darg)
927 if isinstance(d, mappable):
927 if isinstance(d, mappable):
928 lm = context.overlaymap(mapping, d.tomap(context))
928 lm = context.overlaymap(mapping, d.tomap(context))
929 return runsymbol(context, lm, memb)
929 return runsymbol(context, lm, memb)
930 try:
930 try:
931 return d.getmember(context, mapping, memb)
931 return d.getmember(context, mapping, memb)
932 except error.ParseError as err:
932 except error.ParseError as err:
933 sym = findsymbolicname(darg)
933 sym = findsymbolicname(darg)
934 if not sym:
934 if not sym:
935 raise
935 raise
936 hint = _("keyword '%s' does not support member operation") % sym
936 hint = _("keyword '%s' does not support member operation") % sym
937 raise error.ParseError(bytes(err), hint=hint)
937 raise error.ParseError(bytes(err), hint=hint)
938
938
939 def runnegate(context, mapping, data):
939 def runnegate(context, mapping, data):
940 data = evalinteger(context, mapping, data,
940 data = evalinteger(context, mapping, data,
941 _('negation needs an integer argument'))
941 _('negation needs an integer argument'))
942 return -data
942 return -data
943
943
944 def runarithmetic(context, mapping, data):
944 def runarithmetic(context, mapping, data):
945 func, left, right = data
945 func, left, right = data
946 left = evalinteger(context, mapping, left,
946 left = evalinteger(context, mapping, left,
947 _('arithmetic only defined on integers'))
947 _('arithmetic only defined on integers'))
948 right = evalinteger(context, mapping, right,
948 right = evalinteger(context, mapping, right,
949 _('arithmetic only defined on integers'))
949 _('arithmetic only defined on integers'))
950 try:
950 try:
951 return func(left, right)
951 return func(left, right)
952 except ZeroDivisionError:
952 except ZeroDivisionError:
953 raise error.Abort(_('division by zero is not defined'))
953 raise error.Abort(_('division by zero is not defined'))
954
954
955 def joinitems(itemiter, sep):
955 def joinitems(itemiter, sep):
956 """Join items with the separator; Returns generator of bytes"""
956 """Join items with the separator; Returns generator of bytes"""
957 first = True
957 first = True
958 for x in itemiter:
958 for x in itemiter:
959 if first:
959 if first:
960 first = False
960 first = False
961 elif sep:
961 elif sep:
962 yield sep
962 yield sep
963 yield x
963 yield x
General Comments 0
You need to be logged in to leave comments. Login now