##// END OF EJS Templates
templatefilters: add slashpath() to convert path separator to slash...
Yuya Nishihara -
r35460:dad8a507 default
parent child Browse files
Show More
@@ -1,445 +1,450 b''
1 1 # templatefilters.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 os
11 11 import re
12 12 import time
13 13
14 14 from . import (
15 15 encoding,
16 16 error,
17 17 hbisect,
18 18 node,
19 19 pycompat,
20 20 registrar,
21 21 templatekw,
22 22 url,
23 23 util,
24 24 )
25 25
26 26 urlerr = util.urlerr
27 27 urlreq = util.urlreq
28 28
29 29 if pycompat.ispy3:
30 30 long = int
31 31
32 32 # filters are callables like:
33 33 # fn(obj)
34 34 # with:
35 35 # obj - object to be filtered (text, date, list and so on)
36 36 filters = {}
37 37
38 38 templatefilter = registrar.templatefilter(filters)
39 39
40 40 @templatefilter('addbreaks')
41 41 def addbreaks(text):
42 42 """Any text. Add an XHTML "<br />" tag before the end of
43 43 every line except the last.
44 44 """
45 45 return text.replace('\n', '<br/>\n')
46 46
47 47 agescales = [("year", 3600 * 24 * 365, 'Y'),
48 48 ("month", 3600 * 24 * 30, 'M'),
49 49 ("week", 3600 * 24 * 7, 'W'),
50 50 ("day", 3600 * 24, 'd'),
51 51 ("hour", 3600, 'h'),
52 52 ("minute", 60, 'm'),
53 53 ("second", 1, 's')]
54 54
55 55 @templatefilter('age')
56 56 def age(date, abbrev=False):
57 57 """Date. Returns a human-readable date/time difference between the
58 58 given date/time and the current date/time.
59 59 """
60 60
61 61 def plural(t, c):
62 62 if c == 1:
63 63 return t
64 64 return t + "s"
65 65 def fmt(t, c, a):
66 66 if abbrev:
67 67 return "%d%s" % (c, a)
68 68 return "%d %s" % (c, plural(t, c))
69 69
70 70 now = time.time()
71 71 then = date[0]
72 72 future = False
73 73 if then > now:
74 74 future = True
75 75 delta = max(1, int(then - now))
76 76 if delta > agescales[0][1] * 30:
77 77 return 'in the distant future'
78 78 else:
79 79 delta = max(1, int(now - then))
80 80 if delta > agescales[0][1] * 2:
81 81 return util.shortdate(date)
82 82
83 83 for t, s, a in agescales:
84 84 n = delta // s
85 85 if n >= 2 or s == 1:
86 86 if future:
87 87 return '%s from now' % fmt(t, n, a)
88 88 return '%s ago' % fmt(t, n, a)
89 89
90 90 @templatefilter('basename')
91 91 def basename(path):
92 92 """Any text. Treats the text as a path, and returns the last
93 93 component of the path after splitting by the path separator
94 94 (ignoring trailing separators). For example, "foo/bar/baz" becomes
95 95 "baz" and "foo/bar//" becomes "bar".
96 96 """
97 97 return os.path.basename(path)
98 98
99 99 @templatefilter('count')
100 100 def count(i):
101 101 """List or text. Returns the length as an integer."""
102 102 return len(i)
103 103
104 104 @templatefilter('domain')
105 105 def domain(author):
106 106 """Any text. Finds the first string that looks like an email
107 107 address, and extracts just the domain component. Example: ``User
108 108 <user@example.com>`` becomes ``example.com``.
109 109 """
110 110 f = author.find('@')
111 111 if f == -1:
112 112 return ''
113 113 author = author[f + 1:]
114 114 f = author.find('>')
115 115 if f >= 0:
116 116 author = author[:f]
117 117 return author
118 118
119 119 @templatefilter('email')
120 120 def email(text):
121 121 """Any text. Extracts the first string that looks like an email
122 122 address. Example: ``User <user@example.com>`` becomes
123 123 ``user@example.com``.
124 124 """
125 125 return util.email(text)
126 126
127 127 @templatefilter('escape')
128 128 def escape(text):
129 129 """Any text. Replaces the special XML/XHTML characters "&", "<"
130 130 and ">" with XML entities, and filters out NUL characters.
131 131 """
132 132 return url.escape(text.replace('\0', ''), True)
133 133
134 134 para_re = None
135 135 space_re = None
136 136
137 137 def fill(text, width, initindent='', hangindent=''):
138 138 '''fill many paragraphs with optional indentation.'''
139 139 global para_re, space_re
140 140 if para_re is None:
141 141 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
142 142 space_re = re.compile(r' +')
143 143
144 144 def findparas():
145 145 start = 0
146 146 while True:
147 147 m = para_re.search(text, start)
148 148 if not m:
149 149 uctext = unicode(text[start:], encoding.encoding)
150 150 w = len(uctext)
151 151 while 0 < w and uctext[w - 1].isspace():
152 152 w -= 1
153 153 yield (uctext[:w].encode(encoding.encoding),
154 154 uctext[w:].encode(encoding.encoding))
155 155 break
156 156 yield text[start:m.start(0)], m.group(1)
157 157 start = m.end(1)
158 158
159 159 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
160 160 width, initindent, hangindent) + rest
161 161 for para, rest in findparas()])
162 162
163 163 @templatefilter('fill68')
164 164 def fill68(text):
165 165 """Any text. Wraps the text to fit in 68 columns."""
166 166 return fill(text, 68)
167 167
168 168 @templatefilter('fill76')
169 169 def fill76(text):
170 170 """Any text. Wraps the text to fit in 76 columns."""
171 171 return fill(text, 76)
172 172
173 173 @templatefilter('firstline')
174 174 def firstline(text):
175 175 """Any text. Returns the first line of text."""
176 176 try:
177 177 return text.splitlines(True)[0].rstrip('\r\n')
178 178 except IndexError:
179 179 return ''
180 180
181 181 @templatefilter('hex')
182 182 def hexfilter(text):
183 183 """Any text. Convert a binary Mercurial node identifier into
184 184 its long hexadecimal representation.
185 185 """
186 186 return node.hex(text)
187 187
188 188 @templatefilter('hgdate')
189 189 def hgdate(text):
190 190 """Date. Returns the date as a pair of numbers: "1157407993
191 191 25200" (Unix timestamp, timezone offset).
192 192 """
193 193 return "%d %d" % text
194 194
195 195 @templatefilter('isodate')
196 196 def isodate(text):
197 197 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
198 198 +0200".
199 199 """
200 200 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
201 201
202 202 @templatefilter('isodatesec')
203 203 def isodatesec(text):
204 204 """Date. Returns the date in ISO 8601 format, including
205 205 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
206 206 filter.
207 207 """
208 208 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
209 209
210 210 def indent(text, prefix):
211 211 '''indent each non-empty line of text after first with prefix.'''
212 212 lines = text.splitlines()
213 213 num_lines = len(lines)
214 214 endswithnewline = text[-1:] == '\n'
215 215 def indenter():
216 216 for i in xrange(num_lines):
217 217 l = lines[i]
218 218 if i and l.strip():
219 219 yield prefix
220 220 yield l
221 221 if i < num_lines - 1 or endswithnewline:
222 222 yield '\n'
223 223 return "".join(indenter())
224 224
225 225 @templatefilter('json')
226 226 def json(obj, paranoid=True):
227 227 if obj is None:
228 228 return 'null'
229 229 elif obj is False:
230 230 return 'false'
231 231 elif obj is True:
232 232 return 'true'
233 233 elif isinstance(obj, (int, long, float)):
234 234 return pycompat.bytestr(obj)
235 235 elif isinstance(obj, bytes):
236 236 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
237 237 elif isinstance(obj, str):
238 238 # This branch is unreachable on Python 2, because bytes == str
239 239 # and we'll return in the next-earlier block in the elif
240 240 # ladder. On Python 3, this helps us catch bugs before they
241 241 # hurt someone.
242 242 raise error.ProgrammingError(
243 243 'Mercurial only does output with bytes on Python 3: %r' % obj)
244 244 elif util.safehasattr(obj, 'keys'):
245 245 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
246 246 json(v, paranoid))
247 247 for k, v in sorted(obj.iteritems())]
248 248 return '{' + ', '.join(out) + '}'
249 249 elif util.safehasattr(obj, '__iter__'):
250 250 out = [json(i, paranoid) for i in obj]
251 251 return '[' + ', '.join(out) + ']'
252 252 else:
253 253 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
254 254
255 255 @templatefilter('lower')
256 256 def lower(text):
257 257 """Any text. Converts the text to lowercase."""
258 258 return encoding.lower(text)
259 259
260 260 @templatefilter('nonempty')
261 261 def nonempty(str):
262 262 """Any text. Returns '(none)' if the string is empty."""
263 263 return str or "(none)"
264 264
265 265 @templatefilter('obfuscate')
266 266 def obfuscate(text):
267 267 """Any text. Returns the input text rendered as a sequence of
268 268 XML entities.
269 269 """
270 270 text = unicode(text, encoding.encoding, 'replace')
271 271 return ''.join(['&#%d;' % ord(c) for c in text])
272 272
273 273 @templatefilter('permissions')
274 274 def permissions(flags):
275 275 if "l" in flags:
276 276 return "lrwxrwxrwx"
277 277 if "x" in flags:
278 278 return "-rwxr-xr-x"
279 279 return "-rw-r--r--"
280 280
281 281 @templatefilter('person')
282 282 def person(author):
283 283 """Any text. Returns the name before an email address,
284 284 interpreting it as per RFC 5322.
285 285
286 286 >>> person(b'foo@bar')
287 287 'foo'
288 288 >>> person(b'Foo Bar <foo@bar>')
289 289 'Foo Bar'
290 290 >>> person(b'"Foo Bar" <foo@bar>')
291 291 'Foo Bar'
292 292 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
293 293 'Foo "buz" Bar'
294 294 >>> # The following are invalid, but do exist in real-life
295 295 ...
296 296 >>> person(b'Foo "buz" Bar <foo@bar>')
297 297 'Foo "buz" Bar'
298 298 >>> person(b'"Foo Bar <foo@bar>')
299 299 'Foo Bar'
300 300 """
301 301 if '@' not in author:
302 302 return author
303 303 f = author.find('<')
304 304 if f != -1:
305 305 return author[:f].strip(' "').replace('\\"', '"')
306 306 f = author.find('@')
307 307 return author[:f].replace('.', ' ')
308 308
309 309 @templatefilter('revescape')
310 310 def revescape(text):
311 311 """Any text. Escapes all "special" characters, except @.
312 312 Forward slashes are escaped twice to prevent web servers from prematurely
313 313 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
314 314 """
315 315 return urlreq.quote(text, safe='/@').replace('/', '%252F')
316 316
317 317 @templatefilter('rfc3339date')
318 318 def rfc3339date(text):
319 319 """Date. Returns a date using the Internet date format
320 320 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
321 321 """
322 322 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
323 323
324 324 @templatefilter('rfc822date')
325 325 def rfc822date(text):
326 326 """Date. Returns a date using the same format used in email
327 327 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
328 328 """
329 329 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
330 330
331 331 @templatefilter('short')
332 332 def short(text):
333 333 """Changeset hash. Returns the short form of a changeset hash,
334 334 i.e. a 12 hexadecimal digit string.
335 335 """
336 336 return text[:12]
337 337
338 338 @templatefilter('shortbisect')
339 339 def shortbisect(text):
340 340 """Any text. Treats `text` as a bisection status, and
341 341 returns a single-character representing the status (G: good, B: bad,
342 342 S: skipped, U: untested, I: ignored). Returns single space if `text`
343 343 is not a valid bisection status.
344 344 """
345 345 return hbisect.shortlabel(text) or ' '
346 346
347 347 @templatefilter('shortdate')
348 348 def shortdate(text):
349 349 """Date. Returns a date like "2006-09-18"."""
350 350 return util.shortdate(text)
351 351
352 @templatefilter('slashpath')
353 def slashpath(path):
354 """Any text. Replaces the native path separator with slash."""
355 return util.pconvert(path)
356
352 357 @templatefilter('splitlines')
353 358 def splitlines(text):
354 359 """Any text. Split text into a list of lines."""
355 360 return templatekw.hybridlist(text.splitlines(), name='line')
356 361
357 362 @templatefilter('stringescape')
358 363 def stringescape(text):
359 364 return util.escapestr(text)
360 365
361 366 @templatefilter('stringify')
362 367 def stringify(thing):
363 368 """Any type. Turns the value into text by converting values into
364 369 text and concatenating them.
365 370 """
366 371 thing = templatekw.unwraphybrid(thing)
367 372 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
368 373 return "".join([stringify(t) for t in thing if t is not None])
369 374 if thing is None:
370 375 return ""
371 376 return pycompat.bytestr(thing)
372 377
373 378 @templatefilter('stripdir')
374 379 def stripdir(text):
375 380 """Treat the text as path and strip a directory level, if
376 381 possible. For example, "foo" and "foo/bar" becomes "foo".
377 382 """
378 383 dir = os.path.dirname(text)
379 384 if dir == "":
380 385 return os.path.basename(text)
381 386 else:
382 387 return dir
383 388
384 389 @templatefilter('tabindent')
385 390 def tabindent(text):
386 391 """Any text. Returns the text, with every non-empty line
387 392 except the first starting with a tab character.
388 393 """
389 394 return indent(text, '\t')
390 395
391 396 @templatefilter('upper')
392 397 def upper(text):
393 398 """Any text. Converts the text to uppercase."""
394 399 return encoding.upper(text)
395 400
396 401 @templatefilter('urlescape')
397 402 def urlescape(text):
398 403 """Any text. Escapes all "special" characters. For example,
399 404 "foo bar" becomes "foo%20bar".
400 405 """
401 406 return urlreq.quote(text)
402 407
403 408 @templatefilter('user')
404 409 def userfilter(text):
405 410 """Any text. Returns a short representation of a user name or email
406 411 address."""
407 412 return util.shortuser(text)
408 413
409 414 @templatefilter('emailuser')
410 415 def emailuser(text):
411 416 """Any text. Returns the user portion of an email address."""
412 417 return util.emailuser(text)
413 418
414 419 @templatefilter('utf8')
415 420 def utf8(text):
416 421 """Any text. Converts from the local character encoding to UTF-8."""
417 422 return encoding.fromlocal(text)
418 423
419 424 @templatefilter('xmlescape')
420 425 def xmlescape(text):
421 426 text = (text
422 427 .replace('&', '&amp;')
423 428 .replace('<', '&lt;')
424 429 .replace('>', '&gt;')
425 430 .replace('"', '&quot;')
426 431 .replace("'", '&#39;')) # &apos; invalid in HTML
427 432 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
428 433
429 434 def websub(text, websubtable):
430 435 """:websub: Any text. Only applies to hgweb. Applies the regular
431 436 expression replacements defined in the websub section.
432 437 """
433 438 if websubtable:
434 439 for regexp, format in websubtable:
435 440 text = regexp.sub(format, text)
436 441 return text
437 442
438 443 def loadfilter(ui, extname, registrarobj):
439 444 """Load template filter from specified registrarobj
440 445 """
441 446 for name, func in registrarobj._table.iteritems():
442 447 filters[name] = func
443 448
444 449 # tell hggettext to extract docstrings from these functions:
445 450 i18nfunctions = filters.values()
@@ -1,154 +1,163 b''
1 1 $ hg init repo
2 2 $ cd repo
3 3 $ echo 0 > a
4 4 $ echo 0 > b
5 5 $ echo 0 > t.h
6 6 $ mkdir t
7 7 $ echo 0 > t/x
8 8 $ echo 0 > t/b
9 9 $ echo 0 > t/e.h
10 10 $ mkdir dir.h
11 11 $ echo 0 > dir.h/foo
12 12
13 13 $ hg ci -A -m m
14 14 adding a
15 15 adding b
16 16 adding dir.h/foo
17 17 adding t.h
18 18 adding t/b
19 19 adding t/e.h
20 20 adding t/x
21 21
22 22 $ touch nottracked
23 23
24 24 $ hg locate a
25 25 a
26 26
27 27 $ hg locate NONEXISTENT
28 28 [1]
29 29
30 30 $ hg locate
31 31 a
32 32 b
33 33 dir.h/foo
34 34 t.h
35 35 t/b
36 36 t/e.h
37 37 t/x
38 38
39 39 $ hg rm a
40 40 $ hg ci -m m
41 41
42 42 $ hg locate a
43 43 [1]
44 44 $ hg locate NONEXISTENT
45 45 [1]
46 46 $ hg locate relpath:NONEXISTENT
47 47 [1]
48 48 $ hg locate
49 49 b
50 50 dir.h/foo
51 51 t.h
52 52 t/b
53 53 t/e.h
54 54 t/x
55 55 $ hg locate -r 0 a
56 56 a
57 57 $ hg locate -r 0 NONEXISTENT
58 58 [1]
59 59 $ hg locate -r 0 relpath:NONEXISTENT
60 60 [1]
61 61 $ hg locate -r 0
62 62 a
63 63 b
64 64 dir.h/foo
65 65 t.h
66 66 t/b
67 67 t/e.h
68 68 t/x
69 69
70 70 -I/-X with relative path should work:
71 71
72 72 $ cd t
73 73 $ hg locate
74 74 b
75 75 dir.h/foo
76 76 t.h
77 77 t/b
78 78 t/e.h
79 79 t/x
80 80 $ hg locate -I ../t
81 81 t/b
82 82 t/e.h
83 83 t/x
84 84
85 85 Issue294: hg remove --after dir fails when dir.* also exists
86 86
87 87 $ cd ..
88 88 $ rm -r t
89 89
90 90 $ hg rm t/b
91 91
92 92 $ hg locate 't/**'
93 93 t/b
94 94 t/e.h
95 95 t/x
96 96
97 97 $ hg files
98 98 b
99 99 dir.h/foo
100 100 t.h
101 101 t/e.h
102 102 t/x
103 103 $ hg files b
104 104 b
105 105
106 106 $ mkdir otherdir
107 107 $ cd otherdir
108 108
109 109 $ hg files path:
110 110 ../b
111 111 ../dir.h/foo
112 112 ../t.h
113 113 ../t/e.h
114 114 ../t/x
115 115 $ hg files path:.
116 116 ../b
117 117 ../dir.h/foo
118 118 ../t.h
119 119 ../t/e.h
120 120 ../t/x
121 121
122 122 $ hg locate b
123 123 ../b
124 124 ../t/b
125 125 $ hg locate '*.h'
126 126 ../t.h
127 127 ../t/e.h
128 128 $ hg locate path:t/x
129 129 ../t/x
130 130 $ hg locate 're:.*\.h$'
131 131 ../t.h
132 132 ../t/e.h
133 133 $ hg locate -r 0 b
134 134 ../b
135 135 ../t/b
136 136 $ hg locate -r 0 '*.h'
137 137 ../t.h
138 138 ../t/e.h
139 139 $ hg locate -r 0 path:t/x
140 140 ../t/x
141 141 $ hg locate -r 0 're:.*\.h$'
142 142 ../t.h
143 143 ../t/e.h
144 144
145 145 $ hg files
146 146 ../b
147 147 ../dir.h/foo
148 148 ../t.h
149 149 ../t/e.h
150 150 ../t/x
151 151 $ hg files .
152 152 [1]
153 153
154 Convert native path separator to slash (issue5572)
155
156 $ hg files -T '{path|slashpath}\n'
157 ../b
158 ../dir.h/foo
159 ../t.h
160 ../t/e.h
161 ../t/x
162
154 163 $ cd ../..
General Comments 0
You need to be logged in to leave comments. Login now