# Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/ # # Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org # and TWiki Contributors. All Rights Reserved. TWiki Contributors # are listed in the AUTHORS file in the root of this distribution. # NOTE: Please extend that file, not this notice. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. For # more details read LICENSE in the root of this distribution. # # 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. # # As per the GPL, removal of this notice is prohibited. =begin twiki ---+ package TWiki::UI::RDiff UI functions for diffing. =cut package TWiki::UI::RDiff; use strict; use TWiki; use TWiki::Store; use TWiki::Prefs; use TWiki::UI; use TWiki::Time; use Error qw( :try ); use TWiki::OopsException; #TODO: this needs to be exposed to plugins and whoever might want to over-ride the rendering of diffs #Hash, indexed by diffType (+,-,c,u,l.....) #contains {colour, CssClassName} my %format = ( '+' => [ '#ccccff', 'twikiDiffAddedMarker'], '-' => [ '#ff9999', 'twikiDiffDeletedMarker'], 'c' => [ '#99ff99', 'twikiDiffChangedText'], 'u' => [ '#ffffff', 'twikiDiffUnchangedText'], 'l' => [ '#eeeeee', 'twikiDiffLineNumberHeader'] ); #SVEN - new design. #main gets the info (NO MAJOR CHANGES NEEDED) #parseDiffs reads the diffs and interprets the information into types {"+", "-", "u", "c", "l"} (add, remove, unchanged, changed, lineNumber} where line number is for diffs that skip unchanged lines (diff -u etc) #so renderDiffs would get an array of [changeType, $oldstring, $newstring] # corresponding to Algorithm::Diff's output #renderDiffs iterates through the interpreted info and makes it into TML / HTML? (mmm) #and can be over-ridden :) #(now can we do this in a way that automagically can cope eith word / letter based diffs?) #NOTE: if we do our own diffs in perl we can go straight to renderDiffs #TODO: I'm starting to think that we should have a variable number of lines of context. more context if you are doing a 1.13 tp 1.14 diff, less when you do a show page history. #TODO: ***URGENT*** the diff rendering dies badly when you have table cell changes and context #TODO: ?type={history|diff} so that you can do a normal diff between r1.3 and r1.32 (rather than a history) (and when doing a history, we maybe should not expand %SEARCH... #| Description: | twiki render a cell of data from a Diff | #| Parameter: =$data= | | #| Parameter: =$topic= | | #| Return: =$text= | Formatted html text | #| TODO: | this should move to Render.pm | sub _renderCellData { my( $session, $data, $web, $topic ) = @_; if ( $data ){ $data =~ s(^%META:FIELD{(.*)}%.*$) (_renderAttrs($1,'|*FORM FIELD $title*|$name|$value|'))gem; $data =~ s(^%META:([A-Z]+){(.*)}%$) ('|*META '.$1.'*|'._renderAttrs($2).'|')gem; $data = $session->handleCommonTags( $data, $web, $topic ); $data = $session->{renderer}->getRenderedVersion( $data, $web, $topic ); # Match up table tags, remove comments if( $data =~ m/<\/?(th|td|table)\b/i ) { # data has or , need to fix ables my $bTable = ( $data =~ s/( type tags) $data =~ s//
<--$1--><\/pre>/gos;
    }
    return $data;
}

# Simple method to expand attribute values in a format string
sub _renderAttrs {
    my( $p, $f) = @_;
    my $attrs = new TWiki::Attrs( $p );
    if( $f ) {
        for my $key ( keys %$attrs ) {
            my $av = TWiki::Store::dataDecode( $attrs->{$key} );
            $f =~ s/\$$key\b/$av/g;
        }
    } else {
        $f = $attrs->stringify();
    }
    return $f;
}

sub _sideBySideRow {
    my( $left, $right, $lc, $rc ) = @_;

    my $d1 = CGI::td({ bgcolor=>$format{$lc}[0],
                       class=>$format{$lc}[1],
                       valign=>'top'}, $left.' ' );
    my $d2 = CGI::td({ bgcolor=>$format{$rc}[0],
                       class=>$format{$rc}[1],
                       valign=>'top'}, $right.' ' );
    return CGI::Tr( $d1 . $d2 );
}

