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