#!/usr/bin/env python # Copyright (C) 2002 Dekel Tsur # # 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" \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{} 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"(? 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"(?] Show the differences between two latex/lyx files. ldiff to compare two files. ldiff to compare with the most recent version checked into CVS. ldiff -r to compare with revision of . ldiff -r -r to compare revision with revision . 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"(?