#| Description: | render the Diff entry using side by side |
#| Parameter: =$diffType= | {+,-,u,c,l} denotes the patch operation |
#| Parameter: =$left= | the text blob before the opteration |
#| Parameter: =$right= | the text after the operation |
#| Return: =$result= | Formatted html text |
#| TODO: | this should move to Render.pm |
sub _renderSideBySide
{
    my ( $session, $web, $topic, $diffType, $left, $right ) = @_;
    my $result = '';

    $left = _renderCellData( $session, $left, $web, $topic );
    $right = _renderCellData( $session, $right, $web, $topic );

    if ( $diffType eq '-') {
        $result .= _sideBySideRow( $left, $right, '-', 'u' )
    } elsif ( $diffType eq "+") {
        $result .= _sideBySideRow( $left, $right, 'u', '+' )
    } elsif ( $diffType eq "u") {
        $result .= _sideBySideRow( $left, $right, 'u', 'u' )
    } elsif ( $diffType eq "c") {
        $result .= _sideBySideRow( $left, $right, 'c', 'c' )
    } elsif ( $diffType eq "l" && $left ne '' && $right ne '' ) {
        $result .= CGI::Tr({
                            bgcolor=>$format{l}[0],
                            class=>$format{l}[1],
                           },
                           CGI::th({align=>'center'},
                                   ($session->{i18n}->maketext('Line: [_1]',$left))).
                           CGI::th({align=>'center'},
                                   ($session->{i18n}->maketext('Line: [_1]',$right)))
                          );
    }
    # unhide html comments ( type tags)
    $result =~  s//
<--$1--><\/pre>/gos;

    return $result;
}

#| Description: | render the Diff array (no TML conversion) |
#| Parameter: =$diffType= | {+,-,u,c,l} denotes the patch operation |
#| Parameter: =$left= | the text blob before the opteration |
#| Parameter: =$right= | the text after the operation |
#| Return: =$result= | Formatted html text |
#| TODO: | this should move to Render.pm |
sub _renderDebug
{
    my ( $diffType, $left, $right ) = @_;
    my $result = '';

    #de-html-ize
    $left =~ s/&/&/go;
    $left =~ s//>/go;
    $right =~ s/&/&/go;
    $right =~ s//>/go;
	
	$result = CGI::Tr(
		CGI::td( 'type: '. $diffType ));
		
	my %classMap =
	  (
	   '+' => [ 'twikiDiffAddedText'],
	   '-' => [ 'twikiDiffDeletedText'],
	   'c' => [ 'twikiDiffChangedText'],
	   'u' => [ 'twikiDiffUnchangedText'],
	   'l' => [ 'twikiDiffLineNumberHeader']
	  );
  
  	my $styleClass = ' '.$classMap{$diffType}[0] || '';
  	my $styleClassLeft = ($diffType ne 'c') ? $styleClass : '';
  	my $styleClassRight = $styleClass;
  	
	if ($diffType ne '+') {
   	    $result .= CGI::Tr( {class=>'twikiDiffDebug'},
		    CGI::td( {class=>'twikiDiffDebugLeft'.$styleClassLeft}, CGI::div( $left) ));
	}
	if (($diffType ne '-') && ($diffType ne 'l')) {
	    $result .= CGI::Tr( {class=>'twikiDiffDebug'},
		    CGI::td( {class=>'twikiDiffDebugRight'.$styleClassRight}, CGI::div( $right) ));
	}
        # unhide html comments ( type tags)
        $result =~  s//
<--$1--><\/pre>/gos;

    return $result;
}

sub _sequentialRow {
    my( $bg, $hdrcls, $bodycls, $data, $code, $char ) = @_;
    my $row = '';
    if( $char ) {
        $row = CGI::td({bgcolor=>$format{$code}[0],
                        class=>$format{$code}[1],
                        valign=>'top',
                        width=>"1%"},
                       $char.CGI::br().$char);
    } else {
      $row = CGI::td( " " );
    }
    $row .= CGI::td({class=>"twikiDiff${bodycls}Text"}, $data);
    $row = CGI::Tr( $row );
    if( $bg ) {
        return CGI::Tr(CGI::td({bgcolor=>$bg,
                                class=>"twikiDiff${hdrcls}Header",
                                colspan=>9},
                               CGI::b( " $hdrcls: "))).$row;
    } else {
        return $row;
    }
}

