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