1# templater.py - template expansion for output 2# 3# Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8"""Slightly complicated template engine for commands and hgweb 9 10This module provides low-level interface to the template engine. See the 11formatter and cmdutil modules if you are looking for high-level functions 12such as ``cmdutil.rendertemplate(ctx, tmpl)``. 13 14Internal Data Types 15------------------- 16 17Template keywords and functions take a dictionary of current symbols and 18resources (a "mapping") and return result. Inputs and outputs must be one 19of the following data types: 20 21bytes 22 a byte string, which is generally a human-readable text in local encoding. 23 24generator 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 29None 30 sometimes represents an empty value, which can be stringified to ''. 31 32True, False, int, float 33 can be stringified as such. 34 35wrappedbytes, wrappedvalue 36 a wrapper for the above printable types. 37 38date 39 represents a (unixtime, offset) tuple. 40 41hybrid 42 represents a list/dict of printable values, which can also be converted 43 to mappings by % operator. 44 45hybriditem 46 represents a scalar printable value, also supports % operator. 47 48revslist 49 represents a list of revision numbers. 50 51mappinggenerator, mappinglist 52 represents mappings (i.e. a list of dicts), which may have default 53 output format. 54 55mappingdict 56 represents a single mapping (i.e. a dict), which may have default output 57 format. 58 59mappingnone 60 represents None of Optional[mappable], which will be mapped to an empty 61 string by % operation. 62 63mappedgenerator 64 a lazily-evaluated list of byte strings, which is e.g. a result of % 65 operation. 66""" 67 68from __future__ import absolute_import, print_function 69 70import abc 71import os 72 73from .i18n import _ 74from .pycompat import ( 75 FileNotFoundError, 76 getattr, 77) 78from . import ( 79 config, 80 encoding, 81 error, 82 parser, 83 pycompat, 84 templatefilters, 85 templatefuncs, 86 templateutil, 87 util, 88) 89from .utils import ( 90 resourceutil, 91 stringutil, 92) 93 94# template parsing 95 96elements = { 97 # token-type: binding-strength, primary, prefix, infix, suffix 98 b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None), 99 b".": (18, None, None, (b".", 18), None), 100 b"%": (15, None, None, (b"%", 15), None), 101 b"|": (15, None, None, (b"|", 15), None), 102 b"*": (5, None, None, (b"*", 5), None), 103 b"/": (5, None, None, (b"/", 5), None), 104 b"+": (4, None, None, (b"+", 4), None), 105 b"-": (4, None, (b"negate", 19), (b"-", 4), None), 106 b"=": (3, None, None, (b"keyvalue", 3), None), 107 b",": (2, None, None, (b"list", 2), None), 108 b")": (0, None, None, None, None), 109 b"integer": (0, b"integer", None, None, None), 110 b"symbol": (0, b"symbol", None, None, None), 111 b"string": (0, b"string", None, None, None), 112 b"template": (0, b"template", None, None, None), 113 b"end": (0, None, None, None, None), 114} 115 116 117def tokenize(program, start, end, term=None): 118 """Parse a template expression into a stream of tokens, which must end 119 with term if specified""" 120 pos = start 121 program = pycompat.bytestr(program) 122 while pos < end: 123 c = program[pos] 124 if c.isspace(): # skip inter-token whitespace 125 pass 126 elif c in b"(=,).%|+-*/": # handle simple operators 127 yield (c, None, pos) 128 elif c in b'"\'': # handle quoted templates 129 s = pos + 1 130 data, pos = _parsetemplate(program, s, end, c) 131 yield (b'template', data, s) 132 pos -= 1 133 elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'): 134 # handle quoted strings 135 c = program[pos + 1] 136 s = pos = pos + 2 137 while pos < end: # find closing quote 138 d = program[pos] 139 if d == b'\\': # skip over escaped characters 140 pos += 2 141 continue 142 if d == c: 143 yield (b'string', program[s:pos], s) 144 break 145 pos += 1 146 else: 147 raise error.ParseError(_(b"unterminated string"), s) 148 elif c.isdigit(): 149 s = pos 150 while pos < end: 151 d = program[pos] 152 if not d.isdigit(): 153 break 154 pos += 1 155 yield (b'integer', program[s:pos], s) 156 pos -= 1 157 elif ( 158 c == b'\\' 159 and program[pos : pos + 2] in (br"\'", br'\"') 160 or c == b'r' 161 and program[pos : pos + 3] in (br"r\'", br'r\"') 162 ): 163 # handle escaped quoted strings for compatibility with 2.9.2-3.4, 164 # where some of nested templates were preprocessed as strings and 165 # then compiled. therefore, \"...\" was allowed. (issue4733) 166 # 167 # processing flow of _evalifliteral() at 5ab28a2e9962: 168 # outer template string -> stringify() -> compiletemplate() 169 # ------------------------ ------------ ------------------ 170 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}] 171 # ~~~~~~~~ 172 # escaped quoted string 173 if c == b'r': 174 pos += 1 175 token = b'string' 176 else: 177 token = b'template' 178 quote = program[pos : pos + 2] 179 s = pos = pos + 2 180 while pos < end: # find closing escaped quote 181 if program.startswith(b'\\\\\\', pos, end): 182 pos += 4 # skip over double escaped characters 183 continue 184 if program.startswith(quote, pos, end): 185 # interpret as if it were a part of an outer string 186 data = parser.unescapestr(program[s:pos]) 187 if token == b'template': 188 data = _parsetemplate(data, 0, len(data))[0] 189 yield (token, data, s) 190 pos += 1 191 break 192 pos += 1 193 else: 194 raise error.ParseError(_(b"unterminated string"), s) 195 elif c.isalnum() or c in b'_': 196 s = pos 197 pos += 1 198 while pos < end: # find end of symbol 199 d = program[pos] 200 if not (d.isalnum() or d == b"_"): 201 break 202 pos += 1 203 sym = program[s:pos] 204 yield (b'symbol', sym, s) 205 pos -= 1 206 elif c == term: 207 yield (b'end', None, pos) 208 return 209 else: 210 raise error.ParseError(_(b"syntax error"), pos) 211 pos += 1 212 if term: 213 raise error.ParseError(_(b"unterminated template expansion"), start) 214 yield (b'end', None, pos) 215 216 217def _parsetemplate(tmpl, start, stop, quote=b''): 218 r""" 219 >>> _parsetemplate(b'foo{bar}"baz', 0, 12) 220 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12) 221 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"') 222 ([('string', 'foo'), ('symbol', 'bar')], 9) 223 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"') 224 ([('string', 'foo')], 4) 225 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"') 226 ([('string', 'foo"'), ('string', 'bar')], 9) 227 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"') 228 ([('string', 'foo\\')], 6) 229 """ 230 parsed = [] 231 for typ, val, pos in _scantemplate(tmpl, start, stop, quote): 232 if typ == b'string': 233 parsed.append((typ, val)) 234 elif typ == b'template': 235 parsed.append(val) 236 elif typ == b'end': 237 return parsed, pos 238 else: 239 raise error.ProgrammingError(b'unexpected type: %s' % typ) 240 raise error.ProgrammingError(b'unterminated scanning of template') 241 242 243def scantemplate(tmpl, raw=False): 244 r"""Scan (type, start, end) positions of outermost elements in template 245 246 If raw=True, a backslash is not taken as an escape character just like 247 r'' string in Python. Note that this is different from r'' literal in 248 template in that no template fragment can appear in r'', e.g. r'{foo}' 249 is a literal '{foo}', but ('{foo}', raw=True) is a template expression 250 'foo'. 251 252 >>> list(scantemplate(b'foo{bar}"baz')) 253 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)] 254 >>> list(scantemplate(b'outer{"inner"}outer')) 255 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)] 256 >>> list(scantemplate(b'foo\\{escaped}')) 257 [('string', 0, 5), ('string', 5, 13)] 258 >>> list(scantemplate(b'foo\\{escaped}', raw=True)) 259 [('string', 0, 4), ('template', 4, 13)] 260 """ 261 last = None 262 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw): 263 if last: 264 yield last + (pos,) 265 if typ == b'end': 266 return 267 else: 268 last = (typ, pos) 269 raise error.ProgrammingError(b'unterminated scanning of template') 270 271 272def _scantemplate(tmpl, start, stop, quote=b'', raw=False): 273 """Parse template string into chunks of strings and template expressions""" 274 sepchars = b'{' + quote 275 unescape = [parser.unescapestr, pycompat.identity][raw] 276 pos = start 277 p = parser.parser(elements) 278 try: 279 while pos < stop: 280 n = min( 281 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)), 282 key=lambda n: (n < 0, n), 283 ) 284 if n < 0: 285 yield (b'string', unescape(tmpl[pos:stop]), pos) 286 pos = stop 287 break 288 c = tmpl[n : n + 1] 289 bs = 0 # count leading backslashes 290 if not raw: 291 bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\')) 292 if bs % 2 == 1: 293 # escaped (e.g. '\{', '\\\{', but not '\\{') 294 yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos) 295 pos = n + 1 296 continue 297 if n > pos: 298 yield (b'string', unescape(tmpl[pos:n]), pos) 299 if c == quote: 300 yield (b'end', None, n + 1) 301 return 302 303 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}')) 304 if not tmpl.startswith(b'}', pos): 305 raise error.ParseError(_(b"invalid token"), pos) 306 yield (b'template', parseres, n) 307 pos += 1 308 309 if quote: 310 raise error.ParseError(_(b"unterminated string"), start) 311 except error.ParseError as inst: 312 _addparseerrorhint(inst, tmpl) 313 raise 314 yield (b'end', None, pos) 315 316 317def _addparseerrorhint(inst, tmpl): 318 if inst.location is None: 319 return 320 loc = inst.location 321 # Offset the caret location by the number of newlines before the 322 # location of the error, since we will replace one-char newlines 323 # with the two-char literal r'\n'. 324 offset = tmpl[:loc].count(b'\n') 325 tmpl = tmpl.replace(b'\n', br'\n') 326 # We want the caret to point to the place in the template that 327 # failed to parse, but in a hint we get a open paren at the 328 # start. Therefore, we print "loc + 1" spaces (instead of "loc") 329 # to line up the caret with the location of the error. 330 inst.hint = tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here') 331 332 333def _unnesttemplatelist(tree): 334 """Expand list of templates to node tuple 335 336 >>> def f(tree): 337 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree)))) 338 >>> f((b'template', [])) 339 (string '') 340 >>> f((b'template', [(b'string', b'foo')])) 341 (string 'foo') 342 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')])) 343 (template 344 (string 'foo') 345 (symbol 'rev')) 346 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str 347 (template 348 (symbol 'rev')) 349 >>> f((b'template', [(b'template', [(b'string', b'foo')])])) 350 (string 'foo') 351 """ 352 if not isinstance(tree, tuple): 353 return tree 354 op = tree[0] 355 if op != b'template': 356 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:]) 357 358 assert len(tree) == 2 359 xs = tuple(_unnesttemplatelist(x) for x in tree[1]) 360 if not xs: 361 return (b'string', b'') # empty template "" 362 elif len(xs) == 1 and xs[0][0] == b'string': 363 return xs[0] # fast path for string with no template fragment "x" 364 else: 365 return (op,) + xs 366 367 368def parse(tmpl): 369 """Parse template string into tree""" 370 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl)) 371 assert pos == len(tmpl), b'unquoted template should be consumed' 372 return _unnesttemplatelist((b'template', parsed)) 373 374 375def parseexpr(expr): 376 """Parse a template expression into tree 377 378 >>> parseexpr(b'"foo"') 379 ('string', 'foo') 380 >>> parseexpr(b'foo(bar)') 381 ('func', ('symbol', 'foo'), ('symbol', 'bar')) 382 >>> from . import error 383 >>> from . import pycompat 384 >>> try: 385 ... parseexpr(b'foo(') 386 ... except error.ParseError as e: 387 ... pycompat.sysstr(e.message) 388 ... e.location 389 'not a prefix: end' 390 4 391 >>> try: 392 ... parseexpr(b'"foo" "bar"') 393 ... except error.ParseError as e: 394 ... pycompat.sysstr(e.message) 395 ... e.location 396 'invalid token' 397 7 398 """ 399 try: 400 return _parseexpr(expr) 401 except error.ParseError as inst: 402 _addparseerrorhint(inst, expr) 403 raise 404 405 406def _parseexpr(expr): 407 p = parser.parser(elements) 408 tree, pos = p.parse(tokenize(expr, 0, len(expr))) 409 if pos != len(expr): 410 raise error.ParseError(_(b'invalid token'), pos) 411 return _unnesttemplatelist(tree) 412 413 414def prettyformat(tree): 415 return parser.prettyformat(tree, (b'integer', b'string', b'symbol')) 416 417 418def compileexp(exp, context, curmethods): 419 """Compile parsed template tree to (func, data) pair""" 420 if not exp: 421 raise error.ParseError(_(b"missing argument")) 422 t = exp[0] 423 return curmethods[t](exp, context) 424 425 426# template evaluation 427 428 429def getsymbol(exp): 430 if exp[0] == b'symbol': 431 return exp[1] 432 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0]) 433 434 435def getlist(x): 436 if not x: 437 return [] 438 if x[0] == b'list': 439 return getlist(x[1]) + [x[2]] 440 return [x] 441 442 443def gettemplate(exp, context): 444 """Compile given template tree or load named template from map file; 445 returns (func, data) pair""" 446 if exp[0] in (b'template', b'string'): 447 return compileexp(exp, context, methods) 448 if exp[0] == b'symbol': 449 # unlike runsymbol(), here 'symbol' is always taken as template name 450 # even if it exists in mapping. this allows us to override mapping 451 # by web templates, e.g. 'changelogtag' is redefined in map file. 452 return context._load(exp[1]) 453 raise error.ParseError(_(b"expected template specifier")) 454 455 456def _runrecursivesymbol(context, mapping, key): 457 raise error.InputError(_(b"recursive reference '%s' in template") % key) 458 459 460def buildtemplate(exp, context): 461 ctmpl = [compileexp(e, context, methods) for e in exp[1:]] 462 return (templateutil.runtemplate, ctmpl) 463 464 465def buildfilter(exp, context): 466 n = getsymbol(exp[2]) 467 if n in context._filters: 468 filt = context._filters[n] 469 arg = compileexp(exp[1], context, methods) 470 return (templateutil.runfilter, (arg, filt)) 471 if n in context._funcs: 472 f = context._funcs[n] 473 args = _buildfuncargs(exp[1], context, methods, n, f._argspec) 474 return (f, args) 475 raise error.ParseError(_(b"unknown function '%s'") % n) 476 477 478def buildmap(exp, context): 479 darg = compileexp(exp[1], context, methods) 480 targ = gettemplate(exp[2], context) 481 return (templateutil.runmap, (darg, targ)) 482 483 484def buildmember(exp, context): 485 darg = compileexp(exp[1], context, methods) 486 memb = getsymbol(exp[2]) 487 return (templateutil.runmember, (darg, memb)) 488 489 490def buildnegate(exp, context): 491 arg = compileexp(exp[1], context, exprmethods) 492 return (templateutil.runnegate, arg) 493 494 495def buildarithmetic(exp, context, func): 496 left = compileexp(exp[1], context, exprmethods) 497 right = compileexp(exp[2], context, exprmethods) 498 return (templateutil.runarithmetic, (func, left, right)) 499 500 501def buildfunc(exp, context): 502 n = getsymbol(exp[1]) 503 if n in context._funcs: 504 f = context._funcs[n] 505 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec) 506 return (f, args) 507 if n in context._filters: 508 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None) 509 if len(args) != 1: 510 raise error.ParseError(_(b"filter %s expects one argument") % n) 511 f = context._filters[n] 512 return (templateutil.runfilter, (args[0], f)) 513 raise error.ParseError(_(b"unknown function '%s'") % n) 514 515 516def _buildfuncargs(exp, context, curmethods, funcname, argspec): 517 """Compile parsed tree of function arguments into list or dict of 518 (func, data) pairs 519 520 >>> context = engine(lambda t: (templateutil.runsymbol, t)) 521 >>> def fargs(expr, argspec): 522 ... x = _parseexpr(expr) 523 ... n = getsymbol(x[1]) 524 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec) 525 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys()) 526 ['l', 'k'] 527 >>> args = fargs(b'a(opts=1, k=2)', b'**opts') 528 >>> list(args.keys()), list(args[b'opts'].keys()) 529 (['opts'], ['opts', 'k']) 530 """ 531 532 def compiledict(xs): 533 return util.sortdict( 534 (k, compileexp(x, context, curmethods)) 535 for k, x in pycompat.iteritems(xs) 536 ) 537 538 def compilelist(xs): 539 return [compileexp(x, context, curmethods) for x in xs] 540 541 if not argspec: 542 # filter or function with no argspec: return list of positional args 543 return compilelist(getlist(exp)) 544 545 # function with argspec: return dict of named args 546 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec) 547 treeargs = parser.buildargsdict( 548 getlist(exp), 549 funcname, 550 argspec, 551 keyvaluenode=b'keyvalue', 552 keynode=b'symbol', 553 ) 554 compargs = util.sortdict() 555 if varkey: 556 compargs[varkey] = compilelist(treeargs.pop(varkey)) 557 if optkey: 558 compargs[optkey] = compiledict(treeargs.pop(optkey)) 559 compargs.update(compiledict(treeargs)) 560 return compargs 561 562 563def buildkeyvaluepair(exp, content): 564 raise error.ParseError(_(b"can't use a key-value pair in this context")) 565 566 567def buildlist(exp, context): 568 raise error.ParseError( 569 _(b"can't use a list in this context"), 570 hint=_(b'check place of comma and parens'), 571 ) 572 573 574# methods to interpret function arguments or inner expressions (e.g. {_(x)}) 575exprmethods = { 576 b"integer": lambda e, c: (templateutil.runinteger, e[1]), 577 b"string": lambda e, c: (templateutil.runstring, e[1]), 578 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]), 579 b"template": buildtemplate, 580 b"group": lambda e, c: compileexp(e[1], c, exprmethods), 581 b".": buildmember, 582 b"|": buildfilter, 583 b"%": buildmap, 584 b"func": buildfunc, 585 b"keyvalue": buildkeyvaluepair, 586 b"list": buildlist, 587 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b), 588 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b), 589 b"negate": buildnegate, 590 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b), 591 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b), 592} 593 594# methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"}) 595methods = exprmethods.copy() 596methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable 597 598 599class _aliasrules(parser.basealiasrules): 600 """Parsing and expansion rule set of template aliases""" 601 602 _section = _(b'template alias') 603 _parse = staticmethod(_parseexpr) 604 605 @staticmethod 606 def _trygetfunc(tree): 607 """Return (name, args) if tree is func(...) or ...|filter; otherwise 608 None""" 609 if tree[0] == b'func' and tree[1][0] == b'symbol': 610 return tree[1][1], getlist(tree[2]) 611 if tree[0] == b'|' and tree[2][0] == b'symbol': 612 return tree[2][1], [tree[1]] 613 614 615def expandaliases(tree, aliases): 616 """Return new tree of aliases are expanded""" 617 aliasmap = _aliasrules.buildmap(aliases) 618 return _aliasrules.expand(aliasmap, tree) 619 620 621# template engine 622 623 624def unquotestring(s): 625 '''unwrap quotes if any; otherwise returns unmodified string''' 626 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]: 627 return s 628 return s[1:-1] 629 630 631class resourcemapper(object): # pytype: disable=ignored-metaclass 632 """Mapper of internal template resources""" 633 634 __metaclass__ = abc.ABCMeta 635 636 @abc.abstractmethod 637 def availablekeys(self, mapping): 638 """Return a set of available resource keys based on the given mapping""" 639 640 @abc.abstractmethod 641 def knownkeys(self): 642 """Return a set of supported resource keys""" 643 644 @abc.abstractmethod 645 def lookup(self, mapping, key): 646 """Return a resource for the key if available; otherwise None""" 647 648 @abc.abstractmethod 649 def populatemap(self, context, origmapping, newmapping): 650 """Return a dict of additional mapping items which should be paired 651 with the given new mapping""" 652 653 654class nullresourcemapper(resourcemapper): 655 def availablekeys(self, mapping): 656 return set() 657 658 def knownkeys(self): 659 return set() 660 661 def lookup(self, mapping, key): 662 return None 663 664 def populatemap(self, context, origmapping, newmapping): 665 return {} 666 667 668class engine(object): 669 """template expansion engine. 670 671 template expansion works like this. a map file contains key=value 672 pairs. if value is quoted, it is treated as string. otherwise, it 673 is treated as name of template file. 674 675 templater is asked to expand a key in map. it looks up key, and 676 looks for strings like this: {foo}. it expands {foo} by looking up 677 foo in map, and substituting it. expansion is recursive: it stops 678 when there is no more {foo} to replace. 679 680 expansion also allows formatting and filtering. 681 682 format uses key to expand each item in list. syntax is 683 {key%format}. 684 685 filter uses function to transform value. syntax is 686 {key|filter1|filter2|...}.""" 687 688 def __init__(self, loader, filters=None, defaults=None, resources=None): 689 self._loader = loader 690 if filters is None: 691 filters = {} 692 self._filters = filters 693 self._funcs = templatefuncs.funcs # make this a parameter if needed 694 if defaults is None: 695 defaults = {} 696 if resources is None: 697 resources = nullresourcemapper() 698 self._defaults = defaults 699 self._resources = resources 700 self._cache = {} # key: (func, data) 701 self._tmplcache = {} # literal template: (func, data) 702 703 def overlaymap(self, origmapping, newmapping): 704 """Create combined mapping from the original mapping and partial 705 mapping to override the original""" 706 # do not copy symbols which overrides the defaults depending on 707 # new resources, so the defaults will be re-evaluated (issue5612) 708 knownres = self._resources.knownkeys() 709 newres = self._resources.availablekeys(newmapping) 710 mapping = { 711 k: v 712 for k, v in pycompat.iteritems(origmapping) 713 if ( 714 k in knownres # not a symbol per self.symbol() 715 or newres.isdisjoint(self._defaultrequires(k)) 716 ) 717 } 718 mapping.update(newmapping) 719 mapping.update( 720 self._resources.populatemap(self, origmapping, newmapping) 721 ) 722 return mapping 723 724 def _defaultrequires(self, key): 725 """Resource keys required by the specified default symbol function""" 726 v = self._defaults.get(key) 727 if v is None or not callable(v): 728 return () 729 return getattr(v, '_requires', ()) 730 731 def symbol(self, mapping, key): 732 """Resolve symbol to value or function; None if nothing found""" 733 v = None 734 if key not in self._resources.knownkeys(): 735 v = mapping.get(key) 736 if v is None: 737 v = self._defaults.get(key) 738 return v 739 740 def availableresourcekeys(self, mapping): 741 """Return a set of available resource keys based on the given mapping""" 742 return self._resources.availablekeys(mapping) 743 744 def knownresourcekeys(self): 745 """Return a set of supported resource keys""" 746 return self._resources.knownkeys() 747 748 def resource(self, mapping, key): 749 """Return internal data (e.g. cache) used for keyword/function 750 evaluation""" 751 v = self._resources.lookup(mapping, key) 752 if v is None: 753 raise templateutil.ResourceUnavailable( 754 _(b'template resource not available: %s') % key 755 ) 756 return v 757 758 def _load(self, t): 759 '''load, parse, and cache a template''' 760 if t not in self._cache: 761 x = self._loader(t) 762 # put poison to cut recursion while compiling 't' 763 self._cache[t] = (_runrecursivesymbol, t) 764 try: 765 self._cache[t] = compileexp(x, self, methods) 766 except: # re-raises 767 del self._cache[t] 768 raise 769 return self._cache[t] 770 771 def _parse(self, tmpl): 772 """Parse and cache a literal template""" 773 if tmpl not in self._tmplcache: 774 x = parse(tmpl) 775 self._tmplcache[tmpl] = compileexp(x, self, methods) 776 return self._tmplcache[tmpl] 777 778 def preload(self, t): 779 """Load, parse, and cache the specified template if available""" 780 try: 781 self._load(t) 782 return True 783 except templateutil.TemplateNotFound: 784 return False 785 786 def process(self, t, mapping): 787 """Perform expansion. t is name of map element to expand. 788 mapping contains added elements for use during expansion. Is a 789 generator.""" 790 func, data = self._load(t) 791 return self._expand(func, data, mapping) 792 793 def expand(self, tmpl, mapping): 794 """Perform expansion over a literal template 795 796 No user aliases will be expanded since this is supposed to be called 797 with an internal template string. 798 """ 799 func, data = self._parse(tmpl) 800 return self._expand(func, data, mapping) 801 802 def _expand(self, func, data, mapping): 803 # populate additional items only if they don't exist in the given 804 # mapping. this is slightly different from overlaymap() because the 805 # initial 'revcache' may contain pre-computed items. 806 extramapping = self._resources.populatemap(self, {}, mapping) 807 if extramapping: 808 extramapping.update(mapping) 809 mapping = extramapping 810 return templateutil.flatten(self, mapping, func(self, mapping, data)) 811 812 813def stylelist(): 814 path = templatedir() 815 if not path: 816 return _(b'no templates found, try `hg debuginstall` for more info') 817 dirlist = os.listdir(path) 818 stylelist = [] 819 for file in dirlist: 820 split = file.split(b".") 821 if split[-1] in (b'orig', b'rej'): 822 continue 823 if split[0] == b"map-cmdline": 824 stylelist.append(split[1]) 825 return b", ".join(sorted(stylelist)) 826 827 828def _open_mapfile(mapfile): 829 if os.path.exists(mapfile): 830 return util.posixfile(mapfile, b'rb') 831 raise error.Abort( 832 _(b"style '%s' not found") % mapfile, 833 hint=_(b"available styles: %s") % stylelist(), 834 ) 835 836 837def _readmapfile(fp, mapfile): 838 """Load template elements from the given map file""" 839 if pycompat.iswindows: 840 # quick hack to make sure we can process '/' in the code dealing with 841 # ressource. Ideally we would make sure we use `/` instead of `ossep` 842 # in the templater code, but that seems a bigger and less certain 843 # change that we better left for the default branch. 844 name_paths = mapfile.split(pycompat.ossep) 845 mapfile = b'/'.join(name_paths) 846 base = os.path.dirname(mapfile) 847 conf = config.config() 848 849 def include(rel, remap, sections): 850 subresource = None 851 if base: 852 abs = os.path.normpath(os.path.join(base, rel)) 853 if os.path.isfile(abs): 854 subresource = util.posixfile(abs, b'rb') 855 if not subresource: 856 if pycompat.ossep not in rel: 857 abs = rel 858 try: 859 subresource = resourceutil.open_resource( 860 b'mercurial.templates', rel 861 ) 862 except FileNotFoundError: 863 subresource = None 864 else: 865 dir = templatedir() 866 if dir: 867 abs = os.path.normpath(os.path.join(dir, rel)) 868 if os.path.isfile(abs): 869 subresource = util.posixfile(abs, b'rb') 870 if subresource: 871 data = subresource.read() 872 conf.parse( 873 abs, 874 data, 875 sections=sections, 876 remap=remap, 877 include=include, 878 ) 879 880 data = fp.read() 881 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include) 882 883 cache = {} 884 tmap = {} 885 aliases = [] 886 887 val = conf.get(b'templates', b'__base__') 888 if val and val[0] not in b"'\"": 889 # treat as a pointer to a base class for this style 890 path = os.path.normpath(os.path.join(base, val)) 891 892 # fallback check in template paths 893 if not os.path.exists(path): 894 dir = templatedir() 895 if dir is not None: 896 p2 = os.path.normpath(os.path.join(dir, val)) 897 if os.path.isfile(p2): 898 path = p2 899 else: 900 p3 = os.path.normpath(os.path.join(p2, b"map")) 901 if os.path.isfile(p3): 902 path = p3 903 904 fp = _open_mapfile(path) 905 cache, tmap, aliases = _readmapfile(fp, path) 906 907 for key, val in conf.items(b'templates'): 908 if not val: 909 raise error.ParseError( 910 _(b'missing value'), conf.source(b'templates', key) 911 ) 912 if val[0] in b"'\"": 913 if val[0] != val[-1]: 914 raise error.ParseError( 915 _(b'unmatched quotes'), conf.source(b'templates', key) 916 ) 917 cache[key] = unquotestring(val) 918 elif key != b'__base__': 919 tmap[key] = os.path.join(base, val) 920 aliases.extend(conf.items(b'templatealias')) 921 return cache, tmap, aliases 922 923 924class loader(object): 925 """Load template fragments optionally from a map file""" 926 927 def __init__(self, cache, aliases): 928 if cache is None: 929 cache = {} 930 self.cache = cache.copy() 931 self._map = {} 932 self._aliasmap = _aliasrules.buildmap(aliases) 933 934 def __contains__(self, key): 935 return key in self.cache or key in self._map 936 937 def load(self, t): 938 """Get parsed tree for the given template name. Use a local cache.""" 939 if t not in self.cache: 940 try: 941 mapfile, fp = open_template(self._map[t]) 942 self.cache[t] = fp.read() 943 except KeyError as inst: 944 raise templateutil.TemplateNotFound( 945 _(b'"%s" not in template map') % inst.args[0] 946 ) 947 except IOError as inst: 948 reason = _(b'template file %s: %s') % ( 949 self._map[t], 950 stringutil.forcebytestr(inst.args[1]), 951 ) 952 raise IOError(inst.args[0], encoding.strfromlocal(reason)) 953 return self._parse(self.cache[t]) 954 955 def _parse(self, tmpl): 956 x = parse(tmpl) 957 if self._aliasmap: 958 x = _aliasrules.expand(self._aliasmap, x) 959 return x 960 961 def _findsymbolsused(self, tree, syms): 962 if not tree: 963 return 964 op = tree[0] 965 if op == b'symbol': 966 s = tree[1] 967 if s in syms[0]: 968 return # avoid recursion: s -> cache[s] -> s 969 syms[0].add(s) 970 if s in self.cache or s in self._map: 971 # s may be a reference for named template 972 self._findsymbolsused(self.load(s), syms) 973 return 974 if op in {b'integer', b'string'}: 975 return 976 # '{arg|func}' == '{func(arg)}' 977 if op == b'|': 978 syms[1].add(getsymbol(tree[2])) 979 self._findsymbolsused(tree[1], syms) 980 return 981 if op == b'func': 982 syms[1].add(getsymbol(tree[1])) 983 self._findsymbolsused(tree[2], syms) 984 return 985 for x in tree[1:]: 986 self._findsymbolsused(x, syms) 987 988 def symbolsused(self, t): 989 """Look up (keywords, filters/functions) referenced from the name 990 template 't' 991 992 This may load additional templates from the map file. 993 """ 994 syms = (set(), set()) 995 self._findsymbolsused(self.load(t), syms) 996 return syms 997 998 999class templater(object): 1000 def __init__( 1001 self, 1002 filters=None, 1003 defaults=None, 1004 resources=None, 1005 cache=None, 1006 aliases=(), 1007 minchunk=1024, 1008 maxchunk=65536, 1009 ): 1010 """Create template engine optionally with preloaded template fragments 1011 1012 - ``filters``: a dict of functions to transform a value into another. 1013 - ``defaults``: a dict of symbol values/functions; may be overridden 1014 by a ``mapping`` dict. 1015 - ``resources``: a resourcemapper object to look up internal data 1016 (e.g. cache), inaccessible from user template. 1017 - ``cache``: a dict of preloaded template fragments. 1018 - ``aliases``: a list of alias (name, replacement) pairs. 1019 1020 self.cache may be updated later to register additional template 1021 fragments. 1022 """ 1023 allfilters = templatefilters.filters.copy() 1024 if filters: 1025 allfilters.update(filters) 1026 self._loader = loader(cache, aliases) 1027 self._proc = engine(self._loader.load, allfilters, defaults, resources) 1028 self._minchunk, self._maxchunk = minchunk, maxchunk 1029 1030 @classmethod 1031 def frommapfile( 1032 cls, 1033 mapfile, 1034 fp=None, 1035 filters=None, 1036 defaults=None, 1037 resources=None, 1038 cache=None, 1039 minchunk=1024, 1040 maxchunk=65536, 1041 ): 1042 """Create templater from the specified map file""" 1043 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk) 1044 if not fp: 1045 fp = _open_mapfile(mapfile) 1046 cache, tmap, aliases = _readmapfile(fp, mapfile) 1047 t._loader.cache.update(cache) 1048 t._loader._map = tmap 1049 t._loader._aliasmap = _aliasrules.buildmap(aliases) 1050 return t 1051 1052 def __contains__(self, key): 1053 return key in self._loader 1054 1055 @property 1056 def cache(self): 1057 return self._loader.cache 1058 1059 # for highlight extension to insert one-time 'colorize' filter 1060 @property 1061 def _filters(self): 1062 return self._proc._filters 1063 1064 @property 1065 def defaults(self): 1066 return self._proc._defaults 1067 1068 def load(self, t): 1069 """Get parsed tree for the given template name. Use a local cache.""" 1070 return self._loader.load(t) 1071 1072 def symbolsuseddefault(self): 1073 """Look up (keywords, filters/functions) referenced from the default 1074 unnamed template 1075 1076 This may load additional templates from the map file. 1077 """ 1078 return self.symbolsused(b'') 1079 1080 def symbolsused(self, t): 1081 """Look up (keywords, filters/functions) referenced from the name 1082 template 't' 1083 1084 This may load additional templates from the map file. 1085 """ 1086 return self._loader.symbolsused(t) 1087 1088 def renderdefault(self, mapping): 1089 """Render the default unnamed template and return result as string""" 1090 return self.render(b'', mapping) 1091 1092 def render(self, t, mapping): 1093 """Render the specified named template and return result as string""" 1094 return b''.join(self.generate(t, mapping)) 1095 1096 def generate(self, t, mapping): 1097 """Return a generator that renders the specified named template and 1098 yields chunks""" 1099 stream = self._proc.process(t, mapping) 1100 if self._minchunk: 1101 stream = util.increasingchunks( 1102 stream, min=self._minchunk, max=self._maxchunk 1103 ) 1104 return stream 1105 1106 1107def templatedir(): 1108 '''return the directory used for template files, or None.''' 1109 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates')) 1110 return path if os.path.isdir(path) else None 1111 1112 1113def open_template(name, templatepath=None): 1114 """returns a file-like object for the given template, and its full path 1115 1116 If the name is a relative path and we're in a frozen binary, the template 1117 will be read from the mercurial.templates package instead. The returned path 1118 will then be the relative path. 1119 """ 1120 # Does the name point directly to a map file? 1121 if os.path.isfile(name) or os.path.isabs(name): 1122 return name, open(name, mode='rb') 1123 1124 # Does the name point to a template in the provided templatepath, or 1125 # in mercurial/templates/ if no path was provided? 1126 if templatepath is None: 1127 templatepath = templatedir() 1128 if templatepath is not None: 1129 f = os.path.join(templatepath, name) 1130 return f, open(f, mode='rb') 1131 1132 # Otherwise try to read it using the resources API 1133 if pycompat.iswindows: 1134 # quick hack to make sure we can process '/' in the code dealing with 1135 # ressource. Ideally we would make sure we use `/` instead of `ossep` 1136 # in the templater code, but that seems a bigger and less certain 1137 # change that we better left for the default branch. 1138 name_paths = name.split(pycompat.ossep) 1139 name = b'/'.join(name_paths) 1140 name_parts = name.split(b'/') 1141 package_name = b'.'.join([b'mercurial', b'templates'] + name_parts[:-1]) 1142 return ( 1143 name, 1144 resourceutil.open_resource(package_name, name_parts[-1]), 1145 ) 1146 1147 1148def try_open_template(name, templatepath=None): 1149 try: 1150 return open_template(name, templatepath) 1151 except (EnvironmentError, ImportError): 1152 return None, None 1153