Produce adaptive, css-only, single-file, pretty html output
This commit is contained in:
parent
53503591ad
commit
3a6a186049
2 changed files with 435 additions and 352 deletions
2
deps/codeformats/xcode.css
vendored
2
deps/codeformats/xcode.css
vendored
|
|
@ -89,4 +89,4 @@ span.right_diff_del {
|
||||||
}
|
}
|
||||||
span.clearbg {
|
span.clearbg {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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="✔ 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="✔ 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="✔ 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="✔ 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="✔ 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">
|
|
||||||
❬ 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">
|
|
||||||
❭ 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)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue