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