File Coverage

blib/lib/DBIx/XHTML_Table.pm
Criterion Covered Total %
statement 229 421 54.3
branch 66 200 33.0
condition 21 84 25.0
subroutine 33 51 64.7
pod 24 24 100.0
total 373 780 47.8


tags if applicable tag info tag info tag to the output
line stmt bran cond sub pod time code
1             package DBIx::XHTML_Table;
2              
3 7     7   54272 use strict;
  7         11  
  7         198  
4 7     7   27 use warnings;
  7         9  
  7         254  
5             our $VERSION = '1.46';
6              
7 7     7   10227 use DBI;
  7         99550  
  7         432  
8 7     7   62 use Carp;
  7         9  
  7         463  
9              
10             # GLOBALS
11 7     7   32 use vars qw(%ESCAPES $T $N);
  7         7  
  7         36995  
12             ($T,$N) = ("\t","\n");
13             %ESCAPES = (
14             '&' => '&',
15             '<' => '<',
16             '>' => '>',
17             '"' => '"',
18             );
19              
20             #################### CONSTRUCTOR ###################################
21              
22             # see POD for documentation
23             sub new {
24 2     2 1 97 my $class = shift;
25 2         9 my $self = {
26             null_value => ' ',
27             };
28 2         6 bless $self, $class;
29              
30             # last arg might be GTCH (global table config hash)
31 2 50       13 $self->{'global'} = pop if ref $_[$#_] eq 'HASH';
32              
33             # note: disconnected handles aren't caught :(
34              
35 2 50       18 if (UNIVERSAL::isa($_[0],'DBI::db')) {
    50          
36             # use supplied db handle
37 0         0 $self->{'dbh'} = $_[0];
38 0         0 $self->{'keep_alive'} = 1;
39             }
40             elsif (ref($_[0]) eq 'ARRAY') {
41             # go ahead and accept a pre-built 2d array ref
42 2         12 $self->_do_black_magic(@_);
43             }
44             else {
45             # create my own db handle
46 0         0 eval { $self->{'dbh'} = DBI->connect(@_) };
  0         0  
47 0 0 0     0 carp $@ and return undef if $@;
48             }
49              
50 2         7 return $self;
51             }
52              
53             #################### OBJECT METHODS ################################
54              
55             sub exec_query {
56 0     0 1 0 my ($self,$sql,$vars) = @_;
57              
58 0 0       0 carp "can't call exec_query(): do database handle" unless $self->{'dbh'};
59              
60 0         0 eval {
61             $self->{'sth'} = (UNIVERSAL::isa($sql,'DBI::st'))
62             ? $sql
63 0 0       0 : $self->{'dbh'}->prepare($sql)
64             ;
65 0         0 $self->{'sth'}->execute(@$vars);
66             };
67 0 0 0     0 carp $@ and return undef if $@;
68              
69             # store the results
70 0         0 $self->{'fields_arry'} = [ @{$self->{'sth'}->{'NAME'}} ];
  0         0  
71 0         0 $self->{'fields_hash'} = $self->_reset_fields_hash();
72 0         0 $self->{'rows'} = $self->{'sth'}->fetchall_arrayref();
73 0 0       0 carp "can't call exec_query(): no data was returned from query" unless @{$self->{'rows'}};
  0         0  
74              
75 0 0       0 if (exists $self->{'pk'}) {
76             # remove the primary key info from the arry and hash
77 0         0 $self->{'pk_index'} = delete $self->{'fields_hash'}->{$self->{'pk'}};
78 0 0       0 splice(@{$self->{'fields_arry'}},$self->{'pk_index'},1) if defined $self->{'pk_index'};
  0         0  
79             }
80              
81 0         0 return $self;
82             }
83              
84             sub output {
85 7     7 1 354 my ($self,$config,$no_ws) = @_;
86 7 50 0     25 carp "can't call output(): no data" and return '' unless $self->{'rows'};
87              
88             # have to deprecate old arguments ...
89 7 50       15 if ($no_ws) {
90 0         0 carp "scalar arguments to output() are deprecated, use hash reference";
91 0         0 $N = $T = '';
92             }
93 7 50 33     49 if ($config and not ref $config) {
    50          
94 0         0 carp "scalar arguments to output() are deprecated, use hash reference";
95 0         0 $self->{'no_head'} = $config;
96             }
97             elsif ($config) {
98 7         18 $self->{'no_head'} = $config->{'no_head'};
99 7         11 $self->{'no_ucfirst'} = $config->{'no_ucfirst'};
100 7 50       23 $N = $T = '' if $config->{'no_indent'};
101 7 50       19 if ($config->{'no_whitespace'}) {
102 0         0 carp "no_whitespace attrib deprecated, use no_indent";
103 0         0 $N = $T = '';
104             }
105             }
106              
107 7         17 return $self->_build_table();
108             }
109              
110             sub modify {
111 1     1 1 2 my ($self,$tag,$attribs,$cols) = @_;
112 1         2 $tag = lc $tag;
113              
114             # apply attributes to specified columns
115 1 50       4 if (ref $attribs eq 'HASH') {
116 1 50 33     6 $cols = 'global' unless defined( $cols) && length( $cols );
117 1         2 $cols = $self->_refinate($cols);
118              
119 1         6 while (my($attr,$val) = each %$attribs) {
120 1         7 $self->{lc $_}->{$tag}->{$attr} = $val for @$cols;
121             }
122             }
123             # or handle a special case (e.g.
)
124             else {
125             # cols is really attribs now, attribs is just a scalar
126 0         0 $self->{'global'}->{$tag} = $attribs;
127              
128             # there is only one caption - no need to rotate attribs
129 0 0       0 if (ref $cols->{'style'} eq 'HASH') {
130 0         0 $cols->{'style'} = join('; ',map { "$_: ".$cols->{'style'}->{$_} } sort keys %{$cols->{'style'}}) . ';';
  0         0  
  0         0  
131             }
132              
133 0         0 $self->{'global'}->{$tag."_attribs"} = $cols;
134             }
135              
136 1         2 return $self;
137             }
138              
139             sub map_cell {
140 0     0 1 0 my ($self,$sub,$cols) = @_;
141              
142 0 0 0     0 carp "map_cell() is being ignored - no data" and return $self unless $self->{'rows'};
143              
144 0         0 $cols = $self->_refinate($cols);
145 0         0 for (@$cols) {
146 0         0 my $key;
147 0 0       0 if (defined $self->{'fields_hash'}->{$_}) {
    0          
148 0         0 $key = $_;
149             } elsif( defined $self->{'fields_hash'}->{lc $_}) {
150 0         0 $key = lc $_;
151             } else {
152 0         0 SEARCH: for my $k (sort keys %{ $self->{'fields_hash'} }) {
  0         0  
153 0 0       0 if (lc( $k ) eq lc( $_ )) {
154 0         0 $key = $k;
155 0         0 last SEARCH;
156             }
157             }
158             }
159 0 0       0 next unless $key;
160 0         0 $self->{'map_cell'}->{$key} = $sub;
161             }
162 0         0 return $self;
163             }
164              
165             sub map_head {
166 1     1 1 3 my ($self,$sub,$cols) = @_;
167              
168 1 50 0     3 carp "map_head() is being ignored - no data" and return $self unless $self->{'rows'};
169              
170 1         5 $cols = $self->_refinate($cols);
171 1         3 for (@$cols) {
172 1         1 my $key;
173 1 50       4 if (defined $self->{'fields_hash'}->{$_}) {
    0          
174 1         2 $key = $_;
175             } elsif( defined $self->{'fields_hash'}->{lc $_}) {
176 0         0 $key = lc $_;
177             } else {
178 0         0 SEARCH: for my $k (sort keys %{ $self->{'fields_hash'} }) {
  0         0  
179 0 0       0 if (lc( $k ) eq lc( $_ )) {
180 0         0 $key = $k;
181 0         0 last SEARCH;
182             }
183             }
184             }
185 1 50       2 next unless $key;
186 1         3 $self->{'map_head'}->{$key} = $sub;
187             }
188              
189 1         2 return $self;
190             }
191              
192             sub add_col_tag {
193 0     0 1 0 my ($self,$attribs) = @_;
194 0 0       0 $self->{'global'}->{'colgroup'} = {} unless $self->{'colgroups'};
195 0         0 push @{$self->{'colgroups'}}, $attribs;
  0         0  
196              
197 0         0 return $self;
198             }
199              
200             sub calc_totals {
201 1     1 1 3 my ($self,$cols,$mask) = @_;
202 1 50       4 return undef unless $self->{'rows'};
203              
204 1         1 $self->{'totals_mask'} = $mask;
205 1         3 $cols = $self->_refinate($cols);
206              
207 1         1 my @indexes;
208 1         3 for (@$cols) {
209 1         1 my $index;
210 1 50       3 if (exists $self->{'fields_hash'}->{$_}) {
    0          
211 1         2 $index = $self->{'fields_hash'}->{$_};
212             } elsif (exists $self->{'fields_hash'}->{lc $_}) {
213 0         0 $index = $self->{'fields_hash'}->{lc $_};
214             } else {
215 0         0 SEARCH: for my $k (sort keys %{ $self->{'fields_hash'} }) {
  0         0  
216 0 0       0 if (lc( $k ) eq lc( $_ )) {
217 0         0 $index = $self->{'fields_hash'}->{$k};
218 0         0 last SEARCH;
219             }
220             }
221             }
222 1         3 push @indexes, $index;
223             }
224              
225 1         4 $self->{'totals'} = $self->_total_chunk($self->{'rows'},\@indexes);
226              
227 1         3 return $self;
228             }
229              
230             sub calc_subtotals {
231 0     0 1 0 my ($self,$cols,$mask,$nodups) = @_;
232 0 0       0 return undef unless $self->{'rows'};
233              
234 0         0 $self->{'subtotals_mask'} = $mask;
235 0         0 $cols = $self->_refinate($cols);
236              
237 0         0 my @indexes;
238 0         0 for (@$cols) {
239 0         0 my $index;
240 0 0       0 if (exists $self->{'fields_hash'}->{$_}) {
    0          
241 0         0 $index = $self->{'fields_hash'}->{$_};
242             } elsif (exists $self->{'fields_hash'}->{lc $_}) {
243 0         0 $index = $self->{'fields_hash'}->{lc $_};
244             } else {
245 0         0 SEARCH: for my $k (sort keys %{ $self->{'fields_hash'} }) {
  0         0  
246 0 0       0 if (lc( $k ) eq lc( $_ )) {
247 0         0 $index = $self->{'fields_hash'}->{$k};
248 0         0 last SEARCH;
249             }
250             }
251             }
252 0         0 push @indexes, $index;
253             }
254              
255 0         0 my $beg = 0;
256 0         0 foreach my $end (@{$self->{'body_breaks'}}) {
  0         0  
257 0         0 my $chunk = ([@{$self->{'rows'}}[$beg..$end]]);
  0         0  
258 0         0 push @{$self->{'sub_totals'}}, $self->_total_chunk($chunk,\@indexes);
  0         0  
259 0         0 $beg = $end + 1;
260             }
261              
262 0         0 return $self;
263             }
264              
265             sub set_row_colors {
266 0     0 1 0 my ($self,$colors,$myattrib) = @_;
267              
268 0 0       0 return $self unless ref $colors eq 'ARRAY';
269 0 0       0 return $self unless $#$colors >= 1;
270              
271 0 0       0 my $ref = ($myattrib)
272             ? { $myattrib => [@$colors] }
273             : { style => {background => [@$colors]} }
274             ;
275              
276 0         0 $self->modify(tr => $ref, 'body');
277              
278             # maybe that should be global?
279             #$self->modify(tr => $ref);
280              
281 0         0 return $self;
282             }
283              
284             sub set_col_colors {
285 0     0 1 0 my ($self,$colors,$myattrib) = @_;
286              
287 0 0       0 return $self unless ref $colors eq 'ARRAY';
288 0 0       0 return $self unless $#$colors >= 1;
289              
290 0         0 my $cols = $self->_refinate();
291              
292             # trick #1: truncate colors to cols
293 0 0       0 $#$colors = $#$cols if $#$colors > $#$cols;
294              
295             # trick #2: keep adding colors
296             #unless ($#$cols % 2 and $#$colors % 2) {
297 0         0 my $temp = [@$colors];
298 0         0 push(@$colors,_rotate($temp)) until $#$colors == $#$cols;
299             #}
300              
301 0 0       0 my $ref = ($myattrib)
302             ? { $myattrib => [@$colors] }
303             : { style => {background => [@$colors]} }
304             ;
305              
306 0         0 $self->modify(td => $ref, $_) for @$cols;
307              
308 0         0 return $self;
309             }
310              
311             sub set_group {
312 1     1 1 2 my ($self,$group,$nodup,$value) = @_;
313 1 50 33     7 $self->{'nodup'} = $value || $self->{'null_value'} if $nodup;
314              
315 1         1 my $index;
316 1 50       8 if ($group =~ /^\d+$/) {
    50          
    0          
317 0         0 $index = $group;
318             } elsif (exists $self->{'fields_hash'}->{$group}) {
319 1         2 $index = $self->{'fields_hash'}->{$group};
320 1         2 $self->{'group'} = $group;
321             } elsif (exists $self->{'fields_hash'}->{lc $group}) {
322 0         0 $index = $self->{'fields_hash'}->{lc $group};
323 0         0 $self->{'group'} = lc $group;
324             } else {
325 0         0 SEARCH: for my $k (sort keys %{ $self->{'fields_hash'} }) {
  0         0  
326 0 0       0 if (lc( $k ) eq lc( $group )) {
327 0         0 $index = $self->{'fields_hash'}->{$k};
328 0         0 $self->{'group'} = $k;
329 0         0 last SEARCH;
330             }
331             }
332             }
333              
334             # initialize the first 'repetition'
335 1         2 my $rep = $self->{'rows'}->[0]->[$index];
336              
337             # loop through the whole rows array, storing
338             # the points at which a new group starts
339 1         3 for my $i (0..$self->get_row_count - 1) {
340 2         3 my $new = $self->{'rows'}->[$i]->[$index];
341 2 50       4 push @{$self->{'body_breaks'}}, $i - 1 unless ($rep eq $new);
  0         0  
342 2         3 $rep = $new;
343             }
344              
345 1         2 push @{$self->{'body_breaks'}}, $self->get_row_count - 1;
  1         3  
346              
347 1         2 return $self;
348             }
349              
350             sub set_pk {
351 0     0 1 0 my $self = shift;
352 0   0     0 my $pk = shift || 'id';
353 0 0 0     0 $pk = $pk =~ /^\d+$/ ? $self->_lookup_name($pk) || $pk : $pk;
354 0 0       0 carp "can't call set_pk(): too late to set primary key" if exists $self->{'rows'};
355 0         0 $self->{'pk'} = $pk;
356              
357 0         0 return $self;
358             }
359              
360             sub set_null_value {
361 0     0 1 0 my ($self,$value) = @_;
362 0         0 $self->{'null_value'} = $value;
363 0         0 return $self;
364             }
365              
366             sub get_col_count {
367 1     1 1 2 my ($self) = @_;
368 1         2 my $count = scalar @{$self->{'fields_arry'}};
  1         2  
369 1         3 return $count;
370             }
371              
372             sub get_row_count {
373 6     6 1 7 my ($self) = @_;
374 6         7 my $count = scalar @{$self->{'rows'}};
  6         12  
375 6         14 return $count;
376             }
377              
378             sub get_current_row {
379 0     0 1 0 return shift->{'current_row'};
380             }
381              
382             sub get_current_col {
383 0     0 1 0 return shift->{'current_col'};
384             }
385              
386             sub reset {
387 0     0 1 0 my ($self) = @_;
388             }
389              
390             sub add_cols {
391 0     0 1 0 my ($self,$config) = @_;
392 0 0       0 $config = [$config] unless ref $config eq 'ARRAY';
393              
394 0         0 foreach (@$config) {
395 0 0       0 next unless ref $_ eq 'HASH';
396 0         0 my ($name,$data,$pos) = @$_{(qw(name data before))};
397 0         0 my $max_pos = $self->get_col_count();
398              
399 0 0 0     0 $pos = $self->_lookup_index(ucfirst $pos || '') || $max_pos unless defined $pos && $pos =~ /^\d+$/;
      0        
400 0 0       0 $pos = $max_pos if $pos > $max_pos;
401 0 0       0 $data = [$data] unless ref $data eq 'ARRAY';
402              
403 0         0 splice(@{$self->{'fields_arry'}},$pos,0,$name);
  0         0  
404 0         0 $self->_reset_fields_hash();
405 0         0 splice(@$_,$pos,0,_rotate($data)) for (@{$self->{rows}});
  0         0  
406             }
407              
408 0         0 return $self;
409             }
410              
411             sub drop_cols {
412 0     0 1 0 my ($self,$cols) = @_;
413 0         0 $cols = $self->_refinate($cols);
414              
415 0         0 foreach my $col (@$cols) {
416 0         0 my $index = delete $self->{'fields_hash'}->{$col};
417 0         0 splice(@{$self->{'fields_arry'}},$index,1);
  0         0  
418 0         0 $self->_reset_fields_hash();
419 0         0 splice(@$_,$index,1) for (@{$self->{'rows'}});
  0         0  
420             }
421              
422 0         0 return $self;
423             }
424              
425             ###################### DEPRECATED ##################################
426              
427             sub get_table {
428 0     0 1 0 carp "get_table() is deprecated. Use output() instead";
429 0         0 output(@_);
430             }
431              
432             sub modify_tag {
433 0     0 1 0 carp "modify_tag() is deprecated. Use modify() instead";
434 0         0 modify(@_);
435             }
436              
437             sub map_col {
438 0     0 1 0 carp "map_col() is deprecated. Use map_cell() instead";
439 0         0 map_cell(@_);
440             }
441              
442             #################### UNDER THE HOOD ################################
443              
444             # repeat: it only looks complicated
445              
446             sub _build_table {
447 7     7   10 my ($self) = @_;
448 7         12 my $attribs = $self->{'global'}->{'table'};
449              
450 7         7 my ($head,$body,$foot);
451 7         16 $head = $self->_build_head;
452 7 50       38 $body = $self->{'rows'} ? $self->_build_body : '';
453 7 100       18 $foot = $self->{'totals'} ? $self->_build_foot : '';
454              
455             # w3c says tfoot comes before tbody ...
456 7         14 my $cdata = $head . $foot . $body;
457              
458 7         18 return _tag_it('table', $attribs, $cdata) . $N;
459             }
460              
461             sub _build_head {
462 7     7   10 my ($self) = @_;
463 7         6 my ($attribs,$cdata,$caption);
464 7         9 my $output = '';
465              
466             # build the
tag if applicable
467 7 50       19 if ($caption = $self->{'global'}->{'caption'}) {
468 0         0 $attribs = $self->{'global'}->{'caption_attribs'};
469 0 0       0 $cdata = $self->{'encode_cells'} ? $self->_xml_encode($caption) : $caption;
470 0         0 $output .= $N.$T . _tag_it('caption', $attribs, $cdata);
471             }
472              
473             # build the
474 7 50       15 if ($attribs = $self->{'global'}->{'colgroup'}) {
475 0         0 $cdata = $self->_build_head_colgroups();
476 0         0 $output .= $N.$T . _tag_it('colgroup', $attribs, $cdata);
477             }
478              
479             # go ahead and stop if they don't want the head
480 7 50       18 return "$output\n" if $self->{'no_head'};
481              
482             # prepare
483             my $tr_attribs = _merge_attribs(
484 7         30 $self->{'head'}->{'tr'}, $self->{'global'}->{'tr'}
485             );
486 7         24 my $tr_cdata = $self->_build_head_row();
487              
488             # prepare the
489 7   33     34 $attribs = $self->{'head'}->{'thead'} || $self->{'global'}->{'thead'};
490 7         23 $cdata = $N.$T . _tag_it('tr', $tr_attribs, $tr_cdata) . $N.$T;
491              
492             # add the
493 7         16 $output .= $N.$T . _tag_it('thead', $attribs, $cdata) . $N;
494             }
495              
496             sub _build_head_colgroups {
497 0     0   0 my ($self) = @_;
498 0         0 my (@cols,$output);
499              
500 0 0       0 return unless $self->{'colgroups'};
501 0 0       0 return undef unless @cols = @{$self->{'colgroups'}};
  0         0  
502              
503 0         0 foreach (@cols) {
504 0         0 $output .= $N.$T.$T . _tag_it('col', $_);
505             }
506 0         0 $output .= $N.$T;
507              
508 0         0 return $output;
509             }
510              
511             sub _build_head_row {
512 7     7   9 my ($self) = @_;
513 7         9 my $output = $N;
514 7         8 my @copy = @{$self->{'fields_arry'}};
  7         23  
515              
516 7         14 foreach my $field (@copy) {
517             my $attribs = _merge_attribs(
518             $self->{$field}->{'th'} || $self->{'head'}->{'th'},
519 14   33     68 $self->{'global'}->{'th'} || $self->{'head'}->{'th'},
      33        
520             );
521              
522 14 100       40 if (my $sub = $self->{'map_head'}->{$field}) {
    50          
523 4         7 $field = $sub->($field);
524             }
525             elsif (!$self->{'no_ucfirst'}) {
526 10         23 $field = ucfirst( lc( $field ) );
527             }
528              
529             # bug 21761 "Special XML characters should be expressed as entities"
530 14 100       74 $field = $self->_xml_encode( $field ) if $self->{'encode_cells'};
531              
532 14         30 $output .= $T.$T . _tag_it('th', $attribs, $field) . $N;
533             }
534              
535 7         21 return $output . $T;
536             }
537              
538             sub _build_body {
539              
540 7     7   10 my ($self) = @_;
541 7         7 my $beg = 0;
542 7         8 my $output;
543              
544             # if a group was not set via set_group(), then use the entire 2-d array
545             my @indicies = exists $self->{'body_breaks'}
546 7 100       20 ? @{$self->{'body_breaks'}}
  3         6  
547             : ($self->get_row_count - 1);
548              
549             # the skinny here is to grab a slice of the rows, one for each group
550 7         12 foreach my $end (@indicies) {
551 7   50     14 my $body_group = $self->_build_body_group([@{$self->{'rows'}}[$beg..$end]]) || '';
552 7   33     28 my $attribs = $self->{'global'}->{'tbody'} || $self->{'body'}->{'tbody'};
553 7         13 my $cdata = $N . $body_group . $T;
554              
555 7         14 $output .= $T . _tag_it('tbody',$attribs,$cdata) . $N;
556 7         15 $beg = $end + 1;
557             }
558 7         16 return $output;
559             }
560              
561             sub _build_body_group {
562              
563 7     7   9 my ($self,$chunk) = @_;
564 7         6 my ($output,$cdata);
565             my $attribs = _merge_attribs(
566 7         23 $self->{'body'}->{'tr'}, $self->{'global'}->{'tr'}
567             );
568 7         14 my $pk_col = '';
569              
570             # build the rows
571 7         22 for my $i (0..$#$chunk) {
572 14         13 my @row = @{$chunk->[$i]};
  14         30  
573 14 50       34 $pk_col = splice(@row,$self->{'pk_index'},1) if defined $self->{'pk_index'};
574 14   100     81 $cdata = $self->_build_body_row(\@row, ($i and $self->{'nodup'} or 0), $pk_col);
575 14         57 $output .= $T . _tag_it('tr',$attribs,$cdata) . $N;
576             }
577              
578             # build the subtotal row if applicable
579 7 50       9 if (my $subtotals = shift @{$self->{'sub_totals'}}) {
  7         26  
580 0         0 $cdata = $self->_build_body_subtotal($subtotals);
581 0         0 $output .= $T . _tag_it('tr',$attribs,$cdata) . $N;
582             }
583              
584 7         21 return $output;
585             }
586              
587             sub _build_body_row {
588 14     14   21 my ($self,$row,$nodup,$pk) = @_;
589              
590 14         13 my $group = $self->{'group'};
591 14 100       35 my $index = $self->_lookup_index($group) if $group;
592 14         18 my $output = $N;
593              
594 14         23 $self->{'current_row'} = $pk;
595              
596 14         27 for (0..$#$row) {
597 28         49 my $name = $self->_lookup_name($_);
598             my $attribs = _merge_attribs(
599             $self->{$name}->{'td'} || $self->{'body'}->{'td'},
600 28   33     129 $self->{'global'}->{'td'} || $self->{'body'}->{'td'},
      33        
601             );
602              
603             # suppress warnings AND keep 0 from becoming  
604 28 50       84 $row->[$_] = '' unless defined($row->[$_]);
605              
606             # bug 21761 "Special XML characters should be expressed as entities"
607 28 100       55 $row->[$_] = $self->_xml_encode( $row->[$_] ) if $self->{'encode_cells'};
608              
609             my $cdata = ($row->[$_] =~ /^\s+$/)
610 28 50       89 ? $self->{'null_value'}
611             : $row->[$_]
612             ;
613              
614 28         47 $self->{'current_col'} = $name;
615              
616             $cdata = ($nodup and $index == $_)
617             ? $self->{'nodup'}
618 28 100 100     110 : _map_it($self->{'map_cell'}->{$name},$cdata)
619             ;
620              
621 28         60 $output .= $T.$T . _tag_it('td', $attribs, $cdata) . $N;
622             }
623 14         39 return $output . $T;
624             }
625              
626             sub _build_body_subtotal {
627 0     0   0 my ($self,$row) = @_;
628 0         0 my $output = $N;
629              
630 0 0       0 return '' unless $row;
631              
632 0         0 for (0..$#$row) {
633 0         0 my $name = $self->_lookup_name($_);
634 0         0 my $sum = ($row->[$_]);
635             my $attribs = _merge_attribs(
636             $self->{$name}->{'th'} || $self->{'body'}->{'th'},
637 0   0     0 $self->{'global'}->{'th'} || $self->{'body'}->{'th'},
      0        
638             );
639              
640             # use sprintf if mask was supplied
641 0 0 0     0 if ($self->{'subtotals_mask'} and defined $sum) {
642 0         0 $sum = sprintf($self->{'subtotals_mask'},$sum);
643             }
644             else {
645 0 0       0 $sum = (defined $sum) ? $sum : $self->{'null_value'};
646             }
647              
648 0         0 $output .= $T.$T . _tag_it('th', $attribs, $sum) . $N;
649             }
650 0         0 return $output . $T;
651             }
652              
653             sub _build_foot {
654 2     2   8 my ($self) = @_;
655              
656             my $tr_attribs = _merge_attribs(
657             # notice that foot is 1st and global 2nd - different than rest
658 2         6 $self->{'foot'}->{'tr'}, $self->{'global'}->{'tr'}
659             );
660 2         6 my $tr_cdata = $self->_build_foot_row();
661              
662 2   33     6 my $attribs = $self->{'foot'}->{'tfoot'} || $self->{'global'}->{'tfoot'};
663 2         4 my $cdata = $N.$T . _tag_it('tr', $tr_attribs, $tr_cdata) . $N.$T;
664              
665 2         3 return $T . _tag_it('tfoot',$attribs,$cdata) . $N;
666             }
667              
668             sub _build_foot_row {
669 2     2   2 my ($self) = @_;
670              
671 2         3 my $output = $N;
672 2         3 my $row = $self->{'totals'};
673              
674 2         4 for (0..$#$row) {
675 4         6 my $name = $self->_lookup_name($_);
676             my $attribs = _merge_attribs(
677             $self->{$name}->{'th'} || $self->{'foot'}->{'th'},
678 4   33     18 $self->{'global'}->{'th'} || $self->{'foot'}->{'th'},
      33        
679             );
680 4         5 my $sum = ($row->[$_]);
681              
682             # use sprintf if mask was supplied
683 4 50 33     12 if ($self->{'totals_mask'} and defined $sum) {
684 0         0 $sum = sprintf($self->{'totals_mask'},$sum)
685             }
686             else {
687 4 100       8 $sum = defined $sum ? $sum : $self->{'null_value'};
688             }
689              
690 4         9 $output .= $T.$T . _tag_it('th', $attribs, $sum) . $N;
691             }
692 2         4 return $output . $T;
693             }
694              
695             # builds a tag and it's enclosed data
696             sub _tag_it {
697 92     92   106 my ($name,$attribs,$cdata) = @_;
698 92         108 my $text = "<\L$name\E";
699              
700             # build the attributes if any - skip blank vals
701 92         77 for my $k (sort keys %{$attribs}) {
  92         207  
702 4         5 my $v = $attribs->{$k};
703 4 50       9 if (ref $v eq 'HASH') {
704             $v = join('; ', map {
705 4         6 my $attrib = $_;
  4         4  
706             my $value = (ref $v->{$_} eq 'ARRAY')
707             ? _rotate($v->{$_})
708 4 50       10 : $v->{$_};
709 4   50     19 join(': ',$attrib,$value||'');
710             } sort keys %$v) . ';';
711             }
712 4 50       7 $v = _rotate($v) if (ref $v eq 'ARRAY');
713 4 50       18 $text .= qq| \L$k\E="$v"| unless $v =~ /^$/;
714             }
715 92 50       457 $text .= (defined $cdata) ? ">$cdata" : '/>';
716             }
717              
718             # used by map_cell() and map_head()
719             sub _map_it {
720 25     25   35 my ($sub,$datum) = @_;
721 25 50       57 return $datum unless $sub;
722 0         0 return $datum = $sub->($datum);
723             }
724              
725             # used by calc_totals() and calc_subtotals()
726             sub _total_chunk {
727 1     1   1 my ($self,$chunk,$indexes) = @_;
728 1         2 my %totals;
729              
730 1         2 foreach my $row (@$chunk) {
731 2         3 foreach (@$indexes) {
732 2 50       11 $totals{$_} += $row->[$_] if $row->[$_] =~ /^[-0-9\.]+$/;
733             }
734             }
735              
736 1 100       24 return [ map { defined $totals{$_} ? $totals{$_} : undef } (0 .. $self->get_col_count() - 1) ];
  2         8  
737             }
738              
739             # uses %ESCAPES to convert the '4 Horsemen' of XML
740             # big thanks to Matt Sergeant
741             sub _xml_encode {
742 6     6   13 my ($self,$str) = @_;
743 6         27 $str =~ s/([&<>"])/$ESCAPES{$1}/ge;
  24         87  
744 6         17 return $str;
745             }
746              
747             # returns value of and moves first element to last
748             sub _rotate {
749 4     4   4 my $ref = shift;
750 4         299 my $next = shift @$ref;
751 4         15 push @$ref, $next;
752 4         6 return $next;
753             }
754              
755             # always returns an array ref
756             sub _refinate {
757 3     3   3 my ($self,$ref) = @_;
758 3 50 33     8 $ref = undef if ref($ref) eq 'ARRAY' && scalar( @$ref ) < 1;
759 3 50       10 $ref = [@{$self->{'fields_arry'}}] unless defined $ref;
  0         0  
760 3 50       9 $ref = [$ref] unless ref $ref eq 'ARRAY';
761 3 50 0     4 return [map {$_ =~ /^\d+$/ ? $self->_lookup_name($_) || $_ : $_} @$ref];
  3         17  
762             }
763              
764             sub _merge_attribs {
765 62     62   73 my ($hash1,$hash2) = @_;
766              
767 62 100       169 return $hash1 unless $hash2;
768 4 50       6 return $hash2 unless $hash1;
769              
770 4         11 return {%$hash2,%$hash1};
771             }
772              
773             sub _lookup_name {
774 32     32   35 my ($self,$index) = @_;
775 32         50 return $self->{'fields_arry'}->[$index];
776             }
777              
778             sub _lookup_index {
779 6     6   4 my ($self,$name) = @_;
780 6         19 return $self->{'fields_hash'}->{$name};
781             }
782              
783             sub _reset_fields_hash {
784 2     2   3 my $self = shift;
785 2         5 my $i = 0;
786 2         3 $self->{fields_hash} = { map { $_ => $i++ } @{$self->{fields_arry}} };
  4         21  
  2         6  
787             }
788              
789             # assigns a non-DBI supplied data table (2D array ref)
790             sub _do_black_magic {
791 2     2   5 my ($self,$ref,$headers) = @_;
792 2 50       11 croak "bad data" unless ref( $ref->[0] ) eq 'ARRAY';
793 2 50       7 $self->{'fields_arry'} = $headers ? [@$headers] : [ @{ shift @$ref } ];
  2         16  
794 2         10 $self->{'fields_hash'} = $self->_reset_fields_hash();
795 2         8 $self->{'rows'} = $ref;
796             }
797              
798             # disconnect database handle if i created it
799             sub DESTROY {
800 2     2   931 my ($self) = @_;
801 2 50       10 unless ($self->{'keep_alive'}) {
802 2 50       87 $self->{'dbh'}->disconnect if defined $self->{'dbh'};
803             }
804             }
805              
806             1;
807             __END__