diff2html.usage()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
#! /usr/bin/python
2
# coding=utf-8
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
#
18
#
19
# Transform a unified diff from stdin to a colored
20
# side-by-side HTML page on stdout.
21
#
22
# Authors: Olivier Matz <[email protected]>
23
#          Alan De Smet <[email protected]>
24
#          Sergey Satskiy <[email protected]>
25
#          scito <info at scito.ch>
26
#
27
# Inspired by diff2html.rb from Dave Burt <dave (at) burt.id.au>
28
# (mainly for html theme)
29
#
30
# TODO:
31
# - The sane function currently mashes non-ASCII characters to "."
32
#   Instead be clever and convert to something like "xF0"
33
#   (the hex value), and mark with a <span>.  Even more clever:
34
#   Detect if the character is "printable" for whatever definition,
35
#   and display those directly.
36
37
import sys, re, html.entities, getopt, io, codecs, datetime
38
from functools import reduce
39
try:
40
    from simplediff import diff, string_diff
41
except ImportError:
42
    sys.stderr.write("info: simplediff module not found, only linediff is available\n")
43
    sys.stderr.write("info: it can be downloaded at https://github.com/paulgb/simplediff\n")
44
45
# minimum line size, we add a zero-sized breakable space every
46
# LINESIZE characters
47
linesize = 20
48
tabsize = 8
49
show_CR = False
50
encoding = "utf-8"
51
lang = "en"
52
algorithm = 0
53
54
desc = "File comparison"
55
dtnow = datetime.datetime.now()
56
modified_date = "%s+01:00"%dtnow.isoformat()
57
58
html_hdr = """<!DOCTYPE html>
59
<html lang="{5}" dir="ltr"
60
    xmlns:dc="http://purl.org/dc/terms/">
61
<head>
62
    <meta charset="{1}" />
63
    <meta name="generator" content="diff2html.py (http://git.droids-corp.org/gitweb/?p=diff2html)" />
64
    <!--meta name="author" content="Fill in" /-->
65
    <title>HTML Diff{0}</title>
66
    <link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEXAAAAAgAD///+K/HwIAAAAJUlEQVQI12NYBQQM2IgGBQ4mCIEQW7oyK4phampkGIQAc1G1AQCRxCNbyW92oQAAAABJRU5ErkJggg==" type="image/png" />
67
    <meta property="dc:language" content="{5}" />
68
    <!--meta property="dc:date" content="{3}" /-->
69
    <meta property="dc:modified" content="{4}" />
70
    <meta name="description" content="{2}" />
71
    <meta property="dc:abstract" content="{2}" />
72
    <style>
73
        table {{ border:0px; border-collapse:collapse; width: 100%; font-size:0.75em; font-family: Lucida Console, monospace }}
74
        td.line {{ color:#8080a0 }}
75
        th {{ background: black; color: white }}
76
        tr.diffunmodified td {{ background: #D0D0E0 }}
77
        tr.diffhunk td {{ background: #A0A0A0 }}
78
        tr.diffadded td {{ background: #CCFFCC }}
79
        tr.diffdeleted td {{ background: #FFCCCC }}
80
        tr.diffchanged td {{ background: #FFFFA0 }}
81
        span.diffchanged2 {{ background: #E0C880 }}
82
        span.diffponct {{ color: #B08080 }}
83
        tr.diffmisc td {{}}
84
        tr.diffseparator td {{}}
85
    </style>
86
</head>
87
<body>
88
"""
89
90
html_footer = """
91
<footer>
92
    <p>Modified at {1}. HTML formatting created by <a href="http://git.droids-corp.org/gitweb/?p=diff2html;a=summary">diff2html</a>.    </p>
93
</footer>
94
</body></html>
95
"""
96
97
table_hdr = """
98
		<table class="diff">
99
"""
100
101
table_footer = """
102
</table>
103
"""
104
105
DIFFON = "\x01"
106
DIFFOFF = "\x02"
107
108
buf = []
109
add_cpt, del_cpt = 0, 0
110
line1, line2 = 0, 0
111
hunk_off1, hunk_size1, hunk_off2, hunk_size2 = 0, 0, 0, 0
112
113
114
# Characters we're willing to word wrap on
115
WORDBREAK = " \t;.,/):-"
116
117
def sane(x):
118
    r = ""
