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