##// END OF EJS Templates
stringutil: move person function from templatefilters...
Connor Sheehan -
r37173:fb7140f1 default
parent child Browse files
Show More
@@ -1,457 +1,436 b''
1 # templatefilters.py - common template expansion filters
1 # templatefilters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12 import time
12 import time
13
13
14 from . import (
14 from . import (
15 encoding,
15 encoding,
16 error,
16 error,
17 node,
17 node,
18 pycompat,
18 pycompat,
19 registrar,
19 registrar,
20 templateutil,
20 templateutil,
21 url,
21 url,
22 util,
22 util,
23 )
23 )
24 from .utils import (
24 from .utils import (
25 dateutil,
25 dateutil,
26 stringutil,
26 stringutil,
27 )
27 )
28
28
29 urlerr = util.urlerr
29 urlerr = util.urlerr
30 urlreq = util.urlreq
30 urlreq = util.urlreq
31
31
32 if pycompat.ispy3:
32 if pycompat.ispy3:
33 long = int
33 long = int
34
34
35 # filters are callables like:
35 # filters are callables like:
36 # fn(obj)
36 # fn(obj)
37 # with:
37 # with:
38 # obj - object to be filtered (text, date, list and so on)
38 # obj - object to be filtered (text, date, list and so on)
39 filters = {}
39 filters = {}
40
40
41 templatefilter = registrar.templatefilter(filters)
41 templatefilter = registrar.templatefilter(filters)
42
42
43 @templatefilter('addbreaks')
43 @templatefilter('addbreaks')
44 def addbreaks(text):
44 def addbreaks(text):
45 """Any text. Add an XHTML "<br />" tag before the end of
45 """Any text. Add an XHTML "<br />" tag before the end of
46 every line except the last.
46 every line except the last.
47 """
47 """
48 return text.replace('\n', '<br/>\n')
48 return text.replace('\n', '<br/>\n')
49
49
50 agescales = [("year", 3600 * 24 * 365, 'Y'),
50 agescales = [("year", 3600 * 24 * 365, 'Y'),
51 ("month", 3600 * 24 * 30, 'M'),
51 ("month", 3600 * 24 * 30, 'M'),
52 ("week", 3600 * 24 * 7, 'W'),
52 ("week", 3600 * 24 * 7, 'W'),
53 ("day", 3600 * 24, 'd'),
53 ("day", 3600 * 24, 'd'),
54 ("hour", 3600, 'h'),
54 ("hour", 3600, 'h'),
55 ("minute", 60, 'm'),
55 ("minute", 60, 'm'),
56 ("second", 1, 's')]
56 ("second", 1, 's')]
57
57
58 @templatefilter('age')
58 @templatefilter('age')
59 def age(date, abbrev=False):
59 def age(date, abbrev=False):
60 """Date. Returns a human-readable date/time difference between the
60 """Date. Returns a human-readable date/time difference between the
61 given date/time and the current date/time.
61 given date/time and the current date/time.
62 """
62 """
63
63
64 def plural(t, c):
64 def plural(t, c):
65 if c == 1:
65 if c == 1:
66 return t
66 return t
67 return t + "s"
67 return t + "s"
68 def fmt(t, c, a):
68 def fmt(t, c, a):
69 if abbrev:
69 if abbrev:
70 return "%d%s" % (c, a)
70 return "%d%s" % (c, a)
71 return "%d %s" % (c, plural(t, c))
71 return "%d %s" % (c, plural(t, c))
72
72
73 now = time.time()
73 now = time.time()
74 then = date[0]
74 then = date[0]
75 future = False
75 future = False
76 if then > now:
76 if then > now:
77 future = True
77 future = True
78 delta = max(1, int(then - now))
78 delta = max(1, int(then - now))
79 if delta > agescales[0][1] * 30:
79 if delta > agescales[0][1] * 30:
80 return 'in the distant future'
80 return 'in the distant future'
81 else:
81 else:
82 delta = max(1, int(now - then))
82 delta = max(1, int(now - then))
83 if delta > agescales[0][1] * 2:
83 if delta > agescales[0][1] * 2:
84 return dateutil.shortdate(date)
84 return dateutil.shortdate(date)
85
85
86 for t, s, a in agescales:
86 for t, s, a in agescales:
87 n = delta // s
87 n = delta // s
88 if n >= 2 or s == 1:
88 if n >= 2 or s == 1:
89 if future:
89 if future:
90 return '%s from now' % fmt(t, n, a)
90 return '%s from now' % fmt(t, n, a)
91 return '%s ago' % fmt(t, n, a)
91 return '%s ago' % fmt(t, n, a)
92
92
93 @templatefilter('basename')
93 @templatefilter('basename')
94 def basename(path):
94 def basename(path):
95 """Any text. Treats the text as a path, and returns the last
95 """Any text. Treats the text as a path, and returns the last
96 component of the path after splitting by the path separator.
96 component of the path after splitting by the path separator.
97 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
97 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
98 """
98 """
99 return os.path.basename(path)
99 return os.path.basename(path)
100
100
101 @templatefilter('count')
101 @templatefilter('count')
102 def count(i):
102 def count(i):
103 """List or text. Returns the length as an integer."""
103 """List or text. Returns the length as an integer."""
104 return len(i)
104 return len(i)
105
105
106 @templatefilter('dirname')
106 @templatefilter('dirname')
107 def dirname(path):
107 def dirname(path):
108 """Any text. Treats the text as a path, and strips the last
108 """Any text. Treats the text as a path, and strips the last
109 component of the path after splitting by the path separator.
109 component of the path after splitting by the path separator.
110 """
110 """
111 return os.path.dirname(path)
111 return os.path.dirname(path)
112
112
113 @templatefilter('domain')
113 @templatefilter('domain')
114 def domain(author):
114 def domain(author):
115 """Any text. Finds the first string that looks like an email
115 """Any text. Finds the first string that looks like an email
116 address, and extracts just the domain component. Example: ``User
116 address, and extracts just the domain component. Example: ``User
117 <user@example.com>`` becomes ``example.com``.
117 <user@example.com>`` becomes ``example.com``.
118 """
118 """
119 f = author.find('@')
119 f = author.find('@')
120 if f == -1:
120 if f == -1:
121 return ''
121 return ''
122 author = author[f + 1:]
122 author = author[f + 1:]
123 f = author.find('>')
123 f = author.find('>')
124 if f >= 0:
124 if f >= 0:
125 author = author[:f]
125 author = author[:f]
126 return author
126 return author
127
127
128 @templatefilter('email')
128 @templatefilter('email')
129 def email(text):
129 def email(text):
130 """Any text. Extracts the first string that looks like an email
130 """Any text. Extracts the first string that looks like an email
131 address. Example: ``User <user@example.com>`` becomes
131 address. Example: ``User <user@example.com>`` becomes
132 ``user@example.com``.
132 ``user@example.com``.
133 """
133 """
134 return stringutil.email(text)
134 return stringutil.email(text)
135
135
136 @templatefilter('escape')
136 @templatefilter('escape')
137 def escape(text):
137 def escape(text):
138 """Any text. Replaces the special XML/XHTML characters "&", "<"
138 """Any text. Replaces the special XML/XHTML characters "&", "<"
139 and ">" with XML entities, and filters out NUL characters.
139 and ">" with XML entities, and filters out NUL characters.
140 """
140 """
141 return url.escape(text.replace('\0', ''), True)
141 return url.escape(text.replace('\0', ''), True)
142
142
143 para_re = None
143 para_re = None
144 space_re = None
144 space_re = None
145
145
146 def fill(text, width, initindent='', hangindent=''):
146 def fill(text, width, initindent='', hangindent=''):
147 '''fill many paragraphs with optional indentation.'''
147 '''fill many paragraphs with optional indentation.'''
148 global para_re, space_re
148 global para_re, space_re
149 if para_re is None:
149 if para_re is None:
150 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
150 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
151 space_re = re.compile(br' +')
151 space_re = re.compile(br' +')
152
152
153 def findparas():
153 def findparas():
154 start = 0
154 start = 0
155 while True:
155 while True:
156 m = para_re.search(text, start)
156 m = para_re.search(text, start)
157 if not m:
157 if not m:
158 uctext = encoding.unifromlocal(text[start:])
158 uctext = encoding.unifromlocal(text[start:])
159 w = len(uctext)
159 w = len(uctext)
160 while 0 < w and uctext[w - 1].isspace():
160 while 0 < w and uctext[w - 1].isspace():
161 w -= 1
161 w -= 1
162 yield (encoding.unitolocal(uctext[:w]),
162 yield (encoding.unitolocal(uctext[:w]),
163 encoding.unitolocal(uctext[w:]))
163 encoding.unitolocal(uctext[w:]))
164 break
164 break
165 yield text[start:m.start(0)], m.group(1)
165 yield text[start:m.start(0)], m.group(1)
166 start = m.end(1)
166 start = m.end(1)
167
167
168 return "".join([stringutil.wrap(space_re.sub(' ',
168 return "".join([stringutil.wrap(space_re.sub(' ',
169 stringutil.wrap(para, width)),
169 stringutil.wrap(para, width)),
170 width, initindent, hangindent) + rest
170 width, initindent, hangindent) + rest
171 for para, rest in findparas()])
171 for para, rest in findparas()])
172
172
173 @templatefilter('fill68')
173 @templatefilter('fill68')
174 def fill68(text):
174 def fill68(text):
175 """Any text. Wraps the text to fit in 68 columns."""
175 """Any text. Wraps the text to fit in 68 columns."""
176 return fill(text, 68)
176 return fill(text, 68)
177
177
178 @templatefilter('fill76')
178 @templatefilter('fill76')
179 def fill76(text):
179 def fill76(text):
180 """Any text. Wraps the text to fit in 76 columns."""
180 """Any text. Wraps the text to fit in 76 columns."""
181 return fill(text, 76)
181 return fill(text, 76)
182
182
183 @templatefilter('firstline')
183 @templatefilter('firstline')
184 def firstline(text):
184 def firstline(text):
185 """Any text. Returns the first line of text."""
185 """Any text. Returns the first line of text."""
186 try:
186 try:
187 return text.splitlines(True)[0].rstrip('\r\n')
187 return text.splitlines(True)[0].rstrip('\r\n')
188 except IndexError:
188 except IndexError:
189 return ''
189 return ''
190
190
191 @templatefilter('hex')
191 @templatefilter('hex')
192 def hexfilter(text):
192 def hexfilter(text):
193 """Any text. Convert a binary Mercurial node identifier into
193 """Any text. Convert a binary Mercurial node identifier into
194 its long hexadecimal representation.
194 its long hexadecimal representation.
195 """
195 """
196 return node.hex(text)
196 return node.hex(text)
197
197
198 @templatefilter('hgdate')
198 @templatefilter('hgdate')
199 def hgdate(text):
199 def hgdate(text):
200 """Date. Returns the date as a pair of numbers: "1157407993
200 """Date. Returns the date as a pair of numbers: "1157407993
201 25200" (Unix timestamp, timezone offset).
201 25200" (Unix timestamp, timezone offset).
202 """
202 """
203 return "%d %d" % text
203 return "%d %d" % text
204
204
205 @templatefilter('isodate')
205 @templatefilter('isodate')
206 def isodate(text):
206 def isodate(text):
207 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
207 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
208 +0200".
208 +0200".
209 """
209 """
210 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
210 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
211
211
212 @templatefilter('isodatesec')
212 @templatefilter('isodatesec')
213 def isodatesec(text):
213 def isodatesec(text):
214 """Date. Returns the date in ISO 8601 format, including
214 """Date. Returns the date in ISO 8601 format, including
215 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
215 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
216 filter.
216 filter.
217 """
217 """
218 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
218 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
219
219
220 def indent(text, prefix):
220 def indent(text, prefix):
221 '''indent each non-empty line of text after first with prefix.'''
221 '''indent each non-empty line of text after first with prefix.'''
222 lines = text.splitlines()
222 lines = text.splitlines()
223 num_lines = len(lines)
223 num_lines = len(lines)
224 endswithnewline = text[-1:] == '\n'
224 endswithnewline = text[-1:] == '\n'
225 def indenter():
225 def indenter():
226 for i in xrange(num_lines):
226 for i in xrange(num_lines):
227 l = lines[i]
227 l = lines[i]
228 if i and l.strip():
228 if i and l.strip():
229 yield prefix
229 yield prefix
230 yield l
230 yield l
231 if i < num_lines - 1 or endswithnewline:
231 if i < num_lines - 1 or endswithnewline:
232 yield '\n'
232 yield '\n'
233 return "".join(indenter())
233 return "".join(indenter())
234
234
235 @templatefilter('json')
235 @templatefilter('json')
236 def json(obj, paranoid=True):
236 def json(obj, paranoid=True):
237 if obj is None:
237 if obj is None:
238 return 'null'
238 return 'null'
239 elif obj is False:
239 elif obj is False:
240 return 'false'
240 return 'false'
241 elif obj is True:
241 elif obj is True:
242 return 'true'
242 return 'true'
243 elif isinstance(obj, (int, long, float)):
243 elif isinstance(obj, (int, long, float)):
244 return pycompat.bytestr(obj)
244 return pycompat.bytestr(obj)
245 elif isinstance(obj, bytes):
245 elif isinstance(obj, bytes):
246 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
246 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
247 elif isinstance(obj, str):
247 elif isinstance(obj, str):
248 # This branch is unreachable on Python 2, because bytes == str
248 # This branch is unreachable on Python 2, because bytes == str
249 # and we'll return in the next-earlier block in the elif
249 # and we'll return in the next-earlier block in the elif
250 # ladder. On Python 3, this helps us catch bugs before they
250 # ladder. On Python 3, this helps us catch bugs before they
251 # hurt someone.
251 # hurt someone.
252 raise error.ProgrammingError(
252 raise error.ProgrammingError(
253 'Mercurial only does output with bytes on Python 3: %r' % obj)
253 'Mercurial only does output with bytes on Python 3: %r' % obj)
254 elif util.safehasattr(obj, 'keys'):
254 elif util.safehasattr(obj, 'keys'):
255 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
255 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
256 json(v, paranoid))
256 json(v, paranoid))
257 for k, v in sorted(obj.iteritems())]
257 for k, v in sorted(obj.iteritems())]
258 return '{' + ', '.join(out) + '}'
258 return '{' + ', '.join(out) + '}'
259 elif util.safehasattr(obj, '__iter__'):
259 elif util.safehasattr(obj, '__iter__'):
260 out = [json(i, paranoid) for i in obj]
260 out = [json(i, paranoid) for i in obj]
261 return '[' + ', '.join(out) + ']'
261 return '[' + ', '.join(out) + ']'
262 else:
262 else:
263 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
263 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
264
264
265 @templatefilter('lower')
265 @templatefilter('lower')
266 def lower(text):
266 def lower(text):
267 """Any text. Converts the text to lowercase."""
267 """Any text. Converts the text to lowercase."""
268 return encoding.lower(text)
268 return encoding.lower(text)
269
269
270 @templatefilter('nonempty')
270 @templatefilter('nonempty')
271 def nonempty(text):
271 def nonempty(text):
272 """Any text. Returns '(none)' if the string is empty."""
272 """Any text. Returns '(none)' if the string is empty."""
273 return text or "(none)"
273 return text or "(none)"
274
274
275 @templatefilter('obfuscate')
275 @templatefilter('obfuscate')
276 def obfuscate(text):
276 def obfuscate(text):
277 """Any text. Returns the input text rendered as a sequence of
277 """Any text. Returns the input text rendered as a sequence of
278 XML entities.
278 XML entities.
279 """
279 """
280 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
280 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
281 return ''.join(['&#%d;' % ord(c) for c in text])
281 return ''.join(['&#%d;' % ord(c) for c in text])
282
282
283 @templatefilter('permissions')
283 @templatefilter('permissions')
284 def permissions(flags):
284 def permissions(flags):
285 if "l" in flags:
285 if "l" in flags:
286 return "lrwxrwxrwx"
286 return "lrwxrwxrwx"
287 if "x" in flags:
287 if "x" in flags:
288 return "-rwxr-xr-x"
288 return "-rwxr-xr-x"
289 return "-rw-r--r--"
289 return "-rw-r--r--"
290
290
291 @templatefilter('person')
291 @templatefilter('person')
292 def person(author):
292 def person(author):
293 """Any text. Returns the name before an email address,
293 """Any text. Returns the name before an email address,
294 interpreting it as per RFC 5322.
294 interpreting it as per RFC 5322.
295
296 >>> person(b'foo@bar')
297 'foo'
298 >>> person(b'Foo Bar <foo@bar>')
299 'Foo Bar'
300 >>> person(b'"Foo Bar" <foo@bar>')
301 'Foo Bar'
302 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
303 'Foo "buz" Bar'
304 >>> # The following are invalid, but do exist in real-life
305 ...
306 >>> person(b'Foo "buz" Bar <foo@bar>')
307 'Foo "buz" Bar'
308 >>> person(b'"Foo Bar <foo@bar>')
309 'Foo Bar'
310 """
295 """
311 if '@' not in author:
296 return stringutil.person(author)
312 return author
313 f = author.find('<')
314 if f != -1:
315 return author[:f].strip(' "').replace('\\"', '"')
316 f = author.find('@')
317 return author[:f].replace('.', ' ')
318
297
319 @templatefilter('revescape')
298 @templatefilter('revescape')
320 def revescape(text):
299 def revescape(text):
321 """Any text. Escapes all "special" characters, except @.
300 """Any text. Escapes all "special" characters, except @.
322 Forward slashes are escaped twice to prevent web servers from prematurely
301 Forward slashes are escaped twice to prevent web servers from prematurely
323 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
302 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
324 """
303 """
325 return urlreq.quote(text, safe='/@').replace('/', '%252F')
304 return urlreq.quote(text, safe='/@').replace('/', '%252F')
326
305
327 @templatefilter('rfc3339date')
306 @templatefilter('rfc3339date')
328 def rfc3339date(text):
307 def rfc3339date(text):
329 """Date. Returns a date using the Internet date format
308 """Date. Returns a date using the Internet date format
330 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
309 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
331 """
310 """
332 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
311 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
333
312
334 @templatefilter('rfc822date')
313 @templatefilter('rfc822date')
335 def rfc822date(text):
314 def rfc822date(text):
336 """Date. Returns a date using the same format used in email
315 """Date. Returns a date using the same format used in email
337 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
316 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
338 """
317 """
339 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
318 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
340
319
341 @templatefilter('short')
320 @templatefilter('short')
342 def short(text):
321 def short(text):
343 """Changeset hash. Returns the short form of a changeset hash,
322 """Changeset hash. Returns the short form of a changeset hash,
344 i.e. a 12 hexadecimal digit string.
323 i.e. a 12 hexadecimal digit string.
345 """
324 """
346 return text[:12]
325 return text[:12]
347
326
348 @templatefilter('shortbisect')
327 @templatefilter('shortbisect')
349 def shortbisect(label):
328 def shortbisect(label):
350 """Any text. Treats `label` as a bisection status, and
329 """Any text. Treats `label` as a bisection status, and
351 returns a single-character representing the status (G: good, B: bad,
330 returns a single-character representing the status (G: good, B: bad,
352 S: skipped, U: untested, I: ignored). Returns single space if `text`
331 S: skipped, U: untested, I: ignored). Returns single space if `text`
353 is not a valid bisection status.
332 is not a valid bisection status.
354 """
333 """
355 if label:
334 if label:
356 return label[0:1].upper()
335 return label[0:1].upper()
357 return ' '
336 return ' '
358
337
359 @templatefilter('shortdate')
338 @templatefilter('shortdate')
360 def shortdate(text):
339 def shortdate(text):
361 """Date. Returns a date like "2006-09-18"."""
340 """Date. Returns a date like "2006-09-18"."""
362 return dateutil.shortdate(text)
341 return dateutil.shortdate(text)
363
342
364 @templatefilter('slashpath')
343 @templatefilter('slashpath')
365 def slashpath(path):
344 def slashpath(path):
366 """Any text. Replaces the native path separator with slash."""
345 """Any text. Replaces the native path separator with slash."""
367 return util.pconvert(path)
346 return util.pconvert(path)
368
347
369 @templatefilter('splitlines')
348 @templatefilter('splitlines')
370 def splitlines(text):
349 def splitlines(text):
371 """Any text. Split text into a list of lines."""
350 """Any text. Split text into a list of lines."""
372 return templateutil.hybridlist(text.splitlines(), name='line')
351 return templateutil.hybridlist(text.splitlines(), name='line')
373
352
374 @templatefilter('stringescape')
353 @templatefilter('stringescape')
375 def stringescape(text):
354 def stringescape(text):
376 return stringutil.escapestr(text)
355 return stringutil.escapestr(text)
377
356
378 @templatefilter('stringify')
357 @templatefilter('stringify')
379 def stringify(thing):
358 def stringify(thing):
380 """Any type. Turns the value into text by converting values into
359 """Any type. Turns the value into text by converting values into
381 text and concatenating them.
360 text and concatenating them.
382 """
361 """
383 return templateutil.stringify(thing)
362 return templateutil.stringify(thing)
384
363
385 @templatefilter('stripdir')
364 @templatefilter('stripdir')
386 def stripdir(text):
365 def stripdir(text):
387 """Treat the text as path and strip a directory level, if
366 """Treat the text as path and strip a directory level, if
388 possible. For example, "foo" and "foo/bar" becomes "foo".
367 possible. For example, "foo" and "foo/bar" becomes "foo".
389 """
368 """
390 dir = os.path.dirname(text)
369 dir = os.path.dirname(text)
391 if dir == "":
370 if dir == "":
392 return os.path.basename(text)
371 return os.path.basename(text)
393 else:
372 else:
394 return dir
373 return dir
395
374
396 @templatefilter('tabindent')
375 @templatefilter('tabindent')
397 def tabindent(text):
376 def tabindent(text):
398 """Any text. Returns the text, with every non-empty line
377 """Any text. Returns the text, with every non-empty line
399 except the first starting with a tab character.
378 except the first starting with a tab character.
400 """
379 """
401 return indent(text, '\t')
380 return indent(text, '\t')
402
381
403 @templatefilter('upper')
382 @templatefilter('upper')
404 def upper(text):
383 def upper(text):
405 """Any text. Converts the text to uppercase."""
384 """Any text. Converts the text to uppercase."""
406 return encoding.upper(text)
385 return encoding.upper(text)
407
386
408 @templatefilter('urlescape')
387 @templatefilter('urlescape')
409 def urlescape(text):
388 def urlescape(text):
410 """Any text. Escapes all "special" characters. For example,
389 """Any text. Escapes all "special" characters. For example,
411 "foo bar" becomes "foo%20bar".
390 "foo bar" becomes "foo%20bar".
412 """
391 """
413 return urlreq.quote(text)
392 return urlreq.quote(text)
414
393
415 @templatefilter('user')
394 @templatefilter('user')
416 def userfilter(text):
395 def userfilter(text):
417 """Any text. Returns a short representation of a user name or email
396 """Any text. Returns a short representation of a user name or email
418 address."""
397 address."""
419 return stringutil.shortuser(text)
398 return stringutil.shortuser(text)
420
399
421 @templatefilter('emailuser')
400 @templatefilter('emailuser')
422 def emailuser(text):
401 def emailuser(text):
423 """Any text. Returns the user portion of an email address."""
402 """Any text. Returns the user portion of an email address."""
424 return stringutil.emailuser(text)
403 return stringutil.emailuser(text)
425
404
426 @templatefilter('utf8')
405 @templatefilter('utf8')
427 def utf8(text):
406 def utf8(text):
428 """Any text. Converts from the local character encoding to UTF-8."""
407 """Any text. Converts from the local character encoding to UTF-8."""
429 return encoding.fromlocal(text)
408 return encoding.fromlocal(text)
430
409
431 @templatefilter('xmlescape')
410 @templatefilter('xmlescape')
432 def xmlescape(text):
411 def xmlescape(text):
433 text = (text
412 text = (text
434 .replace('&', '&amp;')
413 .replace('&', '&amp;')
435 .replace('<', '&lt;')
414 .replace('<', '&lt;')
436 .replace('>', '&gt;')
415 .replace('>', '&gt;')
437 .replace('"', '&quot;')
416 .replace('"', '&quot;')
438 .replace("'", '&#39;')) # &apos; invalid in HTML
417 .replace("'", '&#39;')) # &apos; invalid in HTML
439 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
418 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
440
419
441 def websub(text, websubtable):
420 def websub(text, websubtable):
442 """:websub: Any text. Only applies to hgweb. Applies the regular
421 """:websub: Any text. Only applies to hgweb. Applies the regular
443 expression replacements defined in the websub section.
422 expression replacements defined in the websub section.
444 """
423 """
445 if websubtable:
424 if websubtable:
446 for regexp, format in websubtable:
425 for regexp, format in websubtable:
447 text = regexp.sub(format, text)
426 text = regexp.sub(format, text)
448 return text
427 return text
449
428
450 def loadfilter(ui, extname, registrarobj):
429 def loadfilter(ui, extname, registrarobj):
451 """Load template filter from specified registrarobj
430 """Load template filter from specified registrarobj
452 """
431 """
453 for name, func in registrarobj._table.iteritems():
432 for name, func in registrarobj._table.iteritems():
454 filters[name] = func
433 filters[name] = func
455
434
456 # tell hggettext to extract docstrings from these functions:
435 # tell hggettext to extract docstrings from these functions:
457 i18nfunctions = filters.values()
436 i18nfunctions = filters.values()
@@ -1,311 +1,338 b''
1 # stringutil.py - utility for generic string formatting, parsing, etc.
1 # stringutil.py - utility for generic string formatting, parsing, etc.
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import codecs
12 import codecs
13 import re as remod
13 import re as remod
14 import textwrap
14 import textwrap
15
15
16 from ..i18n import _
16 from ..i18n import _
17
17
18 from .. import (
18 from .. import (
19 encoding,
19 encoding,
20 error,
20 error,
21 pycompat,
21 pycompat,
22 )
22 )
23
23
24 _DATA_ESCAPE_MAP = {pycompat.bytechr(i): br'\x%02x' % i for i in range(256)}
24 _DATA_ESCAPE_MAP = {pycompat.bytechr(i): br'\x%02x' % i for i in range(256)}
25 _DATA_ESCAPE_MAP.update({
25 _DATA_ESCAPE_MAP.update({
26 b'\\': b'\\\\',
26 b'\\': b'\\\\',
27 b'\r': br'\r',
27 b'\r': br'\r',
28 b'\n': br'\n',
28 b'\n': br'\n',
29 })
29 })
30 _DATA_ESCAPE_RE = remod.compile(br'[\x00-\x08\x0a-\x1f\\\x7f-\xff]')
30 _DATA_ESCAPE_RE = remod.compile(br'[\x00-\x08\x0a-\x1f\\\x7f-\xff]')
31
31
32 def escapedata(s):
32 def escapedata(s):
33 if isinstance(s, bytearray):
33 if isinstance(s, bytearray):
34 s = bytes(s)
34 s = bytes(s)
35
35
36 return _DATA_ESCAPE_RE.sub(lambda m: _DATA_ESCAPE_MAP[m.group(0)], s)
36 return _DATA_ESCAPE_RE.sub(lambda m: _DATA_ESCAPE_MAP[m.group(0)], s)
37
37
38 def binary(s):
38 def binary(s):
39 """return true if a string is binary data"""
39 """return true if a string is binary data"""
40 return bool(s and '\0' in s)
40 return bool(s and '\0' in s)
41
41
42 def stringmatcher(pattern, casesensitive=True):
42 def stringmatcher(pattern, casesensitive=True):
43 """
43 """
44 accepts a string, possibly starting with 're:' or 'literal:' prefix.
44 accepts a string, possibly starting with 're:' or 'literal:' prefix.
45 returns the matcher name, pattern, and matcher function.
45 returns the matcher name, pattern, and matcher function.
46 missing or unknown prefixes are treated as literal matches.
46 missing or unknown prefixes are treated as literal matches.
47
47
48 helper for tests:
48 helper for tests:
49 >>> def test(pattern, *tests):
49 >>> def test(pattern, *tests):
50 ... kind, pattern, matcher = stringmatcher(pattern)
50 ... kind, pattern, matcher = stringmatcher(pattern)
51 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
51 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
52 >>> def itest(pattern, *tests):
52 >>> def itest(pattern, *tests):
53 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
53 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
54 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
54 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
55
55
56 exact matching (no prefix):
56 exact matching (no prefix):
57 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
57 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
58 ('literal', 'abcdefg', [False, False, True])
58 ('literal', 'abcdefg', [False, False, True])
59
59
60 regex matching ('re:' prefix)
60 regex matching ('re:' prefix)
61 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
61 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
62 ('re', 'a.+b', [False, False, True])
62 ('re', 'a.+b', [False, False, True])
63
63
64 force exact matches ('literal:' prefix)
64 force exact matches ('literal:' prefix)
65 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
65 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
66 ('literal', 're:foobar', [False, True])
66 ('literal', 're:foobar', [False, True])
67
67
68 unknown prefixes are ignored and treated as literals
68 unknown prefixes are ignored and treated as literals
69 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
69 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
70 ('literal', 'foo:bar', [False, False, True])
70 ('literal', 'foo:bar', [False, False, True])
71
71
72 case insensitive regex matches
72 case insensitive regex matches
73 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
73 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
74 ('re', 'A.+b', [False, False, True])
74 ('re', 'A.+b', [False, False, True])
75
75
76 case insensitive literal matches
76 case insensitive literal matches
77 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
77 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
78 ('literal', 'ABCDEFG', [False, False, True])
78 ('literal', 'ABCDEFG', [False, False, True])
79 """
79 """
80 if pattern.startswith('re:'):
80 if pattern.startswith('re:'):
81 pattern = pattern[3:]
81 pattern = pattern[3:]
82 try:
82 try:
83 flags = 0
83 flags = 0
84 if not casesensitive:
84 if not casesensitive:
85 flags = remod.I
85 flags = remod.I
86 regex = remod.compile(pattern, flags)
86 regex = remod.compile(pattern, flags)
87 except remod.error as e:
87 except remod.error as e:
88 raise error.ParseError(_('invalid regular expression: %s')
88 raise error.ParseError(_('invalid regular expression: %s')
89 % e)
89 % e)
90 return 're', pattern, regex.search
90 return 're', pattern, regex.search
91 elif pattern.startswith('literal:'):
91 elif pattern.startswith('literal:'):
92 pattern = pattern[8:]
92 pattern = pattern[8:]
93
93
94 match = pattern.__eq__
94 match = pattern.__eq__
95
95
96 if not casesensitive:
96 if not casesensitive:
97 ipat = encoding.lower(pattern)
97 ipat = encoding.lower(pattern)
98 match = lambda s: ipat == encoding.lower(s)
98 match = lambda s: ipat == encoding.lower(s)
99 return 'literal', pattern, match
99 return 'literal', pattern, match
100
100
101 def shortuser(user):
101 def shortuser(user):
102 """Return a short representation of a user name or email address."""
102 """Return a short representation of a user name or email address."""
103 f = user.find('@')
103 f = user.find('@')
104 if f >= 0:
104 if f >= 0:
105 user = user[:f]
105 user = user[:f]
106 f = user.find('<')
106 f = user.find('<')
107 if f >= 0:
107 if f >= 0:
108 user = user[f + 1:]
108 user = user[f + 1:]
109 f = user.find(' ')
109 f = user.find(' ')
110 if f >= 0:
110 if f >= 0:
111 user = user[:f]
111 user = user[:f]
112 f = user.find('.')
112 f = user.find('.')
113 if f >= 0:
113 if f >= 0:
114 user = user[:f]
114 user = user[:f]
115 return user
115 return user
116
116
117 def emailuser(user):
117 def emailuser(user):
118 """Return the user portion of an email address."""
118 """Return the user portion of an email address."""
119 f = user.find('@')
119 f = user.find('@')
120 if f >= 0:
120 if f >= 0:
121 user = user[:f]
121 user = user[:f]
122 f = user.find('<')
122 f = user.find('<')
123 if f >= 0:
123 if f >= 0:
124 user = user[f + 1:]
124 user = user[f + 1:]
125 return user
125 return user
126
126
127 def email(author):
127 def email(author):
128 '''get email of author.'''
128 '''get email of author.'''
129 r = author.find('>')
129 r = author.find('>')
130 if r == -1:
130 if r == -1:
131 r = None
131 r = None
132 return author[author.find('<') + 1:r]
132 return author[author.find('<') + 1:r]
133
133
134 def person(author):
135 """Returns the name before an email address,
136 interpreting it as per RFC 5322
137
138 >>> person(b'foo@bar')
139 'foo'
140 >>> person(b'Foo Bar <foo@bar>')
141 'Foo Bar'
142 >>> person(b'"Foo Bar" <foo@bar>')
143 'Foo Bar'
144 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
145 'Foo "buz" Bar'
146 >>> # The following are invalid, but do exist in real-life
147 ...
148 >>> person(b'Foo "buz" Bar <foo@bar>')
149 'Foo "buz" Bar'
150 >>> person(b'"Foo Bar <foo@bar>')
151 'Foo Bar'
152 """
153 if '@' not in author:
154 return author
155 f = author.find('<')
156 if f != -1:
157 return author[:f].strip(' "').replace('\\"', '"')
158 f = author.find('@')
159 return author[:f].replace('.', ' ')
160
134 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
161 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
135
162
136 def isauthorwellformed(author):
163 def isauthorwellformed(author):
137 '''Return True if the author field is well formed
164 '''Return True if the author field is well formed
138 (ie "Contributor Name <contrib@email.dom>")
165 (ie "Contributor Name <contrib@email.dom>")
139
166
140 >>> isauthorwellformed(b'Good Author <good@author.com>')
167 >>> isauthorwellformed(b'Good Author <good@author.com>')
141 True
168 True
142 >>> isauthorwellformed(b'Author <good@author.com>')
169 >>> isauthorwellformed(b'Author <good@author.com>')
143 True
170 True
144 >>> isauthorwellformed(b'Bad Author')
171 >>> isauthorwellformed(b'Bad Author')
145 False
172 False
146 >>> isauthorwellformed(b'Bad Author <author@author.com')
173 >>> isauthorwellformed(b'Bad Author <author@author.com')
147 False
174 False
148 >>> isauthorwellformed(b'Bad Author author@author.com')
175 >>> isauthorwellformed(b'Bad Author author@author.com')
149 False
176 False
150 >>> isauthorwellformed(b'<author@author.com>')
177 >>> isauthorwellformed(b'<author@author.com>')
151 False
178 False
152 >>> isauthorwellformed(b'Bad Author <author>')
179 >>> isauthorwellformed(b'Bad Author <author>')
153 False
180 False
154 '''
181 '''
155 return _correctauthorformat.match(author) is not None
182 return _correctauthorformat.match(author) is not None
156
183
157 def ellipsis(text, maxlength=400):
184 def ellipsis(text, maxlength=400):
158 """Trim string to at most maxlength (default: 400) columns in display."""
185 """Trim string to at most maxlength (default: 400) columns in display."""
159 return encoding.trim(text, maxlength, ellipsis='...')
186 return encoding.trim(text, maxlength, ellipsis='...')
160
187
161 def escapestr(s):
188 def escapestr(s):
162 # call underlying function of s.encode('string_escape') directly for
189 # call underlying function of s.encode('string_escape') directly for
163 # Python 3 compatibility
190 # Python 3 compatibility
164 return codecs.escape_encode(s)[0]
191 return codecs.escape_encode(s)[0]
165
192
166 def unescapestr(s):
193 def unescapestr(s):
167 return codecs.escape_decode(s)[0]
194 return codecs.escape_decode(s)[0]
168
195
169 def forcebytestr(obj):
196 def forcebytestr(obj):
170 """Portably format an arbitrary object (e.g. exception) into a byte
197 """Portably format an arbitrary object (e.g. exception) into a byte
171 string."""
198 string."""
172 try:
199 try:
173 return pycompat.bytestr(obj)
200 return pycompat.bytestr(obj)
174 except UnicodeEncodeError:
201 except UnicodeEncodeError:
175 # non-ascii string, may be lossy
202 # non-ascii string, may be lossy
176 return pycompat.bytestr(encoding.strtolocal(str(obj)))
203 return pycompat.bytestr(encoding.strtolocal(str(obj)))
177
204
178 def uirepr(s):
205 def uirepr(s):
179 # Avoid double backslash in Windows path repr()
206 # Avoid double backslash in Windows path repr()
180 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
207 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
181
208
182 # delay import of textwrap
209 # delay import of textwrap
183 def _MBTextWrapper(**kwargs):
210 def _MBTextWrapper(**kwargs):
184 class tw(textwrap.TextWrapper):
211 class tw(textwrap.TextWrapper):
185 """
212 """
186 Extend TextWrapper for width-awareness.
213 Extend TextWrapper for width-awareness.
187
214
188 Neither number of 'bytes' in any encoding nor 'characters' is
215 Neither number of 'bytes' in any encoding nor 'characters' is
189 appropriate to calculate terminal columns for specified string.
216 appropriate to calculate terminal columns for specified string.
190
217
191 Original TextWrapper implementation uses built-in 'len()' directly,
218 Original TextWrapper implementation uses built-in 'len()' directly,
192 so overriding is needed to use width information of each characters.
219 so overriding is needed to use width information of each characters.
193
220
194 In addition, characters classified into 'ambiguous' width are
221 In addition, characters classified into 'ambiguous' width are
195 treated as wide in East Asian area, but as narrow in other.
222 treated as wide in East Asian area, but as narrow in other.
196
223
197 This requires use decision to determine width of such characters.
224 This requires use decision to determine width of such characters.
198 """
225 """
199 def _cutdown(self, ucstr, space_left):
226 def _cutdown(self, ucstr, space_left):
200 l = 0
227 l = 0
201 colwidth = encoding.ucolwidth
228 colwidth = encoding.ucolwidth
202 for i in xrange(len(ucstr)):
229 for i in xrange(len(ucstr)):
203 l += colwidth(ucstr[i])
230 l += colwidth(ucstr[i])
204 if space_left < l:
231 if space_left < l:
205 return (ucstr[:i], ucstr[i:])
232 return (ucstr[:i], ucstr[i:])
206 return ucstr, ''
233 return ucstr, ''
207
234
208 # overriding of base class
235 # overriding of base class
209 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
236 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
210 space_left = max(width - cur_len, 1)
237 space_left = max(width - cur_len, 1)
211
238
212 if self.break_long_words:
239 if self.break_long_words:
213 cut, res = self._cutdown(reversed_chunks[-1], space_left)
240 cut, res = self._cutdown(reversed_chunks[-1], space_left)
214 cur_line.append(cut)
241 cur_line.append(cut)
215 reversed_chunks[-1] = res
242 reversed_chunks[-1] = res
216 elif not cur_line:
243 elif not cur_line:
217 cur_line.append(reversed_chunks.pop())
244 cur_line.append(reversed_chunks.pop())
218
245
219 # this overriding code is imported from TextWrapper of Python 2.6
246 # this overriding code is imported from TextWrapper of Python 2.6
220 # to calculate columns of string by 'encoding.ucolwidth()'
247 # to calculate columns of string by 'encoding.ucolwidth()'
221 def _wrap_chunks(self, chunks):
248 def _wrap_chunks(self, chunks):
222 colwidth = encoding.ucolwidth
249 colwidth = encoding.ucolwidth
223
250
224 lines = []
251 lines = []
225 if self.width <= 0:
252 if self.width <= 0:
226 raise ValueError("invalid width %r (must be > 0)" % self.width)
253 raise ValueError("invalid width %r (must be > 0)" % self.width)
227
254
228 # Arrange in reverse order so items can be efficiently popped
255 # Arrange in reverse order so items can be efficiently popped
229 # from a stack of chucks.
256 # from a stack of chucks.
230 chunks.reverse()
257 chunks.reverse()
231
258
232 while chunks:
259 while chunks:
233
260
234 # Start the list of chunks that will make up the current line.
261 # Start the list of chunks that will make up the current line.
235 # cur_len is just the length of all the chunks in cur_line.
262 # cur_len is just the length of all the chunks in cur_line.
236 cur_line = []
263 cur_line = []
237 cur_len = 0
264 cur_len = 0
238
265
239 # Figure out which static string will prefix this line.
266 # Figure out which static string will prefix this line.
240 if lines:
267 if lines:
241 indent = self.subsequent_indent
268 indent = self.subsequent_indent
242 else:
269 else:
243 indent = self.initial_indent
270 indent = self.initial_indent
244
271
245 # Maximum width for this line.
272 # Maximum width for this line.
246 width = self.width - len(indent)
273 width = self.width - len(indent)
247
274
248 # First chunk on line is whitespace -- drop it, unless this
275 # First chunk on line is whitespace -- drop it, unless this
249 # is the very beginning of the text (i.e. no lines started yet).
276 # is the very beginning of the text (i.e. no lines started yet).
250 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
277 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
251 del chunks[-1]
278 del chunks[-1]
252
279
253 while chunks:
280 while chunks:
254 l = colwidth(chunks[-1])
281 l = colwidth(chunks[-1])
255
282
256 # Can at least squeeze this chunk onto the current line.
283 # Can at least squeeze this chunk onto the current line.
257 if cur_len + l <= width:
284 if cur_len + l <= width:
258 cur_line.append(chunks.pop())
285 cur_line.append(chunks.pop())
259 cur_len += l
286 cur_len += l
260
287
261 # Nope, this line is full.
288 # Nope, this line is full.
262 else:
289 else:
263 break
290 break
264
291
265 # The current line is full, and the next chunk is too big to
292 # The current line is full, and the next chunk is too big to
266 # fit on *any* line (not just this one).
293 # fit on *any* line (not just this one).
267 if chunks and colwidth(chunks[-1]) > width:
294 if chunks and colwidth(chunks[-1]) > width:
268 self._handle_long_word(chunks, cur_line, cur_len, width)
295 self._handle_long_word(chunks, cur_line, cur_len, width)
269
296
270 # If the last chunk on this line is all whitespace, drop it.
297 # If the last chunk on this line is all whitespace, drop it.
271 if (self.drop_whitespace and
298 if (self.drop_whitespace and
272 cur_line and cur_line[-1].strip() == r''):
299 cur_line and cur_line[-1].strip() == r''):
273 del cur_line[-1]
300 del cur_line[-1]
274
301
275 # Convert current line back to a string and store it in list
302 # Convert current line back to a string and store it in list
276 # of all lines (return value).
303 # of all lines (return value).
277 if cur_line:
304 if cur_line:
278 lines.append(indent + r''.join(cur_line))
305 lines.append(indent + r''.join(cur_line))
279
306
280 return lines
307 return lines
281
308
282 global _MBTextWrapper
309 global _MBTextWrapper
283 _MBTextWrapper = tw
310 _MBTextWrapper = tw
284 return tw(**kwargs)
311 return tw(**kwargs)
285
312
286 def wrap(line, width, initindent='', hangindent=''):
313 def wrap(line, width, initindent='', hangindent=''):
287 maxindent = max(len(hangindent), len(initindent))
314 maxindent = max(len(hangindent), len(initindent))
288 if width <= maxindent:
315 if width <= maxindent:
289 # adjust for weird terminal size
316 # adjust for weird terminal size
290 width = max(78, maxindent + 1)
317 width = max(78, maxindent + 1)
291 line = line.decode(pycompat.sysstr(encoding.encoding),
318 line = line.decode(pycompat.sysstr(encoding.encoding),
292 pycompat.sysstr(encoding.encodingmode))
319 pycompat.sysstr(encoding.encodingmode))
293 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
320 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
294 pycompat.sysstr(encoding.encodingmode))
321 pycompat.sysstr(encoding.encodingmode))
295 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
322 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
296 pycompat.sysstr(encoding.encodingmode))
323 pycompat.sysstr(encoding.encodingmode))
297 wrapper = _MBTextWrapper(width=width,
324 wrapper = _MBTextWrapper(width=width,
298 initial_indent=initindent,
325 initial_indent=initindent,
299 subsequent_indent=hangindent)
326 subsequent_indent=hangindent)
300 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
327 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
301
328
302 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
329 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
303 '0': False, 'no': False, 'false': False, 'off': False,
330 '0': False, 'no': False, 'false': False, 'off': False,
304 'never': False}
331 'never': False}
305
332
306 def parsebool(s):
333 def parsebool(s):
307 """Parse s into a boolean.
334 """Parse s into a boolean.
308
335
309 If s is not a valid boolean, returns None.
336 If s is not a valid boolean, returns None.
310 """
337 """
311 return _booleans.get(s.lower(), None)
338 return _booleans.get(s.lower(), None)
General Comments 0
You need to be logged in to leave comments. Login now