diff --git a/deps/codeformats/xcode.css b/deps/codeformats/xcode.css index c0b847b..96c5bf6 100644 --- a/deps/codeformats/xcode.css +++ b/deps/codeformats/xcode.css @@ -89,4 +89,4 @@ span.right_diff_del { } span.clearbg { background-color: transparent; -} \ No newline at end of file +} diff --git a/diff2HtmlCompare.py b/diff2HtmlCompare.py index 644ce93..350cc9d 100644 --- a/diff2HtmlCompare.py +++ b/diff2HtmlCompare.py @@ -22,408 +22,491 @@ import io import os +import string +import html +import textwrap import sys import difflib import argparse -import pygments import webbrowser -from pygments.lexers import guess_lexer_for_filename -from pygments.lexer import RegexLexer +from collections import defaultdict +from pathlib import Path +import re +from itertools import groupby, chain + +import pygments from pygments.formatters import HtmlFormatter -from pygments.token import * +from pygments.lexer import RegexLexer +from pygments import token -# Monokai is not quite right yet -PYGMENTS_STYLES = ["vs", "xcode"] -HTML_TEMPLATE = """ +HTML_TEMPLATE = r''' - +
- -' + diff_class = '' + source_pos = 0 + for _lineno, ttype, value in tokens: + css_class = get_token_class(ttype) - 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 + while diff_markers: + next_marker_pos, next_marker_type = diff_markers[0] + if source_pos <= next_marker_pos < source_pos + len(value): + split_pos = next_marker_pos - source_pos + left, value = value[:split_pos], value[split_pos:] + line += f'{html.escape(left)}' + source_pos += len(left) + diff_class = ' word_change' if next_marker_type.startswith('\0') else '' + diff_markers = diff_markers[1:] 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' + break + line += f'{html.escape(value)}' + source_pos += len(value) - def _wrap_tablelinenos(self, inner): - dummyoutfile = io.StringIO() - lncount = 0 - for t, line in inner: - if t: - lncount += 1 + if css_class is not None: + line += '' - # compatibility Python v2/v3 - if sys.version_info > (3,0): - dummyoutfile.write(line) - else: - dummyoutfile.write(unicode(line)) + line += '' + self.lines.append(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 + for _ours_empty, (lineno_theirs, _diff_theirs), change in diff: + self.lines.append(f'') + assert change and lineno_theirs - lines = [] - for i in self.getDiffLineNos(): - lines.append('%s' % (i,)) +def html_diff_content(old, new): + diff = list(difflib._mdiff(old.splitlines(), new.splitlines())) - ls = ''.join(lines) + fmt_l = RecordFormatter('left', diff) + pygments.highlight(old, SexprLexer(), fmt_l) - # in case you wonder about the seemingly redundant
'
- ' ' + - ls + ' | ')
- else:
- yield 0, ('
+ ''')
- def __init__(self, fromfile, tofile, fromtxt=None, totxt=None, name=None):
- self.filename = name
- self.fromfile = fromfile
- if fromtxt == None:
- try:
- with io.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 io.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 = io.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\
+ description = """Given two source files or directories 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).')
-
+ parser.add_argument('-b', '--open', action='store_true', help='Open output file in a browser')
+ parser.add_argument('-s', '--syntax-css', help='Path to custom Pygments CSS file for code syntax highlighting')
+ parser.add_argument('-t', '--pagetitle', help='Override page title of output HTML file')
+ parser.add_argument('-o', '--output', default=sys.stdout, type=argparse.FileType('w'), help='Name of output file (default: stdout)')
+ parser.add_argument('--header', action='store_true', help='Only output HTML header with stylesheets and stuff, and no diff')
+ parser.add_argument('--content', action='store_true', help='Only output HTML content, without header')
+ parser.add_argument('old', help='source file or directory to compare ("before" file)')
+ parser.add_argument('new', help='source file or directory 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)
+ if args.open and args.output == sys.stdout:
+ print('Error: --open requires --output to be given.')
+ parser.print_usage()
+ sys.exit(2)
+
+ old, new = Path(args.old), Path(args.new)
+ if not old.exists():
+ print(f'Error: Path "{old}" does not exist.')
+ sys.exit(1)
+
+ if not new.exists():
+ print(f'Error: Path "{new}" does not exist.')
+ sys.exit(1)
+
+ if old.is_file() != new.is_file():
+ print(f'Error: You must give either two files, or two paths to compare, not a mix of both.')
+ sys.exit(1)
+
+ if old.is_file():
+ found_files = {str(new): (old, new)}
+ else:
+ found_files = defaultdict(lambda: [None, None])
+ for fn in old.glob('**/*'):
+ found_files[str(fn.relative_to(old))][0] = fn
+ for fn in new.glob('**/*'):
+ found_files[str(fn.relative_to(new))][1] = fn
+
+ pagetitle = args.pagetitle or f'diff: {old} / {new}'
+ if args.syntax_css:
+ syntax_css = Path(args.syntax_css).read_text()
+ else:
+ syntax_css = PYGMENTS_CSS
+
+ diff_blocks = []
+ for suffix, (old, new) in sorted(found_files.items()):
+ old = '' if old is None else old.read_text()
+ new = '' if new is None else new.read_text()
+
+ diff_blocks.append(html_diff_block(old, new, suffix))
+
+ print(string.Template(HTML_TEMPLATE).substitute(
+ title=pagetitle,
+ pygments_css=syntax_css,
+ body='\n'.join(diff_blocks)), file=args.output)
+
+ if args.open:
+ webbrowser.open('file://' + str(Path(args.output.name).absolute()))
- outputpath = "index.html"
- main(args.file1, args.file2, outputpath, args)
- if args.show:
- show(outputpath)
{filename}
+
+ {code}
+
+ |