blob: 44ab18b942666820c1ddb9ac850bbbf7a8d2e164 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility for outputting a HTML diff of two multi-line strings.
The main purpose of this utility is to show the difference between
text baselines (-expected.txt files) and actual text results.
Note, in the standard library module difflib, there is also a HtmlDiff class,
although it outputs a larger and more complex HTML table than we need.
"""
import cgi
import difflib
_TEMPLATE = """<html>
<head>
<style>
table { white-space: pre-wrap; font-family: monospace; border-collapse: collapse; }
th { color: #444; background: #eed; text-align: right; vertical-align: baseline; padding: 1px 4px 1px 4px; }
.del { background: #faa; }
.add { background: #afa; }
</style>
</head>
<body><table>%s</table></body>
</html>
"""
def html_diff(a_text, b_text):
"""Returns a diff between two strings as HTML."""
# Diffs can be between multiple text files of different encodings
# so we always want to deal with them as byte arrays, not unicode strings.
assert isinstance(a_text, str)
assert isinstance(b_text, str)
a_lines = a_text.splitlines(True)
b_lines = b_text.splitlines(True)
return _TEMPLATE % HtmlDiffGenerator().generate_tbody(a_lines, b_lines)
class HtmlDiffGenerator(object):
def __init__(self):
self.a_line_no = None
self.b_line_no = None
self.a_lines_len = None
def generate_tbody(self, a_lines, b_lines):
self.a_line_no = 0
self.b_line_no = 0
self.a_lines_len = len(a_lines)
self.b_lines_len = len(b_lines)
matcher = difflib.SequenceMatcher(None, a_lines, b_lines)
output = []
for tag, a_start, a_end, b_start, b_end in matcher.get_opcodes():
output.append(self._format_chunk(tag, a_lines[a_start:a_end], b_lines[b_start:b_end]))
return ''.join(output)
def _format_chunk(self, tag, a_chunk, b_chunk):
if tag == 'delete':
return self._format_delete(a_chunk)
if tag == 'insert':
return self._format_insert(b_chunk)
if tag == 'replace':
return self._format_delete(a_chunk) + self._format_insert(b_chunk)
assert tag == 'equal'
return self._format_equal(a_chunk)
def _format_equal(self, common_chunk):
output = ''
if len(common_chunk) <= 7:
for line in common_chunk:
output += self._format_equal_line(line)
else:
# Do not show context lines at the beginning of the file.
if self.a_line_no == 0 and self.b_line_no == 0:
self.a_line_no += 3
self.b_line_no += 3
else:
for line in common_chunk[0:3]:
output += self._format_equal_line(line)
self.a_line_no += len(common_chunk) - 6
self.b_line_no += len(common_chunk) - 6
output += '<tr><td colspan=3>\n\n</tr>'
# Do not show context lines at the end of the file.
if self.a_line_no + 3 != self.a_lines_len or self.b_line_no + 3 != self.b_lines_len:
for line in common_chunk[len(common_chunk) - 3:len(common_chunk)]:
output += self._format_equal_line(line)
return output
def _format_equal_line(self, line):
self.a_line_no += 1
self.b_line_no += 1
return '<tr><th>%d<th>%d<td>%s</tr>' % (self.a_line_no, self.b_line_no, cgi.escape(line))
def _format_insert(self, chunk):
output = ''
for line in chunk:
self.b_line_no += 1
output += '<tr><th><th>%d<td class="add">%s</tr>' % (self.b_line_no, cgi.escape(line))
return output
def _format_delete(self, chunk):
output = ''
for line in chunk:
self.a_line_no += 1
output += '<tr><th>%d<th><td class="del">%s</tr>' % (self.a_line_no, cgi.escape(line))
return output