119
    for i in x:
120
        j = ord(i)
121
        if i not in ['\t', '\n'] and (j < 32):
122
            r = r + "."
123
        else:
124
            r = r + i
125
    return r
126
127
def linediff(s, t):
128
    '''
129
    Original line diff algorithm of diff2html. It's character based.
130
    '''
131
    if len(s):
132
        s = str(reduce(lambda x, y:x+y, [ sane(c) for c in s ]))
133
    if len(t):
134
        t = str(reduce(lambda x, y:x+y, [ sane(c) for c in t ]))
135
136
    m, n = len(s), len(t)
137
    d = [[(0, 0) for i in range(n+1)] for i in range(m+1)]
138
139
140
    d[0][0] = (0, (0, 0))
141
    for i in range(m+1)[1:]:
142
        d[i][0] = (i,(i-1, 0))
143
    for j in range(n+1)[1:]:
144
        d[0][j] = (j,(0, j-1))
145
146
    for i in range(m+1)[1:]:
147
        for j in range(n+1)[1:]:
148
            if s[i-1] == t[j-1]:
149
                cost = 0
150
            else:
151
                cost = 1
152
            d[i][j] = min((d[i-1][j][0] + 1, (i-1, j)),
153
                          (d[i][j-1][0] + 1, (i, j-1)),
154
                          (d[i-1][j-1][0] + cost, (i-1, j-1)))
155
156
    l = []
157
    coord = (m, n)
158
    while coord != (0, 0):
159
        l.insert(0, coord)
160
        x, y = coord
161
        coord = d[x][y][1]
162
163
    l1 = []
164
    l2 = []
165
166
    for coord in l:
167
        cx, cy = coord
168
        child_val = d[cx][cy][0]
169
170
        father_coord = d[cx][cy][1]
171
        fx, fy = father_coord
172
        father_val = d[fx][fy][0]
173
174
        diff = (cx-fx, cy-fy)
175
176
        if diff == (0, 1):
177
            l1.append("")
178
            l2.append(DIFFON + t[fy] + DIFFOFF)
179
        elif diff == (1, 0):
180
            l1.append(DIFFON + s[fx] + DIFFOFF)
181
            l2.append("")
182
        elif child_val-father_val == 1:
183
            l1.append(DIFFON + s[fx] + DIFFOFF)
184
            l2.append(DIFFON + t[fy] + DIFFOFF)
185
        else:
186
            l1.append(s[fx])
187
            l2.append(t[fy])
188
189
    r1, r2 = (reduce(lambda x, y:x+y, l1), reduce(lambda x, y:x+y, l2))
190
    return r1, r2
191
192
193
def diff_changed(old, new):
194
    '''
195
    Returns the differences basend on characters between two strings
196
    wrapped with DIFFON and DIFFOFF using `diff`.
197
    '''
198
    con = {'=': (lambda x: x),
199
           '+': (lambda x: DIFFON + x + DIFFOFF),
200
           '-': (lambda x: '')}
201
    return "".join([(con[a])("".join(b)) for a, b in diff(old, new)])
202
203
204
def diff_changed_ts(old, new):
205
    '''
206
    Returns a tuple for a two sided comparison based on characters, see `diff_changed`.
207
    '''
208
    return (diff_changed(new, old), diff_changed(old, new))
209
210
211
def word_diff(old, new):
212
    '''
213
    Returns the difference between the old and new strings based on words. Punctuation is not part of the word.
214
215
    Params:
216
        old the old string
217
        new the new string
218
219
    Returns:
220
        the output of `diff` on the two strings after splitting them
221
        on whitespace (a list of change instructions; see the docstring
222
        of `diff`)
223
    '''
224
    separator_pattern = '(\W+)';
225
    return diff(re.split(separator_pattern, old, flags=re.UNICODE), re.split(separator_pattern, new, flags=re.UNICODE))