#| Description: | render the Diff using old style sequential blocks |
#| Parameter: =$diffType= | {+,-,u,c,l} denotes the patch operation |
#| Parameter: =$left= | the text blob before the opteration |
#| Parameter: =$right= | the text after the operation |
#| Return: =$result= | Formatted html text |
#| TODO: | this should move to Render.pm |
sub _renderSequential
{
    my ( $session, $web, $topic, $diffType, $left, $right ) = @_;
    my $result = '';

    #note: I have made the colspan 9 to make sure that it spans all columns (thought there are only 2 now)
    if ( $diffType eq '-') {
        $result .=
          _sequentialRow( '#FFD7D7',
                          ($session->{i18n}->maketext('Deleted')),
                          'Deleted',
                          _renderCellData( $session, $left, $web, $topic ),
                          '-', '<');
    } elsif ( $diffType eq '+') {
        $result .=
          _sequentialRow( '#D0FFD0',
                          ($session->{i18n}->maketext('Added')),
                          'Added',
                          _renderCellData( $session, $right, $web, $topic ),
                          '+', '>' );
    } elsif ( $diffType eq 'u') {
        $result .=
          _sequentialRow( undef,
                          ($session->{i18n}->maketext('Unchanged')),
                          'Unchanged',
                          _renderCellData( $session, $right, $web, $topic ),
                          'u', '' );
    } elsif ( $diffType eq 'c') {
        $result .=
          _sequentialRow( '#D0FFD0',
                          ($session->{i18n}->maketext('Changed')),
                          'Deleted',
                          _renderCellData( $session, $left, $web, $topic ),
                          '-', '<' );
        $result .=
          _sequentialRow( undef,
                          ($session->{i18n}->maketext('Changed')),
                          'Added',
                          _renderCellData( $session, $right, $web, $topic ),
                          '+', '>' );
    } elsif ( $diffType eq 'l' && $left ne '' && $right ne '' ) {
        $result .= CGI::Tr({bgcolor=>$format{l}[0],
                            class=>'twikiDiffLineNumberHeader'},
                           CGI::th({align=>'left',
                                    colspan=>9},
                                    ($session->{i18n}->maketext('Line: [_1] to [_2]',$left,$right))
                                  )
                           );
    }

    # unhide html comments ( type tags)
    $result =~  s//
<--$1--><\/pre>/gos;

    return $result;
}

