# MIT License # # Copyright (c) 2016 Alex Goodman # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies # of the Software, and to permit persons to whom the Software is furnished to do # so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import io import os import sys import difflib import argparse import pygments import webbrowser from pygments.lexers import guess_lexer_for_filename from pygments.lexer import RegexLexer from pygments.formatters import HtmlFormatter from pygments.token import * # Monokai is not quite right yet PYGMENTS_STYLES = ["vs", "xcode"] HTML_TEMPLATE = """ %(html_title)s
%(page_title)s
❬ Original
01234567890123456789012345678901234567890123456789012345678901234567890123456789
%(original_code)s
❭ Modified
01234567890123456789012345678901234567890123456789012345678901234567890123456789
%(modified_code)s
""" class DefaultLexer(RegexLexer): """ Simply lex each line as a token. """ name = 'Default' aliases = ['default'] filenames = ['*'] tokens = { 'root': [ (r'.*\n', Text), ] } class DiffHtmlFormatter(HtmlFormatter): """ Formats a single source file with pygments and adds diff highlights based on the diff details given. """ isLeft = False diffs = None def __init__(self, isLeft, diffs, *args, **kwargs): self.isLeft = isLeft self.diffs = diffs super(DiffHtmlFormatter, self).__init__(*args, **kwargs) def wrap(self, source, outfile): return self._wrap_code(source) def getDiffLineNos(self): retlinenos = [] for idx, ((left_no, left_line), (right_no, right_line), change) in enumerate(self.diffs): no = None if self.isLeft: if change: if isinstance(left_no, int) and isinstance(right_no, int): no = '' + \ str(left_no) + "" elif isinstance(left_no, int) and not isinstance(right_no, int): no = '' + \ str(left_no) + "" elif not isinstance(left_no, int) and isinstance(right_no, int): no = ' ' else: no = '' + str(left_no) + "" else: if change: if isinstance(left_no, int) and isinstance(right_no, int): no = '' + \ str(right_no) + "" elif isinstance(left_no, int) and not isinstance(right_no, int): no = ' ' elif not isinstance(left_no, int) and isinstance(right_no, int): no = '' + \ str(right_no) + "" else: no = '' + str(right_no) + "" retlinenos.append(no) return retlinenos def _wrap_code(self, source): source = list(source) yield 0, '
'

        for idx, ((left_no, left_line), (right_no, right_line), change) in enumerate(self.diffs):
            # print idx, ((left_no, left_line),(right_no, right_line),change)
            try:
                if self.isLeft:
                    if change:
                        if isinstance(left_no, int) and isinstance(right_no, int) and left_no <= len(source):
                            i, t = source[left_no - 1]
                            t = '' + t + ""
                        elif isinstance(left_no, int) and not isinstance(right_no, int) and left_no <= len(source):
                            i, t = source[left_no - 1]
                            t = '' + t + ""
                        elif not isinstance(left_no, int) and isinstance(right_no, int):
                            i, t = 1, left_line
                            t = '' + t + ""
                        else:
                            raise
                    else:
                        if left_no <= len(source):
                            i, t = source[left_no - 1]
                        else:
                            i = 1
                            t = left_line
                else:
                    if change:
                        if isinstance(left_no, int) and isinstance(right_no, int) and right_no <= len(source):
                            i, t = source[right_no - 1]
                            t = '' + t + ""
                        elif isinstance(left_no, int) and not isinstance(right_no, int):
                            i, t = 1, right_line
                            t = '' + t + ""
                        elif not isinstance(left_no, int) and isinstance(right_no, int) and right_no <= len(source):
                            i, t = source[right_no - 1]
                            t = '' + t + ""
                        else:
                            raise
                    else:
                        if right_no <= len(source):
                            i, t = source[right_no - 1]
                        else:
                            i = 1
                            t = right_line
                yield i, t
            except:
                # print "WARNING! failed to enumerate diffs fully!"
                pass  # this is expected sometimes
        yield 0, '\n
' def _wrap_tablelinenos(self, inner): dummyoutfile = io.StringIO() lncount = 0 for t, line in inner: if t: lncount += 1 # compatibility Python v2/v3 if sys.version_info > (3,0): dummyoutfile.write(line) else: dummyoutfile.write(unicode(line)) fl = self.linenostart mw = len(str(lncount + fl - 1)) sp = self.linenospecial st = self.linenostep la = self.lineanchors aln = self.anchorlinenos nocls = self.noclasses lines = [] for i in self.getDiffLineNos(): lines.append('%s' % (i,)) ls = ''.join(lines) # 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... if nocls: yield 0, ('' % self.cssclass + '
' '
' +
                      ls + '
') else: yield 0, ('' % self.cssclass + '
' +
                      ls + '
') yield 0, dummyoutfile.getvalue() yield 0, '
' class CodeDiff(object): """ Manages a pair of source files and generates a single html diff page comparing the contents. """ pygmentsCssFile = "./deps/codeformats/%s.css" diffCssFile = "./deps/diff.css" diffJsFile = "./deps/diff.js" resetCssFile = "./deps/reset.css" jqueryJsFile = "./deps/jquery.min.js" def __init__(self, fromfile, tofile, fromtxt=None, totxt=None, name=None): self.filename = name self.fromfile = fromfile if fromtxt == None: try: with open(fromfile) as f: self.fromlines = f.readlines() except Exception as e: print("Problem reading file %s" % fromfile) print(e) sys.exit(1) else: self.fromlines = [n + "\n" for n in fromtxt.split("\n")] self.leftcode = "".join(self.fromlines) self.tofile = tofile if totxt == None: try: with open(tofile) as f: self.tolines = f.readlines() except Exception as e: print("Problem reading file %s" % tofile) print(e) sys.exit(1) else: self.tolines = [n + "\n" for n in totxt.split("\n")] self.rightcode = "".join(self.tolines) def getDiffDetails(self, fromdesc='', todesc='', context=False, numlines=5, tabSize=8): # change tabs to spaces before it gets more difficult after we insert # markkup def expand_tabs(line): # hide real spaces line = line.replace(' ', '\0') # expand tabs into spaces line = line.expandtabs(tabSize) # replace spaces from expanded tabs back into tab characters # (we'll replace them with markup after we do differencing) line = line.replace(' ', '\t') return line.replace('\0', ' ').rstrip('\n') self.fromlines = [expand_tabs(line) for line in self.fromlines] self.tolines = [expand_tabs(line) for line in self.tolines] # create diffs iterator which generates side by side from/to data if context: context_lines = numlines else: context_lines = None diffs = difflib._mdiff(self.fromlines, self.tolines, context_lines, linejunk=None, charjunk=difflib.IS_CHARACTER_JUNK) return list(diffs) def format(self, options): self.diffs = self.getDiffDetails(self.fromfile, self.tofile) if options.verbose: for diff in self.diffs: print("%-6s %-80s %-80s" % (diff[2], diff[0], diff[1])) fields = ((self.leftcode, True, self.fromfile), (self.rightcode, False, self.tofile)) codeContents = [] for (code, isLeft, filename) in fields: inst = DiffHtmlFormatter(isLeft, self.diffs, nobackground=False, linenos=True, style=options.syntax_css) try: self.lexer = guess_lexer_for_filename(self.filename, code) except pygments.util.ClassNotFound: if options.verbose: print("No Lexer Found! Using default...") self.lexer = DefaultLexer() formatted = pygments.highlight(code, self.lexer, inst) codeContents.append(formatted) answers = { "html_title": self.filename, "reset_css": self.resetCssFile, "pygments_css": self.pygmentsCssFile % options.syntax_css, "diff_css": self.diffCssFile, "page_title": self.filename, "original_code": codeContents[0], "modified_code": codeContents[1], "jquery_js": self.jqueryJsFile, "diff_js": self.diffJsFile, "page_width": "page-80-width" if options.print_width else "page-full-width" } self.htmlContents = HTML_TEMPLATE % answers def write(self, path): fh = open(path, 'w') fh.write(self.htmlContents) fh.close() def main(file1, file2, outputpath, options): codeDiff = CodeDiff(file1, file2, name=file2) codeDiff.format(options) codeDiff.write(outputpath) def show(outputpath): path = os.path.abspath(outputpath) webbrowser.open('file://' + path) if __name__ == "__main__": description = """Given two source files this application\ creates an html page which highlights the differences between the two. """ parser = argparse.ArgumentParser(description=description) parser.add_argument('-s', '--show', action='store_true', help='show html in a browser.') parser.add_argument('-p', '--print-width', action='store_true', help='Restrict code to 80 columns wide. (printer friendly in landscape)') parser.add_argument('-c', '--syntax-css', action='store', default="vs", help='Pygments CSS for code syntax highlighting. Can be one of: %s' % str(PYGMENTS_STYLES)) parser.add_argument('-v', '--verbose', action='store_true', help='show verbose output.') parser.add_argument('file1', help='source file to compare ("before" file).') parser.add_argument('file2', help='source file to compare ("after" file).') args = parser.parse_args() if args.syntax_css not in PYGMENTS_STYLES: raise ValueError("Syntax CSS (-c) must be one of %r." % PYGMENTS_STYLES) outputpath = "index.html" main(args.file1, args.file2, outputpath, args) if args.show: show(outputpath)