# # TWiki WikiClone ($wikiversion has version info) # # Copyright (C) 2001 Peter Thoeny, Peter@Thoeny.com # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details, published at # http://www.gnu.org/copyleft/gpl.html # # ========================= # # This is the spreadsheet TWiki plugin. # # Each plugin is a package that contains the subs: # # initPlugin ( $topic, $web, $user ) # commonTagsHandler ( $text, $topic, $web ) # startRenderingHandler( $text, $web ) # outsidePREHandler ( $text ) # insidePREHandler ( $text ) # endRenderingHandler ( $text ) # # initPlugin is required, all other are optional. # For increased performance, DISABLE handlers you don't need. # ========================= package TWiki::Plugins::SpreadSheetPlugin; # ========================= use vars qw( $web $topic $user $installWeb $VERSION $debug $skipInclude $renderingWeb @tableMatrix %namedMatrix $cPos $rPos $escToken ); $VERSION = '1.001'; $escToken = "\263"; # ========================= sub initPlugin { ( $topic, $web, $user, $installWeb ) = @_; # check for Plugins.pm versions if( $TWiki::Plugins::VERSION < 1 ) { &TWiki::Func::writeWarning( "Version mismatch between SpreadSheetPlugin and Plugins.pm" ); return 0; } $renderingWeb = $web; # Get plugin debug flag $debug = &TWiki::Func::getPreferencesFlag( "SPREADSHEETPLUGIN_DEBUG" ); # Get plugin debug flag $skipInclude = &TWiki::Func::getPreferencesFlag( "SPREADSHEETPLUGIN_SKIPINCLUDE" ); # Plugin correctly initialized &TWiki::Func::writeDebug( "- TWiki::Plugins::SpreadSheetPlugin::initPlugin( $web.$topic ) is OK" ) if $debug; return 1; } # ========================= sub commonTagsHandler { ### my ( $text, $topic, $web ) = @_; # do not uncomment, use $_[0], $_[1]... instead &TWiki::Func::writeDebug( "- SpreadSheetPlugin::commonTagsHandler( $_[2].$_[1] )" ) if $debug; if( ( $_[3] ) && ( $skipInclude ) ) { # bail out, handler called from an %INCLUDE{}% return; } unless( $_[0] =~ /%CALC\{.*?\}%/ || $_[0] =~ /%CALCRANGE\{.*?\}%/ ) { # nothing to do return; } %namedMatrix = (); @tableMatrix = (); $cPos = -1; $rPos = -1; my $result = ""; my $insidePRE = 0; my $insideTABLE = 0; my $line = ""; my $before = ""; my $cell = ""; my @row = (); my $tableName = ""; $_[0] =~ s/\r//go; $_[0] =~ s/\\\n//go; # Join lines ending in "\" foreach( split( /\n/, $_[0] ) ) { # change state: m|
|i       && ( $insidePRE = 1 );
        m||i  && ( $insidePRE = 1 );
        m|
