Skip to content

Commit

Permalink
Merge pull request #144 from jedie/diff-improvments
Browse files Browse the repository at this point in the history
Multiline diff formatting improvements #137
  • Loading branch information
jedie authored Feb 4, 2021
2 parents 8b55abd + 36f4702 commit a2d55bc
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 51 deletions.
87 changes: 73 additions & 14 deletions reversion_compare/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,85 @@ def highlight_diff(diff_text):
return f'<pre class="highlight">{html}</pre>'


def diff2lines(diff):
"""
Group a sequence of diff_match_patch diff operations based on the
lines they affect, so we can generate a nice-looking HTML diff.
Example input:
[(DIFF_EQUAL, "equal\ntext"),
(DIFF_DELETE, "deleted\n"),
(DIFF_INSERT, "added\ntext")]
Example output:
[(DIFF_EQUAL, "equal")],
[(DIFF_EQUAL, "text"), (DIFF_DELETE, "deleted")],
[(DIFF_INSERT, "added")],
[(DIFF_INSERT, "text")],
"""
curr_line = []
for op, data in diff:
data = escape(data)
for line in data.splitlines(keepends=True):
curr_line.append((op, line.rstrip("\r\n")))
if line.endswith("\n"):
yield curr_line
curr_line = []
if curr_line:
yield curr_line


def lines2html(lines):
"""
Convert a sequence of diff operations grouped by line (via diff2lines())
to HTML for the diff viewer.
Example input:
[(DIFF_EQUAL, "equal")],
[(DIFF_EQUAL, "text"), (DIFF_DELETE, "deleted")],
[(DIFF_INSERT, "added")],
[(DIFF_INSERT, "text")],
Example output:
'equal\n'
'<span class="diff-line diff-del">text<del>deleted</del><del>⏎</del></span>\n'
'<span class="diff-line diff-ins"><ins>added</ins></span>\n'
'<span class="diff-line diff-del diff-ins"><ins>text</ins><del>removed</del></span>\n'
"""
html = []

for diff in lines:
line = ''
line_changes = set()
for op, data in diff:
if op == diff_match_patch.DIFF_EQUAL:
line += data
elif op == diff_match_patch.DIFF_INSERT:
line += f'<ins>{data or "⏎"}</ins>'
line_changes.add('ins')
elif op == diff_match_patch.DIFF_DELETE:
line += f'<del>{data or "⏎"}</del>'
line_changes.add('del')
else:
raise TypeError(f'Unknown diff op: {op!r}')
if line_changes:
classes = ' '.join(f'diff-{change_type}' for change_type in sorted(line_changes))
html.append(f'<span class="diff-line {classes}">{line}</span>\n')
else:
html.append(f'{line}\n')

return ''.join(html)


def diff_match_patch_pretty_html(diff):
"""
Similar to diff_match_patch.diff_prettyHtml but generated the same html as our
reversion_compare.helpers.highlight_diff
"""
html = ['<pre class="highlight">']
for (op, line) in diff:
line = escape(line)

if op == diff_match_patch.DIFF_INSERT:
line = f'<ins>{line}</ins>'
elif op == diff_match_patch.DIFF_DELETE:
line = f'<del>{line}</del>'
elif op != diff_match_patch.DIFF_EQUAL:
raise TypeError(f'Unknown op: {op!r}')

html.append(line)

html.append('</pre>')
return ''.join(html)
html.extend(lines2html(diff2lines(diff)))
html.append("</pre>")
return "".join(html)


