source: trunk/TablePlugin/lib/Foswiki/Plugins/TablePlugin/Core.pm @ 14577

Revision 14577, 62.7 KB checked in by GeorgeClark, 6 weeks ago (diff)

Item11354: Sync up minor difference 115 > trunk

  • Property svn:keywords set to Revision Date
Line 
1# See bottom of file for license and copyright information
2
3use strict;
4use warnings;
5
6package Foswiki::Plugins::TablePlugin::Core;
7
8use Foswiki::Func;
9use Foswiki::Plugins::TablePlugin ();
10use Foswiki::Time;
11use Error qw(:try);
12
13my @curTable;
14my $translationToken;
15my $insideTABLE;
16my $currTablePre;
17my $didWriteDefaultStyle;
18my $defaultAttrs;          # to write generic table CSS
19my $tableSpecificAttrs;    # to write table specific table CSS
20my $combinedTableAttrs;    # default and specific table attributes
21my $styles   = {};         # hash of default and specific styles
22my @messages = ();
23
24# not yet refactored:
25my $tableCount;
26my $sortCol;
27my $MAX_SORT_COLS;
28my $requestedTable;
29my $up;
30my $sortTablesInText;
31my $sortAttachments;
32my $sortColFromUrl;
33my $url;
34my $currentSortDirection;
35my @rowspan;
36
37my $HEAD_ID_DEFAULT_STYLE =
38  'TABLEPLUGIN_default';    # this name is part of the API, do not change
39my $HEAD_ID_SPECIFIC_STYLE =
40  'TABLEPLUGIN_specific';    # this name is part of the API, do not change
41
42my $PATTERN_TABLE = qr/%TABLE(?:{(.*?)})?%/;
43my $URL_ICON =
44    Foswiki::Func::getPubUrlPath() . '/'
45  . $Foswiki::cfg{SystemWebName}
46  . '/DocumentGraphics/';
47my $GIF_TABLE_SORT_ASCENDING = CGI::img(
48    {
49        src    => $URL_ICON . 'tablesortup.gif',
50        border => 0,
51        width  => 11,
52        height => 13,
53        alt    => 'Sorted ascending',
54        title  => 'Sorted ascending'
55    }
56);
57
58my $GIF_TABLE_SORT_DESCENDING = CGI::img(
59    {
60        src    => $URL_ICON . 'tablesortdown.gif',
61        border => 0,
62        width  => 11,
63        height => 13,
64        alt    => 'Sorted descending',
65        title  => 'Sorted descending'
66    }
67);
68
69my $GIF_TABLE_SORT_BOTH = CGI::img(
70    {
71        src    => $URL_ICON . 'tablesortdiamond.gif',
72        border => 0,
73        width  => 11,
74        height => 13,
75        alt    => 'Sort',
76        title  => 'Sort'
77    }
78);
79my $CHAR_SORT_ASCENDING = CGI::span( { class => 'tableSortIcon tableSortUp' },
80    $GIF_TABLE_SORT_ASCENDING );
81my $CHAR_SORT_DESCENDING =
82  CGI::span( { class => 'tableSortIcon tableSortDown' },
83    $GIF_TABLE_SORT_DESCENDING );
84my $CHAR_SORT_BOTH =
85  CGI::span( { class => 'tableSortIcon tableSortUp' }, $GIF_TABLE_SORT_BOTH );
86
87my $SORT_DIRECTION = {
88    'ASCENDING'  => 0,
89    'DESCENDING' => 1,
90    'NONE'       => 2,
91};
92
93my $PATTERN_ATTRIBUTE_SIZE = qr'([0-9]+)(px|%)*'o;
94
95my $TABLE_RULES = {};
96$TABLE_RULES->{all}->{TD}        = $TABLE_RULES->{all}->{TH} =
97  $TABLE_RULES->{data_all}->{TD} = $TABLE_RULES->{header_all}->{TH} =
98  'border-style:solid';
99$TABLE_RULES->{none}->{TD}        = $TABLE_RULES->{none}->{TH} =
100  $TABLE_RULES->{data_none}->{TD} = $TABLE_RULES->{header_none}->{TH} =
101  'border-style:none';
102$TABLE_RULES->{cols}->{TD}        = $TABLE_RULES->{cols}->{TH} =
103  $TABLE_RULES->{data_cols}->{TD} = $TABLE_RULES->{header_cols}->{TH} =
104  'border-style:none solid';
105$TABLE_RULES->{rows}->{TD}        = $TABLE_RULES->{rows}->{TH} =
106  $TABLE_RULES->{data_rows}->{TD} = $TABLE_RULES->{header_rows}->{TH} =
107  'border-style:solid none';
108$TABLE_RULES->{groups}->{TD} = 'border-style:none';
109$TABLE_RULES->{groups}->{TH} = 'border-style:solid none';
110
111my $TABLE_FRAME = {};
112$TABLE_FRAME->{void}   = 'border-style:none';
113$TABLE_FRAME->{above}  = 'border-style:solid none none none';
114$TABLE_FRAME->{below}  = 'border-style:none none solid none';
115$TABLE_FRAME->{lhs}    = 'border-style:none none none solid';
116$TABLE_FRAME->{rhs}    = 'border-style:none solid none none';
117$TABLE_FRAME->{hsides} = 'border-style:solid none solid none';
118$TABLE_FRAME->{vsides} = 'border-style:none solid none solid';
119$TABLE_FRAME->{box}    = 'border-style:solid';
120$TABLE_FRAME->{border} = 'border-style:solid';
121
122sub _init {
123    _debug("_init");
124    $translationToken = "\0";
125
126    # the maximum number of columns we will handle
127    $MAX_SORT_COLS        = 10000;
128    $didWriteDefaultStyle = 0;
129    $tableCount           = 0;
130    $currTablePre         = '';
131    $combinedTableAttrs   = {};
132    $tableSpecificAttrs   = {};
133}
134
135# called one time
136sub _initDefaults {
137    _debug('_initDefaults');
138    $defaultAttrs = {
139        headerrows    => 0,
140        footerrows    => 0,
141        sort          => 1,
142        class         => 'foswikiTable',
143        sortAllTables => $sortTablesInText,
144    };
145    _parseDefaultAttributes(
146        %{Foswiki::Plugins::TablePlugin::pluginAttributes} );
147
148    $combinedTableAttrs = _mergeHashes( {}, $defaultAttrs );
149}
150
151sub _addDefaultStyles {
152    return if $Foswiki::Plugins::TablePlugin::writtenToHead;
153    $Foswiki::Plugins::TablePlugin::writtenToHead = 1;
154
155    # create CSS styles tables in general
156    my ( $id, @styles ) = _createCssStyles( 1, $defaultAttrs );
157    _addHeadStyles( $HEAD_ID_DEFAULT_STYLE, @styles ) if scalar @styles;
158}
159
160sub _resetReusedVariables {
161    _debug('_resetReusedVariables');
162    $currTablePre       = '';
163    $combinedTableAttrs = _mergeHashes( {}, $defaultAttrs );
164    $tableSpecificAttrs = {};
165    $sortCol            = 0;
166    @messages           = ();
167}
168
169=pod
170
171=cut
172
173sub _storeAttribute {
174    my ( $inAttrName, $inValue, $inCollection ) = @_;
175
176    if ( !$inCollection ) {
177        _debug('_storeAttribute -- missing inCollection!');
178        return;
179    }
180    return if !defined $inValue;
181    return if !defined $inAttrName || $inAttrName eq '';
182    $inCollection->{$inAttrName} = $inValue;
183}
184
185=pod
186
187=cut
188
189sub _parseDefaultAttributes {
190    my (%params) = @_;
191
192    _debug('_parseDefaultAttributes');
193
194    _parseAttributes( 0, $defaultAttrs, \%params );
195}
196
197=pod
198
199=cut
200
201sub _parseTableSpecificTableAttributes {
202    my (%params) = @_;
203
204    _debug('_parseTableSpecificTableAttributes');
205
206    _parseAttributes( 1, $tableSpecificAttrs, \%params );
207
208    # remove default values from hash
209    while ( my ( $key, $value ) = each %{$tableSpecificAttrs} ) {
210        delete $tableSpecificAttrs->{$key}
211          if $defaultAttrs->{$key} && $value eq $defaultAttrs->{$key};
212    }
213    $combinedTableAttrs =
214      _mergeHashes( $combinedTableAttrs, $tableSpecificAttrs );
215    _debugData( 'combinedTableAttrs', $combinedTableAttrs );
216
217    # create CSS styles for this table only
218    my ( $id, @styles ) = _createCssStyles( 0, $tableSpecificAttrs );
219    _debugData( "after _createCssStyles, id=$id; styles", \@styles );
220
221    _addHeadStyles( $id, @styles ) if scalar @styles;
222
223    return $currTablePre . '<nop>';
224}
225
226=pod
227
228=cut
229
230sub _parseAttributes {
231    my ( $isTableSpecific, $inCollection, $inParams ) = @_;
232
233    _debugData( "isTableSpecific=$isTableSpecific; _parseAttributes=",
234        $inParams );
235
236    # include topic to read definitions
237    if ( $inParams->{include} ) {
238        my ( $includeParams, $message ) =
239          _getIncludeParams( $inParams->{include} );
240
241        if ($includeParams) {
242            $inParams = $includeParams;
243        }
244        if ($message) {
245            push( @messages, $message );
246        }
247    }
248
249    # table attributes
250    # some will be used for css styling as well
251
252    _storeAttribute( 'generateInlineMarkup',
253        Foswiki::Func::isTrue( $inParams->{inlinemarkup} ),
254        $inCollection )
255      if defined $inParams->{inlinemarkup};
256
257    # sort attributes
258    if ( defined $inParams->{sort} ) {
259        my $sort = Foswiki::Func::isTrue( $inParams->{sort} );
260        _storeAttribute( 'sort',          $sort, $inCollection );
261        _storeAttribute( 'sortAllTables', $sort, $inCollection );
262    }
263    if ( defined( $inParams->{initsort} )
264        and int( $inParams->{initsort} ) > 0 )
265    {
266        _storeAttribute( 'initSort', $inParams->{initsort}, $inCollection );
267
268        # override sort attribute: we are sorting after all
269        _storeAttribute( 'sort', 1, $inCollection );
270    }
271
272    if ( $inParams->{initdirection} ) {
273        _storeAttribute( 'initDirection', $SORT_DIRECTION->{'ASCENDING'},
274            $inCollection )
275          if $inParams->{initdirection} =~ /^down$/i;
276        _storeAttribute( 'initDirection', $SORT_DIRECTION->{'DESCENDING'},
277            $inCollection )
278          if $inParams->{initdirection} =~ /^up$/i;
279    }
280
281    # If EditTablePlugin is installed and we are editing a table,
282    # the CGI parameter 'sort' is defined as "off" to disable all
283    # header sorting ((Item5135)
284    my $cgi          = Foswiki::Func::getCgiQuery();
285    my $urlParamSort = $cgi->param('sort');
286    if ( $urlParamSort && $urlParamSort =~ /^off$/oi ) {
287        delete $inCollection->{sortAllTables};
288    }
289
290    # If EditTablePlugin is installed and we are editing a table, the
291    # 'disableallsort' TABLE parameter is added to disable initsort and header
292    # sorting in the table that is being edited. (Item5135)
293    if ( Foswiki::Func::isTrue( $inParams->{disableallsort} ) ) {
294        $inCollection->{sortAllTables} = 0;
295        delete $inCollection->{initSort};
296    }
297
298    if ($isTableSpecific) {
299
300        _storeAttribute( 'summary', $inParams->{summary}, $inCollection );
301        my $id =
302          defined $inParams->{id}
303          ? $inParams->{id}
304          : 'table'
305          . $Foswiki::Plugins::TablePlugin::topic
306          . ( $tableCount + 1 );
307        _storeAttribute( 'id',         $id,                     $inCollection );
308        _storeAttribute( 'headerrows', $inParams->{headerrows}, $inCollection );
309        _storeAttribute( 'footerrows', $inParams->{footerrows}, $inCollection );
310    }
311    _storeAttribute( 'border', $inParams->{tableborder}, $inCollection );
312    _storeAttribute( 'tableBorderColor', $inParams->{tablebordercolor},
313        $inCollection );
314    _storeAttribute( 'cellpadding', $inParams->{cellpadding}, $inCollection );
315    _storeAttribute( 'cellspacing', $inParams->{cellspacing}, $inCollection );
316    _storeAttribute( 'frame',       $inParams->{tableframe},  $inCollection );
317
318    # tablerules css settings
319    my @tableRulesList = ();
320    if ( $inParams->{tablerules} ) {
321
322        # store tablerules as array, so that headerrules and datarules
323        # can be appended to that list
324        my $param = _cleanParamValue( $inParams->{tablerules} );
325        if ($param) {
326            push( @tableRulesList, $param );
327        }
328    }
329    if ( $inParams->{headerrules} ) {
330        my $param = _cleanParamValue( $inParams->{headerrules} );
331        if ($param) {
332            $param = "header_$param";
333            push( @tableRulesList, $param );
334        }
335    }
336    if ( $inParams->{datarules} ) {
337        my $param = _cleanParamValue( $inParams->{datarules} );
338        if ($param) {
339            $param = "data_$param";
340            push( @tableRulesList, $param );
341        }
342    }
343    $inCollection->{tableRules} = \@tableRulesList if scalar @tableRulesList;
344
345    # use 'rules' as table attribute only (not to define css styles)
346    # but set to
347    my $rules =
348      ( defined $inParams->{headerrules} || defined $inParams->{datarules} )
349      ? 'none'
350      : $inParams->{tablerules};
351    _storeAttribute( 'rules', $rules, $inCollection );
352
353    _storeAttribute( 'width', $inParams->{tablewidth}, $inCollection );
354
355    # css attributes
356    _storeAttribute( 'headerColor', $inParams->{headercolor}, $inCollection );
357    _storeAttribute( 'headerBg',    $inParams->{headerbg},    $inCollection );
358    _storeAttribute( 'cellBorder',  $inParams->{cellborder},  $inCollection );
359    _storeAttribute( 'headerAlignListRef',
360        _arrayRefFromParam( $inParams->{headeralign} ),
361        $inCollection );
362    _storeAttribute( 'dataAlignListRef',
363        _arrayRefFromParam( $inParams->{dataalign} ),
364        $inCollection );
365    _storeAttribute( 'columnWidthsListRef',
366        _arrayRefFromParam( $inParams->{columnwidths} ),
367        $inCollection );
368    _storeAttribute( 'vAlign', $inParams->{valign} || 'top', $inCollection );
369    _storeAttribute( 'dataVAlign',   $inParams->{datavalign},   $inCollection );
370    _storeAttribute( 'headerVAlign', $inParams->{headervalign}, $inCollection );
371    _storeAttribute( 'headerBgSorted',
372        $inParams->{headerbgsorted} || $inParams->{headerbg},
373        $inCollection );
374    _storeAttribute( 'dataBgListRef', _arrayRefFromParam( $inParams->{databg} ),
375        $inCollection );
376    _storeAttribute(
377        'dataBgSortedListRef',
378        _arrayRefFromParam( $inParams->{databgsorted} || $inParams->{databg} ),
379        $inCollection
380    );
381    _storeAttribute( 'dataColorListRef',
382        _arrayRefFromParam( $inParams->{datacolor} ),
383        $inCollection );
384    _storeAttribute( 'tableCaption', $inParams->{caption}, $inCollection );
385
386    # remove empty attributes
387    while ( my ( $key, $value ) = each %{$inCollection} ) {
388        delete $inCollection->{$key} if !defined $value || $value eq '';
389    }
390
391    _debugData( '_parseAttributes result:', $inCollection );
392}
393
394=pod
395
396_getIncludeParams( $includeTopic ) -> \%params
397
398From $includeTopic read the first TABLE tag and return its parameters.
399
400=cut
401
402sub _getIncludeParams {
403    my ($inIncludeTopic) = @_;
404
405    my ( $includeWeb, $includeTopic ) =
406      Foswiki::Func::normalizeWebTopicName( $Foswiki::Plugins::TablePlugin::web,
407        $inIncludeTopic );
408
409    _debug("_getIncludeParams:$inIncludeTopic");
410    _debug("\t includeTopic=$includeTopic") if $includeTopic;
411
412    if ( !Foswiki::Func::topicExists( $includeWeb, $includeTopic ) ) {
413        _debug("TablePlugin: included topic $inIncludeTopic does not exist.");
414        return ( undef,
415'%MAKETEXT{"Warning: \'include\' topic <nop>[_1] does not exist!" args="'
416              . "$includeWeb.$includeTopic"
417              . '"}%' );
418    }
419    else {
420
421        my $text = Foswiki::Func::readTopicText( $includeWeb, $includeTopic );
422
423        if ( $text =~ m/$PATTERN_TABLE/s ) {
424            _debug("\t PATTERN_TABLE=$PATTERN_TABLE; 1=$1");
425            my $paramString = $1;
426
427            if (   $includeWeb ne $Foswiki::Plugins::TablePlugin::web
428                || $includeTopic ne $Foswiki::Plugins::TablePlugin::topic )
429            {
430
431                # expand common vars, except oneself to prevent recursion
432                $paramString =
433                  Foswiki::Func::expandCommonVariables( $paramString,
434                    $includeTopic, $includeWeb );
435            }
436            my %params = Foswiki::Func::extractParameters($paramString);
437            return ( \%params, undef );
438        }
439        else {
440            return ( undef,
441'%MAKETEXT{"Warning: table definition in \'include\' topic [_1] does not exist!" args="'
442                  . "$includeWeb.$includeTopic"
443                  . '"}%' );
444        }
445    }
446}
447
448=pod
449
450_convertStringToDate ( $text ) -> $number
451
452Convert text to number if syntactically possible, otherwise return undef.
453Assumes that the text has been stripped from HTML markup.
454
455=cut
456
457sub _convertStringToDate {
458    my ($text) = @_;
459
460    return undef if !defined $text;
461    return undef if $text eq '';
462    return undef if ( $text =~ /^\s*$/ );
463
464    my $date = undef;
465
466    if ( $text =~ /^\s*-?[0-9]+(\.[0-9])*\s*$/ ) {
467        _debug("\t this is a number");
468    }
469    else {
470        try {
471            $date = Foswiki::Time::parseTime($text);
472            _debug("\t is a date");
473        }
474        catch Error::Simple with {
475
476            # nope, wasn't a date
477            _debug("\t $text is not a date");
478        };
479    }
480
481    return $date;
482}
483
484=pod
485
486_convertStringToNumber ( $text ) -> $number
487
488Convert text to number if syntactically possible, otherwise return undef.
489Assumes that the text has been stripped from HTML markup.
490
491=cut
492
493sub _convertStringToNumber {
494    my ($text) = @_;
495
496    return undef if !defined $text;
497    return undef if $text eq '';
498    return undef if ( $text =~ /^\s*$/ );
499
500    # very course testing on IP (could in fact be anything with n.n. syntax
501    if (
502        $text =~ m/
503        ^               
504        \s*                     # any space
505        (?:                     # don't need to capture
506        [0-9]+          # digits
507        \.                      # dot
508        )                       #
509        {2,}            # repeat more than once: exclude decimal numbers
510        .*?                     # any string
511        $
512        /x
513      )
514    {
515        _debug("\t $text looks like an IP address, or something similar");
516
517        # should be sorted by text
518        return undef;
519    }
520
521    if (
522        $text =~ m/
523                ^
524                \s*                     # any space
525                (                       #
526                -*                      # possible minus
527                [0-9]+          # digits
528                \.*         # possible decimal
529                [0-9]*          # possible fracture digits
530                )                       # end capture of number
531                .*$                     # any string
532                /x
533      )
534    {
535
536        _debug("\t $1 is a number");
537
538        # make sure to return a number, not a string
539        return $1 * 1.0;
540    }
541    return undef;
542}
543
544sub _processTableRow {
545    my ( $thePre, $theRow ) = @_;
546
547    $currTablePre = $thePre || '';
548    my $span = 0;
549    my $l1   = 0;
550    my $l2   = 0;
551
552    if ( !$insideTABLE ) {
553        @curTable = ();
554        @rowspan  = ();
555
556        $tableCount++;
557        $currentSortDirection = $SORT_DIRECTION->{'NONE'};
558
559        if (   defined $requestedTable
560            && $requestedTable == $tableCount
561            && defined $sortColFromUrl )
562        {
563            $sortCol = $sortColFromUrl;
564            $sortCol = 0 unless ( $sortCol =~ m/^[0-9]+$/ );
565            $sortCol = $MAX_SORT_COLS if ( $sortCol > $MAX_SORT_COLS );
566            $currentSortDirection = _getCurrentSortDirection($up);
567        }
568        elsif ( defined $combinedTableAttrs->{initSort} ) {
569            $sortCol = $combinedTableAttrs->{initSort} - 1;
570            $sortCol = $MAX_SORT_COLS if ( $sortCol > $MAX_SORT_COLS );
571            $currentSortDirection =
572              _getCurrentSortDirection( $combinedTableAttrs->{initDirection} );
573        }
574    }
575
576    $theRow =~ s/\t/   /go;    # change tabs to space
577    $theRow =~ s/\s*$//o;      # remove trailing spaces
578    $theRow =~
579      s/(\|\|+)/'colspan'.$translationToken.length($1)."\|"/geo;  # calc COLSPAN
580    my $colCount = 0;
581    my @row      = ();
582    $span = 0;
583    my $value = '';
584
585    foreach ( split( /\|/, $theRow ) ) {
586        my $attr = {};
587        $span = 1;
588
589        #AS 25-5-01 Fix to avoid matching also single columns
590        if (s/colspan$translationToken([0-9]+)//) {
591            $span = $1;
592            $attr->{colspan} = $span;
593        }
594        s/^\s+$/ &nbsp; /o;
595        ( $l1, $l2 ) = ( 0, 0 );
596        if (/^(\s*).*?(\s*)$/) {
597            $l1 = length($1);
598            $l2 = length($2);
599        }
600        if ( $l1 >= 2 ) {
601            if ( $l2 <= 1 ) {
602                $attr->{align} = 'right';
603            }
604            else {
605                $attr->{align} = 'center';
606            }
607        }
608        if ( $span <= 2 ) {
609            $attr->{class} =
610              _appendColNumberCssClass( $attr->{class}, $colCount );
611        }
612
613        # html attribute: (column) width
614        if ( $combinedTableAttrs->{generateInlineMarkup}
615            && defined $combinedTableAttrs->{columnWidthsListRef} )
616        {
617            my @columnWidths = @{ $combinedTableAttrs->{columnWidthsListRef} };
618            if (   defined $columnWidths[$colCount]
619                && $columnWidths[$colCount]
620                && $span <= 2 )
621            {
622                $attr->{width} = $columnWidths[$colCount];
623            }
624        }
625
626        # END html attribute
627
628        if (/^(\s|<[^>]*>)*\^(\s|<[^>]*>)*$/) {    # row span above
629            $rowspan[$colCount]++;
630            push @row, { text => $value, type => 'Y' };
631        }
632        else {
633            for ( my $col = $colCount ; $col < ( $colCount + $span ) ; $col++ )
634            {
635                if ( defined( $rowspan[$col] ) && $rowspan[$col] ) {
636                    my $nRows = scalar(@curTable);
637                    my $rspan = $rowspan[$col] + 1;
638                    if ( $rspan > 1 ) {
639                        $curTable[ $nRows - $rspan ][$col]->{attrs}->{rowspan} =
640                          $rspan;
641                    }
642                    undef( $rowspan[$col] );
643                }
644            }
645
646            if (
647                (
648                    (
649                        defined $requestedTable
650                        && $requestedTable == $tableCount
651                    )
652                    || defined $combinedTableAttrs->{initSort}
653                )
654                && defined $sortCol
655                && $colCount == $sortCol
656              )
657            {
658
659                # CSS class name
660                if ( $currentSortDirection == $SORT_DIRECTION->{'ASCENDING'} ) {
661                    $attr->{class} =
662                      _appendSortedAscendingCssClass( $attr->{class} );
663                }
664                if ( $currentSortDirection == $SORT_DIRECTION->{'DESCENDING'} )
665                {
666                    $attr->{class} =
667                      _appendSortedDescendingCssClass( $attr->{class} );
668                }
669            }
670
671            my $type = '';
672            if (/^\s*\*(.*)\*\s*$/) {
673                $value = $1;
674                $type  = 'th';
675
676                # html attribute: align
677                if ( $combinedTableAttrs->{generateInlineMarkup}
678                    && defined $combinedTableAttrs->{headerAlignListRef} )
679                {
680                    my @headerAlign =
681                      @{ $combinedTableAttrs->{headerAlignListRef} };
682                    if (@headerAlign) {
683                        my $align =
684                          @headerAlign[ $colCount % ( $#headerAlign + 1 ) ];
685                        $attr->{align} = $align;
686                    }
687                }
688
689                # END html attribute
690
691                # html attribute: valign
692                if ( $combinedTableAttrs->{generateInlineMarkup} ) {
693                    if ( defined $combinedTableAttrs->{headerVAlign} ) {
694                        $attr->{valign} = $combinedTableAttrs->{headerVAlign};
695                    }
696                    elsif ( defined $combinedTableAttrs->{vAlign} ) {
697                        $attr->{valign} = $combinedTableAttrs->{vAlign};
698                    }
699                }
700
701                # END html attribute
702            }
703            else {
704                if (/^\s*(.*?)\s*$/) {    # strip white spaces
705                    $_ = $1;
706                }
707                $value = $_;
708                $type  = 'td';
709
710                # html attribute: align
711                if ( $combinedTableAttrs->{generateInlineMarkup}
712                    && defined $combinedTableAttrs->{dataAlignListRef} )
713                {
714                    my @dataAlign =
715                      @{ $combinedTableAttrs->{dataAlignListRef} };
716                    if (@dataAlign) {
717                        my $align =
718                          @dataAlign[ $colCount % ( $#dataAlign + 1 ) ];
719                        $attr->{align} = $align;
720                    }
721                }
722
723                # END html attribute
724
725                # html attribute: valign
726                if ( $combinedTableAttrs->{generateInlineMarkup} ) {
727                    if ( defined $combinedTableAttrs->{dataVAlign} ) {
728                        $attr->{valign} = $combinedTableAttrs->{dataVAlign};
729                    }
730                    elsif ( defined $combinedTableAttrs->{vAlign} ) {
731                        $attr->{valign} = $combinedTableAttrs->{vAlign};
732                    }
733                }
734
735                # END html attribute
736            }
737
738            push @row, { text => $value, attrs => $attr, type => $type };
739        }
740        while ( $span > 1 ) {
741            push @row, { text => $value, type => 'X' };
742            $colCount++;
743            $span--;
744        }
745        $colCount++;
746    }
747    push @curTable, \@row;
748    return $currTablePre
749      . '<nop>';    # Avoid Foswiki converting empty lines to new paras
750}
751
752sub _headerRowCount {
753    my ($table) = @_;
754
755    my $headerCount = 0;
756    my $footerCount = 0;
757    my $endheader   = 0;
758
759    # All cells in header are headings?
760    foreach my $row (@$table) {
761        my $isHeader = 1;
762        foreach my $cell (@$row) {
763            if ( $cell->{type} ne 'th' ) {
764                $isHeader    = 0;
765                $endheader   = 1;
766                $footerCount = 0 if $footerCount;
767            }
768        }
769        unless ($endheader) {
770            $headerCount++ if $isHeader;
771        }
772        else {
773            $footerCount++ if $isHeader;
774        }
775    }
776
777    # Some cells came after the footer - so there isn't one.
778    $footerCount = 0 if ( $endheader > 1 );
779
780    return ( $headerCount, $footerCount );
781}
782
783=pod
784
785_setSortTypeForCells ( $col, \@table )
786
787Sets a sort key for each cell.
788
789=cut
790
791sub _setSortTypeForCells {
792    my ( $col, $table ) = @_;
793
794    foreach my $row ( @{$table} ) {
795
796        my $rowText = _stripHtml( $row->[$col]->{text} );
797
798        my $num  = _convertStringToNumber($rowText);
799        my $date = _convertStringToDate($rowText);
800
801        $row->[$col]->{sortText}   = '';
802        $row->[$col]->{number}     = 0;
803        $row->[$col]->{dateString} = '';
804
805        if ( defined $date ) {
806
807            # date has just converted to a number
808            $row->[$col]->{number} = $date;
809
810            # add dateString value in case dates are equal
811            $row->[$col]->{dateString} = $rowText;
812        }
813        elsif ( defined $num ) {
814            $row->[$col]->{number} = $num;
815
816# when sorting mixed numbers and text, make the text sort value as low as possible
817            $row->[$col]->{sortText} = ' ';
818        }
819        else {
820            $row->[$col]->{sortText} = lc $rowText;
821        }
822
823    }
824}
825
826# Remove HTML from text so it can be sorted
827sub _stripHtml {
828    my ($text) = @_;
829
830    return undef if !defined $text;
831    $text =~
832      s/\[\[[^\]]+\]\[([^\]]+)\]\]/$1/go; # extract label from [[...][...]] link
833
834    my $orgtext =
835      $text;    # in case we will have removed all contents with stripping html
836    $text =~ s/<[^>]+>//go;    # strip HTML
837    $text =~ s/\&nbsp;/ /go;
838    $text = _getImageTextForSorting($orgtext) if ( $text eq '' );
839    $text =~ s/[\[\]\*\|=_\&\<\>]/ /g;    # remove Wiki formatting chars
840    $text =~ s/^ *//go;                   # strip leading space space
841
842    return $text;
843}
844
845=pod
846
847Retrieve text data from an image html tag to be used for sorting.
848First try the alt tag string. If not available, return the url string.
849If not available, return the original string.
850
851=cut
852
853sub _getImageTextForSorting {
854    my ($text) = @_;
855
856    # try to see _if_ there is any img data for sorting
857    my $hasImageTag = ( $text =~ m/\<\s*img([^>]+)>/ );
858    return $text if ( !$hasImageTag );
859
860    # first try to get the alt text
861    my $key = 'alt';
862    $text =~ m/$key=\s*[\"\']([^\"\']*)/;
863    return $1 if ( $1 ne '' );
864
865    # else
866
867    # no alt text; use the url
868    $key = 'url';
869    $text =~ m/$key=\s*[\"\']([^\"\']*)/;
870    return $1 if ( $1 ne '' );
871
872    # else
873
874    return $text;
875}
876
877=pod
878
879Appends $className to $classList, separated by a space. 
880
881=cut
882
883sub _appendToClassList {
884    my ( $classList, $className ) = @_;
885    $classList = $classList ? $classList .= ' ' : '';
886    $classList .= $className;
887    return $classList;
888}
889
890sub _appendSortedCssClass {
891    my ($classList) = @_;
892
893    return _appendToClassList( $classList, 'foswikiSortedCol' );
894}
895
896sub _appendRowNumberCssClass {
897    my ( $classList, $colListName, $rowNum ) = @_;
898
899    my $rowClassName = 'foswikiTableRow' . $colListName . $rowNum;
900    return _appendToClassList( $classList, $rowClassName );
901}
902
903sub _appendColNumberCssClass {
904    my ( $classList, $colNum ) = @_;
905
906    my $colClassName = 'foswikiTableCol' . $colNum;
907    return _appendToClassList( $classList, $colClassName );
908}
909
910sub _appendFirstColumnCssClass {
911    my ($classList) = @_;
912
913    return _appendToClassList( $classList, 'foswikiFirstCol' );
914}
915
916sub _appendLastColumnCssClass {
917    my ($classList) = @_;
918
919    return _appendToClassList( $classList, 'foswikiLastCol' );
920}
921
922sub _appendLastRowCssClass {
923    my ($classList) = @_;
924
925    return _appendToClassList( $classList, 'foswikiLast' );
926}
927
928sub _appendSortedAscendingCssClass {
929    my ($classList) = @_;
930
931    return _appendToClassList( $classList, 'foswikiSortedAscendingCol' );
932}
933
934sub _appendSortedDescendingCssClass {
935    my ($classList) = @_;
936
937    return _appendToClassList( $classList, 'foswikiSortedDescendingCol' );
938}
939
940# The default sort direction.
941sub _getDefaultSortDirection {
942    return $SORT_DIRECTION->{'ASCENDING'};
943}
944
945# Gets the current sort direction.
946sub _getCurrentSortDirection {
947    my ($currentDirection) = @_;
948    $currentDirection = $SORT_DIRECTION->{'ASCENDING'}
949      unless defined $currentDirection && $currentDirection =~ m/[0-2]+/;
950    $currentDirection ||= _getDefaultSortDirection();
951    return $currentDirection;
952}
953
954# Gets the new sort direction (needed for sort button) based on the current sort
955# direction.
956sub _getNewSortDirection {
957    my ($currentDirection) = @_;
958    if ( !defined $currentDirection ) {
959        return _getDefaultSortDirection();
960    }
961    my $newDirection;
962    if ( $currentDirection == $SORT_DIRECTION->{'ASCENDING'} ) {
963        $newDirection = $SORT_DIRECTION->{'DESCENDING'};
964    }
965    elsif ( $currentDirection == $SORT_DIRECTION->{'DESCENDING'} ) {
966        $newDirection = $SORT_DIRECTION->{'NONE'};
967    }
968    elsif ( $currentDirection == $SORT_DIRECTION->{'NONE'} ) {
969        $newDirection = $SORT_DIRECTION->{'ASCENDING'};
970    }
971    else {
972        $newDirection = _getDefaultSortDirection();
973    }
974
975    return $newDirection;
976}
977
978=pod
979
980_createCssStyles( $writeDefaults, $inAttrs ) -> ($id, @styles)
981
982Explicitly set styles override html styling (in this file marked with comment '# html attribute').
983
984=cut
985
986sub _createCssStyles {
987    my ( $writeDefaults, $inAttrs ) = @_;
988
989    _debug("_createCssStyles; writeDefaults=$writeDefaults");
990
991    my $_styles      = {};
992    my $setAttribute = sub {
993        my ( $tableSelector, $type, $rule ) = @_;
994
995        return if !$rule;
996        $type ||= '#';    # for table selector only, if no type
997        my $storedType = $_styles->{$tableSelector}->{$type} || '';
998        if ( !defined $storedType ) {
999            @{ $_styles->{$tableSelector}->{$type} } = ();
1000        }
1001        if ( $rule ne $storedType ) {
1002            push @{ $_styles->{$tableSelector}->{$type} }, $rule;
1003        }
1004    };
1005
1006    if ( $writeDefaults && !$didWriteDefaultStyle ) {
1007        my $tableSelector = '.foswikiTable';
1008        my $attr          = 'padding-left:.3em; vertical-align:text-bottom';
1009        &$setAttribute( $tableSelector, '.tableSortIcon img', $attr );
1010
1011        if ( $inAttrs->{cellpadding} ) {
1012            my $attr =
1013              'padding:' . addDefaultSizeUnit( $inAttrs->{cellpadding} );
1014            &$setAttribute( $tableSelector, 'td', $attr );
1015            &$setAttribute( $tableSelector, 'th', $attr );
1016        }
1017    }
1018
1019    my $tableSelector;
1020    my $id;
1021    if ($writeDefaults) {
1022        $id            = 'default';
1023        $tableSelector = ".foswikiTable";
1024    }
1025    else {
1026        $id            = $inAttrs->{id};
1027        $tableSelector = ".foswikiTable#$id";
1028    }
1029
1030    # tablerules
1031    if ( $inAttrs->{tableRules} ) {
1032        my @rules = @{ $inAttrs->{tableRules} };
1033
1034        my $attr_td;
1035        my $attr_th;
1036        foreach my $rule (@rules) {
1037            $attr_td = $TABLE_RULES->{$rule}->{TD}
1038              if $TABLE_RULES->{$rule}->{TD};
1039            $attr_th = $TABLE_RULES->{$rule}->{TH}
1040              if $TABLE_RULES->{$rule}->{TH};
1041        }
1042        &$setAttribute( $tableSelector, 'th', $attr_th );
1043        &$setAttribute( $tableSelector, 'td', $attr_td );
1044    }
1045
1046    # tableframe
1047    if ( $inAttrs->{frame} ) {
1048        my $attr = $TABLE_FRAME->{ $inAttrs->{frame} };
1049        &$setAttribute( $tableSelector, '', $attr );
1050    }
1051
1052    # tableborder
1053    if ( defined $inAttrs->{border} ) {
1054        my $tableBorderWidth = $inAttrs->{border} || 0;
1055        my $attr = 'border-width:' . addDefaultSizeUnit($tableBorderWidth);
1056        &$setAttribute( $tableSelector, '', $attr );
1057    }
1058
1059    # tableBorderColor
1060    if ( defined $inAttrs->{tableBorderColor} ) {
1061        my $attr;
1062        $attr = 'border-color:' . $inAttrs->{tableBorderColor};
1063        &$setAttribute( $tableSelector, '', $attr );
1064        $attr = 'border-top-color:' . $inAttrs->{tableBorderColor};
1065        &$setAttribute( $tableSelector, '', $attr );
1066        $attr = 'border-bottom-color:' . $inAttrs->{tableBorderColor};
1067        &$setAttribute( $tableSelector, '', $attr );
1068        $attr = 'border-left-color:' . $inAttrs->{tableBorderColor};
1069        &$setAttribute( $tableSelector, '', $attr );
1070        $attr = 'border-right-color:' . $inAttrs->{tableBorderColor};
1071        &$setAttribute( $tableSelector, '', $attr );
1072    }
1073
1074    # cellSpacing
1075    if ( defined $inAttrs->{cellspacing} ) {
1076
1077        # do not use border-collapse:collapse
1078        my $attr = 'border-collapse:separate';
1079        &$setAttribute( $tableSelector, '', $attr );
1080    }
1081
1082    # cellpadding
1083    if ( defined $inAttrs->{cellpadding} ) {
1084        my $attr = 'padding:' . addDefaultSizeUnit( $inAttrs->{cellpadding} );
1085        &$setAttribute( $tableSelector, 'td', $attr );
1086        &$setAttribute( $tableSelector, 'th', $attr );
1087    }
1088
1089    # cellborder
1090    if ( defined $inAttrs->{cellBorder} ) {
1091        my $cellBorderWidth = $inAttrs->{cellBorder} || 0;
1092        my $attr = 'border-width:' . addDefaultSizeUnit($cellBorderWidth);
1093        &$setAttribute( $tableSelector, 'td', $attr );
1094        &$setAttribute( $tableSelector, 'th', $attr );
1095    }
1096
1097    # tablewidth
1098    if ( defined $inAttrs->{width} ) {
1099        my $width = addDefaultSizeUnit( $inAttrs->{width} );
1100        my $attr  = 'width:' . $width;
1101        &$setAttribute( $tableSelector, '', $attr );
1102    }
1103
1104    # valign
1105    if ( defined $inAttrs->{vAlign} ) {
1106        my $attr = 'vertical-align:' . $inAttrs->{vAlign};
1107        &$setAttribute( $tableSelector, 'td', $attr );
1108        &$setAttribute( $tableSelector, 'th', $attr );
1109    }
1110
1111    # headerVAlign
1112    if ( defined $inAttrs->{headerVAlign} ) {
1113        my $attr = 'vertical-align:' . $inAttrs->{headerVAlign};
1114        &$setAttribute( $tableSelector, 'th', $attr );
1115    }
1116
1117    # dataVAlign
1118    if ( defined $inAttrs->{dataVAlign} ) {
1119        my $attr = 'vertical-align:' . $inAttrs->{dataVAlign};
1120        &$setAttribute( $tableSelector, 'td', $attr );
1121    }
1122
1123    # headerbg
1124    if ( defined $inAttrs->{headerBg} ) {
1125        my $color =
1126          ( $inAttrs->{headerBg} =~ /none/i )
1127          ? 'transparent'
1128          : $inAttrs->{headerBg};
1129        my $attr = 'background-color:' . $color;
1130        &$setAttribute( $tableSelector, 'th', $attr );
1131    }
1132
1133    # headerbgsorted
1134    if ( defined $inAttrs->{headerBgSorted} ) {
1135        my $color =
1136          ( $inAttrs->{headerBgSorted} =~ /none/i )
1137          ? 'transparent'
1138          : $inAttrs->{headerBgSorted};
1139        my $attr = 'background-color:' . $color;
1140        &$setAttribute( $tableSelector, 'th.foswikiSortedCol', $attr );
1141    }
1142
1143    # headercolor
1144    if ( defined $inAttrs->{headerColor} ) {
1145        my $attr = 'color:' . $inAttrs->{headerColor};
1146        &$setAttribute( $tableSelector, 'th',           $attr );
1147        &$setAttribute( $tableSelector, 'th a:link',    $attr );
1148        &$setAttribute( $tableSelector, 'th a:visited', $attr );
1149        &$setAttribute( $tableSelector, 'th a:xhover',  $attr )
1150          ; # just to make sorting work: hover should be last. below we will remove the x again.
1151        if ( defined $inAttrs->{headerBg} ) {
1152            my $hoverBackgroundColor = $inAttrs->{headerBg};
1153            $attr = 'background-color:' . $hoverBackgroundColor;
1154            &$setAttribute( $tableSelector, 'th a:xhover', $attr );
1155        }
1156    }
1157
1158    # databg (array)
1159    if ( defined $inAttrs->{dataBgListRef} ) {
1160        my @dataBg    = @{ $inAttrs->{dataBgListRef} };
1161        my $noneColor = ( $dataBg[0] =~ /none/i ) ? 'transparent' : '';
1162        my $count     = 0;
1163        foreach my $color (@dataBg) {
1164            $color = $noneColor if $noneColor;
1165            next if !$color;
1166            my $rowSelector = 'foswikiTableRow' . 'dataBg' . $count;
1167            my $attr        = "background-color:$color";
1168            &$setAttribute( $tableSelector, "tr.$rowSelector td", $attr );
1169            $count++;
1170        }
1171    }
1172
1173    # databgsorted (array)
1174    if ( defined $inAttrs->{dataBgSortedListRef} ) {
1175        my @dataBgSorted = @{ $inAttrs->{dataBgSortedListRef} };
1176        my $noneColor    = ( $dataBgSorted[0] =~ /none/i ) ? 'transparent' : '';
1177        my $count        = 0;
1178        foreach my $color (@dataBgSorted) {
1179            $color = $noneColor if $noneColor;
1180            next if !$color;
1181            my $rowSelector = 'foswikiTableRow' . 'dataBg' . $count;
1182            my $attr        = "background-color:$color";
1183            &$setAttribute( $tableSelector,
1184                "tr.$rowSelector td.foswikiSortedCol", $attr );
1185            $count++;
1186        }
1187    }
1188
1189    # datacolor (array)
1190    if ( defined $inAttrs->{dataColorListRef} ) {
1191        my @dataColor = @{ $inAttrs->{dataColorListRef} };
1192        unless ( $dataColor[0] =~ /none/i ) {
1193            my $count = 0;
1194            foreach my $color (@dataColor) {
1195                next if !$color;
1196                my $rowSelector = 'foswikiTableRow' . 'dataColor' . $count;
1197                my $attr        = "color:$color";
1198                &$setAttribute( $tableSelector, "tr.$rowSelector td", $attr );
1199                $count++;
1200            }
1201        }
1202    }
1203
1204    # columnwidths
1205    if ( defined $inAttrs->{columnWidthsListRef} ) {
1206        my @columnWidths = @{ $inAttrs->{columnWidthsListRef} };
1207        my $count        = 0;
1208        foreach my $width (@columnWidths) {
1209            next if !$width;
1210            $width = addDefaultSizeUnit($width);
1211            my $colSelector = 'foswikiTableCol';
1212            $colSelector .= $count;
1213            my $attr = 'width:' . $width;
1214            &$setAttribute( $tableSelector, "td.$colSelector", $attr );
1215            &$setAttribute( $tableSelector, "th.$colSelector", $attr );
1216            $count++;
1217        }
1218    }
1219
1220    # headeralign
1221    if ( defined $inAttrs->{headerAlignListRef} ) {
1222        my @headerAlign = @{ $inAttrs->{headerAlignListRef} };
1223        if ( scalar @headerAlign == 1 ) {
1224            my $align = $headerAlign[0];
1225            my $attr  = 'text-align:' . $align;
1226            &$setAttribute( $tableSelector, 'th', $attr );
1227        }
1228        else {
1229            my $count = 0;
1230            foreach my $align (@headerAlign) {
1231                next if !$align;
1232                my $colSelector = 'foswikiTableCol';
1233                $colSelector .= $count;
1234                my $attr = 'text-align:' . $align;
1235                &$setAttribute( $tableSelector, "th.$colSelector", $attr );
1236                $count++;
1237            }
1238        }
1239    }
1240
1241    # dataAlign
1242    if ( defined $inAttrs->{dataAlignListRef} ) {
1243        my @dataAlign = @{ $inAttrs->{dataAlignListRef} };
1244        if ( scalar @dataAlign == 1 ) {
1245            my $align = $dataAlign[0];
1246            my $attr  = 'text-align:' . $align;
1247            &$setAttribute( $tableSelector, 'td', $attr );
1248        }
1249        else {
1250            my $count = 0;
1251            foreach my $align (@dataAlign) {
1252                next if !$align;
1253                my $colSelector = 'foswikiTableCol';
1254                $colSelector .= $count;
1255                my $attr = 'text-align:' . $align;
1256                &$setAttribute( $tableSelector, "td.$colSelector", $attr );
1257                $count++;
1258            }
1259        }
1260    }
1261
1262    my @styles = ();
1263    foreach my $tableSelector ( sort keys %{$_styles} ) {
1264        foreach my $selector ( sort keys %{ $_styles->{$tableSelector} } ) {
1265            my $selectors =
1266              join( '; ', @{ $_styles->{$tableSelector}->{$selector} } );
1267            $selector =~ s/xhover/hover/go;    # remove sorting hack
1268                 # TODO: optimize by combining identical rules
1269            if ( $selector eq '#' ) {
1270                push @styles, "$tableSelector {$selectors}";
1271            }
1272            else {
1273                push @styles, "$tableSelector $selector {$selectors}";
1274            }
1275        }
1276    }
1277
1278    return ( $id, @styles );
1279}
1280
1281sub _addHeadStyles {
1282    my ( $inId, @inStyles ) = @_;
1283
1284    return if !scalar @inStyles;
1285
1286    $styles->{seendIds}->{$inId} = 1;
1287    if ( $inId eq $HEAD_ID_DEFAULT_STYLE ) {
1288        $styles->{$HEAD_ID_DEFAULT_STYLE}->{'default'} = \@inStyles;
1289        _writeStyleToHead( $HEAD_ID_DEFAULT_STYLE,
1290            $styles->{$HEAD_ID_DEFAULT_STYLE} );
1291    }
1292    else {
1293        $styles->{$HEAD_ID_SPECIFIC_STYLE}->{$inId} = \@inStyles;
1294        _writeStyleToHead( $HEAD_ID_SPECIFIC_STYLE,
1295            $styles->{$HEAD_ID_SPECIFIC_STYLE} );
1296    }
1297}
1298
1299sub _writeStyleToHead {
1300    my ( $inId, $inStyles ) = @_;
1301
1302    my @allStyles = ();
1303    foreach my $id ( sort keys %{$inStyles} ) {
1304        push @allStyles, @{ $inStyles->{$id} };
1305    }
1306    my $styleText = join( "\n", @allStyles );
1307
1308    my $header = <<EOS;
1309<style type="text/css" media="all">
1310$styleText
1311</style>
1312EOS
1313    $header =~ s/(.*?)\s*$/$1/;    # remove last newline
1314    Foswiki::Func::addToHEAD( $inId, $header, $HEAD_ID_DEFAULT_STYLE );
1315}
1316
1317=pod
1318
1319StaticMethod addDefaultSizeUnit ($text) -> $text
1320
1321Adds size unit 'px' if this is missing from the size text.
1322
1323=cut
1324
1325sub addDefaultSizeUnit {
1326    my ($inSize) = @_;
1327
1328    my $unit = '';
1329    if ( $inSize =~ m/$PATTERN_ATTRIBUTE_SIZE/ ) {
1330        $unit = 'px' if !$2;
1331    }
1332    return "$inSize$unit";
1333}
1334
1335sub emitTable {
1336
1337    _addDefaultStyles();
1338
1339    _debug('emitTable');
1340
1341    #Validate headerrows/footerrows and modify if out of range
1342    if ( $combinedTableAttrs->{headerrows} > scalar @curTable ) {
1343        $combinedTableAttrs->{headerrows} =
1344          scalar @curTable;    # limit header to size of table!
1345    }
1346    if ( $combinedTableAttrs->{headerrows} + $combinedTableAttrs->{footerrows} >
1347        @curTable )
1348    {
1349        $combinedTableAttrs->{footerrows} = scalar @curTable -
1350          $combinedTableAttrs->{headerrows};    # and footer to whatever is left
1351    }
1352
1353    my $sortThisTable =
1354      ( !defined $combinedTableAttrs->{sortAllTables}
1355          || $combinedTableAttrs->{sortAllTables} == 0 )
1356      ? 0
1357      : $combinedTableAttrs->{sort};
1358
1359    if ( $combinedTableAttrs->{headerrows} == 0 ) {
1360        my ( $headerRowCount, $footerRowCount ) = _headerRowCount( \@curTable );
1361
1362        # override default setting with calculated header count
1363        $combinedTableAttrs->{headerrows} = $headerRowCount;
1364        $combinedTableAttrs->{footerrows} = $footerRowCount;
1365    }
1366
1367    my $tableTagAttributes = {};
1368    $tableTagAttributes->{class}       = $combinedTableAttrs->{class};
1369    $tableTagAttributes->{border}      = $combinedTableAttrs->{border};
1370    $tableTagAttributes->{cellspacing} = $combinedTableAttrs->{cellspacing};
1371    $tableTagAttributes->{cellpadding} = $combinedTableAttrs->{cellpadding};
1372    $tableTagAttributes->{id}          = $combinedTableAttrs->{id}
1373      || undef;
1374    $tableTagAttributes->{summary} = $combinedTableAttrs->{summary};
1375    $tableTagAttributes->{frame}   = $combinedTableAttrs->{frame};
1376    $tableTagAttributes->{rules}   = $combinedTableAttrs->{rules};
1377    $tableTagAttributes->{width}   = $combinedTableAttrs->{width};
1378
1379    # remove empty attributes
1380    while ( my ( $key, $value ) = each %{$tableTagAttributes} ) {
1381        delete $tableTagAttributes->{$key} if !defined $value || $value eq '';
1382    }
1383
1384    my $text = $currTablePre . CGI::start_table($tableTagAttributes);
1385    $text .= $currTablePre . CGI::caption( $combinedTableAttrs->{tableCaption} )
1386      if $combinedTableAttrs->{tableCaption};
1387
1388    # count the number of cols to prevent looping over non-existing columns
1389    my $maxCols = 0;
1390
1391    # Flush out any remaining rowspans
1392    for ( my $i = 0 ; $i < @rowspan ; $i++ ) {
1393        if ( defined( $rowspan[$i] ) && $rowspan[$i] ) {
1394            my $nRows = scalar(@curTable);
1395            my $rspan = $rowspan[$i] + 1;
1396            my $r     = $nRows - $rspan;
1397            $curTable[$r][$i]->{attrs} ||= {};
1398            if ( $rspan > 1 ) {
1399                $curTable[$r][$i]->{attrs}->{rowspan} = $rspan;
1400            }
1401        }
1402    }
1403
1404    if (
1405        (
1406               $sortThisTable
1407            && defined $sortCol
1408            && defined $requestedTable
1409            && $requestedTable == $tableCount
1410        )
1411        || defined $combinedTableAttrs->{initSort}
1412      )
1413    {
1414
1415        # DG 08 Aug 2002: Allow multi-line headers
1416        my @header = splice( @curTable, 0, $combinedTableAttrs->{headerrows} );
1417
1418        # DG 08 Aug 2002: Skip sorting any trailers as well
1419        my @trailer = ();
1420        if ( $combinedTableAttrs->{footerrows}
1421            && scalar(@curTable) > $combinedTableAttrs->{footerrows} )
1422        {
1423            @trailer = splice( @curTable, -$combinedTableAttrs->{footerrows} );
1424        }
1425
1426        # Count the maximum number of columns of this table
1427        for my $row ( 0 .. $#curTable ) {
1428            my $thisRowMaxColCount = 0;
1429            for my $col ( 0 .. $#{ $curTable[$row] } ) {
1430                $thisRowMaxColCount++;
1431            }
1432            $maxCols = $thisRowMaxColCount
1433              if ( $thisRowMaxColCount > $maxCols );
1434        }
1435
1436        # Handle multi-row labels by killing rowspans in sorted tables
1437        for my $row ( 0 .. $#curTable ) {
1438            for my $col ( 0 .. $#{ $curTable[$row] } ) {
1439
1440                # SMELL: why do we need to specify a rowspan of 1?
1441                $curTable[$row][$col]->{attrs}->{rowspan} = 1;
1442                if ( $curTable[$row][$col]->{type} eq 'Y' ) {
1443                    $curTable[$row][$col]->{text} =
1444                      $curTable[ $row - 1 ][$col]->{text};
1445                    $curTable[$row][$col]->{type} = 'td';
1446                }
1447            }
1448        }
1449
1450       # url requested sort on column beyond end of table.  Force to last column
1451        $sortCol = 0 unless ( $sortCol =~ m/^[0-9]+$/ );
1452        $sortCol = $maxCols - 1 if ( $sortCol >= $maxCols );
1453
1454        # only get the column type if within bounds
1455        if ( $sortCol < $maxCols ) {
1456            _setSortTypeForCells( $sortCol, \@curTable );
1457        }
1458
1459        _debug("currentSortDirection:$currentSortDirection");
1460
1461        if (   $combinedTableAttrs->{sort}
1462            && $currentSortDirection == $SORT_DIRECTION->{'ASCENDING'} )
1463        {
1464            @curTable = sort {
1465                     $a->[$sortCol]->{sortText} cmp $b->[$sortCol]->{sortText}
1466                  || $a->[$sortCol]->{number} <=> $b->[$sortCol]->{number}
1467                  || $a->[$sortCol]->{dateString}
1468                  cmp $b->[$sortCol]->{dateString}
1469            } @curTable;
1470        }
1471        elsif ($combinedTableAttrs->{sort}
1472            && $currentSortDirection == $SORT_DIRECTION->{'DESCENDING'} )
1473        {
1474            @curTable = sort {
1475                     $b->[$sortCol]->{sortText} cmp $a->[$sortCol]->{sortText}
1476                  || $b->[$sortCol]->{number} <=> $a->[$sortCol]->{number}
1477                  || $b->[$sortCol]->{dateString}
1478                  cmp $a->[$sortCol]->{dateString}
1479            } @curTable;
1480        }
1481
1482        # DG 08 Aug 2002: Cleanup after the header/trailer splicing
1483        # this is probably awfully inefficient - but how big is a table?
1484        @curTable = ( @header, @curTable, @trailer );
1485    }    # if defined $sortCol ...
1486
1487    my $rowCount       = 0;
1488    my $numberOfRows   = scalar(@curTable);
1489    my $dataColorCount = 0;
1490
1491    my @headerRowList = ();
1492    my @bodyRowList   = ();
1493    my @footerRowList = ();
1494
1495    my $isPastHeaderRows = 0;
1496    my $singleIndent     = "\n\t";
1497    my $doubleIndent     = "\n\t\t";
1498    my $tripleIndent     = "\n\t\t\t";
1499
1500    # Only *one* row of the table has sort links, and it will either
1501    # be the last row in the header or the first row in the footer.
1502    my $sortLinksWritten = 0;
1503
1504    foreach my $row (@curTable) {
1505        my $rowtext  = '';
1506        my $colCount = 0;
1507
1508        # keep track of header cells: if all cells are header cells, do not
1509        # update the data color count
1510        my $headerCellCount  = 0;
1511        my $numberOfCols     = scalar(@$row);
1512        my $writingSortLinks = 0;
1513
1514        foreach my $fcell (@$row) {
1515
1516            # check if cell exists
1517            next if ( !$fcell || !$fcell->{type} );
1518
1519            my $tableAnchor = '';
1520            next
1521              if ( $fcell->{type} eq 'X' )
1522              ;    # data was there so sort could work with col spanning
1523            my $type = $fcell->{type};
1524            my $cell = $fcell->{text};
1525            my $attr = $fcell->{attrs} || {};
1526
1527            my $newDirection;
1528            my $isSorted = 0;
1529
1530            if (
1531                   $currentSortDirection != $SORT_DIRECTION->{'NONE'}
1532                && defined $sortCol
1533                && $colCount == $sortCol
1534
1535                # Removing the line below hides the marking of sorted columns
1536                # until the user clicks on a header (KJL)
1537                # && defined $requestedTable && $requestedTable == $tableCount
1538                #                && $sortType ne ''
1539              )
1540            {
1541                $isSorted     = 1;
1542                $newDirection = _getNewSortDirection($currentSortDirection);
1543            }
1544            else {
1545                $newDirection = _getDefaultSortDirection();
1546            }
1547
1548            if ( $type eq 'th' ) {
1549                $headerCellCount++;
1550
1551                # html attribute: bgcolor
1552                if ( $combinedTableAttrs->{generateInlineMarkup}
1553                    && defined $combinedTableAttrs->{headerBg} )
1554                {
1555                    $attr->{bgcolor} = $combinedTableAttrs->{headerBg}
1556                      unless ( $combinedTableAttrs->{headerBg} =~ /none/i );
1557                }
1558
1559                # END html attribute
1560
1561                if ($isSorted) {
1562                    if ( $currentSortDirection ==
1563                        $SORT_DIRECTION->{'ASCENDING'} )
1564                    {
1565                        $tableAnchor = $CHAR_SORT_ASCENDING;
1566                    }
1567                    if ( $currentSortDirection ==
1568                        $SORT_DIRECTION->{'DESCENDING'} )
1569                    {
1570                        $tableAnchor = $CHAR_SORT_DESCENDING;
1571                    }
1572
1573                    # html attribute: (sorted header cell) bgcolor
1574                    # overrides earlier set bgcolor
1575                    if ( $combinedTableAttrs->{generateInlineMarkup}
1576                        && defined $combinedTableAttrs->{headerBgSorted} )
1577                    {
1578                        $attr->{bgcolor} = $combinedTableAttrs->{headerBgSorted}
1579                          unless (
1580                            $combinedTableAttrs->{headerBgSorted} =~ /none/i );
1581                    }
1582
1583                    # END html attribute
1584                }
1585
1586                if (
1587                       defined $sortCol
1588                    && $colCount == $sortCol
1589                    && defined $requestedTable
1590                    && $requestedTable == $tableCount
1591                    && (   $combinedTableAttrs->{headerrows}
1592                        || $combinedTableAttrs->{footerrows} )
1593                  )
1594                {
1595
1596                    $tableAnchor =
1597                      CGI::a( { name => 'sorted_table' }, '<!-- -->' )
1598                      . $tableAnchor;
1599                }
1600
1601                # html attribute: headercolor (font style)
1602                if ( $combinedTableAttrs->{generateInlineMarkup}
1603                    && defined $combinedTableAttrs->{headerColor} )
1604                {
1605                    my $fontStyle =
1606                      { color => $combinedTableAttrs->{headerColor} };
1607                    $cell = CGI::font( $fontStyle, $cell );
1608                }
1609
1610                # END html attribute
1611
1612                if (
1613                    $sortThisTable
1614                    && (
1615                        ( $rowCount == $combinedTableAttrs->{headerrows} - 1 )
1616                        || (  !$combinedTableAttrs->{headerrows}
1617                            && $rowCount ==
1618                            $numberOfRows - $combinedTableAttrs->{footerrows} )
1619                    )
1620                    && ( $writingSortLinks || !$sortLinksWritten )
1621                  )
1622                {
1623                    $writingSortLinks = 1;
1624                    my $linkAttributes = {
1625                        href => $url
1626                          . 'sortcol='
1627                          . $colCount
1628                          . ';table='
1629                          . $tableCount . ';up='
1630                          . $newDirection
1631                          . '#sorted_table',
1632                        rel   => 'nofollow',
1633                        title => 'Sort by this column'
1634                    };
1635
1636                    if ( $cell =~ /\[\[|href/o ) {
1637                        $cell .= CGI::a( $linkAttributes, $CHAR_SORT_BOTH )
1638                          . $tableAnchor;
1639                    }
1640                    else {
1641                        $cell = CGI::a( $linkAttributes, $cell ) . $tableAnchor;
1642                    }
1643                }
1644
1645            }
1646            else {
1647
1648                $type = 'td' unless $type eq 'Y';
1649
1650                # html attribute: bgcolor
1651                if ( $combinedTableAttrs->{generateInlineMarkup} ) {
1652                    if ( $isSorted
1653                        && defined $combinedTableAttrs->{dataBgSortedListRef} )
1654                    {
1655                        my @dataBg =
1656                          @{ $combinedTableAttrs->{dataBgSortedListRef} };
1657
1658                        unless ( $dataBg[0] =~ /none/ ) {
1659                            $attr->{bgcolor} =
1660                              $dataBg[ $dataColorCount % ( $#dataBg + 1 ) ];
1661                        }
1662                    }
1663                    elsif ( defined $combinedTableAttrs->{dataBgListRef} ) {
1664                        my @dataBg = @{ $combinedTableAttrs->{dataBgListRef} };
1665                        unless ( $dataBg[0] =~ /none/i ) {
1666                            $attr->{bgcolor} =
1667                              $dataBg[ $dataColorCount % ( $#dataBg + 1 ) ];
1668                        }
1669                    }
1670                }
1671
1672                # END html attribute
1673
1674                # html attribute: datacolor (font style)
1675                if ( $combinedTableAttrs->{generateInlineMarkup}
1676                    && defined $combinedTableAttrs->{dataColorListRef} )
1677                {
1678                    my @dataColor =
1679                      @{ $combinedTableAttrs->{dataColorListRef} };
1680                    my $color =
1681                      $dataColor[ $dataColorCount % ( $#dataColor + 1 ) ];
1682                    unless ( $color =~ /^(none)$/i ) {
1683                        my $cellAttrs = { color => $color };
1684                        $cell = CGI::font( $cellAttrs, ' ' . $cell . ' ' );
1685                    }
1686                }
1687
1688                # END html attribute
1689
1690            }    ###if( $type eq 'th' )
1691
1692            if ($isSorted) {
1693                $attr->{class} = _appendSortedCssClass( $attr->{class} );
1694            }
1695
1696            if ($writingSortLinks) {
1697                $sortLinksWritten = 1;
1698            }
1699
1700            my $isLastRow = ( $rowCount == $numberOfRows - 1 );
1701            if ( $attr->{rowspan} ) {
1702                $isLastRow =
1703                  ( ( $rowCount + ( $attr->{rowspan} - 1 ) ) ==
1704                      $numberOfRows - 1 );
1705            }
1706
1707            # CSS class name
1708            $attr->{class} = _appendFirstColumnCssClass( $attr->{class} )
1709              if $colCount == 0;
1710            my $isLastCol = ( $colCount == $numberOfCols - 1 );
1711            $attr->{class} = _appendLastColumnCssClass( $attr->{class} )
1712              if $isLastCol;
1713
1714            $attr->{class} = _appendLastRowCssClass( $attr->{class} )
1715              if $isLastRow;
1716
1717            $colCount++;
1718            next if ( $type eq 'Y' );
1719            my $fn = 'CGI::' . $type;
1720            no strict 'refs';
1721            $rowtext .= "$tripleIndent" . &$fn( $attr, " $cell " );
1722            use strict 'refs';
1723        }    # foreach my $fcell ( @$row )
1724
1725        # assign css class names to tr
1726        # based on settings: dataBg, dataBgSorted
1727        my $trClassName = '';
1728
1729        # just 2 css names is too limited, but we will keep it for compatibility
1730        # with existing style sheets
1731        my $rowTypeName =
1732          ( $rowCount % 2 ) ? 'foswikiTableEven' : 'foswikiTableOdd';
1733        $trClassName = _appendToClassList( $trClassName, $rowTypeName );
1734
1735        if ( $combinedTableAttrs->{dataBgSortedListRef} ) {
1736            my @dataBgSorted = @{ $combinedTableAttrs->{dataBgSortedListRef} };
1737            my $modRowNum = $dataColorCount % ( $#dataBgSorted + 1 );
1738            $trClassName =
1739              _appendRowNumberCssClass( $trClassName, 'dataBgSorted',
1740                $modRowNum );
1741        }
1742        if ( $combinedTableAttrs->{dataBgListRef} ) {
1743            my @dataBg = @{ $combinedTableAttrs->{dataBgListRef} };
1744            my $modRowNum = $dataColorCount % ( $#dataBg + 1 );
1745            $trClassName =
1746              _appendRowNumberCssClass( $trClassName, 'dataBg', $modRowNum );
1747        }
1748        if ( $combinedTableAttrs->{dataColorListRef} ) {
1749            my @dataColor = @{ $combinedTableAttrs->{dataColorListRef} };
1750            my $modRowNum = $dataColorCount % ( $#dataColor + 1 );
1751            $trClassName =
1752              _appendRowNumberCssClass( $trClassName, 'dataColor', $modRowNum );
1753        }
1754        $rowtext .= $doubleIndent;
1755        my $rowHTML =
1756          $doubleIndent . CGI::Tr( { class => $trClassName }, $rowtext );
1757
1758        my $isHeaderRow =
1759          $rowCount <
1760          $combinedTableAttrs->{headerrows}; #( $headerCellCount == $colCount );
1761        my $isFooterRow =
1762          ( ( $numberOfRows - $rowCount ) <=
1763              $combinedTableAttrs->{footerrows} );
1764
1765        if ( !$isHeaderRow && !$isFooterRow ) {
1766
1767        # don't include non-adjacent header rows to the top block of header rows
1768            $isPastHeaderRows = 1;
1769        }
1770
1771        if ($isFooterRow) {
1772            push @footerRowList, $rowHTML;
1773        }
1774        elsif ( $isHeaderRow && !$isPastHeaderRows ) {
1775            push( @headerRowList, $rowHTML );
1776        }
1777        else {
1778            push @bodyRowList, $rowHTML;
1779            $dataColorCount++;
1780        }
1781
1782        if ($isHeaderRow) {
1783
1784            # reset data color count to start with first color after
1785            # each table heading
1786            $dataColorCount = 0;
1787        }
1788
1789        $rowCount++;
1790    }    # foreach my $row ( @curTable )
1791
1792    my $thead =
1793        "$singleIndent<thead>"
1794      . join( "", @headerRowList )
1795      . "$singleIndent</thead>";
1796    $text .= $currTablePre . $thead if scalar @headerRowList;
1797
1798    my $tfoot =
1799        "$singleIndent<tfoot>"
1800      . join( "", @footerRowList )
1801      . "$singleIndent</tfoot>";
1802    $text .= $currTablePre . $tfoot if scalar @footerRowList;
1803
1804    my $tbody;
1805    if ( scalar @bodyRowList ) {
1806        $tbody =
1807            "$singleIndent<tbody>"
1808          . join( "", @bodyRowList )
1809          . "$singleIndent</tbody>";
1810    }
1811    else {
1812
1813        # A HTML table requires a body, which cannot be empty (Item8991).
1814        # So we provide one, but prevent it from being displayed.
1815        $tbody =
1816"$singleIndent<tbody>$doubleIndent<tr style=\"display:none;\">$tripleIndent<td></td>$doubleIndent</tr>$singleIndent</tbody>\n";
1817    }
1818
1819    if ( scalar @messages ) {
1820        $text =
1821            '<span class="foswikiAlert">'
1822          . Foswiki::Func::expandCommonVariables( join( "\n", @messages ) )
1823          . '</span>' . "\n"
1824          . $text;
1825    }
1826
1827    $text .= $currTablePre . $tbody;
1828    $text .= $currTablePre . CGI::end_table() . "\n";
1829
1830    return $text;
1831}
1832
1833sub handler {
1834    ### my ( $text, $removed ) = @_;
1835
1836    _debug('handler');
1837
1838    unless ($Foswiki::Plugins::TablePlugin::initialised) {
1839        $insideTABLE = 0;
1840
1841        # Even if $tableCount is initialized already at plugin init
1842        # we need to reset it again each time preRenderingHandler
1843        # calls this handler sub. Important for initialiseWhenRender API
1844        $tableCount = 0;
1845
1846        my $cgi = Foswiki::Func::getCgiQuery();
1847        return unless $cgi;
1848
1849        # Copy existing values
1850        my ( @origSort, @origTable, @origUp );
1851        @origSort  = $cgi->param('sortcol');
1852        @origTable = $cgi->param('table');
1853        @origUp    = $cgi->param('up');        # NOTE: internal parameter
1854        $cgi->delete( 'sortcol', 'table', 'up' );
1855        $url = $cgi->url( -absolute => 1, -path => 1 ) . '?';
1856        my $queryString = $cgi->query_string();
1857        $url .= $queryString . ';' if $queryString;
1858
1859        # Restore parameters, so we don't interfere on the remaining execution
1860        $cgi->param( -name => 'sortcol', -value => \@origSort )  if @origSort;
1861        $cgi->param( -name => 'table',   -value => \@origTable ) if @origTable;
1862        $cgi->param( -name => 'up',      -value => \@origUp )    if @origUp;
1863
1864        $sortColFromUrl =
1865          $cgi->param('sortcol');              # zero based: 0 is first column
1866        if ( defined $sortColFromUrl && $sortColFromUrl !~ m/^[0-9]+$/ ) {
1867            $sortColFromUrl = 0;
1868        }
1869
1870        $requestedTable = $cgi->param('table');
1871        $requestedTable = 0
1872          unless ( defined $requestedTable && $requestedTable =~ m/^[0-9]+$/ );
1873
1874        $up = $cgi->param('up');
1875
1876        $sortTablesInText = 0;
1877        $sortAttachments  = 0;
1878        my $tmp = Foswiki::Func::getPreferencesValue('TABLEPLUGIN_SORT')
1879          || 'all';
1880        if ( !$tmp || $tmp =~ /^all$/oi ) {
1881            $sortTablesInText = 1;
1882            $sortAttachments  = 1;
1883        }
1884        elsif ( $tmp =~ /^attachments$/oi ) {
1885            $sortAttachments = 1;
1886        }
1887
1888        _initDefaults();    # first time
1889        $Foswiki::Plugins::TablePlugin::initialised = 1;
1890    }
1891
1892    $insideTABLE = 0;
1893
1894    my $defaultSort = $combinedTableAttrs->{sortAllTables};
1895
1896    my $acceptable = $combinedTableAttrs->{sortAllTables};
1897    my @lines = split( /\r?\n/, $_[0] );
1898    for (@lines) {
1899        if (
1900s/$PATTERN_TABLE/_parseTableSpecificTableAttributes(Foswiki::Func::extractParameters($1))/se
1901          )
1902        {
1903            $acceptable = 1;
1904        }
1905        elsif (s/^(\s*)\|(.*\|\s*)$/_processTableRow($1,$2)/eo) {
1906            $insideTABLE = 1;
1907        }
1908        elsif ($insideTABLE) {
1909            $_           = emitTable() . $_;
1910            $insideTABLE = 0;
1911
1912            $combinedTableAttrs->{sortAllTables} = $defaultSort;
1913            $acceptable = $defaultSort;
1914
1915            # prepare for next table
1916            _resetReusedVariables();
1917        }
1918    }
1919    $_[0] = join( "\n", @lines );
1920
1921    if ($insideTABLE) {
1922        $_[0] .= emitTable();
1923    }
1924
1925    # prepare for next table
1926    _resetReusedVariables();
1927}
1928
1929=pod
1930
1931_mergeHashes (\%a, \%b ) -> \%merged
1932
1933Merges 2 hash references.
1934
1935=cut
1936
1937sub _mergeHashes {
1938    my ( $A, $B ) = @_;
1939
1940    my %merged = ();
1941    while ( my ( $k, $v ) = each(%$A) ) {
1942        $merged{$k} = $v;
1943    }
1944    while ( my ( $k, $v ) = each(%$B) ) {
1945        $merged{$k} = $v;
1946    }
1947    return \%merged;
1948}
1949
1950=pod
1951
1952=cut
1953
1954sub _cleanParamValue {
1955    my ($inValue) = @_;
1956
1957    return undef if !$inValue;
1958
1959    $inValue =~ s/ //go;    # remove spaces
1960    return $inValue;
1961}
1962
1963=pod
1964
1965=cut
1966
1967sub _arrayRefFromParam {
1968    my ($inValue) = @_;
1969
1970    return undef if !$inValue;
1971
1972    $inValue =~ s/ //go;    # remove spaces
1973    my @list = split( /,/, $inValue );
1974    return \@list;
1975}
1976
1977=pod
1978
1979Shorthand debugging call.
1980
1981=cut
1982
1983sub _debug {
1984    return Foswiki::Plugins::TablePlugin::debug( 'TablePlugin::Core', @_ );
1985}
1986
1987sub _debugData {
1988    return Foswiki::Plugins::TablePlugin::debugData( 'TablePlugin::Core', @_ );
1989}
1990
19911;
1992__END__
1993Foswiki - The Free and Open Source Wiki, http://foswiki.org/
1994
1995Copyright (C) 2008-2012 Foswiki Contributors. Foswiki Contributors
1996are listed in the AUTHORS file in the root of this distribution.
1997NOTE: Please extend that file, not this notice.
1998
1999Additional copyrights apply to some or all of the code in this
2000file as follows:
2001
2002Copyright (C) 2005-2006 TWiki Contributors
2003Copyright (C) 2001-2004 Peter Thoeny, peter@thoeny.org
2004Copyright (C) 2001-2003 John Talintyre, jet@cheerful.com
2005
2006This program is free software; you can redistribute it and/or
2007modify it under the terms of the GNU General Public License
2008as published by the Free Software Foundation; either version 2
2009of the License, or (at your option) any later version. For
2010more details read LICENSE in the root of this distribution.
2011
2012This program is distributed in the hope that it will be useful,
2013but WITHOUT ANY WARRANTY; without even the implied warranty of
2014MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
2015
2016As per the GPL, removal of this notice is prohibited.
Note: See TracBrowser for help on using the repository browser.