|i && ( $insidePRE = 0 ); m||i && ( $insidePRE = 0 ); if( ! ( $insidePRE ) ) { if( /^\s*\|.*\|\s*$/ ) { # inside | table | if( ! $insideTABLE ) { $insideTABLE = 1; @tableMatrix = (); # reset table matrix $cPos = -1; $rPos = -1; } $line = $_; $line =~ s/^(\s*\|)(.*)\|\s*$/$2/o; $before = $1; @row = split( /\|/o, $line, -1 ); push @tableMatrix, [ @row ]; $rPos++; $line = "$before"; for( $cPos = 0; $cPos < @row; $cPos++ ) { $cell = $row[$cPos]; $cell =~ s/%CALC\{(.*?)\}%/&doCalc($1)/geo; $cell =~ s/%CALCRANGE\{(.*?)\}%/&doRangeDef($1)/geo; $line .= "$cell|"; } s/.*/$line/o; $result .= "$_\n"; } elsif ( /^\s*\#\|.*\|\s*$/ ) { # inside hidden table row ( #| fdsfdsf | ) if( ! $insideTABLE ) { $insideTABLE = 1; @tableMatrix = (); $cPos = -1; $rPos = -1; } $line = $_; $line =~ s/^\s*\#\|(.*)\|\s*$/$1/o; @row = split( /\|/o, $line, -1 ); push @tableMatrix, [ @row ]; $rPos++; for( $cPos = 0; $cPos < @row; $cPos++ ) { $cell = $row[$cPos]; $cell =~ s/%CALC\{(.*?)\}%/&doCalc($1)/geo; $cell =~ s/%CALCRANGE\{(.*?)\}%/&doRangeDef($1)/geo; } # don't generate an output line in this case } else { # outside | table | if( $insideTABLE ) { $insideTABLE = 0; } s/%CALC\{(.*?)\}%/&doCalc($1)/geo; s/%CALCRANGE\{(.*?)\}%/&doRangeDef($1)/geo; $result .= "$_\n"; } } else { $result .= "$_\n"; } } $_[0] = $result; } # ========================= sub doCalc { my( $theAttributes ) = @_; my $text = &TWiki::extractNameValuePair( $theAttributes ); # Add nesting level to parenthesis, # e.g. "A(B())" gets "A-esc-1(B-esc-2(-esc-2)-esc-1)" $text =~ s/([\(\)])/addNestingLevel($1, \$level)/geo; $text = doFunc( "MAIN", $text ); if( ( $rPos >= 0 ) && ( $cPos >= 0 ) ) { # update cell in table matrix $tableMatrix[$rPos][$cPos] = $text; } return $text; } # ========================= sub doRangeDef { my( $theAttributes ) = @_; my $name = &TWiki::extractNameValuePair( $theAttributes, "name" ); my $text = &TWiki::extractNameValuePair( $theAttributes, "range" ); &TWiki::writeDebug( "- SpreadSheetPlugin::doRangeDef: @_ : $theAttributes ($name = $text)" ) if $debug; # Add nesting level to parenthesis, # e.g. "A(B())" gets "A-esc-1(B-esc-2(-esc-2)-esc-1)" $text =~ s/([\(\)])/addNestingLevel($1, \$level)/geo; $text = doFunc( "MAIN", $text ); $namedMatrix{$name} = getTableRangeMatrix($text); &TWiki::writeDebug( "- SpreadSheetPlugin::doRangeDef: namedMatrix \{ $name \} = $namedMatrix{$name} -> $$namedMatrix{$name} " ) if $debug; return ""; # result is always empty string } # ========================= sub addNestingLevel { my( $theParen, $theLevelRef ) = @_; my $result = ""; if( $theParen eq "(" ) { $$theLevelRef++; $result = "$escToken$$theLevelRef$theParen"; } else { $result = "$escToken$$theLevelRef$theParen"; $$theLevelRef--; } return $result; } # ========================= sub doFunc { my( $theFunc, $theAttr ) = @_; &TWiki::writeDebug( "- SpreadSheetPlugin::doFunc: $theFunc( $theAttr ) start" ) if $debug; # Handle functions recursively $theAttr =~ s/\$([A-Z]+)$escToken([0-9]+)\((.*?)$escToken\2\)/&doFunc($1,$3)/geo; # Clean up unbalanced mess $theAttr =~ s/$escToken\-*[0-9]+([\(\)])/$1/go; my $result = ""; my $i = 0; if( $theFunc eq "MAIN" ) { $result = $theAttr; } elsif( $theFunc eq "T" ) { $result = ""; my @arr = getTableCell( $theAttr ); if( @arr ) { $result = $arr[0]; } } elsif( $theFunc eq "TRIM" ) { $result = $theAttr || ""; $result =~ s/^\s*//o; $result =~ s/\s*$//o; $result =~ s/\s+/ /go; } elsif( $theFunc eq "FORMAT" ) { # Format FORMAT(TYPE, precision, value) returns formatted value -- JimStraus - 05 Jan 2003 my( $format, $res, $value ) = split( /,\s*/, $theAttr ); $format =~ s/^\s*(.*?)\s*$/$1/; #Strip leading and trailing spaces $res =~ s/^\s*(.*?)\s*$/$1/; $value =~ s/^\s*(.*?)\s*$/$1/; if ($format eq "DOLLAR") { my $neg = 1 if $value < 0; $value = abs($value); $result = sprintf("%0.${res}f", $value); my $temp = reverse $result; $temp =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; $result = "\$" . (scalar reverse $temp); $result = "(".$result.")" if $neg; } elsif ($format eq "COMMA") { $result = sprintf("%0.${res}f", $value); my $temp = reverse $result; $temp =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; $result = scalar reverse $temp; } elsif ($format eq "PERCENT") { $result = sprintf("%0.${res}f\%", $value * 100); } elsif ($format eq "NUMBER") { $result = sprintf("%0.${res}f", $value); } else { #FORMAT not recognized, just return value $result = $value; } } elsif( $theFunc eq "EXACT" ) { $result = 0; my( $str1, $str2 ) = split( /,\s*/, $theAttr, 2 ); $str1 = "" unless( $str1 ); $str2 = "" unless( $str2 ); $str1 =~ s/^\s*(.*?)\s*$/$1/o; # cut leading and trailing spaces $str2 =~ s/^\s*(.*?)\s*$/$1/o; $result = 1 if( $str1 eq $str2 ); } elsif( $theFunc =~ /^(EVAL|INT|ROUND)$/ ) { $result = safeEvalPerl( $theFunc, $theAttr ); } elsif( $theFunc eq "IF" ) { # IF(condition, value if true, value if false) my( $condition, $str1, $str2 ) = split( /,\s*/, $theAttr, 3 ); $condition =~ s/^\s*(.*?)\s*$/$1/o; $result = safeEvalPerl( "EVAL", $condition ); if( $result =~ /^ERROR/ ) { # return error message } elsif( $result ) { $result = $str1 || ""; } else { $result = $str2 || ""; } } elsif( $theFunc eq "UPPER" ) { $result = uc( $theAttr ); } elsif( $theFunc eq "LOWER" ) { $result = lc( $theAttr ); } elsif( $theFunc eq "CHAR" ) { $theAttr =~ /([0-9]+)/; $i = $1 || 0; $i = 255 if $i > 255; $i = 0 if $i < 0; $result = chr( $i ); } elsif( $theFunc eq "CODE" ) { $result = ord( $theAttr ); } elsif( $theFunc eq "LENGTH" ) { $result = length( $theAttr ); } elsif( $theFunc eq "ROW" ) { $i = $theAttr || 0; $result = $rPos + $i + 1; } elsif( $theFunc eq "COLUMN" ) { $i = $theAttr || 0; $result = $cPos + $i + 1; } elsif( $theFunc eq "LEFT" ) { $i = $rPos + 1; $result = "R$i:C0..R$i:C$cPos"; } elsif( $theFunc eq "ABOVE" ) { $i = $cPos + 1; $result = "R0:C$i..R$rPos:C$i"; } elsif( $theFunc eq "RIGHT" ) { $i = $rPos + 1; $result = "R$i:C$cPos..R$i:C32000"; } elsif( $theFunc eq "DEF" ) { # Format DEF(list) returns first defined cell # Added by MF 26/3/2002, fixed by PeterThoeny my @arr = getTableRange( $theAttr ); foreach my $cell ( @arr ) { if( $cell ) { $cell =~ s/^\s*(.*?)\s*$/$1/o; if( $cell ) { $result = $cell; last; } } } } elsif( $theFunc eq "MAX" ) { my @arr = sort { $a <=> $b } grep { /./ } grep { defined $_ } getTableRangeAsFloat( $theAttr ); $result = $arr[$#arr]; } elsif( $theFunc eq "MIN" ) { my @arr = sort { $a <=> $b } grep { /./ } grep { defined $_ } getTableRangeAsFloat( $theAttr ); $result = $arr[0]; } elsif( $theFunc eq "SUM" ) { $result = 0; my @arr = getTableRangeAsFloat( $theAttr ); foreach $i ( @arr ) { $result += $i if defined $i; } } elsif( $theFunc eq "AVERAGE" ) { $result = 0; my $items = 0; my @arr = getTableRangeAsFloat( $theAttr ); foreach $i ( @arr ) { if( defined $i ) { $result += $i; $items++; } } if( $items > 0 ) { $result = $result / $items; } } elsif( $theFunc eq "COUNTSTR" ) { $result = 0; # count any string $i = 0; # count string equal second attr my( $range, $str ) = split( /,\s*/, $theAttr, 2 ); $str =~ s/\s*$//o if( $str ); my @arr = getTableRange( $range ); foreach my $cell ( @arr ) { if( defined $cell ) { $cell =~ s/^\s*(.*?)\s*$/$1/o; $result++ if( $cell ); $i++ if( $str && ( $cell eq $str ) ); } } $result = $i if( $str ); } elsif( $theFunc eq "COUNTITEMS" ) { $result = ""; my @arr = getTableRange( $theAttr ); my %items = (); my $key = ""; foreach $key ( @arr ) { $key =~ s/^\s*(.*?)\s*$/$1/o if( $key ); if( $key ) { if( exists( $items{ $key } ) ) { $items{ $key }++; } else { $items{ $key } = 1; } } } foreach $key ( sort keys %items ) { $result .= "$key: $items{ $key }
"; } $result =~ s|
$||o; } &TWiki::writeDebug( "- SpreadSheetPlugin::doFunc: $theFunc( $theAttr ) returns: $result" ) if $debug; return $result; } # ========================= sub safeEvalPerl { my( $theFunc, $theText ) = @_; # Allow only simple math with operators - + * / % ( ) $theText =~ s/\%\s*[^\-\+\*\/0-9\.\(\)]+//go; # defuse %hash but keep modulus # keep only numbers and operators (shh... don't tell anyone, we support comparison operators) $theText =~ s/[^\!\<\=\>\-\+\*\/\%0-9\.\(\)]*//go; $theText =~ /(.*)/; $theText = $1; # untainted variable my $result = eval "$theText"; if( $@ ) { $result = "ERROR: $@"; $result =~ s/[\n\r]//go; } else { $result = 0 unless( $result ); # logical false is "0" $result += 0.5 if( $theFunc eq "ROUND" ); $result = int( $result ) unless( $theFunc eq "EVAL" ); } return $result; } # ========================= sub getTableRangeAsInteger { my( $theAttr ) = @_; my $val = 0; my @arr = getTableRange( $theAttr ); (my $baz = "foo") =~ s/foo//; # reset search vars. defensive coding for my $i (0 .. $#arr ) { $val = $arr[$i]; # search first integer pattern, skip over HTML tags if( $val =~ /^\s*(?:<[^>]*>)*([\-\+]*[0-9]+).*/o ) { $arr[$i] = $1; # untainted variable, possibly undef } else { $arr[$i] = undef; } } return @arr; } # ========================= sub getTableRangeAsFloat { my( $theAttr ) = @_; my $val = 0; my @arr = getTableRange( $theAttr ); (my $baz = "foo") =~ s/foo//; # reset search vars. defensive coding for my $i (0 .. $#arr ) { $val = $arr[$i] || ""; # search first float pattern, skip over HTML tags if( $val =~ /^\s*(?:<[^>]*>)*([\-\+]*[0-9\.]+).*/o ) { $arr[$i] = $1; # untainted variable, possibly undef } else { $arr[$i] = undef; } } return @arr; } # ========================= sub getTableRangeMatrix { my( $theAttr ) = @_; my @arr = (); if( $rPos < 0 ) { return \@arr; } my $pMatrix = \@tableMatrix; if ($theAttr =~ /\s*([A-Za-z]\w*)\s*\[(.*)\]/) { &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRangeMatrix( named $1\[$2\] )" ) if $debug; # using a name - but does it exist? if ( ! $namedMatrix{$1} ) { return \@arr; } $pMatrix = $namedMatrix{$1}; $theAttr = $2; } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRangeMatrix( $theAttr )" ) if $debug; $theAttr =~ /\s*R([0-9]+)\:C([0-9]+)\s*\.\.+\s*R([0-9]+)\:C([0-9]+)/; if( ! $4 ) { return \@arr; } my $r1 = $1 - 1; my $c1 = $2 - 1; my $r2 = $3 - 1; my $c2 = $4 - 1; my $r = 0; my $c = 0; my $maxR = $#$pMatrix; &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRangeMatrix() R$r1:C$c1..R$r2:C$c2 maxR=$maxR" ) if $debug; if( $c1 < 0 ) { $c1 = 0; } if( $c2 < 0 ) { $c2 = 0; } if( $c2 < $c1 ) { $c = $c1; $c1 = $c2; $c2 = $c; } if( $r1 > $maxR ) { $r1 = $maxR; } if( $r1 < 0 ) { $r1 = 0; } if( $r2 > $maxR ) { $r2 = $maxR; } if( $r2 < 0 ) { $r2 = 0; } if( $r2 < $r1 ) { $r = $r1; $r1 = $r2; $r2 = $r; } my @row = (); my $right = 0; my $pRow = (); for $r ( $r1 .. $r2 ) { $pRow = $$pMatrix[$r]; @row = (); &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRangeMatrix() r = $r, prow = $#$pRow . @$pRow " ) if $debug; $right = $c2; if ($c1 <= $#$pRow) { if ($right > $#$pRow) { $right = $#$pRow; } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRangeMatrix() getting R$r:C$c1 .. R$r:C$right " ) if $debug; for $c ( $c1 .. $right ) { push( @row, $$pRow[$c] ); } } else { @row = (); # sparse row } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRangeMatrix() row = @row" ) if $debug; push @arr, [ @row ]; } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRangeMatrix() returns @arr" ) if $debug; return \@arr; } # ========================= sub getTableRange { my( $theAttr ) = @_; my @arr = (); if( $rPos < 0 ) { return @arr; } my $pMatrix = \@tableMatrix; if ($theAttr =~ /\s*([A-Za-z]\w*)\s*\[(.*)\]/) { &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRange named ( $1\[$2\] )" ) if $debug; # using a name - but does it exist? if ( ! $namedMatrix{$1} ) { return @arr; } $pMatrix = $namedMatrix{$1}; $theAttr = $2; } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRange( $theAttr )" ) if $debug; $theAttr =~ /\s*R([0-9]+)\:C([0-9]+)\s*\.\.+\s*R([0-9]+)\:C([0-9]+)/; if( ! $4 ) { return @arr; } my $r1 = $1 - 1; my $c1 = $2 - 1; my $r2 = $3 - 1; my $c2 = $4 - 1; my $r = 0; my $c = 0; my $maxR = $#$pMatrix; if( $c1 < 0 ) { $c1 = 0; } if( $c2 < 0 ) { $c2 = 0; } if( $c2 < $c1 ) { $c = $c1; $c1 = $c2; $c2 = $c; } if( $r1 > $maxR ) { $r1 = $maxR; } if( $r1 < 0 ) { $r1 = 0; } if( $r2 > $maxR ) { $r2 = $maxR; } if( $r2 < 0 ) { $r2 = 0; } if( $r2 < $r1 ) { $r = $r1; $r1 = $r2; $r2 = $r; } my $pRow = (); for $r ( $r1 .. $r2 ) { $pRow = $$pMatrix[$r]; for $c ( $c1 .. $c2 ) { if( $c <= $#$pRow ) { push( @arr, $$pRow[$c] ); } } } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRange() returns @arr" ) if $debug; return @arr; } # ========================= sub getTableCell { my( $theAttr ) = @_; my @arr = (); if( $rPos < 0 ) { return @arr; } my $pMatrix = \@tableMatrix; if ($theAttr =~ /\s*([A-Za-z]\w*)\s*\[(.*)\]/) { &TWiki::writeDebug( "- SpreadSheetPlugin::getTableCell named ( $1\[$2\] )" ) if $debug; # using a name - but does it exist? if ( ! $namedMatrix{$1} ) { return @arr; } $pMatrix = $namedMatrix{$1}; $theAttr = $2; } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableCell( $theAttr )" ) if $debug; $theAttr =~ /\s*R([0-9]+)\:C([0-9]+)/; if( ! $2 ) { return @arr; } my $r = $1 - 1; my $c = $2 - 1; my $maxR = $#$pMatrix; $pRow = $$pMatrix[$r]; &TWiki::writeDebug( "- SpreadSheetPlugin::getTableCell getting R$r:C$c maxR = $maxR row = @$pRow" ) if $debug; if( $c <= $#$pRow ) { push( @arr, $$pRow[$c] ); } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableCell() returns @arr" ) if $debug; return @arr; } # ========================= sub DISABLE_startRenderingHandler { ### my ( $text, $web ) = @_; # do not uncomment, use $_[0], $_[1] instead &TWiki::Func::writeDebug( "- SpreadSheetPlugin::startRenderingHandler( $$_[1] )" ) if $debug; # This handler is called by getRenderedVersion just before the line loop $renderingWeb = $_[1]; } # ========================= sub DISABLE_outsidePREHandler { ### my ( $text ) = @_; # do not uncomment, use $_[0] instead &TWiki::Func::writeDebug( "- SpreadSheetPlugin::outsidePREHandler( $web.$topic )" ) if $debug; # This handler is called by getRenderedVersion, in loop outside of
 tag
    # This is the place to define customized rendering rules

    # do custom extension rule, like for example:
    # $_[0] =~ s/old/new/go;
}

# =========================
sub DISABLE_insidePREHandler
{
### my ( $text ) = @_;   # do not uncomment, use $_[0] instead

    &TWiki::Func::writeDebug( "- SpreadSheetPlugin::insidePREHandler( $web.$topic )" ) if $debug;

    # This handler is called by getRenderedVersion, in loop inside of 
 tag
    # This is the place to define customized rendering rules

    # do custom extension rule, like for example:
    # $_[0] =~ s/old/new/go;
}

# =========================
sub DISABLE_endRenderingHandler
{
### my ( $text ) = @_;   # do not uncomment, use $_[0] instead

    &TWiki::Func::writeDebug( "- SpreadSheetPlugin::endRenderingHandler( $_[0] )" ) if $debug;

    # This handler is called by getRenderedVersion just after the line loop

}

# =========================

1;

# EOF