def generate_dmp_diff(value1, value2, cleanup=SEMANTIC):
Expand Down
16 changes: 13 additions & 3 deletions reversion_compare/templates/reversion-compare/compare.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@
/* minimal style for the diffs */
pre.highlight {
max-width: 900px;
white-space: pre-line;
}
del, ins {
color: #000;
text-decoration: none;
}
del { background-color: #ffe6e6; }
ins { background-color: #e6ffe6; }
del { background-color: #fdb8c0 }
ins { background-color: #acf2bd; }
sup.follow { color: #5555ff; }
.diff-line {
display: inline-block;
width: 100%;
margin-left: -1.0em;
padding-left: 0.5em;
background-color: #f6f6f6;
color: #222;
}
.diff-line.diff-ins { border-left: 0.5em solid #bef5cb; background-color: #e6ffed; }
.diff-line.diff-del { border-left: 0.5em solid #fdaeb7; background-color: #ffeef0; }
.diff-line.diff-ins.diff-del { border-left: 0.5em solid #bef5cb; background-color: #e6ffed; } /* mixed del/ins == green */
</style>
{% endblock %}

Expand Down
151 changes: 124 additions & 27 deletions reversion_compare_tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
from reversion_compare.helpers import EFFICIENCY, SEMANTIC, generate_dmp_diff, generate_ndiff, html_diff
from diff_match_patch import diff_match_patch

from reversion_compare.helpers import (
EFFICIENCY,
SEMANTIC,
diff2lines,
generate_dmp_diff,
generate_ndiff,
html_diff,
lines2html,
)


DIFF_EQUAL = diff_match_patch.DIFF_EQUAL
DIFF_INSERT = diff_match_patch.DIFF_INSERT
DIFF_DELETE = diff_match_patch.DIFF_DELETE


def test_generate_ndiff():
Expand Down Expand Up @@ -32,17 +47,21 @@ def test_generate_dmp_diff():
value1='one',
value2='two',
)
assert html == '<pre class="highlight"><del>one</del><ins>two</ins></pre>'
assert html == (
'<pre class="highlight">'
'<span class="diff-line diff-del diff-ins"><del>one</del><ins>two</ins></span>\n'
'</pre>'
)

html = generate_dmp_diff(
value1='aaa\nccc\nddd\n',
value2='aaa\nbbb\nccc\n',
)
assert html == (
'<pre class="highlight">aaa\n'
'<del>ccc\n'
'ddd</del><ins>bbb\n'
'ccc</ins>\n'
'<span class="diff-line diff-del"><del>ccc</del></span>\n'
'<span class="diff-line diff-del diff-ins"><del>ddd</del><ins>bbb</ins></span>\n'
'<span class="diff-line diff-ins"><ins>ccc</ins></span>\n'
'</pre>'
)

Expand All @@ -56,20 +75,22 @@ def test_generate_dmp_diff_no_cleanup():
value2='two',
cleanup=None
)
assert html == '<pre class="highlight"><ins>tw</ins>o<del>ne</del></pre>'
assert html == (
'<pre class="highlight">'
'<span class="diff-line diff-del diff-ins"><ins>tw</ins>o<del>ne</del></span>\n'
'</pre>'
)

html = generate_dmp_diff(
value1='aaa\nccc\nddd\n',
value2='aaa\nbbb\nccc\n',
cleanup=None
)
assert html == (
'<pre class="highlight">'
'aaa\n'
'<ins>bbb\n'
'</ins>ccc\n'
'<del>ddd\n'
'</del>'
'<pre class="highlight">aaa\n'
'<span class="diff-line diff-ins"><ins>bbb</ins></span>\n'
'ccc\n'
'<span class="diff-line diff-del"><del>ddd</del></span>\n'
'</pre>'
)

Expand All @@ -83,20 +104,22 @@ def test_generate_dmp_diff_efficiency():
value2='two',
cleanup=EFFICIENCY
)
assert html == '<pre class="highlight"><ins>tw</ins>o<del>ne</del></pre>'
assert html == (
'<pre class="highlight">'
'<span class="diff-line diff-del diff-ins"><ins>tw</ins>o<del>ne</del></span>\n'
'</pre>'
)

html = generate_dmp_diff(
value1='aaa\nccc\nddd\n',
value2='aaa\nbbb\nccc\n',
cleanup=EFFICIENCY
)
assert html == (
'<pre class="highlight">'
'aaa\n'
'<ins>bbb\n'
'</ins>ccc\n'
'<del>ddd\n'
'</del>'
'<pre class="highlight">aaa\n'
'<span class="diff-line diff-ins"><ins>bbb</ins></span>\n'
'ccc\n'
'<span class="diff-line diff-del"><del>ddd</del></span>\n'
'</pre>'
)

Expand All @@ -112,8 +135,8 @@ def test_generate_dmp_diff_semantic():
)
assert html == (
'<pre class="highlight">'
'xxx<del>1</del><ins>2</ins>xxx\n'
'X'
'<span class="diff-line diff-del diff-ins">xxx<del>1</del><ins>2</ins>xxx</span>\n'
'X\n'
'</pre>'
)

Expand All @@ -124,7 +147,7 @@ def test_generate_dmp_diff_semantic():
)
assert html == (
'<pre class="highlight">'
'<del>one</del><ins>two</ins>'
'<span class="diff-line diff-del diff-ins"><del>one</del><ins>two</ins></span>\n'
'</pre>'
)

Expand All @@ -134,11 +157,10 @@ def test_generate_dmp_diff_semantic():
cleanup=SEMANTIC
)
assert html == (
'<pre class="highlight">'
'aaa\n'
'<del>ccc\n'
'ddd</del><ins>bbb\n'
'ccc</ins>\n'
'<pre class="highlight">aaa\n'
'<span class="diff-line diff-del"><del>ccc</del></span>\n'
'<span class="diff-line diff-del diff-ins"><del>ddd</del><ins>bbb</ins></span>\n'
'<span class="diff-line diff-ins"><ins>ccc</ins></span>\n'
'</pre>'
)

Expand All @@ -158,6 +180,81 @@ def test_html_diff():
)
assert html == (
'<pre class="highlight">'
'<span class="diff-line diff-del diff-ins">'
'<del>m</del><ins>M</ins>ore than 20 <del>C</del><ins>c</ins>haracters<ins>,</ins> or?'
'</span>\n'
'</pre>'
)


def test_diff2lines():
assert list(diff2lines(
[
(DIFF_EQUAL, 'equal\ntext'),
(DIFF_DELETE, 'deleted\n'),
(DIFF_INSERT, 'added\ntext'),
]
)) == [
[(DIFF_EQUAL, 'equal')],
[(DIFF_EQUAL, 'text'), (DIFF_DELETE, 'deleted')],
[(DIFF_INSERT, 'added')],
[(DIFF_INSERT, 'text')],
]

# html escaping
assert list(diff2lines(
[
(DIFF_EQUAL, '<equal>\ntext'),
(DIFF_DELETE, '&deleted\n'),
(DIFF_INSERT, 'added\ntext'),
]
)) == [
[(DIFF_EQUAL, '&lt;equal&gt;')],
[(DIFF_EQUAL, 'text'), (DIFF_DELETE, '&amp;deleted')],
[(DIFF_INSERT, 'added')],
[(DIFF_INSERT, 'text')],
]

# \r\n line feeds
assert list(diff2lines(
[
(DIFF_EQUAL, 'equal\r\ntext'),
(DIFF_DELETE, 'deleted\r\n'),
(DIFF_INSERT, 'added\r\ntext'),
]
)) == [
[(DIFF_EQUAL, 'equal')],
[(DIFF_EQUAL, 'text'), (DIFF_DELETE, 'deleted')],
[(DIFF_INSERT, 'added')],
[(DIFF_INSERT, 'text')],
]

# Whitespace is retained
assert list(diff2lines(
[
(DIFF_EQUAL, 'equal\ntext '),
(DIFF_DELETE, 'deleted\n'),
(DIFF_INSERT, 'added\n text'),
]
)) == [
[(DIFF_EQUAL, 'equal')],
[(DIFF_EQUAL, 'text '), (DIFF_DELETE, 'deleted')],
[(DIFF_INSERT, 'added')],
[(DIFF_INSERT, ' text')],
]


def test_lines2html():
assert lines2html(
[
[(DIFF_EQUAL, 'equal')],
[(DIFF_EQUAL, 'text'), (DIFF_DELETE, 'deleted'), (DIFF_DELETE, '')],
[(DIFF_INSERT, 'added')],
[(DIFF_INSERT, 'text'), (DIFF_DELETE, 'removed')],
]
) == (
'equal\n'
'<span class="diff-line diff-del">text<del>deleted</del><del>⏎</del></span>\n'
'<span class="diff-line diff-ins"><ins>added</ins></span>\n'
'<span class="diff-line diff-del diff-ins"><ins>text</ins><del>removed</del></span>\n'
)
15 changes: 8 additions & 7 deletions reversion_compare_tests/test_variant_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,26 +148,27 @@ def test_all_changes(self):
"""
<div class="module">
<pre class="highlight">
http<ins>s</ins>://<del>www.pylucid.org</del><ins>github.com/jedie</ins>/
<span class="diff-line diff-del diff-ins">http<ins>s</ins>://
<del>www.pylucid.org</del><ins>github.com/jedie</ins>/</span>
</pre>
</div>
""",

"<h3>file field</h3>",
f"""
<div class="module">
<pre class="highlight">
/media{settings.UNITTEST_TEMP_PATH}/<del>foo</del><ins>bar</ins>
</pre>
<pre class="highlight"><span class="diff-line diff-del diff-ins">
{settings.UNITTEST_TEMP_PATH}/<del>foo</del><ins>bar</ins></span>
</pre>
</div>
""",

"<h3>filepath</h3>",
f"""
<div class="module">
<pre class="highlight">
{settings.UNITTEST_TEMP_PATH}/<del>foo</del><ins>bar</ins>
</pre>
<pre class="highlight"><span class="diff-line diff-del diff-ins">
{settings.UNITTEST_TEMP_PATH}/<del>foo</del><ins>bar</ins></span>
</pre>
</div>
""",

Expand Down

0 comments on commit a2d55bc

Please sign in to comment.