##// END OF EJS Templates
templater: remove __iter__() from _hybrid, resolve it explicitly...
Yuya Nishihara -
r31880:a0f2d83f default
parent child Browse files
Show More
@@ -1,431 +1,432 b''
1 1 # template-filters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import cgi
11 11 import os
12 12 import re
13 13 import time
14 14
15 15 from . import (
16 16 encoding,
17 17 hbisect,
18 18 node,
19 19 registrar,
20 20 templatekw,
21 21 util,
22 22 )
23 23
24 24 urlerr = util.urlerr
25 25 urlreq = util.urlreq
26 26
27 27 # filters are callables like:
28 28 # fn(obj)
29 29 # with:
30 30 # obj - object to be filtered (text, date, list and so on)
31 31 filters = {}
32 32
33 33 templatefilter = registrar.templatefilter(filters)
34 34
35 35 @templatefilter('addbreaks')
36 36 def addbreaks(text):
37 37 """Any text. Add an XHTML "<br />" tag before the end of
38 38 every line except the last.
39 39 """
40 40 return text.replace('\n', '<br/>\n')
41 41
42 42 agescales = [("year", 3600 * 24 * 365, 'Y'),
43 43 ("month", 3600 * 24 * 30, 'M'),
44 44 ("week", 3600 * 24 * 7, 'W'),
45 45 ("day", 3600 * 24, 'd'),
46 46 ("hour", 3600, 'h'),
47 47 ("minute", 60, 'm'),
48 48 ("second", 1, 's')]
49 49
50 50 @templatefilter('age')
51 51 def age(date, abbrev=False):
52 52 """Date. Returns a human-readable date/time difference between the
53 53 given date/time and the current date/time.
54 54 """
55 55
56 56 def plural(t, c):
57 57 if c == 1:
58 58 return t
59 59 return t + "s"
60 60 def fmt(t, c, a):
61 61 if abbrev:
62 62 return "%d%s" % (c, a)
63 63 return "%d %s" % (c, plural(t, c))
64 64
65 65 now = time.time()
66 66 then = date[0]
67 67 future = False
68 68 if then > now:
69 69 future = True
70 70 delta = max(1, int(then - now))
71 71 if delta > agescales[0][1] * 30:
72 72 return 'in the distant future'
73 73 else:
74 74 delta = max(1, int(now - then))
75 75 if delta > agescales[0][1] * 2:
76 76 return util.shortdate(date)
77 77
78 78 for t, s, a in agescales:
79 79 n = delta // s
80 80 if n >= 2 or s == 1:
81 81 if future:
82 82 return '%s from now' % fmt(t, n, a)
83 83 return '%s ago' % fmt(t, n, a)
84 84
85 85 @templatefilter('basename')
86 86 def basename(path):
87 87 """Any text. Treats the text as a path, and returns the last
88 88 component of the path after splitting by the path separator
89 89 (ignoring trailing separators). For example, "foo/bar/baz" becomes
90 90 "baz" and "foo/bar//" becomes "bar".
91 91 """
92 92 return os.path.basename(path)
93 93
94 94 @templatefilter('count')
95 95 def count(i):
96 96 """List or text. Returns the length as an integer."""
97 97 return len(i)
98 98
99 99 @templatefilter('domain')
100 100 def domain(author):
101 101 """Any text. Finds the first string that looks like an email
102 102 address, and extracts just the domain component. Example: ``User
103 103 <user@example.com>`` becomes ``example.com``.
104 104 """
105 105 f = author.find('@')
106 106 if f == -1:
107 107 return ''
108 108 author = author[f + 1:]
109 109 f = author.find('>')
110 110 if f >= 0:
111 111 author = author[:f]
112 112 return author
113 113
114 114 @templatefilter('email')
115 115 def email(text):
116 116 """Any text. Extracts the first string that looks like an email
117 117 address. Example: ``User <user@example.com>`` becomes
118 118 ``user@example.com``.
119 119 """
120 120 return util.email(text)
121 121
122 122 @templatefilter('escape')
123 123 def escape(text):
124 124 """Any text. Replaces the special XML/XHTML characters "&", "<"
125 125 and ">" with XML entities, and filters out NUL characters.
126 126 """
127 127 return cgi.escape(text.replace('\0', ''), True)
128 128
129 129 para_re = None
130 130 space_re = None
131 131
132 132 def fill(text, width, initindent='', hangindent=''):
133 133 '''fill many paragraphs with optional indentation.'''
134 134 global para_re, space_re
135 135 if para_re is None:
136 136 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
137 137 space_re = re.compile(r' +')
138 138
139 139 def findparas():
140 140 start = 0
141 141 while True:
142 142 m = para_re.search(text, start)
143 143 if not m:
144 144 uctext = unicode(text[start:], encoding.encoding)
145 145 w = len(uctext)
146 146 while 0 < w and uctext[w - 1].isspace():
147 147 w -= 1
148 148 yield (uctext[:w].encode(encoding.encoding),
149 149 uctext[w:].encode(encoding.encoding))
150 150 break
151 151 yield text[start:m.start(0)], m.group(1)
152 152 start = m.end(1)
153 153
154 154 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
155 155 width, initindent, hangindent) + rest
156 156 for para, rest in findparas()])
157 157
158 158 @templatefilter('fill68')
159 159 def fill68(text):
160 160 """Any text. Wraps the text to fit in 68 columns."""
161 161 return fill(text, 68)
162 162
163 163 @templatefilter('fill76')
164 164 def fill76(text):
165 165 """Any text. Wraps the text to fit in 76 columns."""
166 166 return fill(text, 76)
167 167
168 168 @templatefilter('firstline')
169 169 def firstline(text):
170 170 """Any text. Returns the first line of text."""
171 171 try:
172 172 return text.splitlines(True)[0].rstrip('\r\n')
173 173 except IndexError:
174 174 return ''
175 175
176 176 @templatefilter('hex')
177 177 def hexfilter(text):
178 178 """Any text. Convert a binary Mercurial node identifier into
179 179 its long hexadecimal representation.
180 180 """
181 181 return node.hex(text)
182 182
183 183 @templatefilter('hgdate')
184 184 def hgdate(text):
185 185 """Date. Returns the date as a pair of numbers: "1157407993
186 186 25200" (Unix timestamp, timezone offset).
187 187 """
188 188 return "%d %d" % text
189 189
190 190 @templatefilter('isodate')
191 191 def isodate(text):
192 192 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
193 193 +0200".
194 194 """
195 195 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
196 196
197 197 @templatefilter('isodatesec')
198 198 def isodatesec(text):
199 199 """Date. Returns the date in ISO 8601 format, including
200 200 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
201 201 filter.
202 202 """
203 203 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
204 204
205 205 def indent(text, prefix):
206 206 '''indent each non-empty line of text after first with prefix.'''
207 207 lines = text.splitlines()
208 208 num_lines = len(lines)
209 209 endswithnewline = text[-1:] == '\n'
210 210 def indenter():
211 211 for i in xrange(num_lines):
212 212 l = lines[i]
213 213 if i and l.strip():
214 214 yield prefix
215 215 yield l
216 216 if i < num_lines - 1 or endswithnewline:
217 217 yield '\n'
218 218 return "".join(indenter())
219 219
220 220 @templatefilter('json')
221 221 def json(obj, paranoid=True):
222 222 if obj is None:
223 223 return 'null'
224 224 elif obj is False:
225 225 return 'false'
226 226 elif obj is True:
227 227 return 'true'
228 228 elif isinstance(obj, (int, long, float)):
229 229 return str(obj)
230 230 elif isinstance(obj, str):
231 231 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
232 232 elif util.safehasattr(obj, 'keys'):
233 233 out = ['%s: %s' % (json(k), json(v))
234 234 for k, v in sorted(obj.iteritems())]
235 235 return '{' + ', '.join(out) + '}'
236 236 elif util.safehasattr(obj, '__iter__'):
237 237 out = [json(i) for i in obj]
238 238 return '[' + ', '.join(out) + ']'
239 239 else:
240 240 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
241 241
242 242 @templatefilter('lower')
243 243 def lower(text):
244 244 """Any text. Converts the text to lowercase."""
245 245 return encoding.lower(text)
246 246
247 247 @templatefilter('nonempty')
248 248 def nonempty(str):
249 249 """Any text. Returns '(none)' if the string is empty."""
250 250 return str or "(none)"
251 251
252 252 @templatefilter('obfuscate')
253 253 def obfuscate(text):
254 254 """Any text. Returns the input text rendered as a sequence of
255 255 XML entities.
256 256 """
257 257 text = unicode(text, encoding.encoding, 'replace')
258 258 return ''.join(['&#%d;' % ord(c) for c in text])
259 259
260 260 @templatefilter('permissions')
261 261 def permissions(flags):
262 262 if "l" in flags:
263 263 return "lrwxrwxrwx"
264 264 if "x" in flags:
265 265 return "-rwxr-xr-x"
266 266 return "-rw-r--r--"
267 267
268 268 @templatefilter('person')
269 269 def person(author):
270 270 """Any text. Returns the name before an email address,
271 271 interpreting it as per RFC 5322.
272 272
273 273 >>> person('foo@bar')
274 274 'foo'
275 275 >>> person('Foo Bar <foo@bar>')
276 276 'Foo Bar'
277 277 >>> person('"Foo Bar" <foo@bar>')
278 278 'Foo Bar'
279 279 >>> person('"Foo \"buz\" Bar" <foo@bar>')
280 280 'Foo "buz" Bar'
281 281 >>> # The following are invalid, but do exist in real-life
282 282 ...
283 283 >>> person('Foo "buz" Bar <foo@bar>')
284 284 'Foo "buz" Bar'
285 285 >>> person('"Foo Bar <foo@bar>')
286 286 'Foo Bar'
287 287 """
288 288 if '@' not in author:
289 289 return author
290 290 f = author.find('<')
291 291 if f != -1:
292 292 return author[:f].strip(' "').replace('\\"', '"')
293 293 f = author.find('@')
294 294 return author[:f].replace('.', ' ')
295 295
296 296 @templatefilter('revescape')
297 297 def revescape(text):
298 298 """Any text. Escapes all "special" characters, except @.
299 299 Forward slashes are escaped twice to prevent web servers from prematurely
300 300 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
301 301 """
302 302 return urlreq.quote(text, safe='/@').replace('/', '%252F')
303 303
304 304 @templatefilter('rfc3339date')
305 305 def rfc3339date(text):
306 306 """Date. Returns a date using the Internet date format
307 307 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
308 308 """
309 309 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
310 310
311 311 @templatefilter('rfc822date')
312 312 def rfc822date(text):
313 313 """Date. Returns a date using the same format used in email
314 314 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
315 315 """
316 316 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
317 317
318 318 @templatefilter('short')
319 319 def short(text):
320 320 """Changeset hash. Returns the short form of a changeset hash,
321 321 i.e. a 12 hexadecimal digit string.
322 322 """
323 323 return text[:12]
324 324
325 325 @templatefilter('shortbisect')
326 326 def shortbisect(text):
327 327 """Any text. Treats `text` as a bisection status, and
328 328 returns a single-character representing the status (G: good, B: bad,
329 329 S: skipped, U: untested, I: ignored). Returns single space if `text`
330 330 is not a valid bisection status.
331 331 """
332 332 return hbisect.shortlabel(text) or ' '
333 333
334 334 @templatefilter('shortdate')
335 335 def shortdate(text):
336 336 """Date. Returns a date like "2006-09-18"."""
337 337 return util.shortdate(text)
338 338
339 339 @templatefilter('splitlines')
340 340 def splitlines(text):
341 341 """Any text. Split text into a list of lines."""
342 342 return templatekw.showlist('line', text.splitlines(), 'lines')
343 343
344 344 @templatefilter('stringescape')
345 345 def stringescape(text):
346 346 return util.escapestr(text)
347 347
348 348 @templatefilter('stringify')
349 349 def stringify(thing):
350 350 """Any type. Turns the value into text by converting values into
351 351 text and concatenating them.
352 352 """
353 thing = templatekw.unwraphybrid(thing)
353 354 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
354 355 return "".join([stringify(t) for t in thing if t is not None])
355 356 if thing is None:
356 357 return ""
357 358 return str(thing)
358 359
359 360 @templatefilter('stripdir')
360 361 def stripdir(text):
361 362 """Treat the text as path and strip a directory level, if
362 363 possible. For example, "foo" and "foo/bar" becomes "foo".
363 364 """
364 365 dir = os.path.dirname(text)
365 366 if dir == "":
366 367 return os.path.basename(text)
367 368 else:
368 369 return dir
369 370
370 371 @templatefilter('tabindent')
371 372 def tabindent(text):
372 373 """Any text. Returns the text, with every non-empty line
373 374 except the first starting with a tab character.
374 375 """
375 376 return indent(text, '\t')
376 377
377 378 @templatefilter('upper')
378 379 def upper(text):
379 380 """Any text. Converts the text to uppercase."""
380 381 return encoding.upper(text)
381 382
382 383 @templatefilter('urlescape')
383 384 def urlescape(text):
384 385 """Any text. Escapes all "special" characters. For example,
385 386 "foo bar" becomes "foo%20bar".
386 387 """
387 388 return urlreq.quote(text)
388 389
389 390 @templatefilter('user')
390 391 def userfilter(text):
391 392 """Any text. Returns a short representation of a user name or email
392 393 address."""
393 394 return util.shortuser(text)
394 395
395 396 @templatefilter('emailuser')
396 397 def emailuser(text):
397 398 """Any text. Returns the user portion of an email address."""
398 399 return util.emailuser(text)
399 400
400 401 @templatefilter('utf8')
401 402 def utf8(text):
402 403 """Any text. Converts from the local character encoding to UTF-8."""
403 404 return encoding.fromlocal(text)
404 405
405 406 @templatefilter('xmlescape')
406 407 def xmlescape(text):
407 408 text = (text
408 409 .replace('&', '&amp;')
409 410 .replace('<', '&lt;')
410 411 .replace('>', '&gt;')
411 412 .replace('"', '&quot;')
412 413 .replace("'", '&#39;')) # &apos; invalid in HTML
413 414 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
414 415
415 416 def websub(text, websubtable):
416 417 """:websub: Any text. Only applies to hgweb. Applies the regular
417 418 expression replacements defined in the websub section.
418 419 """
419 420 if websubtable:
420 421 for regexp, format in websubtable:
421 422 text = regexp.sub(format, text)
422 423 return text
423 424
424 425 def loadfilter(ui, extname, registrarobj):
425 426 """Load template filter from specified registrarobj
426 427 """
427 428 for name, func in registrarobj._table.iteritems():
428 429 filters[name] = func
429 430
430 431 # tell hggettext to extract docstrings from these functions:
431 432 i18nfunctions = filters.values()
@@ -1,649 +1,654 b''
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from .node import hex, nullid
12 12 from . import (
13 13 encoding,
14 14 error,
15 15 hbisect,
16 16 patch,
17 17 registrar,
18 18 scmutil,
19 19 util,
20 20 )
21 21
22 22 class _hybrid(object):
23 23 """Wrapper for list or dict to support legacy template
24 24
25 25 This class allows us to handle both:
26 26 - "{files}" (legacy command-line-specific list hack) and
27 27 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
28 28 and to access raw values:
29 29 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
30 30 - "{get(extras, key)}"
31 31 """
32 32
33 33 def __init__(self, gen, values, makemap, joinfmt):
34 34 self.gen = gen
35 35 self.values = values
36 36 self._makemap = makemap
37 37 self.joinfmt = joinfmt
38 def __iter__(self):
39 return self.gen
40 38 def itermaps(self):
41 39 makemap = self._makemap
42 40 for x in self.values:
43 41 yield makemap(x)
44 42 def __contains__(self, x):
45 43 return x in self.values
46 44 def __len__(self):
47 45 return len(self.values)
48 46 def __getattr__(self, name):
49 47 if name != 'get':
50 48 raise AttributeError(name)
51 49 return getattr(self.values, name)
52 50
51 def unwraphybrid(thing):
52 """Return an object which can be stringified possibly by using a legacy
53 template"""
54 if not util.safehasattr(thing, 'gen'):
55 return thing
56 return thing.gen
57
53 58 def showlist(name, values, plural=None, element=None, separator=' ', **args):
54 59 if not element:
55 60 element = name
56 61 f = _showlist(name, values, plural, separator, **args)
57 62 return _hybrid(f, values, lambda x: {element: x}, lambda d: d[element])
58 63
59 64 def _showlist(name, values, plural=None, separator=' ', **args):
60 65 '''expand set of values.
61 66 name is name of key in template map.
62 67 values is list of strings or dicts.
63 68 plural is plural of name, if not simply name + 's'.
64 69 separator is used to join values as a string
65 70
66 71 expansion works like this, given name 'foo'.
67 72
68 73 if values is empty, expand 'no_foos'.
69 74
70 75 if 'foo' not in template map, return values as a string,
71 76 joined by 'separator'.
72 77
73 78 expand 'start_foos'.
74 79
75 80 for each value, expand 'foo'. if 'last_foo' in template
76 81 map, expand it instead of 'foo' for last key.
77 82
78 83 expand 'end_foos'.
79 84 '''
80 85 templ = args['templ']
81 86 if plural:
82 87 names = plural
83 88 else: names = name + 's'
84 89 if not values:
85 90 noname = 'no_' + names
86 91 if noname in templ:
87 92 yield templ(noname, **args)
88 93 return
89 94 if name not in templ:
90 95 if isinstance(values[0], str):
91 96 yield separator.join(values)
92 97 else:
93 98 for v in values:
94 99 yield dict(v, **args)
95 100 return
96 101 startname = 'start_' + names
97 102 if startname in templ:
98 103 yield templ(startname, **args)
99 104 vargs = args.copy()
100 105 def one(v, tag=name):
101 106 try:
102 107 vargs.update(v)
103 108 except (AttributeError, ValueError):
104 109 try:
105 110 for a, b in v:
106 111 vargs[a] = b
107 112 except ValueError:
108 113 vargs[name] = v
109 114 return templ(tag, **vargs)
110 115 lastname = 'last_' + name
111 116 if lastname in templ:
112 117 last = values.pop()
113 118 else:
114 119 last = None
115 120 for v in values:
116 121 yield one(v)
117 122 if last is not None:
118 123 yield one(last, tag=lastname)
119 124 endname = 'end_' + names
120 125 if endname in templ:
121 126 yield templ(endname, **args)
122 127
123 128 def _formatrevnode(ctx):
124 129 """Format changeset as '{rev}:{node|formatnode}', which is the default
125 130 template provided by cmdutil.changeset_templater"""
126 131 repo = ctx.repo()
127 132 if repo.ui.debugflag:
128 133 hexnode = ctx.hex()
129 134 else:
130 135 hexnode = ctx.hex()[:12]
131 136 return '%d:%s' % (scmutil.intrev(ctx.rev()), hexnode)
132 137
133 138 def getfiles(repo, ctx, revcache):
134 139 if 'files' not in revcache:
135 140 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
136 141 return revcache['files']
137 142
138 143 def getlatesttags(repo, ctx, cache, pattern=None):
139 144 '''return date, distance and name for the latest tag of rev'''
140 145
141 146 cachename = 'latesttags'
142 147 if pattern is not None:
143 148 cachename += '-' + pattern
144 149 match = util.stringmatcher(pattern)[2]
145 150 else:
146 151 match = util.always
147 152
148 153 if cachename not in cache:
149 154 # Cache mapping from rev to a tuple with tag date, tag
150 155 # distance and tag name
151 156 cache[cachename] = {-1: (0, 0, ['null'])}
152 157 latesttags = cache[cachename]
153 158
154 159 rev = ctx.rev()
155 160 todo = [rev]
156 161 while todo:
157 162 rev = todo.pop()
158 163 if rev in latesttags:
159 164 continue
160 165 ctx = repo[rev]
161 166 tags = [t for t in ctx.tags()
162 167 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
163 168 and match(t))]
164 169 if tags:
165 170 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
166 171 continue
167 172 try:
168 173 # The tuples are laid out so the right one can be found by
169 174 # comparison.
170 175 pdate, pdist, ptag = max(
171 176 latesttags[p.rev()] for p in ctx.parents())
172 177 except KeyError:
173 178 # Cache miss - recurse
174 179 todo.append(rev)
175 180 todo.extend(p.rev() for p in ctx.parents())
176 181 continue
177 182 latesttags[rev] = pdate, pdist + 1, ptag
178 183 return latesttags[rev]
179 184
180 185 def getrenamedfn(repo, endrev=None):
181 186 rcache = {}
182 187 if endrev is None:
183 188 endrev = len(repo)
184 189
185 190 def getrenamed(fn, rev):
186 191 '''looks up all renames for a file (up to endrev) the first
187 192 time the file is given. It indexes on the changerev and only
188 193 parses the manifest if linkrev != changerev.
189 194 Returns rename info for fn at changerev rev.'''
190 195 if fn not in rcache:
191 196 rcache[fn] = {}
192 197 fl = repo.file(fn)
193 198 for i in fl:
194 199 lr = fl.linkrev(i)
195 200 renamed = fl.renamed(fl.node(i))
196 201 rcache[fn][lr] = renamed
197 202 if lr >= endrev:
198 203 break
199 204 if rev in rcache[fn]:
200 205 return rcache[fn][rev]
201 206
202 207 # If linkrev != rev (i.e. rev not found in rcache) fallback to
203 208 # filectx logic.
204 209 try:
205 210 return repo[rev][fn].renamed()
206 211 except error.LookupError:
207 212 return None
208 213
209 214 return getrenamed
210 215
211 216 # default templates internally used for rendering of lists
212 217 defaulttempl = {
213 218 'parent': '{rev}:{node|formatnode} ',
214 219 'manifest': '{rev}:{node|formatnode}',
215 220 'file_copy': '{name} ({source})',
216 221 'envvar': '{key}={value}',
217 222 'extra': '{key}={value|stringescape}'
218 223 }
219 224 # filecopy is preserved for compatibility reasons
220 225 defaulttempl['filecopy'] = defaulttempl['file_copy']
221 226
222 227 # keywords are callables like:
223 228 # fn(repo, ctx, templ, cache, revcache, **args)
224 229 # with:
225 230 # repo - current repository instance
226 231 # ctx - the changectx being displayed
227 232 # templ - the templater instance
228 233 # cache - a cache dictionary for the whole templater run
229 234 # revcache - a cache dictionary for the current revision
230 235 keywords = {}
231 236
232 237 templatekeyword = registrar.templatekeyword(keywords)
233 238
234 239 @templatekeyword('author')
235 240 def showauthor(repo, ctx, templ, **args):
236 241 """String. The unmodified author of the changeset."""
237 242 return ctx.user()
238 243
239 244 @templatekeyword('bisect')
240 245 def showbisect(repo, ctx, templ, **args):
241 246 """String. The changeset bisection status."""
242 247 return hbisect.label(repo, ctx.node())
243 248
244 249 @templatekeyword('branch')
245 250 def showbranch(**args):
246 251 """String. The name of the branch on which the changeset was
247 252 committed.
248 253 """
249 254 return args['ctx'].branch()
250 255
251 256 @templatekeyword('branches')
252 257 def showbranches(**args):
253 258 """List of strings. The name of the branch on which the
254 259 changeset was committed. Will be empty if the branch name was
255 260 default. (DEPRECATED)
256 261 """
257 262 branch = args['ctx'].branch()
258 263 if branch != 'default':
259 264 return showlist('branch', [branch], plural='branches', **args)
260 265 return showlist('branch', [], plural='branches', **args)
261 266
262 267 @templatekeyword('bookmarks')
263 268 def showbookmarks(**args):
264 269 """List of strings. Any bookmarks associated with the
265 270 changeset. Also sets 'active', the name of the active bookmark.
266 271 """
267 272 repo = args['ctx']._repo
268 273 bookmarks = args['ctx'].bookmarks()
269 274 active = repo._activebookmark
270 275 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
271 276 f = _showlist('bookmark', bookmarks, **args)
272 277 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
273 278
274 279 @templatekeyword('children')
275 280 def showchildren(**args):
276 281 """List of strings. The children of the changeset."""
277 282 ctx = args['ctx']
278 283 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
279 284 return showlist('children', childrevs, element='child', **args)
280 285
281 286 # Deprecated, but kept alive for help generation a purpose.
282 287 @templatekeyword('currentbookmark')
283 288 def showcurrentbookmark(**args):
284 289 """String. The active bookmark, if it is
285 290 associated with the changeset (DEPRECATED)"""
286 291 return showactivebookmark(**args)
287 292
288 293 @templatekeyword('activebookmark')
289 294 def showactivebookmark(**args):
290 295 """String. The active bookmark, if it is
291 296 associated with the changeset"""
292 297 active = args['repo']._activebookmark
293 298 if active and active in args['ctx'].bookmarks():
294 299 return active
295 300 return ''
296 301
297 302 @templatekeyword('date')
298 303 def showdate(repo, ctx, templ, **args):
299 304 """Date information. The date when the changeset was committed."""
300 305 return ctx.date()
301 306
302 307 @templatekeyword('desc')
303 308 def showdescription(repo, ctx, templ, **args):
304 309 """String. The text of the changeset description."""
305 310 s = ctx.description()
306 311 if isinstance(s, encoding.localstr):
307 312 # try hard to preserve utf-8 bytes
308 313 return encoding.tolocal(encoding.fromlocal(s).strip())
309 314 else:
310 315 return s.strip()
311 316
312 317 @templatekeyword('diffstat')
313 318 def showdiffstat(repo, ctx, templ, **args):
314 319 """String. Statistics of changes with the following format:
315 320 "modified files: +added/-removed lines"
316 321 """
317 322 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
318 323 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
319 324 return '%s: +%s/-%s' % (len(stats), adds, removes)
320 325
321 326 @templatekeyword('envvars')
322 327 def showenvvars(repo, **args):
323 328 """A dictionary of environment variables. (EXPERIMENTAL)"""
324 329
325 330 env = repo.ui.exportableenviron()
326 331 env = util.sortdict((k, env[k]) for k in sorted(env))
327 332 makemap = lambda k: {'key': k, 'value': env[k]}
328 333 c = [makemap(k) for k in env]
329 334 f = _showlist('envvar', c, plural='envvars', **args)
330 335 return _hybrid(f, env, makemap,
331 336 lambda x: '%s=%s' % (x['key'], x['value']))
332 337
333 338 @templatekeyword('extras')
334 339 def showextras(**args):
335 340 """List of dicts with key, value entries of the 'extras'
336 341 field of this changeset."""
337 342 extras = args['ctx'].extra()
338 343 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
339 344 makemap = lambda k: {'key': k, 'value': extras[k]}
340 345 c = [makemap(k) for k in extras]
341 346 f = _showlist('extra', c, plural='extras', **args)
342 347 return _hybrid(f, extras, makemap,
343 348 lambda x: '%s=%s' % (x['key'], util.escapestr(x['value'])))
344 349
345 350 @templatekeyword('file_adds')
346 351 def showfileadds(**args):
347 352 """List of strings. Files added by this changeset."""
348 353 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
349 354 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
350 355 element='file', **args)
351 356
352 357 @templatekeyword('file_copies')
353 358 def showfilecopies(**args):
354 359 """List of strings. Files copied in this changeset with
355 360 their sources.
356 361 """
357 362 cache, ctx = args['cache'], args['ctx']
358 363 copies = args['revcache'].get('copies')
359 364 if copies is None:
360 365 if 'getrenamed' not in cache:
361 366 cache['getrenamed'] = getrenamedfn(args['repo'])
362 367 copies = []
363 368 getrenamed = cache['getrenamed']
364 369 for fn in ctx.files():
365 370 rename = getrenamed(fn, ctx.rev())
366 371 if rename:
367 372 copies.append((fn, rename[0]))
368 373
369 374 copies = util.sortdict(copies)
370 375 makemap = lambda k: {'name': k, 'source': copies[k]}
371 376 c = [makemap(k) for k in copies]
372 377 f = _showlist('file_copy', c, plural='file_copies', **args)
373 378 return _hybrid(f, copies, makemap,
374 379 lambda x: '%s (%s)' % (x['name'], x['source']))
375 380
376 381 # showfilecopiesswitch() displays file copies only if copy records are
377 382 # provided before calling the templater, usually with a --copies
378 383 # command line switch.
379 384 @templatekeyword('file_copies_switch')
380 385 def showfilecopiesswitch(**args):
381 386 """List of strings. Like "file_copies" but displayed
382 387 only if the --copied switch is set.
383 388 """
384 389 copies = args['revcache'].get('copies') or []
385 390 copies = util.sortdict(copies)
386 391 makemap = lambda k: {'name': k, 'source': copies[k]}
387 392 c = [makemap(k) for k in copies]
388 393 f = _showlist('file_copy', c, plural='file_copies', **args)
389 394 return _hybrid(f, copies, makemap,
390 395 lambda x: '%s (%s)' % (x['name'], x['source']))
391 396
392 397 @templatekeyword('file_dels')
393 398 def showfiledels(**args):
394 399 """List of strings. Files removed by this changeset."""
395 400 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
396 401 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
397 402 element='file', **args)
398 403
399 404 @templatekeyword('file_mods')
400 405 def showfilemods(**args):
401 406 """List of strings. Files modified by this changeset."""
402 407 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
403 408 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
404 409 element='file', **args)
405 410
406 411 @templatekeyword('files')
407 412 def showfiles(**args):
408 413 """List of strings. All files modified, added, or removed by this
409 414 changeset.
410 415 """
411 416 return showlist('file', args['ctx'].files(), **args)
412 417
413 418 @templatekeyword('graphnode')
414 419 def showgraphnode(repo, ctx, **args):
415 420 """String. The character representing the changeset node in
416 421 an ASCII revision graph"""
417 422 wpnodes = repo.dirstate.parents()
418 423 if wpnodes[1] == nullid:
419 424 wpnodes = wpnodes[:1]
420 425 if ctx.node() in wpnodes:
421 426 return '@'
422 427 elif ctx.obsolete():
423 428 return 'x'
424 429 elif ctx.closesbranch():
425 430 return '_'
426 431 else:
427 432 return 'o'
428 433
429 434 @templatekeyword('index')
430 435 def showindex(**args):
431 436 """Integer. The current iteration of the loop. (0 indexed)"""
432 437 # just hosts documentation; should be overridden by template mapping
433 438 raise error.Abort(_("can't use index in this context"))
434 439
435 440 @templatekeyword('latesttag')
436 441 def showlatesttag(**args):
437 442 """List of strings. The global tags on the most recent globally
438 443 tagged ancestor of this changeset. If no such tags exist, the list
439 444 consists of the single string "null".
440 445 """
441 446 return showlatesttags(None, **args)
442 447
443 448 def showlatesttags(pattern, **args):
444 449 """helper method for the latesttag keyword and function"""
445 450 repo, ctx = args['repo'], args['ctx']
446 451 cache = args['cache']
447 452 latesttags = getlatesttags(repo, ctx, cache, pattern)
448 453
449 454 # latesttag[0] is an implementation detail for sorting csets on different
450 455 # branches in a stable manner- it is the date the tagged cset was created,
451 456 # not the date the tag was created. Therefore it isn't made visible here.
452 457 makemap = lambda v: {
453 458 'changes': _showchangessincetag,
454 459 'distance': latesttags[1],
455 460 'latesttag': v, # BC with {latesttag % '{latesttag}'}
456 461 'tag': v
457 462 }
458 463
459 464 tags = latesttags[2]
460 465 f = _showlist('latesttag', tags, separator=':', **args)
461 466 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
462 467
463 468 @templatekeyword('latesttagdistance')
464 469 def showlatesttagdistance(repo, ctx, templ, cache, **args):
465 470 """Integer. Longest path to the latest tag."""
466 471 return getlatesttags(repo, ctx, cache)[1]
467 472
468 473 @templatekeyword('changessincelatesttag')
469 474 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
470 475 """Integer. All ancestors not in the latest tag."""
471 476 latesttag = getlatesttags(repo, ctx, cache)[2][0]
472 477
473 478 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
474 479
475 480 def _showchangessincetag(repo, ctx, **args):
476 481 offset = 0
477 482 revs = [ctx.rev()]
478 483 tag = args['tag']
479 484
480 485 # The only() revset doesn't currently support wdir()
481 486 if ctx.rev() is None:
482 487 offset = 1
483 488 revs = [p.rev() for p in ctx.parents()]
484 489
485 490 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
486 491
487 492 @templatekeyword('manifest')
488 493 def showmanifest(**args):
489 494 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
490 495 mnode = ctx.manifestnode()
491 496 if mnode is None:
492 497 # just avoid crash, we might want to use the 'ff...' hash in future
493 498 return
494 499 args = args.copy()
495 500 args.update({'rev': repo.manifestlog._revlog.rev(mnode),
496 501 'node': hex(mnode)})
497 502 return templ('manifest', **args)
498 503
499 504 def shownames(namespace, **args):
500 505 """helper method to generate a template keyword for a namespace"""
501 506 ctx = args['ctx']
502 507 repo = ctx.repo()
503 508 ns = repo.names[namespace]
504 509 names = ns.names(repo, ctx.node())
505 510 return showlist(ns.templatename, names, plural=namespace, **args)
506 511
507 512 @templatekeyword('namespaces')
508 513 def shownamespaces(**args):
509 514 """Dict of lists. Names attached to this changeset per
510 515 namespace."""
511 516 ctx = args['ctx']
512 517 repo = ctx.repo()
513 518 namespaces = util.sortdict((k, showlist('name', ns.names(repo, ctx.node()),
514 519 **args))
515 520 for k, ns in repo.names.iteritems())
516 521 f = _showlist('namespace', list(namespaces), **args)
517 522 return _hybrid(f, namespaces,
518 523 lambda k: {'namespace': k, 'names': namespaces[k]},
519 524 lambda x: x['namespace'])
520 525
521 526 @templatekeyword('node')
522 527 def shownode(repo, ctx, templ, **args):
523 528 """String. The changeset identification hash, as a 40 hexadecimal
524 529 digit string.
525 530 """
526 531 return ctx.hex()
527 532
528 533 @templatekeyword('obsolete')
529 534 def showobsolete(repo, ctx, templ, **args):
530 535 """String. Whether the changeset is obsolete.
531 536 """
532 537 if ctx.obsolete():
533 538 return 'obsolete'
534 539 return ''
535 540
536 541 @templatekeyword('p1rev')
537 542 def showp1rev(repo, ctx, templ, **args):
538 543 """Integer. The repository-local revision number of the changeset's
539 544 first parent, or -1 if the changeset has no parents."""
540 545 return ctx.p1().rev()
541 546
542 547 @templatekeyword('p2rev')
543 548 def showp2rev(repo, ctx, templ, **args):
544 549 """Integer. The repository-local revision number of the changeset's
545 550 second parent, or -1 if the changeset has no second parent."""
546 551 return ctx.p2().rev()
547 552
548 553 @templatekeyword('p1node')
549 554 def showp1node(repo, ctx, templ, **args):
550 555 """String. The identification hash of the changeset's first parent,
551 556 as a 40 digit hexadecimal string. If the changeset has no parents, all
552 557 digits are 0."""
553 558 return ctx.p1().hex()
554 559
555 560 @templatekeyword('p2node')
556 561 def showp2node(repo, ctx, templ, **args):
557 562 """String. The identification hash of the changeset's second
558 563 parent, as a 40 digit hexadecimal string. If the changeset has no second
559 564 parent, all digits are 0."""
560 565 return ctx.p2().hex()
561 566
562 567 @templatekeyword('parents')
563 568 def showparents(**args):
564 569 """List of strings. The parents of the changeset in "rev:node"
565 570 format. If the changeset has only one "natural" parent (the predecessor
566 571 revision) nothing is shown."""
567 572 repo = args['repo']
568 573 ctx = args['ctx']
569 574 pctxs = scmutil.meaningfulparents(repo, ctx)
570 575 prevs = [str(p.rev()) for p in pctxs] # ifcontains() needs a list of str
571 576 parents = [[('rev', p.rev()),
572 577 ('node', p.hex()),
573 578 ('phase', p.phasestr())]
574 579 for p in pctxs]
575 580 f = _showlist('parent', parents, **args)
576 581 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
577 582 lambda d: _formatrevnode(d['ctx']))
578 583
579 584 @templatekeyword('phase')
580 585 def showphase(repo, ctx, templ, **args):
581 586 """String. The changeset phase name."""
582 587 return ctx.phasestr()
583 588
584 589 @templatekeyword('phaseidx')
585 590 def showphaseidx(repo, ctx, templ, **args):
586 591 """Integer. The changeset phase index."""
587 592 return ctx.phase()
588 593
589 594 @templatekeyword('rev')
590 595 def showrev(repo, ctx, templ, **args):
591 596 """Integer. The repository-local changeset revision number."""
592 597 return scmutil.intrev(ctx.rev())
593 598
594 599 def showrevslist(name, revs, **args):
595 600 """helper to generate a list of revisions in which a mapped template will
596 601 be evaluated"""
597 602 repo = args['ctx'].repo()
598 603 revs = [str(r) for r in revs] # ifcontains() needs a list of str
599 604 f = _showlist(name, revs, **args)
600 605 return _hybrid(f, revs,
601 606 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
602 607 lambda d: d[name])
603 608
604 609 @templatekeyword('subrepos')
605 610 def showsubrepos(**args):
606 611 """List of strings. Updated subrepositories in the changeset."""
607 612 ctx = args['ctx']
608 613 substate = ctx.substate
609 614 if not substate:
610 615 return showlist('subrepo', [], **args)
611 616 psubstate = ctx.parents()[0].substate or {}
612 617 subrepos = []
613 618 for sub in substate:
614 619 if sub not in psubstate or substate[sub] != psubstate[sub]:
615 620 subrepos.append(sub) # modified or newly added in ctx
616 621 for sub in psubstate:
617 622 if sub not in substate:
618 623 subrepos.append(sub) # removed in ctx
619 624 return showlist('subrepo', sorted(subrepos), **args)
620 625
621 626 # don't remove "showtags" definition, even though namespaces will put
622 627 # a helper function for "tags" keyword into "keywords" map automatically,
623 628 # because online help text is built without namespaces initialization
624 629 @templatekeyword('tags')
625 630 def showtags(**args):
626 631 """List of strings. Any tags associated with the changeset."""
627 632 return shownames('tags', **args)
628 633
629 634 def loadkeyword(ui, extname, registrarobj):
630 635 """Load template keyword from specified registrarobj
631 636 """
632 637 for name, func in registrarobj._table.iteritems():
633 638 keywords[name] = func
634 639
635 640 @templatekeyword('termwidth')
636 641 def termwidth(repo, ctx, templ, **args):
637 642 """Integer. The width of the current terminal."""
638 643 return repo.ui.termwidth()
639 644
640 645 @templatekeyword('troubles')
641 646 def showtroubles(**args):
642 647 """List of strings. Evolution troubles affecting the changeset.
643 648
644 649 (EXPERIMENTAL)
645 650 """
646 651 return showlist('trouble', args['ctx'].troubles(), **args)
647 652
648 653 # tell hggettext to extract docstrings from these functions:
649 654 i18nfunctions = keywords.values()
@@ -1,1293 +1,1295 b''
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11 import re
12 12 import types
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 color,
17 17 config,
18 18 encoding,
19 19 error,
20 20 minirst,
21 21 parser,
22 22 pycompat,
23 23 registrar,
24 24 revset as revsetmod,
25 25 revsetlang,
26 26 templatefilters,
27 27 templatekw,
28 28 util,
29 29 )
30 30
31 31 # template parsing
32 32
33 33 elements = {
34 34 # token-type: binding-strength, primary, prefix, infix, suffix
35 35 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
36 36 ",": (2, None, None, ("list", 2), None),
37 37 "|": (5, None, None, ("|", 5), None),
38 38 "%": (6, None, None, ("%", 6), None),
39 39 ")": (0, None, None, None, None),
40 40 "+": (3, None, None, ("+", 3), None),
41 41 "-": (3, None, ("negate", 10), ("-", 3), None),
42 42 "*": (4, None, None, ("*", 4), None),
43 43 "/": (4, None, None, ("/", 4), None),
44 44 "integer": (0, "integer", None, None, None),
45 45 "symbol": (0, "symbol", None, None, None),
46 46 "string": (0, "string", None, None, None),
47 47 "template": (0, "template", None, None, None),
48 48 "end": (0, None, None, None, None),
49 49 }
50 50
51 51 def tokenize(program, start, end, term=None):
52 52 """Parse a template expression into a stream of tokens, which must end
53 53 with term if specified"""
54 54 pos = start
55 55 while pos < end:
56 56 c = program[pos]
57 57 if c.isspace(): # skip inter-token whitespace
58 58 pass
59 59 elif c in "(,)%|+-*/": # handle simple operators
60 60 yield (c, None, pos)
61 61 elif c in '"\'': # handle quoted templates
62 62 s = pos + 1
63 63 data, pos = _parsetemplate(program, s, end, c)
64 64 yield ('template', data, s)
65 65 pos -= 1
66 66 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
67 67 # handle quoted strings
68 68 c = program[pos + 1]
69 69 s = pos = pos + 2
70 70 while pos < end: # find closing quote
71 71 d = program[pos]
72 72 if d == '\\': # skip over escaped characters
73 73 pos += 2
74 74 continue
75 75 if d == c:
76 76 yield ('string', program[s:pos], s)
77 77 break
78 78 pos += 1
79 79 else:
80 80 raise error.ParseError(_("unterminated string"), s)
81 81 elif c.isdigit():
82 82 s = pos
83 83 while pos < end:
84 84 d = program[pos]
85 85 if not d.isdigit():
86 86 break
87 87 pos += 1
88 88 yield ('integer', program[s:pos], s)
89 89 pos -= 1
90 90 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
91 91 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
92 92 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
93 93 # where some of nested templates were preprocessed as strings and
94 94 # then compiled. therefore, \"...\" was allowed. (issue4733)
95 95 #
96 96 # processing flow of _evalifliteral() at 5ab28a2e9962:
97 97 # outer template string -> stringify() -> compiletemplate()
98 98 # ------------------------ ------------ ------------------
99 99 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
100 100 # ~~~~~~~~
101 101 # escaped quoted string
102 102 if c == 'r':
103 103 pos += 1
104 104 token = 'string'
105 105 else:
106 106 token = 'template'
107 107 quote = program[pos:pos + 2]
108 108 s = pos = pos + 2
109 109 while pos < end: # find closing escaped quote
110 110 if program.startswith('\\\\\\', pos, end):
111 111 pos += 4 # skip over double escaped characters
112 112 continue
113 113 if program.startswith(quote, pos, end):
114 114 # interpret as if it were a part of an outer string
115 115 data = parser.unescapestr(program[s:pos])
116 116 if token == 'template':
117 117 data = _parsetemplate(data, 0, len(data))[0]
118 118 yield (token, data, s)
119 119 pos += 1
120 120 break
121 121 pos += 1
122 122 else:
123 123 raise error.ParseError(_("unterminated string"), s)
124 124 elif c.isalnum() or c in '_':
125 125 s = pos
126 126 pos += 1
127 127 while pos < end: # find end of symbol
128 128 d = program[pos]
129 129 if not (d.isalnum() or d == "_"):
130 130 break
131 131 pos += 1
132 132 sym = program[s:pos]
133 133 yield ('symbol', sym, s)
134 134 pos -= 1
135 135 elif c == term:
136 136 yield ('end', None, pos + 1)
137 137 return
138 138 else:
139 139 raise error.ParseError(_("syntax error"), pos)
140 140 pos += 1
141 141 if term:
142 142 raise error.ParseError(_("unterminated template expansion"), start)
143 143 yield ('end', None, pos)
144 144
145 145 def _parsetemplate(tmpl, start, stop, quote=''):
146 146 r"""
147 147 >>> _parsetemplate('foo{bar}"baz', 0, 12)
148 148 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
149 149 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
150 150 ([('string', 'foo'), ('symbol', 'bar')], 9)
151 151 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
152 152 ([('string', 'foo')], 4)
153 153 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
154 154 ([('string', 'foo"'), ('string', 'bar')], 9)
155 155 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
156 156 ([('string', 'foo\\')], 6)
157 157 """
158 158 parsed = []
159 159 sepchars = '{' + quote
160 160 pos = start
161 161 p = parser.parser(elements)
162 162 while pos < stop:
163 163 n = min((tmpl.find(c, pos, stop) for c in sepchars),
164 164 key=lambda n: (n < 0, n))
165 165 if n < 0:
166 166 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
167 167 pos = stop
168 168 break
169 169 c = tmpl[n]
170 170 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
171 171 if bs % 2 == 1:
172 172 # escaped (e.g. '\{', '\\\{', but not '\\{')
173 173 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
174 174 pos = n + 1
175 175 continue
176 176 if n > pos:
177 177 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
178 178 if c == quote:
179 179 return parsed, n + 1
180 180
181 181 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
182 182 parsed.append(parseres)
183 183
184 184 if quote:
185 185 raise error.ParseError(_("unterminated string"), start)
186 186 return parsed, pos
187 187
188 188 def _unnesttemplatelist(tree):
189 189 """Expand list of templates to node tuple
190 190
191 191 >>> def f(tree):
192 192 ... print prettyformat(_unnesttemplatelist(tree))
193 193 >>> f(('template', []))
194 194 ('string', '')
195 195 >>> f(('template', [('string', 'foo')]))
196 196 ('string', 'foo')
197 197 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
198 198 (template
199 199 ('string', 'foo')
200 200 ('symbol', 'rev'))
201 201 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
202 202 (template
203 203 ('symbol', 'rev'))
204 204 >>> f(('template', [('template', [('string', 'foo')])]))
205 205 ('string', 'foo')
206 206 """
207 207 if not isinstance(tree, tuple):
208 208 return tree
209 209 op = tree[0]
210 210 if op != 'template':
211 211 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
212 212
213 213 assert len(tree) == 2
214 214 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
215 215 if not xs:
216 216 return ('string', '') # empty template ""
217 217 elif len(xs) == 1 and xs[0][0] == 'string':
218 218 return xs[0] # fast path for string with no template fragment "x"
219 219 else:
220 220 return (op,) + xs
221 221
222 222 def parse(tmpl):
223 223 """Parse template string into tree"""
224 224 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
225 225 assert pos == len(tmpl), 'unquoted template should be consumed'
226 226 return _unnesttemplatelist(('template', parsed))
227 227
228 228 def _parseexpr(expr):
229 229 """Parse a template expression into tree
230 230
231 231 >>> _parseexpr('"foo"')
232 232 ('string', 'foo')
233 233 >>> _parseexpr('foo(bar)')
234 234 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
235 235 >>> _parseexpr('foo(')
236 236 Traceback (most recent call last):
237 237 ...
238 238 ParseError: ('not a prefix: end', 4)
239 239 >>> _parseexpr('"foo" "bar"')
240 240 Traceback (most recent call last):
241 241 ...
242 242 ParseError: ('invalid token', 7)
243 243 """
244 244 p = parser.parser(elements)
245 245 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
246 246 if pos != len(expr):
247 247 raise error.ParseError(_('invalid token'), pos)
248 248 return _unnesttemplatelist(tree)
249 249
250 250 def prettyformat(tree):
251 251 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
252 252
253 253 def compileexp(exp, context, curmethods):
254 254 """Compile parsed template tree to (func, data) pair"""
255 255 t = exp[0]
256 256 if t in curmethods:
257 257 return curmethods[t](exp, context)
258 258 raise error.ParseError(_("unknown method '%s'") % t)
259 259
260 260 # template evaluation
261 261
262 262 def getsymbol(exp):
263 263 if exp[0] == 'symbol':
264 264 return exp[1]
265 265 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
266 266
267 267 def getlist(x):
268 268 if not x:
269 269 return []
270 270 if x[0] == 'list':
271 271 return getlist(x[1]) + [x[2]]
272 272 return [x]
273 273
274 274 def gettemplate(exp, context):
275 275 """Compile given template tree or load named template from map file;
276 276 returns (func, data) pair"""
277 277 if exp[0] in ('template', 'string'):
278 278 return compileexp(exp, context, methods)
279 279 if exp[0] == 'symbol':
280 280 # unlike runsymbol(), here 'symbol' is always taken as template name
281 281 # even if it exists in mapping. this allows us to override mapping
282 282 # by web templates, e.g. 'changelogtag' is redefined in map file.
283 283 return context._load(exp[1])
284 284 raise error.ParseError(_("expected template specifier"))
285 285
286 286 def evalfuncarg(context, mapping, arg):
287 287 func, data = arg
288 288 # func() may return string, generator of strings or arbitrary object such
289 289 # as date tuple, but filter does not want generator.
290 290 thing = func(context, mapping, data)
291 291 if isinstance(thing, types.GeneratorType):
292 292 thing = stringify(thing)
293 293 return thing
294 294
295 295 def evalboolean(context, mapping, arg):
296 296 """Evaluate given argument as boolean, but also takes boolean literals"""
297 297 func, data = arg
298 298 if func is runsymbol:
299 299 thing = func(context, mapping, data, default=None)
300 300 if thing is None:
301 301 # not a template keyword, takes as a boolean literal
302 302 thing = util.parsebool(data)
303 303 else:
304 304 thing = func(context, mapping, data)
305 305 if isinstance(thing, bool):
306 306 return thing
307 307 # other objects are evaluated as strings, which means 0 is True, but
308 308 # empty dict/list should be False as they are expected to be ''
309 309 return bool(stringify(thing))
310 310
311 311 def evalinteger(context, mapping, arg, err):
312 312 v = evalfuncarg(context, mapping, arg)
313 313 try:
314 314 return int(v)
315 315 except (TypeError, ValueError):
316 316 raise error.ParseError(err)
317 317
318 318 def evalstring(context, mapping, arg):
319 319 func, data = arg
320 320 return stringify(func(context, mapping, data))
321 321
322 322 def evalstringliteral(context, mapping, arg):
323 323 """Evaluate given argument as string template, but returns symbol name
324 324 if it is unknown"""
325 325 func, data = arg
326 326 if func is runsymbol:
327 327 thing = func(context, mapping, data, default=data)
328 328 else:
329 329 thing = func(context, mapping, data)
330 330 return stringify(thing)
331 331
332 332 def runinteger(context, mapping, data):
333 333 return int(data)
334 334
335 335 def runstring(context, mapping, data):
336 336 return data
337 337
338 338 def _recursivesymbolblocker(key):
339 339 def showrecursion(**args):
340 340 raise error.Abort(_("recursive reference '%s' in template") % key)
341 341 return showrecursion
342 342
343 343 def _runrecursivesymbol(context, mapping, key):
344 344 raise error.Abort(_("recursive reference '%s' in template") % key)
345 345
346 346 def runsymbol(context, mapping, key, default=''):
347 347 v = mapping.get(key)
348 348 if v is None:
349 349 v = context._defaults.get(key)
350 350 if v is None:
351 351 # put poison to cut recursion. we can't move this to parsing phase
352 352 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
353 353 safemapping = mapping.copy()
354 354 safemapping[key] = _recursivesymbolblocker(key)
355 355 try:
356 356 v = context.process(key, safemapping)
357 357 except TemplateNotFound:
358 358 v = default
359 359 if callable(v):
360 360 return v(**mapping)
361 361 return v
362 362
363 363 def buildtemplate(exp, context):
364 364 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
365 365 return (runtemplate, ctmpl)
366 366
367 367 def runtemplate(context, mapping, template):
368 368 for func, data in template:
369 369 yield func(context, mapping, data)
370 370
371 371 def buildfilter(exp, context):
372 372 arg = compileexp(exp[1], context, methods)
373 373 n = getsymbol(exp[2])
374 374 if n in context._filters:
375 375 filt = context._filters[n]
376 376 return (runfilter, (arg, filt))
377 377 if n in funcs:
378 378 f = funcs[n]
379 379 return (f, [arg])
380 380 raise error.ParseError(_("unknown function '%s'") % n)
381 381
382 382 def runfilter(context, mapping, data):
383 383 arg, filt = data
384 384 thing = evalfuncarg(context, mapping, arg)
385 385 try:
386 386 return filt(thing)
387 387 except (ValueError, AttributeError, TypeError):
388 388 if isinstance(arg[1], tuple):
389 389 dt = arg[1][1]
390 390 else:
391 391 dt = arg[1]
392 392 raise error.Abort(_("template filter '%s' is not compatible with "
393 393 "keyword '%s'") % (filt.func_name, dt))
394 394
395 395 def buildmap(exp, context):
396 396 func, data = compileexp(exp[1], context, methods)
397 397 tfunc, tdata = gettemplate(exp[2], context)
398 398 return (runmap, (func, data, tfunc, tdata))
399 399
400 400 def runmap(context, mapping, data):
401 401 func, data, tfunc, tdata = data
402 402 d = func(context, mapping, data)
403 403 if util.safehasattr(d, 'itermaps'):
404 404 diter = d.itermaps()
405 405 else:
406 406 try:
407 407 diter = iter(d)
408 408 except TypeError:
409 409 if func is runsymbol:
410 410 raise error.ParseError(_("keyword '%s' is not iterable") % data)
411 411 else:
412 412 raise error.ParseError(_("%r is not iterable") % d)
413 413
414 414 for i, v in enumerate(diter):
415 415 lm = mapping.copy()
416 416 lm['index'] = i
417 417 if isinstance(v, dict):
418 418 lm.update(v)
419 419 lm['originalnode'] = mapping.get('node')
420 420 yield tfunc(context, lm, tdata)
421 421 else:
422 422 # v is not an iterable of dicts, this happen when 'key'
423 423 # has been fully expanded already and format is useless.
424 424 # If so, return the expanded value.
425 425 yield v
426 426
427 427 def buildnegate(exp, context):
428 428 arg = compileexp(exp[1], context, exprmethods)
429 429 return (runnegate, arg)
430 430
431 431 def runnegate(context, mapping, data):
432 432 data = evalinteger(context, mapping, data,
433 433 _('negation needs an integer argument'))
434 434 return -data
435 435
436 436 def buildarithmetic(exp, context, func):
437 437 left = compileexp(exp[1], context, exprmethods)
438 438 right = compileexp(exp[2], context, exprmethods)
439 439 return (runarithmetic, (func, left, right))
440 440
441 441 def runarithmetic(context, mapping, data):
442 442 func, left, right = data
443 443 left = evalinteger(context, mapping, left,
444 444 _('arithmetic only defined on integers'))
445 445 right = evalinteger(context, mapping, right,
446 446 _('arithmetic only defined on integers'))
447 447 try:
448 448 return func(left, right)
449 449 except ZeroDivisionError:
450 450 raise error.Abort(_('division by zero is not defined'))
451 451
452 452 def buildfunc(exp, context):
453 453 n = getsymbol(exp[1])
454 454 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
455 455 if n in funcs:
456 456 f = funcs[n]
457 457 return (f, args)
458 458 if n in context._filters:
459 459 if len(args) != 1:
460 460 raise error.ParseError(_("filter %s expects one argument") % n)
461 461 f = context._filters[n]
462 462 return (runfilter, (args[0], f))
463 463 raise error.ParseError(_("unknown function '%s'") % n)
464 464
465 465 # dict of template built-in functions
466 466 funcs = {}
467 467
468 468 templatefunc = registrar.templatefunc(funcs)
469 469
470 470 @templatefunc('date(date[, fmt])')
471 471 def date(context, mapping, args):
472 472 """Format a date. See :hg:`help dates` for formatting
473 473 strings. The default is a Unix date format, including the timezone:
474 474 "Mon Sep 04 15:13:13 2006 0700"."""
475 475 if not (1 <= len(args) <= 2):
476 476 # i18n: "date" is a keyword
477 477 raise error.ParseError(_("date expects one or two arguments"))
478 478
479 479 date = evalfuncarg(context, mapping, args[0])
480 480 fmt = None
481 481 if len(args) == 2:
482 482 fmt = evalstring(context, mapping, args[1])
483 483 try:
484 484 if fmt is None:
485 485 return util.datestr(date)
486 486 else:
487 487 return util.datestr(date, fmt)
488 488 except (TypeError, ValueError):
489 489 # i18n: "date" is a keyword
490 490 raise error.ParseError(_("date expects a date information"))
491 491
492 492 @templatefunc('diff([includepattern [, excludepattern]])')
493 493 def diff(context, mapping, args):
494 494 """Show a diff, optionally
495 495 specifying files to include or exclude."""
496 496 if len(args) > 2:
497 497 # i18n: "diff" is a keyword
498 498 raise error.ParseError(_("diff expects zero, one, or two arguments"))
499 499
500 500 def getpatterns(i):
501 501 if i < len(args):
502 502 s = evalstring(context, mapping, args[i]).strip()
503 503 if s:
504 504 return [s]
505 505 return []
506 506
507 507 ctx = mapping['ctx']
508 508 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
509 509
510 510 return ''.join(chunks)
511 511
512 512 @templatefunc('files(pattern)')
513 513 def files(context, mapping, args):
514 514 """All files of the current changeset matching the pattern. See
515 515 :hg:`help patterns`."""
516 516 if not len(args) == 1:
517 517 # i18n: "files" is a keyword
518 518 raise error.ParseError(_("files expects one argument"))
519 519
520 520 raw = evalstring(context, mapping, args[0])
521 521 ctx = mapping['ctx']
522 522 m = ctx.match([raw])
523 523 files = list(ctx.matches(m))
524 524 return templatekw.showlist("file", files, **mapping)
525 525
526 526 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
527 527 def fill(context, mapping, args):
528 528 """Fill many
529 529 paragraphs with optional indentation. See the "fill" filter."""
530 530 if not (1 <= len(args) <= 4):
531 531 # i18n: "fill" is a keyword
532 532 raise error.ParseError(_("fill expects one to four arguments"))
533 533
534 534 text = evalstring(context, mapping, args[0])
535 535 width = 76
536 536 initindent = ''
537 537 hangindent = ''
538 538 if 2 <= len(args) <= 4:
539 539 width = evalinteger(context, mapping, args[1],
540 540 # i18n: "fill" is a keyword
541 541 _("fill expects an integer width"))
542 542 try:
543 543 initindent = evalstring(context, mapping, args[2])
544 544 hangindent = evalstring(context, mapping, args[3])
545 545 except IndexError:
546 546 pass
547 547
548 548 return templatefilters.fill(text, width, initindent, hangindent)
549 549
550 550 @templatefunc('formatnode(node)')
551 551 def formatnode(context, mapping, args):
552 552 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
553 553 if len(args) != 1:
554 554 # i18n: "formatnode" is a keyword
555 555 raise error.ParseError(_("formatnode expects one argument"))
556 556
557 557 ui = mapping['ui']
558 558 node = evalstring(context, mapping, args[0])
559 559 if ui.debugflag:
560 560 return node
561 561 return templatefilters.short(node)
562 562
563 563 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])')
564 564 def pad(context, mapping, args):
565 565 """Pad text with a
566 566 fill character."""
567 567 if not (2 <= len(args) <= 4):
568 568 # i18n: "pad" is a keyword
569 569 raise error.ParseError(_("pad() expects two to four arguments"))
570 570
571 571 width = evalinteger(context, mapping, args[1],
572 572 # i18n: "pad" is a keyword
573 573 _("pad() expects an integer width"))
574 574
575 575 text = evalstring(context, mapping, args[0])
576 576
577 577 left = False
578 578 fillchar = ' '
579 579 if len(args) > 2:
580 580 fillchar = evalstring(context, mapping, args[2])
581 581 if len(color.stripeffects(fillchar)) != 1:
582 582 # i18n: "pad" is a keyword
583 583 raise error.ParseError(_("pad() expects a single fill character"))
584 584 if len(args) > 3:
585 585 left = evalboolean(context, mapping, args[3])
586 586
587 587 fillwidth = width - encoding.colwidth(color.stripeffects(text))
588 588 if fillwidth <= 0:
589 589 return text
590 590 if left:
591 591 return fillchar * fillwidth + text
592 592 else:
593 593 return text + fillchar * fillwidth
594 594
595 595 @templatefunc('indent(text, indentchars[, firstline])')
596 596 def indent(context, mapping, args):
597 597 """Indents all non-empty lines
598 598 with the characters given in the indentchars string. An optional
599 599 third parameter will override the indent for the first line only
600 600 if present."""
601 601 if not (2 <= len(args) <= 3):
602 602 # i18n: "indent" is a keyword
603 603 raise error.ParseError(_("indent() expects two or three arguments"))
604 604
605 605 text = evalstring(context, mapping, args[0])
606 606 indent = evalstring(context, mapping, args[1])
607 607
608 608 if len(args) == 3:
609 609 firstline = evalstring(context, mapping, args[2])
610 610 else:
611 611 firstline = indent
612 612
613 613 # the indent function doesn't indent the first line, so we do it here
614 614 return templatefilters.indent(firstline + text, indent)
615 615
616 616 @templatefunc('get(dict, key)')
617 617 def get(context, mapping, args):
618 618 """Get an attribute/key from an object. Some keywords
619 619 are complex types. This function allows you to obtain the value of an
620 620 attribute on these types."""
621 621 if len(args) != 2:
622 622 # i18n: "get" is a keyword
623 623 raise error.ParseError(_("get() expects two arguments"))
624 624
625 625 dictarg = evalfuncarg(context, mapping, args[0])
626 626 if not util.safehasattr(dictarg, 'get'):
627 627 # i18n: "get" is a keyword
628 628 raise error.ParseError(_("get() expects a dict as first argument"))
629 629
630 630 key = evalfuncarg(context, mapping, args[1])
631 631 return dictarg.get(key)
632 632
633 633 @templatefunc('if(expr, then[, else])')
634 634 def if_(context, mapping, args):
635 635 """Conditionally execute based on the result of
636 636 an expression."""
637 637 if not (2 <= len(args) <= 3):
638 638 # i18n: "if" is a keyword
639 639 raise error.ParseError(_("if expects two or three arguments"))
640 640
641 641 test = evalboolean(context, mapping, args[0])
642 642 if test:
643 643 yield args[1][0](context, mapping, args[1][1])
644 644 elif len(args) == 3:
645 645 yield args[2][0](context, mapping, args[2][1])
646 646
647 647 @templatefunc('ifcontains(needle, haystack, then[, else])')
648 648 def ifcontains(context, mapping, args):
649 649 """Conditionally execute based
650 650 on whether the item "needle" is in "haystack"."""
651 651 if not (3 <= len(args) <= 4):
652 652 # i18n: "ifcontains" is a keyword
653 653 raise error.ParseError(_("ifcontains expects three or four arguments"))
654 654
655 655 needle = evalstring(context, mapping, args[0])
656 656 haystack = evalfuncarg(context, mapping, args[1])
657 657
658 658 if needle in haystack:
659 659 yield args[2][0](context, mapping, args[2][1])
660 660 elif len(args) == 4:
661 661 yield args[3][0](context, mapping, args[3][1])
662 662
663 663 @templatefunc('ifeq(expr1, expr2, then[, else])')
664 664 def ifeq(context, mapping, args):
665 665 """Conditionally execute based on
666 666 whether 2 items are equivalent."""
667 667 if not (3 <= len(args) <= 4):
668 668 # i18n: "ifeq" is a keyword
669 669 raise error.ParseError(_("ifeq expects three or four arguments"))
670 670
671 671 test = evalstring(context, mapping, args[0])
672 672 match = evalstring(context, mapping, args[1])
673 673 if test == match:
674 674 yield args[2][0](context, mapping, args[2][1])
675 675 elif len(args) == 4:
676 676 yield args[3][0](context, mapping, args[3][1])
677 677
678 678 @templatefunc('join(list, sep)')
679 679 def join(context, mapping, args):
680 680 """Join items in a list with a delimiter."""
681 681 if not (1 <= len(args) <= 2):
682 682 # i18n: "join" is a keyword
683 683 raise error.ParseError(_("join expects one or two arguments"))
684 684
685 685 joinset = args[0][0](context, mapping, args[0][1])
686 686 if util.safehasattr(joinset, 'itermaps'):
687 687 jf = joinset.joinfmt
688 688 joinset = [jf(x) for x in joinset.itermaps()]
689 689
690 690 joiner = " "
691 691 if len(args) > 1:
692 692 joiner = evalstring(context, mapping, args[1])
693 693
694 694 first = True
695 695 for x in joinset:
696 696 if first:
697 697 first = False
698 698 else:
699 699 yield joiner
700 700 yield x
701 701
702 702 @templatefunc('label(label, expr)')
703 703 def label(context, mapping, args):
704 704 """Apply a label to generated content. Content with
705 705 a label applied can result in additional post-processing, such as
706 706 automatic colorization."""
707 707 if len(args) != 2:
708 708 # i18n: "label" is a keyword
709 709 raise error.ParseError(_("label expects two arguments"))
710 710
711 711 ui = mapping['ui']
712 712 thing = evalstring(context, mapping, args[1])
713 713 # preserve unknown symbol as literal so effects like 'red', 'bold',
714 714 # etc. don't need to be quoted
715 715 label = evalstringliteral(context, mapping, args[0])
716 716
717 717 return ui.label(thing, label)
718 718
719 719 @templatefunc('latesttag([pattern])')
720 720 def latesttag(context, mapping, args):
721 721 """The global tags matching the given pattern on the
722 722 most recent globally tagged ancestor of this changeset.
723 723 If no such tags exist, the "{tag}" template resolves to
724 724 the string "null"."""
725 725 if len(args) > 1:
726 726 # i18n: "latesttag" is a keyword
727 727 raise error.ParseError(_("latesttag expects at most one argument"))
728 728
729 729 pattern = None
730 730 if len(args) == 1:
731 731 pattern = evalstring(context, mapping, args[0])
732 732
733 733 return templatekw.showlatesttags(pattern, **mapping)
734 734
735 735 @templatefunc('localdate(date[, tz])')
736 736 def localdate(context, mapping, args):
737 737 """Converts a date to the specified timezone.
738 738 The default is local date."""
739 739 if not (1 <= len(args) <= 2):
740 740 # i18n: "localdate" is a keyword
741 741 raise error.ParseError(_("localdate expects one or two arguments"))
742 742
743 743 date = evalfuncarg(context, mapping, args[0])
744 744 try:
745 745 date = util.parsedate(date)
746 746 except AttributeError: # not str nor date tuple
747 747 # i18n: "localdate" is a keyword
748 748 raise error.ParseError(_("localdate expects a date information"))
749 749 if len(args) >= 2:
750 750 tzoffset = None
751 751 tz = evalfuncarg(context, mapping, args[1])
752 752 if isinstance(tz, str):
753 753 tzoffset, remainder = util.parsetimezone(tz)
754 754 if remainder:
755 755 tzoffset = None
756 756 if tzoffset is None:
757 757 try:
758 758 tzoffset = int(tz)
759 759 except (TypeError, ValueError):
760 760 # i18n: "localdate" is a keyword
761 761 raise error.ParseError(_("localdate expects a timezone"))
762 762 else:
763 763 tzoffset = util.makedate()[1]
764 764 return (date[0], tzoffset)
765 765
766 766 @templatefunc('mod(a, b)')
767 767 def mod(context, mapping, args):
768 768 """Calculate a mod b such that a / b + a mod b == a"""
769 769 if not len(args) == 2:
770 770 # i18n: "mod" is a keyword
771 771 raise error.ParseError(_("mod expects two arguments"))
772 772
773 773 func = lambda a, b: a % b
774 774 return runarithmetic(context, mapping, (func, args[0], args[1]))
775 775
776 776 @templatefunc('relpath(path)')
777 777 def relpath(context, mapping, args):
778 778 """Convert a repository-absolute path into a filesystem path relative to
779 779 the current working directory."""
780 780 if len(args) != 1:
781 781 # i18n: "relpath" is a keyword
782 782 raise error.ParseError(_("relpath expects one argument"))
783 783
784 784 repo = mapping['ctx'].repo()
785 785 path = evalstring(context, mapping, args[0])
786 786 return repo.pathto(path)
787 787
788 788 @templatefunc('revset(query[, formatargs...])')
789 789 def revset(context, mapping, args):
790 790 """Execute a revision set query. See
791 791 :hg:`help revset`."""
792 792 if not len(args) > 0:
793 793 # i18n: "revset" is a keyword
794 794 raise error.ParseError(_("revset expects one or more arguments"))
795 795
796 796 raw = evalstring(context, mapping, args[0])
797 797 ctx = mapping['ctx']
798 798 repo = ctx.repo()
799 799
800 800 def query(expr):
801 801 m = revsetmod.match(repo.ui, expr)
802 802 return m(repo)
803 803
804 804 if len(args) > 1:
805 805 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
806 806 revs = query(revsetlang.formatspec(raw, *formatargs))
807 807 revs = list(revs)
808 808 else:
809 809 revsetcache = mapping['cache'].setdefault("revsetcache", {})
810 810 if raw in revsetcache:
811 811 revs = revsetcache[raw]
812 812 else:
813 813 revs = query(raw)
814 814 revs = list(revs)
815 815 revsetcache[raw] = revs
816 816
817 817 return templatekw.showrevslist("revision", revs, **mapping)
818 818
819 819 @templatefunc('rstdoc(text, style)')
820 820 def rstdoc(context, mapping, args):
821 821 """Format reStructuredText."""
822 822 if len(args) != 2:
823 823 # i18n: "rstdoc" is a keyword
824 824 raise error.ParseError(_("rstdoc expects two arguments"))
825 825
826 826 text = evalstring(context, mapping, args[0])
827 827 style = evalstring(context, mapping, args[1])
828 828
829 829 return minirst.format(text, style=style, keep=['verbose'])
830 830
831 831 @templatefunc('separate(sep, args)')
832 832 def separate(context, mapping, args):
833 833 """Add a separator between non-empty arguments."""
834 834 if not args:
835 835 # i18n: "separate" is a keyword
836 836 raise error.ParseError(_("separate expects at least one argument"))
837 837
838 838 sep = evalstring(context, mapping, args[0])
839 839 first = True
840 840 for arg in args[1:]:
841 841 argstr = evalstring(context, mapping, arg)
842 842 if not argstr:
843 843 continue
844 844 if first:
845 845 first = False
846 846 else:
847 847 yield sep
848 848 yield argstr
849 849
850 850 @templatefunc('shortest(node, minlength=4)')
851 851 def shortest(context, mapping, args):
852 852 """Obtain the shortest representation of
853 853 a node."""
854 854 if not (1 <= len(args) <= 2):
855 855 # i18n: "shortest" is a keyword
856 856 raise error.ParseError(_("shortest() expects one or two arguments"))
857 857
858 858 node = evalstring(context, mapping, args[0])
859 859
860 860 minlength = 4
861 861 if len(args) > 1:
862 862 minlength = evalinteger(context, mapping, args[1],
863 863 # i18n: "shortest" is a keyword
864 864 _("shortest() expects an integer minlength"))
865 865
866 866 # _partialmatch() of filtered changelog could take O(len(repo)) time,
867 867 # which would be unacceptably slow. so we look for hash collision in
868 868 # unfiltered space, which means some hashes may be slightly longer.
869 869 cl = mapping['ctx']._repo.unfiltered().changelog
870 870 def isvalid(test):
871 871 try:
872 872 if cl._partialmatch(test) is None:
873 873 return False
874 874
875 875 try:
876 876 i = int(test)
877 877 # if we are a pure int, then starting with zero will not be
878 878 # confused as a rev; or, obviously, if the int is larger than
879 879 # the value of the tip rev
880 880 if test[0] == '0' or i > len(cl):
881 881 return True
882 882 return False
883 883 except ValueError:
884 884 return True
885 885 except error.RevlogError:
886 886 return False
887 887
888 888 shortest = node
889 889 startlength = max(6, minlength)
890 890 length = startlength
891 891 while True:
892 892 test = node[:length]
893 893 if isvalid(test):
894 894 shortest = test
895 895 if length == minlength or length > startlength:
896 896 return shortest
897 897 length -= 1
898 898 else:
899 899 length += 1
900 900 if len(shortest) <= length:
901 901 return shortest
902 902
903 903 @templatefunc('strip(text[, chars])')
904 904 def strip(context, mapping, args):
905 905 """Strip characters from a string. By default,
906 906 strips all leading and trailing whitespace."""
907 907 if not (1 <= len(args) <= 2):
908 908 # i18n: "strip" is a keyword
909 909 raise error.ParseError(_("strip expects one or two arguments"))
910 910
911 911 text = evalstring(context, mapping, args[0])
912 912 if len(args) == 2:
913 913 chars = evalstring(context, mapping, args[1])
914 914 return text.strip(chars)
915 915 return text.strip()
916 916
917 917 @templatefunc('sub(pattern, replacement, expression)')
918 918 def sub(context, mapping, args):
919 919 """Perform text substitution
920 920 using regular expressions."""
921 921 if len(args) != 3:
922 922 # i18n: "sub" is a keyword
923 923 raise error.ParseError(_("sub expects three arguments"))
924 924
925 925 pat = evalstring(context, mapping, args[0])
926 926 rpl = evalstring(context, mapping, args[1])
927 927 src = evalstring(context, mapping, args[2])
928 928 try:
929 929 patre = re.compile(pat)
930 930 except re.error:
931 931 # i18n: "sub" is a keyword
932 932 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
933 933 try:
934 934 yield patre.sub(rpl, src)
935 935 except re.error:
936 936 # i18n: "sub" is a keyword
937 937 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
938 938
939 939 @templatefunc('startswith(pattern, text)')
940 940 def startswith(context, mapping, args):
941 941 """Returns the value from the "text" argument
942 942 if it begins with the content from the "pattern" argument."""
943 943 if len(args) != 2:
944 944 # i18n: "startswith" is a keyword
945 945 raise error.ParseError(_("startswith expects two arguments"))
946 946
947 947 patn = evalstring(context, mapping, args[0])
948 948 text = evalstring(context, mapping, args[1])
949 949 if text.startswith(patn):
950 950 return text
951 951 return ''
952 952
953 953 @templatefunc('word(number, text[, separator])')
954 954 def word(context, mapping, args):
955 955 """Return the nth word from a string."""
956 956 if not (2 <= len(args) <= 3):
957 957 # i18n: "word" is a keyword
958 958 raise error.ParseError(_("word expects two or three arguments, got %d")
959 959 % len(args))
960 960
961 961 num = evalinteger(context, mapping, args[0],
962 962 # i18n: "word" is a keyword
963 963 _("word expects an integer index"))
964 964 text = evalstring(context, mapping, args[1])
965 965 if len(args) == 3:
966 966 splitter = evalstring(context, mapping, args[2])
967 967 else:
968 968 splitter = None
969 969
970 970 tokens = text.split(splitter)
971 971 if num >= len(tokens) or num < -len(tokens):
972 972 return ''
973 973 else:
974 974 return tokens[num]
975 975
976 976 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
977 977 exprmethods = {
978 978 "integer": lambda e, c: (runinteger, e[1]),
979 979 "string": lambda e, c: (runstring, e[1]),
980 980 "symbol": lambda e, c: (runsymbol, e[1]),
981 981 "template": buildtemplate,
982 982 "group": lambda e, c: compileexp(e[1], c, exprmethods),
983 983 # ".": buildmember,
984 984 "|": buildfilter,
985 985 "%": buildmap,
986 986 "func": buildfunc,
987 987 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
988 988 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
989 989 "negate": buildnegate,
990 990 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
991 991 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
992 992 }
993 993
994 994 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
995 995 methods = exprmethods.copy()
996 996 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
997 997
998 998 class _aliasrules(parser.basealiasrules):
999 999 """Parsing and expansion rule set of template aliases"""
1000 1000 _section = _('template alias')
1001 1001 _parse = staticmethod(_parseexpr)
1002 1002
1003 1003 @staticmethod
1004 1004 def _trygetfunc(tree):
1005 1005 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1006 1006 None"""
1007 1007 if tree[0] == 'func' and tree[1][0] == 'symbol':
1008 1008 return tree[1][1], getlist(tree[2])
1009 1009 if tree[0] == '|' and tree[2][0] == 'symbol':
1010 1010 return tree[2][1], [tree[1]]
1011 1011
1012 1012 def expandaliases(tree, aliases):
1013 1013 """Return new tree of aliases are expanded"""
1014 1014 aliasmap = _aliasrules.buildmap(aliases)
1015 1015 return _aliasrules.expand(aliasmap, tree)
1016 1016
1017 1017 # template engine
1018 1018
1019 1019 stringify = templatefilters.stringify
1020 1020
1021 1021 def _flatten(thing):
1022 1022 '''yield a single stream from a possibly nested set of iterators'''
1023 thing = templatekw.unwraphybrid(thing)
1023 1024 if isinstance(thing, str):
1024 1025 yield thing
1025 1026 elif thing is None:
1026 1027 pass
1027 1028 elif not util.safehasattr(thing, '__iter__'):
1028 1029 yield str(thing)
1029 1030 else:
1030 1031 for i in thing:
1032 i = templatekw.unwraphybrid(i)
1031 1033 if isinstance(i, str):
1032 1034 yield i
1033 1035 elif i is None:
1034 1036 pass
1035 1037 elif not util.safehasattr(i, '__iter__'):
1036 1038 yield str(i)
1037 1039 else:
1038 1040 for j in _flatten(i):
1039 1041 yield j
1040 1042
1041 1043 def unquotestring(s):
1042 1044 '''unwrap quotes if any; otherwise returns unmodified string'''
1043 1045 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1044 1046 return s
1045 1047 return s[1:-1]
1046 1048
1047 1049 class engine(object):
1048 1050 '''template expansion engine.
1049 1051
1050 1052 template expansion works like this. a map file contains key=value
1051 1053 pairs. if value is quoted, it is treated as string. otherwise, it
1052 1054 is treated as name of template file.
1053 1055
1054 1056 templater is asked to expand a key in map. it looks up key, and
1055 1057 looks for strings like this: {foo}. it expands {foo} by looking up
1056 1058 foo in map, and substituting it. expansion is recursive: it stops
1057 1059 when there is no more {foo} to replace.
1058 1060
1059 1061 expansion also allows formatting and filtering.
1060 1062
1061 1063 format uses key to expand each item in list. syntax is
1062 1064 {key%format}.
1063 1065
1064 1066 filter uses function to transform value. syntax is
1065 1067 {key|filter1|filter2|...}.'''
1066 1068
1067 1069 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1068 1070 self._loader = loader
1069 1071 if filters is None:
1070 1072 filters = {}
1071 1073 self._filters = filters
1072 1074 if defaults is None:
1073 1075 defaults = {}
1074 1076 self._defaults = defaults
1075 1077 self._aliasmap = _aliasrules.buildmap(aliases)
1076 1078 self._cache = {} # key: (func, data)
1077 1079
1078 1080 def _load(self, t):
1079 1081 '''load, parse, and cache a template'''
1080 1082 if t not in self._cache:
1081 1083 # put poison to cut recursion while compiling 't'
1082 1084 self._cache[t] = (_runrecursivesymbol, t)
1083 1085 try:
1084 1086 x = parse(self._loader(t))
1085 1087 if self._aliasmap:
1086 1088 x = _aliasrules.expand(self._aliasmap, x)
1087 1089 self._cache[t] = compileexp(x, self, methods)
1088 1090 except: # re-raises
1089 1091 del self._cache[t]
1090 1092 raise
1091 1093 return self._cache[t]
1092 1094
1093 1095 def process(self, t, mapping):
1094 1096 '''Perform expansion. t is name of map element to expand.
1095 1097 mapping contains added elements for use during expansion. Is a
1096 1098 generator.'''
1097 1099 func, data = self._load(t)
1098 1100 return _flatten(func(self, mapping, data))
1099 1101
1100 1102 engines = {'default': engine}
1101 1103
1102 1104 def stylelist():
1103 1105 paths = templatepaths()
1104 1106 if not paths:
1105 1107 return _('no templates found, try `hg debuginstall` for more info')
1106 1108 dirlist = os.listdir(paths[0])
1107 1109 stylelist = []
1108 1110 for file in dirlist:
1109 1111 split = file.split(".")
1110 1112 if split[-1] in ('orig', 'rej'):
1111 1113 continue
1112 1114 if split[0] == "map-cmdline":
1113 1115 stylelist.append(split[1])
1114 1116 return ", ".join(sorted(stylelist))
1115 1117
1116 1118 def _readmapfile(mapfile):
1117 1119 """Load template elements from the given map file"""
1118 1120 if not os.path.exists(mapfile):
1119 1121 raise error.Abort(_("style '%s' not found") % mapfile,
1120 1122 hint=_("available styles: %s") % stylelist())
1121 1123
1122 1124 base = os.path.dirname(mapfile)
1123 1125 conf = config.config(includepaths=templatepaths())
1124 1126 conf.read(mapfile)
1125 1127
1126 1128 cache = {}
1127 1129 tmap = {}
1128 1130 for key, val in conf[''].items():
1129 1131 if not val:
1130 1132 raise error.ParseError(_('missing value'), conf.source('', key))
1131 1133 if val[0] in "'\"":
1132 1134 if val[0] != val[-1]:
1133 1135 raise error.ParseError(_('unmatched quotes'),
1134 1136 conf.source('', key))
1135 1137 cache[key] = unquotestring(val)
1136 1138 elif key == "__base__":
1137 1139 # treat as a pointer to a base class for this style
1138 1140 path = util.normpath(os.path.join(base, val))
1139 1141
1140 1142 # fallback check in template paths
1141 1143 if not os.path.exists(path):
1142 1144 for p in templatepaths():
1143 1145 p2 = util.normpath(os.path.join(p, val))
1144 1146 if os.path.isfile(p2):
1145 1147 path = p2
1146 1148 break
1147 1149 p3 = util.normpath(os.path.join(p2, "map"))
1148 1150 if os.path.isfile(p3):
1149 1151 path = p3
1150 1152 break
1151 1153
1152 1154 bcache, btmap = _readmapfile(path)
1153 1155 for k in bcache:
1154 1156 if k not in cache:
1155 1157 cache[k] = bcache[k]
1156 1158 for k in btmap:
1157 1159 if k not in tmap:
1158 1160 tmap[k] = btmap[k]
1159 1161 else:
1160 1162 val = 'default', val
1161 1163 if ':' in val[1]:
1162 1164 val = val[1].split(':', 1)
1163 1165 tmap[key] = val[0], os.path.join(base, val[1])
1164 1166 return cache, tmap
1165 1167
1166 1168 class TemplateNotFound(error.Abort):
1167 1169 pass
1168 1170
1169 1171 class templater(object):
1170 1172
1171 1173 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1172 1174 minchunk=1024, maxchunk=65536):
1173 1175 '''set up template engine.
1174 1176 filters is dict of functions. each transforms a value into another.
1175 1177 defaults is dict of default map definitions.
1176 1178 aliases is list of alias (name, replacement) pairs.
1177 1179 '''
1178 1180 if filters is None:
1179 1181 filters = {}
1180 1182 if defaults is None:
1181 1183 defaults = {}
1182 1184 if cache is None:
1183 1185 cache = {}
1184 1186 self.cache = cache.copy()
1185 1187 self.map = {}
1186 1188 self.filters = templatefilters.filters.copy()
1187 1189 self.filters.update(filters)
1188 1190 self.defaults = defaults
1189 1191 self._aliases = aliases
1190 1192 self.minchunk, self.maxchunk = minchunk, maxchunk
1191 1193 self.ecache = {}
1192 1194
1193 1195 @classmethod
1194 1196 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1195 1197 minchunk=1024, maxchunk=65536):
1196 1198 """Create templater from the specified map file"""
1197 1199 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1198 1200 cache, tmap = _readmapfile(mapfile)
1199 1201 t.cache.update(cache)
1200 1202 t.map = tmap
1201 1203 return t
1202 1204
1203 1205 def __contains__(self, key):
1204 1206 return key in self.cache or key in self.map
1205 1207
1206 1208 def load(self, t):
1207 1209 '''Get the template for the given template name. Use a local cache.'''
1208 1210 if t not in self.cache:
1209 1211 try:
1210 1212 self.cache[t] = util.readfile(self.map[t][1])
1211 1213 except KeyError as inst:
1212 1214 raise TemplateNotFound(_('"%s" not in template map') %
1213 1215 inst.args[0])
1214 1216 except IOError as inst:
1215 1217 raise IOError(inst.args[0], _('template file %s: %s') %
1216 1218 (self.map[t][1], inst.args[1]))
1217 1219 return self.cache[t]
1218 1220
1219 1221 def __call__(self, t, **mapping):
1220 1222 ttype = t in self.map and self.map[t][0] or 'default'
1221 1223 if ttype not in self.ecache:
1222 1224 try:
1223 1225 ecls = engines[ttype]
1224 1226 except KeyError:
1225 1227 raise error.Abort(_('invalid template engine: %s') % ttype)
1226 1228 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1227 1229 self._aliases)
1228 1230 proc = self.ecache[ttype]
1229 1231
1230 1232 stream = proc.process(t, mapping)
1231 1233 if self.minchunk:
1232 1234 stream = util.increasingchunks(stream, min=self.minchunk,
1233 1235 max=self.maxchunk)
1234 1236 return stream
1235 1237
1236 1238 def templatepaths():
1237 1239 '''return locations used for template files.'''
1238 1240 pathsrel = ['templates']
1239 1241 paths = [os.path.normpath(os.path.join(util.datapath, f))
1240 1242 for f in pathsrel]
1241 1243 return [p for p in paths if os.path.isdir(p)]
1242 1244
1243 1245 def templatepath(name):
1244 1246 '''return location of template file. returns None if not found.'''
1245 1247 for p in templatepaths():
1246 1248 f = os.path.join(p, name)
1247 1249 if os.path.exists(f):
1248 1250 return f
1249 1251 return None
1250 1252
1251 1253 def stylemap(styles, paths=None):
1252 1254 """Return path to mapfile for a given style.
1253 1255
1254 1256 Searches mapfile in the following locations:
1255 1257 1. templatepath/style/map
1256 1258 2. templatepath/map-style
1257 1259 3. templatepath/map
1258 1260 """
1259 1261
1260 1262 if paths is None:
1261 1263 paths = templatepaths()
1262 1264 elif isinstance(paths, str):
1263 1265 paths = [paths]
1264 1266
1265 1267 if isinstance(styles, str):
1266 1268 styles = [styles]
1267 1269
1268 1270 for style in styles:
1269 1271 # only plain name is allowed to honor template paths
1270 1272 if (not style
1271 1273 or style in (os.curdir, os.pardir)
1272 1274 or pycompat.ossep in style
1273 1275 or pycompat.osaltsep and pycompat.osaltsep in style):
1274 1276 continue
1275 1277 locations = [os.path.join(style, 'map'), 'map-' + style]
1276 1278 locations.append('map')
1277 1279
1278 1280 for path in paths:
1279 1281 for location in locations:
1280 1282 mapfile = os.path.join(path, location)
1281 1283 if os.path.isfile(mapfile):
1282 1284 return style, mapfile
1283 1285
1284 1286 raise RuntimeError("No hgweb templates found in %r" % paths)
1285 1287
1286 1288 def loadfunction(ui, extname, registrarobj):
1287 1289 """Load template function from specified registrarobj
1288 1290 """
1289 1291 for name, func in registrarobj._table.iteritems():
1290 1292 funcs[name] = func
1291 1293
1292 1294 # tell hggettext to extract docstrings from these functions:
1293 1295 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now