# # 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 $cPos $rPos $escToken $prefsAttrs $swapsep $percentfmt $amountfmt $negamountfmt $dollarfmt $negdollarfmt $eurofmt $negeurofmt ); $VERSION = '1.003'; $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" ); $prefsAttrs = &TWiki::Func::getPreferencesValue( "CALCATTRIBUTES" ); my $tmp; $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "swapsep" ); $swapsep = $tmp if ($tmp); $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "amountfmt" ); $amountfmt = $tmp if ($tmp); $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "negamountfmt" ); $negamountfmt = $tmp if ($tmp); $dollarfmt = $amountfmt || "!#"; $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "dollarfmt" ); $dollarfmt = $tmp if ($tmp); $negdollarfmt = $negamountfmt || "(!#)"; $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "negdollarfmt" ); $negdollarfmt = $tmp if ($tmp); $eurofmt = $amountfmt || "! #"; $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "eurofmt" ); $eurofmt = $tmp if ($tmp); $negeurofmt = $negamountfmt || "-# !"; $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "negeurofmt" ); $negeurofmt = $tmp if ($tmp); $percfmt = "#%"; $tmp = &TWiki::Func::extractNameValuePair( $prefsAttrs, "percfmt" ); $percfmt = $tmp if ($tmp); # 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\{.*?\}%/ ) { # nothing to do return; } @tableMatrix = (); $cPos = -1; $rPos = -1; my $result = ""; my $insidePRE = 0; my $insideTABLE = 0; my $line = ""; my $before = ""; my $cell = ""; my @row = (); $_[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; $line .= "$cell|"; } s/.*/$line/o; } else { # outside | table | if( $insideTABLE ) { $insideTABLE = 0; } s/%CALC\{(.*?)\}%/&doCalc($1)/geo; } } $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 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 = getTableRange( "$theAttr..$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 =~ /^(DOLLAR|EURO)$/) { 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; $temp =~ tr/,./.,/ if ($swapsep); $result = scalar reverse $temp; $temp = ($neg ? $negdollarfmt : $dollarfmt) if ($format eq "DOLLAR"); $temp = ($neg ? $negeurofmt : $eurofmt) if ($format eq "EURO"); $temp =~ s/#/$result/g; $temp =~ s/!/\$/g if ($format eq "DOLLAR"); $temp =~ s/!/€/g if ($format eq "EURO"); $result = $temp; } elsif ($format eq "COMMA") { $result = sprintf("%0.${res}f", $value); my $temp = reverse $result; $temp =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; $temp =~ tr/,./.,/ if ($swapsep); $result = scalar reverse $temp; } elsif ($format eq "PERCENT") { $result = sprintf("%0.${res}f", $value * 100); $result =~ tr/,./.,/ if ($swapsep); $temp = $percfmt; $temp =~ s/#/$result/g; $temp =~ s/!/\%/g; $result = $temp; } elsif ($format eq "NUMBER") { $result = sprintf("%0.${res}f", $value); $result =~ tr/,./.,/ if ($swapsep); } 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; } elsif( $theFunc =~ /^(FIND|SEARCH)$/ ) { my( $searchString, $string, $pos ) = split( /,\s*/, $theAttr, 3 ); $result = ""; $pos--; $pos = 0 if( $pos < 0 ); pos( $string ) = $pos if( $pos ); $searchString = quotemeta( $searchString ) if( $theFunc eq "FIND" ); # using zero width lookahead '(?=...)' to keep pos at the beginning of match if( eval '$string =~ m/(?=$searchString)/g' && $string ) { $result = pos( $string ) + 1; } } elsif( $theFunc eq "REPLACE" ) { my( $string, $start, $num, $replace ) = split ( /,\s*/, $theAttr, 4 ); $result = $string; $start-- unless ($start < 1); if( eval 'substr( $string, $start, $num, $replace )' && $string ) { $result = $string; } } elsif( $theFunc eq "SUBSTITUTE" ) { my( $string, $from, $to, $inst, $options ) = split( /,\s*/, $theAttr ); $result = $string; $from = quotemeta( $from ) unless( $options && $options =~ /r/i); if( $inst ) { # replace Nth instance my $count = 0; if( eval '$string =~ s/($from)/if( ++$count == $inst ) { $to; } else { $1; }/gex;' && $string ) { $result = $string; } } else { # global replace if( eval '$string =~ s/$from/$to/g' && $string ) { $result = $string; } } } elsif( $theFunc eq "TRANSLATE" ) { $result = $theAttr; # greedy match for comma separated parameters (in case first parameter has embedded commas) if( $theAttr =~ /^(.*)\,\s*(.+)\,\s*(.+)$/ ) { my $string = $1 || ""; my $from = $2; my $to = $3; $from =~ s/\$comma/,/g; $from =~ s/\$sp/ /g; $from = quotemeta( $from ); $to =~ s/\$comma/,/g; $to =~ s/\$sp/ /g; $to = quotemeta( $to ); $from =~ s/([a-zA-Z0-9])\\\-([a-zA-Z0-9])/$1\-$2/g; # fix quotemeta (allow only ranges) $to =~ s/([a-zA-Z0-9])\\\-([a-zA-Z0-9])/$1\-$2/g; $result = $string; if( $string && eval "\$string =~ tr/$from/$to/" ) { $result = $string; } } } elsif ( $theFunc eq "NOP" ) { # pass everything through, this will allow plugins to defy plugin order # for example the %SEARCH{}% variable $theAttr =~ s/\$per/%/g; $result = $theAttr; } &TWiki::writeDebug( "- SpreadSheetPlugin::doFunc: $theFunc( $theAttr ) returns: $result" ) if $debug; return $result; } # ========================= sub safeEvalPerl { my( $theFunc, $theText ) = @_; # Allow only simple math with operators - + * / % ( ) $theText =~ tr/,././d if ($swapsep); $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]; $val =~ tr/,././d if ($swapsep); # 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] || ""; $val =~ tr/,././d if ($swapsep); # 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 getTableRange { my( $theAttr ) = @_; my @arr = (); if( $rPos < 0 ) { return @arr; } &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; if( $c1 < 0 ) { $c1 = 0; } if( $c2 < 0 ) { $c2 = 0; } if( $c2 < $c1 ) { $c = $c1; $c1 = $c2; $c2 = $c; } if( $r1 > $rPos ) { $r1 = $rPos; } if( $r1 < 0 ) { $r1 = 0; } if( $r2 > $rPos ) { $r2 = $rPos; } if( $r2 < 0 ) { $r2 = 0; } if( $r2 < $r1 ) { $r = $r1; $r1 = $r2; $r2 = $r; } my $pRow = (); for $r ( $r1 .. $r2 ) { $pRow = $tableMatrix[$r]; for $c ( $c1 .. $c2 ) { if( $c < @$pRow ) { push( @arr, $$pRow[$c] ); } } } &TWiki::writeDebug( "- SpreadSheetPlugin::getTableRange() 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