diff --git a/rhodecode/lib/vcs/utils/annotate.py b/rhodecode/lib/vcs/utils/annotate.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/annotate.py @@ -0,0 +1,177 @@ +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.nodes import FileNode +from pygments.formatters import HtmlFormatter +from pygments import highlight + +import StringIO + + +def annotate_highlight(filenode, annotate_from_changeset_func=None, + order=None, headers=None, **options): + """ + Returns html portion containing annotated table with 3 columns: line + numbers, changeset information and pygmentized line of code. + + :param filenode: FileNode object + :param annotate_from_changeset_func: function taking changeset and + returning single annotate cell; needs break line at the end + :param order: ordered sequence of ``ls`` (line numbers column), + ``annotate`` (annotate column), ``code`` (code column); Default is + ``['ls', 'annotate', 'code']`` + :param headers: dictionary with headers (keys are whats in ``order`` + parameter) + """ + options['linenos'] = True + formatter = AnnotateHtmlFormatter(filenode=filenode, order=order, + headers=headers, + annotate_from_changeset_func=annotate_from_changeset_func, **options) + lexer = filenode.lexer + highlighted = highlight(filenode.content, lexer, formatter) + return highlighted + + +class AnnotateHtmlFormatter(HtmlFormatter): + + def __init__(self, filenode, annotate_from_changeset_func=None, + order=None, **options): + """ + If ``annotate_from_changeset_func`` is passed it should be a function + which returns string from the given changeset. For example, we may pass + following function as ``annotate_from_changeset_func``:: + + def changeset_to_anchor(changeset): + return '%s\n' %\ + (changeset.id, changeset.id) + + :param annotate_from_changeset_func: see above + :param order: (default: ``['ls', 'annotate', 'code']``); order of + columns; + :param options: standard pygment's HtmlFormatter options, there is + extra option tough, ``headers``. For instance we can pass:: + + formatter = AnnotateHtmlFormatter(filenode, headers={ + 'ls': '#', + 'annotate': 'Annotate', + 'code': 'Code', + }) + + """ + super(AnnotateHtmlFormatter, self).__init__(**options) + self.annotate_from_changeset_func = annotate_from_changeset_func + self.order = order or ('ls', 'annotate', 'code') + headers = options.pop('headers', None) + if headers and not ('ls' in headers and 'annotate' in headers and + 'code' in headers): + raise ValueError("If headers option dict is specified it must " + "all 'ls', 'annotate' and 'code' keys") + self.headers = headers + if isinstance(filenode, FileNode): + self.filenode = filenode + else: + raise VCSError("This formatter expect FileNode parameter, not %r" + % type(filenode)) + + def annotate_from_changeset(self, changeset): + """ + Returns full html line for single changeset per annotated line. + """ + if self.annotate_from_changeset_func: + return self.annotate_from_changeset_func(changeset) + else: + return ''.join((changeset.id, '\n')) + + def _wrap_tablelinenos(self, inner): + dummyoutfile = StringIO.StringIO() + lncount = 0 + for t, line in inner: + if t: + lncount += 1 + dummyoutfile.write(line) + + fl = self.linenostart + mw = len(str(lncount + fl - 1)) + sp = self.linenospecial + st = self.linenostep + la = self.lineanchors + aln = self.anchorlinenos + if sp: + lines = [] + + for i in range(fl, fl + lncount): + if i % st == 0: + if i % sp == 0: + if aln: + lines.append('' + '%*d' % + (la, i, mw, i)) + else: + lines.append('' + '%*d' % (mw, i)) + else: + if aln: + lines.append('' + '%*d' % (la, i, mw, i)) + else: + lines.append('%*d' % (mw, i)) + else: + lines.append('') + ls = '\n'.join(lines) + else: + lines = [] + for i in range(fl, fl + lncount): + if i % st == 0: + if aln: + lines.append('%*d' \ + % (la, i, mw, i)) + else: + lines.append('%*d' % (mw, i)) + else: + lines.append('') + ls = '\n'.join(lines) + + annotate_changesets = [tup[1] for tup in self.filenode.annotate] + # If pygments cropped last lines break we need do that too + ln_cs = len(annotate_changesets) + ln_ = len(ls.splitlines()) + if ln_cs > ln_: + annotate_changesets = annotate_changesets[:ln_ - ln_cs] + annotate = ''.join((self.annotate_from_changeset(changeset) + for changeset in annotate_changesets)) + # in case you wonder about the seemingly redundant
here: + # since the content in the other cell also is wrapped in a div, + # some browsers in some configurations seem to mess up the formatting. + ''' + yield 0, ('' % self.cssclass + + '' + + '
' +
+                  ls + '
') + yield 0, dummyoutfile.getvalue() + yield 0, '
' + + ''' + headers_row = [] + if self.headers: + headers_row = [''] + for key in self.order: + td = ''.join(('', self.headers[key], '')) + headers_row.append(td) + headers_row.append('') + + body_row_start = [''] + for key in self.order: + if key == 'ls': + body_row_start.append( + '
' +
+                    ls + '
') + elif key == 'annotate': + body_row_start.append( + '
' +
+                    annotate + '
') + elif key == 'code': + body_row_start.append('') + yield 0, ('' % self.cssclass + + ''.join(headers_row) + + ''.join(body_row_start) + ) + yield 0, dummyoutfile.getvalue() + yield 0, '
'