##// END OF EJS Templates
templater: add hook point to populate additional mapping items...
Yuya Nishihara -
r37120:638a2412 default
parent child Browse files
Show More
@@ -1,592 +1,595
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 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 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.verbose = verbose
49 49 ... ui.pushbuffer()
50 50 ... try:
51 51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 52 ... pycompat.byteskwargs(opts)))
53 53 ... finally:
54 54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55 55
56 56 Basic example:
57 57
58 58 >>> def files(ui, fm):
59 59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 60 ... for f in files:
61 61 ... fm.startitem()
62 62 ... fm.write(b'path', b'%s', f[0])
63 63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 65 ... fm.data(size=f[1])
66 66 ... fm.plain(b'\\n')
67 67 ... fm.end()
68 68 >>> show(files)
69 69 foo
70 70 bar
71 71 >>> show(files, verbose=True)
72 72 foo 1970-01-01 00:00:00
73 73 bar 1970-01-01 00:00:01
74 74 >>> show(files, template=b'json')
75 75 [
76 76 {
77 77 "date": [0, 0],
78 78 "path": "foo",
79 79 "size": 123
80 80 },
81 81 {
82 82 "date": [1, 0],
83 83 "path": "bar",
84 84 "size": 456
85 85 }
86 86 ]
87 87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 88 path: foo
89 89 date: 1970-01-01T00:00:00+00:00
90 90 path: bar
91 91 date: 1970-01-01T00:00:01+00:00
92 92
93 93 Nested example:
94 94
95 95 >>> def subrepos(ui, fm):
96 96 ... fm.startitem()
97 97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 98 ... files(ui, fm.nested(b'files'))
99 99 ... fm.end()
100 100 >>> show(subrepos)
101 101 [baz]
102 102 foo
103 103 bar
104 104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 105 baz: foo, bar
106 106 """
107 107
108 108 from __future__ import absolute_import, print_function
109 109
110 110 import collections
111 111 import contextlib
112 112 import itertools
113 113 import os
114 114
115 115 from .i18n import _
116 116 from .node import (
117 117 hex,
118 118 short,
119 119 )
120 120
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 templatefilters,
125 125 templatekw,
126 126 templater,
127 127 templateutil,
128 128 util,
129 129 )
130 130 from .utils import dateutil
131 131
132 132 pickle = util.pickle
133 133
134 134 class _nullconverter(object):
135 135 '''convert non-primitive data types to be processed by formatter'''
136 136
137 137 # set to True if context object should be stored as item
138 138 storecontext = False
139 139
140 140 @staticmethod
141 141 def formatdate(date, fmt):
142 142 '''convert date tuple to appropriate format'''
143 143 return date
144 144 @staticmethod
145 145 def formatdict(data, key, value, fmt, sep):
146 146 '''convert dict or key-value pairs to appropriate dict format'''
147 147 # use plain dict instead of util.sortdict so that data can be
148 148 # serialized as a builtin dict in pickle output
149 149 return dict(data)
150 150 @staticmethod
151 151 def formatlist(data, name, fmt, sep):
152 152 '''convert iterable to appropriate list format'''
153 153 return list(data)
154 154
155 155 class baseformatter(object):
156 156 def __init__(self, ui, topic, opts, converter):
157 157 self._ui = ui
158 158 self._topic = topic
159 159 self._style = opts.get("style")
160 160 self._template = opts.get("template")
161 161 self._converter = converter
162 162 self._item = None
163 163 # function to convert node to string suitable for this output
164 164 self.hexfunc = hex
165 165 def __enter__(self):
166 166 return self
167 167 def __exit__(self, exctype, excvalue, traceback):
168 168 if exctype is None:
169 169 self.end()
170 170 def _showitem(self):
171 171 '''show a formatted item once all data is collected'''
172 172 def startitem(self):
173 173 '''begin an item in the format list'''
174 174 if self._item is not None:
175 175 self._showitem()
176 176 self._item = {}
177 177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
178 178 '''convert date tuple to appropriate format'''
179 179 return self._converter.formatdate(date, fmt)
180 180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
181 181 '''convert dict or key-value pairs to appropriate dict format'''
182 182 return self._converter.formatdict(data, key, value, fmt, sep)
183 183 def formatlist(self, data, name, fmt=None, sep=' '):
184 184 '''convert iterable to appropriate list format'''
185 185 # name is mandatory argument for now, but it could be optional if
186 186 # we have default template keyword, e.g. {item}
187 187 return self._converter.formatlist(data, name, fmt, sep)
188 188 def context(self, **ctxs):
189 189 '''insert context objects to be used to render template keywords'''
190 190 ctxs = pycompat.byteskwargs(ctxs)
191 191 assert all(k in {'ctx', 'fctx'} for k in ctxs)
192 192 if self._converter.storecontext:
193 193 self._item.update(ctxs)
194 194 def data(self, **data):
195 195 '''insert data into item that's not shown in default output'''
196 196 data = pycompat.byteskwargs(data)
197 197 self._item.update(data)
198 198 def write(self, fields, deftext, *fielddata, **opts):
199 199 '''do default text output while assigning data to item'''
200 200 fieldkeys = fields.split()
201 201 assert len(fieldkeys) == len(fielddata)
202 202 self._item.update(zip(fieldkeys, fielddata))
203 203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
204 204 '''do conditional write (primarily for plain formatter)'''
205 205 fieldkeys = fields.split()
206 206 assert len(fieldkeys) == len(fielddata)
207 207 self._item.update(zip(fieldkeys, fielddata))
208 208 def plain(self, text, **opts):
209 209 '''show raw text for non-templated mode'''
210 210 def isplain(self):
211 211 '''check for plain formatter usage'''
212 212 return False
213 213 def nested(self, field):
214 214 '''sub formatter to store nested data in the specified field'''
215 215 self._item[field] = data = []
216 216 return _nestedformatter(self._ui, self._converter, data)
217 217 def end(self):
218 218 '''end output for the formatter'''
219 219 if self._item is not None:
220 220 self._showitem()
221 221
222 222 def nullformatter(ui, topic):
223 223 '''formatter that prints nothing'''
224 224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
225 225
226 226 class _nestedformatter(baseformatter):
227 227 '''build sub items and store them in the parent formatter'''
228 228 def __init__(self, ui, converter, data):
229 229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
230 230 self._data = data
231 231 def _showitem(self):
232 232 self._data.append(self._item)
233 233
234 234 def _iteritems(data):
235 235 '''iterate key-value pairs in stable order'''
236 236 if isinstance(data, dict):
237 237 return sorted(data.iteritems())
238 238 return data
239 239
240 240 class _plainconverter(object):
241 241 '''convert non-primitive data types to text'''
242 242
243 243 storecontext = False
244 244
245 245 @staticmethod
246 246 def formatdate(date, fmt):
247 247 '''stringify date tuple in the given format'''
248 248 return dateutil.datestr(date, fmt)
249 249 @staticmethod
250 250 def formatdict(data, key, value, fmt, sep):
251 251 '''stringify key-value pairs separated by sep'''
252 252 prefmt = pycompat.identity
253 253 if fmt is None:
254 254 fmt = '%s=%s'
255 255 prefmt = pycompat.bytestr
256 256 return sep.join(fmt % (prefmt(k), prefmt(v))
257 257 for k, v in _iteritems(data))
258 258 @staticmethod
259 259 def formatlist(data, name, fmt, sep):
260 260 '''stringify iterable separated by sep'''
261 261 prefmt = pycompat.identity
262 262 if fmt is None:
263 263 fmt = '%s'
264 264 prefmt = pycompat.bytestr
265 265 return sep.join(fmt % prefmt(e) for e in data)
266 266
267 267 class plainformatter(baseformatter):
268 268 '''the default text output scheme'''
269 269 def __init__(self, ui, out, topic, opts):
270 270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
271 271 if ui.debugflag:
272 272 self.hexfunc = hex
273 273 else:
274 274 self.hexfunc = short
275 275 if ui is out:
276 276 self._write = ui.write
277 277 else:
278 278 self._write = lambda s, **opts: out.write(s)
279 279 def startitem(self):
280 280 pass
281 281 def data(self, **data):
282 282 pass
283 283 def write(self, fields, deftext, *fielddata, **opts):
284 284 self._write(deftext % fielddata, **opts)
285 285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
286 286 '''do conditional write'''
287 287 if cond:
288 288 self._write(deftext % fielddata, **opts)
289 289 def plain(self, text, **opts):
290 290 self._write(text, **opts)
291 291 def isplain(self):
292 292 return True
293 293 def nested(self, field):
294 294 # nested data will be directly written to ui
295 295 return self
296 296 def end(self):
297 297 pass
298 298
299 299 class debugformatter(baseformatter):
300 300 def __init__(self, ui, out, topic, opts):
301 301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
302 302 self._out = out
303 303 self._out.write("%s = [\n" % self._topic)
304 304 def _showitem(self):
305 305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
306 306 def end(self):
307 307 baseformatter.end(self)
308 308 self._out.write("]\n")
309 309
310 310 class pickleformatter(baseformatter):
311 311 def __init__(self, ui, out, topic, opts):
312 312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
313 313 self._out = out
314 314 self._data = []
315 315 def _showitem(self):
316 316 self._data.append(self._item)
317 317 def end(self):
318 318 baseformatter.end(self)
319 319 self._out.write(pickle.dumps(self._data))
320 320
321 321 class jsonformatter(baseformatter):
322 322 def __init__(self, ui, out, topic, opts):
323 323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
324 324 self._out = out
325 325 self._out.write("[")
326 326 self._first = True
327 327 def _showitem(self):
328 328 if self._first:
329 329 self._first = False
330 330 else:
331 331 self._out.write(",")
332 332
333 333 self._out.write("\n {\n")
334 334 first = True
335 335 for k, v in sorted(self._item.items()):
336 336 if first:
337 337 first = False
338 338 else:
339 339 self._out.write(",\n")
340 340 u = templatefilters.json(v, paranoid=False)
341 341 self._out.write(' "%s": %s' % (k, u))
342 342 self._out.write("\n }")
343 343 def end(self):
344 344 baseformatter.end(self)
345 345 self._out.write("\n]\n")
346 346
347 347 class _templateconverter(object):
348 348 '''convert non-primitive data types to be processed by templater'''
349 349
350 350 storecontext = True
351 351
352 352 @staticmethod
353 353 def formatdate(date, fmt):
354 354 '''return date tuple'''
355 355 return date
356 356 @staticmethod
357 357 def formatdict(data, key, value, fmt, sep):
358 358 '''build object that can be evaluated as either plain string or dict'''
359 359 data = util.sortdict(_iteritems(data))
360 360 def f():
361 361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
362 362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
363 363 gen=f)
364 364 @staticmethod
365 365 def formatlist(data, name, fmt, sep):
366 366 '''build object that can be evaluated as either plain string or list'''
367 367 data = list(data)
368 368 def f():
369 369 yield _plainconverter.formatlist(data, name, fmt, sep)
370 370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
371 371
372 372 class templateformatter(baseformatter):
373 373 def __init__(self, ui, out, topic, opts):
374 374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
375 375 self._out = out
376 376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
377 377 self._tref = spec.ref
378 378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
379 379 resources=templateresources(ui),
380 380 cache=templatekw.defaulttempl)
381 381 self._parts = templatepartsmap(spec, self._t,
382 382 ['docheader', 'docfooter', 'separator'])
383 383 self._counter = itertools.count()
384 384 self._renderitem('docheader', {})
385 385
386 386 def _showitem(self):
387 387 item = self._item.copy()
388 388 item['index'] = index = next(self._counter)
389 389 if index > 0:
390 390 self._renderitem('separator', {})
391 391 self._renderitem(self._tref, item)
392 392
393 393 def _renderitem(self, part, item):
394 394 if part not in self._parts:
395 395 return
396 396 ref = self._parts[part]
397 397
398 398 props = {}
399 399 # explicitly-defined fields precede templatekw
400 400 props.update(item)
401 401 if 'ctx' in item or 'fctx' in item:
402 402 # but template resources must be always available
403 403 props['revcache'] = {}
404 404 self._out.write(self._t.render(ref, props))
405 405
406 406 def end(self):
407 407 baseformatter.end(self)
408 408 self._renderitem('docfooter', {})
409 409
410 410 templatespec = collections.namedtuple(r'templatespec',
411 411 r'ref tmpl mapfile')
412 412
413 413 def lookuptemplate(ui, topic, tmpl):
414 414 """Find the template matching the given -T/--template spec 'tmpl'
415 415
416 416 'tmpl' can be any of the following:
417 417
418 418 - a literal template (e.g. '{rev}')
419 419 - a map-file name or path (e.g. 'changelog')
420 420 - a reference to [templates] in config file
421 421 - a path to raw template file
422 422
423 423 A map file defines a stand-alone template environment. If a map file
424 424 selected, all templates defined in the file will be loaded, and the
425 425 template matching the given topic will be rendered. Aliases won't be
426 426 loaded from user config, but from the map file.
427 427
428 428 If no map file selected, all templates in [templates] section will be
429 429 available as well as aliases in [templatealias].
430 430 """
431 431
432 432 # looks like a literal template?
433 433 if '{' in tmpl:
434 434 return templatespec('', tmpl, None)
435 435
436 436 # perhaps a stock style?
437 437 if not os.path.split(tmpl)[0]:
438 438 mapname = (templater.templatepath('map-cmdline.' + tmpl)
439 439 or templater.templatepath(tmpl))
440 440 if mapname and os.path.isfile(mapname):
441 441 return templatespec(topic, None, mapname)
442 442
443 443 # perhaps it's a reference to [templates]
444 444 if ui.config('templates', tmpl):
445 445 return templatespec(tmpl, None, None)
446 446
447 447 if tmpl == 'list':
448 448 ui.write(_("available styles: %s\n") % templater.stylelist())
449 449 raise error.Abort(_("specify a template"))
450 450
451 451 # perhaps it's a path to a map or a template
452 452 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
453 453 # is it a mapfile for a style?
454 454 if os.path.basename(tmpl).startswith("map-"):
455 455 return templatespec(topic, None, os.path.realpath(tmpl))
456 456 with util.posixfile(tmpl, 'rb') as f:
457 457 tmpl = f.read()
458 458 return templatespec('', tmpl, None)
459 459
460 460 # constant string?
461 461 return templatespec('', tmpl, None)
462 462
463 463 def templatepartsmap(spec, t, partnames):
464 464 """Create a mapping of {part: ref}"""
465 465 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
466 466 if spec.mapfile:
467 467 partsmap.update((p, p) for p in partnames if p in t)
468 468 elif spec.ref:
469 469 for part in partnames:
470 470 ref = '%s:%s' % (spec.ref, part) # select config sub-section
471 471 if ref in t:
472 472 partsmap[part] = ref
473 473 return partsmap
474 474
475 475 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
476 476 """Create a templater from either a literal template or loading from
477 477 a map file"""
478 478 assert not (spec.tmpl and spec.mapfile)
479 479 if spec.mapfile:
480 480 frommapfile = templater.templater.frommapfile
481 481 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
482 482 cache=cache)
483 483 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
484 484 cache=cache)
485 485
486 486 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
487 487 """Create a templater from a string template 'tmpl'"""
488 488 aliases = ui.configitems('templatealias')
489 489 t = templater.templater(defaults=defaults, resources=resources,
490 490 cache=cache, aliases=aliases)
491 491 t.cache.update((k, templater.unquotestring(v))
492 492 for k, v in ui.configitems('templates'))
493 493 if tmpl:
494 494 t.cache[''] = tmpl
495 495 return t
496 496
497 497 class templateresources(templater.resourcemapper):
498 498 """Resource mapper designed for the default templatekw and function"""
499 499
500 500 def __init__(self, ui, repo=None):
501 501 self._resmap = {
502 502 'cache': {}, # for templatekw/funcs to store reusable data
503 503 'repo': repo,
504 504 'ui': ui,
505 505 }
506 506
507 507 def availablekeys(self, context, mapping):
508 508 return {k for k, g in self._gettermap.iteritems()
509 509 if g(self, context, mapping, k) is not None}
510 510
511 511 def knownkeys(self):
512 512 return self._knownkeys
513 513
514 514 def lookup(self, context, mapping, key):
515 515 get = self._gettermap.get(key)
516 516 if not get:
517 517 return None
518 518 return get(self, context, mapping, key)
519 519
520 def populatemap(self, context, origmapping, newmapping):
521 return {}
522
520 523 def _getsome(self, context, mapping, key):
521 524 v = mapping.get(key)
522 525 if v is not None:
523 526 return v
524 527 return self._resmap.get(key)
525 528
526 529 def _getctx(self, context, mapping, key):
527 530 ctx = mapping.get('ctx')
528 531 if ctx is not None:
529 532 return ctx
530 533 fctx = mapping.get('fctx')
531 534 if fctx is not None:
532 535 return fctx.changectx()
533 536
534 537 def _getrepo(self, context, mapping, key):
535 538 ctx = self._getctx(context, mapping, 'ctx')
536 539 if ctx is not None:
537 540 return ctx.repo()
538 541 return self._getsome(context, mapping, key)
539 542
540 543 _gettermap = {
541 544 'cache': _getsome,
542 545 'ctx': _getctx,
543 546 'fctx': _getsome,
544 547 'repo': _getrepo,
545 548 'revcache': _getsome, # per-ctx cache; set later
546 549 'ui': _getsome,
547 550 }
548 551 _knownkeys = set(_gettermap.keys())
549 552
550 553 def formatter(ui, out, topic, opts):
551 554 template = opts.get("template", "")
552 555 if template == "json":
553 556 return jsonformatter(ui, out, topic, opts)
554 557 elif template == "pickle":
555 558 return pickleformatter(ui, out, topic, opts)
556 559 elif template == "debug":
557 560 return debugformatter(ui, out, topic, opts)
558 561 elif template != "":
559 562 return templateformatter(ui, out, topic, opts)
560 563 # developer config: ui.formatdebug
561 564 elif ui.configbool('ui', 'formatdebug'):
562 565 return debugformatter(ui, out, topic, opts)
563 566 # deprecated config: ui.formatjson
564 567 elif ui.configbool('ui', 'formatjson'):
565 568 return jsonformatter(ui, out, topic, opts)
566 569 return plainformatter(ui, out, topic, opts)
567 570
568 571 @contextlib.contextmanager
569 572 def openformatter(ui, filename, topic, opts):
570 573 """Create a formatter that writes outputs to the specified file
571 574
572 575 Must be invoked using the 'with' statement.
573 576 """
574 577 with util.posixfile(filename, 'wb') as out:
575 578 with formatter(ui, out, topic, opts) as fm:
576 579 yield fm
577 580
578 581 @contextlib.contextmanager
579 582 def _neverending(fm):
580 583 yield fm
581 584
582 585 def maybereopen(fm, filename, opts):
583 586 """Create a formatter backed by file if filename specified, else return
584 587 the given formatter
585 588
586 589 Must be invoked using the 'with' statement. This will never call fm.end()
587 590 of the given formatter.
588 591 """
589 592 if filename:
590 593 return openformatter(fm._ui, filename, fm._topic, opts)
591 594 else:
592 595 return _neverending(fm)
@@ -1,897 +1,914
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 _flatten(thing):
531 531 '''yield a single stream from a possibly nested set of iterators'''
532 532 thing = templateutil.unwraphybrid(thing)
533 533 if isinstance(thing, bytes):
534 534 yield thing
535 535 elif isinstance(thing, str):
536 536 # We can only hit this on Python 3, and it's here to guard
537 537 # against infinite recursion.
538 538 raise error.ProgrammingError('Mercurial IO including templates is done'
539 539 ' with bytes, not strings, got %r' % thing)
540 540 elif thing is None:
541 541 pass
542 542 elif not util.safehasattr(thing, '__iter__'):
543 543 yield pycompat.bytestr(thing)
544 544 else:
545 545 for i in thing:
546 546 i = templateutil.unwraphybrid(i)
547 547 if isinstance(i, bytes):
548 548 yield i
549 549 elif i is None:
550 550 pass
551 551 elif not util.safehasattr(i, '__iter__'):
552 552 yield pycompat.bytestr(i)
553 553 else:
554 554 for j in _flatten(i):
555 555 yield j
556 556
557 557 def unquotestring(s):
558 558 '''unwrap quotes if any; otherwise returns unmodified string'''
559 559 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
560 560 return s
561 561 return s[1:-1]
562 562
563 563 class resourcemapper(object):
564 564 """Mapper of internal template resources"""
565 565
566 566 __metaclass__ = abc.ABCMeta
567 567
568 568 @abc.abstractmethod
569 569 def availablekeys(self, context, mapping):
570 570 """Return a set of available resource keys based on the given mapping"""
571 571
572 572 @abc.abstractmethod
573 573 def knownkeys(self):
574 574 """Return a set of supported resource keys"""
575 575
576 576 @abc.abstractmethod
577 577 def lookup(self, context, mapping, key):
578 578 """Return a resource for the key if available; otherwise None"""
579 579
580 @abc.abstractmethod
581 def populatemap(self, context, origmapping, newmapping):
582 """Return a dict of additional mapping items which should be paired
583 with the given new mapping"""
584
580 585 class nullresourcemapper(resourcemapper):
581 586 def availablekeys(self, context, mapping):
582 587 return set()
583 588
584 589 def knownkeys(self):
585 590 return set()
586 591
587 592 def lookup(self, context, mapping, key):
588 593 return None
589 594
595 def populatemap(self, context, origmapping, newmapping):
596 return {}
597
590 598 class engine(object):
591 599 '''template expansion engine.
592 600
593 601 template expansion works like this. a map file contains key=value
594 602 pairs. if value is quoted, it is treated as string. otherwise, it
595 603 is treated as name of template file.
596 604
597 605 templater is asked to expand a key in map. it looks up key, and
598 606 looks for strings like this: {foo}. it expands {foo} by looking up
599 607 foo in map, and substituting it. expansion is recursive: it stops
600 608 when there is no more {foo} to replace.
601 609
602 610 expansion also allows formatting and filtering.
603 611
604 612 format uses key to expand each item in list. syntax is
605 613 {key%format}.
606 614
607 615 filter uses function to transform value. syntax is
608 616 {key|filter1|filter2|...}.'''
609 617
610 618 def __init__(self, loader, filters=None, defaults=None, resources=None,
611 619 aliases=()):
612 620 self._loader = loader
613 621 if filters is None:
614 622 filters = {}
615 623 self._filters = filters
616 624 self._funcs = templatefuncs.funcs # make this a parameter if needed
617 625 if defaults is None:
618 626 defaults = {}
619 627 if resources is None:
620 628 resources = nullresourcemapper()
621 629 self._defaults = defaults
622 630 self._resources = resources
623 631 self._aliasmap = _aliasrules.buildmap(aliases)
624 632 self._cache = {} # key: (func, data)
625 633
626 634 def overlaymap(self, origmapping, newmapping):
627 635 """Create combined mapping from the original mapping and partial
628 636 mapping to override the original"""
629 637 # do not copy symbols which overrides the defaults depending on
630 638 # new resources, so the defaults will be re-evaluated (issue5612)
631 639 knownres = self._resources.knownkeys()
632 640 newres = self._resources.availablekeys(self, newmapping)
633 641 mapping = {k: v for k, v in origmapping.iteritems()
634 642 if (k in knownres # not a symbol per self.symbol()
635 643 or newres.isdisjoint(self._defaultrequires(k)))}
636 644 mapping.update(newmapping)
645 mapping.update(
646 self._resources.populatemap(self, origmapping, newmapping))
637 647 return mapping
638 648
639 649 def _defaultrequires(self, key):
640 650 """Resource keys required by the specified default symbol function"""
641 651 v = self._defaults.get(key)
642 652 if v is None or not callable(v):
643 653 return ()
644 654 return getattr(v, '_requires', ())
645 655
646 656 def symbol(self, mapping, key):
647 657 """Resolve symbol to value or function; None if nothing found"""
648 658 v = None
649 659 if key not in self._resources.knownkeys():
650 660 v = mapping.get(key)
651 661 if v is None:
652 662 v = self._defaults.get(key)
653 663 return v
654 664
655 665 def resource(self, mapping, key):
656 666 """Return internal data (e.g. cache) used for keyword/function
657 667 evaluation"""
658 668 v = self._resources.lookup(self, mapping, key)
659 669 if v is None:
660 670 raise templateutil.ResourceUnavailable(
661 671 _('template resource not available: %s') % key)
662 672 return v
663 673
664 674 def _load(self, t):
665 675 '''load, parse, and cache a template'''
666 676 if t not in self._cache:
667 677 # put poison to cut recursion while compiling 't'
668 678 self._cache[t] = (_runrecursivesymbol, t)
669 679 try:
670 680 x = parse(self._loader(t))
671 681 if self._aliasmap:
672 682 x = _aliasrules.expand(self._aliasmap, x)
673 683 self._cache[t] = compileexp(x, self, methods)
674 684 except: # re-raises
675 685 del self._cache[t]
676 686 raise
677 687 return self._cache[t]
678 688
679 689 def preload(self, t):
680 690 """Load, parse, and cache the specified template if available"""
681 691 try:
682 692 self._load(t)
683 693 return True
684 694 except templateutil.TemplateNotFound:
685 695 return False
686 696
687 697 def process(self, t, mapping):
688 698 '''Perform expansion. t is name of map element to expand.
689 699 mapping contains added elements for use during expansion. Is a
690 700 generator.'''
691 701 func, data = self._load(t)
702 # populate additional items only if they don't exist in the given
703 # mapping. this is slightly different from overlaymap() because the
704 # initial 'revcache' may contain pre-computed items.
705 extramapping = self._resources.populatemap(self, {}, mapping)
706 if extramapping:
707 extramapping.update(mapping)
708 mapping = extramapping
692 709 return _flatten(func(self, mapping, data))
693 710
694 711 engines = {'default': engine}
695 712
696 713 def stylelist():
697 714 paths = templatepaths()
698 715 if not paths:
699 716 return _('no templates found, try `hg debuginstall` for more info')
700 717 dirlist = os.listdir(paths[0])
701 718 stylelist = []
702 719 for file in dirlist:
703 720 split = file.split(".")
704 721 if split[-1] in ('orig', 'rej'):
705 722 continue
706 723 if split[0] == "map-cmdline":
707 724 stylelist.append(split[1])
708 725 return ", ".join(sorted(stylelist))
709 726
710 727 def _readmapfile(mapfile):
711 728 """Load template elements from the given map file"""
712 729 if not os.path.exists(mapfile):
713 730 raise error.Abort(_("style '%s' not found") % mapfile,
714 731 hint=_("available styles: %s") % stylelist())
715 732
716 733 base = os.path.dirname(mapfile)
717 734 conf = config.config(includepaths=templatepaths())
718 735 conf.read(mapfile, remap={'': 'templates'})
719 736
720 737 cache = {}
721 738 tmap = {}
722 739 aliases = []
723 740
724 741 val = conf.get('templates', '__base__')
725 742 if val and val[0] not in "'\"":
726 743 # treat as a pointer to a base class for this style
727 744 path = util.normpath(os.path.join(base, val))
728 745
729 746 # fallback check in template paths
730 747 if not os.path.exists(path):
731 748 for p in templatepaths():
732 749 p2 = util.normpath(os.path.join(p, val))
733 750 if os.path.isfile(p2):
734 751 path = p2
735 752 break
736 753 p3 = util.normpath(os.path.join(p2, "map"))
737 754 if os.path.isfile(p3):
738 755 path = p3
739 756 break
740 757
741 758 cache, tmap, aliases = _readmapfile(path)
742 759
743 760 for key, val in conf['templates'].items():
744 761 if not val:
745 762 raise error.ParseError(_('missing value'),
746 763 conf.source('templates', key))
747 764 if val[0] in "'\"":
748 765 if val[0] != val[-1]:
749 766 raise error.ParseError(_('unmatched quotes'),
750 767 conf.source('templates', key))
751 768 cache[key] = unquotestring(val)
752 769 elif key != '__base__':
753 770 val = 'default', val
754 771 if ':' in val[1]:
755 772 val = val[1].split(':', 1)
756 773 tmap[key] = val[0], os.path.join(base, val[1])
757 774 aliases.extend(conf['templatealias'].items())
758 775 return cache, tmap, aliases
759 776
760 777 class templater(object):
761 778
762 779 def __init__(self, filters=None, defaults=None, resources=None,
763 780 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
764 781 """Create template engine optionally with preloaded template fragments
765 782
766 783 - ``filters``: a dict of functions to transform a value into another.
767 784 - ``defaults``: a dict of symbol values/functions; may be overridden
768 785 by a ``mapping`` dict.
769 786 - ``resources``: a resourcemapper object to look up internal data
770 787 (e.g. cache), inaccessible from user template.
771 788 - ``cache``: a dict of preloaded template fragments.
772 789 - ``aliases``: a list of alias (name, replacement) pairs.
773 790
774 791 self.cache may be updated later to register additional template
775 792 fragments.
776 793 """
777 794 if filters is None:
778 795 filters = {}
779 796 if defaults is None:
780 797 defaults = {}
781 798 if cache is None:
782 799 cache = {}
783 800 self.cache = cache.copy()
784 801 self.map = {}
785 802 self.filters = templatefilters.filters.copy()
786 803 self.filters.update(filters)
787 804 self.defaults = defaults
788 805 self._resources = resources
789 806 self._aliases = aliases
790 807 self.minchunk, self.maxchunk = minchunk, maxchunk
791 808 self.ecache = {}
792 809
793 810 @classmethod
794 811 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
795 812 cache=None, minchunk=1024, maxchunk=65536):
796 813 """Create templater from the specified map file"""
797 814 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
798 815 cache, tmap, aliases = _readmapfile(mapfile)
799 816 t.cache.update(cache)
800 817 t.map = tmap
801 818 t._aliases = aliases
802 819 return t
803 820
804 821 def __contains__(self, key):
805 822 return key in self.cache or key in self.map
806 823
807 824 def load(self, t):
808 825 '''Get the template for the given template name. Use a local cache.'''
809 826 if t not in self.cache:
810 827 try:
811 828 self.cache[t] = util.readfile(self.map[t][1])
812 829 except KeyError as inst:
813 830 raise templateutil.TemplateNotFound(
814 831 _('"%s" not in template map') % inst.args[0])
815 832 except IOError as inst:
816 833 reason = (_('template file %s: %s')
817 834 % (self.map[t][1],
818 835 stringutil.forcebytestr(inst.args[1])))
819 836 raise IOError(inst.args[0], encoding.strfromlocal(reason))
820 837 return self.cache[t]
821 838
822 839 def renderdefault(self, mapping):
823 840 """Render the default unnamed template and return result as string"""
824 841 return self.render('', mapping)
825 842
826 843 def render(self, t, mapping):
827 844 """Render the specified named template and return result as string"""
828 845 return templateutil.stringify(self.generate(t, mapping))
829 846
830 847 def generate(self, t, mapping):
831 848 """Return a generator that renders the specified named template and
832 849 yields chunks"""
833 850 ttype = t in self.map and self.map[t][0] or 'default'
834 851 if ttype not in self.ecache:
835 852 try:
836 853 ecls = engines[ttype]
837 854 except KeyError:
838 855 raise error.Abort(_('invalid template engine: %s') % ttype)
839 856 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
840 857 self._resources, self._aliases)
841 858 proc = self.ecache[ttype]
842 859
843 860 stream = proc.process(t, mapping)
844 861 if self.minchunk:
845 862 stream = util.increasingchunks(stream, min=self.minchunk,
846 863 max=self.maxchunk)
847 864 return stream
848 865
849 866 def templatepaths():
850 867 '''return locations used for template files.'''
851 868 pathsrel = ['templates']
852 869 paths = [os.path.normpath(os.path.join(util.datapath, f))
853 870 for f in pathsrel]
854 871 return [p for p in paths if os.path.isdir(p)]
855 872
856 873 def templatepath(name):
857 874 '''return location of template file. returns None if not found.'''
858 875 for p in templatepaths():
859 876 f = os.path.join(p, name)
860 877 if os.path.exists(f):
861 878 return f
862 879 return None
863 880
864 881 def stylemap(styles, paths=None):
865 882 """Return path to mapfile for a given style.
866 883
867 884 Searches mapfile in the following locations:
868 885 1. templatepath/style/map
869 886 2. templatepath/map-style
870 887 3. templatepath/map
871 888 """
872 889
873 890 if paths is None:
874 891 paths = templatepaths()
875 892 elif isinstance(paths, bytes):
876 893 paths = [paths]
877 894
878 895 if isinstance(styles, bytes):
879 896 styles = [styles]
880 897
881 898 for style in styles:
882 899 # only plain name is allowed to honor template paths
883 900 if (not style
884 901 or style in (pycompat.oscurdir, pycompat.ospardir)
885 902 or pycompat.ossep in style
886 903 or pycompat.osaltsep and pycompat.osaltsep in style):
887 904 continue
888 905 locations = [os.path.join(style, 'map'), 'map-' + style]
889 906 locations.append('map')
890 907
891 908 for path in paths:
892 909 for location in locations:
893 910 mapfile = os.path.join(path, location)
894 911 if os.path.isfile(mapfile):
895 912 return style, mapfile
896 913
897 914 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now