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