##// END OF EJS Templates
templater: drop symbols which should be overridden by new 'ctx' (issue5612)...
Yuya Nishihara -
r37093:46859b43 default
parent child Browse files
Show More
@@ -1,588 +1,592
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 in {'ctx', 'fctx'} for k in ctxs)
191 assert all(k in {'ctx', 'fctx'} 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 props = {}
398 props = {}
399 # explicitly-defined fields precede templatekw
399 # explicitly-defined fields precede templatekw
400 props.update(item)
400 props.update(item)
401 if 'ctx' in item or 'fctx' in item:
401 if 'ctx' in item or 'fctx' in item:
402 # but template resources must be always available
402 # but template resources must be always available
403 props['revcache'] = {}
403 props['revcache'] = {}
404 self._out.write(self._t.render(ref, props))
404 self._out.write(self._t.render(ref, props))
405
405
406 def end(self):
406 def end(self):
407 baseformatter.end(self)
407 baseformatter.end(self)
408 self._renderitem('docfooter', {})
408 self._renderitem('docfooter', {})
409
409
410 templatespec = collections.namedtuple(r'templatespec',
410 templatespec = collections.namedtuple(r'templatespec',
411 r'ref tmpl mapfile')
411 r'ref tmpl mapfile')
412
412
413 def lookuptemplate(ui, topic, tmpl):
413 def lookuptemplate(ui, topic, tmpl):
414 """Find the template matching the given -T/--template spec 'tmpl'
414 """Find the template matching the given -T/--template spec 'tmpl'
415
415
416 'tmpl' can be any of the following:
416 'tmpl' can be any of the following:
417
417
418 - a literal template (e.g. '{rev}')
418 - a literal template (e.g. '{rev}')
419 - a map-file name or path (e.g. 'changelog')
419 - a map-file name or path (e.g. 'changelog')
420 - a reference to [templates] in config file
420 - a reference to [templates] in config file
421 - a path to raw template file
421 - a path to raw template file
422
422
423 A map file defines a stand-alone template environment. If a map file
423 A map file defines a stand-alone template environment. If a map file
424 selected, all templates defined in the file will be loaded, and the
424 selected, all templates defined in the file will be loaded, and the
425 template matching the given topic will be rendered. Aliases won't be
425 template matching the given topic will be rendered. Aliases won't be
426 loaded from user config, but from the map file.
426 loaded from user config, but from the map file.
427
427
428 If no map file selected, all templates in [templates] section will be
428 If no map file selected, all templates in [templates] section will be
429 available as well as aliases in [templatealias].
429 available as well as aliases in [templatealias].
430 """
430 """
431
431
432 # looks like a literal template?
432 # looks like a literal template?
433 if '{' in tmpl:
433 if '{' in tmpl:
434 return templatespec('', tmpl, None)
434 return templatespec('', tmpl, None)
435
435
436 # perhaps a stock style?
436 # perhaps a stock style?
437 if not os.path.split(tmpl)[0]:
437 if not os.path.split(tmpl)[0]:
438 mapname = (templater.templatepath('map-cmdline.' + tmpl)
438 mapname = (templater.templatepath('map-cmdline.' + tmpl)
439 or templater.templatepath(tmpl))
439 or templater.templatepath(tmpl))
440 if mapname and os.path.isfile(mapname):
440 if mapname and os.path.isfile(mapname):
441 return templatespec(topic, None, mapname)
441 return templatespec(topic, None, mapname)
442
442
443 # perhaps it's a reference to [templates]
443 # perhaps it's a reference to [templates]
444 if ui.config('templates', tmpl):
444 if ui.config('templates', tmpl):
445 return templatespec(tmpl, None, None)
445 return templatespec(tmpl, None, None)
446
446
447 if tmpl == 'list':
447 if tmpl == 'list':
448 ui.write(_("available styles: %s\n") % templater.stylelist())
448 ui.write(_("available styles: %s\n") % templater.stylelist())
449 raise error.Abort(_("specify a template"))
449 raise error.Abort(_("specify a template"))
450
450
451 # perhaps it's a path to a map or a template
451 # perhaps it's a path to a map or a template
452 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
452 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
453 # is it a mapfile for a style?
453 # is it a mapfile for a style?
454 if os.path.basename(tmpl).startswith("map-"):
454 if os.path.basename(tmpl).startswith("map-"):
455 return templatespec(topic, None, os.path.realpath(tmpl))
455 return templatespec(topic, None, os.path.realpath(tmpl))
456 with util.posixfile(tmpl, 'rb') as f:
456 with util.posixfile(tmpl, 'rb') as f:
457 tmpl = f.read()
457 tmpl = f.read()
458 return templatespec('', tmpl, None)
458 return templatespec('', tmpl, None)
459
459
460 # constant string?
460 # constant string?
461 return templatespec('', tmpl, None)
461 return templatespec('', tmpl, None)
462
462
463 def templatepartsmap(spec, t, partnames):
463 def templatepartsmap(spec, t, partnames):
464 """Create a mapping of {part: ref}"""
464 """Create a mapping of {part: ref}"""
465 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
465 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
466 if spec.mapfile:
466 if spec.mapfile:
467 partsmap.update((p, p) for p in partnames if p in t)
467 partsmap.update((p, p) for p in partnames if p in t)
468 elif spec.ref:
468 elif spec.ref:
469 for part in partnames:
469 for part in partnames:
470 ref = '%s:%s' % (spec.ref, part) # select config sub-section
470 ref = '%s:%s' % (spec.ref, part) # select config sub-section
471 if ref in t:
471 if ref in t:
472 partsmap[part] = ref
472 partsmap[part] = ref
473 return partsmap
473 return partsmap
474
474
475 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
475 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
476 """Create a templater from either a literal template or loading from
476 """Create a templater from either a literal template or loading from
477 a map file"""
477 a map file"""
478 assert not (spec.tmpl and spec.mapfile)
478 assert not (spec.tmpl and spec.mapfile)
479 if spec.mapfile:
479 if spec.mapfile:
480 frommapfile = templater.templater.frommapfile
480 frommapfile = templater.templater.frommapfile
481 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
481 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
482 cache=cache)
482 cache=cache)
483 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
483 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
484 cache=cache)
484 cache=cache)
485
485
486 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
486 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
487 """Create a templater from a string template 'tmpl'"""
487 """Create a templater from a string template 'tmpl'"""
488 aliases = ui.configitems('templatealias')
488 aliases = ui.configitems('templatealias')
489 t = templater.templater(defaults=defaults, resources=resources,
489 t = templater.templater(defaults=defaults, resources=resources,
490 cache=cache, aliases=aliases)
490 cache=cache, aliases=aliases)
491 t.cache.update((k, templater.unquotestring(v))
491 t.cache.update((k, templater.unquotestring(v))
492 for k, v in ui.configitems('templates'))
492 for k, v in ui.configitems('templates'))
493 if tmpl:
493 if tmpl:
494 t.cache[''] = tmpl
494 t.cache[''] = tmpl
495 return t
495 return t
496
496
497 class templateresources(templater.resourcemapper):
497 class templateresources(templater.resourcemapper):
498 """Resource mapper designed for the default templatekw and function"""
498 """Resource mapper designed for the default templatekw and function"""
499
499
500 def __init__(self, ui, repo=None):
500 def __init__(self, ui, repo=None):
501 self._resmap = {
501 self._resmap = {
502 'cache': {}, # for templatekw/funcs to store reusable data
502 'cache': {}, # for templatekw/funcs to store reusable data
503 'repo': repo,
503 'repo': repo,
504 'ui': ui,
504 'ui': ui,
505 }
505 }
506
506
507 def availablekeys(self, context, mapping):
508 return {k for k, g in self._gettermap.iteritems()
509 if g(self, context, mapping, k) is not None}
510
507 def knownkeys(self):
511 def knownkeys(self):
508 return self._knownkeys
512 return self._knownkeys
509
513
510 def lookup(self, context, mapping, key):
514 def lookup(self, context, mapping, key):
511 get = self._gettermap.get(key)
515 get = self._gettermap.get(key)
512 if not get:
516 if not get:
513 return None
517 return None
514 return get(self, context, mapping, key)
518 return get(self, context, mapping, key)
515
519
516 def _getsome(self, context, mapping, key):
520 def _getsome(self, context, mapping, key):
517 v = mapping.get(key)
521 v = mapping.get(key)
518 if v is not None:
522 if v is not None:
519 return v
523 return v
520 return self._resmap.get(key)
524 return self._resmap.get(key)
521
525
522 def _getctx(self, context, mapping, key):
526 def _getctx(self, context, mapping, key):
523 ctx = mapping.get('ctx')
527 ctx = mapping.get('ctx')
524 if ctx is not None:
528 if ctx is not None:
525 return ctx
529 return ctx
526 fctx = mapping.get('fctx')
530 fctx = mapping.get('fctx')
527 if fctx is not None:
531 if fctx is not None:
528 return fctx.changectx()
532 return fctx.changectx()
529
533
530 def _getrepo(self, context, mapping, key):
534 def _getrepo(self, context, mapping, key):
531 ctx = self._getctx(context, mapping, 'ctx')
535 ctx = self._getctx(context, mapping, 'ctx')
532 if ctx is not None:
536 if ctx is not None:
533 return ctx.repo()
537 return ctx.repo()
534 return self._getsome(context, mapping, key)
538 return self._getsome(context, mapping, key)
535
539
536 _gettermap = {
540 _gettermap = {
537 'cache': _getsome,
541 'cache': _getsome,
538 'ctx': _getctx,
542 'ctx': _getctx,
539 'fctx': _getsome,
543 'fctx': _getsome,
540 'repo': _getrepo,
544 'repo': _getrepo,
541 'revcache': _getsome, # per-ctx cache; set later
545 'revcache': _getsome, # per-ctx cache; set later
542 'ui': _getsome,
546 'ui': _getsome,
543 }
547 }
544 _knownkeys = set(_gettermap.keys())
548 _knownkeys = set(_gettermap.keys())
545
549
546 def formatter(ui, out, topic, opts):
550 def formatter(ui, out, topic, opts):
547 template = opts.get("template", "")
551 template = opts.get("template", "")
548 if template == "json":
552 if template == "json":
549 return jsonformatter(ui, out, topic, opts)
553 return jsonformatter(ui, out, topic, opts)
550 elif template == "pickle":
554 elif template == "pickle":
551 return pickleformatter(ui, out, topic, opts)
555 return pickleformatter(ui, out, topic, opts)
552 elif template == "debug":
556 elif template == "debug":
553 return debugformatter(ui, out, topic, opts)
557 return debugformatter(ui, out, topic, opts)
554 elif template != "":
558 elif template != "":
555 return templateformatter(ui, out, topic, opts)
559 return templateformatter(ui, out, topic, opts)
556 # developer config: ui.formatdebug
560 # developer config: ui.formatdebug
557 elif ui.configbool('ui', 'formatdebug'):
561 elif ui.configbool('ui', 'formatdebug'):
558 return debugformatter(ui, out, topic, opts)
562 return debugformatter(ui, out, topic, opts)
559 # deprecated config: ui.formatjson
563 # deprecated config: ui.formatjson
560 elif ui.configbool('ui', 'formatjson'):
564 elif ui.configbool('ui', 'formatjson'):
561 return jsonformatter(ui, out, topic, opts)
565 return jsonformatter(ui, out, topic, opts)
562 return plainformatter(ui, out, topic, opts)
566 return plainformatter(ui, out, topic, opts)
563
567
564 @contextlib.contextmanager
568 @contextlib.contextmanager
565 def openformatter(ui, filename, topic, opts):
569 def openformatter(ui, filename, topic, opts):
566 """Create a formatter that writes outputs to the specified file
570 """Create a formatter that writes outputs to the specified file
567
571
568 Must be invoked using the 'with' statement.
572 Must be invoked using the 'with' statement.
569 """
573 """
570 with util.posixfile(filename, 'wb') as out:
574 with util.posixfile(filename, 'wb') as out:
571 with formatter(ui, out, topic, opts) as fm:
575 with formatter(ui, out, topic, opts) as fm:
572 yield fm
576 yield fm
573
577
574 @contextlib.contextmanager
578 @contextlib.contextmanager
575 def _neverending(fm):
579 def _neverending(fm):
576 yield fm
580 yield fm
577
581
578 def maybereopen(fm, filename, opts):
582 def maybereopen(fm, filename, opts):
579 """Create a formatter backed by file if filename specified, else return
583 """Create a formatter backed by file if filename specified, else return
580 the given formatter
584 the given formatter
581
585
582 Must be invoked using the 'with' statement. This will never call fm.end()
586 Must be invoked using the 'with' statement. This will never call fm.end()
583 of the given formatter.
587 of the given formatter.
584 """
588 """
585 if filename:
589 if filename:
586 return openformatter(fm._ui, filename, fm._topic, opts)
590 return openformatter(fm._ui, filename, fm._topic, opts)
587 else:
591 else:
588 return _neverending(fm)
592 return _neverending(fm)
@@ -1,873 +1,893
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Slightly complicated template engine for commands and hgweb
8 """Slightly complicated template engine for commands and hgweb
9
9
10 This module provides low-level interface to the template engine. See the
10 This module provides low-level interface to the template engine. See the
11 formatter and cmdutil modules if you are looking for high-level functions
11 formatter and cmdutil modules if you are looking for high-level functions
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
13
14 Internal Data Types
14 Internal Data Types
15 -------------------
15 -------------------
16
16
17 Template keywords and functions take a dictionary of current symbols and
17 Template keywords and functions take a dictionary of current symbols and
18 resources (a "mapping") and return result. Inputs and outputs must be one
18 resources (a "mapping") and return result. Inputs and outputs must be one
19 of the following data types:
19 of the following data types:
20
20
21 bytes
21 bytes
22 a byte string, which is generally a human-readable text in local encoding.
22 a byte string, which is generally a human-readable text in local encoding.
23
23
24 generator
24 generator
25 a lazily-evaluated byte string, which is a possibly nested generator of
25 a lazily-evaluated byte string, which is a possibly nested generator of
26 values of any printable types, and will be folded by ``stringify()``
26 values of any printable types, and will be folded by ``stringify()``
27 or ``flatten()``.
27 or ``flatten()``.
28
28
29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 returns a generator of dicts.)
30 returns a generator of dicts.)
31
31
32 None
32 None
33 sometimes represents an empty value, which can be stringified to ''.
33 sometimes represents an empty value, which can be stringified to ''.
34
34
35 True, False, int, float
35 True, False, int, float
36 can be stringified as such.
36 can be stringified as such.
37
37
38 date tuple
38 date tuple
39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
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 mappable
45 mappable
46 represents a scalar printable value, also supports % operator.
46 represents a scalar printable value, also supports % operator.
47 """
47 """
48
48
49 from __future__ import absolute_import, print_function
49 from __future__ import absolute_import, print_function
50
50
51 import abc
51 import abc
52 import os
52 import os
53
53
54 from .i18n import _
54 from .i18n import _
55 from . import (
55 from . import (
56 config,
56 config,
57 encoding,
57 encoding,
58 error,
58 error,
59 parser,
59 parser,
60 pycompat,
60 pycompat,
61 templatefilters,
61 templatefilters,
62 templatefuncs,
62 templatefuncs,
63 templateutil,
63 templateutil,
64 util,
64 util,
65 )
65 )
66
66
67 # template parsing
67 # template parsing
68
68
69 elements = {
69 elements = {
70 # token-type: binding-strength, primary, prefix, infix, suffix
70 # token-type: binding-strength, primary, prefix, infix, suffix
71 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
71 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
72 ".": (18, None, None, (".", 18), None),
72 ".": (18, None, None, (".", 18), None),
73 "%": (15, None, None, ("%", 15), None),
73 "%": (15, None, None, ("%", 15), None),
74 "|": (15, None, None, ("|", 15), None),
74 "|": (15, None, None, ("|", 15), None),
75 "*": (5, None, None, ("*", 5), None),
75 "*": (5, None, None, ("*", 5), None),
76 "/": (5, None, None, ("/", 5), None),
76 "/": (5, None, None, ("/", 5), None),
77 "+": (4, None, None, ("+", 4), None),
77 "+": (4, None, None, ("+", 4), None),
78 "-": (4, None, ("negate", 19), ("-", 4), None),
78 "-": (4, None, ("negate", 19), ("-", 4), None),
79 "=": (3, None, None, ("keyvalue", 3), None),
79 "=": (3, None, None, ("keyvalue", 3), None),
80 ",": (2, None, None, ("list", 2), None),
80 ",": (2, None, None, ("list", 2), None),
81 ")": (0, None, None, None, None),
81 ")": (0, None, None, None, None),
82 "integer": (0, "integer", None, None, None),
82 "integer": (0, "integer", None, None, None),
83 "symbol": (0, "symbol", None, None, None),
83 "symbol": (0, "symbol", None, None, None),
84 "string": (0, "string", None, None, None),
84 "string": (0, "string", None, None, None),
85 "template": (0, "template", None, None, None),
85 "template": (0, "template", None, None, None),
86 "end": (0, None, None, None, None),
86 "end": (0, None, None, None, None),
87 }
87 }
88
88
89 def tokenize(program, start, end, term=None):
89 def tokenize(program, start, end, term=None):
90 """Parse a template expression into a stream of tokens, which must end
90 """Parse a template expression into a stream of tokens, which must end
91 with term if specified"""
91 with term if specified"""
92 pos = start
92 pos = start
93 program = pycompat.bytestr(program)
93 program = pycompat.bytestr(program)
94 while pos < end:
94 while pos < end:
95 c = program[pos]
95 c = program[pos]
96 if c.isspace(): # skip inter-token whitespace
96 if c.isspace(): # skip inter-token whitespace
97 pass
97 pass
98 elif c in "(=,).%|+-*/": # handle simple operators
98 elif c in "(=,).%|+-*/": # handle simple operators
99 yield (c, None, pos)
99 yield (c, None, pos)
100 elif c in '"\'': # handle quoted templates
100 elif c in '"\'': # handle quoted templates
101 s = pos + 1
101 s = pos + 1
102 data, pos = _parsetemplate(program, s, end, c)
102 data, pos = _parsetemplate(program, s, end, c)
103 yield ('template', data, s)
103 yield ('template', data, s)
104 pos -= 1
104 pos -= 1
105 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
105 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
106 # handle quoted strings
106 # handle quoted strings
107 c = program[pos + 1]
107 c = program[pos + 1]
108 s = pos = pos + 2
108 s = pos = pos + 2
109 while pos < end: # find closing quote
109 while pos < end: # find closing quote
110 d = program[pos]
110 d = program[pos]
111 if d == '\\': # skip over escaped characters
111 if d == '\\': # skip over escaped characters
112 pos += 2
112 pos += 2
113 continue
113 continue
114 if d == c:
114 if d == c:
115 yield ('string', program[s:pos], s)
115 yield ('string', program[s:pos], s)
116 break
116 break
117 pos += 1
117 pos += 1
118 else:
118 else:
119 raise error.ParseError(_("unterminated string"), s)
119 raise error.ParseError(_("unterminated string"), s)
120 elif c.isdigit():
120 elif c.isdigit():
121 s = pos
121 s = pos
122 while pos < end:
122 while pos < end:
123 d = program[pos]
123 d = program[pos]
124 if not d.isdigit():
124 if not d.isdigit():
125 break
125 break
126 pos += 1
126 pos += 1
127 yield ('integer', program[s:pos], s)
127 yield ('integer', program[s:pos], s)
128 pos -= 1
128 pos -= 1
129 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
129 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
130 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
130 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
131 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
131 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
132 # where some of nested templates were preprocessed as strings and
132 # where some of nested templates were preprocessed as strings and
133 # then compiled. therefore, \"...\" was allowed. (issue4733)
133 # then compiled. therefore, \"...\" was allowed. (issue4733)
134 #
134 #
135 # processing flow of _evalifliteral() at 5ab28a2e9962:
135 # processing flow of _evalifliteral() at 5ab28a2e9962:
136 # outer template string -> stringify() -> compiletemplate()
136 # outer template string -> stringify() -> compiletemplate()
137 # ------------------------ ------------ ------------------
137 # ------------------------ ------------ ------------------
138 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
138 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
139 # ~~~~~~~~
139 # ~~~~~~~~
140 # escaped quoted string
140 # escaped quoted string
141 if c == 'r':
141 if c == 'r':
142 pos += 1
142 pos += 1
143 token = 'string'
143 token = 'string'
144 else:
144 else:
145 token = 'template'
145 token = 'template'
146 quote = program[pos:pos + 2]
146 quote = program[pos:pos + 2]
147 s = pos = pos + 2
147 s = pos = pos + 2
148 while pos < end: # find closing escaped quote
148 while pos < end: # find closing escaped quote
149 if program.startswith('\\\\\\', pos, end):
149 if program.startswith('\\\\\\', pos, end):
150 pos += 4 # skip over double escaped characters
150 pos += 4 # skip over double escaped characters
151 continue
151 continue
152 if program.startswith(quote, pos, end):
152 if program.startswith(quote, pos, end):
153 # interpret as if it were a part of an outer string
153 # interpret as if it were a part of an outer string
154 data = parser.unescapestr(program[s:pos])
154 data = parser.unescapestr(program[s:pos])
155 if token == 'template':
155 if token == 'template':
156 data = _parsetemplate(data, 0, len(data))[0]
156 data = _parsetemplate(data, 0, len(data))[0]
157 yield (token, data, s)
157 yield (token, data, s)
158 pos += 1
158 pos += 1
159 break
159 break
160 pos += 1
160 pos += 1
161 else:
161 else:
162 raise error.ParseError(_("unterminated string"), s)
162 raise error.ParseError(_("unterminated string"), s)
163 elif c.isalnum() or c in '_':
163 elif c.isalnum() or c in '_':
164 s = pos
164 s = pos
165 pos += 1
165 pos += 1
166 while pos < end: # find end of symbol
166 while pos < end: # find end of symbol
167 d = program[pos]
167 d = program[pos]
168 if not (d.isalnum() or d == "_"):
168 if not (d.isalnum() or d == "_"):
169 break
169 break
170 pos += 1
170 pos += 1
171 sym = program[s:pos]
171 sym = program[s:pos]
172 yield ('symbol', sym, s)
172 yield ('symbol', sym, s)
173 pos -= 1
173 pos -= 1
174 elif c == term:
174 elif c == term:
175 yield ('end', None, pos)
175 yield ('end', None, pos)
176 return
176 return
177 else:
177 else:
178 raise error.ParseError(_("syntax error"), pos)
178 raise error.ParseError(_("syntax error"), pos)
179 pos += 1
179 pos += 1
180 if term:
180 if term:
181 raise error.ParseError(_("unterminated template expansion"), start)
181 raise error.ParseError(_("unterminated template expansion"), start)
182 yield ('end', None, pos)
182 yield ('end', None, pos)
183
183
184 def _parsetemplate(tmpl, start, stop, quote=''):
184 def _parsetemplate(tmpl, start, stop, quote=''):
185 r"""
185 r"""
186 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
186 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
187 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
187 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
188 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
188 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
189 ([('string', 'foo'), ('symbol', 'bar')], 9)
189 ([('string', 'foo'), ('symbol', 'bar')], 9)
190 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
190 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
191 ([('string', 'foo')], 4)
191 ([('string', 'foo')], 4)
192 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
192 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
193 ([('string', 'foo"'), ('string', 'bar')], 9)
193 ([('string', 'foo"'), ('string', 'bar')], 9)
194 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
194 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
195 ([('string', 'foo\\')], 6)
195 ([('string', 'foo\\')], 6)
196 """
196 """
197 parsed = []
197 parsed = []
198 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
198 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
199 if typ == 'string':
199 if typ == 'string':
200 parsed.append((typ, val))
200 parsed.append((typ, val))
201 elif typ == 'template':
201 elif typ == 'template':
202 parsed.append(val)
202 parsed.append(val)
203 elif typ == 'end':
203 elif typ == 'end':
204 return parsed, pos
204 return parsed, pos
205 else:
205 else:
206 raise error.ProgrammingError('unexpected type: %s' % typ)
206 raise error.ProgrammingError('unexpected type: %s' % typ)
207 raise error.ProgrammingError('unterminated scanning of template')
207 raise error.ProgrammingError('unterminated scanning of template')
208
208
209 def scantemplate(tmpl, raw=False):
209 def scantemplate(tmpl, raw=False):
210 r"""Scan (type, start, end) positions of outermost elements in template
210 r"""Scan (type, start, end) positions of outermost elements in template
211
211
212 If raw=True, a backslash is not taken as an escape character just like
212 If raw=True, a backslash is not taken as an escape character just like
213 r'' string in Python. Note that this is different from r'' literal in
213 r'' string in Python. Note that this is different from r'' literal in
214 template in that no template fragment can appear in r'', e.g. r'{foo}'
214 template in that no template fragment can appear in r'', e.g. r'{foo}'
215 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
215 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
216 'foo'.
216 'foo'.
217
217
218 >>> list(scantemplate(b'foo{bar}"baz'))
218 >>> list(scantemplate(b'foo{bar}"baz'))
219 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
219 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
220 >>> list(scantemplate(b'outer{"inner"}outer'))
220 >>> list(scantemplate(b'outer{"inner"}outer'))
221 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
221 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
222 >>> list(scantemplate(b'foo\\{escaped}'))
222 >>> list(scantemplate(b'foo\\{escaped}'))
223 [('string', 0, 5), ('string', 5, 13)]
223 [('string', 0, 5), ('string', 5, 13)]
224 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
224 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
225 [('string', 0, 4), ('template', 4, 13)]
225 [('string', 0, 4), ('template', 4, 13)]
226 """
226 """
227 last = None
227 last = None
228 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
228 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
229 if last:
229 if last:
230 yield last + (pos,)
230 yield last + (pos,)
231 if typ == 'end':
231 if typ == 'end':
232 return
232 return
233 else:
233 else:
234 last = (typ, pos)
234 last = (typ, pos)
235 raise error.ProgrammingError('unterminated scanning of template')
235 raise error.ProgrammingError('unterminated scanning of template')
236
236
237 def _scantemplate(tmpl, start, stop, quote='', raw=False):
237 def _scantemplate(tmpl, start, stop, quote='', raw=False):
238 """Parse template string into chunks of strings and template expressions"""
238 """Parse template string into chunks of strings and template expressions"""
239 sepchars = '{' + quote
239 sepchars = '{' + quote
240 unescape = [parser.unescapestr, pycompat.identity][raw]
240 unescape = [parser.unescapestr, pycompat.identity][raw]
241 pos = start
241 pos = start
242 p = parser.parser(elements)
242 p = parser.parser(elements)
243 try:
243 try:
244 while pos < stop:
244 while pos < stop:
245 n = min((tmpl.find(c, pos, stop) for c in sepchars),
245 n = min((tmpl.find(c, pos, stop) for c in sepchars),
246 key=lambda n: (n < 0, n))
246 key=lambda n: (n < 0, n))
247 if n < 0:
247 if n < 0:
248 yield ('string', unescape(tmpl[pos:stop]), pos)
248 yield ('string', unescape(tmpl[pos:stop]), pos)
249 pos = stop
249 pos = stop
250 break
250 break
251 c = tmpl[n:n + 1]
251 c = tmpl[n:n + 1]
252 bs = 0 # count leading backslashes
252 bs = 0 # count leading backslashes
253 if not raw:
253 if not raw:
254 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
254 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
255 if bs % 2 == 1:
255 if bs % 2 == 1:
256 # escaped (e.g. '\{', '\\\{', but not '\\{')
256 # escaped (e.g. '\{', '\\\{', but not '\\{')
257 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
257 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
258 pos = n + 1
258 pos = n + 1
259 continue
259 continue
260 if n > pos:
260 if n > pos:
261 yield ('string', unescape(tmpl[pos:n]), pos)
261 yield ('string', unescape(tmpl[pos:n]), pos)
262 if c == quote:
262 if c == quote:
263 yield ('end', None, n + 1)
263 yield ('end', None, n + 1)
264 return
264 return
265
265
266 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
266 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
267 if not tmpl.startswith('}', pos):
267 if not tmpl.startswith('}', pos):
268 raise error.ParseError(_("invalid token"), pos)
268 raise error.ParseError(_("invalid token"), pos)
269 yield ('template', parseres, n)
269 yield ('template', parseres, n)
270 pos += 1
270 pos += 1
271
271
272 if quote:
272 if quote:
273 raise error.ParseError(_("unterminated string"), start)
273 raise error.ParseError(_("unterminated string"), start)
274 except error.ParseError as inst:
274 except error.ParseError as inst:
275 if len(inst.args) > 1: # has location
275 if len(inst.args) > 1: # has location
276 loc = inst.args[1]
276 loc = inst.args[1]
277 # Offset the caret location by the number of newlines before the
277 # Offset the caret location by the number of newlines before the
278 # location of the error, since we will replace one-char newlines
278 # location of the error, since we will replace one-char newlines
279 # with the two-char literal r'\n'.
279 # with the two-char literal r'\n'.
280 offset = tmpl[:loc].count('\n')
280 offset = tmpl[:loc].count('\n')
281 tmpl = tmpl.replace('\n', br'\n')
281 tmpl = tmpl.replace('\n', br'\n')
282 # We want the caret to point to the place in the template that
282 # We want the caret to point to the place in the template that
283 # failed to parse, but in a hint we get a open paren at the
283 # failed to parse, but in a hint we get a open paren at the
284 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
284 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
285 # to line up the caret with the location of the error.
285 # to line up the caret with the location of the error.
286 inst.hint = (tmpl + '\n'
286 inst.hint = (tmpl + '\n'
287 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
287 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
288 raise
288 raise
289 yield ('end', None, pos)
289 yield ('end', None, pos)
290
290
291 def _unnesttemplatelist(tree):
291 def _unnesttemplatelist(tree):
292 """Expand list of templates to node tuple
292 """Expand list of templates to node tuple
293
293
294 >>> def f(tree):
294 >>> def f(tree):
295 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
295 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
296 >>> f((b'template', []))
296 >>> f((b'template', []))
297 (string '')
297 (string '')
298 >>> f((b'template', [(b'string', b'foo')]))
298 >>> f((b'template', [(b'string', b'foo')]))
299 (string 'foo')
299 (string 'foo')
300 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
300 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
301 (template
301 (template
302 (string 'foo')
302 (string 'foo')
303 (symbol 'rev'))
303 (symbol 'rev'))
304 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
304 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
305 (template
305 (template
306 (symbol 'rev'))
306 (symbol 'rev'))
307 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
307 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
308 (string 'foo')
308 (string 'foo')
309 """
309 """
310 if not isinstance(tree, tuple):
310 if not isinstance(tree, tuple):
311 return tree
311 return tree
312 op = tree[0]
312 op = tree[0]
313 if op != 'template':
313 if op != 'template':
314 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
314 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
315
315
316 assert len(tree) == 2
316 assert len(tree) == 2
317 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
317 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
318 if not xs:
318 if not xs:
319 return ('string', '') # empty template ""
319 return ('string', '') # empty template ""
320 elif len(xs) == 1 and xs[0][0] == 'string':
320 elif len(xs) == 1 and xs[0][0] == 'string':
321 return xs[0] # fast path for string with no template fragment "x"
321 return xs[0] # fast path for string with no template fragment "x"
322 else:
322 else:
323 return (op,) + xs
323 return (op,) + xs
324
324
325 def parse(tmpl):
325 def parse(tmpl):
326 """Parse template string into tree"""
326 """Parse template string into tree"""
327 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
327 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
328 assert pos == len(tmpl), 'unquoted template should be consumed'
328 assert pos == len(tmpl), 'unquoted template should be consumed'
329 return _unnesttemplatelist(('template', parsed))
329 return _unnesttemplatelist(('template', parsed))
330
330
331 def _parseexpr(expr):
331 def _parseexpr(expr):
332 """Parse a template expression into tree
332 """Parse a template expression into tree
333
333
334 >>> _parseexpr(b'"foo"')
334 >>> _parseexpr(b'"foo"')
335 ('string', 'foo')
335 ('string', 'foo')
336 >>> _parseexpr(b'foo(bar)')
336 >>> _parseexpr(b'foo(bar)')
337 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
337 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
338 >>> _parseexpr(b'foo(')
338 >>> _parseexpr(b'foo(')
339 Traceback (most recent call last):
339 Traceback (most recent call last):
340 ...
340 ...
341 ParseError: ('not a prefix: end', 4)
341 ParseError: ('not a prefix: end', 4)
342 >>> _parseexpr(b'"foo" "bar"')
342 >>> _parseexpr(b'"foo" "bar"')
343 Traceback (most recent call last):
343 Traceback (most recent call last):
344 ...
344 ...
345 ParseError: ('invalid token', 7)
345 ParseError: ('invalid token', 7)
346 """
346 """
347 p = parser.parser(elements)
347 p = parser.parser(elements)
348 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
348 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
349 if pos != len(expr):
349 if pos != len(expr):
350 raise error.ParseError(_('invalid token'), pos)
350 raise error.ParseError(_('invalid token'), pos)
351 return _unnesttemplatelist(tree)
351 return _unnesttemplatelist(tree)
352
352
353 def prettyformat(tree):
353 def prettyformat(tree):
354 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
354 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
355
355
356 def compileexp(exp, context, curmethods):
356 def compileexp(exp, context, curmethods):
357 """Compile parsed template tree to (func, data) pair"""
357 """Compile parsed template tree to (func, data) pair"""
358 if not exp:
358 if not exp:
359 raise error.ParseError(_("missing argument"))
359 raise error.ParseError(_("missing argument"))
360 t = exp[0]
360 t = exp[0]
361 if t in curmethods:
361 if t in curmethods:
362 return curmethods[t](exp, context)
362 return curmethods[t](exp, context)
363 raise error.ParseError(_("unknown method '%s'") % t)
363 raise error.ParseError(_("unknown method '%s'") % t)
364
364
365 # template evaluation
365 # template evaluation
366
366
367 def getsymbol(exp):
367 def getsymbol(exp):
368 if exp[0] == 'symbol':
368 if exp[0] == 'symbol':
369 return exp[1]
369 return exp[1]
370 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
370 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
371
371
372 def getlist(x):
372 def getlist(x):
373 if not x:
373 if not x:
374 return []
374 return []
375 if x[0] == 'list':
375 if x[0] == 'list':
376 return getlist(x[1]) + [x[2]]
376 return getlist(x[1]) + [x[2]]
377 return [x]
377 return [x]
378
378
379 def gettemplate(exp, context):
379 def gettemplate(exp, context):
380 """Compile given template tree or load named template from map file;
380 """Compile given template tree or load named template from map file;
381 returns (func, data) pair"""
381 returns (func, data) pair"""
382 if exp[0] in ('template', 'string'):
382 if exp[0] in ('template', 'string'):
383 return compileexp(exp, context, methods)
383 return compileexp(exp, context, methods)
384 if exp[0] == 'symbol':
384 if exp[0] == 'symbol':
385 # unlike runsymbol(), here 'symbol' is always taken as template name
385 # unlike runsymbol(), here 'symbol' is always taken as template name
386 # even if it exists in mapping. this allows us to override mapping
386 # even if it exists in mapping. this allows us to override mapping
387 # by web templates, e.g. 'changelogtag' is redefined in map file.
387 # by web templates, e.g. 'changelogtag' is redefined in map file.
388 return context._load(exp[1])
388 return context._load(exp[1])
389 raise error.ParseError(_("expected template specifier"))
389 raise error.ParseError(_("expected template specifier"))
390
390
391 def _runrecursivesymbol(context, mapping, key):
391 def _runrecursivesymbol(context, mapping, key):
392 raise error.Abort(_("recursive reference '%s' in template") % key)
392 raise error.Abort(_("recursive reference '%s' in template") % key)
393
393
394 def buildtemplate(exp, context):
394 def buildtemplate(exp, context):
395 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
395 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
396 return (templateutil.runtemplate, ctmpl)
396 return (templateutil.runtemplate, ctmpl)
397
397
398 def buildfilter(exp, context):
398 def buildfilter(exp, context):
399 n = getsymbol(exp[2])
399 n = getsymbol(exp[2])
400 if n in context._filters:
400 if n in context._filters:
401 filt = context._filters[n]
401 filt = context._filters[n]
402 arg = compileexp(exp[1], context, methods)
402 arg = compileexp(exp[1], context, methods)
403 return (templateutil.runfilter, (arg, filt))
403 return (templateutil.runfilter, (arg, filt))
404 if n in context._funcs:
404 if n in context._funcs:
405 f = context._funcs[n]
405 f = context._funcs[n]
406 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
406 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
407 return (f, args)
407 return (f, args)
408 raise error.ParseError(_("unknown function '%s'") % n)
408 raise error.ParseError(_("unknown function '%s'") % n)
409
409
410 def buildmap(exp, context):
410 def buildmap(exp, context):
411 darg = compileexp(exp[1], context, methods)
411 darg = compileexp(exp[1], context, methods)
412 targ = gettemplate(exp[2], context)
412 targ = gettemplate(exp[2], context)
413 return (templateutil.runmap, (darg, targ))
413 return (templateutil.runmap, (darg, targ))
414
414
415 def buildmember(exp, context):
415 def buildmember(exp, context):
416 darg = compileexp(exp[1], context, methods)
416 darg = compileexp(exp[1], context, methods)
417 memb = getsymbol(exp[2])
417 memb = getsymbol(exp[2])
418 return (templateutil.runmember, (darg, memb))
418 return (templateutil.runmember, (darg, memb))
419
419
420 def buildnegate(exp, context):
420 def buildnegate(exp, context):
421 arg = compileexp(exp[1], context, exprmethods)
421 arg = compileexp(exp[1], context, exprmethods)
422 return (templateutil.runnegate, arg)
422 return (templateutil.runnegate, arg)
423
423
424 def buildarithmetic(exp, context, func):
424 def buildarithmetic(exp, context, func):
425 left = compileexp(exp[1], context, exprmethods)
425 left = compileexp(exp[1], context, exprmethods)
426 right = compileexp(exp[2], context, exprmethods)
426 right = compileexp(exp[2], context, exprmethods)
427 return (templateutil.runarithmetic, (func, left, right))
427 return (templateutil.runarithmetic, (func, left, right))
428
428
429 def buildfunc(exp, context):
429 def buildfunc(exp, context):
430 n = getsymbol(exp[1])
430 n = getsymbol(exp[1])
431 if n in context._funcs:
431 if n in context._funcs:
432 f = context._funcs[n]
432 f = context._funcs[n]
433 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
433 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
434 return (f, args)
434 return (f, args)
435 if n in context._filters:
435 if n in context._filters:
436 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
436 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
437 if len(args) != 1:
437 if len(args) != 1:
438 raise error.ParseError(_("filter %s expects one argument") % n)
438 raise error.ParseError(_("filter %s expects one argument") % n)
439 f = context._filters[n]
439 f = context._filters[n]
440 return (templateutil.runfilter, (args[0], f))
440 return (templateutil.runfilter, (args[0], f))
441 raise error.ParseError(_("unknown function '%s'") % n)
441 raise error.ParseError(_("unknown function '%s'") % n)
442
442
443 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
443 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
444 """Compile parsed tree of function arguments into list or dict of
444 """Compile parsed tree of function arguments into list or dict of
445 (func, data) pairs
445 (func, data) pairs
446
446
447 >>> context = engine(lambda t: (templateutil.runsymbol, t))
447 >>> context = engine(lambda t: (templateutil.runsymbol, t))
448 >>> def fargs(expr, argspec):
448 >>> def fargs(expr, argspec):
449 ... x = _parseexpr(expr)
449 ... x = _parseexpr(expr)
450 ... n = getsymbol(x[1])
450 ... n = getsymbol(x[1])
451 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
451 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
452 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
452 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
453 ['l', 'k']
453 ['l', 'k']
454 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
454 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
455 >>> list(args.keys()), list(args[b'opts'].keys())
455 >>> list(args.keys()), list(args[b'opts'].keys())
456 (['opts'], ['opts', 'k'])
456 (['opts'], ['opts', 'k'])
457 """
457 """
458 def compiledict(xs):
458 def compiledict(xs):
459 return util.sortdict((k, compileexp(x, context, curmethods))
459 return util.sortdict((k, compileexp(x, context, curmethods))
460 for k, x in xs.iteritems())
460 for k, x in xs.iteritems())
461 def compilelist(xs):
461 def compilelist(xs):
462 return [compileexp(x, context, curmethods) for x in xs]
462 return [compileexp(x, context, curmethods) for x in xs]
463
463
464 if not argspec:
464 if not argspec:
465 # filter or function with no argspec: return list of positional args
465 # filter or function with no argspec: return list of positional args
466 return compilelist(getlist(exp))
466 return compilelist(getlist(exp))
467
467
468 # function with argspec: return dict of named args
468 # function with argspec: return dict of named args
469 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
469 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
470 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
470 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
471 keyvaluenode='keyvalue', keynode='symbol')
471 keyvaluenode='keyvalue', keynode='symbol')
472 compargs = util.sortdict()
472 compargs = util.sortdict()
473 if varkey:
473 if varkey:
474 compargs[varkey] = compilelist(treeargs.pop(varkey))
474 compargs[varkey] = compilelist(treeargs.pop(varkey))
475 if optkey:
475 if optkey:
476 compargs[optkey] = compiledict(treeargs.pop(optkey))
476 compargs[optkey] = compiledict(treeargs.pop(optkey))
477 compargs.update(compiledict(treeargs))
477 compargs.update(compiledict(treeargs))
478 return compargs
478 return compargs
479
479
480 def buildkeyvaluepair(exp, content):
480 def buildkeyvaluepair(exp, content):
481 raise error.ParseError(_("can't use a key-value pair in this context"))
481 raise error.ParseError(_("can't use a key-value pair in this context"))
482
482
483 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
483 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
484 exprmethods = {
484 exprmethods = {
485 "integer": lambda e, c: (templateutil.runinteger, e[1]),
485 "integer": lambda e, c: (templateutil.runinteger, e[1]),
486 "string": lambda e, c: (templateutil.runstring, e[1]),
486 "string": lambda e, c: (templateutil.runstring, e[1]),
487 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
487 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
488 "template": buildtemplate,
488 "template": buildtemplate,
489 "group": lambda e, c: compileexp(e[1], c, exprmethods),
489 "group": lambda e, c: compileexp(e[1], c, exprmethods),
490 ".": buildmember,
490 ".": buildmember,
491 "|": buildfilter,
491 "|": buildfilter,
492 "%": buildmap,
492 "%": buildmap,
493 "func": buildfunc,
493 "func": buildfunc,
494 "keyvalue": buildkeyvaluepair,
494 "keyvalue": buildkeyvaluepair,
495 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
495 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
496 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
496 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
497 "negate": buildnegate,
497 "negate": buildnegate,
498 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
498 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
499 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
499 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
500 }
500 }
501
501
502 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
502 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
503 methods = exprmethods.copy()
503 methods = exprmethods.copy()
504 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
504 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
505
505
506 class _aliasrules(parser.basealiasrules):
506 class _aliasrules(parser.basealiasrules):
507 """Parsing and expansion rule set of template aliases"""
507 """Parsing and expansion rule set of template aliases"""
508 _section = _('template alias')
508 _section = _('template alias')
509 _parse = staticmethod(_parseexpr)
509 _parse = staticmethod(_parseexpr)
510
510
511 @staticmethod
511 @staticmethod
512 def _trygetfunc(tree):
512 def _trygetfunc(tree):
513 """Return (name, args) if tree is func(...) or ...|filter; otherwise
513 """Return (name, args) if tree is func(...) or ...|filter; otherwise
514 None"""
514 None"""
515 if tree[0] == 'func' and tree[1][0] == 'symbol':
515 if tree[0] == 'func' and tree[1][0] == 'symbol':
516 return tree[1][1], getlist(tree[2])
516 return tree[1][1], getlist(tree[2])
517 if tree[0] == '|' and tree[2][0] == 'symbol':
517 if tree[0] == '|' and tree[2][0] == 'symbol':
518 return tree[2][1], [tree[1]]
518 return tree[2][1], [tree[1]]
519
519
520 def expandaliases(tree, aliases):
520 def expandaliases(tree, aliases):
521 """Return new tree of aliases are expanded"""
521 """Return new tree of aliases are expanded"""
522 aliasmap = _aliasrules.buildmap(aliases)
522 aliasmap = _aliasrules.buildmap(aliases)
523 return _aliasrules.expand(aliasmap, tree)
523 return _aliasrules.expand(aliasmap, tree)
524
524
525 # template engine
525 # template engine
526
526
527 def _flatten(thing):
527 def _flatten(thing):
528 '''yield a single stream from a possibly nested set of iterators'''
528 '''yield a single stream from a possibly nested set of iterators'''
529 thing = templateutil.unwraphybrid(thing)
529 thing = templateutil.unwraphybrid(thing)
530 if isinstance(thing, bytes):
530 if isinstance(thing, bytes):
531 yield thing
531 yield thing
532 elif isinstance(thing, str):
532 elif isinstance(thing, str):
533 # We can only hit this on Python 3, and it's here to guard
533 # We can only hit this on Python 3, and it's here to guard
534 # against infinite recursion.
534 # against infinite recursion.
535 raise error.ProgrammingError('Mercurial IO including templates is done'
535 raise error.ProgrammingError('Mercurial IO including templates is done'
536 ' with bytes, not strings, got %r' % thing)
536 ' with bytes, not strings, got %r' % thing)
537 elif thing is None:
537 elif thing is None:
538 pass
538 pass
539 elif not util.safehasattr(thing, '__iter__'):
539 elif not util.safehasattr(thing, '__iter__'):
540 yield pycompat.bytestr(thing)
540 yield pycompat.bytestr(thing)
541 else:
541 else:
542 for i in thing:
542 for i in thing:
543 i = templateutil.unwraphybrid(i)
543 i = templateutil.unwraphybrid(i)
544 if isinstance(i, bytes):
544 if isinstance(i, bytes):
545 yield i
545 yield i
546 elif i is None:
546 elif i is None:
547 pass
547 pass
548 elif not util.safehasattr(i, '__iter__'):
548 elif not util.safehasattr(i, '__iter__'):
549 yield pycompat.bytestr(i)
549 yield pycompat.bytestr(i)
550 else:
550 else:
551 for j in _flatten(i):
551 for j in _flatten(i):
552 yield j
552 yield j
553
553
554 def unquotestring(s):
554 def unquotestring(s):
555 '''unwrap quotes if any; otherwise returns unmodified string'''
555 '''unwrap quotes if any; otherwise returns unmodified string'''
556 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
556 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
557 return s
557 return s
558 return s[1:-1]
558 return s[1:-1]
559
559
560 class resourcemapper(object):
560 class resourcemapper(object):
561 """Mapper of internal template resources"""
561 """Mapper of internal template resources"""
562
562
563 __metaclass__ = abc.ABCMeta
563 __metaclass__ = abc.ABCMeta
564
564
565 @abc.abstractmethod
565 @abc.abstractmethod
566 def availablekeys(self, context, mapping):
567 """Return a set of available resource keys based on the given mapping"""
568
569 @abc.abstractmethod
566 def knownkeys(self):
570 def knownkeys(self):
567 """Return a set of supported resource keys"""
571 """Return a set of supported resource keys"""
568
572
569 @abc.abstractmethod
573 @abc.abstractmethod
570 def lookup(self, context, mapping, key):
574 def lookup(self, context, mapping, key):
571 """Return a resource for the key if available; otherwise None"""
575 """Return a resource for the key if available; otherwise None"""
572
576
573 class nullresourcemapper(resourcemapper):
577 class nullresourcemapper(resourcemapper):
578 def availablekeys(self, context, mapping):
579 return set()
580
574 def knownkeys(self):
581 def knownkeys(self):
575 return set()
582 return set()
576
583
577 def lookup(self, context, mapping, key):
584 def lookup(self, context, mapping, key):
578 return None
585 return None
579
586
580 class engine(object):
587 class engine(object):
581 '''template expansion engine.
588 '''template expansion engine.
582
589
583 template expansion works like this. a map file contains key=value
590 template expansion works like this. a map file contains key=value
584 pairs. if value is quoted, it is treated as string. otherwise, it
591 pairs. if value is quoted, it is treated as string. otherwise, it
585 is treated as name of template file.
592 is treated as name of template file.
586
593
587 templater is asked to expand a key in map. it looks up key, and
594 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
595 looks for strings like this: {foo}. it expands {foo} by looking up
589 foo in map, and substituting it. expansion is recursive: it stops
596 foo in map, and substituting it. expansion is recursive: it stops
590 when there is no more {foo} to replace.
597 when there is no more {foo} to replace.
591
598
592 expansion also allows formatting and filtering.
599 expansion also allows formatting and filtering.
593
600
594 format uses key to expand each item in list. syntax is
601 format uses key to expand each item in list. syntax is
595 {key%format}.
602 {key%format}.
596
603
597 filter uses function to transform value. syntax is
604 filter uses function to transform value. syntax is
598 {key|filter1|filter2|...}.'''
605 {key|filter1|filter2|...}.'''
599
606
600 def __init__(self, loader, filters=None, defaults=None, resources=None,
607 def __init__(self, loader, filters=None, defaults=None, resources=None,
601 aliases=()):
608 aliases=()):
602 self._loader = loader
609 self._loader = loader
603 if filters is None:
610 if filters is None:
604 filters = {}
611 filters = {}
605 self._filters = filters
612 self._filters = filters
606 self._funcs = templatefuncs.funcs # make this a parameter if needed
613 self._funcs = templatefuncs.funcs # make this a parameter if needed
607 if defaults is None:
614 if defaults is None:
608 defaults = {}
615 defaults = {}
609 if resources is None:
616 if resources is None:
610 resources = nullresourcemapper()
617 resources = nullresourcemapper()
611 self._defaults = defaults
618 self._defaults = defaults
612 self._resources = resources
619 self._resources = resources
613 self._aliasmap = _aliasrules.buildmap(aliases)
620 self._aliasmap = _aliasrules.buildmap(aliases)
614 self._cache = {} # key: (func, data)
621 self._cache = {} # key: (func, data)
615
622
616 def overlaymap(self, origmapping, newmapping):
623 def overlaymap(self, origmapping, newmapping):
617 """Create combined mapping from the original mapping and partial
624 """Create combined mapping from the original mapping and partial
618 mapping to override the original"""
625 mapping to override the original"""
619 mapping = origmapping.copy()
626 # do not copy symbols which overrides the defaults depending on
627 # new resources, so the defaults will be re-evaluated (issue5612)
628 knownres = self._resources.knownkeys()
629 newres = self._resources.availablekeys(self, newmapping)
630 mapping = {k: v for k, v in origmapping.iteritems()
631 if (k in knownres # not a symbol per self.symbol()
632 or newres.isdisjoint(self._defaultrequires(k)))}
620 mapping.update(newmapping)
633 mapping.update(newmapping)
621 return mapping
634 return mapping
622
635
636 def _defaultrequires(self, key):
637 """Resource keys required by the specified default symbol function"""
638 v = self._defaults.get(key)
639 if v is None or not callable(v):
640 return ()
641 return getattr(v, '_requires', ())
642
623 def symbol(self, mapping, key):
643 def symbol(self, mapping, key):
624 """Resolve symbol to value or function; None if nothing found"""
644 """Resolve symbol to value or function; None if nothing found"""
625 v = None
645 v = None
626 if key not in self._resources.knownkeys():
646 if key not in self._resources.knownkeys():
627 v = mapping.get(key)
647 v = mapping.get(key)
628 if v is None:
648 if v is None:
629 v = self._defaults.get(key)
649 v = self._defaults.get(key)
630 return v
650 return v
631
651
632 def resource(self, mapping, key):
652 def resource(self, mapping, key):
633 """Return internal data (e.g. cache) used for keyword/function
653 """Return internal data (e.g. cache) used for keyword/function
634 evaluation"""
654 evaluation"""
635 v = self._resources.lookup(self, mapping, key)
655 v = self._resources.lookup(self, mapping, key)
636 if v is None:
656 if v is None:
637 raise templateutil.ResourceUnavailable(
657 raise templateutil.ResourceUnavailable(
638 _('template resource not available: %s') % key)
658 _('template resource not available: %s') % key)
639 return v
659 return v
640
660
641 def _load(self, t):
661 def _load(self, t):
642 '''load, parse, and cache a template'''
662 '''load, parse, and cache a template'''
643 if t not in self._cache:
663 if t not in self._cache:
644 # put poison to cut recursion while compiling 't'
664 # put poison to cut recursion while compiling 't'
645 self._cache[t] = (_runrecursivesymbol, t)
665 self._cache[t] = (_runrecursivesymbol, t)
646 try:
666 try:
647 x = parse(self._loader(t))
667 x = parse(self._loader(t))
648 if self._aliasmap:
668 if self._aliasmap:
649 x = _aliasrules.expand(self._aliasmap, x)
669 x = _aliasrules.expand(self._aliasmap, x)
650 self._cache[t] = compileexp(x, self, methods)
670 self._cache[t] = compileexp(x, self, methods)
651 except: # re-raises
671 except: # re-raises
652 del self._cache[t]
672 del self._cache[t]
653 raise
673 raise
654 return self._cache[t]
674 return self._cache[t]
655
675
656 def preload(self, t):
676 def preload(self, t):
657 """Load, parse, and cache the specified template if available"""
677 """Load, parse, and cache the specified template if available"""
658 try:
678 try:
659 self._load(t)
679 self._load(t)
660 return True
680 return True
661 except templateutil.TemplateNotFound:
681 except templateutil.TemplateNotFound:
662 return False
682 return False
663
683
664 def process(self, t, mapping):
684 def process(self, t, mapping):
665 '''Perform expansion. t is name of map element to expand.
685 '''Perform expansion. t is name of map element to expand.
666 mapping contains added elements for use during expansion. Is a
686 mapping contains added elements for use during expansion. Is a
667 generator.'''
687 generator.'''
668 func, data = self._load(t)
688 func, data = self._load(t)
669 return _flatten(func(self, mapping, data))
689 return _flatten(func(self, mapping, data))
670
690
671 engines = {'default': engine}
691 engines = {'default': engine}
672
692
673 def stylelist():
693 def stylelist():
674 paths = templatepaths()
694 paths = templatepaths()
675 if not paths:
695 if not paths:
676 return _('no templates found, try `hg debuginstall` for more info')
696 return _('no templates found, try `hg debuginstall` for more info')
677 dirlist = os.listdir(paths[0])
697 dirlist = os.listdir(paths[0])
678 stylelist = []
698 stylelist = []
679 for file in dirlist:
699 for file in dirlist:
680 split = file.split(".")
700 split = file.split(".")
681 if split[-1] in ('orig', 'rej'):
701 if split[-1] in ('orig', 'rej'):
682 continue
702 continue
683 if split[0] == "map-cmdline":
703 if split[0] == "map-cmdline":
684 stylelist.append(split[1])
704 stylelist.append(split[1])
685 return ", ".join(sorted(stylelist))
705 return ", ".join(sorted(stylelist))
686
706
687 def _readmapfile(mapfile):
707 def _readmapfile(mapfile):
688 """Load template elements from the given map file"""
708 """Load template elements from the given map file"""
689 if not os.path.exists(mapfile):
709 if not os.path.exists(mapfile):
690 raise error.Abort(_("style '%s' not found") % mapfile,
710 raise error.Abort(_("style '%s' not found") % mapfile,
691 hint=_("available styles: %s") % stylelist())
711 hint=_("available styles: %s") % stylelist())
692
712
693 base = os.path.dirname(mapfile)
713 base = os.path.dirname(mapfile)
694 conf = config.config(includepaths=templatepaths())
714 conf = config.config(includepaths=templatepaths())
695 conf.read(mapfile, remap={'': 'templates'})
715 conf.read(mapfile, remap={'': 'templates'})
696
716
697 cache = {}
717 cache = {}
698 tmap = {}
718 tmap = {}
699 aliases = []
719 aliases = []
700
720
701 val = conf.get('templates', '__base__')
721 val = conf.get('templates', '__base__')
702 if val and val[0] not in "'\"":
722 if val and val[0] not in "'\"":
703 # treat as a pointer to a base class for this style
723 # treat as a pointer to a base class for this style
704 path = util.normpath(os.path.join(base, val))
724 path = util.normpath(os.path.join(base, val))
705
725
706 # fallback check in template paths
726 # fallback check in template paths
707 if not os.path.exists(path):
727 if not os.path.exists(path):
708 for p in templatepaths():
728 for p in templatepaths():
709 p2 = util.normpath(os.path.join(p, val))
729 p2 = util.normpath(os.path.join(p, val))
710 if os.path.isfile(p2):
730 if os.path.isfile(p2):
711 path = p2
731 path = p2
712 break
732 break
713 p3 = util.normpath(os.path.join(p2, "map"))
733 p3 = util.normpath(os.path.join(p2, "map"))
714 if os.path.isfile(p3):
734 if os.path.isfile(p3):
715 path = p3
735 path = p3
716 break
736 break
717
737
718 cache, tmap, aliases = _readmapfile(path)
738 cache, tmap, aliases = _readmapfile(path)
719
739
720 for key, val in conf['templates'].items():
740 for key, val in conf['templates'].items():
721 if not val:
741 if not val:
722 raise error.ParseError(_('missing value'),
742 raise error.ParseError(_('missing value'),
723 conf.source('templates', key))
743 conf.source('templates', key))
724 if val[0] in "'\"":
744 if val[0] in "'\"":
725 if val[0] != val[-1]:
745 if val[0] != val[-1]:
726 raise error.ParseError(_('unmatched quotes'),
746 raise error.ParseError(_('unmatched quotes'),
727 conf.source('templates', key))
747 conf.source('templates', key))
728 cache[key] = unquotestring(val)
748 cache[key] = unquotestring(val)
729 elif key != '__base__':
749 elif key != '__base__':
730 val = 'default', val
750 val = 'default', val
731 if ':' in val[1]:
751 if ':' in val[1]:
732 val = val[1].split(':', 1)
752 val = val[1].split(':', 1)
733 tmap[key] = val[0], os.path.join(base, val[1])
753 tmap[key] = val[0], os.path.join(base, val[1])
734 aliases.extend(conf['templatealias'].items())
754 aliases.extend(conf['templatealias'].items())
735 return cache, tmap, aliases
755 return cache, tmap, aliases
736
756
737 class templater(object):
757 class templater(object):
738
758
739 def __init__(self, filters=None, defaults=None, resources=None,
759 def __init__(self, filters=None, defaults=None, resources=None,
740 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
760 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
741 """Create template engine optionally with preloaded template fragments
761 """Create template engine optionally with preloaded template fragments
742
762
743 - ``filters``: a dict of functions to transform a value into another.
763 - ``filters``: a dict of functions to transform a value into another.
744 - ``defaults``: a dict of symbol values/functions; may be overridden
764 - ``defaults``: a dict of symbol values/functions; may be overridden
745 by a ``mapping`` dict.
765 by a ``mapping`` dict.
746 - ``resources``: a resourcemapper object to look up internal data
766 - ``resources``: a resourcemapper object to look up internal data
747 (e.g. cache), inaccessible from user template.
767 (e.g. cache), inaccessible from user template.
748 - ``cache``: a dict of preloaded template fragments.
768 - ``cache``: a dict of preloaded template fragments.
749 - ``aliases``: a list of alias (name, replacement) pairs.
769 - ``aliases``: a list of alias (name, replacement) pairs.
750
770
751 self.cache may be updated later to register additional template
771 self.cache may be updated later to register additional template
752 fragments.
772 fragments.
753 """
773 """
754 if filters is None:
774 if filters is None:
755 filters = {}
775 filters = {}
756 if defaults is None:
776 if defaults is None:
757 defaults = {}
777 defaults = {}
758 if cache is None:
778 if cache is None:
759 cache = {}
779 cache = {}
760 self.cache = cache.copy()
780 self.cache = cache.copy()
761 self.map = {}
781 self.map = {}
762 self.filters = templatefilters.filters.copy()
782 self.filters = templatefilters.filters.copy()
763 self.filters.update(filters)
783 self.filters.update(filters)
764 self.defaults = defaults
784 self.defaults = defaults
765 self._resources = resources
785 self._resources = resources
766 self._aliases = aliases
786 self._aliases = aliases
767 self.minchunk, self.maxchunk = minchunk, maxchunk
787 self.minchunk, self.maxchunk = minchunk, maxchunk
768 self.ecache = {}
788 self.ecache = {}
769
789
770 @classmethod
790 @classmethod
771 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
791 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
772 cache=None, minchunk=1024, maxchunk=65536):
792 cache=None, minchunk=1024, maxchunk=65536):
773 """Create templater from the specified map file"""
793 """Create templater from the specified map file"""
774 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
794 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
775 cache, tmap, aliases = _readmapfile(mapfile)
795 cache, tmap, aliases = _readmapfile(mapfile)
776 t.cache.update(cache)
796 t.cache.update(cache)
777 t.map = tmap
797 t.map = tmap
778 t._aliases = aliases
798 t._aliases = aliases
779 return t
799 return t
780
800
781 def __contains__(self, key):
801 def __contains__(self, key):
782 return key in self.cache or key in self.map
802 return key in self.cache or key in self.map
783
803
784 def load(self, t):
804 def load(self, t):
785 '''Get the template for the given template name. Use a local cache.'''
805 '''Get the template for the given template name. Use a local cache.'''
786 if t not in self.cache:
806 if t not in self.cache:
787 try:
807 try:
788 self.cache[t] = util.readfile(self.map[t][1])
808 self.cache[t] = util.readfile(self.map[t][1])
789 except KeyError as inst:
809 except KeyError as inst:
790 raise templateutil.TemplateNotFound(
810 raise templateutil.TemplateNotFound(
791 _('"%s" not in template map') % inst.args[0])
811 _('"%s" not in template map') % inst.args[0])
792 except IOError as inst:
812 except IOError as inst:
793 reason = (_('template file %s: %s')
813 reason = (_('template file %s: %s')
794 % (self.map[t][1], util.forcebytestr(inst.args[1])))
814 % (self.map[t][1], util.forcebytestr(inst.args[1])))
795 raise IOError(inst.args[0], encoding.strfromlocal(reason))
815 raise IOError(inst.args[0], encoding.strfromlocal(reason))
796 return self.cache[t]
816 return self.cache[t]
797
817
798 def renderdefault(self, mapping):
818 def renderdefault(self, mapping):
799 """Render the default unnamed template and return result as string"""
819 """Render the default unnamed template and return result as string"""
800 return self.render('', mapping)
820 return self.render('', mapping)
801
821
802 def render(self, t, mapping):
822 def render(self, t, mapping):
803 """Render the specified named template and return result as string"""
823 """Render the specified named template and return result as string"""
804 return templateutil.stringify(self.generate(t, mapping))
824 return templateutil.stringify(self.generate(t, mapping))
805
825
806 def generate(self, t, mapping):
826 def generate(self, t, mapping):
807 """Return a generator that renders the specified named template and
827 """Return a generator that renders the specified named template and
808 yields chunks"""
828 yields chunks"""
809 ttype = t in self.map and self.map[t][0] or 'default'
829 ttype = t in self.map and self.map[t][0] or 'default'
810 if ttype not in self.ecache:
830 if ttype not in self.ecache:
811 try:
831 try:
812 ecls = engines[ttype]
832 ecls = engines[ttype]
813 except KeyError:
833 except KeyError:
814 raise error.Abort(_('invalid template engine: %s') % ttype)
834 raise error.Abort(_('invalid template engine: %s') % ttype)
815 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
835 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
816 self._resources, self._aliases)
836 self._resources, self._aliases)
817 proc = self.ecache[ttype]
837 proc = self.ecache[ttype]
818
838
819 stream = proc.process(t, mapping)
839 stream = proc.process(t, mapping)
820 if self.minchunk:
840 if self.minchunk:
821 stream = util.increasingchunks(stream, min=self.minchunk,
841 stream = util.increasingchunks(stream, min=self.minchunk,
822 max=self.maxchunk)
842 max=self.maxchunk)
823 return stream
843 return stream
824
844
825 def templatepaths():
845 def templatepaths():
826 '''return locations used for template files.'''
846 '''return locations used for template files.'''
827 pathsrel = ['templates']
847 pathsrel = ['templates']
828 paths = [os.path.normpath(os.path.join(util.datapath, f))
848 paths = [os.path.normpath(os.path.join(util.datapath, f))
829 for f in pathsrel]
849 for f in pathsrel]
830 return [p for p in paths if os.path.isdir(p)]
850 return [p for p in paths if os.path.isdir(p)]
831
851
832 def templatepath(name):
852 def templatepath(name):
833 '''return location of template file. returns None if not found.'''
853 '''return location of template file. returns None if not found.'''
834 for p in templatepaths():
854 for p in templatepaths():
835 f = os.path.join(p, name)
855 f = os.path.join(p, name)
836 if os.path.exists(f):
856 if os.path.exists(f):
837 return f
857 return f
838 return None
858 return None
839
859
840 def stylemap(styles, paths=None):
860 def stylemap(styles, paths=None):
841 """Return path to mapfile for a given style.
861 """Return path to mapfile for a given style.
842
862
843 Searches mapfile in the following locations:
863 Searches mapfile in the following locations:
844 1. templatepath/style/map
864 1. templatepath/style/map
845 2. templatepath/map-style
865 2. templatepath/map-style
846 3. templatepath/map
866 3. templatepath/map
847 """
867 """
848
868
849 if paths is None:
869 if paths is None:
850 paths = templatepaths()
870 paths = templatepaths()
851 elif isinstance(paths, bytes):
871 elif isinstance(paths, bytes):
852 paths = [paths]
872 paths = [paths]
853
873
854 if isinstance(styles, bytes):
874 if isinstance(styles, bytes):
855 styles = [styles]
875 styles = [styles]
856
876
857 for style in styles:
877 for style in styles:
858 # only plain name is allowed to honor template paths
878 # only plain name is allowed to honor template paths
859 if (not style
879 if (not style
860 or style in (pycompat.oscurdir, pycompat.ospardir)
880 or style in (pycompat.oscurdir, pycompat.ospardir)
861 or pycompat.ossep in style
881 or pycompat.ossep in style
862 or pycompat.osaltsep and pycompat.osaltsep in style):
882 or pycompat.osaltsep and pycompat.osaltsep in style):
863 continue
883 continue
864 locations = [os.path.join(style, 'map'), 'map-' + style]
884 locations = [os.path.join(style, 'map'), 'map-' + style]
865 locations.append('map')
885 locations.append('map')
866
886
867 for path in paths:
887 for path in paths:
868 for location in locations:
888 for location in locations:
869 mapfile = os.path.join(path, location)
889 mapfile = os.path.join(path, location)
870 if os.path.isfile(mapfile):
890 if os.path.isfile(mapfile):
871 return style, mapfile
891 return style, mapfile
872
892
873 raise RuntimeError("No hgweb templates found in %r" % paths)
893 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,163 +1,173
1 #require serve
1 #require serve
2
2
3 #if no-outer-repo
3 #if no-outer-repo
4
4
5 no repo
5 no repo
6
6
7 $ hg id
7 $ hg id
8 abort: there is no Mercurial repository here (.hg not found)
8 abort: there is no Mercurial repository here (.hg not found)
9 [255]
9 [255]
10
10
11 #endif
11 #endif
12
12
13 create repo
13 create repo
14
14
15 $ hg init test
15 $ hg init test
16 $ cd test
16 $ cd test
17 $ echo a > a
17 $ echo a > a
18 $ hg ci -Ama
18 $ hg ci -Ama
19 adding a
19 adding a
20
20
21 basic id usage
21 basic id usage
22
22
23 $ hg id
23 $ hg id
24 cb9a9f314b8b tip
24 cb9a9f314b8b tip
25 $ hg id --debug
25 $ hg id --debug
26 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b tip
26 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b tip
27 $ hg id -q
27 $ hg id -q
28 cb9a9f314b8b
28 cb9a9f314b8b
29 $ hg id -v
29 $ hg id -v
30 cb9a9f314b8b tip
30 cb9a9f314b8b tip
31
31
32 with options
32 with options
33
33
34 $ hg id -r.
34 $ hg id -r.
35 cb9a9f314b8b tip
35 cb9a9f314b8b tip
36 $ hg id -n
36 $ hg id -n
37 0
37 0
38 $ hg id -t
38 $ hg id -t
39 tip
39 tip
40 $ hg id -b
40 $ hg id -b
41 default
41 default
42 $ hg id -i
42 $ hg id -i
43 cb9a9f314b8b
43 cb9a9f314b8b
44 $ hg id -n -t -b -i
44 $ hg id -n -t -b -i
45 cb9a9f314b8b 0 default tip
45 cb9a9f314b8b 0 default tip
46 $ hg id -Tjson
46 $ hg id -Tjson
47 [
47 [
48 {
48 {
49 "bookmarks": [],
49 "bookmarks": [],
50 "branch": "default",
50 "branch": "default",
51 "dirty": "",
51 "dirty": "",
52 "id": "cb9a9f314b8b",
52 "id": "cb9a9f314b8b",
53 "node": "ffffffffffffffffffffffffffffffffffffffff",
53 "node": "ffffffffffffffffffffffffffffffffffffffff",
54 "parents": [{"node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "rev": 0}],
54 "parents": [{"node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "rev": 0}],
55 "tags": ["tip"]
55 "tags": ["tip"]
56 }
56 }
57 ]
57 ]
58
58
59 test template keywords and functions which require changectx:
59 test template keywords and functions which require changectx:
60
60
61 $ hg id -T '{rev} {node|shortest}\n'
61 $ hg id -T '{rev} {node|shortest}\n'
62 2147483647 ffff
62 2147483647 ffff
63 $ hg id -T '{parents % "{rev} {node|shortest} {desc}\n"}'
63 $ hg id -T '{parents % "{rev} {node|shortest} {desc}\n"}'
64 0 cb9a a
64 0 cb9a a
65
65
66 test nested template: '{tags}'/'{node}' constants shouldn't override the
67 default keywords, but '{id}' persists because there's no default keyword
68 for '{id}' (issue5612)
69
70 $ hg id -T '{tags}\n'
71 tip
72 $ hg id -T '{revset("null:.") % "{rev}:{node|short} {tags} {id}\n"}'
73 -1:000000000000 cb9a9f314b8b
74 0:cb9a9f314b8b tip cb9a9f314b8b
75
66 with modifications
76 with modifications
67
77
68 $ echo b > a
78 $ echo b > a
69 $ hg id -n -t -b -i
79 $ hg id -n -t -b -i
70 cb9a9f314b8b+ 0+ default tip
80 cb9a9f314b8b+ 0+ default tip
71 $ hg id -Tjson
81 $ hg id -Tjson
72 [
82 [
73 {
83 {
74 "bookmarks": [],
84 "bookmarks": [],
75 "branch": "default",
85 "branch": "default",
76 "dirty": "+",
86 "dirty": "+",
77 "id": "cb9a9f314b8b+",
87 "id": "cb9a9f314b8b+",
78 "node": "ffffffffffffffffffffffffffffffffffffffff",
88 "node": "ffffffffffffffffffffffffffffffffffffffff",
79 "parents": [{"node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "rev": 0}],
89 "parents": [{"node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "rev": 0}],
80 "tags": ["tip"]
90 "tags": ["tip"]
81 }
91 }
82 ]
92 ]
83
93
84 other local repo
94 other local repo
85
95
86 $ cd ..
96 $ cd ..
87 $ hg -R test id
97 $ hg -R test id
88 cb9a9f314b8b+ tip
98 cb9a9f314b8b+ tip
89 #if no-outer-repo
99 #if no-outer-repo
90 $ hg id test
100 $ hg id test
91 cb9a9f314b8b+ tip
101 cb9a9f314b8b+ tip
92 #endif
102 #endif
93
103
94 with remote http repo
104 with remote http repo
95
105
96 $ cd test
106 $ cd test
97 $ hg serve -p $HGPORT1 -d --pid-file=hg.pid
107 $ hg serve -p $HGPORT1 -d --pid-file=hg.pid
98 $ cat hg.pid >> $DAEMON_PIDS
108 $ cat hg.pid >> $DAEMON_PIDS
99 $ hg id http://localhost:$HGPORT1/
109 $ hg id http://localhost:$HGPORT1/
100 cb9a9f314b8b
110 cb9a9f314b8b
101
111
102 remote with rev number?
112 remote with rev number?
103
113
104 $ hg id -n http://localhost:$HGPORT1/
114 $ hg id -n http://localhost:$HGPORT1/
105 abort: can't query remote revision number, branch, or tags
115 abort: can't query remote revision number, branch, or tags
106 [255]
116 [255]
107
117
108 remote with tags?
118 remote with tags?
109
119
110 $ hg id -t http://localhost:$HGPORT1/
120 $ hg id -t http://localhost:$HGPORT1/
111 abort: can't query remote revision number, branch, or tags
121 abort: can't query remote revision number, branch, or tags
112 [255]
122 [255]
113
123
114 remote with branch?
124 remote with branch?
115
125
116 $ hg id -b http://localhost:$HGPORT1/
126 $ hg id -b http://localhost:$HGPORT1/
117 abort: can't query remote revision number, branch, or tags
127 abort: can't query remote revision number, branch, or tags
118 [255]
128 [255]
119
129
120 test bookmark support
130 test bookmark support
121
131
122 $ hg bookmark Y
132 $ hg bookmark Y
123 $ hg bookmark Z
133 $ hg bookmark Z
124 $ hg bookmarks
134 $ hg bookmarks
125 Y 0:cb9a9f314b8b
135 Y 0:cb9a9f314b8b
126 * Z 0:cb9a9f314b8b
136 * Z 0:cb9a9f314b8b
127 $ hg id
137 $ hg id
128 cb9a9f314b8b+ tip Y/Z
138 cb9a9f314b8b+ tip Y/Z
129 $ hg id --bookmarks
139 $ hg id --bookmarks
130 Y Z
140 Y Z
131
141
132 test remote identify with bookmarks
142 test remote identify with bookmarks
133
143
134 $ hg id http://localhost:$HGPORT1/
144 $ hg id http://localhost:$HGPORT1/
135 cb9a9f314b8b Y/Z
145 cb9a9f314b8b Y/Z
136 $ hg id --bookmarks http://localhost:$HGPORT1/
146 $ hg id --bookmarks http://localhost:$HGPORT1/
137 Y Z
147 Y Z
138 $ hg id -r . http://localhost:$HGPORT1/
148 $ hg id -r . http://localhost:$HGPORT1/
139 cb9a9f314b8b Y/Z
149 cb9a9f314b8b Y/Z
140 $ hg id --bookmarks -r . http://localhost:$HGPORT1/
150 $ hg id --bookmarks -r . http://localhost:$HGPORT1/
141 Y Z
151 Y Z
142
152
143 test invalid lookup
153 test invalid lookup
144
154
145 $ hg id -r noNoNO http://localhost:$HGPORT1/
155 $ hg id -r noNoNO http://localhost:$HGPORT1/
146 abort: unknown revision 'noNoNO'!
156 abort: unknown revision 'noNoNO'!
147 [255]
157 [255]
148
158
149 Make sure we do not obscure unknown requires file entries (issue2649)
159 Make sure we do not obscure unknown requires file entries (issue2649)
150
160
151 $ echo fake >> .hg/requires
161 $ echo fake >> .hg/requires
152 $ hg id
162 $ hg id
153 abort: repository requires features unknown to this Mercurial: fake!
163 abort: repository requires features unknown to this Mercurial: fake!
154 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
164 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
155 [255]
165 [255]
156
166
157 $ cd ..
167 $ cd ..
158 #if no-outer-repo
168 #if no-outer-repo
159 $ hg id test
169 $ hg id test
160 abort: repository requires features unknown to this Mercurial: fake!
170 abort: repository requires features unknown to this Mercurial: fake!
161 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
171 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
162 [255]
172 [255]
163 #endif
173 #endif
General Comments 0
You need to be logged in to leave comments. Login now