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