##// END OF EJS Templates
fixes #326 some html special chars where not escaped in diffs + code garden in helpers
marcink -
r1781:089c81cf beta
parent child Browse files
Show More
@@ -1,447 +1,447 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30
30
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from mercurial.match import match
34
35 from vcs.exceptions import VCSError
33 from vcs.exceptions import VCSError
36 from vcs.nodes import FileNode
34 from vcs.nodes import FileNode
35 import markupsafe
36
37
37
38 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
38 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
39 """
39 """
40 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
40 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
41
41
42 :param ignore_whitespace: ignore whitespaces in diff
42 :param ignore_whitespace: ignore whitespaces in diff
43 """
43 """
44
44
45 for filenode in (filenode_old, filenode_new):
45 for filenode in (filenode_old, filenode_new):
46 if not isinstance(filenode, FileNode):
46 if not isinstance(filenode, FileNode):
47 raise VCSError("Given object should be FileNode object, not %s"
47 raise VCSError("Given object should be FileNode object, not %s"
48 % filenode.__class__)
48 % filenode.__class__)
49
49
50 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
50 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
51 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
51 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
52
52
53 repo = filenode_new.changeset.repository
53 repo = filenode_new.changeset.repository
54 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
54 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
55 ignore_whitespace, context)
55 ignore_whitespace, context)
56
56
57 return vcs_gitdiff
57 return vcs_gitdiff
58
58
59
59
60 class DiffProcessor(object):
60 class DiffProcessor(object):
61 """
61 """
62 Give it a unified diff and it returns a list of the files that were
62 Give it a unified diff and it returns a list of the files that were
63 mentioned in the diff together with a dict of meta information that
63 mentioned in the diff together with a dict of meta information that
64 can be used to render it in a HTML template.
64 can be used to render it in a HTML template.
65 """
65 """
66 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
66 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
67
67
68 def __init__(self, diff, differ='diff', format='udiff'):
68 def __init__(self, diff, differ='diff', format='udiff'):
69 """
69 """
70 :param diff: a text in diff format or generator
70 :param diff: a text in diff format or generator
71 :param format: format of diff passed, `udiff` or `gitdiff`
71 :param format: format of diff passed, `udiff` or `gitdiff`
72 """
72 """
73 if isinstance(diff, basestring):
73 if isinstance(diff, basestring):
74 diff = [diff]
74 diff = [diff]
75
75
76 self.__udiff = diff
76 self.__udiff = diff
77 self.__format = format
77 self.__format = format
78 self.adds = 0
78 self.adds = 0
79 self.removes = 0
79 self.removes = 0
80
80
81 if isinstance(self.__udiff, basestring):
81 if isinstance(self.__udiff, basestring):
82 self.lines = iter(self.__udiff.splitlines(1))
82 self.lines = iter(self.__udiff.splitlines(1))
83
83
84 elif self.__format == 'gitdiff':
84 elif self.__format == 'gitdiff':
85 udiff_copy = self.copy_iterator()
85 udiff_copy = self.copy_iterator()
86 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
86 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
87 else:
87 else:
88 udiff_copy = self.copy_iterator()
88 udiff_copy = self.copy_iterator()
89 self.lines = imap(self.escaper, udiff_copy)
89 self.lines = imap(self.escaper, udiff_copy)
90
90
91 # Select a differ.
91 # Select a differ.
92 if differ == 'difflib':
92 if differ == 'difflib':
93 self.differ = self._highlight_line_difflib
93 self.differ = self._highlight_line_difflib
94 else:
94 else:
95 self.differ = self._highlight_line_udiff
95 self.differ = self._highlight_line_udiff
96
96
97 def escaper(self, string):
97 def escaper(self, string):
98 return string.replace('<', '&lt;').replace('>', '&gt;')
98 return markupsafe.escape(string)
99
99
100 def copy_iterator(self):
100 def copy_iterator(self):
101 """
101 """
102 make a fresh copy of generator, we should not iterate thru
102 make a fresh copy of generator, we should not iterate thru
103 an original as it's needed for repeating operations on
103 an original as it's needed for repeating operations on
104 this instance of DiffProcessor
104 this instance of DiffProcessor
105 """
105 """
106 self.__udiff, iterator_copy = tee(self.__udiff)
106 self.__udiff, iterator_copy = tee(self.__udiff)
107 return iterator_copy
107 return iterator_copy
108
108
109 def _extract_rev(self, line1, line2):
109 def _extract_rev(self, line1, line2):
110 """
110 """
111 Extract the filename and revision hint from a line.
111 Extract the filename and revision hint from a line.
112 """
112 """
113
113
114 try:
114 try:
115 if line1.startswith('--- ') and line2.startswith('+++ '):
115 if line1.startswith('--- ') and line2.startswith('+++ '):
116 l1 = line1[4:].split(None, 1)
116 l1 = line1[4:].split(None, 1)
117 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
117 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
118 old_rev = l1[1] if len(l1) == 2 else 'old'
118 old_rev = l1[1] if len(l1) == 2 else 'old'
119
119
120 l2 = line2[4:].split(None, 1)
120 l2 = line2[4:].split(None, 1)
121 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
121 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
122 new_rev = l2[1] if len(l2) == 2 else 'new'
122 new_rev = l2[1] if len(l2) == 2 else 'new'
123
123
124 filename = old_filename if (old_filename !=
124 filename = old_filename if (old_filename !=
125 'dev/null') else new_filename
125 'dev/null') else new_filename
126
126
127 return filename, new_rev, old_rev
127 return filename, new_rev, old_rev
128 except (ValueError, IndexError):
128 except (ValueError, IndexError):
129 pass
129 pass
130
130
131 return None, None, None
131 return None, None, None
132
132
133 def _parse_gitdiff(self, diffiterator):
133 def _parse_gitdiff(self, diffiterator):
134 def line_decoder(l):
134 def line_decoder(l):
135 if l.startswith('+') and not l.startswith('+++'):
135 if l.startswith('+') and not l.startswith('+++'):
136 self.adds += 1
136 self.adds += 1
137 elif l.startswith('-') and not l.startswith('---'):
137 elif l.startswith('-') and not l.startswith('---'):
138 self.removes += 1
138 self.removes += 1
139 return l.decode('utf8', 'replace')
139 return l.decode('utf8', 'replace')
140
140
141 output = list(diffiterator)
141 output = list(diffiterator)
142 size = len(output)
142 size = len(output)
143
143
144 if size == 2:
144 if size == 2:
145 l = []
145 l = []
146 l.extend([output[0]])
146 l.extend([output[0]])
147 l.extend(output[1].splitlines(1))
147 l.extend(output[1].splitlines(1))
148 return map(line_decoder, l)
148 return map(line_decoder, l)
149 elif size == 1:
149 elif size == 1:
150 return map(line_decoder, output[0].splitlines(1))
150 return map(line_decoder, output[0].splitlines(1))
151 elif size == 0:
151 elif size == 0:
152 return []
152 return []
153
153
154 raise Exception('wrong size of diff %s' % size)
154 raise Exception('wrong size of diff %s' % size)
155
155
156 def _highlight_line_difflib(self, line, next):
156 def _highlight_line_difflib(self, line, next_):
157 """
157 """
158 Highlight inline changes in both lines.
158 Highlight inline changes in both lines.
159 """
159 """
160
160
161 if line['action'] == 'del':
161 if line['action'] == 'del':
162 old, new = line, next
162 old, new = line, next_
163 else:
163 else:
164 old, new = next, line
164 old, new = next_, line
165
165
166 oldwords = re.split(r'(\W)', old['line'])
166 oldwords = re.split(r'(\W)', old['line'])
167 newwords = re.split(r'(\W)', new['line'])
167 newwords = re.split(r'(\W)', new['line'])
168
168
169 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
169 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
170
170
171 oldfragments, newfragments = [], []
171 oldfragments, newfragments = [], []
172 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
172 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
173 oldfrag = ''.join(oldwords[i1:i2])
173 oldfrag = ''.join(oldwords[i1:i2])
174 newfrag = ''.join(newwords[j1:j2])
174 newfrag = ''.join(newwords[j1:j2])
175 if tag != 'equal':
175 if tag != 'equal':
176 if oldfrag:
176 if oldfrag:
177 oldfrag = '<del>%s</del>' % oldfrag
177 oldfrag = '<del>%s</del>' % oldfrag
178 if newfrag:
178 if newfrag:
179 newfrag = '<ins>%s</ins>' % newfrag
179 newfrag = '<ins>%s</ins>' % newfrag
180 oldfragments.append(oldfrag)
180 oldfragments.append(oldfrag)
181 newfragments.append(newfrag)
181 newfragments.append(newfrag)
182
182
183 old['line'] = "".join(oldfragments)
183 old['line'] = "".join(oldfragments)
184 new['line'] = "".join(newfragments)
184 new['line'] = "".join(newfragments)
185
185
186 def _highlight_line_udiff(self, line, next):
186 def _highlight_line_udiff(self, line, next_):
187 """
187 """
188 Highlight inline changes in both lines.
188 Highlight inline changes in both lines.
189 """
189 """
190 start = 0
190 start = 0
191 limit = min(len(line['line']), len(next['line']))
191 limit = min(len(line['line']), len(next_['line']))
192 while start < limit and line['line'][start] == next['line'][start]:
192 while start < limit and line['line'][start] == next_['line'][start]:
193 start += 1
193 start += 1
194 end = -1
194 end = -1
195 limit -= start
195 limit -= start
196 while -end <= limit and line['line'][end] == next['line'][end]:
196 while -end <= limit and line['line'][end] == next_['line'][end]:
197 end -= 1
197 end -= 1
198 end += 1
198 end += 1
199 if start or end:
199 if start or end:
200 def do(l):
200 def do(l):
201 last = end + len(l['line'])
201 last = end + len(l['line'])
202 if l['action'] == 'add':
202 if l['action'] == 'add':
203 tag = 'ins'
203 tag = 'ins'
204 else:
204 else:
205 tag = 'del'
205 tag = 'del'
206 l['line'] = '%s<%s>%s</%s>%s' % (
206 l['line'] = '%s<%s>%s</%s>%s' % (
207 l['line'][:start],
207 l['line'][:start],
208 tag,
208 tag,
209 l['line'][start:last],
209 l['line'][start:last],
210 tag,
210 tag,
211 l['line'][last:]
211 l['line'][last:]
212 )
212 )
213 do(line)
213 do(line)
214 do(next)
214 do(next_)
215
215
216 def _parse_udiff(self):
216 def _parse_udiff(self):
217 """
217 """
218 Parse the diff an return data for the template.
218 Parse the diff an return data for the template.
219 """
219 """
220 lineiter = self.lines
220 lineiter = self.lines
221 files = []
221 files = []
222 try:
222 try:
223 line = lineiter.next()
223 line = lineiter.next()
224 # skip first context
224 # skip first context
225 skipfirst = True
225 skipfirst = True
226 while 1:
226 while 1:
227 # continue until we found the old file
227 # continue until we found the old file
228 if not line.startswith('--- '):
228 if not line.startswith('--- '):
229 line = lineiter.next()
229 line = lineiter.next()
230 continue
230 continue
231
231
232 chunks = []
232 chunks = []
233 filename, old_rev, new_rev = \
233 filename, old_rev, new_rev = \
234 self._extract_rev(line, lineiter.next())
234 self._extract_rev(line, lineiter.next())
235 files.append({
235 files.append({
236 'filename': filename,
236 'filename': filename,
237 'old_revision': old_rev,
237 'old_revision': old_rev,
238 'new_revision': new_rev,
238 'new_revision': new_rev,
239 'chunks': chunks
239 'chunks': chunks
240 })
240 })
241
241
242 line = lineiter.next()
242 line = lineiter.next()
243 while line:
243 while line:
244 match = self._chunk_re.match(line)
244 match = self._chunk_re.match(line)
245 if not match:
245 if not match:
246 break
246 break
247
247
248 lines = []
248 lines = []
249 chunks.append(lines)
249 chunks.append(lines)
250
250
251 old_line, old_end, new_line, new_end = \
251 old_line, old_end, new_line, new_end = \
252 [int(x or 1) for x in match.groups()[:-1]]
252 [int(x or 1) for x in match.groups()[:-1]]
253 old_line -= 1
253 old_line -= 1
254 new_line -= 1
254 new_line -= 1
255 context = len(match.groups()) == 5
255 context = len(match.groups()) == 5
256 old_end += old_line
256 old_end += old_line
257 new_end += new_line
257 new_end += new_line
258
258
259 if context:
259 if context:
260 if not skipfirst:
260 if not skipfirst:
261 lines.append({
261 lines.append({
262 'old_lineno': '...',
262 'old_lineno': '...',
263 'new_lineno': '...',
263 'new_lineno': '...',
264 'action': 'context',
264 'action': 'context',
265 'line': line,
265 'line': line,
266 })
266 })
267 else:
267 else:
268 skipfirst = False
268 skipfirst = False
269
269
270 line = lineiter.next()
270 line = lineiter.next()
271 while old_line < old_end or new_line < new_end:
271 while old_line < old_end or new_line < new_end:
272 if line:
272 if line:
273 command, line = line[0], line[1:]
273 command, line = line[0], line[1:]
274 else:
274 else:
275 command = ' '
275 command = ' '
276 affects_old = affects_new = False
276 affects_old = affects_new = False
277
277
278 # ignore those if we don't expect them
278 # ignore those if we don't expect them
279 if command in '#@':
279 if command in '#@':
280 continue
280 continue
281 elif command == '+':
281 elif command == '+':
282 affects_new = True
282 affects_new = True
283 action = 'add'
283 action = 'add'
284 elif command == '-':
284 elif command == '-':
285 affects_old = True
285 affects_old = True
286 action = 'del'
286 action = 'del'
287 else:
287 else:
288 affects_old = affects_new = True
288 affects_old = affects_new = True
289 action = 'unmod'
289 action = 'unmod'
290
290
291 old_line += affects_old
291 old_line += affects_old
292 new_line += affects_new
292 new_line += affects_new
293 lines.append({
293 lines.append({
294 'old_lineno': affects_old and old_line or '',
294 'old_lineno': affects_old and old_line or '',
295 'new_lineno': affects_new and new_line or '',
295 'new_lineno': affects_new and new_line or '',
296 'action': action,
296 'action': action,
297 'line': line
297 'line': line
298 })
298 })
299 line = lineiter.next()
299 line = lineiter.next()
300
300
301 except StopIteration:
301 except StopIteration:
302 pass
302 pass
303
303
304 # highlight inline changes
304 # highlight inline changes
305 for file in files:
305 for _ in files:
306 for chunk in chunks:
306 for chunk in chunks:
307 lineiter = iter(chunk)
307 lineiter = iter(chunk)
308 #first = True
308 #first = True
309 try:
309 try:
310 while 1:
310 while 1:
311 line = lineiter.next()
311 line = lineiter.next()
312 if line['action'] != 'unmod':
312 if line['action'] != 'unmod':
313 nextline = lineiter.next()
313 nextline = lineiter.next()
314 if nextline['action'] == 'unmod' or \
314 if nextline['action'] == 'unmod' or \
315 nextline['action'] == line['action']:
315 nextline['action'] == line['action']:
316 continue
316 continue
317 self.differ(line, nextline)
317 self.differ(line, nextline)
318 except StopIteration:
318 except StopIteration:
319 pass
319 pass
320
320
321 return files
321 return files
322
322
323 def prepare(self):
323 def prepare(self):
324 """
324 """
325 Prepare the passed udiff for HTML rendering. It'l return a list
325 Prepare the passed udiff for HTML rendering. It'l return a list
326 of dicts
326 of dicts
327 """
327 """
328 return self._parse_udiff()
328 return self._parse_udiff()
329
329
330 def _safe_id(self, idstring):
330 def _safe_id(self, idstring):
331 """Make a string safe for including in an id attribute.
331 """Make a string safe for including in an id attribute.
332
332
333 The HTML spec says that id attributes 'must begin with
333 The HTML spec says that id attributes 'must begin with
334 a letter ([A-Za-z]) and may be followed by any number
334 a letter ([A-Za-z]) and may be followed by any number
335 of letters, digits ([0-9]), hyphens ("-"), underscores
335 of letters, digits ([0-9]), hyphens ("-"), underscores
336 ("_"), colons (":"), and periods (".")'. These regexps
336 ("_"), colons (":"), and periods (".")'. These regexps
337 are slightly over-zealous, in that they remove colons
337 are slightly over-zealous, in that they remove colons
338 and periods unnecessarily.
338 and periods unnecessarily.
339
339
340 Whitespace is transformed into underscores, and then
340 Whitespace is transformed into underscores, and then
341 anything which is not a hyphen or a character that
341 anything which is not a hyphen or a character that
342 matches \w (alphanumerics and underscore) is removed.
342 matches \w (alphanumerics and underscore) is removed.
343
343
344 """
344 """
345 # Transform all whitespace to underscore
345 # Transform all whitespace to underscore
346 idstring = re.sub(r'\s', "_", '%s' % idstring)
346 idstring = re.sub(r'\s', "_", '%s' % idstring)
347 # Remove everything that is not a hyphen or a member of \w
347 # Remove everything that is not a hyphen or a member of \w
348 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
348 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
349 return idstring
349 return idstring
350
350
351 def raw_diff(self):
351 def raw_diff(self):
352 """
352 """
353 Returns raw string as udiff
353 Returns raw string as udiff
354 """
354 """
355 udiff_copy = self.copy_iterator()
355 udiff_copy = self.copy_iterator()
356 if self.__format == 'gitdiff':
356 if self.__format == 'gitdiff':
357 udiff_copy = self._parse_gitdiff(udiff_copy)
357 udiff_copy = self._parse_gitdiff(udiff_copy)
358 return u''.join(udiff_copy)
358 return u''.join(udiff_copy)
359
359
360 def as_html(self, table_class='code-difftable', line_class='line',
360 def as_html(self, table_class='code-difftable', line_class='line',
361 new_lineno_class='lineno old', old_lineno_class='lineno new',
361 new_lineno_class='lineno old', old_lineno_class='lineno new',
362 code_class='code'):
362 code_class='code'):
363 """
363 """
364 Return udiff as html table with customized css classes
364 Return udiff as html table with customized css classes
365 """
365 """
366 def _link_to_if(condition, label, url):
366 def _link_to_if(condition, label, url):
367 """
367 """
368 Generates a link if condition is meet or just the label if not.
368 Generates a link if condition is meet or just the label if not.
369 """
369 """
370
370
371 if condition:
371 if condition:
372 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
372 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
373 'label': label}
373 'label': label}
374 else:
374 else:
375 return label
375 return label
376 diff_lines = self.prepare()
376 diff_lines = self.prepare()
377 _html_empty = True
377 _html_empty = True
378 _html = []
378 _html = []
379 _html.append('''<table class="%(table_class)s">\n''' \
379 _html.append('''<table class="%(table_class)s">\n''' \
380 % {'table_class': table_class})
380 % {'table_class': table_class})
381 for diff in diff_lines:
381 for diff in diff_lines:
382 for line in diff['chunks']:
382 for line in diff['chunks']:
383 _html_empty = False
383 _html_empty = False
384 for change in line:
384 for change in line:
385 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
385 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
386 % {'line_class': line_class,
386 % {'line_class': line_class,
387 'action': change['action']})
387 'action': change['action']})
388 anchor_old_id = ''
388 anchor_old_id = ''
389 anchor_new_id = ''
389 anchor_new_id = ''
390 anchor_old = "%(filename)s_o%(oldline_no)s" % \
390 anchor_old = "%(filename)s_o%(oldline_no)s" % \
391 {'filename': self._safe_id(diff['filename']),
391 {'filename': self._safe_id(diff['filename']),
392 'oldline_no': change['old_lineno']}
392 'oldline_no': change['old_lineno']}
393 anchor_new = "%(filename)s_n%(oldline_no)s" % \
393 anchor_new = "%(filename)s_n%(oldline_no)s" % \
394 {'filename': self._safe_id(diff['filename']),
394 {'filename': self._safe_id(diff['filename']),
395 'oldline_no': change['new_lineno']}
395 'oldline_no': change['new_lineno']}
396 cond_old = change['old_lineno'] != '...' and \
396 cond_old = change['old_lineno'] != '...' and \
397 change['old_lineno']
397 change['old_lineno']
398 cond_new = change['new_lineno'] != '...' and \
398 cond_new = change['new_lineno'] != '...' and \
399 change['new_lineno']
399 change['new_lineno']
400 if cond_old:
400 if cond_old:
401 anchor_old_id = 'id="%s"' % anchor_old
401 anchor_old_id = 'id="%s"' % anchor_old
402 if cond_new:
402 if cond_new:
403 anchor_new_id = 'id="%s"' % anchor_new
403 anchor_new_id = 'id="%s"' % anchor_new
404 ###########################################################
404 ###########################################################
405 # OLD LINE NUMBER
405 # OLD LINE NUMBER
406 ###########################################################
406 ###########################################################
407 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
407 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
408 % {'a_id': anchor_old_id,
408 % {'a_id': anchor_old_id,
409 'old_lineno_cls': old_lineno_class})
409 'old_lineno_cls': old_lineno_class})
410
410
411 _html.append('''%(link)s''' \
411 _html.append('''%(link)s''' \
412 % {'link':
412 % {'link':
413 _link_to_if(cond_old, change['old_lineno'], '#%s' \
413 _link_to_if(cond_old, change['old_lineno'], '#%s' \
414 % anchor_old)})
414 % anchor_old)})
415 _html.append('''</td>\n''')
415 _html.append('''</td>\n''')
416 ###########################################################
416 ###########################################################
417 # NEW LINE NUMBER
417 # NEW LINE NUMBER
418 ###########################################################
418 ###########################################################
419
419
420 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
420 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
421 % {'a_id': anchor_new_id,
421 % {'a_id': anchor_new_id,
422 'new_lineno_cls': new_lineno_class})
422 'new_lineno_cls': new_lineno_class})
423
423
424 _html.append('''%(link)s''' \
424 _html.append('''%(link)s''' \
425 % {'link':
425 % {'link':
426 _link_to_if(cond_new, change['new_lineno'], '#%s' \
426 _link_to_if(cond_new, change['new_lineno'], '#%s' \
427 % anchor_new)})
427 % anchor_new)})
428 _html.append('''</td>\n''')
428 _html.append('''</td>\n''')
429 ###########################################################
429 ###########################################################
430 # CODE
430 # CODE
431 ###########################################################
431 ###########################################################
432 _html.append('''\t<td class="%(code_class)s">''' \
432 _html.append('''\t<td class="%(code_class)s">''' \
433 % {'code_class': code_class})
433 % {'code_class': code_class})
434 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
434 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
435 % {'code': change['line']})
435 % {'code': change['line']})
436 _html.append('''\t</td>''')
436 _html.append('''\t</td>''')
437 _html.append('''\n</tr>\n''')
437 _html.append('''\n</tr>\n''')
438 _html.append('''</table>''')
438 _html.append('''</table>''')
439 if _html_empty:
439 if _html_empty:
440 return None
440 return None
441 return ''.join(_html)
441 return ''.join(_html)
442
442
443 def stat(self):
443 def stat(self):
444 """
444 """
445 Returns tuple of added, and removed lines for this instance
445 Returns tuple of added, and removed lines for this instance
446 """
446 """
447 return self.adds, self.removes
447 return self.adds, self.removes
@@ -1,739 +1,751 b''
1 """Helper functions
1 """Helper functions
2
2
3 Consists of functions to typically be used within templates, but also
3 Consists of functions to typically be used within templates, but also
4 available to Controllers. This module is available to both as 'h'.
4 available to Controllers. This module is available to both as 'h'.
5 """
5 """
6 import random
6 import random
7 import hashlib
7 import hashlib
8 import StringIO
8 import StringIO
9 import urllib
9 import urllib
10 import math
10 import math
11
11
12 from datetime import datetime
12 from datetime import datetime
13 from pygments.formatters.html import HtmlFormatter
13 from pygments.formatters.html import HtmlFormatter
14 from pygments import highlight as code_highlight
14 from pygments import highlight as code_highlight
15 from pylons import url, request, config
15 from pylons import url, request, config
16 from pylons.i18n.translation import _, ungettext
16 from pylons.i18n.translation import _, ungettext
17
17
18 from webhelpers.html import literal, HTML, escape
18 from webhelpers.html import literal, HTML, escape
19 from webhelpers.html.tools import *
19 from webhelpers.html.tools import *
20 from webhelpers.html.builder import make_tag
20 from webhelpers.html.builder import make_tag
21 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
21 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
22 end_form, file, form, hidden, image, javascript_link, link_to, \
22 end_form, file, form, hidden, image, javascript_link, link_to, \
23 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
23 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
24 submit, text, password, textarea, title, ul, xml_declaration, radio
24 submit, text, password, textarea, title, ul, xml_declaration, radio
25 from webhelpers.html.tools import auto_link, button_to, highlight, \
25 from webhelpers.html.tools import auto_link, button_to, highlight, \
26 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
26 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
27 from webhelpers.number import format_byte_size, format_bit_size
27 from webhelpers.number import format_byte_size, format_bit_size
28 from webhelpers.pylonslib import Flash as _Flash
28 from webhelpers.pylonslib import Flash as _Flash
29 from webhelpers.pylonslib.secure_form import secure_form
29 from webhelpers.pylonslib.secure_form import secure_form
30 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
30 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
31 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
31 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
32 replace_whitespace, urlify, truncate, wrap_paragraphs
32 replace_whitespace, urlify, truncate, wrap_paragraphs
33 from webhelpers.date import time_ago_in_words
33 from webhelpers.date import time_ago_in_words
34 from webhelpers.paginate import Page
34 from webhelpers.paginate import Page
35 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
35 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
36 convert_boolean_attrs, NotGiven, _make_safe_id_component
36 convert_boolean_attrs, NotGiven, _make_safe_id_component
37
37
38 from rhodecode.lib.annotate import annotate_highlight
38 from rhodecode.lib.annotate import annotate_highlight
39 from rhodecode.lib.utils import repo_name_slug
39 from rhodecode.lib.utils import repo_name_slug
40 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
40 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
41 from rhodecode.lib.markup_renderer import MarkupRenderer
41 from rhodecode.lib.markup_renderer import MarkupRenderer
42
42
43 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
43 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
44 """
44 """
45 Reset button
45 Reset button
46 """
46 """
47 _set_input_attrs(attrs, type, name, value)
47 _set_input_attrs(attrs, type, name, value)
48 _set_id_attr(attrs, id, name)
48 _set_id_attr(attrs, id, name)
49 convert_boolean_attrs(attrs, ["disabled"])
49 convert_boolean_attrs(attrs, ["disabled"])
50 return HTML.input(**attrs)
50 return HTML.input(**attrs)
51
51
52 reset = _reset
52 reset = _reset
53 safeid = _make_safe_id_component
53 safeid = _make_safe_id_component
54
54
55
55
56 def FID(raw_id,path):
56 def FID(raw_id,path):
57 """
57 """
58 Creates a uniqe ID for filenode based on it's path and revision
58 Creates a uniqe ID for filenode based on it's path and revision
59
59
60 :param raw_id:
60 :param raw_id:
61 :param path:
61 :param path:
62 """
62 """
63 return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
63 return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
64
64
65
65
66 def get_token():
66 def get_token():
67 """Return the current authentication token, creating one if one doesn't
67 """Return the current authentication token, creating one if one doesn't
68 already exist.
68 already exist.
69 """
69 """
70 token_key = "_authentication_token"
70 token_key = "_authentication_token"
71 from pylons import session
71 from pylons import session
72 if not token_key in session:
72 if not token_key in session:
73 try:
73 try:
74 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
74 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
75 except AttributeError: # Python < 2.4
75 except AttributeError: # Python < 2.4
76 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
76 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
77 session[token_key] = token
77 session[token_key] = token
78 if hasattr(session, 'save'):
78 if hasattr(session, 'save'):
79 session.save()
79 session.save()
80 return session[token_key]
80 return session[token_key]
81
81
82 class _GetError(object):
82 class _GetError(object):
83 """Get error from form_errors, and represent it as span wrapped error
83 """Get error from form_errors, and represent it as span wrapped error
84 message
84 message
85
85
86 :param field_name: field to fetch errors for
86 :param field_name: field to fetch errors for
87 :param form_errors: form errors dict
87 :param form_errors: form errors dict
88 """
88 """
89
89
90 def __call__(self, field_name, form_errors):
90 def __call__(self, field_name, form_errors):
91 tmpl = """<span class="error_msg">%s</span>"""
91 tmpl = """<span class="error_msg">%s</span>"""
92 if form_errors and form_errors.has_key(field_name):
92 if form_errors and form_errors.has_key(field_name):
93 return literal(tmpl % form_errors.get(field_name))
93 return literal(tmpl % form_errors.get(field_name))
94
94
95 get_error = _GetError()
95 get_error = _GetError()
96
96
97 class _ToolTip(object):
97 class _ToolTip(object):
98
98
99 def __call__(self, tooltip_title, trim_at=50):
99 def __call__(self, tooltip_title, trim_at=50):
100 """Special function just to wrap our text into nice formatted
100 """Special function just to wrap our text into nice formatted
101 autowrapped text
101 autowrapped text
102
102
103 :param tooltip_title:
103 :param tooltip_title:
104 """
104 """
105 return escape(tooltip_title)
105 return escape(tooltip_title)
106 tooltip = _ToolTip()
106 tooltip = _ToolTip()
107
107
108 class _FilesBreadCrumbs(object):
108 class _FilesBreadCrumbs(object):
109
109
110 def __call__(self, repo_name, rev, paths):
110 def __call__(self, repo_name, rev, paths):
111 if isinstance(paths, str):
111 if isinstance(paths, str):
112 paths = safe_unicode(paths)
112 paths = safe_unicode(paths)
113 url_l = [link_to(repo_name, url('files_home',
113 url_l = [link_to(repo_name, url('files_home',
114 repo_name=repo_name,
114 repo_name=repo_name,
115 revision=rev, f_path=''))]
115 revision=rev, f_path=''))]
116 paths_l = paths.split('/')
116 paths_l = paths.split('/')
117 for cnt, p in enumerate(paths_l):
117 for cnt, p in enumerate(paths_l):
118 if p != '':
118 if p != '':
119 url_l.append(link_to(p,
119 url_l.append(link_to(p,
120 url('files_home',
120 url('files_home',
121 repo_name=repo_name,
121 repo_name=repo_name,
122 revision=rev,
122 revision=rev,
123 f_path='/'.join(paths_l[:cnt + 1])
123 f_path='/'.join(paths_l[:cnt + 1])
124 )
124 )
125 )
125 )
126 )
126 )
127
127
128 return literal('/'.join(url_l))
128 return literal('/'.join(url_l))
129
129
130 files_breadcrumbs = _FilesBreadCrumbs()
130 files_breadcrumbs = _FilesBreadCrumbs()
131
131
132 class CodeHtmlFormatter(HtmlFormatter):
132 class CodeHtmlFormatter(HtmlFormatter):
133 """My code Html Formatter for source codes
133 """My code Html Formatter for source codes
134 """
134 """
135
135
136 def wrap(self, source, outfile):
136 def wrap(self, source, outfile):
137 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
137 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
138
138
139 def _wrap_code(self, source):
139 def _wrap_code(self, source):
140 for cnt, it in enumerate(source):
140 for cnt, it in enumerate(source):
141 i, t = it
141 i, t = it
142 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
142 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
143 yield i, t
143 yield i, t
144
144
145 def _wrap_tablelinenos(self, inner):
145 def _wrap_tablelinenos(self, inner):
146 dummyoutfile = StringIO.StringIO()
146 dummyoutfile = StringIO.StringIO()
147 lncount = 0
147 lncount = 0
148 for t, line in inner:
148 for t, line in inner:
149 if t:
149 if t:
150 lncount += 1
150 lncount += 1
151 dummyoutfile.write(line)
151 dummyoutfile.write(line)
152
152
153 fl = self.linenostart
153 fl = self.linenostart
154 mw = len(str(lncount + fl - 1))
154 mw = len(str(lncount + fl - 1))
155 sp = self.linenospecial
155 sp = self.linenospecial
156 st = self.linenostep
156 st = self.linenostep
157 la = self.lineanchors
157 la = self.lineanchors
158 aln = self.anchorlinenos
158 aln = self.anchorlinenos
159 nocls = self.noclasses
159 nocls = self.noclasses
160 if sp:
160 if sp:
161 lines = []
161 lines = []
162
162
163 for i in range(fl, fl + lncount):
163 for i in range(fl, fl + lncount):
164 if i % st == 0:
164 if i % st == 0:
165 if i % sp == 0:
165 if i % sp == 0:
166 if aln:
166 if aln:
167 lines.append('<a href="#%s%d" class="special">%*d</a>' %
167 lines.append('<a href="#%s%d" class="special">%*d</a>' %
168 (la, i, mw, i))
168 (la, i, mw, i))
169 else:
169 else:
170 lines.append('<span class="special">%*d</span>' % (mw, i))
170 lines.append('<span class="special">%*d</span>' % (mw, i))
171 else:
171 else:
172 if aln:
172 if aln:
173 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
173 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
174 else:
174 else:
175 lines.append('%*d' % (mw, i))
175 lines.append('%*d' % (mw, i))
176 else:
176 else:
177 lines.append('')
177 lines.append('')
178 ls = '\n'.join(lines)
178 ls = '\n'.join(lines)
179 else:
179 else:
180 lines = []
180 lines = []
181 for i in range(fl, fl + lncount):
181 for i in range(fl, fl + lncount):
182 if i % st == 0:
182 if i % st == 0:
183 if aln:
183 if aln:
184 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
184 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
185 else:
185 else:
186 lines.append('%*d' % (mw, i))
186 lines.append('%*d' % (mw, i))
187 else:
187 else:
188 lines.append('')
188 lines.append('')
189 ls = '\n'.join(lines)
189 ls = '\n'.join(lines)
190
190
191 # in case you wonder about the seemingly redundant <div> here: since the
191 # in case you wonder about the seemingly redundant <div> here: since the
192 # content in the other cell also is wrapped in a div, some browsers in
192 # content in the other cell also is wrapped in a div, some browsers in
193 # some configurations seem to mess up the formatting...
193 # some configurations seem to mess up the formatting...
194 if nocls:
194 if nocls:
195 yield 0, ('<table class="%stable">' % self.cssclass +
195 yield 0, ('<table class="%stable">' % self.cssclass +
196 '<tr><td><div class="linenodiv" '
196 '<tr><td><div class="linenodiv" '
197 'style="background-color: #f0f0f0; padding-right: 10px">'
197 'style="background-color: #f0f0f0; padding-right: 10px">'
198 '<pre style="line-height: 125%">' +
198 '<pre style="line-height: 125%">' +
199 ls + '</pre></div></td><td id="hlcode" class="code">')
199 ls + '</pre></div></td><td id="hlcode" class="code">')
200 else:
200 else:
201 yield 0, ('<table class="%stable">' % self.cssclass +
201 yield 0, ('<table class="%stable">' % self.cssclass +
202 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
202 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
203 ls + '</pre></div></td><td id="hlcode" class="code">')
203 ls + '</pre></div></td><td id="hlcode" class="code">')
204 yield 0, dummyoutfile.getvalue()
204 yield 0, dummyoutfile.getvalue()
205 yield 0, '</td></tr></table>'
205 yield 0, '</td></tr></table>'
206
206
207
207
208 def pygmentize(filenode, **kwargs):
208 def pygmentize(filenode, **kwargs):
209 """pygmentize function using pygments
209 """pygmentize function using pygments
210
210
211 :param filenode:
211 :param filenode:
212 """
212 """
213
213
214 return literal(code_highlight(filenode.content,
214 return literal(code_highlight(filenode.content,
215 filenode.lexer, CodeHtmlFormatter(**kwargs)))
215 filenode.lexer, CodeHtmlFormatter(**kwargs)))
216
216
217
217 def pygmentize_annotation(repo_name, filenode, **kwargs):
218 def pygmentize_annotation(repo_name, filenode, **kwargs):
218 """pygmentize function for annotation
219 """
220 pygmentize function for annotation
219
221
220 :param filenode:
222 :param filenode:
221 """
223 """
222
224
223 color_dict = {}
225 color_dict = {}
226
224 def gen_color(n=10000):
227 def gen_color(n=10000):
225 """generator for getting n of evenly distributed colors using
228 """generator for getting n of evenly distributed colors using
226 hsv color and golden ratio. It always return same order of colors
229 hsv color and golden ratio. It always return same order of colors
227
230
228 :returns: RGB tuple
231 :returns: RGB tuple
229 """
232 """
230
233
231 def hsv_to_rgb(h, s, v):
234 def hsv_to_rgb(h, s, v):
232 if s == 0.0: return v, v, v
235 if s == 0.0:
233 i = int(h * 6.0) # XXX assume int() truncates!
236 return v, v, v
237 i = int(h * 6.0) # XXX assume int() truncates!
234 f = (h * 6.0) - i
238 f = (h * 6.0) - i
235 p = v * (1.0 - s)
239 p = v * (1.0 - s)
236 q = v * (1.0 - s * f)
240 q = v * (1.0 - s * f)
237 t = v * (1.0 - s * (1.0 - f))
241 t = v * (1.0 - s * (1.0 - f))
238 i = i % 6
242 i = i % 6
239 if i == 0: return v, t, p
243 if i == 0:
240 if i == 1: return q, v, p
244 return v, t, p
241 if i == 2: return p, v, t
245 if i == 1:
242 if i == 3: return p, q, v
246 return q, v, p
243 if i == 4: return t, p, v
247 if i == 2:
244 if i == 5: return v, p, q
248 return p, v, t
249 if i == 3:
250 return p, q, v
251 if i == 4:
252 return t, p, v
253 if i == 5:
254 return v, p, q
245
255
246 golden_ratio = 0.618033988749895
256 golden_ratio = 0.618033988749895
247 h = 0.22717784590367374
257 h = 0.22717784590367374
248
258
249 for _ in xrange(n):
259 for _ in xrange(n):
250 h += golden_ratio
260 h += golden_ratio
251 h %= 1
261 h %= 1
252 HSV_tuple = [h, 0.95, 0.95]
262 HSV_tuple = [h, 0.95, 0.95]
253 RGB_tuple = hsv_to_rgb(*HSV_tuple)
263 RGB_tuple = hsv_to_rgb(*HSV_tuple)
254 yield map(lambda x:str(int(x * 256)), RGB_tuple)
264 yield map(lambda x: str(int(x * 256)), RGB_tuple)
255
265
256 cgenerator = gen_color()
266 cgenerator = gen_color()
257
267
258 def get_color_string(cs):
268 def get_color_string(cs):
259 if color_dict.has_key(cs):
269 if cs in color_dict:
260 col = color_dict[cs]
270 col = color_dict[cs]
261 else:
271 else:
262 col = color_dict[cs] = cgenerator.next()
272 col = color_dict[cs] = cgenerator.next()
263 return "color: rgb(%s)! important;" % (', '.join(col))
273 return "color: rgb(%s)! important;" % (', '.join(col))
264
274
265 def url_func(repo_name):
275 def url_func(repo_name):
266
276
267 def _url_func(changeset):
277 def _url_func(changeset):
268 author = changeset.author
278 author = changeset.author
269 date = changeset.date
279 date = changeset.date
270 message = tooltip(changeset.message)
280 message = tooltip(changeset.message)
271
281
272 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
282 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
273 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
283 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
274 "</b> %s<br/></div>")
284 "</b> %s<br/></div>")
275
285
276 tooltip_html = tooltip_html % (author, date, message)
286 tooltip_html = tooltip_html % (author, date, message)
277 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
287 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
278 short_id(changeset.raw_id))
288 short_id(changeset.raw_id))
279 uri = link_to(
289 uri = link_to(
280 lnk_format,
290 lnk_format,
281 url('changeset_home', repo_name=repo_name,
291 url('changeset_home', repo_name=repo_name,
282 revision=changeset.raw_id),
292 revision=changeset.raw_id),
283 style=get_color_string(changeset.raw_id),
293 style=get_color_string(changeset.raw_id),
284 class_='tooltip',
294 class_='tooltip',
285 title=tooltip_html
295 title=tooltip_html
286 )
296 )
287
297
288 uri += '\n'
298 uri += '\n'
289 return uri
299 return uri
290 return _url_func
300 return _url_func
291
301
292 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
302 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
293
303
304
294 def is_following_repo(repo_name, user_id):
305 def is_following_repo(repo_name, user_id):
295 from rhodecode.model.scm import ScmModel
306 from rhodecode.model.scm import ScmModel
296 return ScmModel().is_following_repo(repo_name, user_id)
307 return ScmModel().is_following_repo(repo_name, user_id)
297
308
298 flash = _Flash()
309 flash = _Flash()
299
310
300 #==============================================================================
311 #==============================================================================
301 # SCM FILTERS available via h.
312 # SCM FILTERS available via h.
302 #==============================================================================
313 #==============================================================================
303 from vcs.utils import author_name, author_email
314 from vcs.utils import author_name, author_email
304 from rhodecode.lib import credentials_filter, age as _age
315 from rhodecode.lib import credentials_filter, age as _age
305 from rhodecode.model.db import User
316 from rhodecode.model.db import User
306
317
307 age = lambda x:_age(x)
318 age = lambda x: _age(x)
308 capitalize = lambda x: x.capitalize()
319 capitalize = lambda x: x.capitalize()
309 email = author_email
320 email = author_email
310 short_id = lambda x: x[:12]
321 short_id = lambda x: x[:12]
311 hide_credentials = lambda x: ''.join(credentials_filter(x))
322 hide_credentials = lambda x: ''.join(credentials_filter(x))
312
323
313
324
314 def email_or_none(author):
325 def email_or_none(author):
315 _email = email(author)
326 _email = email(author)
316 if _email != '':
327 if _email != '':
317 return _email
328 return _email
318
329
319 # See if it contains a username we can get an email from
330 # See if it contains a username we can get an email from
320 user = User.get_by_username(author_name(author), case_insensitive=True,
331 user = User.get_by_username(author_name(author), case_insensitive=True,
321 cache=True)
332 cache=True)
322 if user is not None:
333 if user is not None:
323 return user.email
334 return user.email
324
335
325 # No valid email, not a valid user in the system, none!
336 # No valid email, not a valid user in the system, none!
326 return None
337 return None
327
338
339
328 def person(author):
340 def person(author):
329 # attr to return from fetched user
341 # attr to return from fetched user
330 person_getter = lambda usr: usr.username
342 person_getter = lambda usr: usr.username
331
343
332 # Valid email in the attribute passed, see if they're in the system
344 # Valid email in the attribute passed, see if they're in the system
333 _email = email(author)
345 _email = email(author)
334 if _email != '':
346 if _email != '':
335 user = User.get_by_email(_email, case_insensitive=True, cache=True)
347 user = User.get_by_email(_email, case_insensitive=True, cache=True)
336 if user is not None:
348 if user is not None:
337 return person_getter(user)
349 return person_getter(user)
338 return _email
350 return _email
339
351
340 # Maybe it's a username?
352 # Maybe it's a username?
341 _author = author_name(author)
353 _author = author_name(author)
342 user = User.get_by_username(_author, case_insensitive=True,
354 user = User.get_by_username(_author, case_insensitive=True,
343 cache=True)
355 cache=True)
344 if user is not None:
356 if user is not None:
345 return person_getter(user)
357 return person_getter(user)
346
358
347 # Still nothing? Just pass back the author name then
359 # Still nothing? Just pass back the author name then
348 return _author
360 return _author
349
361
350 def bool2icon(value):
362 def bool2icon(value):
351 """Returns True/False values represented as small html image of true/false
363 """Returns True/False values represented as small html image of true/false
352 icons
364 icons
353
365
354 :param value: bool value
366 :param value: bool value
355 """
367 """
356
368
357 if value is True:
369 if value is True:
358 return HTML.tag('img', src=url("/images/icons/accept.png"),
370 return HTML.tag('img', src=url("/images/icons/accept.png"),
359 alt=_('True'))
371 alt=_('True'))
360
372
361 if value is False:
373 if value is False:
362 return HTML.tag('img', src=url("/images/icons/cancel.png"),
374 return HTML.tag('img', src=url("/images/icons/cancel.png"),
363 alt=_('False'))
375 alt=_('False'))
364
376
365 return value
377 return value
366
378
367
379
368 def action_parser(user_log, feed=False):
380 def action_parser(user_log, feed=False):
369 """This helper will action_map the specified string action into translated
381 """This helper will action_map the specified string action into translated
370 fancy names with icons and links
382 fancy names with icons and links
371
383
372 :param user_log: user log instance
384 :param user_log: user log instance
373 :param feed: use output for feeds (no html and fancy icons)
385 :param feed: use output for feeds (no html and fancy icons)
374 """
386 """
375
387
376 action = user_log.action
388 action = user_log.action
377 action_params = ' '
389 action_params = ' '
378
390
379 x = action.split(':')
391 x = action.split(':')
380
392
381 if len(x) > 1:
393 if len(x) > 1:
382 action, action_params = x
394 action, action_params = x
383
395
384 def get_cs_links():
396 def get_cs_links():
385 revs_limit = 3 #display this amount always
397 revs_limit = 3 #display this amount always
386 revs_top_limit = 50 #show upto this amount of changesets hidden
398 revs_top_limit = 50 #show upto this amount of changesets hidden
387 revs = action_params.split(',')
399 revs = action_params.split(',')
388 repo_name = user_log.repository.repo_name
400 repo_name = user_log.repository.repo_name
389
401
390 from rhodecode.model.scm import ScmModel
402 from rhodecode.model.scm import ScmModel
391 repo = user_log.repository.scm_instance
403 repo = user_log.repository.scm_instance
392
404
393 message = lambda rev: get_changeset_safe(repo, rev).message
405 message = lambda rev: get_changeset_safe(repo, rev).message
394 cs_links = []
406 cs_links = []
395 cs_links.append(" " + ', '.join ([link_to(rev,
407 cs_links.append(" " + ', '.join ([link_to(rev,
396 url('changeset_home',
408 url('changeset_home',
397 repo_name=repo_name,
409 repo_name=repo_name,
398 revision=rev), title=tooltip(message(rev)),
410 revision=rev), title=tooltip(message(rev)),
399 class_='tooltip') for rev in revs[:revs_limit] ]))
411 class_='tooltip') for rev in revs[:revs_limit] ]))
400
412
401 compare_view = (' <div class="compare_view tooltip" title="%s">'
413 compare_view = (' <div class="compare_view tooltip" title="%s">'
402 '<a href="%s">%s</a> '
414 '<a href="%s">%s</a> '
403 '</div>' % (_('Show all combined changesets %s->%s' \
415 '</div>' % (_('Show all combined changesets %s->%s' \
404 % (revs[0], revs[-1])),
416 % (revs[0], revs[-1])),
405 url('changeset_home', repo_name=repo_name,
417 url('changeset_home', repo_name=repo_name,
406 revision='%s...%s' % (revs[0], revs[-1])
418 revision='%s...%s' % (revs[0], revs[-1])
407 ),
419 ),
408 _('compare view'))
420 _('compare view'))
409 )
421 )
410
422
411 if len(revs) > revs_limit:
423 if len(revs) > revs_limit:
412 uniq_id = revs[0]
424 uniq_id = revs[0]
413 html_tmpl = ('<span> %s '
425 html_tmpl = ('<span> %s '
414 '<a class="show_more" id="_%s" href="#more">%s</a> '
426 '<a class="show_more" id="_%s" href="#more">%s</a> '
415 '%s</span>')
427 '%s</span>')
416 if not feed:
428 if not feed:
417 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
429 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
418 % (len(revs) - revs_limit),
430 % (len(revs) - revs_limit),
419 _('revisions')))
431 _('revisions')))
420
432
421 if not feed:
433 if not feed:
422 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
434 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
423 else:
435 else:
424 html_tmpl = '<span id="%s"> %s </span>'
436 html_tmpl = '<span id="%s"> %s </span>'
425
437
426 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
438 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
427 url('changeset_home',
439 url('changeset_home',
428 repo_name=repo_name, revision=rev),
440 repo_name=repo_name, revision=rev),
429 title=message(rev), class_='tooltip')
441 title=message(rev), class_='tooltip')
430 for rev in revs[revs_limit:revs_top_limit]])))
442 for rev in revs[revs_limit:revs_top_limit]])))
431 if len(revs) > 1:
443 if len(revs) > 1:
432 cs_links.append(compare_view)
444 cs_links.append(compare_view)
433 return ''.join(cs_links)
445 return ''.join(cs_links)
434
446
435 def get_fork_name():
447 def get_fork_name():
436 repo_name = action_params
448 repo_name = action_params
437 return _('fork name ') + str(link_to(action_params, url('summary_home',
449 return _('fork name ') + str(link_to(action_params, url('summary_home',
438 repo_name=repo_name,)))
450 repo_name=repo_name,)))
439
451
440 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
452 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
441 'user_created_repo':(_('[created] repository'), None),
453 'user_created_repo':(_('[created] repository'), None),
442 'user_created_fork':(_('[created] repository as fork'), None),
454 'user_created_fork':(_('[created] repository as fork'), None),
443 'user_forked_repo':(_('[forked] repository'), get_fork_name),
455 'user_forked_repo':(_('[forked] repository'), get_fork_name),
444 'user_updated_repo':(_('[updated] repository'), None),
456 'user_updated_repo':(_('[updated] repository'), None),
445 'admin_deleted_repo':(_('[delete] repository'), None),
457 'admin_deleted_repo':(_('[delete] repository'), None),
446 'admin_created_repo':(_('[created] repository'), None),
458 'admin_created_repo':(_('[created] repository'), None),
447 'admin_forked_repo':(_('[forked] repository'), None),
459 'admin_forked_repo':(_('[forked] repository'), None),
448 'admin_updated_repo':(_('[updated] repository'), None),
460 'admin_updated_repo':(_('[updated] repository'), None),
449 'push':(_('[pushed] into'), get_cs_links),
461 'push':(_('[pushed] into'), get_cs_links),
450 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
462 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
451 'push_remote':(_('[pulled from remote] into'), get_cs_links),
463 'push_remote':(_('[pulled from remote] into'), get_cs_links),
452 'pull':(_('[pulled] from'), None),
464 'pull':(_('[pulled] from'), None),
453 'started_following_repo':(_('[started following] repository'), None),
465 'started_following_repo':(_('[started following] repository'), None),
454 'stopped_following_repo':(_('[stopped following] repository'), None),
466 'stopped_following_repo':(_('[stopped following] repository'), None),
455 }
467 }
456
468
457 action_str = action_map.get(action, action)
469 action_str = action_map.get(action, action)
458 if feed:
470 if feed:
459 action = action_str[0].replace('[', '').replace(']', '')
471 action = action_str[0].replace('[', '').replace(']', '')
460 else:
472 else:
461 action = action_str[0].replace('[', '<span class="journal_highlight">')\
473 action = action_str[0].replace('[', '<span class="journal_highlight">')\
462 .replace(']', '</span>')
474 .replace(']', '</span>')
463
475
464 action_params_func = lambda :""
476 action_params_func = lambda :""
465
477
466 if callable(action_str[1]):
478 if callable(action_str[1]):
467 action_params_func = action_str[1]
479 action_params_func = action_str[1]
468
480
469 return [literal(action), action_params_func]
481 return [literal(action), action_params_func]
470
482
471 def action_parser_icon(user_log):
483 def action_parser_icon(user_log):
472 action = user_log.action
484 action = user_log.action
473 action_params = None
485 action_params = None
474 x = action.split(':')
486 x = action.split(':')
475
487
476 if len(x) > 1:
488 if len(x) > 1:
477 action, action_params = x
489 action, action_params = x
478
490
479 tmpl = """<img src="%s%s" alt="%s"/>"""
491 tmpl = """<img src="%s%s" alt="%s"/>"""
480 map = {'user_deleted_repo':'database_delete.png',
492 map = {'user_deleted_repo':'database_delete.png',
481 'user_created_repo':'database_add.png',
493 'user_created_repo':'database_add.png',
482 'user_created_fork':'arrow_divide.png',
494 'user_created_fork':'arrow_divide.png',
483 'user_forked_repo':'arrow_divide.png',
495 'user_forked_repo':'arrow_divide.png',
484 'user_updated_repo':'database_edit.png',
496 'user_updated_repo':'database_edit.png',
485 'admin_deleted_repo':'database_delete.png',
497 'admin_deleted_repo':'database_delete.png',
486 'admin_created_repo':'database_add.png',
498 'admin_created_repo':'database_add.png',
487 'admin_forked_repo':'arrow_divide.png',
499 'admin_forked_repo':'arrow_divide.png',
488 'admin_updated_repo':'database_edit.png',
500 'admin_updated_repo':'database_edit.png',
489 'push':'script_add.png',
501 'push':'script_add.png',
490 'push_local':'script_edit.png',
502 'push_local':'script_edit.png',
491 'push_remote':'connect.png',
503 'push_remote':'connect.png',
492 'pull':'down_16.png',
504 'pull':'down_16.png',
493 'started_following_repo':'heart_add.png',
505 'started_following_repo':'heart_add.png',
494 'stopped_following_repo':'heart_delete.png',
506 'stopped_following_repo':'heart_delete.png',
495 }
507 }
496 return literal(tmpl % ((url('/images/icons/')),
508 return literal(tmpl % ((url('/images/icons/')),
497 map.get(action, action), action))
509 map.get(action, action), action))
498
510
499
511
500 #==============================================================================
512 #==============================================================================
501 # PERMS
513 # PERMS
502 #==============================================================================
514 #==============================================================================
503 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
515 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
504 HasRepoPermissionAny, HasRepoPermissionAll
516 HasRepoPermissionAny, HasRepoPermissionAll
505
517
506 #==============================================================================
518 #==============================================================================
507 # GRAVATAR URL
519 # GRAVATAR URL
508 #==============================================================================
520 #==============================================================================
509
521
510 def gravatar_url(email_address, size=30):
522 def gravatar_url(email_address, size=30):
511 if (not str2bool(config['app_conf'].get('use_gravatar')) or
523 if (not str2bool(config['app_conf'].get('use_gravatar')) or
512 not email_address or email_address == 'anonymous@rhodecode.org'):
524 not email_address or email_address == 'anonymous@rhodecode.org'):
513 return url("/images/user%s.png" % size)
525 return url("/images/user%s.png" % size)
514
526
515 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
527 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
516 default = 'identicon'
528 default = 'identicon'
517 baseurl_nossl = "http://www.gravatar.com/avatar/"
529 baseurl_nossl = "http://www.gravatar.com/avatar/"
518 baseurl_ssl = "https://secure.gravatar.com/avatar/"
530 baseurl_ssl = "https://secure.gravatar.com/avatar/"
519 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
531 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
520
532
521 if isinstance(email_address, unicode):
533 if isinstance(email_address, unicode):
522 #hashlib crashes on unicode items
534 #hashlib crashes on unicode items
523 email_address = safe_str(email_address)
535 email_address = safe_str(email_address)
524 # construct the url
536 # construct the url
525 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
537 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
526 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
538 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
527
539
528 return gravatar_url
540 return gravatar_url
529
541
530
542
531 #==============================================================================
543 #==============================================================================
532 # REPO PAGER, PAGER FOR REPOSITORY
544 # REPO PAGER, PAGER FOR REPOSITORY
533 #==============================================================================
545 #==============================================================================
534 class RepoPage(Page):
546 class RepoPage(Page):
535
547
536 def __init__(self, collection, page=1, items_per_page=20,
548 def __init__(self, collection, page=1, items_per_page=20,
537 item_count=None, url=None, **kwargs):
549 item_count=None, url=None, **kwargs):
538
550
539 """Create a "RepoPage" instance. special pager for paging
551 """Create a "RepoPage" instance. special pager for paging
540 repository
552 repository
541 """
553 """
542 self._url_generator = url
554 self._url_generator = url
543
555
544 # Safe the kwargs class-wide so they can be used in the pager() method
556 # Safe the kwargs class-wide so they can be used in the pager() method
545 self.kwargs = kwargs
557 self.kwargs = kwargs
546
558
547 # Save a reference to the collection
559 # Save a reference to the collection
548 self.original_collection = collection
560 self.original_collection = collection
549
561
550 self.collection = collection
562 self.collection = collection
551
563
552 # The self.page is the number of the current page.
564 # The self.page is the number of the current page.
553 # The first page has the number 1!
565 # The first page has the number 1!
554 try:
566 try:
555 self.page = int(page) # make it int() if we get it as a string
567 self.page = int(page) # make it int() if we get it as a string
556 except (ValueError, TypeError):
568 except (ValueError, TypeError):
557 self.page = 1
569 self.page = 1
558
570
559 self.items_per_page = items_per_page
571 self.items_per_page = items_per_page
560
572
561 # Unless the user tells us how many items the collections has
573 # Unless the user tells us how many items the collections has
562 # we calculate that ourselves.
574 # we calculate that ourselves.
563 if item_count is not None:
575 if item_count is not None:
564 self.item_count = item_count
576 self.item_count = item_count
565 else:
577 else:
566 self.item_count = len(self.collection)
578 self.item_count = len(self.collection)
567
579
568 # Compute the number of the first and last available page
580 # Compute the number of the first and last available page
569 if self.item_count > 0:
581 if self.item_count > 0:
570 self.first_page = 1
582 self.first_page = 1
571 self.page_count = int(math.ceil(float(self.item_count) /
583 self.page_count = int(math.ceil(float(self.item_count) /
572 self.items_per_page))
584 self.items_per_page))
573 self.last_page = self.first_page + self.page_count - 1
585 self.last_page = self.first_page + self.page_count - 1
574
586
575 # Make sure that the requested page number is the range of
587 # Make sure that the requested page number is the range of
576 # valid pages
588 # valid pages
577 if self.page > self.last_page:
589 if self.page > self.last_page:
578 self.page = self.last_page
590 self.page = self.last_page
579 elif self.page < self.first_page:
591 elif self.page < self.first_page:
580 self.page = self.first_page
592 self.page = self.first_page
581
593
582 # Note: the number of items on this page can be less than
594 # Note: the number of items on this page can be less than
583 # items_per_page if the last page is not full
595 # items_per_page if the last page is not full
584 self.first_item = max(0, (self.item_count) - (self.page *
596 self.first_item = max(0, (self.item_count) - (self.page *
585 items_per_page))
597 items_per_page))
586 self.last_item = ((self.item_count - 1) - items_per_page *
598 self.last_item = ((self.item_count - 1) - items_per_page *
587 (self.page - 1))
599 (self.page - 1))
588
600
589 self.items = list(self.collection[self.first_item:self.last_item + 1])
601 self.items = list(self.collection[self.first_item:self.last_item + 1])
590
602
591
603
592 # Links to previous and next page
604 # Links to previous and next page
593 if self.page > self.first_page:
605 if self.page > self.first_page:
594 self.previous_page = self.page - 1
606 self.previous_page = self.page - 1
595 else:
607 else:
596 self.previous_page = None
608 self.previous_page = None
597
609
598 if self.page < self.last_page:
610 if self.page < self.last_page:
599 self.next_page = self.page + 1
611 self.next_page = self.page + 1
600 else:
612 else:
601 self.next_page = None
613 self.next_page = None
602
614
603 # No items available
615 # No items available
604 else:
616 else:
605 self.first_page = None
617 self.first_page = None
606 self.page_count = 0
618 self.page_count = 0
607 self.last_page = None
619 self.last_page = None
608 self.first_item = None
620 self.first_item = None
609 self.last_item = None
621 self.last_item = None
610 self.previous_page = None
622 self.previous_page = None
611 self.next_page = None
623 self.next_page = None
612 self.items = []
624 self.items = []
613
625
614 # This is a subclass of the 'list' type. Initialise the list now.
626 # This is a subclass of the 'list' type. Initialise the list now.
615 list.__init__(self, reversed(self.items))
627 list.__init__(self, reversed(self.items))
616
628
617
629
618 def changed_tooltip(nodes):
630 def changed_tooltip(nodes):
619 """
631 """
620 Generates a html string for changed nodes in changeset page.
632 Generates a html string for changed nodes in changeset page.
621 It limits the output to 30 entries
633 It limits the output to 30 entries
622
634
623 :param nodes: LazyNodesGenerator
635 :param nodes: LazyNodesGenerator
624 """
636 """
625 if nodes:
637 if nodes:
626 pref = ': <br/> '
638 pref = ': <br/> '
627 suf = ''
639 suf = ''
628 if len(nodes) > 30:
640 if len(nodes) > 30:
629 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
641 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
630 return literal(pref + '<br/> '.join([safe_unicode(x.path)
642 return literal(pref + '<br/> '.join([safe_unicode(x.path)
631 for x in nodes[:30]]) + suf)
643 for x in nodes[:30]]) + suf)
632 else:
644 else:
633 return ': ' + _('No Files')
645 return ': ' + _('No Files')
634
646
635
647
636
648
637 def repo_link(groups_and_repos):
649 def repo_link(groups_and_repos):
638 """
650 """
639 Makes a breadcrumbs link to repo within a group
651 Makes a breadcrumbs link to repo within a group
640 joins &raquo; on each group to create a fancy link
652 joins &raquo; on each group to create a fancy link
641
653
642 ex::
654 ex::
643 group >> subgroup >> repo
655 group >> subgroup >> repo
644
656
645 :param groups_and_repos:
657 :param groups_and_repos:
646 """
658 """
647 groups, repo_name = groups_and_repos
659 groups, repo_name = groups_and_repos
648
660
649 if not groups:
661 if not groups:
650 return repo_name
662 return repo_name
651 else:
663 else:
652 def make_link(group):
664 def make_link(group):
653 return link_to(group.name, url('repos_group_home',
665 return link_to(group.name, url('repos_group_home',
654 group_name=group.group_name))
666 group_name=group.group_name))
655 return literal(' &raquo; '.join(map(make_link, groups)) + \
667 return literal(' &raquo; '.join(map(make_link, groups)) + \
656 " &raquo; " + repo_name)
668 " &raquo; " + repo_name)
657
669
658 def fancy_file_stats(stats):
670 def fancy_file_stats(stats):
659 """
671 """
660 Displays a fancy two colored bar for number of added/deleted
672 Displays a fancy two colored bar for number of added/deleted
661 lines of code on file
673 lines of code on file
662
674
663 :param stats: two element list of added/deleted lines of code
675 :param stats: two element list of added/deleted lines of code
664 """
676 """
665
677
666 a, d, t = stats[0], stats[1], stats[0] + stats[1]
678 a, d, t = stats[0], stats[1], stats[0] + stats[1]
667 width = 100
679 width = 100
668 unit = float(width) / (t or 1)
680 unit = float(width) / (t or 1)
669
681
670 # needs > 9% of width to be visible or 0 to be hidden
682 # needs > 9% of width to be visible or 0 to be hidden
671 a_p = max(9, unit * a) if a > 0 else 0
683 a_p = max(9, unit * a) if a > 0 else 0
672 d_p = max(9, unit * d) if d > 0 else 0
684 d_p = max(9, unit * d) if d > 0 else 0
673 p_sum = a_p + d_p
685 p_sum = a_p + d_p
674
686
675 if p_sum > width:
687 if p_sum > width:
676 #adjust the percentage to be == 100% since we adjusted to 9
688 #adjust the percentage to be == 100% since we adjusted to 9
677 if a_p > d_p:
689 if a_p > d_p:
678 a_p = a_p - (p_sum - width)
690 a_p = a_p - (p_sum - width)
679 else:
691 else:
680 d_p = d_p - (p_sum - width)
692 d_p = d_p - (p_sum - width)
681
693
682 a_v = a if a > 0 else ''
694 a_v = a if a > 0 else ''
683 d_v = d if d > 0 else ''
695 d_v = d if d > 0 else ''
684
696
685
697
686 def cgen(l_type):
698 def cgen(l_type):
687 mapping = {'tr':'top-right-rounded-corner',
699 mapping = {'tr':'top-right-rounded-corner',
688 'tl':'top-left-rounded-corner',
700 'tl':'top-left-rounded-corner',
689 'br':'bottom-right-rounded-corner',
701 'br':'bottom-right-rounded-corner',
690 'bl':'bottom-left-rounded-corner'}
702 'bl':'bottom-left-rounded-corner'}
691 map_getter = lambda x:mapping[x]
703 map_getter = lambda x:mapping[x]
692
704
693 if l_type == 'a' and d_v:
705 if l_type == 'a' and d_v:
694 #case when added and deleted are present
706 #case when added and deleted are present
695 return ' '.join(map(map_getter, ['tl', 'bl']))
707 return ' '.join(map(map_getter, ['tl', 'bl']))
696
708
697 if l_type == 'a' and not d_v:
709 if l_type == 'a' and not d_v:
698 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
710 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
699
711
700 if l_type == 'd' and a_v:
712 if l_type == 'd' and a_v:
701 return ' '.join(map(map_getter, ['tr', 'br']))
713 return ' '.join(map(map_getter, ['tr', 'br']))
702
714
703 if l_type == 'd' and not a_v:
715 if l_type == 'd' and not a_v:
704 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
716 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
705
717
706
718
707
719
708 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
720 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
709 a_p, a_v)
721 a_p, a_v)
710 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
722 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
711 d_p, d_v)
723 d_p, d_v)
712 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
724 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
713
725
714
726
715 def urlify_text(text):
727 def urlify_text(text):
716 import re
728 import re
717
729
718 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
730 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
719 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
731 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
720
732
721 def url_func(match_obj):
733 def url_func(match_obj):
722 url_full = match_obj.groups()[0]
734 url_full = match_obj.groups()[0]
723 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
735 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
724
736
725 return literal(url_pat.sub(url_func, text))
737 return literal(url_pat.sub(url_func, text))
726
738
727
739
728 def rst(source):
740 def rst(source):
729 return literal('<div class="rst-block">%s</div>' %
741 return literal('<div class="rst-block">%s</div>' %
730 MarkupRenderer.rst(source))
742 MarkupRenderer.rst(source))
731
743
732 def rst_w_mentions(source):
744 def rst_w_mentions(source):
733 """
745 """
734 Wrapped rst renderer with @mention highlighting
746 Wrapped rst renderer with @mention highlighting
735
747
736 :param source:
748 :param source:
737 """
749 """
738 return literal('<div class="rst-block">%s</div>' %
750 return literal('<div class="rst-block">%s</div>' %
739 MarkupRenderer.rst_with_mentions(source))
751 MarkupRenderer.rst_with_mentions(source))
General Comments 0
You need to be logged in to leave comments. Login now