226
227
228
def diff_changed_words(old, new):
229
    '''
230
    Returns the difference between two strings based on words (see `word_diff`)
231
    wrapped with DIFFON and DIFFOFF.
232
233
    Returns:
234
        the output of the diff expressed delimited with DIFFON and DIFFOFF.
235
    '''
236
    con = {'=': (lambda x: x),
237
           '+': (lambda x: DIFFON + x + DIFFOFF),
238
           '-': (lambda x: '')}
239
    return "".join([(con[a])("".join(b)) for a, b in word_diff(old, new)])
240
241
242
def diff_changed_words_ts(old, new):
243
    '''
244
    Returns a tuple for a two sided comparison based on words, see `diff_changed_words`.
245
    '''
246
    return (diff_changed_words(new, old), diff_changed_words(old, new))
247
248
249
def convert(s, linesize=0, ponct=0):
250
    i = 0
251
    t = ""
252
    for c in s:
253
        # used by diffs
254
        if c == DIFFON:
255
            t += '<span class="diffchanged2">'
256
        elif c == DIFFOFF:
257
            t += "</span>"
258
259
        # special html chars
260
        elif ord(c) in html.entities.codepoint2name:
261
            t += "&%s;" % (html.entities.codepoint2name[ord(c)])
262
            i += 1
263
264
        # special highlighted chars
265
        elif c == "\t" and ponct == 1:
266
            n = tabsize-(i%tabsize)
267
            if n == 0:
268
                n = tabsize
269
            t += ('<span class="diffponct">&raquo;</span>'+'&nbsp;'*(n-1))
270
        elif c == " " and ponct == 1:
271
            t += '<span class="diffponct">&middot;</span>'
272
        elif c == "\n" and ponct == 1:
273
            if show_CR:
274
                t += '<span class="diffponct">\</span>'
275
        else:
276
            t += c
277
            i += 1
278
279
        if linesize and (WORDBREAK.count(c) == 1):
280
            t += '&#8203;'
281
            i = 0
282
        if linesize and i > linesize:
283
            i = 0
284
            t += "&#8203;"
285
286
    return t
287
288
289
def add_comment(s, output_file):
290
    output_file.write(('<tr class="diffmisc"><td colspan="4">%s</td></tr>\n'%convert(s)).encode(encoding))
291
292
293
def add_filename(f1, f2, output_file):
294
    output_file.write(("<tr><th colspan='2'>%s</th>"%convert(f1, linesize=linesize)).encode(encoding))
295
    output_file.write(("<th colspan='2'>%s</th></tr>\n"%convert(f2, linesize=linesize)).encode(encoding))
296
297
298
def add_hunk(output_file, show_hunk_infos):
299
    if show_hunk_infos:
300
        output_file.write('<tr class="diffhunk"><td colspan="2">Offset %d, %d lines modified</td>'%(hunk_off1, hunk_size1))
301
        output_file.write('<td colspan="2">Offset %d, %d lines modified</td></tr>\n'%(hunk_off2, hunk_size2))
302
    else:
303
        # &#8942; - vertical ellipsis
304
        output_file.write('<tr class="diffhunk"><td colspan="2">&#8942;</td><td colspan="2">&#8942;</td></tr>')
305
306
307
def add_line(s1, s2, output_file):
308
    global line1
309
    global line2
310
311
    orig1 = s1
312
    orig2 = s2
313
314
    if s1 == None and s2 == None:
315
        type_name = "unmodified"
316
    elif s1 == None or s1 == "":
317
        type_name = "added"
318
    elif s2 == None or s1 == "":
319
        type_name = "deleted"
320
    elif s1 == s2:
321
        type_name = "unmodified"
322
    else:
323
        type_name = "changed"
324
        if algorithm == 1:
325
            s1, s2 = diff_changed_words_ts(orig1, orig2)
326
        elif algorithm == 2:
327
            s1, s2 = diff_changed_ts(orig1, orig2)
328
        else: # default
329
            s1, s2 = linediff(orig1, orig2)
330
331
    output_file.write(('<tr class="diff%s">' % type_name).encode(encoding))