#| Description: | uses renderStyle to choose the rendering function to use |
#| Parameter: =$diffArray= | array generated by parseRevisionDiff |
#| Parameter: =$renderStyle= | style of rendering { debug, sequential, sidebyside} |
#| Return: =$text= | output html for one renderes revision diff |
#| TODO: | move into Render.pm |
sub _renderRevisionDiff
{
    my( $session, $web, $topic, $sdiffArray_ref, $renderStyle ) = @_;

#combine sequential array elements that are the same diffType
    my @diffArray = ();
	foreach my $ele ( @$sdiffArray_ref ) {
		if( ( @$ele[1] =~ /^\%META\:TOPICINFO/ ) || ( @$ele[2] =~ /^\%META\:TOPICINFO/ ) ) {
			# do nothing, ignore redundant topic info
			# FIXME: Intelligently remove followup lines in case META:TOPICINFO is the only change
		} elsif( ( @diffArray ) && ( @{$diffArray[$#diffArray]}[0] eq @$ele[0] ) ) {
			@{$diffArray[$#diffArray]}[1] .= "\n".@$ele[1];
			@{$diffArray[$#diffArray]}[2] .= "\n".@$ele[2];
		} else {
			# Store doesn't expand REVINFO and we don't have rev info available now; escape tags to avoid confusion
			@$ele[1] =~ s/\%REVINFO/\%REVINFO/ unless $renderStyle eq 'debug';
			@$ele[2] =~ s/\%REVINFO/\%REVINFO/ unless $renderStyle eq 'debug';
			push @diffArray, $ele;
		}
	}
	my $diffArray_ref = \@diffArray;


    my $result = "";
    my $data = '';
    my $diff_ref = undef;
    for my $next_ref ( @$diffArray_ref ) {
    	if (( @$next_ref[0] eq 'l' ) && ( @$next_ref[1] eq 0 ) && (@$next_ref[2] eq 0)) {
            next;
		}
		if (! $diff_ref ) {
            $diff_ref = $next_ref;
            next;
		}
		if (( @$diff_ref[0] eq '-' ) && ( @$next_ref[0] eq '+' )) {
		    $diff_ref = ['c', @$diff_ref[1], @$next_ref[2]];
            $next_ref = undef;
		}
		if ( $renderStyle eq 'sequential' ) {
		    $result .= _renderSequential( $session, $web, $topic, @$diff_ref );
		} elsif ( $renderStyle eq 'sidebyside' ) {
            $result .= CGI::Tr(CGI::td({ width=>'50%'}, ''),
                               CGI::td({ width=>'50%'}, ''));
		    $result .= _renderSideBySide( $session, $web, $topic, @$diff_ref );
		} elsif ( $renderStyle eq 'debug' ) {
		    $result .= _renderDebug( @$diff_ref );
		}
		$diff_ref = $next_ref;
	}
    #don't forget the last one ;)
    if ( $diff_ref ) {
        if ( $renderStyle eq 'sequential' ) {
            $result .= _renderSequential ( $session, $web, $topic, @$diff_ref );
        } elsif ( $renderStyle eq 'sidebyside' ) {
            $result .= CGI::Tr(CGI::td({ width=>'50%'}, ''),
                               CGI::td({ width=>'50%'}, ''));
            $result .= _renderSideBySide( $session, $web, $topic, @$diff_ref );
        } elsif ( $renderStyle eq 'debug' ) {
            $result .= _renderDebug( @$diff_ref );
        }
    }
    return CGI::table( { class => 'twikiDiffTable',
                         width => '100%',
                         cellspacing => 0,
                         cellpadding => 0}, $result );
}

=pod

---++ StaticMethod diff( $session, $web, $topic, $query )

=diff= command handler.
This method is designed to be
invoked via the =TWiki::UI::run= method.

Renders the differences between version of a TwikiTopic
| topic | topic that we are showing the differences of |
| rev1 | the higher revision |
| rev2 | the lower revision |
| render | the rendering style {sequential, sidebyside, raw, debug} | (preferences) DIFFRENDERSTYLE, =sequential= |
| type | {history, diff, last} history diff, version to version, last version to previous | =history= |
| context | number of lines of context |
| skin | the skin(s) to use to display the diff |
TODO:
   * add a {word} render style
   * move the common CGI param handling to one place
   * move defaults somewhere

=cut

sub diff {
    my $session = shift;

    my $query = $session->{cgiQuery};
    my $webName = $session->{webName};
    my $topic = $session->{topicName};

    TWiki::UI::checkWebExists( $session, $webName, $topic, 'diff' );
    TWiki::UI::checkTopicExists( $session, $webName, $topic, 'diff' );

    my $renderStyle = $query->param('render') ||
      $session->{prefs}->getPreferencesValue( 'DIFFRENDERSTYLE' ) ||
        'sequential';
    my $diffType = $query->param('type') || 'history';
    my $contextLines = $query->param('context');
    unless( defined $contextLines ) {
        $session->{prefs}->getPreferencesValue( 'DIFFCONTEXTLINES' );
        $contextLines = 3 unless defined $contextLines;
    }
    my $rev1 = $query->param( 'rev1' );
    my $rev2 = $query->param( 'rev2' );

    my $skin = $session->getSkin();
    my $tmpl = $session->{templates}->readTemplate( 'rdiff', $skin );
    $tmpl =~ s/\%META{.*?}\%//go;  # remove %META{'parent'}%

    my( $before, $difftmpl, $after, $tail) = split( /%REPEAT%/, $tmpl);

    $before ||= '';
    $after ||= '';
    $tail ||= '';

    my $maxrev = $session->{store}->getRevisionNumber( $webName, $topic );
    $maxrev =~ s/r?1\.//go;  # cut 'r' and major

    $rev1 = $session->{store}->cleanUpRevID( $rev1 );
    $rev1 = $maxrev if( $rev1 < 1 );
    $rev1 = $maxrev if( $rev1 > $maxrev );

    $rev2 = $session->{store}->cleanUpRevID( $rev2 );
    $rev2 = 1 if( $rev2 < 1 );
    $rev2 = $maxrev if( $rev2 > $maxrev );

    if ( $diffType eq 'last' ) {
        $rev1 = $maxrev;
        $rev2 = $maxrev-1;
    }

    my $revTitle1 = $rev1;
    my $revTitle2 = ( $rev1 != $rev2 ) ? $rev2 : '';

    $before =~ s/%REVTITLE1%/$revTitle1/go;
    $before =~ s/%REVTITLE2%/$revTitle2/go;
    $before = $session->handleCommonTags( $before, $webName, $topic );
    $before = $session->{renderer}->getRenderedVersion( $before, $webName, $topic );

    my $page = $before;

    # do one or more diffs
    $difftmpl = $session->handleCommonTags( $difftmpl, $webName, $topic );
    my $r1 = $rev1;
    my $r2 = $rev2;
    my $isMultipleDiff = 0;

    if (( $diffType eq 'history' ) && ( $r1 > $r2 + 1)) {
        $r2 = $r1 - 1;
        $isMultipleDiff = 1;
    }

    do {
        my $diff = $difftmpl;
        $diff =~ s/%REVTITLE1%/$r1/go;

        my $rInfo = '';
        my $text;
        if ( $r1 > $r2 + 1) {
            $rInfo = $session->{i18n}->maketext(
                "Changes from r[_1] to r[_2]", $r2, $r1);
        } else {
            $rInfo = $session->{renderer}->renderRevisionInfo(
                $webName, $topic, undef, $r1, '$date - $wikiusername' );
        }
        # eliminate white space to prevent wrap around in HR table:
        $rInfo =~ s/\s+/ /g;
        my $diffArrayRef = $session->{store}->getRevisionDiff(
            $session->{user}, $webName, $topic, $r2, $r1, $contextLines );
        $text = _renderRevisionDiff( $session, $webName, $topic,
                                     $diffArrayRef, $renderStyle );
        $diff =~ s/%REVINFO1%/$rInfo/go;
        $diff =~ s/%TEXT%/$text/go;
        $page .= $diff;
        $r1 = $r1 - 1;
        $r2 = $r2 - 1;
        $r2 = 1 if( $r2 < 1 );
    } while( $diffType eq 'history' && ( $r1 > $rev2 || $r1 == 1 ));

    if( $TWiki::cfg{Log}{rdiff} ) {
        $session->writeLog( 'rdiff', $webName.'.'.$topic, "$rev1 $rev2" );
    }

    my $i = $maxrev;
    my $j = $maxrev;
    my $revisions = '';
    my $breakRev = 0;
    if( $TWiki::cfg{NumberOfRevisions} > 0 &&
        $TWiki::cfg{NumberOfRevisions} < $maxrev ) {
        $breakRev = $maxrev - $TWiki::cfg{NumberOfRevisions} + 1;
    }
    
    #SMELL: this should be the same variable as in view script, and so on - thus be configurable
    my $revSeperator = '<';

    while( $i > 0 ) {
        $revisions .= ' '.
          CGI::a( { href=>$session->getScriptUrl( 0, 'view', $webName, $topic,
                                                  rev => $i ),
                    rel => 'nofollow' }, 'r'.$i);
        if( $i != 1 ) {
            if( $i == $breakRev ) {
                $i = 1;
            } else {
                if( ( $i == $rev1 ) && ( !$isMultipleDiff ) ) {
                    $revisions .= ' '.$revSeperator;
                } else {
                    $j = $i - 1;
                    $revisions .= ' '.
                      CGI::a( { href=>$session->getScriptUrl(
                          0, 'rdiff', $webName, $topic,
                          rev1 => $i, rev2 => $j ),
                                rel => 'nofollow' },
                              $revSeperator);
                }
            }
        }
        $i--;
    }

    $i = $rev1;
    my $tailResult = '';
    my $revTitle   = '';
    while( $i >= $rev2) {
        $revTitle = CGI::a( { href=>$session->getScriptUrl(
            0, 'view', $webName, $topic, rev => $i ),
                              rel => 'nofollow' },
                            $i);
        my $revInfo = $session->{renderer}->renderRevisionInfo( $webName, $topic, undef, $i );
        $tailResult .= $tail;
        $tailResult =~ s/%REVTITLE%/$revTitle/go;
        $tailResult =~ s/%REVINFO%/$revInfo/go;
        $i--;
    }
    $after =~ s/%TAIL%/$tailResult/go;
    $after =~ s/%REVISIONS%/$revisions/go;
    $after =~ s/%CURRREV%/$rev1/go;
    $after =~ s/%MAXREV%/$maxrev/go;

    $after = $session->handleCommonTags( $after, $webName, $topic );
    $after = $session->{renderer}->getRenderedVersion( $after, $webName, $topic );
    $page .= $after;

    $session->writeCompletePage( $page );
}

1;