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