332
    if s1 != None and s1 != "":
333
        output_file.write(('<td class="diffline">%d </td>' % line1).encode(encoding))
334
        output_file.write('<td class="diffpresent">'.encode(encoding))
335
        output_file.write(convert(s1, linesize=linesize, ponct=1).encode(encoding))
336
        output_file.write('</td>')
337
    else:
338
        s1 = ""
339
        output_file.write('<td colspan="2"> </td>')
340
341
    if s2 != None and s2 != "":
342
        output_file.write(('<td class="diffline">%d </td>'%line2).encode(encoding))
343
        output_file.write('<td class="diffpresent">')
344
        output_file.write(convert(s2, linesize=linesize, ponct=1).encode(encoding))
345
        output_file.write('</td>')
346
    else:
347
        s2 = ""
348
        output_file.write('<td colspan="2"></td>')
349
350
    output_file.write('</tr>\n')
351
352
    if s1 != "":
353
        line1 += 1
354
    if s2 != "":
355
        line2 += 1
356
357
358
def empty_buffer(output_file):
359
    global buf
360
    global add_cpt
361
    global del_cpt
362
363
    if del_cpt == 0 or add_cpt == 0:
364
        for l in buf:
365
            add_line(l[0], l[1], output_file)
366
367
    elif del_cpt != 0 and add_cpt != 0:
368
        l0, l1 = [], []
369
        for l in buf:
370
            if l[0] != None:
371
                l0.append(l[0])
372
            if l[1] != None:
373
                l1.append(l[1])
374
        max_len = (len(l0) > len(l1)) and len(l0) or len(l1)
375
        for i in range(max_len):
376
            s0, s1 = "", ""
377
            if i < len(l0):
378
                s0 = l0[i]
379
            if i < len(l1):
380
                s1 = l1[i]
381
            add_line(s0, s1, output_file)
382
383
    add_cpt, del_cpt = 0, 0
384
    buf = []
385
386
387
def parse_input(input_file, output_file, input_file_name, output_file_name,
388
                exclude_headers, show_hunk_infos):
389
    global add_cpt, del_cpt
390
    global line1, line2
391
    global hunk_off1, hunk_size1, hunk_off2, hunk_size2
392
393
    if not exclude_headers:
394
        title_suffix = ' ' + input_file_name
395
        output_file.write(html_hdr.format(title_suffix, encoding, desc, "", modified_date, lang).encode(encoding))
396
    output_file.write(table_hdr.encode(encoding))
397
398
    while True:
399
        l = input_file.readline()
400
        if l == "":
401
            break
402
403
        m = re.match('^--- ([^\s]*)', l)
404
        if m:
405
            empty_buffer(output_file)
406
            file1 = m.groups()[0]
407
            while True:
408
                l = input_file.readline()
409
                m = re.match('^\+\+\+ ([^\s]*)', l)
410
                if m:
411
                    file2 = m.groups()[0]
412
                    break
413
            add_filename(file1, file2, output_file)
414
            hunk_off1, hunk_size1, hunk_off2, hunk_size2 = 0, 0, 0, 0
415
            continue
416
417
        m = re.match("@@ -(\d+),?(\d*) \+(\d+),?(\d*)", l)
418
        if m:
419
            empty_buffer(output_file)
420
            hunk_data = [x=="" and 1 or int(x) for x in m.groups()]
421
            hunk_off1, hunk_size1, hunk_off2, hunk_size2 = hunk_data
422
            line1, line2 = hunk_off1, hunk_off2
423
            add_hunk(output_file, show_hunk_infos)
424
            continue
425
426
        if hunk_size1 == 0 and hunk_size2 == 0:
427
            empty_buffer(output_file)
428
            add_comment(l, output_file)
429
            continue
430
431
        if re.match("^\+", l):
432
            add_cpt += 1
433
            hunk_size2 -= 1
434
            buf.append((None, l[1:]))
435
            continue
436
437
        if re.match("^\-", l):
438
            del_cpt += 1
439
            hunk_size1 -= 1
440
            buf.append((l[1:], None))
