#!/usr/bin/env python
# Copyright (C) 2002 Dekel Tsur <
[email protected]>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# WARNING:
# Ldiff might have unwanted effect on the current direcory.
# It might be wise to backup your files before running it.
version = "0.5"
import getopt,os,sys,re,string,difflib
add_begin = "\\changestart"
add_end = "\\changeend"
del_begin = "\\overstrikeon"
del_end = "\\overstrikeoff"
###########################################################################
def half(L):
return [L[2*i] for i in xrange((len(L)+1)/2)]
def wdiff(text1, text2, show_deleted):
text1 = re.split(r"([\s~]+)", text1)
text2 = re.split(r"([\s~]+)", text2)
text1b = half(text1)
text2b = half(text2)
L = difflib.SequenceMatcher(None, text1b, text2b).get_opcodes()
L = filter(lambda x:x[0] != 'equal', L)
# Merge two adjacent changed blocks if the common block between them is small
i = 0
while i < len(L)-1:
x = L[i]
y = L[i+1]
if (x[0] == 'replace' or y[0] == 'replace') and y[1] <= x[2]+2 and \
(x[2]-x[1]+y[2]-y[1] > y[1]-x[2] or x[4]-x[3]+y[4]-y[3] > y[3]-x[4]):
L[i] = ('replace', x[1], y[2], x[3], y[4])
del L[i+1]
else:
i += 1
for x in L:
type = x[0]
y = map(lambda a:2*a, x[1:])
if type != 'insert' and show_deleted:
deleted_text = string.join(text1[y[0]:y[1]-1])
# the -1 removes the space at the end of the deleted text
if deleted_text != '':
deleted_text = '%\n'+del_begin+'{}'+deleted_text+'%\n'+del_end+'{} '
else:
deleted_text = ""
if type != 'delete':
text2[y[2]] = deleted_text+'%\n'+add_begin+'{}'+text2[y[2]]
text2[y[3]-1] = '%\n'+add_end+'{}'+text2[y[3]-1]
else:
text2[y[2]] = deleted_text+text2[y[2]]
return string.join(text2, "")
###########################################################################
math_rexp = r"\$|\\\(|\\\)|\\\[|\\\]|\\(?:begin|end)\{(?:equation|eqnarray|align)\*?\}"
def system(command):
print "Running "+command
return os.system(command)
def read_file(file, revision):
if revision != "":
tmpname = "ldiff_tmp_" + file
if revision == "-1":
revision_flag = ""
else:
revision_flag = "-r"+revision
system("cvs diff %s -u %s | patch -R -o%s" % (revision_flag, file, tmpname))
lines = read_file2(tmpname)
os.remove(tmpname)
return lines
else:
return read_file2(file)
def read_file2(file):
if file[-3:] == "lyx":
# If the file is a lyx file, convert it to latex
lyx = os.getenv("LYX")
if lyx == "":
lyx = "lyx"
system(lyx+" -e latex " + file)
file2 = file[:-3]+"tex"
lines = read_file3(file2)
os.remove(file2)
return lines
else:
return read_file3(file)
def read_file3(file):
fh = open(file)
lines = fh.readlines()
fh.close()
return lines
def get_documentclass(lines):
for line in lines:
mo = re.search(r"\\document(class|style).*{(.*)}", line)
if mo:
return mo.group(2)
return "article"
def find_rexp(rexp, lines):
for i in xrange(len(lines)):
if re.search(rexp, lines[i]):
return i
return -1
def preprocess(lines, separate_title):
# Clean input files
# lines = lines of the input file
# separate_title = if True, then include the \title..\maketitle part in
# the second string
# It returns 3 strings: the first contains the preamble line,
# the second may contain the \title...\maketitle part (according to the
# value of separate_title), and the third contain the body of the file
preamble_end = find_rexp(r"\\begin{document}", lines)
preamble_text = string.join(lines[:preamble_end], "")
text_begin = preamble_end
title_text = ""
if separate_title:
title_end = find_rexp(r"\\maketitle", lines)
if title_end > preamble_end:
title_text = string.join(lines[preamble_end:title_end], "")
text_begin = title_end
text = ""
for i in xrange(text_begin, len(lines)):
# Remove comments
line = re.sub(r"(?<!\\)%.*", r"%", lines[i])
# Put %\n before commands
line = re.sub(r"(\\(\w+){)", "%\n\\1", line)
# Put %\n after \emph{
line = re.sub(r"(\\(emph|textbf){)", "\\1%\n", line)
text = text + line
x = re.split("("+math_rexp+")", text)
math_mode = 0
for i in xrange(len(x)):
y = x[i]
if i % 2:
math_mode = not math_mode
if math_mode:
x[i] = x[i]+" "
else:
x[i] = " "+x[i]
elif math_mode:
# Replace x^a by x^{a}.
# This should give smaller diffs if x^a is replaced by x^b
x[i] = re.sub(r"(?<!\\)([_^])(\\\w+|[^{}\\])", r"\1{\2}", x[i])
##x[i] = re.sub(r"(\\\w+|[^{}\\])([_^])", r"{\1}\2", x[i])
# Add some space in order to reduce the diff
# We try to add spaces in "safe" positions, but some spaces are
# added in unwanted places and then removed later
x[i] = re.sub(r"(?<!\\)([=<>+-,()}\\])", r" \1", x[i])
x[i] = re.sub(r"(?<!\\)([=<>+-,(){}])", r"\1 ", x[i])
# Remove some of the spaces that were added above
x[i] = re.sub(r"\\(begin|end|label|ref|cite|text|textrm|mbox){ (\w+) }", r"\\\1{\2}", x[i])
x[i] = re.sub(r"} {", r"}{", x[i])
else:
# put space between \begin{<env>} and the optional argument
x[i] = re.sub(r"(\\begin{\w+})\[", r"\1 [", x[i])
return preamble_text, title_text, string.join(x, "")
invert_array = {"\(":"\)", "\)":"\(","\[":"\]", "\[":"\]" }
def invert_command(command):
if invert_array.has_key(command):
return invert_array[command]
elif command[:6] == "\\begin":
return "\\end"+command[6:]
elif command[:4] == "\\end":
return "\\begin"+command[4:]
else:
return command
def postprocess(text):
# Try to change the code to prevent latex errors
x = re.split("("+math_rexp+"|\\"+del_begin+"\\{\\}|\\"+del_end+"\\{\\})", text)
math_mode = 0
math_mode_save = 0
math_mode_diff = 0
delete_mode = 0
delete_mode_start = 0
delete_math_balance = 0
last_math_command = ""
for i in xrange(len(x)):
if i % 2:
# x[i] is either empty, mathmode start/end command,
# or delete block start/end command
if x[i] == del_begin+"{}":
delete_mode = 1
delete_mode_start = i
delete_math_balance = 0
math_mode_save = math_mode
elif x[i] == del_end+"{}":
delete_mode = 0
if math_mode_save != math_mode:
# We need to make sure that the mode at the end of the deleted
# block is the same as the beginning
x[i] = "{}"+invert_command(last_math_command)+x[i]
math_mode = math_mode_save
elif x[delete_mode_start+1:i] == [""]*(i-delete_mode_start-1):
# There is nothing in the deleted block, so remove the
# delete block start & end commands
x[delete_mode_start] = x[i] = ""
elif x[i] != "": # math start/end
if math_mode and delete_mode and ( \
math_mode_diff != 0 or \
invert_command(x[i]) != last_math_command ):
# If we exit from math mode inside a deleted block, and it is
# not "safe", (namely the math block doesn't have balanced
# brackets, or the command used to exit math mode does
# not match the command in which the math block begins)
# then use mbox to go into text mode
x[i] = ""
x[i+1] = "\mbox{"+x[i+1]+"}"
if i+2 < len(x) and x[i+2] != del_end+"{}":
x[i+2] = ""
elif x[i+1:i+3] == ["", del_end+"{}"] and \
math_mode_save == math_mode:
# The deleted block is about to end, and
# the current math command will cause a mismatch of modes
x[i] = ""
else:
math_mode = not math_mode
last_math_command = x[i]
math_mode_diff = 0
else:
# check balance of brackets
diff = len(re.findall(r"(?<!\\){", x[i])) - \
len(re.findall(r"(?<!\\)}", x[i]))
if delete_mode:
# Remove labels in deleted blocks as they may appear
# in changed block
x[i] = re.sub(r"\\(label)\{.*?\}", "", x[i])
if diff > 0:
# If the number of '{' is greater than the number of '}'
# add diff closing brackets at the end
x[i] += "}"*diff
elif diff < 0:
# If the number of '}' is greater than the number of '{'
# remove the first -diff brackets
x[i] = re.sub(r"(?<!\\)}", "", x[i], -diff)
elif math_mode:
math_mode_diff += diff
return string.join(x, "")
def usage():
print """Usage: ldiff [options] [<file1>] <file2>
Show the differences between two latex/lyx files.
ldiff <file1> <file2> to compare two files.
ldiff <file> to compare <file> with the most recent version checked into CVS.
ldiff -r<rev> <file> to compare <file> with revision <rev> of <file>.
ldiff -r<rev1> -r<rev2> <file> to compare revision <rev1> with revision <rev2>.
Options:
-h, --help This information
-v, --version Output version information
-b, --nocolor Do not colorize the changed text
-d, --nodeleted Don't show deleted text
-t, --notitle Don't show differences in the title
-l, --latex Produce only the latex file
-p, --nodvipost Don't use dvipost
-s, --separation Separation between change bars and text
(default value = -50)
"""
_options = ["help", "version", "nocolor", "nodeleted", "notitle", "latex",\
"nodvipost", "separation="]
try:
opts, args = getopt.getopt(sys.argv[1:], "hvbdtlps:r:", _options)
except getopt.error:
usage()
sys.exit(1)
rev_list = []
deleted = 1
colorize = 1
onlylatex = 0
notitle = 0
dvipost = 1
sep = "-50"
for o, a in opts:
if o in ("-h", "--help"):
usage()
sys.exit()
if o in ("-v", "--version"):
print "ldiff, version "+version
sys.exit()
if o in ("-d", "--nodeleted"):
deleted = 0
if o in ("-b", "--nocolor"):
colorize = 0
if o in ("-t", "--notitle"):
notitle = 1
if o in ("-l", "--latex"):
onlylatex = 1
if o in ("-p", "--nodvipost"):
dvipost = 0
if o in ("-s", "--separation"):
sep = a
if o == "-r":
rev_list.append(a)
if len(args) == 2:
if rev_list != []:
usage()
sys.exit(1)
text1 = read_file(args[0], "")
text2 = read_file(args[1], "")
filebase = args[1][:-4]+"-diff"
elif len(args) == 1:
if len(rev_list) == 0:
rev_list = ["-1", ""]
elif len(rev_list) == 1:
rev_list += [""]
text1 = read_file(args[0], rev_list[0])
text2 = read_file(args[0], rev_list[1])
filebase = args[0][:-4]+"-diff"
else:
usage()
sys.exit()
if get_documentclass(text1) != get_documentclass(text2):
notitle = 1
preamble1, title1, text1 = preprocess(text1, notitle)
preamble2, title2, text2 = preprocess(text2, notitle)
lines = string.split(wdiff(text1, text2, deleted), "\n")
filetex = filebase+".tex"
fh = open(filetex, 'w')
fh.write(preamble2)
if dvipost:
fh.write(r"""
\usepackage{dvipost}
\dvipost{cbexp=0pt}
\dvipost{cbsep=%spt}
""" % sep)
if colorize:
fh.write(r"""
\dvipost{cbstart color push Blue}
\dvipost{cbend color pop}
\dvipost{osstart color push Red}
\dvipost{osend color pop}
""")
else:
fh.write(r"""
\newcommand{%s}{\special{color push Blue}}
\newcommand{%s}{\special{color push Black}}
\newcommand{%s}{\special{color push Red}}
\newcommand{%s}{\special{color push Black}}
""" % (add_begin,add_end,del_begin,del_end))
fh.write(r"""
\makeatletter
\let\ldiff@old@maketitle=\maketitle
\let\ldiff@old@thanks=\thanks
\let\ldiff@old@footnote=\footnote
\let\ldiff@old@endfigure=\endfigure
\let\ldiff@old@endtable=\endtable
\def\maketitle{\ldiff@old@maketitle%s%s}
\def\thanks#1{\ldiff@old@thanks{#1%s}%s}
\long\def\footnote#1{\ldiff@old@footnote{#1%s}%s}
\def\endfigure{%s\ldiff@old@endfigure%s}
\def\endtable{%s\ldiff@old@endtable%s}
\makeatother
""" % (( (del_end+add_end)*2+"{}",)*10) )
fh.write(title2)
text = ""
for line in lines:
line = re.sub(r"(?<!\\)%(.*)"+'\\'+del_end, r"\1"+del_end, line)
line = re.sub(r"(?<!\\)%(.*)", r"\1%", line)
text = text+line+"\n"
if deleted:
text = postprocess(text)
fh.write(text)
fh.close()
if onlylatex:
sys.exit()
filedvi = filebase+".dvi"
fileps = filebase+".ps"
latex_command = "latex --interaction=batchmode "
os.system(latex_command+filetex)
os.system("bibtex "+filebase)
os.system(latex_command+filetex)
os.system(latex_command+filetex)
if dvipost:
os.system("dvipost %s %s" % (filedvi, filedvi))
os.system("dvips %s -o %s" % (filedvi, fileps))
print "\nLatex Warnings:"
os.system("grep Warning "+filebase+".log")
print "\nLatex Errors:"
os.system("grep ^! "+filebase+".log")
os.system("rm %s.{aux,bbl,blg,dvi,log}" % filebase)