##// END OF EJS Templates
templater: factor out thin helper that evaluates argument as string...
Yuya Nishihara -
r28348:ccedb17a default
parent child Browse files
Show More
@@ -1,680 +1,678 b''
1 1 # color.py color output for Mercurial commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.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 '''colorize output from some commands
9 9
10 10 The color extension colorizes output from several Mercurial commands.
11 11 For example, the diff command shows additions in green and deletions
12 12 in red, while the status command shows modified files in magenta. Many
13 13 other commands have analogous colors. It is possible to customize
14 14 these colors.
15 15
16 16 Effects
17 17 -------
18 18
19 19 Other effects in addition to color, like bold and underlined text, are
20 20 also available. By default, the terminfo database is used to find the
21 21 terminal codes used to change color and effect. If terminfo is not
22 22 available, then effects are rendered with the ECMA-48 SGR control
23 23 function (aka ANSI escape codes).
24 24
25 25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 28 'underline'. How each is rendered depends on the terminal emulator.
29 29 Some may not be available for a given terminal type, and will be
30 30 silently ignored.
31 31
32 32 Labels
33 33 ------
34 34
35 35 Text receives color effects depending on the labels that it has. Many
36 36 default Mercurial commands emit labelled text. You can also define
37 37 your own labels in templates using the label function, see :hg:`help
38 38 templates`. A single portion of text may have more than one label. In
39 39 that case, effects given to the last label will override any other
40 40 effects. This includes the special "none" effect, which nullifies
41 41 other effects.
42 42
43 43 Labels are normally invisible. In order to see these labels and their
44 44 position in the text, use the global --color=debug option. The same
45 45 anchor text may be associated to multiple labels, e.g.
46 46
47 47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
48 48
49 49 The following are the default effects for some default labels. Default
50 50 effects may be overridden from your configuration file::
51 51
52 52 [color]
53 53 status.modified = blue bold underline red_background
54 54 status.added = green bold
55 55 status.removed = red bold blue_background
56 56 status.deleted = cyan bold underline
57 57 status.unknown = magenta bold underline
58 58 status.ignored = black bold
59 59
60 60 # 'none' turns off all effects
61 61 status.clean = none
62 62 status.copied = none
63 63
64 64 qseries.applied = blue bold underline
65 65 qseries.unapplied = black bold
66 66 qseries.missing = red bold
67 67
68 68 diff.diffline = bold
69 69 diff.extended = cyan bold
70 70 diff.file_a = red bold
71 71 diff.file_b = green bold
72 72 diff.hunk = magenta
73 73 diff.deleted = red
74 74 diff.inserted = green
75 75 diff.changed = white
76 76 diff.tab =
77 77 diff.trailingwhitespace = bold red_background
78 78
79 79 # Blank so it inherits the style of the surrounding label
80 80 changeset.public =
81 81 changeset.draft =
82 82 changeset.secret =
83 83
84 84 resolve.unresolved = red bold
85 85 resolve.resolved = green bold
86 86
87 87 bookmarks.active = green
88 88
89 89 branches.active = none
90 90 branches.closed = black bold
91 91 branches.current = green
92 92 branches.inactive = none
93 93
94 94 tags.normal = green
95 95 tags.local = black bold
96 96
97 97 rebase.rebased = blue
98 98 rebase.remaining = red bold
99 99
100 100 shelve.age = cyan
101 101 shelve.newest = green bold
102 102 shelve.name = blue bold
103 103
104 104 histedit.remaining = red bold
105 105
106 106 Custom colors
107 107 -------------
108 108
109 109 Because there are only eight standard colors, this module allows you
110 110 to define color names for other color slots which might be available
111 111 for your terminal type, assuming terminfo mode. For instance::
112 112
113 113 color.brightblue = 12
114 114 color.pink = 207
115 115 color.orange = 202
116 116
117 117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
118 118 that have brighter colors defined in the upper eight) and, 'pink' and
119 119 'orange' to colors in 256-color xterm's default color cube. These
120 120 defined colors may then be used as any of the pre-defined eight,
121 121 including appending '_background' to set the background to that color.
122 122
123 123 Modes
124 124 -----
125 125
126 126 By default, the color extension will use ANSI mode (or win32 mode on
127 127 Windows) if it detects a terminal. To override auto mode (to enable
128 128 terminfo mode, for example), set the following configuration option::
129 129
130 130 [color]
131 131 mode = terminfo
132 132
133 133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
134 134 disable color.
135 135
136 136 Note that on some systems, terminfo mode may cause problems when using
137 137 color with the pager extension and less -R. less with the -R option
138 138 will only display ECMA-48 color codes, and terminfo mode may sometimes
139 139 emit codes that less doesn't understand. You can work around this by
140 140 either using ansi mode (or auto mode), or by using less -r (which will
141 141 pass through all terminal control codes, not just color control
142 142 codes).
143 143
144 144 On some systems (such as MSYS in Windows), the terminal may support
145 145 a different color mode than the pager (activated via the "pager"
146 146 extension). It is possible to define separate modes depending on whether
147 147 the pager is active::
148 148
149 149 [color]
150 150 mode = auto
151 151 pagermode = ansi
152 152
153 153 If ``pagermode`` is not defined, the ``mode`` will be used.
154 154 '''
155 155
156 156 import os
157 157
158 158 from mercurial import cmdutil, commands, dispatch, extensions, subrepo, util
159 159 from mercurial import ui as uimod
160 160 from mercurial import templater, error
161 161 from mercurial.i18n import _
162 162
163 163 cmdtable = {}
164 164 command = cmdutil.command(cmdtable)
165 165 # Note for extension authors: ONLY specify testedwith = 'internal' for
166 166 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
167 167 # be specifying the version(s) of Mercurial they are tested with, or
168 168 # leave the attribute unspecified.
169 169 testedwith = 'internal'
170 170
171 171 # start and stop parameters for effects
172 172 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
173 173 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
174 174 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
175 175 'black_background': 40, 'red_background': 41,
176 176 'green_background': 42, 'yellow_background': 43,
177 177 'blue_background': 44, 'purple_background': 45,
178 178 'cyan_background': 46, 'white_background': 47}
179 179
180 180 def _terminfosetup(ui, mode):
181 181 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
182 182
183 183 global _terminfo_params
184 184 # If we failed to load curses, we go ahead and return.
185 185 if not _terminfo_params:
186 186 return
187 187 # Otherwise, see what the config file says.
188 188 if mode not in ('auto', 'terminfo'):
189 189 return
190 190
191 191 _terminfo_params.update((key[6:], (False, int(val)))
192 192 for key, val in ui.configitems('color')
193 193 if key.startswith('color.'))
194 194
195 195 try:
196 196 curses.setupterm()
197 197 except curses.error as e:
198 198 _terminfo_params = {}
199 199 return
200 200
201 201 for key, (b, e) in _terminfo_params.items():
202 202 if not b:
203 203 continue
204 204 if not curses.tigetstr(e):
205 205 # Most terminals don't support dim, invis, etc, so don't be
206 206 # noisy and use ui.debug().
207 207 ui.debug("no terminfo entry for %s\n" % e)
208 208 del _terminfo_params[key]
209 209 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
210 210 # Only warn about missing terminfo entries if we explicitly asked for
211 211 # terminfo mode.
212 212 if mode == "terminfo":
213 213 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
214 214 "ECMA-48 color\n"))
215 215 _terminfo_params = {}
216 216
217 217 def _modesetup(ui, coloropt):
218 218 global _terminfo_params
219 219
220 220 if coloropt == 'debug':
221 221 return 'debug'
222 222
223 223 auto = (coloropt == 'auto')
224 224 always = not auto and util.parsebool(coloropt)
225 225 if not always and not auto:
226 226 return None
227 227
228 228 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
229 229
230 230 mode = ui.config('color', 'mode', 'auto')
231 231
232 232 # If pager is active, color.pagermode overrides color.mode.
233 233 if getattr(ui, 'pageractive', False):
234 234 mode = ui.config('color', 'pagermode', mode)
235 235
236 236 realmode = mode
237 237 if mode == 'auto':
238 238 if os.name == 'nt':
239 239 term = os.environ.get('TERM')
240 240 # TERM won't be defined in a vanilla cmd.exe environment.
241 241
242 242 # UNIX-like environments on Windows such as Cygwin and MSYS will
243 243 # set TERM. They appear to make a best effort attempt at setting it
244 244 # to something appropriate. However, not all environments with TERM
245 245 # defined support ANSI. Since "ansi" could result in terminal
246 246 # gibberish, we error on the side of selecting "win32". However, if
247 247 # w32effects is not defined, we almost certainly don't support
248 248 # "win32", so don't even try.
249 249 if (term and 'xterm' in term) or not w32effects:
250 250 realmode = 'ansi'
251 251 else:
252 252 realmode = 'win32'
253 253 else:
254 254 realmode = 'ansi'
255 255
256 256 def modewarn():
257 257 # only warn if color.mode was explicitly set and we're in
258 258 # an interactive terminal
259 259 if mode == realmode and ui.interactive():
260 260 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
261 261
262 262 if realmode == 'win32':
263 263 _terminfo_params = {}
264 264 if not w32effects:
265 265 modewarn()
266 266 return None
267 267 _effects.update(w32effects)
268 268 elif realmode == 'ansi':
269 269 _terminfo_params = {}
270 270 elif realmode == 'terminfo':
271 271 _terminfosetup(ui, mode)
272 272 if not _terminfo_params:
273 273 ## FIXME Shouldn't we return None in this case too?
274 274 modewarn()
275 275 realmode = 'ansi'
276 276 else:
277 277 return None
278 278
279 279 if always or (auto and formatted):
280 280 return realmode
281 281 return None
282 282
283 283 try:
284 284 import curses
285 285 # Mapping from effect name to terminfo attribute name or color number.
286 286 # This will also force-load the curses module.
287 287 _terminfo_params = {'none': (True, 'sgr0'),
288 288 'standout': (True, 'smso'),
289 289 'underline': (True, 'smul'),
290 290 'reverse': (True, 'rev'),
291 291 'inverse': (True, 'rev'),
292 292 'blink': (True, 'blink'),
293 293 'dim': (True, 'dim'),
294 294 'bold': (True, 'bold'),
295 295 'invisible': (True, 'invis'),
296 296 'italic': (True, 'sitm'),
297 297 'black': (False, curses.COLOR_BLACK),
298 298 'red': (False, curses.COLOR_RED),
299 299 'green': (False, curses.COLOR_GREEN),
300 300 'yellow': (False, curses.COLOR_YELLOW),
301 301 'blue': (False, curses.COLOR_BLUE),
302 302 'magenta': (False, curses.COLOR_MAGENTA),
303 303 'cyan': (False, curses.COLOR_CYAN),
304 304 'white': (False, curses.COLOR_WHITE)}
305 305 except ImportError:
306 306 _terminfo_params = {}
307 307
308 308 _styles = {'grep.match': 'red bold',
309 309 'grep.linenumber': 'green',
310 310 'grep.rev': 'green',
311 311 'grep.change': 'green',
312 312 'grep.sep': 'cyan',
313 313 'grep.filename': 'magenta',
314 314 'grep.user': 'magenta',
315 315 'grep.date': 'magenta',
316 316 'bookmarks.active': 'green',
317 317 'branches.active': 'none',
318 318 'branches.closed': 'black bold',
319 319 'branches.current': 'green',
320 320 'branches.inactive': 'none',
321 321 'diff.changed': 'white',
322 322 'diff.deleted': 'red',
323 323 'diff.diffline': 'bold',
324 324 'diff.extended': 'cyan bold',
325 325 'diff.file_a': 'red bold',
326 326 'diff.file_b': 'green bold',
327 327 'diff.hunk': 'magenta',
328 328 'diff.inserted': 'green',
329 329 'diff.tab': '',
330 330 'diff.trailingwhitespace': 'bold red_background',
331 331 'changeset.public' : '',
332 332 'changeset.draft' : '',
333 333 'changeset.secret' : '',
334 334 'diffstat.deleted': 'red',
335 335 'diffstat.inserted': 'green',
336 336 'histedit.remaining': 'red bold',
337 337 'ui.prompt': 'yellow',
338 338 'log.changeset': 'yellow',
339 339 'patchbomb.finalsummary': '',
340 340 'patchbomb.from': 'magenta',
341 341 'patchbomb.to': 'cyan',
342 342 'patchbomb.subject': 'green',
343 343 'patchbomb.diffstats': '',
344 344 'rebase.rebased': 'blue',
345 345 'rebase.remaining': 'red bold',
346 346 'resolve.resolved': 'green bold',
347 347 'resolve.unresolved': 'red bold',
348 348 'shelve.age': 'cyan',
349 349 'shelve.newest': 'green bold',
350 350 'shelve.name': 'blue bold',
351 351 'status.added': 'green bold',
352 352 'status.clean': 'none',
353 353 'status.copied': 'none',
354 354 'status.deleted': 'cyan bold underline',
355 355 'status.ignored': 'black bold',
356 356 'status.modified': 'blue bold',
357 357 'status.removed': 'red bold',
358 358 'status.unknown': 'magenta bold underline',
359 359 'tags.normal': 'green',
360 360 'tags.local': 'black bold'}
361 361
362 362
363 363 def _effect_str(effect):
364 364 '''Helper function for render_effects().'''
365 365
366 366 bg = False
367 367 if effect.endswith('_background'):
368 368 bg = True
369 369 effect = effect[:-11]
370 370 attr, val = _terminfo_params[effect]
371 371 if attr:
372 372 return curses.tigetstr(val)
373 373 elif bg:
374 374 return curses.tparm(curses.tigetstr('setab'), val)
375 375 else:
376 376 return curses.tparm(curses.tigetstr('setaf'), val)
377 377
378 378 def render_effects(text, effects):
379 379 'Wrap text in commands to turn on each effect.'
380 380 if not text:
381 381 return text
382 382 if not _terminfo_params:
383 383 start = [str(_effects[e]) for e in ['none'] + effects.split()]
384 384 start = '\033[' + ';'.join(start) + 'm'
385 385 stop = '\033[' + str(_effects['none']) + 'm'
386 386 else:
387 387 start = ''.join(_effect_str(effect)
388 388 for effect in ['none'] + effects.split())
389 389 stop = _effect_str('none')
390 390 return ''.join([start, text, stop])
391 391
392 392 def extstyles():
393 393 for name, ext in extensions.extensions():
394 394 _styles.update(getattr(ext, 'colortable', {}))
395 395
396 396 def valideffect(effect):
397 397 'Determine if the effect is valid or not.'
398 398 good = False
399 399 if not _terminfo_params and effect in _effects:
400 400 good = True
401 401 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
402 402 good = True
403 403 return good
404 404
405 405 def configstyles(ui):
406 406 for status, cfgeffects in ui.configitems('color'):
407 407 if '.' not in status or status.startswith('color.'):
408 408 continue
409 409 cfgeffects = ui.configlist('color', status)
410 410 if cfgeffects:
411 411 good = []
412 412 for e in cfgeffects:
413 413 if valideffect(e):
414 414 good.append(e)
415 415 else:
416 416 ui.warn(_("ignoring unknown color/effect %r "
417 417 "(configured in color.%s)\n")
418 418 % (e, status))
419 419 _styles[status] = ' '.join(good)
420 420
421 421 class colorui(uimod.ui):
422 422 _colormode = 'ansi'
423 423 def write(self, *args, **opts):
424 424 if self._colormode is None:
425 425 return super(colorui, self).write(*args, **opts)
426 426
427 427 label = opts.get('label', '')
428 428 if self._buffers:
429 429 if self._bufferapplylabels:
430 430 self._buffers[-1].extend(self.label(a, label) for a in args)
431 431 else:
432 432 self._buffers[-1].extend(args)
433 433 elif self._colormode == 'win32':
434 434 for a in args:
435 435 win32print(a, super(colorui, self).write, **opts)
436 436 else:
437 437 return super(colorui, self).write(
438 438 *[self.label(a, label) for a in args], **opts)
439 439
440 440 def write_err(self, *args, **opts):
441 441 if self._colormode is None:
442 442 return super(colorui, self).write_err(*args, **opts)
443 443
444 444 label = opts.get('label', '')
445 445 if self._bufferstates and self._bufferstates[-1][0]:
446 446 return self.write(*args, **opts)
447 447 if self._colormode == 'win32':
448 448 for a in args:
449 449 win32print(a, super(colorui, self).write_err, **opts)
450 450 else:
451 451 return super(colorui, self).write_err(
452 452 *[self.label(a, label) for a in args], **opts)
453 453
454 454 def showlabel(self, msg, label):
455 455 if label and msg:
456 456 if msg[-1] == '\n':
457 457 return "[%s|%s]\n" % (label, msg[:-1])
458 458 else:
459 459 return "[%s|%s]" % (label, msg)
460 460 else:
461 461 return msg
462 462
463 463 def label(self, msg, label):
464 464 if self._colormode is None:
465 465 return super(colorui, self).label(msg, label)
466 466
467 467 if self._colormode == 'debug':
468 468 return self.showlabel(msg, label)
469 469
470 470 effects = []
471 471 for l in label.split():
472 472 s = _styles.get(l, '')
473 473 if s:
474 474 effects.append(s)
475 475 elif valideffect(l):
476 476 effects.append(l)
477 477 effects = ' '.join(effects)
478 478 if effects:
479 479 return '\n'.join([render_effects(s, effects)
480 480 for s in msg.split('\n')])
481 481 return msg
482 482
483 483 def templatelabel(context, mapping, args):
484 484 if len(args) != 2:
485 485 # i18n: "label" is a keyword
486 486 raise error.ParseError(_("label expects two arguments"))
487 487
488 488 # add known effects to the mapping so symbols like 'red', 'bold',
489 489 # etc. don't need to be quoted
490 490 mapping.update(dict([(k, k) for k in _effects]))
491 491
492 thing = args[1][0](context, mapping, args[1][1])
493 thing = templater.stringify(thing)
492 thing = templater.evalstring(context, mapping, args[1])
494 493
495 494 # apparently, repo could be a string that is the favicon?
496 495 repo = mapping.get('repo', '')
497 496 if isinstance(repo, str):
498 497 return thing
499 498
500 label = args[0][0](context, mapping, args[0][1])
501 label = templater.stringify(label)
499 label = templater.evalstring(context, mapping, args[0])
502 500
503 501 return repo.ui.label(thing, label)
504 502
505 503 def uisetup(ui):
506 504 if ui.plain():
507 505 return
508 506 if not isinstance(ui, colorui):
509 507 colorui.__bases__ = (ui.__class__,)
510 508 ui.__class__ = colorui
511 509 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
512 510 mode = _modesetup(ui_, opts['color'])
513 511 colorui._colormode = mode
514 512 if mode and mode != 'debug':
515 513 extstyles()
516 514 configstyles(ui_)
517 515 return orig(ui_, opts, cmd, cmdfunc)
518 516 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
519 517 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
520 518 # insert the argument in the front,
521 519 # the end of git diff arguments is used for paths
522 520 commands.insert(1, '--color')
523 521 return orig(gitsub, commands, env, stream, cwd)
524 522 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
525 523 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
526 524 templatelabel.__doc__ = templater.funcs['label'].__doc__
527 525 templater.funcs['label'] = templatelabel
528 526
529 527 def extsetup(ui):
530 528 commands.globalopts.append(
531 529 ('', 'color', 'auto',
532 530 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
533 531 # and should not be translated
534 532 _("when to colorize (boolean, always, auto, never, or debug)"),
535 533 _('TYPE')))
536 534
537 535 @command('debugcolor', [], 'hg debugcolor')
538 536 def debugcolor(ui, repo, **opts):
539 537 global _styles
540 538 _styles = {}
541 539 for effect in _effects.keys():
542 540 _styles[effect] = effect
543 541 ui.write(('color mode: %s\n') % ui._colormode)
544 542 ui.write(_('available colors:\n'))
545 543 for label, colors in _styles.items():
546 544 ui.write(('%s\n') % colors, label=label)
547 545
548 546 if os.name != 'nt':
549 547 w32effects = None
550 548 else:
551 549 import re, ctypes
552 550
553 551 _kernel32 = ctypes.windll.kernel32
554 552
555 553 _WORD = ctypes.c_ushort
556 554
557 555 _INVALID_HANDLE_VALUE = -1
558 556
559 557 class _COORD(ctypes.Structure):
560 558 _fields_ = [('X', ctypes.c_short),
561 559 ('Y', ctypes.c_short)]
562 560
563 561 class _SMALL_RECT(ctypes.Structure):
564 562 _fields_ = [('Left', ctypes.c_short),
565 563 ('Top', ctypes.c_short),
566 564 ('Right', ctypes.c_short),
567 565 ('Bottom', ctypes.c_short)]
568 566
569 567 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
570 568 _fields_ = [('dwSize', _COORD),
571 569 ('dwCursorPosition', _COORD),
572 570 ('wAttributes', _WORD),
573 571 ('srWindow', _SMALL_RECT),
574 572 ('dwMaximumWindowSize', _COORD)]
575 573
576 574 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
577 575 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
578 576
579 577 _FOREGROUND_BLUE = 0x0001
580 578 _FOREGROUND_GREEN = 0x0002
581 579 _FOREGROUND_RED = 0x0004
582 580 _FOREGROUND_INTENSITY = 0x0008
583 581
584 582 _BACKGROUND_BLUE = 0x0010
585 583 _BACKGROUND_GREEN = 0x0020
586 584 _BACKGROUND_RED = 0x0040
587 585 _BACKGROUND_INTENSITY = 0x0080
588 586
589 587 _COMMON_LVB_REVERSE_VIDEO = 0x4000
590 588 _COMMON_LVB_UNDERSCORE = 0x8000
591 589
592 590 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
593 591 w32effects = {
594 592 'none': -1,
595 593 'black': 0,
596 594 'red': _FOREGROUND_RED,
597 595 'green': _FOREGROUND_GREEN,
598 596 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
599 597 'blue': _FOREGROUND_BLUE,
600 598 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
601 599 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
602 600 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
603 601 'bold': _FOREGROUND_INTENSITY,
604 602 'black_background': 0x100, # unused value > 0x0f
605 603 'red_background': _BACKGROUND_RED,
606 604 'green_background': _BACKGROUND_GREEN,
607 605 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
608 606 'blue_background': _BACKGROUND_BLUE,
609 607 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
610 608 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
611 609 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
612 610 _BACKGROUND_BLUE),
613 611 'bold_background': _BACKGROUND_INTENSITY,
614 612 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
615 613 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
616 614 }
617 615
618 616 passthrough = set([_FOREGROUND_INTENSITY,
619 617 _BACKGROUND_INTENSITY,
620 618 _COMMON_LVB_UNDERSCORE,
621 619 _COMMON_LVB_REVERSE_VIDEO])
622 620
623 621 stdout = _kernel32.GetStdHandle(
624 622 _STD_OUTPUT_HANDLE) # don't close the handle returned
625 623 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
626 624 w32effects = None
627 625 else:
628 626 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
629 627 if not _kernel32.GetConsoleScreenBufferInfo(
630 628 stdout, ctypes.byref(csbi)):
631 629 # stdout may not support GetConsoleScreenBufferInfo()
632 630 # when called from subprocess or redirected
633 631 w32effects = None
634 632 else:
635 633 origattr = csbi.wAttributes
636 634 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
637 635 re.MULTILINE | re.DOTALL)
638 636
639 637 def win32print(text, orig, **opts):
640 638 label = opts.get('label', '')
641 639 attr = origattr
642 640
643 641 def mapcolor(val, attr):
644 642 if val == -1:
645 643 return origattr
646 644 elif val in passthrough:
647 645 return attr | val
648 646 elif val > 0x0f:
649 647 return (val & 0x70) | (attr & 0x8f)
650 648 else:
651 649 return (val & 0x07) | (attr & 0xf8)
652 650
653 651 # determine console attributes based on labels
654 652 for l in label.split():
655 653 style = _styles.get(l, '')
656 654 for effect in style.split():
657 655 try:
658 656 attr = mapcolor(w32effects[effect], attr)
659 657 except KeyError:
660 658 # w32effects could not have certain attributes so we skip
661 659 # them if not found
662 660 pass
663 661 # hack to ensure regexp finds data
664 662 if not text.startswith('\033['):
665 663 text = '\033[m' + text
666 664
667 665 # Look for ANSI-like codes embedded in text
668 666 m = re.match(ansire, text)
669 667
670 668 try:
671 669 while m:
672 670 for sattr in m.group(1).split(';'):
673 671 if sattr:
674 672 attr = mapcolor(int(sattr), attr)
675 673 _kernel32.SetConsoleTextAttribute(stdout, attr)
676 674 orig(m.group(2), **opts)
677 675 m = re.match(ansire, m.group(3))
678 676 finally:
679 677 # Explicitly reset original attributes
680 678 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,1001 +1,1005 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
9 9
10 10 import os
11 11 import re
12 12 import types
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 config,
17 17 error,
18 18 minirst,
19 19 parser,
20 20 revset as revsetmod,
21 21 templatefilters,
22 22 templatekw,
23 23 util,
24 24 )
25 25
26 26 # template parsing
27 27
28 28 elements = {
29 29 # token-type: binding-strength, primary, prefix, infix, suffix
30 30 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
31 31 ",": (2, None, None, ("list", 2), None),
32 32 "|": (5, None, None, ("|", 5), None),
33 33 "%": (6, None, None, ("%", 6), None),
34 34 ")": (0, None, None, None, None),
35 35 "integer": (0, "integer", None, None, None),
36 36 "symbol": (0, "symbol", None, None, None),
37 37 "string": (0, "string", None, None, None),
38 38 "template": (0, "template", None, None, None),
39 39 "end": (0, None, None, None, None),
40 40 }
41 41
42 42 def tokenize(program, start, end):
43 43 pos = start
44 44 while pos < end:
45 45 c = program[pos]
46 46 if c.isspace(): # skip inter-token whitespace
47 47 pass
48 48 elif c in "(,)%|": # handle simple operators
49 49 yield (c, None, pos)
50 50 elif c in '"\'': # handle quoted templates
51 51 s = pos + 1
52 52 data, pos = _parsetemplate(program, s, end, c)
53 53 yield ('template', data, s)
54 54 pos -= 1
55 55 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
56 56 # handle quoted strings
57 57 c = program[pos + 1]
58 58 s = pos = pos + 2
59 59 while pos < end: # find closing quote
60 60 d = program[pos]
61 61 if d == '\\': # skip over escaped characters
62 62 pos += 2
63 63 continue
64 64 if d == c:
65 65 yield ('string', program[s:pos], s)
66 66 break
67 67 pos += 1
68 68 else:
69 69 raise error.ParseError(_("unterminated string"), s)
70 70 elif c.isdigit() or c == '-':
71 71 s = pos
72 72 if c == '-': # simply take negate operator as part of integer
73 73 pos += 1
74 74 if pos >= end or not program[pos].isdigit():
75 75 raise error.ParseError(_("integer literal without digits"), s)
76 76 pos += 1
77 77 while pos < end:
78 78 d = program[pos]
79 79 if not d.isdigit():
80 80 break
81 81 pos += 1
82 82 yield ('integer', program[s:pos], s)
83 83 pos -= 1
84 84 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
85 85 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
86 86 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
87 87 # where some of nested templates were preprocessed as strings and
88 88 # then compiled. therefore, \"...\" was allowed. (issue4733)
89 89 #
90 90 # processing flow of _evalifliteral() at 5ab28a2e9962:
91 91 # outer template string -> stringify() -> compiletemplate()
92 92 # ------------------------ ------------ ------------------
93 93 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
94 94 # ~~~~~~~~
95 95 # escaped quoted string
96 96 if c == 'r':
97 97 pos += 1
98 98 token = 'string'
99 99 else:
100 100 token = 'template'
101 101 quote = program[pos:pos + 2]
102 102 s = pos = pos + 2
103 103 while pos < end: # find closing escaped quote
104 104 if program.startswith('\\\\\\', pos, end):
105 105 pos += 4 # skip over double escaped characters
106 106 continue
107 107 if program.startswith(quote, pos, end):
108 108 # interpret as if it were a part of an outer string
109 109 data = parser.unescapestr(program[s:pos])
110 110 if token == 'template':
111 111 data = _parsetemplate(data, 0, len(data))[0]
112 112 yield (token, data, s)
113 113 pos += 1
114 114 break
115 115 pos += 1
116 116 else:
117 117 raise error.ParseError(_("unterminated string"), s)
118 118 elif c.isalnum() or c in '_':
119 119 s = pos
120 120 pos += 1
121 121 while pos < end: # find end of symbol
122 122 d = program[pos]
123 123 if not (d.isalnum() or d == "_"):
124 124 break
125 125 pos += 1
126 126 sym = program[s:pos]
127 127 yield ('symbol', sym, s)
128 128 pos -= 1
129 129 elif c == '}':
130 130 yield ('end', None, pos + 1)
131 131 return
132 132 else:
133 133 raise error.ParseError(_("syntax error"), pos)
134 134 pos += 1
135 135 raise error.ParseError(_("unterminated template expansion"), start)
136 136
137 137 def _parsetemplate(tmpl, start, stop, quote=''):
138 138 r"""
139 139 >>> _parsetemplate('foo{bar}"baz', 0, 12)
140 140 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
141 141 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
142 142 ([('string', 'foo'), ('symbol', 'bar')], 9)
143 143 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
144 144 ([('string', 'foo')], 4)
145 145 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
146 146 ([('string', 'foo"'), ('string', 'bar')], 9)
147 147 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
148 148 ([('string', 'foo\\')], 6)
149 149 """
150 150 parsed = []
151 151 sepchars = '{' + quote
152 152 pos = start
153 153 p = parser.parser(elements)
154 154 while pos < stop:
155 155 n = min((tmpl.find(c, pos, stop) for c in sepchars),
156 156 key=lambda n: (n < 0, n))
157 157 if n < 0:
158 158 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
159 159 pos = stop
160 160 break
161 161 c = tmpl[n]
162 162 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
163 163 if bs % 2 == 1:
164 164 # escaped (e.g. '\{', '\\\{', but not '\\{')
165 165 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
166 166 pos = n + 1
167 167 continue
168 168 if n > pos:
169 169 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
170 170 if c == quote:
171 171 return parsed, n + 1
172 172
173 173 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop))
174 174 parsed.append(parseres)
175 175
176 176 if quote:
177 177 raise error.ParseError(_("unterminated string"), start)
178 178 return parsed, pos
179 179
180 180 def compiletemplate(tmpl, context):
181 181 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
182 182 return [compileexp(e, context, methods) for e in parsed]
183 183
184 184 def compileexp(exp, context, curmethods):
185 185 t = exp[0]
186 186 if t in curmethods:
187 187 return curmethods[t](exp, context)
188 188 raise error.ParseError(_("unknown method '%s'") % t)
189 189
190 190 # template evaluation
191 191
192 192 def getsymbol(exp):
193 193 if exp[0] == 'symbol':
194 194 return exp[1]
195 195 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
196 196
197 197 def getlist(x):
198 198 if not x:
199 199 return []
200 200 if x[0] == 'list':
201 201 return getlist(x[1]) + [x[2]]
202 202 return [x]
203 203
204 204 def gettemplate(exp, context):
205 205 if exp[0] == 'template':
206 206 return [compileexp(e, context, methods) for e in exp[1]]
207 207 if exp[0] == 'symbol':
208 208 # unlike runsymbol(), here 'symbol' is always taken as template name
209 209 # even if it exists in mapping. this allows us to override mapping
210 210 # by web templates, e.g. 'changelogtag' is redefined in map file.
211 211 return context._load(exp[1])
212 212 raise error.ParseError(_("expected template specifier"))
213 213
214 214 def evalfuncarg(context, mapping, arg):
215 215 func, data = arg
216 216 # func() may return string, generator of strings or arbitrary object such
217 217 # as date tuple, but filter does not want generator.
218 218 thing = func(context, mapping, data)
219 219 if isinstance(thing, types.GeneratorType):
220 220 thing = stringify(thing)
221 221 return thing
222 222
223 223 def evalinteger(context, mapping, arg, err):
224 224 v = evalfuncarg(context, mapping, arg)
225 225 try:
226 226 return int(v)
227 227 except (TypeError, ValueError):
228 228 raise error.ParseError(err)
229 229
230 def evalstring(context, mapping, arg):
231 func, data = arg
232 return stringify(func(context, mapping, data))
233
230 234 def runinteger(context, mapping, data):
231 235 return int(data)
232 236
233 237 def runstring(context, mapping, data):
234 238 return data
235 239
236 240 def _recursivesymbolblocker(key):
237 241 def showrecursion(**args):
238 242 raise error.Abort(_("recursive reference '%s' in template") % key)
239 243 return showrecursion
240 244
241 245 def _runrecursivesymbol(context, mapping, key):
242 246 raise error.Abort(_("recursive reference '%s' in template") % key)
243 247
244 248 def runsymbol(context, mapping, key):
245 249 v = mapping.get(key)
246 250 if v is None:
247 251 v = context._defaults.get(key)
248 252 if v is None:
249 253 # put poison to cut recursion. we can't move this to parsing phase
250 254 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
251 255 safemapping = mapping.copy()
252 256 safemapping[key] = _recursivesymbolblocker(key)
253 257 try:
254 258 v = context.process(key, safemapping)
255 259 except TemplateNotFound:
256 260 v = ''
257 261 if callable(v):
258 262 return v(**mapping)
259 263 return v
260 264
261 265 def buildtemplate(exp, context):
262 266 ctmpl = [compileexp(e, context, methods) for e in exp[1]]
263 267 if len(ctmpl) == 1:
264 268 return ctmpl[0] # fast path for string with no template fragment
265 269 return (runtemplate, ctmpl)
266 270
267 271 def runtemplate(context, mapping, template):
268 272 for func, data in template:
269 273 yield func(context, mapping, data)
270 274
271 275 def buildfilter(exp, context):
272 276 arg = compileexp(exp[1], context, methods)
273 277 n = getsymbol(exp[2])
274 278 if n in context._filters:
275 279 filt = context._filters[n]
276 280 return (runfilter, (arg, filt))
277 281 if n in funcs:
278 282 f = funcs[n]
279 283 return (f, [arg])
280 284 raise error.ParseError(_("unknown function '%s'") % n)
281 285
282 286 def runfilter(context, mapping, data):
283 287 arg, filt = data
284 288 thing = evalfuncarg(context, mapping, arg)
285 289 try:
286 290 return filt(thing)
287 291 except (ValueError, AttributeError, TypeError):
288 292 if isinstance(arg[1], tuple):
289 293 dt = arg[1][1]
290 294 else:
291 295 dt = arg[1]
292 296 raise error.Abort(_("template filter '%s' is not compatible with "
293 297 "keyword '%s'") % (filt.func_name, dt))
294 298
295 299 def buildmap(exp, context):
296 300 func, data = compileexp(exp[1], context, methods)
297 301 ctmpl = gettemplate(exp[2], context)
298 302 return (runmap, (func, data, ctmpl))
299 303
300 304 def runmap(context, mapping, data):
301 305 func, data, ctmpl = data
302 306 d = func(context, mapping, data)
303 307 if util.safehasattr(d, 'itermaps'):
304 308 d = d.itermaps()
305 309
306 310 for i in d:
307 311 lm = mapping.copy()
308 312 if isinstance(i, dict):
309 313 lm.update(i)
310 314 lm['originalnode'] = mapping.get('node')
311 315 yield runtemplate(context, lm, ctmpl)
312 316 else:
313 317 # v is not an iterable of dicts, this happen when 'key'
314 318 # has been fully expanded already and format is useless.
315 319 # If so, return the expanded value.
316 320 yield i
317 321
318 322 def buildfunc(exp, context):
319 323 n = getsymbol(exp[1])
320 324 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
321 325 if n in funcs:
322 326 f = funcs[n]
323 327 return (f, args)
324 328 if n in context._filters:
325 329 if len(args) != 1:
326 330 raise error.ParseError(_("filter %s expects one argument") % n)
327 331 f = context._filters[n]
328 332 return (runfilter, (args[0], f))
329 333 raise error.ParseError(_("unknown function '%s'") % n)
330 334
331 335 def date(context, mapping, args):
332 336 """:date(date[, fmt]): Format a date. See :hg:`help dates` for formatting
333 337 strings. The default is a Unix date format, including the timezone:
334 338 "Mon Sep 04 15:13:13 2006 0700"."""
335 339 if not (1 <= len(args) <= 2):
336 340 # i18n: "date" is a keyword
337 341 raise error.ParseError(_("date expects one or two arguments"))
338 342
339 343 date = evalfuncarg(context, mapping, args[0])
340 344 fmt = None
341 345 if len(args) == 2:
342 fmt = stringify(args[1][0](context, mapping, args[1][1]))
346 fmt = evalstring(context, mapping, args[1])
343 347 try:
344 348 if fmt is None:
345 349 return util.datestr(date)
346 350 else:
347 351 return util.datestr(date, fmt)
348 352 except (TypeError, ValueError):
349 353 # i18n: "date" is a keyword
350 354 raise error.ParseError(_("date expects a date information"))
351 355
352 356 def diff(context, mapping, args):
353 357 """:diff([includepattern [, excludepattern]]): Show a diff, optionally
354 358 specifying files to include or exclude."""
355 359 if len(args) > 2:
356 360 # i18n: "diff" is a keyword
357 361 raise error.ParseError(_("diff expects zero, one, or two arguments"))
358 362
359 363 def getpatterns(i):
360 364 if i < len(args):
361 s = stringify(args[i][0](context, mapping, args[i][1])).strip()
365 s = evalstring(context, mapping, args[i]).strip()
362 366 if s:
363 367 return [s]
364 368 return []
365 369
366 370 ctx = mapping['ctx']
367 371 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
368 372
369 373 return ''.join(chunks)
370 374
371 375 def fill(context, mapping, args):
372 376 """:fill(text[, width[, initialident[, hangindent]]]): Fill many
373 377 paragraphs with optional indentation. See the "fill" filter."""
374 378 if not (1 <= len(args) <= 4):
375 379 # i18n: "fill" is a keyword
376 380 raise error.ParseError(_("fill expects one to four arguments"))
377 381
378 text = stringify(args[0][0](context, mapping, args[0][1]))
382 text = evalstring(context, mapping, args[0])
379 383 width = 76
380 384 initindent = ''
381 385 hangindent = ''
382 386 if 2 <= len(args) <= 4:
383 387 width = evalinteger(context, mapping, args[1],
384 388 # i18n: "fill" is a keyword
385 389 _("fill expects an integer width"))
386 390 try:
387 initindent = stringify(args[2][0](context, mapping, args[2][1]))
388 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
391 initindent = evalstring(context, mapping, args[2])
392 hangindent = evalstring(context, mapping, args[3])
389 393 except IndexError:
390 394 pass
391 395
392 396 return templatefilters.fill(text, width, initindent, hangindent)
393 397
394 398 def pad(context, mapping, args):
395 399 """:pad(text, width[, fillchar=' '[, right=False]]): Pad text with a
396 400 fill character."""
397 401 if not (2 <= len(args) <= 4):
398 402 # i18n: "pad" is a keyword
399 403 raise error.ParseError(_("pad() expects two to four arguments"))
400 404
401 405 width = evalinteger(context, mapping, args[1],
402 406 # i18n: "pad" is a keyword
403 407 _("pad() expects an integer width"))
404 408
405 text = stringify(args[0][0](context, mapping, args[0][1]))
409 text = evalstring(context, mapping, args[0])
406 410
407 411 right = False
408 412 fillchar = ' '
409 413 if len(args) > 2:
410 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
414 fillchar = evalstring(context, mapping, args[2])
411 415 if len(args) > 3:
412 416 right = util.parsebool(args[3][1])
413 417
414 418 if right:
415 419 return text.rjust(width, fillchar)
416 420 else:
417 421 return text.ljust(width, fillchar)
418 422
419 423 def indent(context, mapping, args):
420 424 """:indent(text, indentchars[, firstline]): Indents all non-empty lines
421 425 with the characters given in the indentchars string. An optional
422 426 third parameter will override the indent for the first line only
423 427 if present."""
424 428 if not (2 <= len(args) <= 3):
425 429 # i18n: "indent" is a keyword
426 430 raise error.ParseError(_("indent() expects two or three arguments"))
427 431
428 text = stringify(args[0][0](context, mapping, args[0][1]))
429 indent = stringify(args[1][0](context, mapping, args[1][1]))
432 text = evalstring(context, mapping, args[0])
433 indent = evalstring(context, mapping, args[1])
430 434
431 435 if len(args) == 3:
432 firstline = stringify(args[2][0](context, mapping, args[2][1]))
436 firstline = evalstring(context, mapping, args[2])
433 437 else:
434 438 firstline = indent
435 439
436 440 # the indent function doesn't indent the first line, so we do it here
437 441 return templatefilters.indent(firstline + text, indent)
438 442
439 443 def get(context, mapping, args):
440 444 """:get(dict, key): Get an attribute/key from an object. Some keywords
441 445 are complex types. This function allows you to obtain the value of an
442 446 attribute on these types."""
443 447 if len(args) != 2:
444 448 # i18n: "get" is a keyword
445 449 raise error.ParseError(_("get() expects two arguments"))
446 450
447 451 dictarg = evalfuncarg(context, mapping, args[0])
448 452 if not util.safehasattr(dictarg, 'get'):
449 453 # i18n: "get" is a keyword
450 454 raise error.ParseError(_("get() expects a dict as first argument"))
451 455
452 456 key = evalfuncarg(context, mapping, args[1])
453 457 return dictarg.get(key)
454 458
455 459 def if_(context, mapping, args):
456 460 """:if(expr, then[, else]): Conditionally execute based on the result of
457 461 an expression."""
458 462 if not (2 <= len(args) <= 3):
459 463 # i18n: "if" is a keyword
460 464 raise error.ParseError(_("if expects two or three arguments"))
461 465
462 test = stringify(args[0][0](context, mapping, args[0][1]))
466 test = evalstring(context, mapping, args[0])
463 467 if test:
464 468 yield args[1][0](context, mapping, args[1][1])
465 469 elif len(args) == 3:
466 470 yield args[2][0](context, mapping, args[2][1])
467 471
468 472 def ifcontains(context, mapping, args):
469 473 """:ifcontains(search, thing, then[, else]): Conditionally execute based
470 474 on whether the item "search" is in "thing"."""
471 475 if not (3 <= len(args) <= 4):
472 476 # i18n: "ifcontains" is a keyword
473 477 raise error.ParseError(_("ifcontains expects three or four arguments"))
474 478
475 item = stringify(args[0][0](context, mapping, args[0][1]))
479 item = evalstring(context, mapping, args[0])
476 480 items = evalfuncarg(context, mapping, args[1])
477 481
478 482 if item in items:
479 483 yield args[2][0](context, mapping, args[2][1])
480 484 elif len(args) == 4:
481 485 yield args[3][0](context, mapping, args[3][1])
482 486
483 487 def ifeq(context, mapping, args):
484 488 """:ifeq(expr1, expr2, then[, else]): Conditionally execute based on
485 489 whether 2 items are equivalent."""
486 490 if not (3 <= len(args) <= 4):
487 491 # i18n: "ifeq" is a keyword
488 492 raise error.ParseError(_("ifeq expects three or four arguments"))
489 493
490 test = stringify(args[0][0](context, mapping, args[0][1]))
491 match = stringify(args[1][0](context, mapping, args[1][1]))
494 test = evalstring(context, mapping, args[0])
495 match = evalstring(context, mapping, args[1])
492 496 if test == match:
493 497 yield args[2][0](context, mapping, args[2][1])
494 498 elif len(args) == 4:
495 499 yield args[3][0](context, mapping, args[3][1])
496 500
497 501 def join(context, mapping, args):
498 502 """:join(list, sep): Join items in a list with a delimiter."""
499 503 if not (1 <= len(args) <= 2):
500 504 # i18n: "join" is a keyword
501 505 raise error.ParseError(_("join expects one or two arguments"))
502 506
503 507 joinset = args[0][0](context, mapping, args[0][1])
504 508 if util.safehasattr(joinset, 'itermaps'):
505 509 jf = joinset.joinfmt
506 510 joinset = [jf(x) for x in joinset.itermaps()]
507 511
508 512 joiner = " "
509 513 if len(args) > 1:
510 joiner = stringify(args[1][0](context, mapping, args[1][1]))
514 joiner = evalstring(context, mapping, args[1])
511 515
512 516 first = True
513 517 for x in joinset:
514 518 if first:
515 519 first = False
516 520 else:
517 521 yield joiner
518 522 yield x
519 523
520 524 def label(context, mapping, args):
521 525 """:label(label, expr): Apply a label to generated content. Content with
522 526 a label applied can result in additional post-processing, such as
523 527 automatic colorization."""
524 528 if len(args) != 2:
525 529 # i18n: "label" is a keyword
526 530 raise error.ParseError(_("label expects two arguments"))
527 531
528 532 # ignore args[0] (the label string) since this is supposed to be a a no-op
529 533 yield args[1][0](context, mapping, args[1][1])
530 534
531 535 def latesttag(context, mapping, args):
532 536 """:latesttag([pattern]): The global tags matching the given pattern on the
533 537 most recent globally tagged ancestor of this changeset."""
534 538 if len(args) > 1:
535 539 # i18n: "latesttag" is a keyword
536 540 raise error.ParseError(_("latesttag expects at most one argument"))
537 541
538 542 pattern = None
539 543 if len(args) == 1:
540 pattern = stringify(args[0][0](context, mapping, args[0][1]))
544 pattern = evalstring(context, mapping, args[0])
541 545
542 546 return templatekw.showlatesttags(pattern, **mapping)
543 547
544 548 def localdate(context, mapping, args):
545 549 """:localdate(date[, tz]): Converts a date to the specified timezone.
546 550 The default is local date."""
547 551 if not (1 <= len(args) <= 2):
548 552 # i18n: "localdate" is a keyword
549 553 raise error.ParseError(_("localdate expects one or two arguments"))
550 554
551 555 date = evalfuncarg(context, mapping, args[0])
552 556 try:
553 557 date = util.parsedate(date)
554 558 except AttributeError: # not str nor date tuple
555 559 # i18n: "localdate" is a keyword
556 560 raise error.ParseError(_("localdate expects a date information"))
557 561 if len(args) >= 2:
558 562 tzoffset = None
559 563 tz = evalfuncarg(context, mapping, args[1])
560 564 if isinstance(tz, str):
561 565 tzoffset = util.parsetimezone(tz)
562 566 if tzoffset is None:
563 567 try:
564 568 tzoffset = int(tz)
565 569 except (TypeError, ValueError):
566 570 # i18n: "localdate" is a keyword
567 571 raise error.ParseError(_("localdate expects a timezone"))
568 572 else:
569 573 tzoffset = util.makedate()[1]
570 574 return (date[0], tzoffset)
571 575
572 576 def revset(context, mapping, args):
573 577 """:revset(query[, formatargs...]): Execute a revision set query. See
574 578 :hg:`help revset`."""
575 579 if not len(args) > 0:
576 580 # i18n: "revset" is a keyword
577 581 raise error.ParseError(_("revset expects one or more arguments"))
578 582
579 raw = stringify(args[0][0](context, mapping, args[0][1]))
583 raw = evalstring(context, mapping, args[0])
580 584 ctx = mapping['ctx']
581 585 repo = ctx.repo()
582 586
583 587 def query(expr):
584 588 m = revsetmod.match(repo.ui, expr)
585 589 return m(repo)
586 590
587 591 if len(args) > 1:
588 592 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
589 593 revs = query(revsetmod.formatspec(raw, *formatargs))
590 594 revs = list(revs)
591 595 else:
592 596 revsetcache = mapping['cache'].setdefault("revsetcache", {})
593 597 if raw in revsetcache:
594 598 revs = revsetcache[raw]
595 599 else:
596 600 revs = query(raw)
597 601 revs = list(revs)
598 602 revsetcache[raw] = revs
599 603
600 604 return templatekw.showrevslist("revision", revs, **mapping)
601 605
602 606 def rstdoc(context, mapping, args):
603 607 """:rstdoc(text, style): Format ReStructuredText."""
604 608 if len(args) != 2:
605 609 # i18n: "rstdoc" is a keyword
606 610 raise error.ParseError(_("rstdoc expects two arguments"))
607 611
608 text = stringify(args[0][0](context, mapping, args[0][1]))
609 style = stringify(args[1][0](context, mapping, args[1][1]))
612 text = evalstring(context, mapping, args[0])
613 style = evalstring(context, mapping, args[1])
610 614
611 615 return minirst.format(text, style=style, keep=['verbose'])
612 616
613 617 def shortest(context, mapping, args):
614 618 """:shortest(node, minlength=4): Obtain the shortest representation of
615 619 a node."""
616 620 if not (1 <= len(args) <= 2):
617 621 # i18n: "shortest" is a keyword
618 622 raise error.ParseError(_("shortest() expects one or two arguments"))
619 623
620 node = stringify(args[0][0](context, mapping, args[0][1]))
624 node = evalstring(context, mapping, args[0])
621 625
622 626 minlength = 4
623 627 if len(args) > 1:
624 628 minlength = evalinteger(context, mapping, args[1],
625 629 # i18n: "shortest" is a keyword
626 630 _("shortest() expects an integer minlength"))
627 631
628 632 cl = mapping['ctx']._repo.changelog
629 633 def isvalid(test):
630 634 try:
631 635 try:
632 636 cl.index.partialmatch(test)
633 637 except AttributeError:
634 638 # Pure mercurial doesn't support partialmatch on the index.
635 639 # Fallback to the slow way.
636 640 if cl._partialmatch(test) is None:
637 641 return False
638 642
639 643 try:
640 644 i = int(test)
641 645 # if we are a pure int, then starting with zero will not be
642 646 # confused as a rev; or, obviously, if the int is larger than
643 647 # the value of the tip rev
644 648 if test[0] == '0' or i > len(cl):
645 649 return True
646 650 return False
647 651 except ValueError:
648 652 return True
649 653 except error.RevlogError:
650 654 return False
651 655
652 656 shortest = node
653 657 startlength = max(6, minlength)
654 658 length = startlength
655 659 while True:
656 660 test = node[:length]
657 661 if isvalid(test):
658 662 shortest = test
659 663 if length == minlength or length > startlength:
660 664 return shortest
661 665 length -= 1
662 666 else:
663 667 length += 1
664 668 if len(shortest) <= length:
665 669 return shortest
666 670
667 671 def strip(context, mapping, args):
668 672 """:strip(text[, chars]): Strip characters from a string. By default,
669 673 strips all leading and trailing whitespace."""
670 674 if not (1 <= len(args) <= 2):
671 675 # i18n: "strip" is a keyword
672 676 raise error.ParseError(_("strip expects one or two arguments"))
673 677
674 text = stringify(args[0][0](context, mapping, args[0][1]))
678 text = evalstring(context, mapping, args[0])
675 679 if len(args) == 2:
676 chars = stringify(args[1][0](context, mapping, args[1][1]))
680 chars = evalstring(context, mapping, args[1])
677 681 return text.strip(chars)
678 682 return text.strip()
679 683
680 684 def sub(context, mapping, args):
681 685 """:sub(pattern, replacement, expression): Perform text substitution
682 686 using regular expressions."""
683 687 if len(args) != 3:
684 688 # i18n: "sub" is a keyword
685 689 raise error.ParseError(_("sub expects three arguments"))
686 690
687 pat = stringify(args[0][0](context, mapping, args[0][1]))
688 rpl = stringify(args[1][0](context, mapping, args[1][1]))
689 src = stringify(args[2][0](context, mapping, args[2][1]))
691 pat = evalstring(context, mapping, args[0])
692 rpl = evalstring(context, mapping, args[1])
693 src = evalstring(context, mapping, args[2])
690 694 try:
691 695 patre = re.compile(pat)
692 696 except re.error:
693 697 # i18n: "sub" is a keyword
694 698 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
695 699 try:
696 700 yield patre.sub(rpl, src)
697 701 except re.error:
698 702 # i18n: "sub" is a keyword
699 703 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
700 704
701 705 def startswith(context, mapping, args):
702 706 """:startswith(pattern, text): Returns the value from the "text" argument
703 707 if it begins with the content from the "pattern" argument."""
704 708 if len(args) != 2:
705 709 # i18n: "startswith" is a keyword
706 710 raise error.ParseError(_("startswith expects two arguments"))
707 711
708 patn = stringify(args[0][0](context, mapping, args[0][1]))
709 text = stringify(args[1][0](context, mapping, args[1][1]))
712 patn = evalstring(context, mapping, args[0])
713 text = evalstring(context, mapping, args[1])
710 714 if text.startswith(patn):
711 715 return text
712 716 return ''
713 717
714 718
715 719 def word(context, mapping, args):
716 720 """:word(number, text[, separator]): Return the nth word from a string."""
717 721 if not (2 <= len(args) <= 3):
718 722 # i18n: "word" is a keyword
719 723 raise error.ParseError(_("word expects two or three arguments, got %d")
720 724 % len(args))
721 725
722 726 num = evalinteger(context, mapping, args[0],
723 727 # i18n: "word" is a keyword
724 728 _("word expects an integer index"))
725 text = stringify(args[1][0](context, mapping, args[1][1]))
729 text = evalstring(context, mapping, args[1])
726 730 if len(args) == 3:
727 splitter = stringify(args[2][0](context, mapping, args[2][1]))
731 splitter = evalstring(context, mapping, args[2])
728 732 else:
729 733 splitter = None
730 734
731 735 tokens = text.split(splitter)
732 736 if num >= len(tokens) or num < -len(tokens):
733 737 return ''
734 738 else:
735 739 return tokens[num]
736 740
737 741 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
738 742 exprmethods = {
739 743 "integer": lambda e, c: (runinteger, e[1]),
740 744 "string": lambda e, c: (runstring, e[1]),
741 745 "symbol": lambda e, c: (runsymbol, e[1]),
742 746 "template": buildtemplate,
743 747 "group": lambda e, c: compileexp(e[1], c, exprmethods),
744 748 # ".": buildmember,
745 749 "|": buildfilter,
746 750 "%": buildmap,
747 751 "func": buildfunc,
748 752 }
749 753
750 754 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
751 755 methods = exprmethods.copy()
752 756 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
753 757
754 758 funcs = {
755 759 "date": date,
756 760 "diff": diff,
757 761 "fill": fill,
758 762 "get": get,
759 763 "if": if_,
760 764 "ifcontains": ifcontains,
761 765 "ifeq": ifeq,
762 766 "indent": indent,
763 767 "join": join,
764 768 "label": label,
765 769 "latesttag": latesttag,
766 770 "localdate": localdate,
767 771 "pad": pad,
768 772 "revset": revset,
769 773 "rstdoc": rstdoc,
770 774 "shortest": shortest,
771 775 "startswith": startswith,
772 776 "strip": strip,
773 777 "sub": sub,
774 778 "word": word,
775 779 }
776 780
777 781 # template engine
778 782
779 783 stringify = templatefilters.stringify
780 784
781 785 def _flatten(thing):
782 786 '''yield a single stream from a possibly nested set of iterators'''
783 787 if isinstance(thing, str):
784 788 yield thing
785 789 elif not util.safehasattr(thing, '__iter__'):
786 790 if thing is not None:
787 791 yield str(thing)
788 792 else:
789 793 for i in thing:
790 794 if isinstance(i, str):
791 795 yield i
792 796 elif not util.safehasattr(i, '__iter__'):
793 797 if i is not None:
794 798 yield str(i)
795 799 elif i is not None:
796 800 for j in _flatten(i):
797 801 yield j
798 802
799 803 def unquotestring(s):
800 804 '''unwrap quotes'''
801 805 if len(s) < 2 or s[0] != s[-1]:
802 806 raise SyntaxError(_('unmatched quotes'))
803 807 return s[1:-1]
804 808
805 809 class engine(object):
806 810 '''template expansion engine.
807 811
808 812 template expansion works like this. a map file contains key=value
809 813 pairs. if value is quoted, it is treated as string. otherwise, it
810 814 is treated as name of template file.
811 815
812 816 templater is asked to expand a key in map. it looks up key, and
813 817 looks for strings like this: {foo}. it expands {foo} by looking up
814 818 foo in map, and substituting it. expansion is recursive: it stops
815 819 when there is no more {foo} to replace.
816 820
817 821 expansion also allows formatting and filtering.
818 822
819 823 format uses key to expand each item in list. syntax is
820 824 {key%format}.
821 825
822 826 filter uses function to transform value. syntax is
823 827 {key|filter1|filter2|...}.'''
824 828
825 829 def __init__(self, loader, filters=None, defaults=None):
826 830 self._loader = loader
827 831 if filters is None:
828 832 filters = {}
829 833 self._filters = filters
830 834 if defaults is None:
831 835 defaults = {}
832 836 self._defaults = defaults
833 837 self._cache = {}
834 838
835 839 def _load(self, t):
836 840 '''load, parse, and cache a template'''
837 841 if t not in self._cache:
838 842 # put poison to cut recursion while compiling 't'
839 843 self._cache[t] = [(_runrecursivesymbol, t)]
840 844 try:
841 845 self._cache[t] = compiletemplate(self._loader(t), self)
842 846 except: # re-raises
843 847 del self._cache[t]
844 848 raise
845 849 return self._cache[t]
846 850
847 851 def process(self, t, mapping):
848 852 '''Perform expansion. t is name of map element to expand.
849 853 mapping contains added elements for use during expansion. Is a
850 854 generator.'''
851 855 return _flatten(runtemplate(self, mapping, self._load(t)))
852 856
853 857 engines = {'default': engine}
854 858
855 859 def stylelist():
856 860 paths = templatepaths()
857 861 if not paths:
858 862 return _('no templates found, try `hg debuginstall` for more info')
859 863 dirlist = os.listdir(paths[0])
860 864 stylelist = []
861 865 for file in dirlist:
862 866 split = file.split(".")
863 867 if split[0] == "map-cmdline":
864 868 stylelist.append(split[1])
865 869 return ", ".join(sorted(stylelist))
866 870
867 871 class TemplateNotFound(error.Abort):
868 872 pass
869 873
870 874 class templater(object):
871 875
872 876 def __init__(self, mapfile, filters=None, defaults=None, cache=None,
873 877 minchunk=1024, maxchunk=65536):
874 878 '''set up template engine.
875 879 mapfile is name of file to read map definitions from.
876 880 filters is dict of functions. each transforms a value into another.
877 881 defaults is dict of default map definitions.'''
878 882 if filters is None:
879 883 filters = {}
880 884 if defaults is None:
881 885 defaults = {}
882 886 if cache is None:
883 887 cache = {}
884 888 self.mapfile = mapfile or 'template'
885 889 self.cache = cache.copy()
886 890 self.map = {}
887 891 if mapfile:
888 892 self.base = os.path.dirname(mapfile)
889 893 else:
890 894 self.base = ''
891 895 self.filters = templatefilters.filters.copy()
892 896 self.filters.update(filters)
893 897 self.defaults = defaults
894 898 self.minchunk, self.maxchunk = minchunk, maxchunk
895 899 self.ecache = {}
896 900
897 901 if not mapfile:
898 902 return
899 903 if not os.path.exists(mapfile):
900 904 raise error.Abort(_("style '%s' not found") % mapfile,
901 905 hint=_("available styles: %s") % stylelist())
902 906
903 907 conf = config.config(includepaths=templatepaths())
904 908 conf.read(mapfile)
905 909
906 910 for key, val in conf[''].items():
907 911 if not val:
908 912 raise SyntaxError(_('%s: missing value') % conf.source('', key))
909 913 if val[0] in "'\"":
910 914 try:
911 915 self.cache[key] = unquotestring(val)
912 916 except SyntaxError as inst:
913 917 raise SyntaxError('%s: %s' %
914 918 (conf.source('', key), inst.args[0]))
915 919 else:
916 920 val = 'default', val
917 921 if ':' in val[1]:
918 922 val = val[1].split(':', 1)
919 923 self.map[key] = val[0], os.path.join(self.base, val[1])
920 924
921 925 def __contains__(self, key):
922 926 return key in self.cache or key in self.map
923 927
924 928 def load(self, t):
925 929 '''Get the template for the given template name. Use a local cache.'''
926 930 if t not in self.cache:
927 931 try:
928 932 self.cache[t] = util.readfile(self.map[t][1])
929 933 except KeyError as inst:
930 934 raise TemplateNotFound(_('"%s" not in template map') %
931 935 inst.args[0])
932 936 except IOError as inst:
933 937 raise IOError(inst.args[0], _('template file %s: %s') %
934 938 (self.map[t][1], inst.args[1]))
935 939 return self.cache[t]
936 940
937 941 def __call__(self, t, **mapping):
938 942 ttype = t in self.map and self.map[t][0] or 'default'
939 943 if ttype not in self.ecache:
940 944 self.ecache[ttype] = engines[ttype](self.load,
941 945 self.filters, self.defaults)
942 946 proc = self.ecache[ttype]
943 947
944 948 stream = proc.process(t, mapping)
945 949 if self.minchunk:
946 950 stream = util.increasingchunks(stream, min=self.minchunk,
947 951 max=self.maxchunk)
948 952 return stream
949 953
950 954 def templatepaths():
951 955 '''return locations used for template files.'''
952 956 pathsrel = ['templates']
953 957 paths = [os.path.normpath(os.path.join(util.datapath, f))
954 958 for f in pathsrel]
955 959 return [p for p in paths if os.path.isdir(p)]
956 960
957 961 def templatepath(name):
958 962 '''return location of template file. returns None if not found.'''
959 963 for p in templatepaths():
960 964 f = os.path.join(p, name)
961 965 if os.path.exists(f):
962 966 return f
963 967 return None
964 968
965 969 def stylemap(styles, paths=None):
966 970 """Return path to mapfile for a given style.
967 971
968 972 Searches mapfile in the following locations:
969 973 1. templatepath/style/map
970 974 2. templatepath/map-style
971 975 3. templatepath/map
972 976 """
973 977
974 978 if paths is None:
975 979 paths = templatepaths()
976 980 elif isinstance(paths, str):
977 981 paths = [paths]
978 982
979 983 if isinstance(styles, str):
980 984 styles = [styles]
981 985
982 986 for style in styles:
983 987 # only plain name is allowed to honor template paths
984 988 if (not style
985 989 or style in (os.curdir, os.pardir)
986 990 or os.sep in style
987 991 or os.altsep and os.altsep in style):
988 992 continue
989 993 locations = [os.path.join(style, 'map'), 'map-' + style]
990 994 locations.append('map')
991 995
992 996 for path in paths:
993 997 for location in locations:
994 998 mapfile = os.path.join(path, location)
995 999 if os.path.isfile(mapfile):
996 1000 return style, mapfile
997 1001
998 1002 raise RuntimeError("No hgweb templates found in %r" % paths)
999 1003
1000 1004 # tell hggettext to extract docstrings from these functions:
1001 1005 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now