441
            continue
442
443
        if re.match("^\ ", l) and hunk_size1 and hunk_size2:
444
            empty_buffer(output_file)
445
            hunk_size1 -= 1
446
            hunk_size2 -= 1
447
            buf.append((l[1:], l[1:]))
448
            continue
449
450
        empty_buffer(output_file)
451
        add_comment(l, output_file)
452
453
    empty_buffer(output_file)
454
    output_file.write(table_footer.encode(encoding))
455
    if not exclude_headers:
456
        output_file.write(html_footer.format("", dtnow.strftime("%d.%m.%Y")).encode(encoding))
457
458
459
def usage():
460
    print('''
461
diff2html.py [-e encoding] [-i file] [-o file] [-x]
462
diff2html.py -h
463
464
Transform a unified diff from stdin to a colored side-by-side HTML
465
page on stdout.
466
stdout may not work with UTF-8, instead use -o option.
467
468
   -i file     set input file, else use stdin
469
   -e encoding set file encoding (default utf-8)
470
   -o file     set output file, else use stdout
471
   -x          exclude html header and footer
472
   -t tabsize  set tab size (default 8)
473
   -l linesize set maximum line size is there is no word break (default 20)
474
   -r          show \\r characters
475
   -k          show hunk infos
476
   -a algo     line diff algorithm (0: linediff characters, 1: word, 2: simplediff characters) (default 0)
477
   -h          show help and exit
478
''')
479
480
def main():
481
    global linesize, tabsize
482
    global show_CR
483
    global encoding
484
    global algorithm
485
486
    input_file_name = ''
487
    output_file_name = ''
488
489
    exclude_headers = False
490
    show_hunk_infos = False
491
492
    try:
493
        opts, args = getopt.getopt(sys.argv[1:], "he:i:o:xt:l:rka:",
494
                                   ["help", "encoding=", "input=", "output=",
495
                                    "exclude-html-headers", "tabsize=",
496
                                    "linesize=", "show-cr", "show-hunk-infos", "algorithm="])
497
    except getopt.GetoptError as err:
498
        print((str(err))) # will print something like "option -a not recognized"
499
        usage()
500
        sys.exit(2)
501
    verbose = False
502
    for o, a in opts:
503
        if o in ("-h", "--help"):
504
            usage()
505
            sys.exit()
506
        elif o in ("-e", "--encoding"):
507
            encoding = a
508
        elif o in ("-i", "--input"):
509
            input_file = codecs.open(a, "r", encoding)
510
            input_file_name = a
511
        elif o in ("-o", "--output"):
512
            output_file = codecs.open(a, "w")
513
            output_file_name = a
514
        elif o in ("-x", "--exclude-html-headers"):
515
            exclude_headers = True
516
        elif o in ("-t", "--tabsize"):
517
            tabsize = int(a)
518
        elif o in ("-l", "--linesize"):
519
            linesize = int(a)
520
        elif o in ("-r", "--show-cr"):
521
            show_CR = True
522
        elif o in ("-k", "--show-hunk-infos"):
523
            show_hunk_infos = True
524
        elif o in ("-a", "--algorithm"):
525
            algorithm = int(a)
526
        else:
527
            assert False, "unhandled option"
528
529
    # Use stdin if not input file is set
530
    if not ('input_file' in locals()):
531
        input_file = codecs.getreader(encoding)(sys.stdin)
532
533
    # Use stdout if not output file is set
534
    if not ('output_file' in locals()):
535
        output_file = codecs.getwriter(encoding)(sys.stdout)
536
537
    parse_input(input_file, output_file, input_file_name, output_file_name,
538
                exclude_headers, show_hunk_infos)
539
540
def parse_from_memory(txt, exclude_headers, show_hunk_infos):
541
    " Parses diff from memory and returns a string with html "
542
    input_stream = io.StringIO(txt)
543
    output_stream = io.StringIO()
544
    parse_input(input_stream, output_stream, '', '', exclude_headers, show_hunk_infos)
545
    return output_stream.getvalue()
546
547
548
if __name__ == "__main__":
549
    main()
550