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