diff options
author | Karl Berry <karl@freefriends.org> | 2009-12-13 01:30:42 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2009-12-13 01:30:42 +0000 |
commit | a489176691d69116ddff58a4021dc22998629d8e (patch) | |
tree | 4904fd261043d5d948d62669ebb54f8f75cb1c73 /Master/texmf-dist/scripts/latexdiff | |
parent | 55680297935c477d4a7ac7725002fd0c15f49f7e (diff) |
install new script latexdiff
git-svn-id: svn://tug.org/texlive/trunk@16379 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/latexdiff')
-rwxr-xr-x | Master/texmf-dist/scripts/latexdiff/latexdiff-vc | 430 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/latexdiff/latexdiff.pl | 3240 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/latexdiff/latexrevise | 523 |
3 files changed, 4193 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/latexdiff/latexdiff-vc b/Master/texmf-dist/scripts/latexdiff/latexdiff-vc new file mode 100755 index 00000000000..3e9a3f246de --- /dev/null +++ b/Master/texmf-dist/scripts/latexdiff/latexdiff-vc @@ -0,0 +1,430 @@ +#!/usr/bin/perl -w +# Place the following 2 lines at beginning to make script independent of location of perl +#eval 'exec perl -w -S $0 ${1+"$@"}' +# if 0; # not running under some shell +# +# latexdiff-vc - wrapper script for applying latexdiff to rcs managed files +# and for automatised creation of postscript or pdf from difference file +# +# Copyright (C) 2005 F J Tilmann (tilmann@esc.cam.ac.uk) +# +# Contributors: S Utcke, H Bruyninckx +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License Version 2 as published by +# the Free Software Foundation. +# +# 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 +# +# Detailed usage information at the end of the file +# +# version 0.25: +# - bbl is allowed as alternative extension (instead of .tex) +# +use Getopt::Long ; +use Pod::Usage qw/pod2usage/ ; +use File::Temp qw/tempdir/ ; +use File::Basename qw/dirname/; +use strict ; + +# Option names +my ($version,$help,$postscript,$pdf,$force,$dir,$cvs,$rcs,$svn,$diffcmd,$patchcmd,@revs); +# Preset Variables +my $latexdiff="latexdiff"; # Program for making the comparison +my $vc=""; +my $tempdir=tempdir(CLEANUP => 1); +# Variables +my ($file1,$file2,$diff,$diffbase,$answer,$options,$infile,$append,$dirname,$cwd); +my (@files,@ldoptions,@tmpfiles,@ptmpfiles,@difffiles); # , + + +Getopt::Long::Configure('pass_through','bundling'); + +GetOptions('revision|r:s' => \@revs, + 'cvs' => \$cvs, + 'rcs' => \$rcs, + 'svn' => \$svn, + 'dir|d:s' => \$dir, + 'postscript|ps' => \$postscript, + 'pdf' => \$pdf, + 'force' => \$force, + 'version' => \$version, + 'help|h' => \$help); + +if ( $help ) { + pod2usage(1) ; +} + +if ( $version ) { + die "This is LATEXDIFF-VC 0.25\n" ; +} + +if ( $cvs ) { + $vc="CVS"; +} +if ( $rcs ) { + die "Cannot specify more than one of --cvs, --rcs or --svn." if ($vc); + $vc="RCS"; +} +if ( $svn ) { + die "Cannot specify more than one of --cvs, --rcs or --svn." if ($vc); + $vc="SVN"; +} + +# check whether the first file name got misinterpreted as an option to an empty -r option +if ( @revs && -f $revs[$#revs] ) { + unshift @ARGV,$revs[$#revs]; + $revs[$#revs]=""; +} +# check whether the first file name got misinterpreted as an option to an empty -d option +if ( defined($dir) && -f $dir ) { + unshift @ARGV,$dir; + $dir=""; +} + +$file2=pop @ARGV; +( defined($file2) && $file2 =~ /\.(tex|bbl)$/ ) or pod2usage("Must specify at least one tex or bbl file"); + +if (! $vc && scalar(@revs)>0 ) { + # have to guess $vc + # check whether we have a special name + if ( $0 =~ /-cvs$/ ) { + $vc="CVS"; + } elsif ( $0 =~ /-rcs$/ ) { + $vc="RCS"; + } elsif ( $0 =~ /-svn$/ ) { + $vc="SVN"; + } elsif ( -e "CVSROOT" || defined($ENV{"CVSROOT"}) ) { + print STDERR "Guess you are using CVS ...\n"; + $vc="CVS"; + } elsif ( -e "$file2,v" ) { + print STDERR "Guess you are using RCS ...\n"; + $vc="RCS"; + } elsif ( -d ".svn" ) { + print STDERR "Guess you are using SVN ...\n"; + $vc="SVN"; + } else { + print STDERR "Cannot figure out version control system, so I default to CVS\n"; + $vc="CVS"; + } +} + +if (defined($dir) && $dir=~/^\.\/?/ ) { + print STDERR "You wrote selected -dir=. but you do not really like to do that, do you ?\n"; + exit 10 +} + +if ( scalar(@revs)>0 ) { + if ( $vc eq "CVS" ) { + $diffcmd = "cvs diff -u -r"; + $patchcmd = "patch -R -p0"; + } elsif ( $vc eq "RCS" ) { + $diffcmd = "rcsdiff -u -r"; + $patchcmd = "patch -R -p0"; + } elsif ( $vc eq "SVN" ) { + $diffcmd = "svn diff -r "; + $patchcmd = "patch -R -p0"; + } else { + print STDERR "Unknown versioning system $vc \n"; + exit 10; + } +} + + +# make file list (last arguments), initial arguments have to be passed to latexdiff +# We assume an argument is a valid file rather than a latexdiff argument +# if it has extension .tex or .bbl + +@files=($file2); +while( $file1=pop @ARGV ) { + if ( $file1 =~ /\.(tex|bbl)$/ ) { + # $file1 looks like a valid file name and is prepended to file list + unshift @files, $file1 ; + } else { + # $file1 looks like an option for latexdiff, push it back to argument stack + unshift @ldoptions, $file1 ; + } +} + +if ( scalar(@revs) == 0 ) { + pod2usage("When -r option is not used, two .tex files (old and new) must be given on the command line") unless @files==2; + # compare two files + $file1=shift @files ; +} + +if ( scalar(@revs) == 2 ) { + $append = "-diff$revs[0]-$revs[1]"; +} elsif ( scalar(@revs) == 1 || $revs[0] ) { + $append = "-diff$revs[0]"; +} else { + $append = "-diff"; +} + +if ( defined ($dir) && ! $dir ) { + # bare -d option => choose directory name + ($dir=$append) =~ s/^-//; +} + +if ( ($vc eq "SVN" || $vc eq "CVS") && scalar(@revs)) { + length($revs[$#revs]) > 0 or $revs[$#revs]="HEAD"; + length($revs[0]) > 0 or $revs[0]="HEAD"; +} + +#exit ; + + +# cycle through all files + +@difffiles=(); +while ( $infile=$file2=shift @files ) { + print STDERR "Working on $infile \n"; + if ( scalar(@revs) == 1 ) { + ($file1=$infile) =~ s/\.(tex|bbl)/-oldtmp-$$.$1/ ; + push @tmpfiles,$file1; + # compare file with previous version ($revs[0]="") or specified version + system("$diffcmd$revs[0] $infile| $patchcmd -o$file1") ; + } elsif ( scalar(@revs) == 2 ) { + ($file1=$infile) =~ s/\.(tex|bbl)/-oldtmp-$$.$1/ ; + $file2 =~ s/\.(tex|bbl)/-newtmp-$$.$1/ ; + push @tmpfiles,$file2; + system("$diffcmd$revs[1] $infile | $patchcmd -o$file2") ; + system("$diffcmd$revs[0] $infile | $patchcmd -o$file1") ; + } + + if ( -z $file1 || -z $file2) { + print STDERR "One or both of the files to compare are empty. Possibly something went wrong in the retrieval of older versions. Aborting ...\n" ; + exit(10); + } + + # Get name of difference file + if ( defined($dir) ) { + $diff="$dir/$infile" ; + } else { + ($diff=$infile) =~ s/\.(tex|bbl)$/$append.$1/ ; + } + # make directories if needed + $dirname=dirname($diff) ; + system("mkdir -p $dirname") unless ( -e $dirname ); + + # Remaining options are passed to latexdiff + $options = join(" ",@ldoptions); + + if ( -e $diff && ! $force ) { + print STDERR "OK to overwrite existing file $diff (y/n)? "; + $answer = <STDIN> ; + unless ($answer =~ /^y/i ) { + unlink @tmpfiles; + die "Abort ... " ; + } + } + print "Running $latexdiff\n"; + unless ( system("$latexdiff $options $file1 $file2 > $diff") == 0 ) { + print STDERR "Something went wrong in $latexdiff. Deleting $diff and abort\n" ; unlink $diff ; exit(5) + }; + print "Generated difference file $diff\n"; + + if ( ( $postscript or $pdf ) and !( scalar(@revs) && greptex( qr/\\document(?:class|style)/ , $diff ) ) ) { + # save filename for later processing if postscript or pdf conversion is requested and either two-file mode was used (scalar(@revs)==0) or the diff file contains documentclass statement (ie. is a root document) + push @difffiles, $diff ; + } + + unlink @tmpfiles; +} + +foreach $diff ( @difffiles ) { + chomp($cwd=(`pwd`)); + if (defined($dir)) { + ( $diff =~ s/$dir\/?// ) ; + chdir $dir ; + } + @ptmpfiles=(); + ( $diffbase=$diff) =~ s/\.(tex)$// ; + + # adapt magically changebar styles to [pdftex] display driver if pdf output was selected + if ( $pdf ) { + system("sed \"s/Package\\[dvips\\]/Package[pdftex]/\" $diff > $diff.tmp$$ ; \\mv $diff.tmp$$ $diff"); + } + print STDERR "PDF: $pdf Postscript: $postscript cwd $cwd\n"; + + if ( system("grep -q \'^[^%]*\\\\bibliography\' $diff") == 0 ) { + if ( $postscript) { + system("latex --interaction=batchmode $diff; bibtex $diffbase"); + push @ptmpfiles, "$diffbase.bbl","$diffbase.bbl" ; + } elsif ( $pdf ) { + system("pdflatex --interaction=batchmode $diff; bibtex $diffbase"); + push @ptmpfiles, "$diffbase.bbl","$diffbase.bbl" ; + } + } + + if ( $postscript ) { + my $dvi="$diffbase.dvi"; + my $ps="$diffbase.ps"; + + system("latex --interaction=batchmode $diff; latex $diff; dvips -o $ps $dvi"); + push @ptmpfiles, "$diffbase.aux","$diffbase.log",$dvi ; + print "Generated postscript file $ps\n"; + } + elsif ( $pdf ) { + system("pdflatex --interaction=batchmode $diff; pdflatex $diff"); + push @ptmpfiles, "$diffbase.aux","$diffbase.log"; + } + unlink @ptmpfiles; + chdir $cwd; +} + +# greptex returns 1 if regex is not matched in filename +# 0 if there is a match +sub greptex { + my ($regex,$filename)=@_; + my ($i)=0; + open (FH, $filename) or die("Couldn't open $filename: $!"); + while (<FH>) { + next if /^\s*%/; # skip comment lines + if ( m/$regex/ ) { + close(FH); + return(0); + } + # only scan 25 lines + $i++; + last if $i>25 ; + } + close(FH); + return(1); +} + + +=head1 NAME + +latexdiff-vc - wrapper script that calls latexdiff for different versions of a file under version management (CVS, RCS or SVN) + +=head1 SYNOPSIS + +B<latexdiff-vc> [ F<latexdiff-options> ] [ F<latexdiff-vc-options> ] B<-r> [F<rev1>] [B<-r> F<rev2>] F<file1.tex> [ F<file2.tex> ...] + + or + +B<latexdiff-vc> [ F<latexdiff-options> ] [ F<latexdiff-vc-options> ][ B<--postscript> | B<--pdf> ] F<old.tex> F<new.tex> + +=head1 DESCRIPTION + +I<latexdiff-vc> is a wrapper script that applies I<latexdiff> to a +file, or multiple files under version control (CVS or RCS), and optionally runs the +sequence of C<latex> and C<dvips> or C<pdflatex> commands necessary to +produce pdf or postscript output of the difference tex file(s). It can +also be applied to a pair of files to automatise the generation of difference +file in postscript or pdf format. + +=head1 OPTIONS + +=over 4 + +=item B<--rcs>, B<--svn>, or B<--cvs> + +Set the version system. +If no version system is specified, latexdiff-vc will venture a guess. + +latexdiff-cvs and latexdiff-rcs are variants of latexdiff-vc which default to +the respective versioning system. However, this default can still be overridden using the options above. + +=item B<-r>, B<-r> F<rev> or B<--revision>, B<--revision=>F<rev> + +Choose revision (under RCS, CVS or SVN). One or two B<-r> options can be +specified, and the resulting in different behaviour: + +=over 4 + +=item B<latexdiff-vc> -r F<file.tex> ... + +compares F<file.tex> with the most recent version checked into RCS. + +=item B<latexdiff-vc> -r F<rev1> F<file.tex> ... + +compares F<file.tex> with revision F<rev1>. + +=item B<latexdiff-vc> -r F<rev1> -r F<rev2> F<file.tex> ... + +compares revisions F<rev1> and F<rev2> of F<file.tex>. + +Multiple files can be specified for all of the above options. All files must have the +extension C<.tex>, though. + +=item B<latexdiff-vc> F<old.tex> F<new.tex> + +compares two files. + +=back + +The name of the difference file is generated automatically and +reported to stdout. + +=item B<-d> or B<--dir> B<-d> F<path> or B<--dir=>F<path> + +Rather than appending the string C<diff> and optionally the version +numbers given to the output-file, this will prepend a directory name C<diff> +to the +original filename, creating the directory and subdirectories should they not exist already. This is particularly useful in order to clone a +complete directory hierarchy. Optionally, a pathname F<path> can be specified, which is prepended instead of C<diff>. + +=item B<--ps> or B<--postscript> + +Generate postscript output from difference file. This will run the +sequence C<latex; latex; dvips> on the difference file (do not use +this option in the rare cases, where three C<latex> commands are +required if you care about correct referencing). If the difference +file contains a C<\bibliography> tag, run the sequence C<latex; +bibtex; latex; latex; dvips>. + +=item B<--pdf> + +Generate pdf output from difference file using C<pdflatex>. This will +run the sequence C<pdflatex; pdflatex> on the difference file, or +C<pdflatex; bibtex; pdflatex; pdflatex> for files requiring bibtex. + +=item B<--force> + +Overwrite existing diff files without asking for confirmation. Default +behaviour is to ask for confirmation before overwriting an existing difference +file. + +=item B<--help> or +B<-h> + +Show help text + +=item B<--version> + +Show version number + +=back + +All other options are passed on to C<latexdiff>. + +=head1 SEE ALSO + +L<latexdiff> + +=head1 PORTABILITY + +I<latexdiff-vc> uses external commands and is therefore +limited to Unix-like systems. It also requires the RCS version control +system and latex to be installed on the system. Modules from Perl 5.8 +or higher are required. + + +=head1 AUTHOR + +Copyright (C) 2005 Frederik Tilmann + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License Version 2 +Contributors: S Utcke, H Bruyninckx + +=cut + diff --git a/Master/texmf-dist/scripts/latexdiff/latexdiff.pl b/Master/texmf-dist/scripts/latexdiff/latexdiff.pl new file mode 100755 index 00000000000..03c70c3ac4e --- /dev/null +++ b/Master/texmf-dist/scripts/latexdiff/latexdiff.pl @@ -0,0 +1,3240 @@ +#!/usr/bin/perl -w +# latexdiff - differences two latex files on the word level +# and produces a latex file with the differences marked up. +# +# Copyright (C) 2004-2007 F J Tilmann (tilmann@esc.cam.ac.uk) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License Version 2 as published by +# the Free Software Foundation. +# +# 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 +# +# Detailed usage information at the end of the file +# +# Version 0.5 A number of minor improvements based on feedback +# Deleted blocks are now shown before added blocks +# Package specific processing +# +# Version 0.43 unreleased typo in list of styles at the end +# Add protect to all \cbstart, \cbend commands +# More robust substitution of deleted math commands +# +# Version 0.42 November 06 Bug fixes only +# +# Version 0.4 March 06 option for fast differencing using UNIX diff command, several minor bug fixes (\par bug, improved highlighting of textcmds) +# +# Version 0.3 August 05 improved parsing of displayed math, --allow-spaces +# option, several minor bug fixes +# +# Version 0.25 October 04 Fix bug with deleted equations, add math mode commands to safecmd, add | to allowed interpunctuation signs +# Version 0.2 September 04 extension to utf-8 and variable encodings +# Version 0.1 August 04 First public release + +# Inserted block for stand-alone version replaces +# use Algorithm::Diff qw(traverse_sequences); +# in standard version +# The following BEGIN block contains a verbatim copy of +# Ned Konz' Algorithm::Diff package version 1.15 except +# that all POD documentation has been stripped out. +# I encourage you to download and install the Algorithm::Diff +# package and use the standard latexdiff version instead +# (current distribution available from http://search.cpan.org/~nedkonz/Algorithm-Diff-1.15 +# the most recent version can be found via http://search.cpan.org/search?module=Algorithm::Diff ) +# Please note that the LICENCSE for Algorithm::Diff : +# "Copyright (c) 2000-2002 Ned Konz. All rights reserved. +# This program is free software; +# you can redistribute it and/or modify it under the same terms +# as Perl itself." +# The stand-alone version of latexdiff is provided as a convenience +# for latex users with no knowledge of PERL who do not wish to install +# additional packages to be able to use latexdiff. If you believe +# the inlining of Algorithm::Diff violates its license please contact +# me and I will modify the latexdiff distribution accordingly. +# Frederik Tilmann (tilmann@esc.cam.ac.uk) +BEGIN { +package Algorithm::Diff; +use strict; +use vars qw($VERSION @EXPORT_OK @ISA @EXPORT); +use integer; # see below in _replaceNextLargerWith() for mod to make + # if you don't use this +require Exporter; +@ISA = qw(Exporter); +@EXPORT = qw(); +@EXPORT_OK = qw(LCS diff traverse_sequences traverse_balanced sdiff); +$VERSION = sprintf('%d.%02d so', (q$Revision: 1.15 $ =~ /\d+/g)); + +# McIlroy-Hunt diff algorithm +# Adapted from the Smalltalk code of Mario I. Wolczko, <mario@wolczko.com> +# by Ned Konz, perl@bike-nomad.com + + +# Create a hash that maps each element of $aCollection to the set of positions +# it occupies in $aCollection, restricted to the elements within the range of +# indexes specified by $start and $end. +# The fourth parameter is a subroutine reference that will be called to +# generate a string to use as a key. +# Additional parameters, if any, will be passed to this subroutine. +# +# my $hashRef = _withPositionsOfInInterval( \@array, $start, $end, $keyGen ); + +sub _withPositionsOfInInterval +{ + my $aCollection = shift; # array ref + my $start = shift; + my $end = shift; + my $keyGen = shift; + my %d; + my $index; + for ( $index = $start ; $index <= $end ; $index++ ) + { + my $element = $aCollection->[$index]; + my $key = &$keyGen( $element, @_ ); + if ( exists( $d{$key} ) ) + { + unshift ( @{ $d{$key} }, $index ); + } + else + { + $d{$key} = [$index]; + } + } + return wantarray ? %d : \%d; +} + +# Find the place at which aValue would normally be inserted into the array. If +# that place is already occupied by aValue, do nothing, and return undef. If +# the place does not exist (i.e., it is off the end of the array), add it to +# the end, otherwise replace the element at that point with aValue. +# It is assumed that the array's values are numeric. +# This is where the bulk (75%) of the time is spent in this module, so try to +# make it fast! + +sub _replaceNextLargerWith +{ + my ( $array, $aValue, $high ) = @_; + $high ||= $#$array; + + # off the end? + if ( $high == -1 || $aValue > $array->[-1] ) + { + push ( @$array, $aValue ); + return $high + 1; + } + + # binary search for insertion point... + my $low = 0; + my $index; + my $found; + while ( $low <= $high ) + { + $index = ( $high + $low ) / 2; + + # $index = int(( $high + $low ) / 2); # without 'use integer' + $found = $array->[$index]; + + if ( $aValue == $found ) + { + return undef; + } + elsif ( $aValue > $found ) + { + $low = $index + 1; + } + else + { + $high = $index - 1; + } + } + + # now insertion point is in $low. + $array->[$low] = $aValue; # overwrite next larger + return $low; +} + +# This method computes the longest common subsequence in $a and $b. + +# Result is array or ref, whose contents is such that +# $a->[ $i ] == $b->[ $result[ $i ] ] +# foreach $i in ( 0 .. $#result ) if $result[ $i ] is defined. + +# An additional argument may be passed; this is a hash or key generating +# function that should return a string that uniquely identifies the given +# element. It should be the case that if the key is the same, the elements +# will compare the same. If this parameter is undef or missing, the key +# will be the element as a string. + +# By default, comparisons will use "eq" and elements will be turned into keys +# using the default stringizing operator '""'. + +# Additional parameters, if any, will be passed to the key generation routine. + +sub _longestCommonSubsequence +{ + my $a = shift; # array ref + my $b = shift; # array ref + my $keyGen = shift; # code ref + my $compare; # code ref + + # set up code refs + # Note that these are optimized. + if ( !defined($keyGen) ) # optimize for strings + { + $keyGen = sub { $_[0] }; + $compare = sub { my ( $a, $b ) = @_; $a eq $b }; + } + else + { + $compare = sub { + my $a = shift; + my $b = shift; + &$keyGen( $a, @_ ) eq &$keyGen( $b, @_ ); + }; + } + + my ( $aStart, $aFinish, $bStart, $bFinish, $matchVector ) = + ( 0, $#$a, 0, $#$b, [] ); + + # First we prune off any common elements at the beginning + while ( $aStart <= $aFinish + and $bStart <= $bFinish + and &$compare( $a->[$aStart], $b->[$bStart], @_ ) ) + { + $matchVector->[ $aStart++ ] = $bStart++; + } + + # now the end + while ( $aStart <= $aFinish + and $bStart <= $bFinish + and &$compare( $a->[$aFinish], $b->[$bFinish], @_ ) ) + { + $matchVector->[ $aFinish-- ] = $bFinish--; + } + + # Now compute the equivalence classes of positions of elements + my $bMatches = + _withPositionsOfInInterval( $b, $bStart, $bFinish, $keyGen, @_ ); + my $thresh = []; + my $links = []; + + my ( $i, $ai, $j, $k ); + for ( $i = $aStart ; $i <= $aFinish ; $i++ ) + { + $ai = &$keyGen( $a->[$i], @_ ); + if ( exists( $bMatches->{$ai} ) ) + { + $k = 0; + for $j ( @{ $bMatches->{$ai} } ) + { + + # optimization: most of the time this will be true + if ( $k and $thresh->[$k] > $j and $thresh->[ $k - 1 ] < $j ) + { + $thresh->[$k] = $j; + } + else + { + $k = _replaceNextLargerWith( $thresh, $j, $k ); + } + + # oddly, it's faster to always test this (CPU cache?). + if ( defined($k) ) + { + $links->[$k] = + [ ( $k ? $links->[ $k - 1 ] : undef ), $i, $j ]; + } + } + } + } + + if (@$thresh) + { + for ( my $link = $links->[$#$thresh] ; $link ; $link = $link->[0] ) + { + $matchVector->[ $link->[1] ] = $link->[2]; + } + } + + return wantarray ? @$matchVector : $matchVector; +} + +sub traverse_sequences +{ + my $a = shift; # array ref + my $b = shift; # array ref + my $callbacks = shift || {}; + my $keyGen = shift; + my $matchCallback = $callbacks->{'MATCH'} || sub { }; + my $discardACallback = $callbacks->{'DISCARD_A'} || sub { }; + my $finishedACallback = $callbacks->{'A_FINISHED'}; + my $discardBCallback = $callbacks->{'DISCARD_B'} || sub { }; + my $finishedBCallback = $callbacks->{'B_FINISHED'}; + my $matchVector = _longestCommonSubsequence( $a, $b, $keyGen, @_ ); + + # Process all the lines in @$matchVector + my $lastA = $#$a; + my $lastB = $#$b; + my $bi = 0; + my $ai; + + for ( $ai = 0 ; $ai <= $#$matchVector ; $ai++ ) + { + my $bLine = $matchVector->[$ai]; + if ( defined($bLine) ) # matched + { + &$discardBCallback( $ai, $bi++, @_ ) while $bi < $bLine; + &$matchCallback( $ai, $bi++, @_ ); + } + else + { + &$discardACallback( $ai, $bi, @_ ); + } + } + + # The last entry (if any) processed was a match. + # $ai and $bi point just past the last matching lines in their sequences. + + while ( $ai <= $lastA or $bi <= $lastB ) + { + + # last A? + if ( $ai == $lastA + 1 and $bi <= $lastB ) + { + if ( defined($finishedACallback) ) + { + &$finishedACallback( $lastA, @_ ); + $finishedACallback = undef; + } + else + { + &$discardBCallback( $ai, $bi++, @_ ) while $bi <= $lastB; + } + } + + # last B? + if ( $bi == $lastB + 1 and $ai <= $lastA ) + { + if ( defined($finishedBCallback) ) + { + &$finishedBCallback( $lastB, @_ ); + $finishedBCallback = undef; + } + else + { + &$discardACallback( $ai++, $bi, @_ ) while $ai <= $lastA; + } + } + + &$discardACallback( $ai++, $bi, @_ ) if $ai <= $lastA; + &$discardBCallback( $ai, $bi++, @_ ) if $bi <= $lastB; + } + + return 1; +} + +sub traverse_balanced +{ + my $a = shift; # array ref + my $b = shift; # array ref + my $callbacks = shift || {}; + my $keyGen = shift; + my $matchCallback = $callbacks->{'MATCH'} || sub { }; + my $discardACallback = $callbacks->{'DISCARD_A'} || sub { }; + my $discardBCallback = $callbacks->{'DISCARD_B'} || sub { }; + my $changeCallback = $callbacks->{'CHANGE'}; + my $matchVector = _longestCommonSubsequence( $a, $b, $keyGen, @_ ); + + # Process all the lines in match vector + my $lastA = $#$a; + my $lastB = $#$b; + my $bi = 0; + my $ai = 0; + my $ma = -1; + my $mb; + + while (1) + { + + # Find next match indices $ma and $mb + do { $ma++ } while ( $ma <= $#$matchVector && !defined $matchVector->[$ma] ); + + last if $ma > $#$matchVector; # end of matchVector? + $mb = $matchVector->[$ma]; + + # Proceed with discard a/b or change events until + # next match + while ( $ai < $ma || $bi < $mb ) + { + + if ( $ai < $ma && $bi < $mb ) + { + + # Change + if ( defined $changeCallback ) + { + &$changeCallback( $ai++, $bi++, @_ ); + } + else + { + &$discardACallback( $ai++, $bi, @_ ); + &$discardBCallback( $ai, $bi++, @_ ); + } + } + elsif ( $ai < $ma ) + { + &$discardACallback( $ai++, $bi, @_ ); + } + else + { + + # $bi < $mb + &$discardBCallback( $ai, $bi++, @_ ); + } + } + + # Match + &$matchCallback( $ai++, $bi++, @_ ); + } + + while ( $ai <= $lastA || $bi <= $lastB ) + { + if ( $ai <= $lastA && $bi <= $lastB ) + { + + # Change + if ( defined $changeCallback ) + { + &$changeCallback( $ai++, $bi++, @_ ); + } + else + { + &$discardACallback( $ai++, $bi, @_ ); + &$discardBCallback( $ai, $bi++, @_ ); + } + } + elsif ( $ai <= $lastA ) + { + &$discardACallback( $ai++, $bi, @_ ); + } + else + { + + # $bi <= $lastB + &$discardBCallback( $ai, $bi++, @_ ); + } + } + + return 1; +} + +sub LCS +{ + my $a = shift; # array ref + my $matchVector = _longestCommonSubsequence( $a, @_ ); + my @retval; + my $i; + for ( $i = 0 ; $i <= $#$matchVector ; $i++ ) + { + if ( defined( $matchVector->[$i] ) ) + { + push ( @retval, $a->[$i] ); + } + } + return wantarray ? @retval : \@retval; +} + +sub diff +{ + my $a = shift; # array ref + my $b = shift; # array ref + my $retval = []; + my $hunk = []; + my $discard = sub { push ( @$hunk, [ '-', $_[0], $a->[ $_[0] ] ] ) }; + my $add = sub { push ( @$hunk, [ '+', $_[1], $b->[ $_[1] ] ] ) }; + my $match = sub { push ( @$retval, $hunk ) if scalar(@$hunk); $hunk = [] }; + traverse_sequences( $a, $b, + { MATCH => $match, DISCARD_A => $discard, DISCARD_B => $add }, @_ ); + &$match(); + return wantarray ? @$retval : $retval; +} + +sub sdiff +{ + my $a = shift; # array ref + my $b = shift; # array ref + my $retval = []; + my $discard = sub { push ( @$retval, [ '-', $a->[ $_[0] ], "" ] ) }; + my $add = sub { push ( @$retval, [ '+', "", $b->[ $_[1] ] ] ) }; + my $change = sub { + push ( @$retval, [ 'c', $a->[ $_[0] ], $b->[ $_[1] ] ] ); + }; + my $match = sub { + push ( @$retval, [ 'u', $a->[ $_[0] ], $b->[ $_[1] ] ] ); + }; + traverse_balanced( + $a, + $b, + { + MATCH => $match, + DISCARD_A => $discard, + DISCARD_B => $add, + CHANGE => $change, + }, + @_ + ); + return wantarray ? @$retval : $retval; +} + +1; +} +import Algorithm::Diff qw(traverse_sequences); +# End of inserted block for stand-alone version + +use Getopt::Long ; +use strict ; +use utf8 ; + +my ($algodiffversion)=split(/ /,$Algorithm::Diff::VERSION); + + +my ($versionstring)=<<EOF ; +This is LATEXDIFF 0.5 (Algorithm::Diff $Algorithm::Diff::VERSION) + (c) 2004-2007 F J Tilmann +EOF + +# Configuration variables: these have to be visible from the subroutines +my $MINWORDSBLOCK=3; # minimum number of tokens to form an independent block + # shorter identical blocks will be merged to the previous word +my $FLOATENV='(?:figure|table|plate)[\w\d*@]*' ; # Environments in which FL variants of defined commands are used +my $PICTUREENV='(?:picture|DIFnomarkup)[\w\d*@]*' ; # Environments in which all change markup is removed +my $MATHENV='(?:equation|displaymath|DOLLARDOLLAR)' ; # Environments turning on display math mode (code also knows about \[ and \] +my $MATHREPL='displaymath'; # Environment introducing deleted maths blocks +my $MATHARRENV='(?:eqnarray|align|gather|multiline|flalign)[*]?' ; # Environments turning on eqnarray math mode +my $MATHARRREPL='eqnarray*'; # Environment introducing deleted maths blocks +my $ARRENV='(?:array|[pbvBV]matrix)'; # Enviroments making arrays in math mode. The underlining style does not cope well with those - as a result in-text math environments are surrounded by \mbox{ } if any of these commands is used in an inline math block +my $COUNTERCMD='(?:footnote|part|section|subsection|subsubsection|paragraph|subparagraph)'; # textcmds which are associated with a counter + # If any of these commands occur in a deleted block + # they will be succeeded by an \addtocounter{...}{-1} + # for the associated counter such that the overall numbers + # should be the same as in the new file + +# Markup strings +# If at all possible, do not change these as parts of the program +# depend on the actual name (particularly post-processing) +# At the very least adapt subroutine postprocess to new tokens. +my $ADDMARKOPEN='\DIFaddbegin '; # Token to mark begin of appended text +my $ADDMARKCLOSE='\DIFaddend '; # Token to mark end of appended text +my $ADDOPEN='\DIFadd{'; # To mark begin of added text passage +my $ADDCLOSE='}'; # To mark end of added text passage +my $ADDCOMMENT='DIF > '; # To mark added comment line +my $DELMARKOPEN='\DIFdelbegin '; # Token to mark begin of deleted text +my $DELMARKCLOSE='\DIFdelend '; # Token to mark end of deleted text +my $DELOPEN='\DIFdel{'; # To mark begin of deleted text passage +my $DELCLOSE='}'; # To mark end of deleted text passage +my $DELCMDOPEN='%DIFDELCMD < '; # To mark begin of deleted commands (must begin with %, i.e., be a comment +my $DELCMDCLOSE="%%%\n"; # To mark end of deleted commands (must end with a new line) +my $AUXCMD='%DIFAUXCMD' ; # follows auxiliary commands put in by latexdiff to make difference file legal + # auxiliary commands must be on a line of their own +my $DELCOMMENT='DIF < '; # To mark deleted comment line + + +# main local variables: +my @TEXTCMDLIST=(); # array containing patterns of commands with text arguments +my @TEXTCMDEXCL=(); # array containing patterns of commands without text arguments (if a pattern + # matches both TEXTCMDLIST and TEXTCMDEXCL it is excluded) +my @CONTEXT1CMDLIST=(); # array containing patterns of commands with text arguments (subset of text commands), + # but which cause confusion if used out of context (e.g. \caption). + # In deleted passages, the command will be disabled but its argument is marked up + # Otherwise they behave exactly like TEXTCMD's +my @CONTEXT1CMDEXCL=(); # exclude list for above, but always empty +my @CONTEXT2CMDLIST=(); # array containing patterns of commands with text arguments, but which fail cause confusion + # if used out of context (e.g. \title). They and their arguments will be disabled in deleted + # passages +my @CONTEXT2CMDEXCL=(); # exclude list for above, but always empty + +my @SAFECMDLIST=(); # array containing patterns of safe commands (which do not break when in the argument of DIFadd or DIFDEL) +my @SAFECMDEXCL=(); + +my ($i,$j,$l); +my ($old,$new); +my ($line); +my ($newpreamble,$oldpreamble); +my (@newpreamble,@oldpreamble,@diffpreamble,@diffbody); +my ($latexdiffpreamble); +my ($oldbody, $newbody, $diffbo); +my ($oldpost, $newpost); +my ($diffall); +# Option names +my ($type,$subtype,$floattype,$config,$preamblefile,$encoding, + $showpreamble,$showsafe,$showtext,$showconfig,$showall, + $replacesafe,$appendsafe,$excludesafe, + $replacetext,$appendtext,$excludetext, + $replacecontext1,$appendcontext1, + $replacecontext2,$appendcontext2, + $help,$verbose,$version,$ignorewarnings,$allowspaces,$flatten,$debug); +my (@configlist, + @appendsafelist,@excludesafelist, + @appendtextlist,@excludetextlist, + @appendcontext1list,@appendcontext2list, + @packagelist); +my ($assign,@config); +# Hash where keys corresponds to the names of all included packages (including the documentclass as another package +# the optional arguments to the package are the values of the hash elements +my ($pkg,%packages); +# Defaults +$type='UNDERLINE'; +$subtype='SAFE'; +$floattype='FLOATSAFE'; + +$verbose=0; +$debug=1; +# define character properties +sub IsNonAsciiPunct { return <<'END' # Unicode punctuation but excluding ASCII punctuation ++utf8::IsPunct +-utf8::IsASCII +END +} +sub IsNonAsciiS { return <<'END' # Unicode symbol but excluding ASCII ++utf8::IsS +-utf8::IsASCII +END +} + + + + my %verbhash; + +Getopt::Long::Configure('bundling'); +GetOptions('type|t=s' => \$type, + 'subtype|s=s' => \$subtype, + 'floattype|f=s' => \$floattype, + 'config|c=s' => \@configlist, + 'preamble|p=s' => \$preamblefile, + 'encoding|e=s' => \$encoding, + 'exclude-safecmd|A=s' => \@excludesafelist, + 'replace-safecmd=s' => \$replacesafe, + 'append-safecmd|a=s' => \@appendsafelist, + 'exclude-textcmd|X=s' => \@excludetextlist, + 'replace-textcmd=s' => \$replacetext, + 'append-textcmd|x=s' => \@appendtextlist, + 'replace-context1cmd=s' => \$replacecontext1, + 'append-context1cmd=s' => \@appendcontext1list, + 'replace-context2cmd=s' => \$replacecontext2, + 'append-context2cmd=s' => \@appendcontext2list, + 'show-preamble' => \$showpreamble, + 'show-safecmd' => \$showsafe, + 'show-textcmd' => \$showtext, + 'show-config' => \$showconfig, + 'show-all' => \$showall, + 'packages=s' => \@packagelist, + 'verbose|V' => \$verbose, + 'ignore-warnings' => \$ignorewarnings, + 'allow-spaces' => \$allowspaces, + 'flatten' => \$flatten, + 'version' => \$version, + 'help|h|H' => \$help); + +if ( $help ) { + usage() ; +} + + +if ( $version ) { + die $versionstring ; +} + +print STDERR $versionstring if $verbose; + +if (defined($showall)){ + $showpreamble=$showsafe=$showtext=$showconfig=1; +} + +# setting extra preamble commands +if (defined($preamblefile)) { + $latexdiffpreamble=join "\n",(extrapream($preamblefile),""); +} else { + $latexdiffpreamble=join "\n",(extrapream($type,$subtype,$floattype),""); +} + +# setting up @SAFECMDLIST and @SAFECMDEXCL +if (defined($replacesafe)) { + init_regex_arr_ext(\@SAFECMDLIST,$replacesafe); +} else { + init_regex_arr_data(\@SAFECMDLIST, "SAFE COMMANDS"); +} +foreach $appendsafe ( @appendsafelist ) { + init_regex_arr_ext(\@SAFECMDLIST, $appendsafe); +} +foreach $excludesafe ( @excludesafelist ) { + init_regex_arr_ext(\@SAFECMDEXCL, $excludesafe); +} + +# Special: treat all cite commands as safe except in UNDERLINE and FONTSTRIKE mode +# (there is a conflict between citation and ulem package, see +# package documentation) +if ( uc($type) ne "UNDERLINE" && uc($type) ne "FONTSTRIKE" && uc($type) ne "CULINECHBAR" ) { + push (@SAFECMDLIST, qr/^cite.*$/); +} + +# setting up @TEXTCMDLIST and @TEXTCMDEXCL +if (defined($replacetext)) { + init_regex_arr_ext(\@TEXTCMDLIST,$replacetext); +} else { + init_regex_arr_data(\@TEXTCMDLIST, "TEXT COMMANDS"); +} +foreach $appendtext ( @appendtextlist ) { + init_regex_arr_ext(\@TEXTCMDLIST, $appendtext); +} +foreach $excludetext ( @excludetextlist ) { + init_regex_arr_ext(\@TEXTCMDEXCL, $excludetext); +} + + +# setting up @CONTEXT1CMDLIST ( @CONTEXT1CMDEXCL exist but is always empty ) +if (defined($replacecontext1)) { + init_regex_arr_ext(\@CONTEXT1CMDLIST,$replacecontext1); +} else { + init_regex_arr_data(\@CONTEXT1CMDLIST, "CONTEXT1 COMMANDS"); +} +foreach $appendcontext1 ( @appendcontext1list ) { + init_regex_arr_ext(\@CONTEXT1CMDLIST, $appendcontext1); +} + + +# setting up @CONTEXT2CMDLIST ( @CONTEXT2CMDEXCL exist but is always empty ) +if (defined($replacecontext2)) { + init_regex_arr_ext(\@CONTEXT2CMDLIST,$replacecontext2); +} else { + init_regex_arr_data(\@CONTEXT2CMDLIST, "CONTEXT2 COMMANDS"); +} +foreach $appendcontext2 ( @appendcontext2list ) { + init_regex_arr_ext(\@CONTEXT2CMDLIST, $appendcontext2); +} + + + + +# setting configuration variables +@config=(); +foreach $config ( @configlist ) { + if (-f $config ) { + open(FILE,$config) or die ("Couldn't open configuration file $config: $!"); + while (<FILE>) { + chomp; + next if /^\s*#/ || /^\s*%/ || /^\s*$/ ; + push (@config,$_); + } + close(FILE); + } + else { +# foreach ( split(",",$config) ) { +# push @config,$_; +# } + push @config,split(",",$config) + } +} +foreach $assign ( @config ) { + $assign=~ m/\s*(\w*)\s*=\s*(\S*)\s*$/ or die "Illegal assignment $assign in configuration list (must be variable=value)"; + if ( $1 eq "MINWORDSBLOCK" ) { $MINWORDSBLOCK = $2; } + elsif ( $1 eq "FLOATENV" ) { $FLOATENV = $2 ; } + elsif ( $1 eq "PICTUREENV" ) { $PICTUREENV = $2 ; } + elsif ( $1 eq "MATHENV" ) { $MATHENV = $2 ; } + elsif ( $1 eq "MATHREPL" ) { $MATHREPL = $2 ; } + elsif ( $1 eq "MATHARRENV" ) { $MATHARRENV = $2 ; } + elsif ( $1 eq "MATHARRREPL" ) { $MATHARRREPL = $2 ; } + elsif ( $1 eq "ARRENV" ) { $ARRENV = $2 ; } + elsif ( $1 eq "COUNTERCMD" ) { $COUNTERCMD = $2 ; } + else { die "Unknown variable $1 in assignment.";} +} + + +foreach $pkg ( @packagelist ) { + map { $packages{$_}="" } split(/,/,$pkg) ; +} + +if ($showpreamble) { + print "\nPreamble commands:\n"; + print $latexdiffpreamble ; +} + +if ($showsafe) { + print "\nCommands safe within scope of $ADDOPEN $ADDCLOSE and $DELOPEN $DELCLOSE (unless excluded):\n"; + print_regex_arr(@SAFECMDLIST); + print "\nCommands not safe within scope of $ADDOPEN $ADDCLOSE and $DELOPEN $DELCLOSE :\n"; + print_regex_arr(@SAFECMDEXCL); +} + +if ($showtext) { + print "\nCommands with last argument textual (unless excluded) and safe in every context:\n"; + print_regex_arr(@TEXTCMDLIST); + print "\nContext1 commands (last argument textual, command will be disabled in deleted passages, last argument will be shown as plain text):\n"; + print_regex_arr(@CONTEXT1CMDLIST); + print "\nContext2 commands (last argument textual, command ant its argument will be disabled in deleted passages):\n"; + print_regex_arr(@CONTEXT2CMDLIST); + print "\nExclude list of Commands with last argument not textual (overrides patterns above):\n"; + print_regex_arr(@TEXTCMDEXCL); +} + +if ($showconfig) { + print "Configuration variables:\n"; + print "MINWORDSBLOCK=$MINWORDSBLOCK\n"; + print "FLOATENV=$FLOATENV\n"; + print "PICTUREENV=$PICTUREENV\n"; + print "MATHENV=$MATHENV\n"; + print "MATHREPL=$MATHREPL\n"; + print "MATHARRENV=$MATHARRENV\n"; + print "MATHARRREPL=$MATHARRREPL\n"; + print "ARRENV=$ARRENV\n"; + print "COUNTERCMD=$COUNTERCMD\n"; +} +if ($showconfig || $showtext || $showsafe || $showpreamble) { + exit 0; } +if ( @ARGV != 2 ) { + print STDERR "2 and only 2 non-option arguments required. Write latexdiff -h to get help\n"; + exit(2); +} + +# Are extra spaces between command arguments permissible? +my $extraspace; +if ($allowspaces) { + $extraspace='\s*'; +} else { + $extraspace=''; +} + +# append context lists to text lists (as text property is implied) +push @TEXTCMDLIST, @CONTEXT1CMDLIST; +push @TEXTCMDLIST, @CONTEXT2CMDLIST; + + +# Patterns. These are used by some of the subroutines, too +# I can only define them down here because value of extraspace depends on an option + my $pat0 = '(?:[^{}]|\\\{|\\\})*'; + my $pat1 = '(?:[^{}]|\\\{|\\\}|\{'.$pat0.'\})*'; + my $pat2 = '(?:[^{}]|\\\{|\\\}|\{'.$pat1.'\})*'; # + my $pat3 = '(?:[^{}]|\\\{|\\\}|\{'.$pat2.'\})*'; + my $pat4 = '(?:[^{}]|\\\{|\\\}|\{'.$pat3.'\})*'; + my $brat0 = '(?:[^\[\]]|\\\[|\\\])*'; + + my $quotemarks = '(?:\'\')|(?:\`\`)'; + my $punct='[0.,\/\'\`:;\"\?\(\)\[\]!~\p{IsNonAsciiPunct}\p{IsNonAsciiS}]'; + my $number='-?\d*\.\d*'; + my $mathpunct='[+=<>\-\|]'; + my $and = '&'; + my $coords= '[\-.,\s\d]*'; +# word: sequence of letters or accents followed by letter + my $word='(?:[-\w\d*]|\\\\[\"\'\`~^][A-Za-z\*])+'; + my $cmdleftright='\\\\(?:left|right)\s*(?:[()\[\]|]|\\\\(?:[|{}]|\w+))'; + + my $cmdoptseq='\\\\[\w\d\*]+'.$extraspace.'(?:(?:\['.$brat0.'\]|\{'. $pat4 . '\}|\(' . $coords .'\))'.$extraspace.')*'; + my $oneletcmd='\\\\.(?:\['.$brat0.'\]|\{'. $pat4 . '\})*'; + my $math='\$(?:[^$]|\\\$)*?\$|\\\\[(].*?\\\\[)]'; +# my $math='\$(?:[^$]|\\\$)*\$'; + my $comment='%.*?\n'; + my $pat=qr/(?:\A\s*)?(?:${and}|${quotemarks}|${number}|${word}|$cmdleftright|${cmdoptseq}|${math}|${oneletcmd}|${comment}|${punct}|${mathpunct}|\{|\})\s*/ ; + + + +# now we are done setting up and can start working +my ($oldfile, $newfile) = @ARGV; + +$encoding=guess_encoding($newfile) unless defined($encoding); + +$encoding = "utf8" if $encoding =~ m/^utf8/i ; +if (lc($encoding) eq "utf8" ) { + binmode(STDOUT, ":utf8"); + binmode(STDERR, ":utf8"); +} + +$old=read_file_with_encoding($oldfile,$encoding); +$new=read_file_with_encoding($newfile,$encoding); + + + + +# reset time +exetime(1); +($oldpreamble,$oldbody,$oldpost)=splitdoc($old,'\\\\begin\{document\}','\\\\end\{document\}'); + + +($newpreamble,$newbody,$newpost)=splitdoc($new,'\\\\begin\{document\}','\\\\end\{document\}'); + + +if ($flatten) { + $oldbody=flatten($oldbody,$oldpreamble,$oldfile,$encoding); + $newbody=flatten($newbody,$newpreamble,$newfile,$encoding); +} + +if ( length $oldpreamble && length $newpreamble ) { + @oldpreamble = split /\n/, $oldpreamble; + @newpreamble = split /\n/, $newpreamble; + + %packages=list_packages(@newpreamble) unless %packages; + if (defined $packages{"hyperref"} ) { + print STDERR "hyperref package detected.\n" if $verbose ; + $latexdiffpreamble =~ s/\{\\DIFadd\}/{\\DIFaddtex}/g; + $latexdiffpreamble =~ s/\{\\DIFdel\}/{\\DIFdeltex}/g; + $latexdiffpreamble .= join "\n",(extrapream("HYPERREF"),""); + } + # insert dummy first line such that line count begins with line 1 (rather than perl's line 0) - just so that line numbers inserted by linediff are correct + unshift @newpreamble,''; + unshift @oldpreamble,''; + print STDERR "Differencing preamble.\n" if $verbose; + @diffpreamble = linediff(\@oldpreamble, \@newpreamble); + # remove dummy line again + shift @diffpreamble; + push @diffpreamble,$latexdiffpreamble; + push @diffpreamble,'\begin{document}'; +} +elsif ( !length $oldpreamble && !length $newpreamble ) { + @diffpreamble=(); +} else { + print STDERR "Either both texts must have preamble or neither text must have the preamble.\n"; + exit(2); +} + +if (defined $packages{"amsmath"} ) { + print STDERR "amsmath package detected.\n" if $verbose ; + $MATHARRREPL='align*'; +} + +print STDERR "Preprocessing body. " if $verbose; +my ($oldleadin,$newleadin)=preprocess($oldbody,$newbody); + + +# run difference algorithm +@diffbody=bodydiff($oldbody, $newbody); +$diffbo=join("",@diffbody); +print STDERR "(",exetime()," s)\n","Postprocessing body. \n " if $verbose; +postprocess($diffbo); +$diffall =join("\n",@diffpreamble) ; +$diffall .= "$newleadin$diffbo" ; +$diffall .= "\\end{document}$newpost" if length $newpreamble ; +if ( lc($encoding) ne "utf8" && lc($encoding) ne "ascii" ) { + print STDERR "Encoding output file to $encoding\n" if $verbose; + $diffall=Encode::encode($encoding,$diffall); + binmode STDOUT; +} +print $diffall; + + +print STDERR "(",exetime()," s)\n","Done.\n" if $verbose; + + + +## guess_encoding(filename) +## reads the first 20 lines of filename and looks for call of inputenc package +## if found, return the option of this package (encoding), otherwise return ascii +sub guess_encoding { + my ($filename)=@_; + my ($i,$enc); + open (FH, $filename) or die("Couldn't open $filename: $!"); + $i=0; + while (<FH>) { + next if /^\s*%/; # skip comment lines + if (m/\\usepackage\[(\w*?)\]\{inputenc\}/) { + close(FH); + return($1); + } + last if (++$i > 20 ); # scan at most 20 non-comment lines + } + close(FH); + return("ascii"); +} + + +sub read_file_with_encoding { + my ($output); + my ($filename, $encoding) = @_; + + if (lc($encoding) eq "utf8" ) { + open (FILE, "<:utf8",$filename) or die("Couldn't open $filename: $!"); + local $/ ; # locally set record operator to undefined, ie. enable whole-file mode + $output=<FILE>; + } elsif ( lc($encoding) eq "ascii") { + open (FILE, $filename) or die("Couldn't open $filename: $!"); + local $/ ; # locally set record operator to undefined, ie. enable whole-file mode + $output=<FILE>; + } else { + require Encode; + open (FILE, "<",$filename) or die("Couldn't open $filename: $!"); + local $/ ; # locally set record operator to undefined, ie. enable whole-file mode + $output=<FILE>; + print STDERR "Converting $filename from $encoding to utf8\n" if $verbose; + $output=Encode::decode($encoding,$output); + } + close FILE; + return $output; +} + +# %packages=list_packages(@preamble) +# scans the arguments for \documentclass and \usepackage statements and constructs a hash +# whose keys are the included packages, and whose values are the associated optional arguments +sub list_packages { + my (@preamble)=@_; + my %packages=(); + foreach $line ( @preamble ) { + # get rid of comments + $line=~s/(?<!\\)%.*$// ; + if ( $line =~ m/\\(?:documentclass|usepackage)(?:\[(.+?)\])?\{(.*?)\}/ ) { +# print STDERR "Found something: |$line|\n"; + if (defined($1)) { + $packages{$2}=$1; + } else { + $packages{$2}=""; + } + } + } + return (%packages); +} + +# flatten($text,$preamble,$filename,$encoding) +# expands \input and \include commands within text +# premable is scanned for includeonly commands +# encoding is the encoding +sub flatten { + my ($text,$preamble,$filename,$encoding)=@_; + my ($includeonly,$dirname,$fname,$newpage,$replacement); + require File::Basename ; + require File::Spec ; + $dirname = File::Basename::dirname($filename); + + if ( ($includeonly) = ($preamble =~ m/\\includeonly{(.*?)}/ ) ) { + $includeonly =~ s/,/|/g; + } else { + $includeonly = '.*?'; + } + + print STDERR "DEBUG: includeonly $includeonly\n"; + + $text=~s/\\input{(.*?)}|\\include{(${includeonly}(?:\.tex)?)}/{ + $fname = $1 if defined($1) ; + $fname = $2 if defined($2) ; + # # add tex extension unless there is a three letter extension already + $fname .= ".tex" unless $fname =~ m|\.\w{3}|; + print STDERR "Include file $fname\n" if $verbose; + print STDERR "DEBUG looking for file ",File::Spec->catfile($dirname,$fname), "\n"; + # content of file becomes replacement value, add \newpage if the command was include + $replacement=read_file_with_encoding(File::Spec->catfile($dirname,$fname), $encoding) or die + "Couldn't find file ",File::Spec->catfile($dirname,$fname),": $!"; + $newpage=(defined($2)? " \\newpage " : "") ; + "$newpage$replacement$newpage"; + }/exg; + + return($text); +} + + +# print_regex_arr(@arr) +# prints regex array without x-ism expansion put in by pearl to stdout +sub print_regex_arr { + my $dumstring; + $dumstring = join(" ",@_); # PERL generates string (?-xism:^ref$) for quoted refex ^ref$ + $dumstring =~ s/\(\?-xism:\^(.*?)\$\)/$1/g; # remove string and ^,$ marks before output + print $dumstring,"\n"; +} + + +# @lines=extrapream($type) +# reads line from appendix (end of file after __END__ token) +sub extrapream { + my $type; + my @retval=("%DIF PREAMBLE EXTENSION ADDED BY LATEXDIFF") ; + my ($copy); + + while (@_) { + $copy=0; + $type=shift ; + if ( -f $type ) { + open (FILE,$type) or die "Cannot open preamble file $type: $!"; + print STDERR "Reading preamble file $type\n" if $verbose ; + while (<FILE>) { + chomp ; + if ( $_ =~ m/%DIF PREAMBLE/ ) { + push (@retval,"$_"); + } else { + push (@retval,"$_ %DIF PREAMBLE"); + } + } + } + else { # not (-f $type) + $type=uc($type); # upcase argument + print STDERR "Preamble Internal Type $type\n" if $verbose; + while (<DATA>) { + if ( m/^%DIF $type/ ) { + $copy=1; } + elsif ( m/^%DIF END $type/ ) { + last; } + chomp; + push (@retval,"$_ %DIF PREAMBLE") if $copy; + } + if ( $copy == 0 ) { + print STDERR "\nPreamble style $type not implemented.\n"; + print STDERR "Write latexdiff -h to get help with available styles\n"; + exit(2); + } + seek DATA,0,0; # rewind DATA handle to file begin + } + } + push (@retval,"%DIF END PREAMBLE EXTENSION ADDED BY LATEXDIFF") ; + return @retval; +} + + +# ($part1,$part2,$part3)=splitdoc($text,$word1,$word2) +# splits $text into 3 parts at $word1 and $word2. +# if neither $word1 nor $word2 exist, $part1 and $part3 are empty, $part2 is $text +# If only $word1 or $word2 exist but not the other, output an error message. + +# NB this version avoids $` and $' for performance reason although it only makes a tiny difference +# (in one test gain a tenth of a second for a 30s run) +sub splitdoc { + my ($text,$word1,$word2)=@_; + my ($part1,$part2,$part3)=("","",""); + my ($rest,$pos); + + if ( $text =~ m/(^[^%]*)($word1)/mg ) { + $pos=pos $text; + $part1=substr($text,0,$pos-length($2)); + $rest=substr($text,$pos); + if ( $rest =~ m/(^[^%]*)($word2)/mg ) { + $pos=pos $rest; + $part2=substr($rest,0,$pos-length($2)); + $part3=substr($rest,$pos); + } + else { + die "$word1 and $word2 not in the correct order or not present as a pair." ; + } + } else { + $part2=$text; + die "$word2 present but not $word1." if ( $text =~ m/(^[^%]*)$word2/ms ); + } + return ($part1,$part2,$part3); +} + + + + + +# bodydiff($old,$new) +sub bodydiff { + my ($oldwords, $newwords) = @_; + my @retwords; + + print STDERR "(",exetime()," s)\n","Splitting into latex tokens \n" if $verbose; + print STDERR "Parsing $oldfile \n" if $verbose; + my @oldwords = splitlatex($oldwords); + print STDERR "Parsing $newfile \n" if $verbose; + my @newwords = splitlatex($newwords); + my $token; + + print STDERR "(",exetime()," s)\n","Pass 1: Expanding text commands and merging isolated identities with changed blocks " if $verbose; + pass1(\@oldwords, \@newwords); + + + print STDERR "(",exetime()," s)\n","Pass 2: inserting DIF tokens and mark up. " if $verbose; + + @retwords=pass2(\@oldwords, \@newwords); + + return(@retwords); +} + + + + +# @words=splitlatex($string) +# split string according to latex rules +# Each element of words is either +# a word (including trailing spaces and punctuation) +# a latex command +sub splitlatex { + my ($string) = @_ ; + my @retval=($string =~ m/$pat/osg); + + if (length($string) != length(join("",@retval))) { + print STDERR "\nWARNING: Inconsistency in length of input string and parsed string:\n This often indicates faulty or non-standard latex code.\n In many cases you can ignore this and the following warning messages.\n Note that character numbers in the following are counted beginning after \\begin{document} and are only approximate." unless $ignorewarnings; + print STDERR "DEBUG Original length ",length($string)," Parsed length ",length(join("",@retval)),"\n" if $debug; + print STDERR "DEBUG Input string: |$string|\n" if (length($string)<500) && $debug; + print STDERR "DEBUG Token parsing: |",join("+",@retval),"|\n" if (length($string)<500) && $debug ; + @retval=(); + # slow way only do this if other m//sg method fails + my $last = 0; + while ( $string =~ m/$pat/osg ) { + my $match=$&; + if ($last + length $& != pos $string ) { + my $pos=pos($string); + my $offset=30<$last ? 30 : $last; + my $dum=substr($string,$last-$offset,$pos-$last+2*$offset); + my $dum1=$dum; + my $cnt=$#retval; + my $i; + $dum1 =~ s/\n/ /g; + unless ($ignorewarnings) { + print STDERR "\n$dum1\n"; + print STDERR " " x 30,"^" x ($pos-$last)," " x 30,"\n"; + print STDERR "Missing characters near word " . (scalar @retval) . " character index: " . $last . "-" . pos($string) . " Length: " . length($match) . " Match: |$match| (expected match marked above).\n"; + } + # put in missing characters `by hand' + push (@retval, substr($dum,$offset,$pos-$last-length($match))); +# Note: there seems to be a bug in substr with utf8 that made the following line output substr which were too long, +# using dum instead appears to work +# push (@retval, substr($string,$last, pos($string)-$last-length($match))); + } + push (@retval, $match); + $last=pos $string; + } + + } + return @retval; +} + + +# pass1( \@seq1,\@seq2) +# Look for differences between seq1 and seq2. +# Where an common-subsequence block is flanked by deleted or appended blocks, +# and is shorter than $MINWORDSBLOCK words it is appended +# to the last deleted or appended word. If the block contains tokens other than words +# or punctuation it is not merged. +# Deleted or appended block consisting of words and safe commands only are +# also merged, to prevent break-up in pass2 (after previous isolated words have been removed) +# If there are commands with textual arguments (e.g. \caption) both in corresponding +# appended and deleted blocks split them such that the command and opening bracket +# are one token, then the rest is split up following standard rules, and the closing +# bracket is a separate token, ie. turn +# "\caption{This is a textual argument}" into +# ("\caption{","This ","is ","a ","textual ","argument","}") +# No return value. Destructively changes sequences +sub pass1 { + my $seq1 = shift ; + my $seq2 = shift ; + + my $len1 = scalar @$seq1; + my $len2 = scalar @$seq2; + my $wpat=qr/^(?:[a-zA-Z.,'`:;?()!]*)[\s~]*$/; #' + + my ($last1,$last2)=(-1,-1) ; + my $cnt=0; + my $block=[]; + my $addblock=[]; + my $delblock=[]; + my $todo=[]; + my $instruction=[]; + my $i; + my (@delmid,@addmid,@dummy); + + my ($addcmds,$delcmds,$matchindex); + my ($addtextblocks,$deltextblocks); + my ($addtokcnt,$deltokcnt,$mattokcnt)=(0,0,0); + my ($addblkcnt,$delblkcnt,$matblkcnt)=(0,0,0); + + my $adddiscard = sub { + if ($cnt > 0 ) { + $matblkcnt++; + # just after an unchanged block +# print STDERR "Unchanged block $cnt, $last1,$last2 \n"; + if ($cnt < $MINWORDSBLOCK + && $cnt==scalar ( + grep { /^$wpat/ || ( /^\\([\w\d\*]+)((?:\[$brat0\]|\{$pat4\})*)/o + && iscmd($1,\@SAFECMDLIST,\@SAFECMDEXCL) + && scalar(@dummy=split(" ",$2))<3 ) } + @$block) ) { + # merge identical blocks shorter than $MINWORDSBLOCK + # and only containing ordinary words + # with preceding different word + # We cannot carry out this merging immediately as this + # would change the index numbers of seq1 and seq2 and confuse + # the algorithm, instead we store in @$todo where we have to merge + push(@$todo, [ $last1,$last2,$cnt,@$block ]); + } + $block = []; + $cnt=0; $last1=-1; $last2=-1; + } + }; + my $discard=sub { $deltokcnt++; + &$adddiscard; #($_[0],$_[1]); + push(@$delblock,[ $seq1->[$_[0]],$_[0] ]); + $last1=$_[0] }; + + my $add = sub { $addtokcnt++; + &$adddiscard; #($_[0],$_[1]); + push(@$addblock,[ $seq2->[$_[1]],$_[1] ]); + $last2=$_[1] }; + + my $match = sub { $mattokcnt++; + if ($cnt==0) { # first word of matching sequence after changed sequence or at beginning of word sequence + $deltextblocks = extracttextblocks($delblock); + $delblkcnt++ if scalar @$delblock; + $addtextblocks = extracttextblocks($addblock); + $addblkcnt++ if scalar @$addblock; + + $delcmds = extractcommands($delblock); + $addcmds = extractcommands($addblock); + # keygen(third argument of _longestCommonSubsequence) implies to sort on command (0th elements of $addcmd elements) + # the calling format for longestCommonSubsequence has changed between versions of + # Algorithm::Diff so we need to check which one we are using + if ( $algodiffversion > 1.15 ) { + ### Algorithm::Diff 1.19 + $matchindex=Algorithm::Diff::_longestCommonSubsequence($delcmds,$addcmds, 0, sub { $_[0]->[0] } ); + } else { + ### Algorithm::Diff 1.15 + $matchindex=Algorithm::Diff::_longestCommonSubsequence($delcmds,$addcmds, sub { $_[0]->[0] } ); + } + + for ($i=0 ; $i<=$#$matchindex ; $i++) { + if (defined($matchindex->[$i])){ + $j=$matchindex->[$i]; + @delmid=splitlatex($delcmds->[$i][3]); + @addmid=splitlatex($addcmds->[$j][3]); + while (scalar(@$deltextblocks) && $deltextblocks->[0][0]<$delcmds->[$i][1]) { + my ($index,$block,$cnt)=@{ shift(@$deltextblocks) }; + push(@$todo, [$index,-1,$cnt,@$block]); + } + push(@$todo, [ $delcmds->[$i][1],-1,-1,$delcmds->[$i][2],@delmid,$delcmds->[$i][4]]); + + while (scalar(@$addtextblocks) && $addtextblocks->[0][0]<$addcmds->[$j][1]) { + my ($index,$block,$cnt)=@{ shift(@$addtextblocks) }; + push(@$todo, [-1,$index,$cnt,@$block]); + } + push(@$todo, [ -1,$addcmds->[$j][1],-1,$addcmds->[$j][2],@addmid,$addcmds->[$j][4]]); + } + } + # mop up remaining textblocks + while (scalar(@$deltextblocks)) { + my ($index,$block,$cnt)=@{ shift(@$deltextblocks) } ; + push(@$todo, [$index,-1,$cnt,@$block]); + } + while (scalar(@$addtextblocks)) { + my ($index,$block,$cnt)=@{ shift(@$addtextblocks) }; + push(@$todo, [-1,$index,$cnt,@$block]); + } + + $addblock=[]; + $delblock=[]; + } + push(@$block,$seq2->[$_[1]]); + $cnt++ }; + + my $keyfunc = sub { join(" ",split(" ",shift())) }; + + traverse_sequences($seq1,$seq2, { MATCH=>$match, DISCARD_A=>$discard, DISCARD_B=>$add }, $keyfunc ); + + + # now carry out the merging/splitting. Refer to elements relative from + # the end (with negative indices) as these offsets don't change before the instruction is executed + # cnt>0: merged small unchanged groups with previous changed blocks + # cnt==-1: split textual commands into components + foreach $instruction ( @$todo) { + ($last1,$last2,$cnt,@$block)=@$instruction ; + if ($cnt>=0) { + splice(@$seq1,$last1-$len1,1+$cnt,join("",$seq1->[$last1-$len1],@$block)) if $last1>=0; + splice(@$seq2,$last2-$len2,1+$cnt,join("",$seq2->[$last2-$len2],@$block)) if $last2>=0; + } else { + splice(@$seq1,$last1-$len1,1,@$block) if $last1>=0; + splice(@$seq2,$last2-$len2,1,@$block) if $last2>=0; + } + } + + if ($verbose) { + print STDERR "\n"; + print STDERR " $mattokcnt matching tokens in $matblkcnt blocks.\n"; + print STDERR " $deltokcnt discarded tokens in $delblkcnt blocks.\n"; + print STDERR " $addtokcnt appended tokens in $addblkcnt blocks.\n"; + } +} + + +# extracttextblocks(\@blockindex) +# $blockindex has the following format +# [ [ token1, index1 ], [token2, index2],.. ] +# where index refers to the index in the original old or new word sequence +# Returns: reference to an array of the form +# [[ $index, $textblock, $cnt ], .. +# where $index index of block to be merged +# $textblock contains all the words to be merged with the word at $index (but does not contain this word) +# $cnt is length of block +# +# requires: iscmd +# +sub extracttextblocks { + my $block=shift; + my ($i,$token,$index); + my $textblock=[]; + my $last=-1; + my $wpat=qr/^(?:[a-zA-Z.,'`:;?()!]*)[\s~]*$/; #' + my $retval=[]; + + for ($i=0;$i< scalar @$block;$i++) { + ($token,$index)=@{ $block->[$i] }; + # store pure text blocks + if ($token =~ /$wpat/ || ( $token =~/^\\([\w\d\*]+)((?:${extraspace}\[$brat0\]${extraspace}|${extraspace}\{$pat4\})*)/o + && iscmd($1,\@SAFECMDLIST,\@SAFECMDEXCL) + && !iscmd($1,\@TEXTCMDLIST,\@TEXTCMDEXCL))) { + # we have text or a command which can be treated as text + if ($last<0) { + # new pure-text block + $last=$index; + } else { + # add to pure-text block + push(@$textblock, $token); + } + } else { + # it is not text + if (scalar(@$textblock)) { + push(@$retval,[ $last, $textblock, scalar(@$textblock) ]); + } + $textblock=[]; + $last=-1; + } + } + # finish processing a possibly unfinished block before returning + if (scalar(@$textblock)) { + push(@$retval,[ $last, $textblock, scalar(@$textblock) ]); + } + return($retval) +} + + + +# extractcommands( \@blockindex ) +# $blockindex has the following format +# [ [ token1, index1 ], [token2, index2],.. ] +# where index refers to the index in the original old or new word sequence +# Returns: reference to an array of the form +# [ [ "\cmd1", index, "\cmd1[optarg]{arg1}{", "arg2" ,"} " ],.. +# where index is just taken from input array +# command must have a textual argument as last argument +# +# requires: iscmd +# +sub extractcommands { + my $block=shift; + my ($i,$token,$index,$cmd,$open,$mid,$closing); + my $retval=[]; + + for ($i=0;$i< scalar @$block;$i++) { + ($token,$index)=@{ $block->[$i] }; + # check if token is an alphanumeric command sequence with at least one non-optional argument + # \cmd[...]{...}{last argument} + # Capturing in the following results in these associations + # $1: \cmd[...]{...}{ + # $2: \cmd + # $3: last argument + # $4: } + trailing spaces + if ( ( $token =~ m/^(\\([\w\d\*]+)(?:${extraspace}\[$brat0\]|${extraspace}\{$pat4\})*${extraspace}\{)($pat4)(\}\s*)$/so ) + && iscmd($2,\@TEXTCMDLIST,\@TEXTCMDEXCL) ) { + # push(@$retval,[ $2,$index,$1,$3,$4 ]); + ($cmd,$open,$mid,$closing) = ($2,$1,$3,$4) ; + $closing =~ s/\}/\\RIGHTBRACE/ ; + push(@$retval,[ $cmd,$index,$open,$mid,$closing ]); + } + } + return $retval; +} + +# iscmd($cmd,\@regexarray,\@regexexcl) checks +# return 1 if $cmd matches any of the patterns in the +# array $@regexarray, and none of the patterns in \@regexexcl, otherwise return 0 +sub iscmd { + my ($cmd,$regexar,$regexexcl)=@_; + my ($ret)=0; + foreach $pat ( @$regexar ) { + if ( $cmd =~ m/^${pat}$/ ) { + $ret=1 ; + last; + } + } + return 0 unless $ret; + foreach $pat ( @$regexexcl ) { + return 0 if ( $cmd =~ m/^${pat}$/ ); + } + return 1; +} + + +# pass2( \@seq1,\@seq2) +# Look for differences between seq1 and seq2. +# Mark begin and end of deleted and appended sequences with tags $DELOPEN and $DELCLOSE +# and $ADDOPEN and $ADDCLOSE, respectively, however exclude { } & and all comands, unless +# they match an element of the whitelist (SAFECMD) +# For words in TEXTCMD but not in SAFECMD, enclose interior with $ADDOPEN and $ADDCLOSE brackets +# Deleted comment lines are marked with %DIF < +# Added comment lines are marked with %DIF > +sub pass2 { + my $seq1 = shift ; + my $seq2 = shift ; + + my ($addtokcnt,$deltokcnt,$mattokcnt)=(0,0,0); + my ($addblkcnt,$delblkcnt,$matblkcnt)=(0,0,0); + + my $retval = []; + my $delhunk = []; + my $addhunk = []; + + my $discard = sub { $deltokcnt++; + push ( @$delhunk, $seq1->[$_[0]]) }; + + my $add = sub { $addtokcnt++; + push ( @$addhunk, $seq2->[$_[1]]) }; + + my $match = sub { $mattokcnt++; + if ( scalar @$delhunk ) { + $delblkcnt++; + # mark up changes, but comment out commands + push @$retval,marktags($DELMARKOPEN,$DELMARKCLOSE,$DELOPEN,$DELCLOSE,$DELCMDOPEN,$DELCMDCLOSE,$DELCOMMENT,$delhunk); + $delhunk = []; + } + if ( scalar @$addhunk ) { + $addblkcnt++; + # we mark up changes, but simply quote commands + push @$retval,marktags($ADDMARKOPEN,$ADDMARKCLOSE,$ADDOPEN,$ADDCLOSE,"","",$ADDCOMMENT,$addhunk); + $addhunk = []; + } + push(@$retval,$seq2->[$_[1]]) }; + + my $keyfunc = sub { join(" ",split(" ",shift())) }; + + traverse_sequences($seq1,$seq2, { MATCH=>$match, DISCARD_A=>$discard, DISCARD_B=>$add }, $keyfunc ); + # clear up unprocessed hunks + push @$retval,marktags($DELMARKOPEN,$DELMARKCLOSE,$DELOPEN,$DELCLOSE,$DELCMDOPEN,$DELCMDCLOSE,$DELCOMMENT,$delhunk) if scalar @$delhunk; + push @$retval,marktags($ADDMARKOPEN,$ADDMARKCLOSE,$ADDOPEN,$ADDCLOSE,"","",$ADDCOMMENT,$addhunk) if scalar @$addhunk; + + + if ($verbose) { + print STDERR "\n"; + print STDERR " $mattokcnt matching tokens. \n"; + print STDERR " $deltokcnt discarded tokens in $delblkcnt blocks.\n"; + print STDERR " $addtokcnt appended tokens in $addblkcnt blocks.\n"; + } + + return(@$retval); +} + +# marktags($openmark,$closemark,$open,$close,$opencmd,$closecmd,$comment,\@block) +# returns ($openmark,$open,$block,$close,$closemark) if @block only contains no commands (except white-listed ones), +# braces, ampersands, or comments +# mark comments with $comment +# exclude all other exceptions from scope of open, close like this +# ($openmark, $open,...,$close, $opencomd,command, command,$closecmd, $open, ..., $close, $closemark) +# If $opencmd begins with "%" marktags assumes it is operating on a deleted block, otherwise on an added block +sub marktags { + my ($openmark,$closemark,$open,$close,$opencmd,$closecmd,$comment,$block)=@_; + my $word; + my (@argtext); + my $retval=[]; + my $noncomment=0; + my $cmd=-1; # -1 at beginning 0: last token written is a ordinary word + # 1: last token written is a command + # for keeping track whether we are just in a command sequence or in a word sequence + my $cmdcomment= ($opencmd =~ m/^%/); # Flag to indicate whether opencmd is a comment (i.e. if we intend to simply comment out changed commands) + my ($command,$commandword,$closingbracket) ; # temporary variables needed below to remember sub-pattern matches + +# split this block to flatten out sequences joined in pass1 + @$block=splitlatex(join "",@$block); + foreach (@$block) { + $word=$_; + if ( $word =~ s/^%/%$comment/ ) { + # a comment + if ($cmd==1) { + push (@$retval,$closecmd) ; + $cmd=-1; + } + push (@$retval,$word); + next; + } + if (! $noncomment) { + push (@$retval,$openmark); + $noncomment=1; + } + # negative lookahead pattern (?!) in second clause is put in to avoid mathcing \( .. \) patterns + if ( $word =~ /^[&{}\[\]]/ || ( $word =~ /^\\(?!\()([\w*@]*)/ && !iscmd($1,\@SAFECMDLIST,\@SAFECMDEXCL)) ) { + # word is a command or other significant token (not in SAFECMDLIST) + ## same conditions as in subroutine extractcommand: + # check if token is an alphanumeric command sequence with at least one non-optional argument + # \cmd[...]{...}{last argument} + # Capturing in the following results in these associations + # $1: \cmd[...]{...}{ + # $2: \cmd + # $3: last argument + # $4: } + trailing spaces + ### pre-0.3 if ( ( $token =~ m/^(\\([\w\d\*]+)(?:\[$brat0\]|\{$pat4\})*\{)($pat4)(\}\s*)$/so ) + if ( ( $word =~ m/^(\\([\w\d\*]+)(?:${extraspace}\[$brat0\]|${extraspace}\{$pat4\})*${extraspace}\{)($pat4)(\}\s*)$/so ) + && iscmd($2,\@TEXTCMDLIST,\@TEXTCMDEXCL) && ( !$cmdcomment || !iscmd($2,\@CONTEXT2CMDLIST, \@CONTEXT2CMDEXCL) ) ) { + # word is a text command - we mark up the interior of the word. But if we are in a deleted block ($cmdcomment=1) and + # $2 (the command) is in context2, just treat it as an ordinary command (i.e. comment it open with $opencmd) + # Because we do not want to disable this command + # here we do not use $opencmd and $closecmd($opencmd is empty) + if ($cmd==1) { + push (@$retval,$closecmd) ; + } elsif ($cmd==0) { + push (@$retval,$close) ; + } + $command=$1; $commandword=$2; $closingbracket=$4; + @argtext=splitlatex($3); # split textual argument into tokens + # and mark it up (but we do not need openmark and closemark) + # insert command with initial arguments, marked-up final argument, and closing bracket + if ( $cmdcomment && iscmd($commandword,\@CONTEXT1CMDLIST, \@CONTEXT1CMDEXCL) ) { + # context1cmd in a deleted environment; delete command itself but keep last argument, marked up + push (@$retval,$opencmd); + $command =~ s/\n/\n${opencmd}/sg ; # repeat opencmd at the beginning of each line + # argument, note that the additional comment character is included + # to suppress linebreak after opening parentheses, which is important + # for latexrevise + push (@$retval,$command,"%\n{$AUXCMD\n",marktags("","",$open,$close,$opencmd,$closecmd,$comment,\@argtext),$closingbracket); + } else { + # normal textcmd or context1cmd in an added block + push (@$retval,$command,marktags("","",$open,$close,$opencmd,$closecmd,$comment,\@argtext),$closingbracket); + } + push (@$retval,$AUXCMD,"\n") if $cmdcomment ; + $cmd=-1 ; + } else { + # ordinary command + push (@$retval,$opencmd) if $cmd==-1 ; + push (@$retval,$close,$opencmd) if $cmd==0 ; + $word =~ s/\n/\n${opencmd}/sg if $cmdcomment ; # if opencmd is a comment, repeat this at the beginning of every line + push (@$retval,$word); + $cmd=1; + } + } else { + # just an ordinary word or word in SAFECMD + push (@$retval,$open) if $cmd==-1 ; + push (@$retval,$closecmd,$open) if $cmd==1 ; + push (@$retval,$word); + $cmd=0; + } + } + push (@$retval,$close) if $cmd==0; + push (@$retval,$closecmd) if $cmd==1; + + push (@$retval,$closemark) if ($noncomment); + return @$retval; +} + +# preprocess($string, ..) +# carry out the following pre-processing steps for all arguments: +# 1. Remove leading white-space +# 2. mark all first empty line (in block of several) with \PAR tokens +# 3. Convert all '\%' into '\PERCENTAGE ' to make parsing regular expressions easier +# 4. Convert all \verb|some verbatim text| commands (where | can be an arbitrary character) +# into \verb{hash} +# 5. Convert \begin{verbatim} some verbatim text \end{verbatim} into \verbatim{hash} +# 6. Convert _n into \SUBSCRIPTNB{n} and _{nnn} into \SUBSCRIPT{nn} +# 7. Convert ^n into \SUPERSCRIPTNB{n} and ^{nnn} into \SUPERSCRIPT{nn} +# 8. a. Convert $$ $$ into \begin{DOLLARDOLLAR} \end{DOLLARDOLLAR} +# b. Convert \[ \] into \begin{SQUAREBRACKET} \end{SQUAREBRACKET} + +# 9. Add final token STOP to the very end. This is put in because the algorithm works better if the last token is identical. This is removed again in postprocessing. +# +# NB: step 6 and 7 is likely to convert some "_" inappropriately, e.g. in file +# names or labels but it does not matter because they are converted back in the postprocessing step +# Returns: leading white space removed in step 1 +sub preprocess { + my @leadin=() ; + for (@_) { + s/^(\s*)//s; + push(@leadin,$1); + s/\n(\s*?)\n((?:\s*\n)*)/\n$1\\PAR\n$2/g ; + s/(?<!\\)\\%/\\PERCENTAGE /g ; # (?<! is negative lookbehind assertion to prevent \\% from being converted + s/(\\verb\*?)(\S)(.*?)\2/"${1}{". tohash(\%verbhash,"${2}${3}${2}") ."}"/esg; + s/\\begin\{(verbatim\*?)\}(.*?)\\end\{\1\}/"\\${1}{". tohash(\%verbhash,"${2}") . "}"/esg; + # Convert _n or _\cmd into \SUBSCRIPTNB{n} or \SUBSCRIPTNB{\cmd} and _{nnn} into \SUBSCRIPT{nn} + s/_([^{\\]|\\\w+)/\\SUBSCRIPTNB{$1}/g ; + s/_{($pat4)}/\\SUBSCRIPT{$1}/g ; + # Convert ^n into \SUPERSCRIPTNB{n} and ^{nnn} into \SUPERSCRIPT{nn} + s/\^([^{\\]|\\\w+)/\\SUPERSCRIPTNB{$1}/g ; + s/\^{($pat4)}/\\SUPERSCRIPT{$1}/g ; + # Convert $$ $$ into \begin{DOLLARDOLLAR} \end{DOLLARDOLLAR} + s/\$\$(.*?)\$\$/\\begin{DOLLARDOLLAR}$1\\end{DOLLARDOLLAR}/sg; + # Convert \[ \] into \begin{SQUAREBRACKET} \end{SQUAREBRACKET} + s/(?<!\\)\\\[/\\begin{SQUAREBRACKET}/sg; + s/\\\]/\\end{SQUAREBRACKET}/sg; + # add final token " STOP" + $_ .= " STOP" + } + return(@leadin); +} + +#hashstring=tohash(\%hash,$string) +# creates a hash value based on string and stores in %hash +sub tohash { + my ($hash,$string)=@_; + my (@arr,$val); + my ($sum,$i)=(0,1); + my ($hstr); + + @arr=unpack('c*',$string); + + foreach $val (@arr) { + $sum += $i*$val; + $i++; + } + $hstr= "$sum"; + if (defined($hash->{$hstr}) && $string ne $hash->{$hstr}) { + warn "Repeated hash value for verbatim mode in spite of different content."; + $hstr="-$hstr"; + } + $hash->{$hstr}=$string; + return($hstr); +} + +#string=fromhash(\%hash,$fromstring) +# restores string value stored in hash +#string=fromhash(\%hash,$fromstring,$prependstring) +# additionally begins each line with prependstring +sub fromhash { + my ($hash,$hstr)=($_[0],$_[1]); + my $retstr=$hash->{$hstr}; + if ( $#_ >= 2) { + $retstr =~ s/^/$_[2]/mg; + } + return $retstr; +} + + +# postprocess($string, ..) +# carry out the following post-processing steps for all arguments: +# * Remove STOP token from the end +# * Replace \RIGHTBRACE by } +# 1. Check all deleted blocks: +# a.where a deleted block contains a matching \begin and +# \end environment (these will be disabled by a %DIFDELCMD statements), for selected environment enable enable +# these commands again (such that for example displayed math in a deleted equation +# is properly within math mode. For math mode environments replace numbered equation +# environments with their display only variety (so that equation numbers in new file and +# diff file are identical). Where the correct type of math environment cannot be determined +# use a place holder MATHMODE +# b.where one of the commands matching $COUNTERCMD is used as a DIFAUXCMD, add a statement +# subtracting one from the respective counter to keep numbering consistent with new file +# Replace all MATHMODE environment commands by the correct environment to achieve matching +# pairs +# c. If in-line math mode contains array environment, enclose the whole environment in \mbox'es +# 2. Remove DIFadd, DIFdel, DIFFaddbegin , ... from picture environments +# 3. Convert DIFadd, DIFdel, DIFFaddbegin , ... into FL varieties +# within floats (currently recognised float environments: plate,table,figure +# plus starred varieties). +# 4. Remove empty %DIFDELCMD < lines +# 4. Convert \begin{SQUAREBRACKET} \end{SQUAREBRACKET} into \[ \] +# Convert \begin{DOLLARDOLLAR} \end{DOLLARDOLLAR} into $$ $$ +# 5. Convert \SUPERSCRIPTNB{n} into ^n and \SUPERSCRIPT{nn} into ^{nnn} +# 6. Convert \SUBSCRIPTNB{n} into _n and \SUBCRIPT{nn} into _{nnn} +# 7. Expand hashes of verb and verbatim environments +# 8. Convert '\PERCENTAGE ' back into '\%' +# 9.. remove all \PAR tokens +# 10. package specific processing: endfloat: make sure \begin{figure} and \end{figure} are always +# on a line by themselves, similarly for table environment +# 4, undo renaming of the \begin and \end in comments + +# +# Note have to manually synchronize substitution commands below and +# DIF.. command names in the header +sub postprocess { + my ($begin,$len,$cnt,$float,$delblock,$addblock); + # second level blocks + my ($begin2,$cnt2,$len2,$eqarrayblock,$mathblock); + + for (@_) { + + # change $'s in comments to something harmless + 1 while s/(%.*)\$/$1DOLLARDIF/mg ; + + # Remove final STOP token + s/ STOP$//; + # Replace \RIGHTBRACE by } + s/\\RIGHTBRACE/}/g; + # Check all deleted blocks: where a deleted block contains a matching \begin and + # \end environment (these will be disabled by a %DIFDELCMD statements), enable + # these commands again (such that for example displayed math in a deleted equation + # is properly within math mode. For math mode environments replace numbered equation + # environments with their display only variety (so that equation numbers in new file and + # diff file are identical + while ( m/\\DIFdelbegin.*?\\DIFdelend/sg ) { + $cnt=0; + $len=length($&); + $begin=pos($_) - $len; + $delblock=$&; + + + ### (.*?[^\n]?)\n? construct is necessary to avoid empty lines in math mode, which result in + ### an error + # displayed math environments + $delblock=~ s/(\%DIFDELCMD < \s*\\begin\{((?:$MATHENV)|SQUAREBRACKET)\}\s*?(?:$DELCMDCLOSE|\n))(.*?[^\n]?)\n?(\%DIFDELCMD < \s*\\end\{\2\})/\\begin{$MATHREPL}$AUXCMD\n$1$3\n\\end{$MATHREPL}$AUXCMD\n$4/sg; + # also transform the opposite pair \end{displaymath} .. \begin{displaymath} but we have to be careful not to interfere with the results of the transformation in the line directly above + $delblock=~ s/(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\end\{((?:$MATHENV)|SQUAREBRACKET)\}\s*?(?:$DELCMDCLOSE|\n))(.*?[^\n]?)\n?(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\begin\{\2\})/\\end{MATHMODE}$AUXCMD\n$1$3\n\\begin{MATHMODE}$AUXCMD\n$4/sg; + + # now look for unpaired %DIFDELCMD < \begin{MATHENV}; if found add \begin{$MATHREPL} and insert \end{$MATHREPL} + # just before end of block; again we use look-behind assertion to avoid matching constructions which have already been converted + if ($delblock=~ s/(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\begin\{((?:$MATHENV)|SQUAREBRACKET)\}\s*?(?:$DELCMDCLOSE|\n))/$1\\begin{$MATHREPL}$AUXCMD\n/sg ) { + $delblock =~ s/(\\DIFdelend$)/\\end{$MATHREPL}$AUXCMD\n$1/s ; + } + # now look for unpaired %DIFDELCMD < \end{MATHENV}; if found add \end{MATHMODE} and insert \begin{MATHMODE} + # just before end of block; again we use look-behind assertion to avoid matching constructions which have already been converted + if ($delblock=~ s/(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\end\{((?:$MATHENV)|SQUAREBRACKET)\}\s*?(?:$DELCMDCLOSE|\n))/$1\\end{MATHMODE}$AUXCMD\n/sg ) { + $delblock =~ s/(\\DIFdelend$)/\\begin{MATHMODE}$AUXCMD\n$1/s ; + } + + + # equation array environment + $delblock=~ s/(\%DIFDELCMD < \s*\\begin\{($MATHARRENV)\}\s*?(?:$DELCMDCLOSE|\n))(.*?[^\n]?)\n?(\%DIFDELCMD < \s*\\end\{\2\})/\\begin{$MATHARRREPL}$AUXCMD\n$1$3\n\\end{$MATHARRREPL}$AUXCMD\n$4/sg; + $delblock=~ s/(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\end\{($MATHARRENV)\}\s*?(?:$DELCMDCLOSE|\n))(.*?[^\n]?)\n?(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\begin\{\2\})/\\end{MATHMODE}$AUXCMD\n$1$3\n\\begin{MATHMODE}$AUXCMD\n$4/sg; + + # now look for unpaired %DIFDELCMD < \begin{MATHARRENV}; if found add \begin{$MATHARRREPL} and insert \end{$MATHARRREPL} + # just before end of block; again we use look-behind assertion to avoid matching constructions which have already been converted + if ($delblock=~ s/(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\begin\{($MATHARRENV)\}\s*?(?:$DELCMDCLOSE|\n))/$1\\begin{$MATHARRREPL}$AUXCMD\n/sg ) { + $delblock =~ s/(\\DIFdelend$)/\\end{$MATHARRREPL}$AUXCMD\n$1/s ; + } + # now look for unpaired %DIFDELCMD < \end{MATHENV}; if found add \end{MATHMODE} and insert \begin{MATHMODE} + # just before end of block; again we use look-behind assertion to avoid matching constructions which have already been converted + if ($delblock=~ s/(?<!${AUXCMD}\n)(\%DIFDELCMD < \s*\\end\{($MATHARRENV)\}\s*?(?:$DELCMDCLOSE|\n))/$1\\end{MATHMODE}$AUXCMD\n/sg ) { + $delblock =~ s/(\\DIFdelend$)/\\begin{MATHMODE}$AUXCMD\n$1/s ; + } + + # parse $delblock for deleted and reinstated eqnarray* environments - within those reinstate \\ and & commands + while ( $delblock =~ m/\\begin\Q{$MATHARRREPL}$AUXCMD\E\n.*?\n\\end\Q{$MATHARRREPL}$AUXCMD\E\n/sg ) { + $cnt2=0; + $len2=length($&); + $begin2=pos($delblock) - $len2; + $eqarrayblock=$&; + # reinstate deleted & and \\ commands + $eqarrayblock=~ s/(\%DIFDELCMD < \s*(\&|\\\\)\s*?(?:$DELCMDCLOSE|\n))/$1$2$AUXCMD\n/sg ; + + substr($delblock,$begin2,$len2)=$eqarrayblock; + pos($delblock) = $begin2 + length($eqarrayblock); + } + + + +# b.where one of the commands matching $COUNTERCMD is used as a DIFAUXCMD, add a statement +# subtracting one from the respective counter to keep numbering consistent with new file + $delblock=~ s/\\($COUNTERCMD)((?:${extraspace}\[$brat0\]${extraspace}|${extraspace}\{$pat4\})*\s*${AUXCMD}\n)/\\$1$2\\addtocounter{$1}{-1}${AUXCMD}\n/sg ; +# c. If in-line math mode contains array environment, enclose the whole environment in \mbox'es + while ( $delblock =~ m/($math)(\s*)/sg ) { + $cnt2=0; + $len2=length($&); + $begin2=pos($delblock) - $len2; + $mathblock="%\n\\mbox{$AUXCMD\n$1\n}$AUXCMD\n"; + next unless $mathblock =~ m/\{$ARRENV\}/ ; + substr($delblock,$begin2,$len2)=$mathblock; + pos($delblock) = $begin2 + length($mathblock); + } + +# splice in modified delblock + substr($_,$begin,$len)=$delblock; + pos = $begin + length($delblock); + } + # make the array modification in added blocks + while ( m/\\DIFaddbegin.*?\\DIFaddend/sg ) { + $cnt=0; + $len=length($&); + $begin=pos($_) - $len; + $addblock=$&; + while ( $addblock =~ m/($math)(\s*)/sg ) { + $cnt2=0; + $len2=length($&); + $begin2=pos($addblock) - $len2; + $mathblock="%\n\\mbox{$AUXCMD\n$1\n}$AUXCMD\n"; + next unless $mathblock =~ m/\{$ARRENV\}/ ; + substr($addblock,$begin2,$len2)=$mathblock; + pos($addblock) = $begin2 + length($mathblock); + } + +# splice in modified addblock + substr($_,$begin,$len)=$addblock; + pos = $begin + length($addblock); + } + + + ### old place for BEGINDIF, ENDDIF replacement + # change begin and end commands within comments such that they + # don't disturb the pattern matching (if there are several \begin or \end in one line + # this substitution is insufficient but that appears unlikely) + s/(%.*)\\begin\{(.*)$/$1\\BEGINDIF\{$2/mg ; + s/(%.*)\\end\{(.*)$/$1\\ENDDIF\{$2/mg ; + + # Replace MATHMODE environments from step 1a above by the correct Math environment + + # The next line is complicated. The negative look-ahead insertion makes sure that no \end{$MATHENV} (or other mathematical + # environments) are between the \begin{$MATHENV} and \end{MATHMODE} commands. This is necessary as the minimal matching + # is not globally minimal but only 'locally' (matching is beginning from the left side of the string) + 1 while s/\\begin{((?:$MATHENV)|(?:$MATHARRENV)|SQUAREBRACKET)}((?:.(?!(?:\\end{(?:(?:$MATHENV)|(?:$MATHARRENV)|SQUAREBRACKET)}|\\begin{MATHMODE})))*?)\\end{MATHMODE}/\\begin{$1}$2\\end{$1}/s; + 1 while s/\\begin{MATHMODE}((?:.(?!\\end{MATHMODE}))*?)\\end{((?:$MATHENV)|(?:$MATHARRENV)|SQUAREBRACKET)}/\\begin{$2}$1\\end{$2}/s; + # convert remaining \begin{MATHMODE} \end{MATHMODE} (and not containing & or \\ )into MATHREPL environments + s/\\begin{MATHMODE}((?:(.(?!(?<!\\)\&|\\\\))*)?)\\end{MATHMODE}/\\begin{$MATHREPL}$1\\end{$MATHREPL}/sg; + # others into MATHARRREPL + s/\\begin{MATHMODE}(.*?)\\end{MATHMODE}/\\begin{$MATHARRREPL}$1\\end{$MATHARRREPL}/sg; + + # now look for AUXCMD math-mode pairs which have only comments (or empty lines between them), and remove the added commands + s/\\begin{((?:$MATHENV)|(?:$MATHARRENV)|SQUAREBRACKET)}$AUXCMD\n((?:\s*%.[^\n]*\n)*)\\end{\1}$AUXCMD\n/$2/sg; + + + # Remove all mark up within picture environments + while ( m/\\begin\{($PICTUREENV)\}.*?\\end\{\1\}/sg ) { + $cnt=0; + $len=length($&); + $begin=pos($_) - $len; + $float=$&; + $float =~ s/\\DIFaddbegin //g; + $float =~ s/\\DIFaddend //g; + $float =~ s/\\DIFadd\{($pat4)\}/$1/g; + $float =~ s/\\DIFdelbegin //g; + $float =~ s/\\DIFdelend //g; + $float =~ s/\\DIFdel\{($pat4)\}//g; + substr($_,$begin,$len)=$float; + pos = $begin + length($float); + } + # Convert DIFadd, DIFdel, DIFFaddbegin , ... into varieties + # within floats (currently recognised float environments: plate,table,figure + # plus starred varieties). + while ( m/\\begin\{($FLOATENV)\}.*?\\end\{\1\}/sg ) { + $cnt=0; + $len=length($&); + $begin=pos($_) - $len; + $float=$&; + $float =~ s/\\DIFaddbegin /\\DIFaddbeginFL /g; + $float =~ s/\\DIFaddend /\\DIFaddendFL /g; + $float =~ s/\\DIFadd\{/\\DIFaddFL{/g; + $float =~ s/\\DIFdelbegin /\\DIFdelbeginFL /g; + $float =~ s/\\DIFdelend /\\DIFdelendFL /g; + $float =~ s/\\DIFdel\{/\\DIFdelFL{/g; + substr($_,$begin,$len)=$float; + pos = $begin + length($float); + } + ### former location of undo renaming of \begin and \end in comments + + # remove empty DIFCMD < lines + s/^\Q${DELCMDOPEN}\E\n//msg; + + # Expand hashes of verb and verbatim environments (note negative look behind assertion to not leak out of DIFDELCMD comments + s/(\\verb\*?)\{([-\d]*?)\}/"${1}". fromhash(\%verbhash,$2)/esg; + s/${DELCMDOPEN}\\(verbatim\*?)\{([-\d]*?)\}/"${DELCMDOPEN}\\begin{${1}}".fromhash(\%verbhash,$2,$DELCMDOPEN)."${DELCMDOPEN}\\end{${1}}"/esg; + s/\\(verbatim\*?)\{([-\d]*?)\}/"\\begin{${1}}".fromhash(\%verbhash,$2)."\\end{${1}}"/esg; + # Convert '\PERCENTAGE ' back into '\%' + s/\\PERCENTAGE /\\%/g; + # remove all \PAR tokens (taking care to properly keep commented out PAR's + # from introducing uncommented newlines - next line) + s/(%DIF < )([^\n]*?)\\PAR\n/$1$2\n$1\n/sg; + # convert PAR commands which are on a line by themselves + s/\n(\s*?)\\PAR\n/\n\n/sg; + # convert remaining PAR commands (which are preceded by non-white space characters, usually "}" ($ADDCLOSE) + s/\\PAR\n/\n\n/sg; + + # package specific processing: + if ( defined($packages{"endfloat"})) { + #endfloat: make sure \begin{figure} and \end{figure} are always + # on a line by themselves, similarly for table environment + print STDERR "endfloat package detected.\n" if $verbose ; + # eliminate whitespace before and after + s/^(\s*)(\\(?:begin|end)\{(?:figure|table)\})(\s*)$/$2/mg; + # split lines with remaining characters before float enviroment conmmand + s/^(.+)(\\(?:begin|end)\{(?:figure|table)\})/$1\n$2/mg; + # split lines with remaining characters after float enviroment conmmand + s/(\\(?:begin|end)\{(?:figure|table)\})(.+)$/$1\n$2/mg; + } + # undo renaming of the \begin and \end and dollars in comments + s/(%.*)\\BEGINDIF\{(.*)$/$1\\begin\{$2/mg ; + s/(%.*)\\ENDDIF\{(.*)$/$1\\end\{$2/mg ; + 1 while s/(%.*)DOLLARDIF/$1\$/mg ; +# Convert \begin{SQUAREBRACKET} \end{SQUAREBRACKET} into \[ \] + s/\\end{SQUAREBRACKET}/\\\]/sg; + s/\\begin{SQUAREBRACKET}/\\\[/sg; +# 4. Convert \begin{DOLLARDOLLAR} \end{DOLLARDOLLAR} into $$ $$ + s/\\begin\{DOLLARDOLLAR\}(.*?)\\end\{DOLLARDOLLAR\}/\$\$$1\$\$/sg; +# 5. Convert \SUPERSCRIPTNB{n} into ^n and \SUPERSCRIPT{nn} into ^{nnn} + s/\\SUPERSCRIPTNB{($pat0)}/^$1/g ; + s/\\SUPERSCRIPT{($pat4)}/^{$1}/g ; + # Convert \SUBSCRIPNB{n} into _n and \SUBCRIPT{nn} into _{nnn} + s/\\SUBSCRIPTNB{($pat0)}/_$1/g ; + s/\\SUBSCRIPT{($pat4)}/_{$1}/g ; + + return; + } +} + + +# @diffs=linediff(\@seq1, \@seq2) +# mark up lines like this +#%DIF mm-mmdnn +#%< old deleted line(s) +#%DIF ------- +#%DIF mmann-nn +#new appended line %< +#%DIF ------- +# Future extension: mark change explicitly +# Assumes: traverse_sequence traverses deletions before insertions in changed sequences +# all line numbers relative to line 0 (first line of real file) +sub linediff { + my $seq1 = shift ; + my $seq2 = shift ; + + my $block = []; + my $retseq = []; + my @begin=('','',''); # dummy initialisation + my $instring ; + + my $discard = sub { @begin=('d',$_[0],$_[1]) unless scalar @$block ; + push(@$block, "%DIF < " . $seq1->[$_[0]]) }; + my $add = sub { if (! scalar @$block) { + @begin=('a',$_[0],$_[1]) ;} + elsif ( $begin[0] eq 'd' ) { + $begin[0]='c'; $begin[2]=$_[1]; + push(@$block, "%DIF -------") } + push(@$block, $seq2->[$_[1]] . " %DIF > " ) }; + my $match = sub { if ( scalar @$block ) { + if ( $begin[0] eq 'd' && $begin[1]!=$_[0]-1) { + $instring = sprintf "%%DIF %d-%dd%d",$begin[1],$_[0]-1,$begin[2]; } + elsif ( $begin[0] eq 'a' && $begin[2]!=$_[1]-1) { + $instring = sprintf "%%DIF %da%d-%d",$begin[1],$begin[2],$_[1]-1; } + elsif ( $begin[0] eq 'c' ) { + $instring = sprintf "%%DIF %sc%s", + ($begin[1]==$_[0]-1) ? "$begin[1]" : $begin[1]."-".($_[0]-1) , + ($begin[2]==$_[1]-1) ? "$begin[2]" : $begin[2]."-".($_[1]-1) ; } + else { + $instring = sprintf "%%DIF %d%s%d",$begin[1],$begin[0],$begin[2]; } + push @$retseq, $instring,@$block, "%DIF -------" ; + $block = []; + } + push @$retseq, $seq2->[$_[1]] + }; + # key function: remove multiple spaces (such that insertion or deletion of redundant white space is not reported) + my $keyfunc = sub { join(" ",split(" ",shift())) }; + + traverse_sequences($seq1,$seq2, { MATCH=>$match, DISCARD_A=>$discard, DISCARD_B=>$add }, $keyfunc ); + push @$retseq, @$block if scalar @$block; + + return wantarray ? @$retseq : $retseq ; +} + + + +# init_regex_arr_data(\@array,"TOKEN INIT") +# scans DATA file handel for line "%% TOKEN INIT" line +# then appends each line not beginning with % into array (as a quoted regex) +sub init_regex_arr_data { + my ($arr,$token)=@_; + my ($copy); + while (<DATA>) { + if ( m/^%%BEGIN $token\s*$/ ) { + $copy=1; } + elsif ( m/^%%END $token\s*/ ) { + last; } + chomp; + push (@$arr,qr/^$_$/) if ( $copy && !/^%/ ) ; + } + seek DATA,0,0; # rewind DATA handle to file begin +} + + +# init_regex_arr_ext(\@array,$arg) +# fills array with regular expressions. +# if arg is a file name, then read in list of regular expressions from that file +# (one expression per line) +# Otherwise treat arg as a comma separated list of regular expressions +sub init_regex_arr_ext { + my ($arr,$arg)=@_; + my $regex; + if ( -f $ arg ) { + open(FILE,"$arg") or die ("Couldn't open $arg: $!"); + while (<FILE>) { + chomp; + next if /^\s*#/ || /^\s*%/ || /^\s*$/ ; + push (@$arr,qr/^$_$/); + } + close(FILE); + } + else { + # assume it is a comma-separated list of reg-ex + foreach $regex (split(qr/(?<!\\),/,$arg)) { + $regex =~ s/\\,/,/g; + push (@$arr,qr/^$regex$/); + } + } +} + +#exetime() returns time since last execution of this command +#exetime(1) resets this time +my $lasttime=-1; # global variable for persistence +sub exetime { + my $reset=0; + my $retval; + if ((scalar @_) >=1) { + $reset=shift; + } + if ($reset) { + $lasttime=times(); + } + else { + $retval=times()-$lasttime; + $lasttime=$lasttime+$retval; + return($retval); + } +} + + +sub usage { + die <<"EOF"; +Usage: $0 [options] old.tex new.tex > diff.tex + +Compares two latex files and writes tex code to stdout, which has the same +format as new.tex but has all changes relative to old.tex marked up or commented. + +--type=markupstyle +-t markupstyle Add code to preamble for selected markup style + Available styles: UNDERLINE CTRADITIONAL TRADITIONAL CFONT FONTSTRIKE INVISIBLE + CHANGEBAR CCHANGEBAR CULINECHBAR CFONTCBHBAR + [ Default: UNDERLINE ] + +--subtype=markstyle +-s markstyle Add code to preamble for selected style for bracketing + commands (e.g. to mark changes in margin) + Available styles: SAFE MARGINAL DVIPSCOL + [ Default: SAFE ] + +--floattype=markstyle +-f markstyle Add code to preamble for selected style which + replace standard marking and markup commands within floats + (e.g., marginal remarks cause an error within floats + so marginal marking can be disabled thus) + Available styles: FLOATSAFE IDENTICAL + [ Default: FLOATSAFE ] + +--encoding=enc +-e enc Specify encoding of old.tex and new.tex. Typical encodings are + ascii, utf8, latin1, latin9. A list of available encodings can be + obtained by executing + perl -MEncode -e 'print join ("\\n",Encode->encodings( ":all" )) ;' + [Default encoding is utf8 unless the first few lines of the preamble contain + an invocation "\\usepackage[..]{inputenc} in which case the + encoding chosen by this command is asssumed. Note that ASCII (standard + latex) is a subset of utf8] + +--preamble=file +-p file Insert file at end of preamble instead of auto-generating + preamble. The preamble must define the following commands + \\DIFaddbegin,\\DIFaddend,\\DIFadd{..}, + \\DIFdelbegin,\\DIFdelend,\\DIFdel{..}, + and varieties for use within floats + \\DIFaddbeginFL,\\DIFaddendFL,\\DIFaddFL{..}, + \\DIFdelbeginFL,\\DIFdelendFL,\\DIFdelFL{..} + (If this option is set -t, -s, and -f options + are ignored.) + +--exclude-safecmd=exclude-file +--exclude-safecmd="cmd1,cmd2,..." +-A exclude-file +--replace-safecmd=replace-file +--append-safecmd=append-file +--append-safecmd="cmd1,cmd2,..." +-a append-file Exclude from, replace or append to the list of regex + matching commands which are safe to use within the + scope of a \\DIFadd or \\DIFdel command. The file must contain + one Perl-RegEx per line (Comment lines beginning with # or % are + ignored). A literal comma within the comma-separated list must be + escaped thus "\\,", Note that the RegEx needs to match the whole of + the token, i.e., /^regex\$/ is implied and that the initial + "\\" of the command is not included. The --exclude-safecmd + and --append-safecmd options can be combined with the --replace-safecmd + option and can be used repeatedly to add cumulatively to the lists. + +--exclude-textcmd=exclude-file +--exclude-textcmd="cmd1,cmd2,..." +-X exclude-file +--replace-textcmd=replace-file +--append-textcmd=append-file +--append-textcmd="cmd1,cmd2,..." +-x append-file Exclude from, replace or append to the list of regex + matching commands whose last argument is text. See + entry for --exclude-safecmd directly above for further details. + +--replace-context1cmd=replace-file +--append-context1cmd=append-file +--append-context1cmd="cmd1,cmd2,..." + Replace or append to the list of regex matching commands + whose last argument is text but which require a particular + context to work, e.g. \\caption will only work within a figure + or table. These commands behave like text commands, except when + they occur in a deleted section, when they are disabled, but their + argument is shown as deleted text. + +--replace-context2cmd=replace-file +--append-context2cmd=append-file +--append-context2cmd="cmd1,cmd2,..." + As corresponding commands for context1. The only difference is that + context2 commands are completely disabled in deleted sections, including + their arguments. + + +--config var1=val1,var2=val2,... +-c var1=val1,.. Set configuration variables. +-c configfile Available variables: + MINWORDSBLOCK (integer) + FLOATENV (RegEx) + PICTUREENV (RegEx) + MATHENV (RegEx) + MATHREPL (String) + MATHARRENV (RegEx) + MATHARRREPL (String) + ARRENV (RegEx) + COUNTERCMD (RegEx) + This option can be repeated. + + +--packages=pkg1,pkg2,.. + Tell latexdiff that .tex file is processed with the packages in list + loaded. This is normally not necessary if the .tex file includes the + preamble, as the preamble is automatically scanned for \\usepackage commands. + Use of the --packages option disables automatic scanning, so if for any + reason package specific parsing needs to be switched off, use --packages=none. + The following packages trigger special behaviour: + endfloat hyperref amsmath + [ Default: scan the preamble for \\usepackage commands to determine + loaded packages.] + +--show-preamble Print generated or included preamble commands to stdout. + +--show-safecmd Print list of regex matching and excluding safe commands. + +--show-textcmd Print list of regex matching and excluding commands with text argument. + +--show-config Show values of configuration variables + +--show-all Show all of the above + + NB For all --show commands, no old.tex or new.tex file needs to be given, and no + differencing takes place. + +--allow-spaces Allow spaces between bracketed or braced arguments to commands + [Default requires arguments to directly follow each other without + intervening spaces] + +--flatten Replace \\input and \\include commands within body by the content + of the files in their argument. If \\includeonly is present in the + preamble, only those files are expanded into the document. However, + no recursion is done, i.e. \\input and \\include commands within + included sections are not expanded. The included files are assumed to + be located in the same directories as the old and new master files, + respectively, making it possible to organise files into old and new directories. + +--help +-h Show this help text. + +--ignore-warnings Suppress warnings about inconsistencies in length between input + and parsed strings and missing characters. + +--verbose +-V Output various status information to stderr during processing. + Default is to work silently. + +--version Show version number. +EOF +} + +=head1 NAME + +latexdiff - determine and markup differences between two latex files + +=head1 SYNOPSIS + +B<latexdiff> [ B<OPTIONS> ] F<old.tex> F<new.tex> > F<diff.tex> + +=head1 DESCRIPTION + +Briefly, I<latexdiff> is a utility program to aid in the management of +revisions of latex documents. It compares two valid latex files, here +called C<old.tex> and C<new.tex>, finds significant differences +between them (i.e., ignoring the number of white spaces and position +of line breaks), and adds special commands to highlight the +differences. Where visual highlighting is not possible, e.g. for changes +in the formatting, the differences are +nevertheless marked up in the source. + +The program treats the preamble differently from the main document. +Differences between the preambles are found using line-based +differencing (similarly to the Unix diff command, but ignoring white +spaces). A comment, "S<C<%DIF E<gt>>>" is appended to each added line, i.e. a +line present in C<new.tex> but not in C<old.tex>. Discarded lines + are deactivated by prepending "S<C<%DIF E<lt>>>". Changed blocks are preceded by +comment lines giving information about line numbers in the original files. Where there are insignificant +differences, the resulting file C<diff.tex> will be similar to +C<new.tex>. At the end of the preamble, the definitions for I<latexdiff> markup commands are inserted. +In differencing the main body of the text, I<latexdiff> attempts to +satisfy the following guidelines (in order of priority): + +=over 3 + +=item 1 + +If both C<old.tex> and C<new.tex> are valid LaTeX, then the resulting +C<diff.tex> should also be valid LateX. (NB If a few plain TeX commands +are used within C<old.tex> or C<new.tex> then C<diff.tex> is not +guaranteed to work but usually will). + +=item 2 + +Significant differences are determined on the level of +individual words. All significant differences, including differences +between comments should be clearly marked in the resulting source code +C<diff.tex>. + +=item 3 + +If a changed passage contains text or text-producing commands, then +running C<diff.tex> through LateX should produce output where added +and discarded passages are highlighted. + +=item 4 + +Where there are insignificant differences, e.g. in the positioning of +line breaks, C<diff.tex> should follow the formatting of C<new.tex> + +=back + +For differencing the same algorithm as I<diff> is used but words +instead of lines are compared. An attempt is made to recognize +blocks which are completely changed such that they can be marked up as a unit. +Comments are differenced line by line +but the number of spaces within comments is ignored. Commands including +all their arguments are generally compared as one unit, i.e., no mark-up +is inserted into the arguments of commands. However, for a selected +number of commands (for example, C<\caption> and all sectioning +commands) the last argument is known to be text. This text is +split into words and differenced just as ordinary text (use options to +show and change the list of text commands, see below). As the +algorithm has no detailed knowledge of LaTeX, it assumes all pairs of +curly braces immediately following a command (i.e. a sequence of +letters beginning with a backslash) are arguments for that command. +As a restriction to condition 1 above it is thus necessary to surround +all arguments with curly braces, and to not insert +extraneous spaces. For example, write + + \section{\textem{This is an emphasized section title}} + +and not + + \section {\textem{This is an emphasized section title}} + +or + + \section\textem{This is an emphasized section title} + +even though all varieties are the same to LaTeX (but see +B<--allow-spaces> option which allows the second variety). + +For environments whose content does not conform to standard LaTeX or +where graphical markup does not make sense all markup commands can be +removed by setting the PICTUREENV configuration variable, set by +default to C<picture> and C<DIFnomarkup> environments; see B<--config> +option). The latter environment (C<DIFnomarkup>) can be used to +protect parts of the latex file where the markup results in illegal +markup. You have to surround the offending passage in both the old and +new file by C<\begin{DIFnomarkup}> and C<\end{DIFnomarkup}>. You must +define the environment in the preambles of both old and new +documents. I prefer to define it as a null-environment, + +C<\newenvironment{DIFnomarkup}{}{}> + +but the choice is yours. Any markup within the environment will be +removed, and generally everything within the environment will just be +taken from the new file. + +It is also possible to difference files which do not have a preamble. + In this case, the file is processed in the main document +mode, but the definitions of the markup commands are not inserted. + +All markup commands inserted by I<latexdiff> begin with "C<\DIF>". Added +blocks containing words, commands or comments which are in C<new.tex> +but not in C<old.tex> are marked by C<\DIFaddbegin> and C<\DIFaddend>. +Discarded blocks are marked by C<\DIFdelbegin> and C<\DIFdelend>. +Within added blocks all text is highlighted with C<\DIFadd> like this: +C<\DIFadd{Added text block}> +Selected `safe' commands can be contained in these text blocks as well +(use options to show and change the list of safe commands, see below). +All other commands as well as braces "{" and "}" are never put within +the scope of C<\DIFadd>. Added comments are marked by prepending +"S<C<%DIF E<gt> >>". + +Within deleted blocks text is highlighted with C<\DIFdel>. Deleted +comments are marked by prepending "S<C<%DIF E<lt> >>". Non-safe command +and curly braces within deleted blocks are commented out with +"S<C<%DIFDELCMD E<lt> >>". + + + +=head1 OPTIONS + +=head2 Preamble + +The following options determine the visual markup style by adding the appropriate +command definitions to the preamble. See the end of this section for a description of +available styles. + +=over 4 + +=item B<--type=markupstyle> or +B<-t markupstyle> + +Add code to preamble for selected markup style. This option defines +C<\DIFadd> and C<\DIFdel> commands. +Available styles: + +C<UNDERLINE CTRADITIONAL TRADITIONAL CFONT FONTSTRIKE INVISIBLE +CHANGEBAR CCHANGEBAR CULINECHBAR CFONTCBHBAR> + +[ Default: C<UNDERLINE> ] + +=item B<--subtype=markstyle> or +B<-s markstyle> + +Add code to preamble for selected style for bracketing +commands (e.g. to mark changes in margin). This option defines +C<\DIFaddbegin>, C<\DIFaddend>, C<\DIFdelbegin> and C<\DIFdelend> commands. +Available styles: C<SAFE MARGINAL DVIPSCOL> + +[ Default: C<SAFE> ] + +=item B<--floattype=markstyle> or +B<-f markstyle> + +Add code to preamble for selected style which +replace standard marking and markup commands within floats +(e.g., marginal remarks cause an error within floats +so marginal marking can be disabled thus). This option defines all +C<\DIF...FL> commands. +Available styles: C<FLOATSAFE TRADITIONALSAFE IDENTICAL> + +[ Default: C<FLOATSAFE> ] + +=item B<--encoding=enc> or +B<-e enc> + +Specify encoding of old.tex and new.tex. Typical encodings are +C<ascii>, C<utf8>, C<latin1>, C<latin9>. A list of available encodings can be +obtained by executing + +C<perl -MEncode -e 'print join ("\n",Encode->encodings( ":all" )) ;' > + +[Default encoding is utf8 unless the first few lines of the preamble contain +an invocation C<\usepackage[..]{inputenc}> in which case the +encoding chosen by this command is asssumed. Note that ASCII (standard +latex) is a subset of utf8] + +=item B<--preamble=file> or +B<-p file> + +Insert file at end of preamble instead of generating +preamble. The preamble must define the following commands +C<\DIFaddbegin, \DIFaddend, \DIFadd{..}, +\DIFdelbegin,\DIFdelend,\DIFdel{..},> +and varieties for use within floats +C<\DIFaddbeginFL, \DIFaddendFL, \DIFaddFL{..}, +\DIFdelbeginFL, \DIFdelendFL, \DIFdelFL{..}> +(If this option is set B<-t>, B<-s>, and B<-f> options +are ignored.) + +=item B<--packages=pkg1,pkg2,..> + +Tell latexdiff that .tex file is processed with the packages in list +loaded. This is normally not necessary if the .tex file includes the +preamble, as the preamble is automatically scanned for C<\usepackage> commands. +Use of the B<--packages> option disables automatic scanning, so if for any +reason package specific parsing needs to be switched off, use B<--packages=none>. +The following packages trigger special behaviour: + +=over 8 + +=item C<amsmath> + +Configuration variable amsmath is set to C<align*> (Default: C<eqnarray*>) + +=item C<endfloat> + +Ensure that C<\begin{figure}> and C<\end{figure}> always appear by themselves on a line. + +=item C<hyperref> + +Change name of C<\DIFadd> and C<\DIFdel> commands to C<\DIFaddtex> and C<\DIFdeltex> and +define new C<\DIFadd> and C<\DIFdel> commands, which provide a wrapper for these commands, +using them for the text but not for the link defining command (where any markup would cause +errors). + +=back + +[ Default: scan the preamble for C<\\usepackage> commands to determine + loaded packages.] + + + +=item B<--show-preamble> + +Print generated or included preamble commands to stdout. + +=back + +=head2 Configuration + +=over 4 + +=item B<--exclude-safecmd=exclude-file> or +B<-A exclude-file> or B<--exclude-safecmd="cmd1,cmd2,..."> + +=item B<--replace-safecmd=replace-file> + +=item B<--append-safecmd=append-file> or +B<-a append-file> or B<--append-safecmd="cmd1,cmd2,..."> + +Exclude from, replace or append to the list of regular expressions (RegEx) +matching commands which are safe to use within the +scope of a C<\DIFadd> or C<\DIFdel> command. The file must contain +one Perl-RegEx per line (Comment lines beginning with # or % are +ignored). Note that the RegEx needs to match the whole of +the token, i.e., /^regex$/ is implied and that the initial +"\" of the command is not included. +The B<--exclude-safecmd> and B<--append-safecmd> options can be combined with the -B<--replace-safecmd> +option and can be used repeatedly to add cumulatively to the lists. + B<--exclude-safecmd> +and B<--append-safecmd> can also take a comma separated list as input. If a +comma for one of the regex is required, escape it thus "\,". In most cases it +will be necessary to protect the comma-separated list from the shell by putting +it in quotation marks. + +=item B<--exclude-textcmd=exclude-file> or +B<-X exclude-file> or B<--exclude-textcmd="cmd1,cmd2,..."> + +=item B<--replace-textcmd=replace-file> + +=item B<--append-textcmd=append-file> or +B<-x append-file> or B<--append-textcmd="cmd1,cmd2,..."> + +Exclude from, replace or append to the list of regular expressions +matching commands whose last argument is text. See +entry for B<--exclude-safecmd> directly above for further details. + + +=item B<--replace-context1cmd=replace-file> + +=item B<--append-context1cmd=append-file> or +=item B<--append-context1cmd="cmd1,cmd2,..."> + +Replace or append to the list of regex matching commands +whose last argument is text but which require a particular +context to work, e.g. \caption will only work within a figure +or table. These commands behave like text commands, except when +they occur in a deleted section, when they are disabled, but their +argument is shown as deleted text. + +=item B<--replace-context1cmd=replace-file> + +=item B<--append-context2cmd=append-file> or +=item B<--append-context2cmd="cmd1,cmd2,..."> +As corresponding commands for context1. The only difference is that +context2 commands are completely disabled in deleted sections, including +their arguments. + + + +=item B<--config var1=val1,var2=val2,...> or B<-c var1=val1,..> + +=item B<-c configfile> + +Set configuration variables. The option can be repeated to set different +variables (as an alternative to the comma-separated list). +Available variables (see below for further explanations): + +C<MINWORDSBLOCK> (integer) + +C<FLOATENV> (RegEx) + +C<PICTUREENV> (RegEx) + +C<MATHENV> (RegEx) + +C<MATHREPL> (String) + +C<MATHARRENV> (RegEx) + +C<MATHARRREPL> (String) + +C<ARRENV> (RegEx) + +C<COUNTERCMD> (RegEx) + +=item B<--show-safecmd> + +Print list of RegEx matching and excluding safe commands. + +=item B<--show-textcmd> + +Print list of RegEx matching and excluding commands with text argument. + +=item B<--show-config> + +Show values of configuration variables. + +=item B<--show-all> + +Combine all --show commands. + +NB For all --show commands, no C<old.tex> or C<new.tex> file needs to be specified, and no +differencing takes place. + + +=back + +=head2 Miscellaneous + +=over 4 + +=item B<--verbose> or B<-V> + +Output various status information to stderr during processing. +Default is to work silently. + +=item B<--ignore-warnings> + +Suppress warnings about inconsistencies in length between input and +parsed strings and missing characters. These warning messages are +often related to non-standard latex or latex constructions with a +syntax unknown to C<latexdiff> but the resulting difference argument +is often fully functional anyway, particularly if the non-standard +latex only occurs in parts of the text which have not changed. + +=item B<--allow-spaces> + +Allow spaces between bracketed or braced arguments to commands. Note +that this option might have undesirable side effects (unrelated scope +might get lumpeded with preceding commands) so should only be used if the +default produces erroneous results. (Default requires arguments to +directly follow each other without intervening spaces). + +=item B<--flatten> + +Replace C<\input> and C<\include> commands within body by the content +of the files in their argument. If C<\includeonly> is present in the +preamble, only those files are expanded into the document. However, +no recursion is done, i.e. C<\input> and C<\include> commands within +included sections are not expanded. The included files are assumed to + be located in the same directories as the old and new master files, +respectively, making it possible to organise files into old and new directories. + +Use of this option is not recommended +primarily the processing time for the large documents is prohibitive, and +the resulting difference document no longer reflects the structure of the +input documents. + +=item B<--help> or +B<-h> + +Show help text + +=item B<--version> + +Show version number + +=back + +=head2 Predefined styles + +=head2 Major types + +The major type determine the markup of plain text and some selected latex commands outside floats by defining the markup commands C<\DIFadd{...}> and C<\DIFdel{...}> . + +=over 10 + +=item C<UNDERLINE> + +Added text is wavy-underlined and blue, discarded text is struck out and red +(Requires color and ulem packages). Overstriking does not work in displayed math equations such that deleted parts of equation are underlined, not struck out (this is a shortcoming inherent to the ulem package). + +=item C<CTRADITIONAL> + +Added text is blue and set in sans-serif, and a red footnote is created for each discarded +piece of text. (Requires color package) + +=item C<TRADITIONAL> + +Like C<CTRADITIONAL> but without the use of color. + +=item C<CFONT> + +Added text is blue and set in sans-serif, and discarded text is red and very small size. + +=item C<FONTSTRIKE> + +Added tex is set in sans-serif, discarded text small and struck out + +=item C<CCHANGEBAR> + +Added text is blue, and discarded text is red. Additionally, the changed text is marked with a bar in the margin (Requires color and changebar packages). + +=item C<CFONTCHBAR> + +Like C<CFONT> but with additional changebars (Requires color and changebar packages). + +=item C<CULINECHBAR> + +Like C<UNDERLINE> but with additional changebars (Requires color, ulem and changebar packages). + +=item C<CHANGEBAR> + +No mark up of text, but mark margins with changebars (Requires changebar package). + +=item C<INVISIBLE> + +No visible markup (but generic markup commands will still be inserted. + +=back + +=head2 Subtypes + +The subtype defines the commands that are inserted at the begin and end of added or discarded blocks, irrespectively of whether these blocks contain text or commands (Defined commands: C<\DIFaddbegin, \DIFaddend, \DIFdelbegin, \DIFdelend>) + +=over 10 + +=item C<SAFE> + +No additional markup (Recommended choice) + +=item C<MARGIN> + +Mark beginning and end of changed blocks with symbols in the margin nearby (using +the standard C<\marginpar> command - note that this sometimes moves somewhat +from the intended position. + +=item C<DVIPSCOL> + +An alternative way of marking added passages in blue, and deleted ones in red. Note +that C<DVIPSCOL> only works with the dvips converter, e.g. not pdflatex. +(it is recommeneded to use instead the main types to effect colored markup, +although in some cases coloring with dvipscol can be more complete). + +=back + +=head2 Float Types + +Some of the markup used in the main text might cause problems when used within +floats (e.g. figures or tables). For this reason alternative versions of all +markup commands are used within floats. The float type defines these alternative commands. + +=over 10 + +=item C<FLOATSAFE> + +Use identical markup for text as in the main body, but set all commands marking the begin and end of changed blocks to null-commands. You have to choose this float type if your subtype is C<MARGIN> as C<\marginpar> does not work properly within floats. + +=item C<TRADITIONALSAFE> + +Mark additions the same way as in the main text. Deleted environments are marked by angular brackets \[ and \] and the deleted text is set in scriptscript size. This float type should always be used with the C<TRADITIONAL> and C<CTRADITIONAL> markup types as the \footnote command does not work properly in floating environments. + +=item C<IDENTICAL> + +Make no difference between the main text and floats. + +=back + + +=head2 Configuration Variables + +=over 10 + +=item C<MINWORDSBLOCK> + +Minimum number of tokens required to form an independent block. This value is +used in the algorithm to detect changes of complete blocks by merging identical text parts of less than C<MINWORDSBLOCK> to the preceding added and discarded parts. + +[ Default: 3 ] + +=item C<FLOATENV> + +Environments whose name matches the regular expression in C<FLOATENV> are +considered floats. Within these environments, the I<latexdiff> markup commands +are replaced by their FL variaties. + +[ Default: S<C<(?:figure|table|plate)[\w\d*@]*> >] + +=item C<PICTUREENV> + +Within environments whose name matches the regular expression in C<PICTUREENV> +all latexdiff markup is removed (in pathologic cases this might lead to + inconsistent markup but this situation should be rare). + +[ Default: S<C<(?:picture|DIFnomarkup)[\w\d*@]*> >] + +=item C<MATHENV>,C<MATHREPL> + +If both \begin and \end for a math environment (environment name matching C<MATHENV> +or \[ and \]) +are within the same deleted block, they are replaced by a \begin and \end commands for C<MATHREPL> +rather than being commented out. + +[ Default: C<MATHENV>=S<C<(?:displaymath|equation)> >, C<MATHREPL>=S<C<displaymath> >] + +=item C<MATHARRENV>,C<MATHARRREPL> + +as C<MATHENV>,C<MATHREPL> but for equation arrays + +[ Default: C<MATHARRENV>=S<C<eqnarray\*?> >, C<MATHREPL>=S<C<eqnarray> >] + +=item C<ARRENV> + +If a match to C<ARRENV> is found within an inline math environment within a deleted or added block, then the inlined math +is surrounded by C<\mbox{>...C<}>. This is necessary as underlining does not work within inlined array environments. + +[ Default: C<ARRENV>=S<C<(?:array|[pbvBV]matrix)> > + +=item C<COUNTERCMD> + +If a command in a deleted block which is also in the textcmd list matches C<COUNTERCMD> then an +additional command C<\addtocounter{>F<cntcmd>C<}{-1}>, where F<cntcmd> is the matching command, is appended in the diff file such that the numbering in the diff file remains synchronized with the +numbering in the new file. + +[ Default: C<COUNTERCMD>=C<(?:footnote|part|section|subsection> ... + +C<|subsubsection|paragraph|subparagraph)> ] + +=back + +=head1 BUGS + +UTF-8 support requires a relatively new version of perl (5.8.0 is sufficient +but 5.6.2 up would work OK, too). + +Option allow-spaces not implemented entirely consistently. It breaks +the rules that number and type of white space does not matter, as +different numbers of inter-argument spaces are treated as significant. + +Please send bug reports +to I<tilmann@esc.cam.ac.uk>. Include the serial number of I<latexdiff> +(from comments at the top of the source or use B<--version>). If you come across latex +files that are error-free and conform to the specifications set out +above, and whose differencing still does not result in error-free +latex, please send me those files, ideally edited to only contain the +offending passage as long as that still reproduces the problem. + +=head1 SEE ALSO + +L<latexrevise> + +=head1 PORTABILITY + +I<latexdiff> does not make use of external commands and thus should run +on any platform supporting Perl 5.6 or higher. If files with encodings +other than ASCII or UTF-8 are processed, Perl 5.8 or higher is required. + +The standard version of I<latexdiff> requires installation of the Perl package +C<Algorithm::Diff> (available from I<www.cpan.org> - +I<http://search.cpan.org/~nedkonz/Algorithm-Diff-1.15>) but a stand-alone +version, I<latexdiff-so>, which has this package inlined, is available, too. +I<latexdiff-fast> requires the I<diff> command to be present. + +=head1 AUTHOR + +Copyright (C) 2004-2007 Frederik Tilmann + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License Version 2 + +Contributors of fixes and additions: V. Kuhlmann, J. Paisley, N. Becker, T. Doerges, K. Huebner +Thanks to everyone who send in bug reports. + +=cut + +__END__ +%%BEGIN SAFE COMMANDS +% Regex matching commands which can safely be in the +% argument of a \DIFadd or \DIFdel command (leave out the \) +arabic +dashbox +emph +fbox +framebox +hspace +math.* +makebox +mbox +pageref +ref +symbol +raisebox +rule +text.* +shortstack +usebox +dag +ddag +copyright +pounds +S +P +oe +OE +ae +AE +aa +AA +o +O +l +L +frac +ss +sqrt +ldots +cdots +vdots +ddots +alpha +beta +gamma +delta +epsilon +varepsilon +zeta +eta +theta +vartheta +iota +kappa +lambda +mu +nu +xi +pi +varpi +rho +varrho +sigma +varsigma +tau +upsilon +phi +varphi +chi +psi +omega +Gamma +Delta +Theta +Lambda +Xi +Pi +Sigma +Upsilon +Phi +Psi +Omega +ps +mp +times +div +ast +star +circ +bullet +cdot +cap +cup +uplus +sqcap +vee +wedge +setminus +wr +diamond +(?:big)?triangle.* +lhd +rhd +unlhd +unrhd +oplus +ominus +otimes +oslash +odot +bigcirc +d?dagger +amalg +leq +prec +preceq +ll +(?:sq)?su[bp]set(?:eq)? +in +vdash +geq +succ(?:eq)? +gg +ni +dashv +equiv +sim(?:eq)? +asymp +approx +cong +neq +doteq +propto +models +perp +mid +parallel +bowtie +Join +smile +frown +.*arrow +(?:long)?mapsto +.*harpoon.* +leadsto +aleph +hbar +imath +jmath +ell +wp +Re +Im +mho +prime +emptyset +nabla +surd +top +bot +angle +forall +exists +neg +flat +natural +sharp +backslash +partial +infty +Box +Diamond +triangle +clubsuit +diamondsuit +heartsuit +spadesuit +sum +prod +coprod +int +oint +big(?:sq)?c[au]p +bigvee +bigwedge +bigodot +bigotimes +bigoplus +biguplus +(?:arc)?(?:cos|sin|tan|cot)h? +csc +arg +deg +det +dim +exp +gcd +hom +inf +ker +lg +lim +liminf +limsup +ln +log +max +min +Pr +sec +sup +(SUPER|SUB)SCRIPTNB +(SUPER|SUB)SCRIPT +%%END SAFE COMMANDS + +%%BEGIN TEXT COMMANDS +% Regex matching commands with a text argument (leave out the \) +addcontents.* +cc +closing +chapter +dashbox +emph +encl +fbox +framebox +footnote +footnotetext +framebox +part +(sub){0,2}section\*? +(sub)?paragraph\*? +makebox +mbox +opening +parbox +raisebox +savebox +sbox +shortstack +signature +text.* +value +underline +sqrt +(SUPER|SUB)SCRIPT +%%END TEXT COMMANDS + +%%BEGIN CONTEXT1 COMMANDS +% Regex matching commands with a text argument (leave out the \), which will fail out of context, but who's arguemtn should be printed as plain text +caption +%%END CONTEXT1 COMMANDS + +%%BEGIN CONTEXT2 COMMANDS +% Regex matching commands with a text argument (leave out the \), which will fail out of context, but who's arguemtn should be printed as plain text +title +author +%%END CONTEXT2 COMMANDS + + +%% TYPES (Commands for highlighting changed blocks) + +%DIF UNDERLINE PREAMBLE +\RequirePackage[normalem]{ulem} +\RequirePackage{color}\definecolor{RED}{rgb}{1,0,0}\definecolor{BLUE}{rgb}{0,0,1} +\providecommand{\DIFadd}[1]{{\protect\color{blue}\uwave{#1}}} +\providecommand{\DIFdel}[1]{{\protect\color{red}\sout{#1}}} +%DIF END UNDERLINE PREAMBLE + +%DIF CTRADITIONAL PREAMBLE +\RequirePackage{color}\definecolor{RED}{rgb}{1,0,0}\definecolor{BLUE}{rgb}{0,0,1} +\RequirePackage[stable]{footmisc} +\providecommand{\DIFadd}[1]{{\protect\color{blue} \sf #1}} +\providecommand{\DIFdel}[1]{{\protect\color{red} [..\footnote{removed: #1} ]}} +%DIF END CTRADITIONAL PREAMBLE + +%DIF TRADITIONAL PREAMBLE +\RequirePackage[stable]{footmisc} +\providecommand{\DIFadd}[1]{{\sf #1}} +\providecommand{\DIFdel}[1]{{[..\footnote{removed: #1} ]}} +%DIF END TRADITIONAL PREAMBLE + +%DIF CFONT PREAMBLE +\RequirePackage{color}\definecolor{RED}{rgb}{1,0,0}\definecolor{BLUE}{rgb}{0,0,1} +\providecommand{\DIFadd}[1]{{\protect\color{blue} \sf #1}} +\providecommand{\DIFdel}[1]{{\protect\color{red} \scriptsize #1}} +%DIF END CFONT PREAMBLE + +%DIF FONTSTRIKE PREAMBLE +\RequirePackage[normalem]{ulem} +\providecommand{\DIFadd}[1]{{\sf #1}} +\providecommand{\DIFdel}[1]{{\footnotesize \sout{#1}}} +%DIF END FONTSTRIKE PREAMBLE + +%DIF CCHANGEBAR PREAMBLE +\RequirePackage[dvips]{changebar} +\RequirePackage{color}\definecolor{RED}{rgb}{1,0,0}\definecolor{BLUE}{rgb}{0,0,1} +\providecommand{\DIFadd}[1]{\protect\cbstart{\protect\color{blue}#1}\protect\cbend} +\providecommand{\DIFdel}[1]{\protect\cbdelete{\protect\color{red}#1}\protect\cbdelete} +%DIF END CCHANGEBAR PREAMBLE + +%DIF CFONTCHBAR PREAMBLE +\RequirePackage[dvips]{changebar} +\RequirePackage{color}\definecolor{RED}{rgb}{1,0,0}\definecolor{BLUE}{rgb}{0,0,1} +\providecommand{\DIFadd}[1]{\protect\cbstart{\protect\color{blue}\sf #1}\protect\cbend} +\providecommand{\DIFdel}[1]{\protect\cbdelete{\protect\color{red}\scriptsize #1}\protect\cbdelete} +%DIF END CFONTCHBAR PREAMBLE + +%DIF CULINECHBAR PREAMBLE +\RequirePackage[normalem]{ulem} +\RequirePackage[dvips]{changebar} +\RequirePackage{color} +\providecommand{\DIFadd}[1]{\protect\cbstart{\protect\color{blue}\uwave{#1}}\protect\cbend} +\providecommand{\DIFdel}[1]{\protect\cbdelete{\protect\color{red}\sout{#1}}\protect\cbdelete} +%DIF END CULINECHBAR PREAMBLE + +%DIF CHANGEBAR PREAMBLE +\RequirePackage[dvips]{changebar} +\providecommand{\DIFadd}[1]{\protect\cbstart{#1}\protect\cbend} +\providecommand{\DIFdel}[1]{\protect\cbdelete} +%DIF END CHANGEBAR PREAMBLE + +%DIF INVISIBLE PREAMBLE +\providecommand{\DIFadd}[1]{#1} +\providecommand{\DIFdel}[1]{} +%DIF END INVISIBLE PREAMBLE + + +%% SUBTYPES (Markers for beginning and end of changed blocks) + +%DIF SAFE PREAMBLE +\providecommand{\DIFaddbegin}{} +\providecommand{\DIFaddend}{} +\providecommand{\DIFdelbegin}{} +\providecommand{\DIFdelend}{} +%DIF END SAFE PREAMBLE + +%DIF MARGIN PREAMBLE +\providecommand{\DIFaddbegin}{\protect\marginpar{a[}} +\providecommand{\DIFaddend}{\protect\marginpar{]}} +\providecommand{\DIFdelbegin}{\protect\marginpar{d[}} +\providecommand{\DIFdelend}{\protect\marginpar{]}} +%DIF END BRACKET PREAMBLE + +%DIF DVIPSCOL PREAMBLE +%Note: only works with dvips converter +\RequirePackage{color} +\RequirePackage{dvipscol} +\providecommand{\DIFaddbegin}{\protect\nogroupcolor{blue}} +\providecommand{\DIFaddend}{\protect\nogroupcolor{black}} +\providecommand{\DIFdelbegin}{\protect\nogroupcolor{red}} +\providecommand{\DIFdelend}{\protect\nogroupcolor{black}} +%DIF END DVIPSCOL PREAMBLE + + +%% FLOAT TYPES + +%DIF FLOATSAFE PREAMBLE +\providecommand{\DIFaddFL}[1]{\DIFadd{#1}} +\providecommand{\DIFdelFL}[1]{\DIFdel{#1}} +\providecommand{\DIFaddbeginFL}{} +\providecommand{\DIFaddendFL}{} +\providecommand{\DIFdelbeginFL}{} +\providecommand{\DIFdelendFL}{} +%DIF END FLOATSAFE PREAMBLE + +%DIF IDENTICAL PREAMBLE +\providecommand{\DIFaddFL}[1]{\DIFadd{#1}} +\providecommand{\DIFdelFL}[1]{\DIFdel{#1}} +\providecommand{\DIFaddbeginFL}{\DIFaddbegin} +\providecommand{\DIFaddendFL}{\DIFaddend} +\providecommand{\DIFdelbeginFL}{\DIFdelbegin} +\providecommand{\DIFdelendFL}{\DIFdelend} +%DIF END IDENTICAL PREAMBLE + +%DIF TRADITIONALSAFE PREAMBLE +% procidecommand color to make this work for TRADITIONAL and CTRADITIONAL +\providecommand{\color}[1]{} +\providecommand{\DIFaddFL}[1]{\DIFadd{#1}} +\providecommand{\DIFdel}[1]{{\protect\color{red}[..{\scriptsize {removed: #1}} ]}} +\providecommand{\DIFaddbeginFL}{} +\providecommand{\DIFaddendFL}{} +\providecommand{\DIFdelbeginFL}{} +\providecommand{\DIFdelendFL}{} +%DIF END FLOATSAFE PREAMBLE + +%% SPECIAL PACKAGE PREAMBLE COMMANDS + +% Standard \DIFadd and \DIFdel are redefined as \DIFaddtex and \DIFdeltex +% when hyperref package is included. +%DIF HYPERREF PREAMBLE +\providecommand{\DIFadd}[1]{\texorpdfstring{\DIFaddtex{#1}}{#1}} +\providecommand{\DIFdel}[1]{\texorpdfstring{\DIFdeltex{#1}}{}} +%DIF END HYPERREF PACKAGE diff --git a/Master/texmf-dist/scripts/latexdiff/latexrevise b/Master/texmf-dist/scripts/latexdiff/latexrevise new file mode 100755 index 00000000000..279dac1fc6a --- /dev/null +++ b/Master/texmf-dist/scripts/latexdiff/latexrevise @@ -0,0 +1,523 @@ +#!/usr/bin/perl -w +# latexrevise - takes output file of latexdiff and removes either discarded +# or appended passages, then deletes all other latexdiff markup +# +# Copyright (C) 2004 F J Tilmann (tilmann@esc.cam.ac.uk) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License Version 2 as published by +# the Free Software Foundation. +# +# 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 +# +# Detailed usage information at the end of the file +# +# Version 0.1 First public release +# Version 0.3 Updated for compatibility with latexdiff 0.3 output (DIFAUXCMD removal) +# + +use Getopt::Long ; +use strict; + +# Markup strings (make sure that this are set to the same value as in +# latexdiff) +# If at all possible, do not change these as parts of the program +# depend on the actual name. +# At the very least adapt subroutine postprocess to new tokens. + +# note: have to use three backslashes to mean one literal one (reason +# unknown +my $ADDMARKOPEN='\\\DIFaddbegin(?:FL)? '; # Token to mark begin of appended text +my $ADDMARKCLOSE='\\\DIFaddend(?:FL)?(?: |\z)'; # Token to mark end of appended text (|\z only deals with the case of \DIFaddend right before \end{document} +my $ADDOPEN='\\\DIFadd(?:FL)?{'; # To mark begin of added text passage +my $ADDCLOSE='}'; # To mark end of added text passage +my $ADDCOMMENT='DIF > '; # To mark added comment line +my $DELMARKOPEN='\\\DIFdelbegin(?:FL)? '; # Token to mark begin of deleted text +my $DELMARKCLOSE='\\\DIFdelend(?:FL)?(?: |\z)'; # Token to mark end of deleted text +my $DELOPEN='\\\DIFdel(?:FL)?{'; # To mark begin of deleted text passage +my $DELCLOSE='}'; # To mark end of deleted text passage +my $ALTEXT='FL'; # string which might be appended to above commands +my $DELCMDOPEN='%DIFDELCMD < '; # To mark begin of deleted commands (must begin with %, i.e., be a comment +my $DELCMDCLOSE="%%%\n"; # To mark end of deleted commands (must end with a new line) +my $AUXCMD='%DIFAUXCMD' ; # follows auxiliary commands put in by latexdiff to make difference file legal +my $DELCOMMENT='DIF < '; # To mark deleted comment line + +my $PREAMBLEXTBEG='^%DIF PREAMBLE EXTENSION ADDED BY LATEXDIFF$'; +my $PREAMBLEXTEND='^%DIF END PREAMBLE EXTENSION ADDED BY LATEXDIFF$'; + + +my $pat0 = '(?:[^{}]|\\\{|\\\})*'; +my $pat1 = '(?:[^{}]|\\\{|\\\}|\{'.$pat0.'\})*'; +my $pat2 = '(?:[^{}]|\\\{|\\\}|\{'.$pat1.'\})*'; +my $pat3 = '(?:[^{}]|\\\{|\\\}|\{'.$pat2.'\})*'; +my $pat4 = '(?:[^{}]|\\\{|\\\}|\{'.$pat3.'\})*'; +my $brat0 = '(?:[^\[\]]|\\\[|\\\])*'; + + +my ($input,$preamble,$body,$post); +my (@matches); +my ($cnt,$prematch,$postmatch); +my ($help); +my ($verbose,$quiet,$accept,$decline,$simplify)=(0,0,0,0,0); +my ($comment,$comenv,$markup,$markenv); + +# A word unlikely ever to be used in a real latex file +my $someword='gobbledegooksygook'; + +Getopt::Long::Configure('bundling'); +GetOptions('accept|a' => \$accept, + 'decline|d'=> \$decline, + 'simplify|s' => \$simplify, + 'comment|c=s' => \$comment, + 'comment-environment|e=s' => \$comenv, + 'markup|m=s' => \$markup, + 'markup-environment|n=s' => \$markenv, + 'no-warnings|q' => \$verbose, + 'verbose|V' => \$verbose, + 'help|h|H' => \$help); + +if ( $help ) { + usage() ; +} + +if ( ($accept && $decline) || ($accept && $simplify) || ($decline && $simplify) ) { + die '-a,-d and -s options are mutually axclusive. Type latexrevise -h to get more help.'; +} + + + +print STDERR "ACCEPT mode\n" if $verbose && $accept; +print STDERR "DECLINE mode\n" if $verbose && $decline; +print STDERR "SIMPLIFY mode. WARNING: The output will not normally be valid latex,\n" if $verbose && $simplify; + +# Slurp old and new files +{ + local $/ ; # locally set record operator to undefined, ie. enable whole-file mode + $input=<>; +} + +# split into parts +($preamble,$body,$post)=splitdoc($input,'\begin{document}','\end{document}'); + +if (length $preamble && ( $accept || $decline ) ) { + # + # WORK ON PREAMBLE + # + # (compare subroutine linediff in latexdiff to make sure correct strings are used) + + # remove extra commands added to preamble by latexdiff + $preamble =~ s/${PREAMBLEXTBEG}.*?${PREAMBLEXTEND}\n{0,1}//smg ; + + if ( $accept ) { + # delete mark up in appended lines + $preamble =~ s/^(.*) %DIF > $/$1/mg ; + } elsif ( $decline ) { + # delete appended lines + # $preamble =~ s/^(.*) %DIF > $//mg ; + $preamble =~ s/^(.*) %DIF > \n//mg ; + # delete markup in deleted lines + $preamble =~ s/^%DIF < //mg ; + } + # remove any remaining DIF markups + #$preamble =~ s/%DIF.*$//mg ; + $preamble =~ s/%DIF.*?\n//sg ; +} +#print $preamble ; + +# +# WORK ON BODY +# +if ($accept) { + # remove ADDMARKOPEN, ADDMARKCLOSE tokens + @matches= $body =~ m/${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}/sg; + checkpure(@matches); + $body =~ s/${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}/$1/sg; + # remove text flanked by DELMARKOPEN, DELMARKCLOSE tokens + @matches= $body =~ m/${DELMARKOPEN}(.*?)${DELMARKCLOSE}/sg; + checkpure(@matches); + $body =~ s/${DELMARKOPEN}(.*?)${DELMARKCLOSE}//sg; + # remove markup of added comments + $body =~ s/%${ADDCOMMENT}(.*?)$/%$1/mg ; + # remove deleted comments (full line) + $body =~ s/^%${DELCOMMENT}.*?\n//mg ; + # remove deleted comments (part of line) + $body =~ s/%${DELCOMMENT}.*?$//mg ; +} +elsif ( $decline) { + # remove DELMARKOPEN, DELMARKCLOSE tokens + @matches= $body =~ m/${DELMARKOPEN}(.*?)${DELMARKCLOSE}/sg; + checkpure(@matches); + $body =~ s/${DELMARKOPEN}(.*?)${DELMARKCLOSE}/$1/sg; + # remove text flanked by ADDMARKOPEN, ADDMARKCLOSE tokens + # as latexdiff algorithm keeps the formatting and white spaces + # of the new text, sometimes whitespace might be inserted or + # removed inappropriately. We try to guess whether this has + # happened + + # Mop up tokens. This must be done already now as otherwise + # detection of white-space problems does not work + $cnt = $body =~ s/${DELOPEN}($pat4)${DELCLOSE}/$1/sg; + # remove markup of deleted commands + $cnt += $body =~ s/${DELCMDOPEN}(.*?)${DELCMDCLOSE}/$1/sg ; + $cnt += $body =~ s/${DELCMDOPEN}//g ; + # remove aux commands + $cnt += $body =~ s/^.*${AUXCMD}$/${someword}/mg; $body =~ s/${someword}\n//g; + + while ( $body =~ m/${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}/s ) { + $prematch=$`; + $postmatch=$'; + checkpure($1); + if ( $prematch =~ /\w$/s && $postmatch =~ /^\w/ ) { + # apparently no white-space between word=>Insert white space + $body =~ s/${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}/ /s ; + } + elsif ( $prematch =~ /\s$/s && $postmatch =~ /^[.,;:]/ ) { + # space immediately before one of ".,:;" => remove this space + $body =~ s/\s${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}//s ; + } + else { + # do not insert or remove any extras + $body =~ s/${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}//s; + } + } +# Alternative without special cases treatment +# @matches= $body =~ m/${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}/sg; +# checkpure(@matches); +# $body =~ s/${ADDMARKOPEN}(.*?)${ADDMARKCLOSE}//sg; + # remove markup of deleted comments + $body =~ s/%${DELCOMMENT}(.*?)$/%$1/mg ; + # remove added comments (full line) + $body =~ s/^%${ADDCOMMENT}.*?\n//mg ; + # remove added comments (part of line) + $body =~ s/%${ADDCOMMENT}.*?$//mg ; +} + +# remove any remaining tokens +if ( $accept || $decline || $simplify ) { + # first substitution command deals with special case of added paragraph + $cnt = $body =~ s/${ADDOPEN}($pat4)\n${ADDCLOSE}\n/$1\n/sg; + $cnt += $body =~ s/${ADDOPEN}($pat4)${ADDCLOSE}/$1/sg; + $cnt==0 || warn 'Remaining $ADDOPEN tokens in DECLINE mode\n' unless ( $quiet || $accept || $simplify ); +} +if ($accept || $simplify ) { + # Note: in decline mode these commands have already been removed above + $cnt = $body =~ s/${DELOPEN}($pat4)${DELCLOSE}/$1/sg; + #### remove markup of deleted commands + $cnt += $body =~ s/${DELCMDOPEN}(.*?)${DELCMDCLOSE}/$1/sg ; + $cnt += $body =~ s/${DELCMDOPEN}//g ; + # remove aux commands + # $cnt += + $body =~ s/^.*${AUXCMD}$/${someword}/mg; $body =~ s/${someword}\n//g; + + #### remove deleted comments + ###$cnt += $body =~ s/${DIFDELCMD}.*?$//mg ; + $cnt==0 || warn 'Remaining $DELOPEN or $DIFDELCMD tokens in ACCEPT mode\n' unless ( $quiet || $simplify ); +} + +# Remove comment commands +if (defined($comment)) { + print STDERR "Removing \\$comment\{..\} sequences ..." if $verbose; + # protect $comments in comments by making them look different + $body =~ s/(%.*)${comment}(.*)$/$1${someword}$2/mg ; + # carry out the substitution + $cnt = 0 + $body =~ s/\\${comment}(?:\[${brat0}\])?\{${pat4}\}//sg ; + print STDERR "$cnt matches found and removed.\n" if $verbose; + # and undo the protection substitution + $body =~ s/(%.*)${someword}(.*)$/$1${comment}$2/mg ; +} +if (defined($comenv)) { + print STDERR "Removing $comenv environments ..." if $verbose; + $body =~ s/(%.*)${comenv}/$1${someword}/mg ; +## $cnt = 0 + $body =~ s/\\begin(?:\[${brat0}\])?\{\$comenv\}.*?\\end\{\$comenv\}//sg ; + $cnt = 0 + $body =~ s/\\begin(?:\[${brat0}\])?\{${comenv}\}.*?\\end\{${comenv}\}\s*?\n//sg ; + print STDERR "$cnt matches found and removed.\n" if $verbose; + $body =~ s/(%.*)${someword}/$1${comenv}/mg ; +} + +if (defined($markup)) { + print STDERR "Removing \\$markup\{..\} cpmmands ..." if $verbose; + # protect $markups in comments by making them look different + $body =~ s/(%.*)${markup}(.*)$/$1${someword}$2/mg ; + # carry out the substitution + $cnt = 0 + $body =~ s/\\${markup}(?:\[${brat0}\])?\{(${pat4})\}/$1/sg ; + print STDERR "$cnt matches found and removed.\n" if $verbose; + # and undo the protection substitution + $body =~ s/(%.*)${someword}(.*)$/$1${markup}$2/mg ; +} +if (defined($markenv)) { + print STDERR "Removing $markenv environments ..." if $verbose; + $body =~ s/(%.*)${markenv}/$1${someword}/mg ; + $cnt = 0 + $body =~ s/\\begin(?:\[${brat0}\])?\{${markenv}\}\n?//sg; + $cnt += 0 + $body =~ s/\\end\{${markenv}\}\n?//sg; + print STDERR $cnt/2, " matches found and removed.\n" if $verbose; + $body =~ s/(%.*)${someword}/$1${markenv}/mg ; +} + + +if ( length $preamble ) { + print "$preamble\\begin{document}${body}\\end{document}$post"; +} else { + print $body; +} + +# checkpure(@matches) +# checks whether any of the strings in matches contains +# $ADDMARKOPEN, $ADDMARKCLOSE,$DELMARKOPEN, or $DELMARKCLOSE +# If so, die reporting nesting problems, otherwise return to caller +sub checkpure { + while (defined($_=shift)) { + if ( /$ADDMARKOPEN/ || /$ADDMARKCLOSE/ + || /$DELMARKOPEN/ || /$DELMARKCLOSE/ ) { + die <<EOF ; +There is a problem with nesting of $ADDMARKOPEN, $ADDMARKCLOSE, +$DELMARKOPEN, and $DELMARKCLOSE tokens that prevents me from +processing the file. Exiting ... +EOF + } + } +} + +# ($part1,$part2,$part3)=splitdoc($text,$word1,$word2) +# splits $text into 3 parts at $word1 and $word2. +# if neither $word1 nor $word2 exist, $part1 and $part3 are empty, $part2 is $text +# If only $word1 or $word2 exist but not the other, output an error message. +# +# same subroutine as in latexdiff +sub splitdoc { + my ($text,$word1,$word2)=@_; + my $l1 = length $word1 ; + my $l2 = length $word2 ; + + my $i = index($text,$word1); + my $j = index($text,$word2); + + my ($part1,$part2,$part3)=("","",""); + + if ( $i<0 && $j<0) { + # no $word1 or $word2 + print STDERR "Old Document not a complete latex file. Assuming it is a tex file with no preamble.\n"; + $part2 = $text; + } elsif ( $i>=0 && $j>$i ) { + $part1 = substr($text,0,$i) ; + $part2 = substr($text,$i+$l1,$j-$i-$l1); + $part3 = substr($text,$j+$l2) unless $j+$l2 >= length $text; + } else { + die "$word1 or $word2 not in the correct order or not present as a pair." + } + return ($part1,$part2,$part3); +} + + + +sub usage { + die <<"EOF"; +Usage: $0 [OPTIONS] [diff.tex] > revised.tex + +Read a file diff.tex (output of latexdiff), and remove its markup. +If no filename is given read from standard input. The command can be used +in ACCEPT, DECLINE, or SIMPLIFY mode, and be used to remove user-defined +latex commands from the input (see options -c, -e, -m, -n below). +In ACCEPT mode, all appended text fragments (or preamble lines) +are kept, and all discarded text fragments (or preamble lines) are +deleted. +In DECLINE mode, all discarded text fragments are kept, and all appended +text fragments are deleted. +If you wish to keep some changes, edit the diff.tex file in +advance, and manually remove those tokens which would otherwise be +deleted. Note that latexrevise only pays attention to the \\DIFaddbegin, +\\DIFaddend, \\DIFdelbegin, and \\DIFdelend tokens and corresponding FL +varieties. All \\DIFadd and \\DIFdel commands (but not their content) are +simply deleted. The commands added by latexdiff to the preamble are also +removed. +In SIMPLIFY mode all latexdiff markup is removed from the body of the text (after +\\begin{document}) except for \\DIFaddbegin, \\DIFaddend, \\DIFdelbegin, \\DIFdelend +tokens and the corresponding FL varieties of those commands. The result +will not in general be valid latex-code but might be easier to read and edit in +preparation for a subsequent run in ACCEPT or DECLINE mode. +In SIMPLIFY mode the preamble is left unmodified. + +-a +--accept Run in ACCEPT mode (delete all blocks marked by \\DIFdelbegin + and \\DIFdelend). + +-d +--decline Run in DECLINE mode (delete all blocks marked by \\DIFaddbegin + and \\DIFaddend). + +-s +--simplify Run in SIMPLIFY mode (Keep all \\DIFaddbegin, \\DIFaddend, + \\DIFdelbegin, \\DIFdelend tokens, but remove all other latexdiff + markup from body. + +Note that the three mode options are mutually exclusive. If no mode option is given, +latexrevise simply removes user annotations and markup according to the following four +options. + + +-c cmd +--comment=cmd Remove \\cmd{...}. cmd is supposed to mark some explicit + anotations which should be removed from the file before + release. + +-e envir +--comment-environment=envir + Remove explicit annotation environments from the text, i.e. remove + \\begin{envir} + ... + \\end{envir} + blocks. + +-m cmd +--markup=cmd Remove the markup command cmd but leave its argument, i.e. + turn \\cmd{abc} into abc. + +-n envir +--markup-environment=envir + Similarly, remove \\begin{envir} and \\end{envir} commands, + but leave content of the environment in the text. + +-q +--no-warnings Do not warn users about \\DIDadd{..} or \\DIFdel statements + which should not be there anymore + +-V +--verbose Verbose output + +EOF +} + +=head1 NAME + +latexrevise - selectively remove markup and text from latexdiff output + +=head1 SYNOPSIS + +B<latexrevise> [ B<OPTIONS> ] [ F<diff.tex> ] > F<revised.tex> + +=head1 DESCRIPTION + +I<latexrevise> reads a file C<diff.tex> (output of I<latexdiff>), and remove the markup commands. +If no filename is given the input is read from standard input. The command can be used +in I<ACCEPT>, I<DECLINE>, or I<SIMPLIFY> mode, or can be used to remove user-defined +latex commands from the input (see B<-c>, B<-e>, B<-m>, and B<-n> below). +In I<ACCEPT> mode, all appended text fragments (or preamble lines) +are kept, and all discarded text fragments (or preamble lines) are +deleted. +In I<DECLINE> mode, all discarded text fragments are kept, and all appended +text fragments are deleted. +If you wish to keep some changes, edit the diff.tex file in +advance, and manually remove those tokens which would otherwise be +deleted. Note that I<latexrevise> only pays attention to the C<\DIFaddbegin>, +C<\DIFaddend>, C<\DIFdelbegin>, and C<\DIFdelend> tokens and corresponding FL +varieties. All C<\DIFadd> and C<\DIFdel> commands (but not their contents) are +simply deleted. The commands added by latexdiff to the preamble are also +removed. +In I<SIMPLIFY> mode, C<\DIFaddbegin, \DIFaddend, \DIFdelbegin, \DIFdelend> +tokens and their corresponding C<FL> varieties are kept but all other markup (e.g. C<DIFadd> and <\DIFdel>) is removed. The result +will not in general be valid latex-code but it will be easier to read and edit in +preparation for a subsequent run in I<ACCEPT> or I<DECLINE> mode. +In I<SIMPLIFY> mode the preamble is left unmodified. + +=head1 OPTIONS + +=over 4 + +=item B<-a> or B<--accept> + +Run in I<ACCEPT> mode (delete all blocks marked by C<\DIFdelbegin> and C<\DIFdelend>). + +=item B<-d> or B<--decline> + +Run in I<DECLINE> mode (delete all blocks marked by C<\DIFaddbegin> +and C<\DIFaddend>). + +=item B<-s> or B<--simplify> + +Run in I<SIMPLIFY> mode (Keep all C<\DIFaddbegin>, C<\DIFaddend>, +C<\DIFdelbegin>, C<\DIFdelend> tokens, but remove all other latexdiff +markup from body). + +=back + +Note that the three mode options are mutually exclusive. If no mode option is given, +I<latexrevise> simply removes user annotations and markup according to the following four +options. + +=over 4 + +=item B<-c cmd> or B<--comment=cmd> + +Remove C<\cmd{...}> sequences. C<cmd> is supposed to mark some explicit +anotations which should be removed from the file before +release. + +=item B<-e envir> or B<--comment-environment=envir> + +Remove explicit annotation environments from the text, i.e. remove + + \begin{envir} + ... + \end{envir} + +blocks. + +=item B<-m cmd> or B<--markup=cmd> + +Remove the markup command C<\cmd> but leave its argument, i.e. +turn C<\cmd{abc}> into C<abc>. + +=item B<-n envir> or B<--markup-environment=envir> + +Similarly, remove C<\begin{envir}> and C<\end{envir}> commands but +leave content of the environment in the text. + + +=item B<-V> or B<--verbose> + +Verbose output + +=item B<-q> or B<--no-warnings> + +Do not warn users about C<\DIDadd{..}> or C<\DIFdel{..}> statements +which should have been removed already. + +=back + +=head1 BUGS + +The current version is a beta version which has not yet been +extensively tested, but worked fine locally. Please send bug reports +to I<tilmann@esc.cam.ac.uk>. Include the serial number of I<latexrevise> +(from comments at the top of the source). If you come across latexdiff +output which is not processed correctly by I<latexrevise> please include the +problem file as well as the old and new files on which it is based, +ideally edited to only contain the offending passage as long as that still +reproduces the problem. + +I<latexrevise> gets confused by commented C<\begin{document}> or +C<\end{document}> statements + +=head1 SEE ALSO + +L<latexdiff> + +=head1 PORTABILITY + +I<latexrevise> does not make use of external commands and thus should run +on any platform supporting PERL v5 or higher. + +=head1 AUTHOR + +Copyright (C) 2004 Frederik Tilmann + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License Version 2 + +=cut |