##// END OF EJS Templates
templater: process mapping dict by resource callables...
Yuya Nishihara -
r36998:036e4483 default
parent child Browse files
Show More
@@ -1,563 +1,566 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'))
98 ... files(ui, fm.nested(b'files'))
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 collections
110 import collections
111 import contextlib
111 import contextlib
112 import itertools
112 import itertools
113 import os
113 import os
114
114
115 from .i18n import _
115 from .i18n import _
116 from .node import (
116 from .node import (
117 hex,
117 hex,
118 short,
118 short,
119 )
119 )
120
120
121 from . import (
121 from . import (
122 error,
122 error,
123 pycompat,
123 pycompat,
124 templatefilters,
124 templatefilters,
125 templatekw,
125 templatekw,
126 templater,
126 templater,
127 templateutil,
127 templateutil,
128 util,
128 util,
129 )
129 )
130 from .utils import dateutil
130 from .utils import dateutil
131
131
132 pickle = util.pickle
132 pickle = util.pickle
133
133
134 class _nullconverter(object):
134 class _nullconverter(object):
135 '''convert non-primitive data types to be processed by formatter'''
135 '''convert non-primitive data types to be processed by formatter'''
136
136
137 # set to True if context object should be stored as item
137 # set to True if context object should be stored as item
138 storecontext = False
138 storecontext = False
139
139
140 @staticmethod
140 @staticmethod
141 def formatdate(date, fmt):
141 def formatdate(date, fmt):
142 '''convert date tuple to appropriate format'''
142 '''convert date tuple to appropriate format'''
143 return date
143 return date
144 @staticmethod
144 @staticmethod
145 def formatdict(data, key, value, fmt, sep):
145 def formatdict(data, key, value, fmt, sep):
146 '''convert dict or key-value pairs to appropriate dict format'''
146 '''convert dict or key-value pairs to appropriate dict format'''
147 # use plain dict instead of util.sortdict so that data can be
147 # use plain dict instead of util.sortdict so that data can be
148 # serialized as a builtin dict in pickle output
148 # serialized as a builtin dict in pickle output
149 return dict(data)
149 return dict(data)
150 @staticmethod
150 @staticmethod
151 def formatlist(data, name, fmt, sep):
151 def formatlist(data, name, fmt, sep):
152 '''convert iterable to appropriate list format'''
152 '''convert iterable to appropriate list format'''
153 return list(data)
153 return list(data)
154
154
155 class baseformatter(object):
155 class baseformatter(object):
156 def __init__(self, ui, topic, opts, converter):
156 def __init__(self, ui, topic, opts, converter):
157 self._ui = ui
157 self._ui = ui
158 self._topic = topic
158 self._topic = topic
159 self._style = opts.get("style")
159 self._style = opts.get("style")
160 self._template = opts.get("template")
160 self._template = opts.get("template")
161 self._converter = converter
161 self._converter = converter
162 self._item = None
162 self._item = None
163 # function to convert node to string suitable for this output
163 # function to convert node to string suitable for this output
164 self.hexfunc = hex
164 self.hexfunc = hex
165 def __enter__(self):
165 def __enter__(self):
166 return self
166 return self
167 def __exit__(self, exctype, excvalue, traceback):
167 def __exit__(self, exctype, excvalue, traceback):
168 if exctype is None:
168 if exctype is None:
169 self.end()
169 self.end()
170 def _showitem(self):
170 def _showitem(self):
171 '''show a formatted item once all data is collected'''
171 '''show a formatted item once all data is collected'''
172 def startitem(self):
172 def startitem(self):
173 '''begin an item in the format list'''
173 '''begin an item in the format list'''
174 if self._item is not None:
174 if self._item is not None:
175 self._showitem()
175 self._showitem()
176 self._item = {}
176 self._item = {}
177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
178 '''convert date tuple to appropriate format'''
178 '''convert date tuple to appropriate format'''
179 return self._converter.formatdate(date, fmt)
179 return self._converter.formatdate(date, fmt)
180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
181 '''convert dict or key-value pairs to appropriate dict format'''
181 '''convert dict or key-value pairs to appropriate dict format'''
182 return self._converter.formatdict(data, key, value, fmt, sep)
182 return self._converter.formatdict(data, key, value, fmt, sep)
183 def formatlist(self, data, name, fmt=None, sep=' '):
183 def formatlist(self, data, name, fmt=None, sep=' '):
184 '''convert iterable to appropriate list format'''
184 '''convert iterable to appropriate list format'''
185 # name is mandatory argument for now, but it could be optional if
185 # name is mandatory argument for now, but it could be optional if
186 # we have default template keyword, e.g. {item}
186 # we have default template keyword, e.g. {item}
187 return self._converter.formatlist(data, name, fmt, sep)
187 return self._converter.formatlist(data, name, fmt, sep)
188 def context(self, **ctxs):
188 def context(self, **ctxs):
189 '''insert context objects to be used to render template keywords'''
189 '''insert context objects to be used to render template keywords'''
190 ctxs = pycompat.byteskwargs(ctxs)
190 ctxs = pycompat.byteskwargs(ctxs)
191 assert all(k == 'ctx' for k in ctxs)
191 assert all(k == 'ctx' for k in ctxs)
192 if self._converter.storecontext:
192 if self._converter.storecontext:
193 self._item.update(ctxs)
193 self._item.update(ctxs)
194 def data(self, **data):
194 def data(self, **data):
195 '''insert data into item that's not shown in default output'''
195 '''insert data into item that's not shown in default output'''
196 data = pycompat.byteskwargs(data)
196 data = pycompat.byteskwargs(data)
197 self._item.update(data)
197 self._item.update(data)
198 def write(self, fields, deftext, *fielddata, **opts):
198 def write(self, fields, deftext, *fielddata, **opts):
199 '''do default text output while assigning data to item'''
199 '''do default text output while assigning data to item'''
200 fieldkeys = fields.split()
200 fieldkeys = fields.split()
201 assert len(fieldkeys) == len(fielddata)
201 assert len(fieldkeys) == len(fielddata)
202 self._item.update(zip(fieldkeys, fielddata))
202 self._item.update(zip(fieldkeys, fielddata))
203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
204 '''do conditional write (primarily for plain formatter)'''
204 '''do conditional write (primarily for plain formatter)'''
205 fieldkeys = fields.split()
205 fieldkeys = fields.split()
206 assert len(fieldkeys) == len(fielddata)
206 assert len(fieldkeys) == len(fielddata)
207 self._item.update(zip(fieldkeys, fielddata))
207 self._item.update(zip(fieldkeys, fielddata))
208 def plain(self, text, **opts):
208 def plain(self, text, **opts):
209 '''show raw text for non-templated mode'''
209 '''show raw text for non-templated mode'''
210 def isplain(self):
210 def isplain(self):
211 '''check for plain formatter usage'''
211 '''check for plain formatter usage'''
212 return False
212 return False
213 def nested(self, field):
213 def nested(self, field):
214 '''sub formatter to store nested data in the specified field'''
214 '''sub formatter to store nested data in the specified field'''
215 self._item[field] = data = []
215 self._item[field] = data = []
216 return _nestedformatter(self._ui, self._converter, data)
216 return _nestedformatter(self._ui, self._converter, data)
217 def end(self):
217 def end(self):
218 '''end output for the formatter'''
218 '''end output for the formatter'''
219 if self._item is not None:
219 if self._item is not None:
220 self._showitem()
220 self._showitem()
221
221
222 def nullformatter(ui, topic):
222 def nullformatter(ui, topic):
223 '''formatter that prints nothing'''
223 '''formatter that prints nothing'''
224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
225
225
226 class _nestedformatter(baseformatter):
226 class _nestedformatter(baseformatter):
227 '''build sub items and store them in the parent formatter'''
227 '''build sub items and store them in the parent formatter'''
228 def __init__(self, ui, converter, data):
228 def __init__(self, ui, converter, data):
229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
230 self._data = data
230 self._data = data
231 def _showitem(self):
231 def _showitem(self):
232 self._data.append(self._item)
232 self._data.append(self._item)
233
233
234 def _iteritems(data):
234 def _iteritems(data):
235 '''iterate key-value pairs in stable order'''
235 '''iterate key-value pairs in stable order'''
236 if isinstance(data, dict):
236 if isinstance(data, dict):
237 return sorted(data.iteritems())
237 return sorted(data.iteritems())
238 return data
238 return data
239
239
240 class _plainconverter(object):
240 class _plainconverter(object):
241 '''convert non-primitive data types to text'''
241 '''convert non-primitive data types to text'''
242
242
243 storecontext = False
243 storecontext = False
244
244
245 @staticmethod
245 @staticmethod
246 def formatdate(date, fmt):
246 def formatdate(date, fmt):
247 '''stringify date tuple in the given format'''
247 '''stringify date tuple in the given format'''
248 return dateutil.datestr(date, fmt)
248 return dateutil.datestr(date, fmt)
249 @staticmethod
249 @staticmethod
250 def formatdict(data, key, value, fmt, sep):
250 def formatdict(data, key, value, fmt, sep):
251 '''stringify key-value pairs separated by sep'''
251 '''stringify key-value pairs separated by sep'''
252 prefmt = pycompat.identity
252 prefmt = pycompat.identity
253 if fmt is None:
253 if fmt is None:
254 fmt = '%s=%s'
254 fmt = '%s=%s'
255 prefmt = pycompat.bytestr
255 prefmt = pycompat.bytestr
256 return sep.join(fmt % (prefmt(k), prefmt(v))
256 return sep.join(fmt % (prefmt(k), prefmt(v))
257 for k, v in _iteritems(data))
257 for k, v in _iteritems(data))
258 @staticmethod
258 @staticmethod
259 def formatlist(data, name, fmt, sep):
259 def formatlist(data, name, fmt, sep):
260 '''stringify iterable separated by sep'''
260 '''stringify iterable separated by sep'''
261 prefmt = pycompat.identity
261 prefmt = pycompat.identity
262 if fmt is None:
262 if fmt is None:
263 fmt = '%s'
263 fmt = '%s'
264 prefmt = pycompat.bytestr
264 prefmt = pycompat.bytestr
265 return sep.join(fmt % prefmt(e) for e in data)
265 return sep.join(fmt % prefmt(e) for e in data)
266
266
267 class plainformatter(baseformatter):
267 class plainformatter(baseformatter):
268 '''the default text output scheme'''
268 '''the default text output scheme'''
269 def __init__(self, ui, out, topic, opts):
269 def __init__(self, ui, out, topic, opts):
270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
271 if ui.debugflag:
271 if ui.debugflag:
272 self.hexfunc = hex
272 self.hexfunc = hex
273 else:
273 else:
274 self.hexfunc = short
274 self.hexfunc = short
275 if ui is out:
275 if ui is out:
276 self._write = ui.write
276 self._write = ui.write
277 else:
277 else:
278 self._write = lambda s, **opts: out.write(s)
278 self._write = lambda s, **opts: out.write(s)
279 def startitem(self):
279 def startitem(self):
280 pass
280 pass
281 def data(self, **data):
281 def data(self, **data):
282 pass
282 pass
283 def write(self, fields, deftext, *fielddata, **opts):
283 def write(self, fields, deftext, *fielddata, **opts):
284 self._write(deftext % fielddata, **opts)
284 self._write(deftext % fielddata, **opts)
285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
286 '''do conditional write'''
286 '''do conditional write'''
287 if cond:
287 if cond:
288 self._write(deftext % fielddata, **opts)
288 self._write(deftext % fielddata, **opts)
289 def plain(self, text, **opts):
289 def plain(self, text, **opts):
290 self._write(text, **opts)
290 self._write(text, **opts)
291 def isplain(self):
291 def isplain(self):
292 return True
292 return True
293 def nested(self, field):
293 def nested(self, field):
294 # nested data will be directly written to ui
294 # nested data will be directly written to ui
295 return self
295 return self
296 def end(self):
296 def end(self):
297 pass
297 pass
298
298
299 class debugformatter(baseformatter):
299 class debugformatter(baseformatter):
300 def __init__(self, ui, out, topic, opts):
300 def __init__(self, ui, out, topic, opts):
301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
302 self._out = out
302 self._out = out
303 self._out.write("%s = [\n" % self._topic)
303 self._out.write("%s = [\n" % self._topic)
304 def _showitem(self):
304 def _showitem(self):
305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
306 def end(self):
306 def end(self):
307 baseformatter.end(self)
307 baseformatter.end(self)
308 self._out.write("]\n")
308 self._out.write("]\n")
309
309
310 class pickleformatter(baseformatter):
310 class pickleformatter(baseformatter):
311 def __init__(self, ui, out, topic, opts):
311 def __init__(self, ui, out, topic, opts):
312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
313 self._out = out
313 self._out = out
314 self._data = []
314 self._data = []
315 def _showitem(self):
315 def _showitem(self):
316 self._data.append(self._item)
316 self._data.append(self._item)
317 def end(self):
317 def end(self):
318 baseformatter.end(self)
318 baseformatter.end(self)
319 self._out.write(pickle.dumps(self._data))
319 self._out.write(pickle.dumps(self._data))
320
320
321 class jsonformatter(baseformatter):
321 class jsonformatter(baseformatter):
322 def __init__(self, ui, out, topic, opts):
322 def __init__(self, ui, out, topic, opts):
323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
324 self._out = out
324 self._out = out
325 self._out.write("[")
325 self._out.write("[")
326 self._first = True
326 self._first = True
327 def _showitem(self):
327 def _showitem(self):
328 if self._first:
328 if self._first:
329 self._first = False
329 self._first = False
330 else:
330 else:
331 self._out.write(",")
331 self._out.write(",")
332
332
333 self._out.write("\n {\n")
333 self._out.write("\n {\n")
334 first = True
334 first = True
335 for k, v in sorted(self._item.items()):
335 for k, v in sorted(self._item.items()):
336 if first:
336 if first:
337 first = False
337 first = False
338 else:
338 else:
339 self._out.write(",\n")
339 self._out.write(",\n")
340 u = templatefilters.json(v, paranoid=False)
340 u = templatefilters.json(v, paranoid=False)
341 self._out.write(' "%s": %s' % (k, u))
341 self._out.write(' "%s": %s' % (k, u))
342 self._out.write("\n }")
342 self._out.write("\n }")
343 def end(self):
343 def end(self):
344 baseformatter.end(self)
344 baseformatter.end(self)
345 self._out.write("\n]\n")
345 self._out.write("\n]\n")
346
346
347 class _templateconverter(object):
347 class _templateconverter(object):
348 '''convert non-primitive data types to be processed by templater'''
348 '''convert non-primitive data types to be processed by templater'''
349
349
350 storecontext = True
350 storecontext = True
351
351
352 @staticmethod
352 @staticmethod
353 def formatdate(date, fmt):
353 def formatdate(date, fmt):
354 '''return date tuple'''
354 '''return date tuple'''
355 return date
355 return date
356 @staticmethod
356 @staticmethod
357 def formatdict(data, key, value, fmt, sep):
357 def formatdict(data, key, value, fmt, sep):
358 '''build object that can be evaluated as either plain string or dict'''
358 '''build object that can be evaluated as either plain string or dict'''
359 data = util.sortdict(_iteritems(data))
359 data = util.sortdict(_iteritems(data))
360 def f():
360 def f():
361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
363 gen=f)
363 gen=f)
364 @staticmethod
364 @staticmethod
365 def formatlist(data, name, fmt, sep):
365 def formatlist(data, name, fmt, sep):
366 '''build object that can be evaluated as either plain string or list'''
366 '''build object that can be evaluated as either plain string or list'''
367 data = list(data)
367 data = list(data)
368 def f():
368 def f():
369 yield _plainconverter.formatlist(data, name, fmt, sep)
369 yield _plainconverter.formatlist(data, name, fmt, sep)
370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
371
371
372 class templateformatter(baseformatter):
372 class templateformatter(baseformatter):
373 def __init__(self, ui, out, topic, opts):
373 def __init__(self, ui, out, topic, opts):
374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
375 self._out = out
375 self._out = out
376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
377 self._tref = spec.ref
377 self._tref = spec.ref
378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
379 resources=templateresources(ui),
379 resources=templateresources(ui),
380 cache=templatekw.defaulttempl)
380 cache=templatekw.defaulttempl)
381 self._parts = templatepartsmap(spec, self._t,
381 self._parts = templatepartsmap(spec, self._t,
382 ['docheader', 'docfooter', 'separator'])
382 ['docheader', 'docfooter', 'separator'])
383 self._counter = itertools.count()
383 self._counter = itertools.count()
384 self._renderitem('docheader', {})
384 self._renderitem('docheader', {})
385
385
386 def _showitem(self):
386 def _showitem(self):
387 item = self._item.copy()
387 item = self._item.copy()
388 item['index'] = index = next(self._counter)
388 item['index'] = index = next(self._counter)
389 if index > 0:
389 if index > 0:
390 self._renderitem('separator', {})
390 self._renderitem('separator', {})
391 self._renderitem(self._tref, item)
391 self._renderitem(self._tref, item)
392
392
393 def _renderitem(self, part, item):
393 def _renderitem(self, part, item):
394 if part not in self._parts:
394 if part not in self._parts:
395 return
395 return
396 ref = self._parts[part]
396 ref = self._parts[part]
397
397
398 # TODO: add support for filectx
398 # TODO: add support for filectx
399 props = {}
399 props = {}
400 # explicitly-defined fields precede templatekw
400 # explicitly-defined fields precede templatekw
401 props.update(item)
401 props.update(item)
402 if 'ctx' in item:
402 if 'ctx' in item:
403 # but template resources must be always available
403 # but template resources must be always available
404 props['repo'] = props['ctx'].repo()
404 props['repo'] = props['ctx'].repo()
405 props['revcache'] = {}
405 props['revcache'] = {}
406 props = pycompat.strkwargs(props)
406 props = pycompat.strkwargs(props)
407 g = self._t(ref, **props)
407 g = self._t(ref, **props)
408 self._out.write(templateutil.stringify(g))
408 self._out.write(templateutil.stringify(g))
409
409
410 def end(self):
410 def end(self):
411 baseformatter.end(self)
411 baseformatter.end(self)
412 self._renderitem('docfooter', {})
412 self._renderitem('docfooter', {})
413
413
414 templatespec = collections.namedtuple(r'templatespec',
414 templatespec = collections.namedtuple(r'templatespec',
415 r'ref tmpl mapfile')
415 r'ref tmpl mapfile')
416
416
417 def lookuptemplate(ui, topic, tmpl):
417 def lookuptemplate(ui, topic, tmpl):
418 """Find the template matching the given -T/--template spec 'tmpl'
418 """Find the template matching the given -T/--template spec 'tmpl'
419
419
420 'tmpl' can be any of the following:
420 'tmpl' can be any of the following:
421
421
422 - a literal template (e.g. '{rev}')
422 - a literal template (e.g. '{rev}')
423 - a map-file name or path (e.g. 'changelog')
423 - a map-file name or path (e.g. 'changelog')
424 - a reference to [templates] in config file
424 - a reference to [templates] in config file
425 - a path to raw template file
425 - a path to raw template file
426
426
427 A map file defines a stand-alone template environment. If a map file
427 A map file defines a stand-alone template environment. If a map file
428 selected, all templates defined in the file will be loaded, and the
428 selected, all templates defined in the file will be loaded, and the
429 template matching the given topic will be rendered. Aliases won't be
429 template matching the given topic will be rendered. Aliases won't be
430 loaded from user config, but from the map file.
430 loaded from user config, but from the map file.
431
431
432 If no map file selected, all templates in [templates] section will be
432 If no map file selected, all templates in [templates] section will be
433 available as well as aliases in [templatealias].
433 available as well as aliases in [templatealias].
434 """
434 """
435
435
436 # looks like a literal template?
436 # looks like a literal template?
437 if '{' in tmpl:
437 if '{' in tmpl:
438 return templatespec('', tmpl, None)
438 return templatespec('', tmpl, None)
439
439
440 # perhaps a stock style?
440 # perhaps a stock style?
441 if not os.path.split(tmpl)[0]:
441 if not os.path.split(tmpl)[0]:
442 mapname = (templater.templatepath('map-cmdline.' + tmpl)
442 mapname = (templater.templatepath('map-cmdline.' + tmpl)
443 or templater.templatepath(tmpl))
443 or templater.templatepath(tmpl))
444 if mapname and os.path.isfile(mapname):
444 if mapname and os.path.isfile(mapname):
445 return templatespec(topic, None, mapname)
445 return templatespec(topic, None, mapname)
446
446
447 # perhaps it's a reference to [templates]
447 # perhaps it's a reference to [templates]
448 if ui.config('templates', tmpl):
448 if ui.config('templates', tmpl):
449 return templatespec(tmpl, None, None)
449 return templatespec(tmpl, None, None)
450
450
451 if tmpl == 'list':
451 if tmpl == 'list':
452 ui.write(_("available styles: %s\n") % templater.stylelist())
452 ui.write(_("available styles: %s\n") % templater.stylelist())
453 raise error.Abort(_("specify a template"))
453 raise error.Abort(_("specify a template"))
454
454
455 # perhaps it's a path to a map or a template
455 # perhaps it's a path to a map or a template
456 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
456 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
457 # is it a mapfile for a style?
457 # is it a mapfile for a style?
458 if os.path.basename(tmpl).startswith("map-"):
458 if os.path.basename(tmpl).startswith("map-"):
459 return templatespec(topic, None, os.path.realpath(tmpl))
459 return templatespec(topic, None, os.path.realpath(tmpl))
460 with util.posixfile(tmpl, 'rb') as f:
460 with util.posixfile(tmpl, 'rb') as f:
461 tmpl = f.read()
461 tmpl = f.read()
462 return templatespec('', tmpl, None)
462 return templatespec('', tmpl, None)
463
463
464 # constant string?
464 # constant string?
465 return templatespec('', tmpl, None)
465 return templatespec('', tmpl, None)
466
466
467 def templatepartsmap(spec, t, partnames):
467 def templatepartsmap(spec, t, partnames):
468 """Create a mapping of {part: ref}"""
468 """Create a mapping of {part: ref}"""
469 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
469 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
470 if spec.mapfile:
470 if spec.mapfile:
471 partsmap.update((p, p) for p in partnames if p in t)
471 partsmap.update((p, p) for p in partnames if p in t)
472 elif spec.ref:
472 elif spec.ref:
473 for part in partnames:
473 for part in partnames:
474 ref = '%s:%s' % (spec.ref, part) # select config sub-section
474 ref = '%s:%s' % (spec.ref, part) # select config sub-section
475 if ref in t:
475 if ref in t:
476 partsmap[part] = ref
476 partsmap[part] = ref
477 return partsmap
477 return partsmap
478
478
479 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
479 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
480 """Create a templater from either a literal template or loading from
480 """Create a templater from either a literal template or loading from
481 a map file"""
481 a map file"""
482 assert not (spec.tmpl and spec.mapfile)
482 assert not (spec.tmpl and spec.mapfile)
483 if spec.mapfile:
483 if spec.mapfile:
484 frommapfile = templater.templater.frommapfile
484 frommapfile = templater.templater.frommapfile
485 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
485 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
486 cache=cache)
486 cache=cache)
487 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
487 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
488 cache=cache)
488 cache=cache)
489
489
490 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
490 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
491 """Create a templater from a string template 'tmpl'"""
491 """Create a templater from a string template 'tmpl'"""
492 aliases = ui.configitems('templatealias')
492 aliases = ui.configitems('templatealias')
493 t = templater.templater(defaults=defaults, resources=resources,
493 t = templater.templater(defaults=defaults, resources=resources,
494 cache=cache, aliases=aliases)
494 cache=cache, aliases=aliases)
495 t.cache.update((k, templater.unquotestring(v))
495 t.cache.update((k, templater.unquotestring(v))
496 for k, v in ui.configitems('templates'))
496 for k, v in ui.configitems('templates'))
497 if tmpl:
497 if tmpl:
498 t.cache[''] = tmpl
498 t.cache[''] = tmpl
499 return t
499 return t
500
500
501 def templateresources(ui, repo=None):
501 def templateresources(ui, repo=None):
502 """Create a dict of template resources designed for the default templatekw
502 """Create a dict of template resources designed for the default templatekw
503 and function"""
503 and function"""
504 resmap = {
504 resmap = {
505 'cache': {}, # for templatekw/funcs to store reusable data
505 'cache': {}, # for templatekw/funcs to store reusable data
506 'repo': repo,
506 'repo': repo,
507 'ui': ui,
507 'ui': ui,
508 }
508 }
509
509
510 def getsome(context, mapping, key):
510 def getsome(context, mapping, key):
511 v = mapping.get(key)
512 if v is not None:
513 return v
511 return resmap.get(key)
514 return resmap.get(key)
512
515
513 return {
516 return {
514 'cache': getsome,
517 'cache': getsome,
515 'ctx': getsome,
518 'ctx': getsome,
516 'repo': getsome,
519 'repo': getsome,
517 'revcache': getsome, # per-ctx cache; set later
520 'revcache': getsome, # per-ctx cache; set later
518 'ui': getsome,
521 'ui': getsome,
519 }
522 }
520
523
521 def formatter(ui, out, topic, opts):
524 def formatter(ui, out, topic, opts):
522 template = opts.get("template", "")
525 template = opts.get("template", "")
523 if template == "json":
526 if template == "json":
524 return jsonformatter(ui, out, topic, opts)
527 return jsonformatter(ui, out, topic, opts)
525 elif template == "pickle":
528 elif template == "pickle":
526 return pickleformatter(ui, out, topic, opts)
529 return pickleformatter(ui, out, topic, opts)
527 elif template == "debug":
530 elif template == "debug":
528 return debugformatter(ui, out, topic, opts)
531 return debugformatter(ui, out, topic, opts)
529 elif template != "":
532 elif template != "":
530 return templateformatter(ui, out, topic, opts)
533 return templateformatter(ui, out, topic, opts)
531 # developer config: ui.formatdebug
534 # developer config: ui.formatdebug
532 elif ui.configbool('ui', 'formatdebug'):
535 elif ui.configbool('ui', 'formatdebug'):
533 return debugformatter(ui, out, topic, opts)
536 return debugformatter(ui, out, topic, opts)
534 # deprecated config: ui.formatjson
537 # deprecated config: ui.formatjson
535 elif ui.configbool('ui', 'formatjson'):
538 elif ui.configbool('ui', 'formatjson'):
536 return jsonformatter(ui, out, topic, opts)
539 return jsonformatter(ui, out, topic, opts)
537 return plainformatter(ui, out, topic, opts)
540 return plainformatter(ui, out, topic, opts)
538
541
539 @contextlib.contextmanager
542 @contextlib.contextmanager
540 def openformatter(ui, filename, topic, opts):
543 def openformatter(ui, filename, topic, opts):
541 """Create a formatter that writes outputs to the specified file
544 """Create a formatter that writes outputs to the specified file
542
545
543 Must be invoked using the 'with' statement.
546 Must be invoked using the 'with' statement.
544 """
547 """
545 with util.posixfile(filename, 'wb') as out:
548 with util.posixfile(filename, 'wb') as out:
546 with formatter(ui, out, topic, opts) as fm:
549 with formatter(ui, out, topic, opts) as fm:
547 yield fm
550 yield fm
548
551
549 @contextlib.contextmanager
552 @contextlib.contextmanager
550 def _neverending(fm):
553 def _neverending(fm):
551 yield fm
554 yield fm
552
555
553 def maybereopen(fm, filename, opts):
556 def maybereopen(fm, filename, opts):
554 """Create a formatter backed by file if filename specified, else return
557 """Create a formatter backed by file if filename specified, else return
555 the given formatter
558 the given formatter
556
559
557 Must be invoked using the 'with' statement. This will never call fm.end()
560 Must be invoked using the 'with' statement. This will never call fm.end()
558 of the given formatter.
561 of the given formatter.
559 """
562 """
560 if filename:
563 if filename:
561 return openformatter(fm._ui, filename, fm._topic, opts)
564 return openformatter(fm._ui, filename, fm._topic, opts)
562 else:
565 else:
563 return _neverending(fm)
566 return _neverending(fm)
@@ -1,800 +1,797 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 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 config,
14 config,
15 encoding,
15 encoding,
16 error,
16 error,
17 parser,
17 parser,
18 pycompat,
18 pycompat,
19 templatefilters,
19 templatefilters,
20 templatefuncs,
20 templatefuncs,
21 templateutil,
21 templateutil,
22 util,
22 util,
23 )
23 )
24
24
25 # template parsing
25 # template parsing
26
26
27 elements = {
27 elements = {
28 # token-type: binding-strength, primary, prefix, infix, suffix
28 # token-type: binding-strength, primary, prefix, infix, suffix
29 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
29 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
30 ".": (18, None, None, (".", 18), None),
30 ".": (18, None, None, (".", 18), None),
31 "%": (15, None, None, ("%", 15), None),
31 "%": (15, None, None, ("%", 15), None),
32 "|": (15, None, None, ("|", 15), None),
32 "|": (15, None, None, ("|", 15), None),
33 "*": (5, None, None, ("*", 5), None),
33 "*": (5, None, None, ("*", 5), None),
34 "/": (5, None, None, ("/", 5), None),
34 "/": (5, None, None, ("/", 5), None),
35 "+": (4, None, None, ("+", 4), None),
35 "+": (4, None, None, ("+", 4), None),
36 "-": (4, None, ("negate", 19), ("-", 4), None),
36 "-": (4, None, ("negate", 19), ("-", 4), None),
37 "=": (3, None, None, ("keyvalue", 3), None),
37 "=": (3, None, None, ("keyvalue", 3), None),
38 ",": (2, None, None, ("list", 2), None),
38 ",": (2, None, None, ("list", 2), None),
39 ")": (0, None, None, None, None),
39 ")": (0, None, None, None, None),
40 "integer": (0, "integer", None, None, None),
40 "integer": (0, "integer", None, None, None),
41 "symbol": (0, "symbol", None, None, None),
41 "symbol": (0, "symbol", None, None, None),
42 "string": (0, "string", None, None, None),
42 "string": (0, "string", None, None, None),
43 "template": (0, "template", None, None, None),
43 "template": (0, "template", None, None, None),
44 "end": (0, None, None, None, None),
44 "end": (0, None, None, None, None),
45 }
45 }
46
46
47 def tokenize(program, start, end, term=None):
47 def tokenize(program, start, end, term=None):
48 """Parse a template expression into a stream of tokens, which must end
48 """Parse a template expression into a stream of tokens, which must end
49 with term if specified"""
49 with term if specified"""
50 pos = start
50 pos = start
51 program = pycompat.bytestr(program)
51 program = pycompat.bytestr(program)
52 while pos < end:
52 while pos < end:
53 c = program[pos]
53 c = program[pos]
54 if c.isspace(): # skip inter-token whitespace
54 if c.isspace(): # skip inter-token whitespace
55 pass
55 pass
56 elif c in "(=,).%|+-*/": # handle simple operators
56 elif c in "(=,).%|+-*/": # handle simple operators
57 yield (c, None, pos)
57 yield (c, None, pos)
58 elif c in '"\'': # handle quoted templates
58 elif c in '"\'': # handle quoted templates
59 s = pos + 1
59 s = pos + 1
60 data, pos = _parsetemplate(program, s, end, c)
60 data, pos = _parsetemplate(program, s, end, c)
61 yield ('template', data, s)
61 yield ('template', data, s)
62 pos -= 1
62 pos -= 1
63 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
63 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
64 # handle quoted strings
64 # handle quoted strings
65 c = program[pos + 1]
65 c = program[pos + 1]
66 s = pos = pos + 2
66 s = pos = pos + 2
67 while pos < end: # find closing quote
67 while pos < end: # find closing quote
68 d = program[pos]
68 d = program[pos]
69 if d == '\\': # skip over escaped characters
69 if d == '\\': # skip over escaped characters
70 pos += 2
70 pos += 2
71 continue
71 continue
72 if d == c:
72 if d == c:
73 yield ('string', program[s:pos], s)
73 yield ('string', program[s:pos], s)
74 break
74 break
75 pos += 1
75 pos += 1
76 else:
76 else:
77 raise error.ParseError(_("unterminated string"), s)
77 raise error.ParseError(_("unterminated string"), s)
78 elif c.isdigit():
78 elif c.isdigit():
79 s = pos
79 s = pos
80 while pos < end:
80 while pos < end:
81 d = program[pos]
81 d = program[pos]
82 if not d.isdigit():
82 if not d.isdigit():
83 break
83 break
84 pos += 1
84 pos += 1
85 yield ('integer', program[s:pos], s)
85 yield ('integer', program[s:pos], s)
86 pos -= 1
86 pos -= 1
87 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
87 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
88 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
88 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
89 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
89 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
90 # where some of nested templates were preprocessed as strings and
90 # where some of nested templates were preprocessed as strings and
91 # then compiled. therefore, \"...\" was allowed. (issue4733)
91 # then compiled. therefore, \"...\" was allowed. (issue4733)
92 #
92 #
93 # processing flow of _evalifliteral() at 5ab28a2e9962:
93 # processing flow of _evalifliteral() at 5ab28a2e9962:
94 # outer template string -> stringify() -> compiletemplate()
94 # outer template string -> stringify() -> compiletemplate()
95 # ------------------------ ------------ ------------------
95 # ------------------------ ------------ ------------------
96 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
96 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
97 # ~~~~~~~~
97 # ~~~~~~~~
98 # escaped quoted string
98 # escaped quoted string
99 if c == 'r':
99 if c == 'r':
100 pos += 1
100 pos += 1
101 token = 'string'
101 token = 'string'
102 else:
102 else:
103 token = 'template'
103 token = 'template'
104 quote = program[pos:pos + 2]
104 quote = program[pos:pos + 2]
105 s = pos = pos + 2
105 s = pos = pos + 2
106 while pos < end: # find closing escaped quote
106 while pos < end: # find closing escaped quote
107 if program.startswith('\\\\\\', pos, end):
107 if program.startswith('\\\\\\', pos, end):
108 pos += 4 # skip over double escaped characters
108 pos += 4 # skip over double escaped characters
109 continue
109 continue
110 if program.startswith(quote, pos, end):
110 if program.startswith(quote, pos, end):
111 # interpret as if it were a part of an outer string
111 # interpret as if it were a part of an outer string
112 data = parser.unescapestr(program[s:pos])
112 data = parser.unescapestr(program[s:pos])
113 if token == 'template':
113 if token == 'template':
114 data = _parsetemplate(data, 0, len(data))[0]
114 data = _parsetemplate(data, 0, len(data))[0]
115 yield (token, data, s)
115 yield (token, data, s)
116 pos += 1
116 pos += 1
117 break
117 break
118 pos += 1
118 pos += 1
119 else:
119 else:
120 raise error.ParseError(_("unterminated string"), s)
120 raise error.ParseError(_("unterminated string"), s)
121 elif c.isalnum() or c in '_':
121 elif c.isalnum() or c in '_':
122 s = pos
122 s = pos
123 pos += 1
123 pos += 1
124 while pos < end: # find end of symbol
124 while pos < end: # find end of symbol
125 d = program[pos]
125 d = program[pos]
126 if not (d.isalnum() or d == "_"):
126 if not (d.isalnum() or d == "_"):
127 break
127 break
128 pos += 1
128 pos += 1
129 sym = program[s:pos]
129 sym = program[s:pos]
130 yield ('symbol', sym, s)
130 yield ('symbol', sym, s)
131 pos -= 1
131 pos -= 1
132 elif c == term:
132 elif c == term:
133 yield ('end', None, pos)
133 yield ('end', None, pos)
134 return
134 return
135 else:
135 else:
136 raise error.ParseError(_("syntax error"), pos)
136 raise error.ParseError(_("syntax error"), pos)
137 pos += 1
137 pos += 1
138 if term:
138 if term:
139 raise error.ParseError(_("unterminated template expansion"), start)
139 raise error.ParseError(_("unterminated template expansion"), start)
140 yield ('end', None, pos)
140 yield ('end', None, pos)
141
141
142 def _parsetemplate(tmpl, start, stop, quote=''):
142 def _parsetemplate(tmpl, start, stop, quote=''):
143 r"""
143 r"""
144 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
144 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
145 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
145 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
146 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
146 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
147 ([('string', 'foo'), ('symbol', 'bar')], 9)
147 ([('string', 'foo'), ('symbol', 'bar')], 9)
148 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
148 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
149 ([('string', 'foo')], 4)
149 ([('string', 'foo')], 4)
150 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
150 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
151 ([('string', 'foo"'), ('string', 'bar')], 9)
151 ([('string', 'foo"'), ('string', 'bar')], 9)
152 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
152 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
153 ([('string', 'foo\\')], 6)
153 ([('string', 'foo\\')], 6)
154 """
154 """
155 parsed = []
155 parsed = []
156 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
156 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
157 if typ == 'string':
157 if typ == 'string':
158 parsed.append((typ, val))
158 parsed.append((typ, val))
159 elif typ == 'template':
159 elif typ == 'template':
160 parsed.append(val)
160 parsed.append(val)
161 elif typ == 'end':
161 elif typ == 'end':
162 return parsed, pos
162 return parsed, pos
163 else:
163 else:
164 raise error.ProgrammingError('unexpected type: %s' % typ)
164 raise error.ProgrammingError('unexpected type: %s' % typ)
165 raise error.ProgrammingError('unterminated scanning of template')
165 raise error.ProgrammingError('unterminated scanning of template')
166
166
167 def scantemplate(tmpl, raw=False):
167 def scantemplate(tmpl, raw=False):
168 r"""Scan (type, start, end) positions of outermost elements in template
168 r"""Scan (type, start, end) positions of outermost elements in template
169
169
170 If raw=True, a backslash is not taken as an escape character just like
170 If raw=True, a backslash is not taken as an escape character just like
171 r'' string in Python. Note that this is different from r'' literal in
171 r'' string in Python. Note that this is different from r'' literal in
172 template in that no template fragment can appear in r'', e.g. r'{foo}'
172 template in that no template fragment can appear in r'', e.g. r'{foo}'
173 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
173 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
174 'foo'.
174 'foo'.
175
175
176 >>> list(scantemplate(b'foo{bar}"baz'))
176 >>> list(scantemplate(b'foo{bar}"baz'))
177 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
177 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
178 >>> list(scantemplate(b'outer{"inner"}outer'))
178 >>> list(scantemplate(b'outer{"inner"}outer'))
179 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
179 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
180 >>> list(scantemplate(b'foo\\{escaped}'))
180 >>> list(scantemplate(b'foo\\{escaped}'))
181 [('string', 0, 5), ('string', 5, 13)]
181 [('string', 0, 5), ('string', 5, 13)]
182 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
182 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
183 [('string', 0, 4), ('template', 4, 13)]
183 [('string', 0, 4), ('template', 4, 13)]
184 """
184 """
185 last = None
185 last = None
186 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
186 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
187 if last:
187 if last:
188 yield last + (pos,)
188 yield last + (pos,)
189 if typ == 'end':
189 if typ == 'end':
190 return
190 return
191 else:
191 else:
192 last = (typ, pos)
192 last = (typ, pos)
193 raise error.ProgrammingError('unterminated scanning of template')
193 raise error.ProgrammingError('unterminated scanning of template')
194
194
195 def _scantemplate(tmpl, start, stop, quote='', raw=False):
195 def _scantemplate(tmpl, start, stop, quote='', raw=False):
196 """Parse template string into chunks of strings and template expressions"""
196 """Parse template string into chunks of strings and template expressions"""
197 sepchars = '{' + quote
197 sepchars = '{' + quote
198 unescape = [parser.unescapestr, pycompat.identity][raw]
198 unescape = [parser.unescapestr, pycompat.identity][raw]
199 pos = start
199 pos = start
200 p = parser.parser(elements)
200 p = parser.parser(elements)
201 try:
201 try:
202 while pos < stop:
202 while pos < stop:
203 n = min((tmpl.find(c, pos, stop) for c in sepchars),
203 n = min((tmpl.find(c, pos, stop) for c in sepchars),
204 key=lambda n: (n < 0, n))
204 key=lambda n: (n < 0, n))
205 if n < 0:
205 if n < 0:
206 yield ('string', unescape(tmpl[pos:stop]), pos)
206 yield ('string', unescape(tmpl[pos:stop]), pos)
207 pos = stop
207 pos = stop
208 break
208 break
209 c = tmpl[n:n + 1]
209 c = tmpl[n:n + 1]
210 bs = 0 # count leading backslashes
210 bs = 0 # count leading backslashes
211 if not raw:
211 if not raw:
212 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
212 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
213 if bs % 2 == 1:
213 if bs % 2 == 1:
214 # escaped (e.g. '\{', '\\\{', but not '\\{')
214 # escaped (e.g. '\{', '\\\{', but not '\\{')
215 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
215 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
216 pos = n + 1
216 pos = n + 1
217 continue
217 continue
218 if n > pos:
218 if n > pos:
219 yield ('string', unescape(tmpl[pos:n]), pos)
219 yield ('string', unescape(tmpl[pos:n]), pos)
220 if c == quote:
220 if c == quote:
221 yield ('end', None, n + 1)
221 yield ('end', None, n + 1)
222 return
222 return
223
223
224 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
224 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
225 if not tmpl.startswith('}', pos):
225 if not tmpl.startswith('}', pos):
226 raise error.ParseError(_("invalid token"), pos)
226 raise error.ParseError(_("invalid token"), pos)
227 yield ('template', parseres, n)
227 yield ('template', parseres, n)
228 pos += 1
228 pos += 1
229
229
230 if quote:
230 if quote:
231 raise error.ParseError(_("unterminated string"), start)
231 raise error.ParseError(_("unterminated string"), start)
232 except error.ParseError as inst:
232 except error.ParseError as inst:
233 if len(inst.args) > 1: # has location
233 if len(inst.args) > 1: # has location
234 loc = inst.args[1]
234 loc = inst.args[1]
235 # Offset the caret location by the number of newlines before the
235 # Offset the caret location by the number of newlines before the
236 # location of the error, since we will replace one-char newlines
236 # location of the error, since we will replace one-char newlines
237 # with the two-char literal r'\n'.
237 # with the two-char literal r'\n'.
238 offset = tmpl[:loc].count('\n')
238 offset = tmpl[:loc].count('\n')
239 tmpl = tmpl.replace('\n', br'\n')
239 tmpl = tmpl.replace('\n', br'\n')
240 # We want the caret to point to the place in the template that
240 # We want the caret to point to the place in the template that
241 # failed to parse, but in a hint we get a open paren at the
241 # failed to parse, but in a hint we get a open paren at the
242 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
242 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
243 # to line up the caret with the location of the error.
243 # to line up the caret with the location of the error.
244 inst.hint = (tmpl + '\n'
244 inst.hint = (tmpl + '\n'
245 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
245 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
246 raise
246 raise
247 yield ('end', None, pos)
247 yield ('end', None, pos)
248
248
249 def _unnesttemplatelist(tree):
249 def _unnesttemplatelist(tree):
250 """Expand list of templates to node tuple
250 """Expand list of templates to node tuple
251
251
252 >>> def f(tree):
252 >>> def f(tree):
253 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
253 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
254 >>> f((b'template', []))
254 >>> f((b'template', []))
255 (string '')
255 (string '')
256 >>> f((b'template', [(b'string', b'foo')]))
256 >>> f((b'template', [(b'string', b'foo')]))
257 (string 'foo')
257 (string 'foo')
258 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
258 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
259 (template
259 (template
260 (string 'foo')
260 (string 'foo')
261 (symbol 'rev'))
261 (symbol 'rev'))
262 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
262 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
263 (template
263 (template
264 (symbol 'rev'))
264 (symbol 'rev'))
265 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
265 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
266 (string 'foo')
266 (string 'foo')
267 """
267 """
268 if not isinstance(tree, tuple):
268 if not isinstance(tree, tuple):
269 return tree
269 return tree
270 op = tree[0]
270 op = tree[0]
271 if op != 'template':
271 if op != 'template':
272 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
272 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
273
273
274 assert len(tree) == 2
274 assert len(tree) == 2
275 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
275 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
276 if not xs:
276 if not xs:
277 return ('string', '') # empty template ""
277 return ('string', '') # empty template ""
278 elif len(xs) == 1 and xs[0][0] == 'string':
278 elif len(xs) == 1 and xs[0][0] == 'string':
279 return xs[0] # fast path for string with no template fragment "x"
279 return xs[0] # fast path for string with no template fragment "x"
280 else:
280 else:
281 return (op,) + xs
281 return (op,) + xs
282
282
283 def parse(tmpl):
283 def parse(tmpl):
284 """Parse template string into tree"""
284 """Parse template string into tree"""
285 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
285 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
286 assert pos == len(tmpl), 'unquoted template should be consumed'
286 assert pos == len(tmpl), 'unquoted template should be consumed'
287 return _unnesttemplatelist(('template', parsed))
287 return _unnesttemplatelist(('template', parsed))
288
288
289 def _parseexpr(expr):
289 def _parseexpr(expr):
290 """Parse a template expression into tree
290 """Parse a template expression into tree
291
291
292 >>> _parseexpr(b'"foo"')
292 >>> _parseexpr(b'"foo"')
293 ('string', 'foo')
293 ('string', 'foo')
294 >>> _parseexpr(b'foo(bar)')
294 >>> _parseexpr(b'foo(bar)')
295 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
295 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
296 >>> _parseexpr(b'foo(')
296 >>> _parseexpr(b'foo(')
297 Traceback (most recent call last):
297 Traceback (most recent call last):
298 ...
298 ...
299 ParseError: ('not a prefix: end', 4)
299 ParseError: ('not a prefix: end', 4)
300 >>> _parseexpr(b'"foo" "bar"')
300 >>> _parseexpr(b'"foo" "bar"')
301 Traceback (most recent call last):
301 Traceback (most recent call last):
302 ...
302 ...
303 ParseError: ('invalid token', 7)
303 ParseError: ('invalid token', 7)
304 """
304 """
305 p = parser.parser(elements)
305 p = parser.parser(elements)
306 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
306 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
307 if pos != len(expr):
307 if pos != len(expr):
308 raise error.ParseError(_('invalid token'), pos)
308 raise error.ParseError(_('invalid token'), pos)
309 return _unnesttemplatelist(tree)
309 return _unnesttemplatelist(tree)
310
310
311 def prettyformat(tree):
311 def prettyformat(tree):
312 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
312 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
313
313
314 def compileexp(exp, context, curmethods):
314 def compileexp(exp, context, curmethods):
315 """Compile parsed template tree to (func, data) pair"""
315 """Compile parsed template tree to (func, data) pair"""
316 if not exp:
316 if not exp:
317 raise error.ParseError(_("missing argument"))
317 raise error.ParseError(_("missing argument"))
318 t = exp[0]
318 t = exp[0]
319 if t in curmethods:
319 if t in curmethods:
320 return curmethods[t](exp, context)
320 return curmethods[t](exp, context)
321 raise error.ParseError(_("unknown method '%s'") % t)
321 raise error.ParseError(_("unknown method '%s'") % t)
322
322
323 # template evaluation
323 # template evaluation
324
324
325 def getsymbol(exp):
325 def getsymbol(exp):
326 if exp[0] == 'symbol':
326 if exp[0] == 'symbol':
327 return exp[1]
327 return exp[1]
328 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
328 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
329
329
330 def getlist(x):
330 def getlist(x):
331 if not x:
331 if not x:
332 return []
332 return []
333 if x[0] == 'list':
333 if x[0] == 'list':
334 return getlist(x[1]) + [x[2]]
334 return getlist(x[1]) + [x[2]]
335 return [x]
335 return [x]
336
336
337 def gettemplate(exp, context):
337 def gettemplate(exp, context):
338 """Compile given template tree or load named template from map file;
338 """Compile given template tree or load named template from map file;
339 returns (func, data) pair"""
339 returns (func, data) pair"""
340 if exp[0] in ('template', 'string'):
340 if exp[0] in ('template', 'string'):
341 return compileexp(exp, context, methods)
341 return compileexp(exp, context, methods)
342 if exp[0] == 'symbol':
342 if exp[0] == 'symbol':
343 # unlike runsymbol(), here 'symbol' is always taken as template name
343 # unlike runsymbol(), here 'symbol' is always taken as template name
344 # even if it exists in mapping. this allows us to override mapping
344 # even if it exists in mapping. this allows us to override mapping
345 # by web templates, e.g. 'changelogtag' is redefined in map file.
345 # by web templates, e.g. 'changelogtag' is redefined in map file.
346 return context._load(exp[1])
346 return context._load(exp[1])
347 raise error.ParseError(_("expected template specifier"))
347 raise error.ParseError(_("expected template specifier"))
348
348
349 def _runrecursivesymbol(context, mapping, key):
349 def _runrecursivesymbol(context, mapping, key):
350 raise error.Abort(_("recursive reference '%s' in template") % key)
350 raise error.Abort(_("recursive reference '%s' in template") % key)
351
351
352 def buildtemplate(exp, context):
352 def buildtemplate(exp, context):
353 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
353 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
354 return (templateutil.runtemplate, ctmpl)
354 return (templateutil.runtemplate, ctmpl)
355
355
356 def buildfilter(exp, context):
356 def buildfilter(exp, context):
357 n = getsymbol(exp[2])
357 n = getsymbol(exp[2])
358 if n in context._filters:
358 if n in context._filters:
359 filt = context._filters[n]
359 filt = context._filters[n]
360 arg = compileexp(exp[1], context, methods)
360 arg = compileexp(exp[1], context, methods)
361 return (templateutil.runfilter, (arg, filt))
361 return (templateutil.runfilter, (arg, filt))
362 if n in context._funcs:
362 if n in context._funcs:
363 f = context._funcs[n]
363 f = context._funcs[n]
364 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
364 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
365 return (f, args)
365 return (f, args)
366 raise error.ParseError(_("unknown function '%s'") % n)
366 raise error.ParseError(_("unknown function '%s'") % n)
367
367
368 def buildmap(exp, context):
368 def buildmap(exp, context):
369 darg = compileexp(exp[1], context, methods)
369 darg = compileexp(exp[1], context, methods)
370 targ = gettemplate(exp[2], context)
370 targ = gettemplate(exp[2], context)
371 return (templateutil.runmap, (darg, targ))
371 return (templateutil.runmap, (darg, targ))
372
372
373 def buildmember(exp, context):
373 def buildmember(exp, context):
374 darg = compileexp(exp[1], context, methods)
374 darg = compileexp(exp[1], context, methods)
375 memb = getsymbol(exp[2])
375 memb = getsymbol(exp[2])
376 return (templateutil.runmember, (darg, memb))
376 return (templateutil.runmember, (darg, memb))
377
377
378 def buildnegate(exp, context):
378 def buildnegate(exp, context):
379 arg = compileexp(exp[1], context, exprmethods)
379 arg = compileexp(exp[1], context, exprmethods)
380 return (templateutil.runnegate, arg)
380 return (templateutil.runnegate, arg)
381
381
382 def buildarithmetic(exp, context, func):
382 def buildarithmetic(exp, context, func):
383 left = compileexp(exp[1], context, exprmethods)
383 left = compileexp(exp[1], context, exprmethods)
384 right = compileexp(exp[2], context, exprmethods)
384 right = compileexp(exp[2], context, exprmethods)
385 return (templateutil.runarithmetic, (func, left, right))
385 return (templateutil.runarithmetic, (func, left, right))
386
386
387 def buildfunc(exp, context):
387 def buildfunc(exp, context):
388 n = getsymbol(exp[1])
388 n = getsymbol(exp[1])
389 if n in context._funcs:
389 if n in context._funcs:
390 f = context._funcs[n]
390 f = context._funcs[n]
391 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
391 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
392 return (f, args)
392 return (f, args)
393 if n in context._filters:
393 if n in context._filters:
394 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
394 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
395 if len(args) != 1:
395 if len(args) != 1:
396 raise error.ParseError(_("filter %s expects one argument") % n)
396 raise error.ParseError(_("filter %s expects one argument") % n)
397 f = context._filters[n]
397 f = context._filters[n]
398 return (templateutil.runfilter, (args[0], f))
398 return (templateutil.runfilter, (args[0], f))
399 raise error.ParseError(_("unknown function '%s'") % n)
399 raise error.ParseError(_("unknown function '%s'") % n)
400
400
401 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
401 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
402 """Compile parsed tree of function arguments into list or dict of
402 """Compile parsed tree of function arguments into list or dict of
403 (func, data) pairs
403 (func, data) pairs
404
404
405 >>> context = engine(lambda t: (runsymbol, t))
405 >>> context = engine(lambda t: (runsymbol, t))
406 >>> def fargs(expr, argspec):
406 >>> def fargs(expr, argspec):
407 ... x = _parseexpr(expr)
407 ... x = _parseexpr(expr)
408 ... n = getsymbol(x[1])
408 ... n = getsymbol(x[1])
409 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
409 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
410 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
410 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
411 ['l', 'k']
411 ['l', 'k']
412 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
412 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
413 >>> list(args.keys()), list(args[b'opts'].keys())
413 >>> list(args.keys()), list(args[b'opts'].keys())
414 (['opts'], ['opts', 'k'])
414 (['opts'], ['opts', 'k'])
415 """
415 """
416 def compiledict(xs):
416 def compiledict(xs):
417 return util.sortdict((k, compileexp(x, context, curmethods))
417 return util.sortdict((k, compileexp(x, context, curmethods))
418 for k, x in xs.iteritems())
418 for k, x in xs.iteritems())
419 def compilelist(xs):
419 def compilelist(xs):
420 return [compileexp(x, context, curmethods) for x in xs]
420 return [compileexp(x, context, curmethods) for x in xs]
421
421
422 if not argspec:
422 if not argspec:
423 # filter or function with no argspec: return list of positional args
423 # filter or function with no argspec: return list of positional args
424 return compilelist(getlist(exp))
424 return compilelist(getlist(exp))
425
425
426 # function with argspec: return dict of named args
426 # function with argspec: return dict of named args
427 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
427 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
428 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
428 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
429 keyvaluenode='keyvalue', keynode='symbol')
429 keyvaluenode='keyvalue', keynode='symbol')
430 compargs = util.sortdict()
430 compargs = util.sortdict()
431 if varkey:
431 if varkey:
432 compargs[varkey] = compilelist(treeargs.pop(varkey))
432 compargs[varkey] = compilelist(treeargs.pop(varkey))
433 if optkey:
433 if optkey:
434 compargs[optkey] = compiledict(treeargs.pop(optkey))
434 compargs[optkey] = compiledict(treeargs.pop(optkey))
435 compargs.update(compiledict(treeargs))
435 compargs.update(compiledict(treeargs))
436 return compargs
436 return compargs
437
437
438 def buildkeyvaluepair(exp, content):
438 def buildkeyvaluepair(exp, content):
439 raise error.ParseError(_("can't use a key-value pair in this context"))
439 raise error.ParseError(_("can't use a key-value pair in this context"))
440
440
441 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
441 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
442 exprmethods = {
442 exprmethods = {
443 "integer": lambda e, c: (templateutil.runinteger, e[1]),
443 "integer": lambda e, c: (templateutil.runinteger, e[1]),
444 "string": lambda e, c: (templateutil.runstring, e[1]),
444 "string": lambda e, c: (templateutil.runstring, e[1]),
445 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
445 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
446 "template": buildtemplate,
446 "template": buildtemplate,
447 "group": lambda e, c: compileexp(e[1], c, exprmethods),
447 "group": lambda e, c: compileexp(e[1], c, exprmethods),
448 ".": buildmember,
448 ".": buildmember,
449 "|": buildfilter,
449 "|": buildfilter,
450 "%": buildmap,
450 "%": buildmap,
451 "func": buildfunc,
451 "func": buildfunc,
452 "keyvalue": buildkeyvaluepair,
452 "keyvalue": buildkeyvaluepair,
453 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
453 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
454 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
454 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
455 "negate": buildnegate,
455 "negate": buildnegate,
456 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
456 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
457 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
457 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
458 }
458 }
459
459
460 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
460 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
461 methods = exprmethods.copy()
461 methods = exprmethods.copy()
462 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
462 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
463
463
464 class _aliasrules(parser.basealiasrules):
464 class _aliasrules(parser.basealiasrules):
465 """Parsing and expansion rule set of template aliases"""
465 """Parsing and expansion rule set of template aliases"""
466 _section = _('template alias')
466 _section = _('template alias')
467 _parse = staticmethod(_parseexpr)
467 _parse = staticmethod(_parseexpr)
468
468
469 @staticmethod
469 @staticmethod
470 def _trygetfunc(tree):
470 def _trygetfunc(tree):
471 """Return (name, args) if tree is func(...) or ...|filter; otherwise
471 """Return (name, args) if tree is func(...) or ...|filter; otherwise
472 None"""
472 None"""
473 if tree[0] == 'func' and tree[1][0] == 'symbol':
473 if tree[0] == 'func' and tree[1][0] == 'symbol':
474 return tree[1][1], getlist(tree[2])
474 return tree[1][1], getlist(tree[2])
475 if tree[0] == '|' and tree[2][0] == 'symbol':
475 if tree[0] == '|' and tree[2][0] == 'symbol':
476 return tree[2][1], [tree[1]]
476 return tree[2][1], [tree[1]]
477
477
478 def expandaliases(tree, aliases):
478 def expandaliases(tree, aliases):
479 """Return new tree of aliases are expanded"""
479 """Return new tree of aliases are expanded"""
480 aliasmap = _aliasrules.buildmap(aliases)
480 aliasmap = _aliasrules.buildmap(aliases)
481 return _aliasrules.expand(aliasmap, tree)
481 return _aliasrules.expand(aliasmap, tree)
482
482
483 # template engine
483 # template engine
484
484
485 def _flatten(thing):
485 def _flatten(thing):
486 '''yield a single stream from a possibly nested set of iterators'''
486 '''yield a single stream from a possibly nested set of iterators'''
487 thing = templateutil.unwraphybrid(thing)
487 thing = templateutil.unwraphybrid(thing)
488 if isinstance(thing, bytes):
488 if isinstance(thing, bytes):
489 yield thing
489 yield thing
490 elif isinstance(thing, str):
490 elif isinstance(thing, str):
491 # We can only hit this on Python 3, and it's here to guard
491 # We can only hit this on Python 3, and it's here to guard
492 # against infinite recursion.
492 # against infinite recursion.
493 raise error.ProgrammingError('Mercurial IO including templates is done'
493 raise error.ProgrammingError('Mercurial IO including templates is done'
494 ' with bytes, not strings, got %r' % thing)
494 ' with bytes, not strings, got %r' % thing)
495 elif thing is None:
495 elif thing is None:
496 pass
496 pass
497 elif not util.safehasattr(thing, '__iter__'):
497 elif not util.safehasattr(thing, '__iter__'):
498 yield pycompat.bytestr(thing)
498 yield pycompat.bytestr(thing)
499 else:
499 else:
500 for i in thing:
500 for i in thing:
501 i = templateutil.unwraphybrid(i)
501 i = templateutil.unwraphybrid(i)
502 if isinstance(i, bytes):
502 if isinstance(i, bytes):
503 yield i
503 yield i
504 elif i is None:
504 elif i is None:
505 pass
505 pass
506 elif not util.safehasattr(i, '__iter__'):
506 elif not util.safehasattr(i, '__iter__'):
507 yield pycompat.bytestr(i)
507 yield pycompat.bytestr(i)
508 else:
508 else:
509 for j in _flatten(i):
509 for j in _flatten(i):
510 yield j
510 yield j
511
511
512 def unquotestring(s):
512 def unquotestring(s):
513 '''unwrap quotes if any; otherwise returns unmodified string'''
513 '''unwrap quotes if any; otherwise returns unmodified string'''
514 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
514 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
515 return s
515 return s
516 return s[1:-1]
516 return s[1:-1]
517
517
518 class engine(object):
518 class engine(object):
519 '''template expansion engine.
519 '''template expansion engine.
520
520
521 template expansion works like this. a map file contains key=value
521 template expansion works like this. a map file contains key=value
522 pairs. if value is quoted, it is treated as string. otherwise, it
522 pairs. if value is quoted, it is treated as string. otherwise, it
523 is treated as name of template file.
523 is treated as name of template file.
524
524
525 templater is asked to expand a key in map. it looks up key, and
525 templater is asked to expand a key in map. it looks up key, and
526 looks for strings like this: {foo}. it expands {foo} by looking up
526 looks for strings like this: {foo}. it expands {foo} by looking up
527 foo in map, and substituting it. expansion is recursive: it stops
527 foo in map, and substituting it. expansion is recursive: it stops
528 when there is no more {foo} to replace.
528 when there is no more {foo} to replace.
529
529
530 expansion also allows formatting and filtering.
530 expansion also allows formatting and filtering.
531
531
532 format uses key to expand each item in list. syntax is
532 format uses key to expand each item in list. syntax is
533 {key%format}.
533 {key%format}.
534
534
535 filter uses function to transform value. syntax is
535 filter uses function to transform value. syntax is
536 {key|filter1|filter2|...}.'''
536 {key|filter1|filter2|...}.'''
537
537
538 def __init__(self, loader, filters=None, defaults=None, resources=None,
538 def __init__(self, loader, filters=None, defaults=None, resources=None,
539 aliases=()):
539 aliases=()):
540 self._loader = loader
540 self._loader = loader
541 if filters is None:
541 if filters is None:
542 filters = {}
542 filters = {}
543 self._filters = filters
543 self._filters = filters
544 self._funcs = templatefuncs.funcs # make this a parameter if needed
544 self._funcs = templatefuncs.funcs # make this a parameter if needed
545 if defaults is None:
545 if defaults is None:
546 defaults = {}
546 defaults = {}
547 if resources is None:
547 if resources is None:
548 resources = {}
548 resources = {}
549 self._defaults = defaults
549 self._defaults = defaults
550 self._resources = resources
550 self._resources = resources
551 self._aliasmap = _aliasrules.buildmap(aliases)
551 self._aliasmap = _aliasrules.buildmap(aliases)
552 self._cache = {} # key: (func, data)
552 self._cache = {} # key: (func, data)
553
553
554 def symbol(self, mapping, key):
554 def symbol(self, mapping, key):
555 """Resolve symbol to value or function; None if nothing found"""
555 """Resolve symbol to value or function; None if nothing found"""
556 v = None
556 v = None
557 if key not in self._resources:
557 if key not in self._resources:
558 v = mapping.get(key)
558 v = mapping.get(key)
559 if v is None:
559 if v is None:
560 v = self._defaults.get(key)
560 v = self._defaults.get(key)
561 return v
561 return v
562
562
563 def resource(self, mapping, key):
563 def resource(self, mapping, key):
564 """Return internal data (e.g. cache) used for keyword/function
564 """Return internal data (e.g. cache) used for keyword/function
565 evaluation"""
565 evaluation"""
566 v = None
566 v = None
567 if key in self._resources:
567 if key in self._resources:
568 v = mapping.get(key)
569 if v is None and key in self._resources:
570 v = self._resources[key](self, mapping, key)
568 v = self._resources[key](self, mapping, key)
571 if v is None:
569 if v is None:
572 raise templateutil.ResourceUnavailable(
570 raise templateutil.ResourceUnavailable(
573 _('template resource not available: %s') % key)
571 _('template resource not available: %s') % key)
574 return v
572 return v
575
573
576 def _load(self, t):
574 def _load(self, t):
577 '''load, parse, and cache a template'''
575 '''load, parse, and cache a template'''
578 if t not in self._cache:
576 if t not in self._cache:
579 # put poison to cut recursion while compiling 't'
577 # put poison to cut recursion while compiling 't'
580 self._cache[t] = (_runrecursivesymbol, t)
578 self._cache[t] = (_runrecursivesymbol, t)
581 try:
579 try:
582 x = parse(self._loader(t))
580 x = parse(self._loader(t))
583 if self._aliasmap:
581 if self._aliasmap:
584 x = _aliasrules.expand(self._aliasmap, x)
582 x = _aliasrules.expand(self._aliasmap, x)
585 self._cache[t] = compileexp(x, self, methods)
583 self._cache[t] = compileexp(x, self, methods)
586 except: # re-raises
584 except: # re-raises
587 del self._cache[t]
585 del self._cache[t]
588 raise
586 raise
589 return self._cache[t]
587 return self._cache[t]
590
588
591 def process(self, t, mapping):
589 def process(self, t, mapping):
592 '''Perform expansion. t is name of map element to expand.
590 '''Perform expansion. t is name of map element to expand.
593 mapping contains added elements for use during expansion. Is a
591 mapping contains added elements for use during expansion. Is a
594 generator.'''
592 generator.'''
595 func, data = self._load(t)
593 func, data = self._load(t)
596 return _flatten(func(self, mapping, data))
594 return _flatten(func(self, mapping, data))
597
595
598 engines = {'default': engine}
596 engines = {'default': engine}
599
597
600 def stylelist():
598 def stylelist():
601 paths = templatepaths()
599 paths = templatepaths()
602 if not paths:
600 if not paths:
603 return _('no templates found, try `hg debuginstall` for more info')
601 return _('no templates found, try `hg debuginstall` for more info')
604 dirlist = os.listdir(paths[0])
602 dirlist = os.listdir(paths[0])
605 stylelist = []
603 stylelist = []
606 for file in dirlist:
604 for file in dirlist:
607 split = file.split(".")
605 split = file.split(".")
608 if split[-1] in ('orig', 'rej'):
606 if split[-1] in ('orig', 'rej'):
609 continue
607 continue
610 if split[0] == "map-cmdline":
608 if split[0] == "map-cmdline":
611 stylelist.append(split[1])
609 stylelist.append(split[1])
612 return ", ".join(sorted(stylelist))
610 return ", ".join(sorted(stylelist))
613
611
614 def _readmapfile(mapfile):
612 def _readmapfile(mapfile):
615 """Load template elements from the given map file"""
613 """Load template elements from the given map file"""
616 if not os.path.exists(mapfile):
614 if not os.path.exists(mapfile):
617 raise error.Abort(_("style '%s' not found") % mapfile,
615 raise error.Abort(_("style '%s' not found") % mapfile,
618 hint=_("available styles: %s") % stylelist())
616 hint=_("available styles: %s") % stylelist())
619
617
620 base = os.path.dirname(mapfile)
618 base = os.path.dirname(mapfile)
621 conf = config.config(includepaths=templatepaths())
619 conf = config.config(includepaths=templatepaths())
622 conf.read(mapfile, remap={'': 'templates'})
620 conf.read(mapfile, remap={'': 'templates'})
623
621
624 cache = {}
622 cache = {}
625 tmap = {}
623 tmap = {}
626 aliases = []
624 aliases = []
627
625
628 val = conf.get('templates', '__base__')
626 val = conf.get('templates', '__base__')
629 if val and val[0] not in "'\"":
627 if val and val[0] not in "'\"":
630 # treat as a pointer to a base class for this style
628 # treat as a pointer to a base class for this style
631 path = util.normpath(os.path.join(base, val))
629 path = util.normpath(os.path.join(base, val))
632
630
633 # fallback check in template paths
631 # fallback check in template paths
634 if not os.path.exists(path):
632 if not os.path.exists(path):
635 for p in templatepaths():
633 for p in templatepaths():
636 p2 = util.normpath(os.path.join(p, val))
634 p2 = util.normpath(os.path.join(p, val))
637 if os.path.isfile(p2):
635 if os.path.isfile(p2):
638 path = p2
636 path = p2
639 break
637 break
640 p3 = util.normpath(os.path.join(p2, "map"))
638 p3 = util.normpath(os.path.join(p2, "map"))
641 if os.path.isfile(p3):
639 if os.path.isfile(p3):
642 path = p3
640 path = p3
643 break
641 break
644
642
645 cache, tmap, aliases = _readmapfile(path)
643 cache, tmap, aliases = _readmapfile(path)
646
644
647 for key, val in conf['templates'].items():
645 for key, val in conf['templates'].items():
648 if not val:
646 if not val:
649 raise error.ParseError(_('missing value'),
647 raise error.ParseError(_('missing value'),
650 conf.source('templates', key))
648 conf.source('templates', key))
651 if val[0] in "'\"":
649 if val[0] in "'\"":
652 if val[0] != val[-1]:
650 if val[0] != val[-1]:
653 raise error.ParseError(_('unmatched quotes'),
651 raise error.ParseError(_('unmatched quotes'),
654 conf.source('templates', key))
652 conf.source('templates', key))
655 cache[key] = unquotestring(val)
653 cache[key] = unquotestring(val)
656 elif key != '__base__':
654 elif key != '__base__':
657 val = 'default', val
655 val = 'default', val
658 if ':' in val[1]:
656 if ':' in val[1]:
659 val = val[1].split(':', 1)
657 val = val[1].split(':', 1)
660 tmap[key] = val[0], os.path.join(base, val[1])
658 tmap[key] = val[0], os.path.join(base, val[1])
661 aliases.extend(conf['templatealias'].items())
659 aliases.extend(conf['templatealias'].items())
662 return cache, tmap, aliases
660 return cache, tmap, aliases
663
661
664 class templater(object):
662 class templater(object):
665
663
666 def __init__(self, filters=None, defaults=None, resources=None,
664 def __init__(self, filters=None, defaults=None, resources=None,
667 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
665 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
668 """Create template engine optionally with preloaded template fragments
666 """Create template engine optionally with preloaded template fragments
669
667
670 - ``filters``: a dict of functions to transform a value into another.
668 - ``filters``: a dict of functions to transform a value into another.
671 - ``defaults``: a dict of symbol values/functions; may be overridden
669 - ``defaults``: a dict of symbol values/functions; may be overridden
672 by a ``mapping`` dict.
670 by a ``mapping`` dict.
673 - ``resources``: a dict of functions returning internal data
671 - ``resources``: a dict of functions returning internal data
674 (e.g. cache), inaccessible from user template; may be overridden by
672 (e.g. cache), inaccessible from user template.
675 a ``mapping`` dict.
676 - ``cache``: a dict of preloaded template fragments.
673 - ``cache``: a dict of preloaded template fragments.
677 - ``aliases``: a list of alias (name, replacement) pairs.
674 - ``aliases``: a list of alias (name, replacement) pairs.
678
675
679 self.cache may be updated later to register additional template
676 self.cache may be updated later to register additional template
680 fragments.
677 fragments.
681 """
678 """
682 if filters is None:
679 if filters is None:
683 filters = {}
680 filters = {}
684 if defaults is None:
681 if defaults is None:
685 defaults = {}
682 defaults = {}
686 if resources is None:
683 if resources is None:
687 resources = {}
684 resources = {}
688 if cache is None:
685 if cache is None:
689 cache = {}
686 cache = {}
690 self.cache = cache.copy()
687 self.cache = cache.copy()
691 self.map = {}
688 self.map = {}
692 self.filters = templatefilters.filters.copy()
689 self.filters = templatefilters.filters.copy()
693 self.filters.update(filters)
690 self.filters.update(filters)
694 self.defaults = defaults
691 self.defaults = defaults
695 self._resources = {'templ': lambda context, mapping, key: self}
692 self._resources = {'templ': lambda context, mapping, key: self}
696 self._resources.update(resources)
693 self._resources.update(resources)
697 self._aliases = aliases
694 self._aliases = aliases
698 self.minchunk, self.maxchunk = minchunk, maxchunk
695 self.minchunk, self.maxchunk = minchunk, maxchunk
699 self.ecache = {}
696 self.ecache = {}
700
697
701 @classmethod
698 @classmethod
702 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
699 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
703 cache=None, minchunk=1024, maxchunk=65536):
700 cache=None, minchunk=1024, maxchunk=65536):
704 """Create templater from the specified map file"""
701 """Create templater from the specified map file"""
705 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
702 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
706 cache, tmap, aliases = _readmapfile(mapfile)
703 cache, tmap, aliases = _readmapfile(mapfile)
707 t.cache.update(cache)
704 t.cache.update(cache)
708 t.map = tmap
705 t.map = tmap
709 t._aliases = aliases
706 t._aliases = aliases
710 return t
707 return t
711
708
712 def __contains__(self, key):
709 def __contains__(self, key):
713 return key in self.cache or key in self.map
710 return key in self.cache or key in self.map
714
711
715 def load(self, t):
712 def load(self, t):
716 '''Get the template for the given template name. Use a local cache.'''
713 '''Get the template for the given template name. Use a local cache.'''
717 if t not in self.cache:
714 if t not in self.cache:
718 try:
715 try:
719 self.cache[t] = util.readfile(self.map[t][1])
716 self.cache[t] = util.readfile(self.map[t][1])
720 except KeyError as inst:
717 except KeyError as inst:
721 raise templateutil.TemplateNotFound(
718 raise templateutil.TemplateNotFound(
722 _('"%s" not in template map') % inst.args[0])
719 _('"%s" not in template map') % inst.args[0])
723 except IOError as inst:
720 except IOError as inst:
724 reason = (_('template file %s: %s')
721 reason = (_('template file %s: %s')
725 % (self.map[t][1], util.forcebytestr(inst.args[1])))
722 % (self.map[t][1], util.forcebytestr(inst.args[1])))
726 raise IOError(inst.args[0], encoding.strfromlocal(reason))
723 raise IOError(inst.args[0], encoding.strfromlocal(reason))
727 return self.cache[t]
724 return self.cache[t]
728
725
729 def render(self, mapping):
726 def render(self, mapping):
730 """Render the default unnamed template and return result as string"""
727 """Render the default unnamed template and return result as string"""
731 mapping = pycompat.strkwargs(mapping)
728 mapping = pycompat.strkwargs(mapping)
732 return templateutil.stringify(self('', **mapping))
729 return templateutil.stringify(self('', **mapping))
733
730
734 def __call__(self, t, **mapping):
731 def __call__(self, t, **mapping):
735 mapping = pycompat.byteskwargs(mapping)
732 mapping = pycompat.byteskwargs(mapping)
736 ttype = t in self.map and self.map[t][0] or 'default'
733 ttype = t in self.map and self.map[t][0] or 'default'
737 if ttype not in self.ecache:
734 if ttype not in self.ecache:
738 try:
735 try:
739 ecls = engines[ttype]
736 ecls = engines[ttype]
740 except KeyError:
737 except KeyError:
741 raise error.Abort(_('invalid template engine: %s') % ttype)
738 raise error.Abort(_('invalid template engine: %s') % ttype)
742 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
739 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
743 self._resources, self._aliases)
740 self._resources, self._aliases)
744 proc = self.ecache[ttype]
741 proc = self.ecache[ttype]
745
742
746 stream = proc.process(t, mapping)
743 stream = proc.process(t, mapping)
747 if self.minchunk:
744 if self.minchunk:
748 stream = util.increasingchunks(stream, min=self.minchunk,
745 stream = util.increasingchunks(stream, min=self.minchunk,
749 max=self.maxchunk)
746 max=self.maxchunk)
750 return stream
747 return stream
751
748
752 def templatepaths():
749 def templatepaths():
753 '''return locations used for template files.'''
750 '''return locations used for template files.'''
754 pathsrel = ['templates']
751 pathsrel = ['templates']
755 paths = [os.path.normpath(os.path.join(util.datapath, f))
752 paths = [os.path.normpath(os.path.join(util.datapath, f))
756 for f in pathsrel]
753 for f in pathsrel]
757 return [p for p in paths if os.path.isdir(p)]
754 return [p for p in paths if os.path.isdir(p)]
758
755
759 def templatepath(name):
756 def templatepath(name):
760 '''return location of template file. returns None if not found.'''
757 '''return location of template file. returns None if not found.'''
761 for p in templatepaths():
758 for p in templatepaths():
762 f = os.path.join(p, name)
759 f = os.path.join(p, name)
763 if os.path.exists(f):
760 if os.path.exists(f):
764 return f
761 return f
765 return None
762 return None
766
763
767 def stylemap(styles, paths=None):
764 def stylemap(styles, paths=None):
768 """Return path to mapfile for a given style.
765 """Return path to mapfile for a given style.
769
766
770 Searches mapfile in the following locations:
767 Searches mapfile in the following locations:
771 1. templatepath/style/map
768 1. templatepath/style/map
772 2. templatepath/map-style
769 2. templatepath/map-style
773 3. templatepath/map
770 3. templatepath/map
774 """
771 """
775
772
776 if paths is None:
773 if paths is None:
777 paths = templatepaths()
774 paths = templatepaths()
778 elif isinstance(paths, bytes):
775 elif isinstance(paths, bytes):
779 paths = [paths]
776 paths = [paths]
780
777
781 if isinstance(styles, bytes):
778 if isinstance(styles, bytes):
782 styles = [styles]
779 styles = [styles]
783
780
784 for style in styles:
781 for style in styles:
785 # only plain name is allowed to honor template paths
782 # only plain name is allowed to honor template paths
786 if (not style
783 if (not style
787 or style in (pycompat.oscurdir, pycompat.ospardir)
784 or style in (pycompat.oscurdir, pycompat.ospardir)
788 or pycompat.ossep in style
785 or pycompat.ossep in style
789 or pycompat.osaltsep and pycompat.osaltsep in style):
786 or pycompat.osaltsep and pycompat.osaltsep in style):
790 continue
787 continue
791 locations = [os.path.join(style, 'map'), 'map-' + style]
788 locations = [os.path.join(style, 'map'), 'map-' + style]
792 locations.append('map')
789 locations.append('map')
793
790
794 for path in paths:
791 for path in paths:
795 for location in locations:
792 for location in locations:
796 mapfile = os.path.join(path, location)
793 mapfile = os.path.join(path, location)
797 if os.path.isfile(mapfile):
794 if os.path.isfile(mapfile):
798 return style, mapfile
795 return style, mapfile
799
796
800 raise RuntimeError("No hgweb templates found in %r" % paths)
797 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now