Produce adaptive, css-only, single-file, pretty html output

This commit is contained in:
jaseg 2022-12-29 18:30:07 +01:00
parent 53503591ad
commit 3a6a186049
2 changed files with 435 additions and 352 deletions

View file

@ -89,4 +89,4 @@ span.right_diff_del {
} }
span.clearbg { span.clearbg {
background-color: transparent; background-color: transparent;
} }

View file

@ -22,408 +22,491 @@
import io import io
import os import os
import string
import html
import textwrap
import sys import sys
import difflib import difflib
import argparse import argparse
import pygments
import webbrowser import webbrowser
from pygments.lexers import guess_lexer_for_filename from collections import defaultdict
from pygments.lexer import RegexLexer from pathlib import Path
import re
from itertools import groupby, chain
import pygments
from pygments.formatters import HtmlFormatter 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'''
<!DOCTYPE html> <!DOCTYPE html>
<html class="no-js"> <html>
<head> <head>
<!--
html_title: browser tab title
reset_css: relative path to reset css file
pygments_css: relative path to pygments css file
diff_css: relative path to diff layout css file
page_title: title shown at the top of the page. This should be the filename of the files being diff'd
original_code: full html contents of original file
modified_code: full html contents of modified file
jquery_js: path to jquery.min.js
diff_js: path to diff.js
-->
<meta charset="utf-8"> <meta charset="utf-8">
<title> <title>$title</title>
%(html_title)s
</title>
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="%(reset_css)s" type="text/css"> <style>
<link rel="stylesheet" href="%(diff_css)s" type="text/css"> html, body {
<link class="syntaxdef" rel="stylesheet" href="%(pygments_css)s" type="text/css"> margin: 0;
padding: 0;
}
.file-container {
font-family: monospace;
font-size: 9pt;
border: solid 1px #e0e0e0;
margin: 15px;
}
.file-title {
background-color: #f8f8f8;
padding: 10px 20px;
font-size: 10pt;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
position: sticky;
top: 0;
z-index: 1;
}
.diff {
overflow-x: auto;
display: grid;
}
.line {
padding-left: calc(4em + 5px);
text-indent: -4em;
padding-top: 2px;
}
.line.left.change, .line.left.insert {
background-color: #fbe9eb;
}
.line.right.change, .line.right.insert {
background-color: #ecfdf0;
}
.lineno.left.change, .lineno.left.insert {
background-color: #f9d7dc;
color: #ae969a;
}
.lineno.right.change, .lineno.right.insert {
background-color: #ddfbe6;
color: #9bb0a1;
}
.right > .word_change {
background-color: #c7f0d2;
color: #004000;
}
.left > .word_change {
background-color: #fac5cd;
color: #400000;
}
.lineno {
word-break: keep-all;
margin: 0;
padding-left: 1em;
padding-right: 5px;
overflow: clip;
position: relative;
text-align: right;
color: #a0a0a0;
background-color: #f8f8f8;
border-right: 1px solid #e0e0e0;
}
.lineno.change, .lineno.insert {
color: #000000;
}
.lineno::before {
position: absolute;
right: 0;
content: "\a\a\a\a\a\a\a\a\a\a\a\a\a\a\a\a\a\a";
white-space: pre;
color: #a0a0a0;
}
/* Unified diff for narrow screens (phones) */
@media screen and (max-width: 70em) {
.diff {
grid-auto-flow: dense;
grid-template-columns: min-content min-content 1fr;
}
.lineno.left {
grid-column: 1;
}
.lineno.left.change {
grid-column: 1 / span 2;
}
.lineno.left.insert {
grid-column: 1;
}
.lineno.right {
grid-column: 2;
}
.lineno.right.change {
grid-column: 1 / span 2;
}
.lineno.right.insert {
grid-column: 2;
}
.line.left, .line.right.empty {
display: none;
}
.line {
grid-column: 3;
}
.line.left.insert {
display: block;
}
.line.left.change {
display: block;
}
.lineno.right.empty {
background-color: #f9d7dc;
}
.lineno.left.empty {
background-color: #ddfbe6;
}
/* line continuation arrows only in right line number column */
.lineno.left::before {
content: "";
}
}
/* Split diff for wide screens (laptops) */
@media screen and not (max-width: 70em) {
.diff {
grid-template-columns: min-content 1fr min-content 1fr;
}
.empty {
background-color: #f0f0f0;
}
/* line continuation arrows only in non-empty lines */
.lineno.empty::before {
content: "";
}
.lineno {
padding-left: 30px;
}
}
</style>
<style>
$pygments_css
</style>
</head> </head>
<body> <body>
<div class="" id="topbar"> $body
<div id="filetitle">
%(page_title)s
</div>
<div class="switches">
<div class="switch">
<input id="showoriginal" class="toggle toggle-yes-no menuoption" type="checkbox" checked>
<label for="showoriginal" data-on="&#10004; Original" data-off="Original"></label>
</div>
<div class="switch">
<input id="showmodified" class="toggle toggle-yes-no menuoption" type="checkbox" checked>
<label for="showmodified" data-on="&#10004; Modified" data-off="Modified"></label>
</div>
<div class="switch">
<input id="highlight" class="toggle toggle-yes-no menuoption" type="checkbox" checked>
<label for="highlight" data-on="&#10004; Highlight" data-off="Highlight"></label>
</div>
<div class="switch">
<input id="codeprintmargin" class="toggle toggle-yes-no menuoption" type="checkbox" checked>
<label for="codeprintmargin" data-on="&#10004; Margin" data-off="Margin"></label>
</div>
<div class="switch">
<input id="dosyntaxhighlight" class="toggle toggle-yes-no menuoption" type="checkbox" checked>
<label for="dosyntaxhighlight" data-on="&#10004; Syntax" data-off="Syntax"></label>
</div>
</div>
</div>
<div id="maincontainer" class="%(page_width)s">
<div id="leftcode" class="left-inner-shadow codebox divider-outside-bottom">
<div class="codefiletab">
&#10092; Original
</div>
<div class="printmargin">
01234567890123456789012345678901234567890123456789012345678901234567890123456789
</div>
%(original_code)s
</div>
<div id="rightcode" class="left-inner-shadow codebox divider-outside-bottom">
<div class="codefiletab">
&#10093; Modified
</div>
<div class="printmargin">
01234567890123456789012345678901234567890123456789012345678901234567890123456789
</div>
%(modified_code)s
</div>
</div>
<script src="%(jquery_js)s" type="text/javascript"></script>
<script src="%(diff_js)s" type="text/javascript"></script>
</body> </body>
</html> </html>
""" '''
PYGMENTS_CSS = '''
body .hll { background-color: #ffffcc }
body { background: #ffffff; }
body .c { color: #177500 } /* Comment */
body .err { color: #000000 } /* Error */
body .k { color: #A90D91 } /* Keyword */
body .l { color: #1C01CE } /* Literal */
body .n { color: #000000 } /* Name */
body .o { color: #000000 } /* Operator */
body .cm { color: #177500 } /* Comment.Multiline */
body .cp { color: #633820 } /* Comment.Preproc */
body .c1 { color: #177500 } /* Comment.Single */
body .cs { color: #177500 } /* Comment.Special */
body .kc { color: #A90D91 } /* Keyword.Constant */
body .kd { color: #A90D91 } /* Keyword.Declaration */
body .kn { color: #A90D91 } /* Keyword.Namespace */
body .kp { color: #A90D91 } /* Keyword.Pseudo */
body .kr { color: #A90D91 } /* Keyword.Reserved */
body .kt { color: #A90D91 } /* Keyword.Type */
body .ld { color: #1C01CE } /* Literal.Date */
body .m { color: #1C01CE } /* Literal.Number */
body .s { color: #C41A16 } /* Literal.String */
body .na { color: #836C28 } /* Name.Attribute */
body .nb { color: #A90D91 } /* Name.Builtin */
body .nc { color: #3F6E75 } /* Name.Class */
body .no { color: #000000 } /* Name.Constant */
body .nd { color: #000000 } /* Name.Decorator */
body .ni { color: #000000 } /* Name.Entity */
body .ne { color: #000000 } /* Name.Exception */
body .nf { color: #000000 } /* Name.Function */
body .nl { color: #000000 } /* Name.Label */
body .nn { color: #000000 } /* Name.Namespace */
body .nx { color: #000000 } /* Name.Other */
body .py { color: #000000 } /* Name.Property */
body .nt { color: #000000 } /* Name.Tag */
body .nv { color: #000000 } /* Name.Variable */
body .ow { color: #000000 } /* Operator.Word */
body .mb { color: #1C01CE } /* Literal.Number.Bin */
body .mf { color: #1C01CE } /* Literal.Number.Float */
body .mh { color: #1C01CE } /* Literal.Number.Hex */
body .mi { color: #1C01CE } /* Literal.Number.Integer */
body .mo { color: #1C01CE } /* Literal.Number.Oct */
body .sb { color: #C41A16 } /* Literal.String.Backtick */
body .sc { color: #2300CE } /* Literal.String.Char */
body .sd { color: #C41A16 } /* Literal.String.Doc */
body .s2 { color: #C41A16 } /* Literal.String.Double */
body .se { color: #C41A16 } /* Literal.String.Escape */
body .sh { color: #C41A16 } /* Literal.String.Heredoc */
body .si { color: #C41A16 } /* Literal.String.Interpol */
body .sx { color: #C41A16 } /* Literal.String.Other */
body .sr { color: #C41A16 } /* Literal.String.Regex */
body .s1 { color: #C41A16 } /* Literal.String.Single */
body .ss { color: #C41A16 } /* Literal.String.Symbol */
body .bp { color: #5B269A } /* Name.Builtin.Pseudo */
body .vc { color: #000000 } /* Name.Variable.Class */
body .vg { color: #000000 } /* Name.Variable.Global */
body .vi { color: #000000 } /* Name.Variable.Instance */
body .il { color: #1C01CE } /* Literal.Number.Integer.Long */
class DefaultLexer(RegexLexer): /*
""" These styles are used to highlight each diff line.
Simply lex each line as a token. Note: for partial like highlight change to "display:block-inline"
""" */
span.left_diff_change {
background-color: #FFE5B5;
display: block
}
span.left_diff_add {
background-color: #eeeeee;
display: block
}
span.left_diff_del {
background-color: #ffdddd;
display: block
}
span.lineno_q {
display: block;
}
span.right_diff_change {
background-color: #FFE5B5;
display: block
}
span.right_diff_add {
background-color: #ddffdd;
display: block
}
span.right_diff_del {
background-color: #eeeeee;
display: block
}
span.clearbg {
background-color: transparent;
}
'''
name = 'Default' class SexprLexer(RegexLexer):
aliases = ['default'] name = 'KiCad S-Expression'
filenames = ['*'] aliases = ['sexp']
filenames = ['*.kicad_mod', '*.kicad_sym']
tokens = { tokens = {
'root': [ 'root': [
(r'.*\n', Text), (r'\s+', token.Whitespace),
(r'[()]', token.Punctuation),
(r'([+-]?\d+\.\d+)(?=[)\s])', token.Number),
(r'(-?\d+)(?=[)\s])', token.Number),
(r'"((?:[^"]|\\")*)"(?=[)\s])', token.String),
(r'([^()"\s]+)(?=[)\s])', token.Name),
] ]
} }
from pygments.formatter import Formatter
from pygments.token import STANDARD_TYPES
class DiffHtmlFormatter(HtmlFormatter): from functools import lru_cache
"""
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): @lru_cache(maxsize=256)
self.isLeft = isLeft def get_token_class(ttype):
self.diffs = diffs while not (name := STANDARD_TYPES.get(ttype)):
super(DiffHtmlFormatter, self).__init__(*args, **kwargs) ttype = ttype.parent
return name
def wrap(self, source, outfile): def iter_token_lines(tokensource):
return self._wrap_code(source) lineno = 1
for ttype, value in tokensource:
left, newline, right = value.partition('\n')
while newline:
yield lineno, ttype, left
lineno += 1
left, newline, right = right.partition('\n')
if left != '':
yield lineno, ttype, left
def getDiffLineNos(self): class RecordFormatter(Formatter):
retlinenos = [] def __init__(self, side, diff):
for idx, ((left_no, left_line), (right_no, right_line), change) in enumerate(self.diffs): self.side = side
no = None if side == 'right':
if self.isLeft: diff = [(right, left, change) for left, right, change in diff]
if change: self.diff = diff
if isinstance(left_no, int) and isinstance(right_no, int):
no = '<span class="lineno_q lineno_leftchange">' + \ def format(self, tokensource, outfile):
str(left_no) + "</span>" diff = iter(self.diff)
elif isinstance(left_no, int) and not isinstance(right_no, int): self.lines = []
no = '<span class="lineno_q lineno_leftdel">' + \ for lineno, tokens in groupby(iter_token_lines(tokensource), key=lambda arg: arg[0]):
str(left_no) + "</span>"
elif not isinstance(left_no, int) and isinstance(right_no, int): for (lineno_ours, diff_ours), (lineno_theirs, _diff_theirs), change in diff:
no = '<span class="lineno_q lineno_leftadd"> </span>' if lineno_ours == lineno:
break
else: else:
no = '<span class="lineno_q">' + str(left_no) + "</span>" self.lines.append(f'<span class="lineno {self.side} empty"></span><span class="line {self.side} empty"></span>')
assert lineno_ours == lineno
if not change:
change_class = ''
elif not lineno_ours or not lineno_theirs:
change_class = ' insert'
else: else:
if change: change_class = ' change'
if isinstance(left_no, int) and isinstance(right_no, int):
no = '<span class="lineno_q lineno_rightchange">' + \
str(right_no) + "</span>"
elif isinstance(left_no, int) and not isinstance(right_no, int):
no = '<span class="lineno_q lineno_rightdel"> </span>'
elif not isinstance(left_no, int) and isinstance(right_no, int):
no = '<span class="lineno_q lineno_rightadd">' + \
str(right_no) + "</span>"
else:
no = '<span class="lineno_q">' + str(right_no) + "</span>"
retlinenos.append(no) line = f'<span class="lineno {self.side}{change_class}">{lineno}</span><span class="line {self.side}{change_class}">'
return retlinenos parts = re.split(r'(\00.|\01|$)', diff_ours)
source_pos = 0
diff_markers = []
if lineno_theirs: # Do not highlight word changes if the whole line got added or removed.
for span, sep in zip(parts[0:-2:2], parts[1:-2:2]):
source_pos += len(span)
diff_markers.append((source_pos, sep))
def _wrap_code(self, source): diff_class = ''
source = list(source) source_pos = 0
yield 0, '<pre>' 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): while diff_markers:
# print idx, ((left_no, left_line),(right_no, right_line),change) next_marker_pos, next_marker_type = diff_markers[0]
try: if source_pos <= next_marker_pos < source_pos + len(value):
if self.isLeft: split_pos = next_marker_pos - source_pos
if change: left, value = value[:split_pos], value[split_pos:]
if isinstance(left_no, int) and isinstance(right_no, int) and left_no <= len(source): line += f'<span class="{css_class}{diff_class}">{html.escape(left)}</span>'
i, t = source[left_no - 1] source_pos += len(left)
t = '<span class="left_diff_change">' + t + "</span>" diff_class = ' word_change' if next_marker_type.startswith('\0') else ''
elif isinstance(left_no, int) and not isinstance(right_no, int) and left_no <= len(source): diff_markers = diff_markers[1:]
i, t = source[left_no - 1]
t = '<span class="left_diff_del">' + t + "</span>"
elif not isinstance(left_no, int) and isinstance(right_no, int):
i, t = 1, left_line
t = '<span class="left_diff_add">' + t + "</span>"
else:
raise
else: else:
if left_no <= len(source): break
i, t = source[left_no - 1] line += f'<span class="{css_class}{diff_class}">{html.escape(value)}</span>'
else: source_pos += len(value)
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 = '<span class="right_diff_change">' + t + "</span>"
elif isinstance(left_no, int) and not isinstance(right_no, int):
i, t = 1, right_line
t = '<span class="right_diff_del">' + t + "</span>"
elif not isinstance(left_no, int) and isinstance(right_no, int) and right_no <= len(source):
i, t = source[right_no - 1]
t = '<span class="right_diff_add">' + t + "</span>"
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</pre>'
def _wrap_tablelinenos(self, inner): if css_class is not None:
dummyoutfile = io.StringIO() line += '</span>'
lncount = 0
for t, line in inner:
if t:
lncount += 1
# compatibility Python v2/v3 line += '</span>'
if sys.version_info > (3,0): self.lines.append(line)
dummyoutfile.write(line)
else:
dummyoutfile.write(unicode(line))
fl = self.linenostart for _ours_empty, (lineno_theirs, _diff_theirs), change in diff:
mw = len(str(lncount + fl - 1)) self.lines.append(f'<span class="lineno {self.side} empty"></span><span class="line {self.side} empty"></span>')
sp = self.linenospecial assert change and lineno_theirs
st = self.linenostep
la = self.lineanchors
aln = self.anchorlinenos
nocls = self.noclasses
lines = [] def html_diff_content(old, new):
for i in self.getDiffLineNos(): diff = list(difflib._mdiff(old.splitlines(), new.splitlines()))
lines.append('%s' % (i,))
ls = ''.join(lines) fmt_l = RecordFormatter('left', diff)
pygments.highlight(old, SexprLexer(), fmt_l)
# in case you wonder about the seemingly redundant <div> here: since the fmt_r = RecordFormatter('right', diff)
# content in the other cell also is wrapped in a div, some browsers in pygments.highlight(new, SexprLexer(), fmt_r)
# some configurations seem to mess up the formatting...
if nocls:
yield 0, ('<table class="%stable">' % self.cssclass +
'<tr><td><div class="linenodiv" '
'style="background-color: #f0f0f0; padding-right: 10px">'
'<pre style="line-height: 125%">' +
ls + '</pre></div></td><td class="code">')
else:
yield 0, ('<table class="%stable">' % self.cssclass +
'<tr><td class="linenos"><div class="linenodiv"><pre>' +
ls + '</pre></div></td><td class="code">')
yield 0, dummyoutfile.getvalue()
yield 0, '</td></tr></table>'
return '\n'.join(chain.from_iterable(zip(fmt_l.lines, fmt_r.lines)))
class CodeDiff(object): def html_diff_block(old, new, filename):
""" code = html_diff_content(old, new)
Manages a pair of source files and generates a single html diff page comparing return textwrap.dedent(f'''<div class="file-container">
the contents. <div class="file-title">{filename}</div>
""" <div class="diff">
pygmentsCssFile = "./deps/codeformats/%s.css" {code}
diffCssFile = "./deps/diff.css" </div>
diffJsFile = "./deps/diff.js" </div>''')
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 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__": 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. """ creates an html page which highlights the differences between the two. """
parser = argparse.ArgumentParser(description=description) parser = argparse.ArgumentParser(description=description)
parser.add_argument('-s', '--show', action='store_true', parser.add_argument('-b', '--open', action='store_true', help='Open output file in a browser')
help='show html in a browser.') parser.add_argument('-s', '--syntax-css', help='Path to custom Pygments CSS file for code syntax highlighting')
parser.add_argument('-p', '--print-width', action='store_true', parser.add_argument('-t', '--pagetitle', help='Override page title of output HTML file')
help='Restrict code to 80 columns wide. (printer friendly in landscape)') parser.add_argument('-o', '--output', default=sys.stdout, type=argparse.FileType('w'), help='Name of output file (default: stdout)')
parser.add_argument('-c', '--syntax-css', action='store', default="vs", parser.add_argument('--header', action='store_true', help='Only output HTML header with stylesheets and stuff, and no diff')
help='Pygments CSS for code syntax highlighting. Can be one of: %s' % str(PYGMENTS_STYLES)) parser.add_argument('--content', action='store_true', help='Only output HTML content, without header')
parser.add_argument('-v', '--verbose', action='store_true', help='show verbose output.') parser.add_argument('old', help='source file or directory to compare ("before" file)')
parser.add_argument('file1', help='source file to compare ("before" file).') parser.add_argument('new', help='source file or directory to compare ("after" file)')
parser.add_argument('file2', help='source file to compare ("after" file).')
args = parser.parse_args() args = parser.parse_args()
if args.syntax_css not in PYGMENTS_STYLES: if args.open and args.output == sys.stdout:
raise ValueError("Syntax CSS (-c) must be one of %r." % PYGMENTS_STYLES) 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)