File Coverage

blib/lib/HTML/QuickTable.pm
Criterion Covered Total %
statement 147 203 72.4
branch 92 122 75.4
condition 50 80 62.5
subroutine 11 14 78.5
pod 2 2 100.0
total 302 421 71.7


\n";
line stmt bran cond sub pod time code
1              
2             package HTML::QuickTable;
3              
4             =head1 NAME
5              
6             HTML::QuickTable - Quickly create fairly complex HTML tables
7              
8             =head1 SYNOPSIS
9              
10             use HTML::QuickTable;
11              
12             my $qt = HTML::QuickTable->new(
13             table_width => '95%', # opt method 1
14             td => {bgcolor => 'gray'}, # opt method 2
15             font_face => 'arial', # set font
16             font => {face => 'arial'}, # same thing
17             labels => 1, # make top ?
18             stylesheet => 1, # use stylesheet?
19             styleclass => 'mytable', # class to use
20             useid => 'results', # id="results_r1c2" etc
21             header => 0, # print header?
22             );
23              
24             my $table1 = $qt->render(\@array_of_data);
25              
26             my $table2 = $qt->render(\%hash_of_keys_and_values);
27              
28             my $table3 = $qt->render($object_with_param_method);
29              
30             =cut
31              
32 1     1   7731 use Carp;
  1         2  
  1         78  
33 1     1   7 use strict;
  1         3  
  1         41  
34 1     1   5 use vars qw($VERSION %INDENT);
  1         6  
  1         4406  
35              
36             $VERSION = do { my @r=(q$Revision: 1.12 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };
37             %INDENT = (
38             table => 0,
39             tr => 1,
40             th => 2,
41             td => 2,
42             );
43              
44             sub _expopts {
45             # This is a general-purpose option-parsing routine that
46             # puts stuff down one level if it has a _ in it; this
47             # allows stuff like "td_height => 50" and "td => {height => 50}"
48 17   100 17   57 my $lev = shift || 0;
49 17         36 my %opt = ();
50 17         21 $lev++;
51 17         39 while (@_) {
52 53         89 my $key = shift;
53 53         64 my $val = shift;
54 53 100 66     4170 if ($key =~ /^([a-zA-Z0-9]+)_(.*)/) {
    100 66        
    100 66        
    100          
    100          
55             # looks like "td_height" or "font_face"
56 6         32 $opt{$1}{$2} = $val;
57             } elsif (ref $val eq 'HASH') {
58             # this allows "table => {width => '95%'}"
59 9         33 $opt{$key} = _expopts($lev, %$val);
60             } elsif ($key eq 'font' && $lev == 1) {
61             # special catch for two options to be FormBuilder-like
62 3         16 $opt{font}{face} = $val;
63             } elsif ($key eq 'lalign' && $lev == 1) {
64 1         5 $opt{th}{align} = $val;
65             } elsif ($key eq 'border' && $lev == 1) {
66             # useful shortcut
67 1         5 $opt{table}{border} = $val;
68             } else {
69             # put regular options in the top-level space
70 33         117 $opt{$key} = $val;
71             }
72             }
73 17         18 $lev--;
74 17 100       111 return wantarray ? %opt : \%opt;
75             }
76              
77             sub new {
78 8     8 1 3501 my $self = shift;
79 8   33     46 my $class = ref($self) || $self;
80 8         30 my %opt = _expopts(0, @_);
81              
82             # counters
83 8         24 $opt{_level} = 0;
84 8         14 $opt{_sentheader} = 0;
85              
86             # special options
87 8 50       27 $opt{table}{border} = delete $opt{border} if exists $opt{border}; # legacy
88 8   100     47 $opt{body} ||= {bgcolor => 'white'};
89 8   100     36 $opt{null} ||= ''; # prevents warnings
90              
91             # stylesheet handling
92 8 100       35 if ($opt{stylesheet}) {
93 2   50     7 $opt{styleclass} ||= 'qt';
94 2         6 delete $opt{font}; # kill font
95             }
96              
97             # setup our font tag separately
98             # do this here or else every call to render() must do it
99 2         10 ($opt{_fo}, $opt{_fc}) = $opt{font}
100 8 100       46 ? (_tag('font', %{$opt{font}}), '')
101             : ('','');
102              
103 8         44 return bless \%opt, $class;
104             }
105              
106             # Internal tag routines stolen from CGI::FormBuilder, which
107             # in turn stole them from CGI.pm
108              
109             sub _escapeurl ($) {
110             # minimalist, not 100% correct, URL escaping
111 0   0 0   0 my $toencode = shift || return undef;
112 0         0 $toencode =~ s!([^a-zA-Z0-9_,.-/])!sprintf("%%%02x",ord($1))!eg;
  0         0  
113 0         0 return $toencode;
114             }
115              
116             sub _escapehtml ($) {
117 107 50   107   405 defined(my $toencode = shift) or return '';
118 107         114 eval { require HTML::Entities };
  107         4359  
119 107 50       20351 if ($@) {
120             # not found; use very basic built-in HTML escaping
121 0         0 $toencode =~ s!&!&!g;
122 0         0 $toencode =~ s!
123 0         0 $toencode =~ s!>!>!g;
124 0         0 $toencode =~ s!"!"!g;
125 0         0 return $toencode;
126             } else {
127             # dispatch to HTML::Entities
128 107         319 return HTML::Entities::encode($toencode);
129             }
130 0         0 return $toencode;
131             }
132              
133             sub _tag ($;@) {
134             # called as _tag('tagname', %attr)
135             # creates an HTML tag on the fly, quick and dirty
136 149   50 149   322 my $name = shift || return;
137 149         149 my @tag;
138 149         229 my %saw = (); # prevent dups
139 149         302 while (@_) {
140             # this cleans out all the internal junk kept in each data
141             # element, returning everything else (for an html tag)
142 95         132 my $key = lc shift;
143 95         147 my $val = _escapehtml shift; # minimalist HTML escaping
144 95 50       1514 push @tag, qq($key="$val") unless $saw{$key}++;
145             }
146 149         976 return '<' . join(' ', $name, sort @tag) . '>';
147             }
148              
149             sub _tohtml ($) {
150 12 100   12   66 defined(my $text = shift) or return;
151              
152             # Need to catch the < and > commonly used in emails
153 10         17 $text = _escapehtml($text);
154              
155             # A couple little catches
156 10         107 $text =~ s!\*([^\*]+)\*!$1!g;
157 10         12 $text =~ s!\_([^\_]+)\_!$1!g;
158              
159             # Also catch links - remember there are a LOT of assumptions here!!!
160 10         13 $text =~ s!(http[s]?://[\=\.\-\/\w+\?]+)(\s+)!$1$2!g;
161 10         33 $text =~ s!([\w\.\-\+\_]+\@[\w\-\.]+)!$1!g; # email addrs
162              
163 10         22 return $text;
164             }
165              
166             sub _toname ($) {
167             # creates a name from a var/file name (like file2name)
168 0     0   0 my $name = shift;
169 0         0 $name =~ s!\.\w+$!!; # lose trailing ".cgi" or whatever
170 0         0 $name =~ s![^a-zA-Z0-9.-/]+! !g;
171 0         0 $name =~ s!\b(\w)!\u$1!g;
172 0         0 return $name;
173             }
174              
175             # These handle styleclass and id generation, if requested
176             sub _getclass {
177 50     50   58 my $self = shift;
178 50 50       91 return '' unless $self->{stylesheet};
179 50   100     183 my $row = shift || 0; # is a row
180              
181             # if styleclass is an array, alternate between
182 50         89 my $class = '';
183 50 100       102 if (ref $self->{styleclass} eq 'ARRAY') {
184 31 100 100     77 if ($row && $self->{_notfirstrow}) { # only alternate rows
185 4   100     7 push @{$self->{_tmpclass}||=[]}, shift @{$self->{styleclass}};
  4         18  
  4         44  
186 4 100       11 unless (@{$self->{styleclass}}) {
  4         24  
187             # have pushed thru all, so start over
188 1         4 $self->{styleclass} = delete $self->{_tmpclass};
189             }
190             }
191 31         49 $class = $self->{styleclass}[0];
192             } else {
193 19         26 $class = $self->{styleclass};
194             }
195 50         131 return $class;
196             }
197              
198             # Generate a unique id for each element
199             sub _getid {
200 19     19   31 my $self = shift;
201 19 50       47 return '' unless $self->{useid};
202 19         50 my $base = join '', @_; # rest is 'r', 42, 'c', 15, etc
203 19 100       89 return $base ? "$self->{useid}_$base" : $self->{useid};
204             }
205              
206             # Keep track of the appropriate indent
207             sub _indent {
208 0     0   0 local $^W = 0;
209 0         0 my $self = shift;
210 0         0 my $what = shift; # element name
211 0         0 return ' ' x $INDENT{$what};
212 0   0     0 my $last = $self->{_lastidt} || '';
213 0 0       0 if (! $last) {
    0          
    0          
    0          
214             # first layer
215 0         0 $self->{_indent} = 0;
216             } elsif ($what eq $last) {
217             # nothing, same
218 0   0     0 $self->{_indent} ||= 0
219             } elsif ($INDENT{$what} > $INDENT{$last}) {
220             # use it as a base
221 0         0 $self->{_indent}++;
222             } elsif ($INDENT{$what} < $INDENT{$last}) {
223             # we're nesting
224 0         0 $self->{_indent}--;
225             }
226 0         0 $self->{_lastidt} = $what;
227 0         0 return ' ' x ($self->{_indent} * $INDENT{$last});
228             }
229              
230             # This recursively renders a data structure into a table
231             sub render {
232             # Do the work and return as a scalar
233 41     41 1 91 my $self = shift;
234 41         63 my($data, $html) = ('','');
235 41         67 my $ref = ref $_[0];
236 41 50       149 if (@_ > 1) {
    50          
    0          
237             # assume that it's an array
238 0         0 $ref = 'ARRAY';
239 0         0 $data = [ @_ ];
240             } elsif ($ref) {
241             # shift it
242 41         88 $data = shift;
243             } elsif (! $self->{_level}) {
244 0         0 croak '[HTML::QuickTable] Argument to render() must be \@array, \%hash, or $object';
245             } else {
246 0         0 $ref = 'ARRAY';
247 0         0 $data = [ @_ ];
248             }
249              
250             # We expand data differently depending on what type of structure it is
251             # Truthfully, all this sub can handle is arrayrefs. Everything else
252             # is converted on the fly by the "else" statement to an arrayref and
253             # this sub is recursively called.
254              
255 41 100       82 if ($ref eq 'ARRAY') {
256              
257             # create our opening table tag
258 38 100       138 my $tab = $self->{_level} ? {width => '100%'} : $self->{table};
259 38 100       93 $tab->{id} = $self->_getid if $self->{useid};
260 38 100       138 $tab->{class} = $self->_getclass if $self->{stylesheet};
261 38 100       122 $html .= _tag('table', %$tab) . "\n" unless ++$self->{_level} == 2;
262              
263 38         80 my @tmprow = ();
264 38 100 100     127 if ($self->{vertical} && ref $data->[0] eq 'ARRAY') {
265             # Whole different algorithm, here we must iterate in a column-
266             # based manner, not a row-based one. This means walking the
267             # array "backwards". Notice the for loops iterate inside-out.
268 2         4 for (my $ci=0; $ci < @{$data->[0]}; $ci++) {
  10         27  
269 8         61 $tmprow[$ci] = [];
270 8         19 for (my $ri=0; $ri < @$data; $ri++) {
271 24         21 push @{$tmprow[$ci]}, $data->[$ri][$ci];
  24         75  
272             }
273             }
274             } else {
275             # non-vertical or already expanded/rearranged
276 36         101 @tmprow = @$data;
277             }
278              
279             # Now, walk all arrays in the same manner, since vert's were rearranged
280 38         44 my $colnum = 0;
281 38   100     143 $self->{_rownum} ||= 0;
282 38         64 for my $row (@tmprow) {
283 124 100       263 unless ($self->{_level} == 2) {
284 33 100       77 $self->{tr}{id} = $self->_getid('r', ++$self->{_rownum}) if $self->{useid};
285 33 100       78 $self->{tr}{class} = $self->_getclass(1) if $self->{stylesheet};
286 33         38 $html .= ' ' . _tag('tr', %{$self->{tr}}) . "\n";
  33         102  
287             }
288 124 100       268 if ($self->{_level} == 1) {
289 27         80 $html .= $self->render($row);
290             }
291             else {
292             # For an array, we do not generate each time, only the first
293             # time per the row/column
294 97         115 my $td = 'td';
295 97 100       210 if (my $l = $self->{labels}) {
296 69 100 100     723 if (($l =~ /[1T]/i && ! $self->{_notfirstrow})
    50 100        
      66        
      100        
      66        
297             || ($l =~ /L/i && ! $colnum)
298             || ($l =~ /R/i && $colnum == (@tmprow-1))
299             ) {
300 23         37 $td = 'th';
301             } elsif ($l =~ /B/i) {
302 0         0 croak "[HTML::QuickTable] Sorry, labels => 'B' is broken - want to patch it?";
303             }
304             }
305              
306             # Catch td class stuff
307 97 100       221 $self->{$td}{id} = $self->_getid('r', $self->{_rownum}, 'c', $colnum+1) if $self->{useid};
308 97 100       229 $self->{$td}{class} = $self->_getclass if $self->{stylesheet};
309              
310             # Recurse data structures
311 97 100       160 if (ref $row) {
312 3         5 $html .= ' ' . _tag($td, %{$self->{$td}}) . $self->{_fo}
  3         11  
313             . $self->render($row) . $self->{_fc} . "\n";
314             }
315             else {
316 94 50 33     355 $row = _toname($row) if $self->{nameopts} && $td eq 'th';
317 94 100       195 $row = _tohtml($row) if $self->{htmlize};
318 94         127 my $tdptr = $self->{$td};
319 94 100       265 unless (defined $row) {
320             # "null", so alter HTML accordingly
321 4         7 $row = $self->{null};
322 4 50       77 $tdptr = $self->{nulltags} if $self->{nulltags};
323 4 50       11 $tdptr->{id} = $self->_getid('r', $self->{_rownum}, 'c', $colnum+1) if $self->{useid};
324 4 100 33     20 $tdptr->{class} ||= $self->_getclass if $self->{stylesheet};
325             }
326 94         101 $html .= ' ' . _tag($td, %{$tdptr}) . $self->{_fo}
  94         253  
327             . $row . $self->{_fc} . "\n";
328             }
329             }
330 124 100       349 unless ($self->{_level} == 2) {
331 33         50 $html .= "
332             }
333 124         226 $colnum++;
334             }
335 38 100       186 $html .= '
' unless $self->{_level}-- == 2 ; 336               337             } else { 338               339             # Must expand the data structure carefully 340 3 50 0     9 if ($ref eq 'HASH') {     0           341             # This assumes that the data struct is consistent; we cannot 342             # handle any other kind because of our assumptions 343             # Guess struct based on the first key we see 344 3         9 my $key = each %$data; 345 3         6 my @new = (); 346 3 50 33     21 if (ref $data->{$key} eq 'HASH') {     50           347             # keylabel => {colname => value, colname => value} 348               349             # this bit of "pre-scanning" gets all the available 350             # column names in our data 351 0         0 my %cols; 352 0         0 my @rows = sort keys %$data; 353 0         0 for my $row (@rows) { 354 0         0 $cols{$_}++ for keys %{$data->{$row}};   0         0   355             } 356               357             # Now that we have a list of all our columns, we must 358             # re-iterate through all our rows (again!) to get vals 359 0         0 my @cols = sort keys %cols; 360 0         0 for my $row (@rows) { 361 0         0 my @thisrow = (); 362 0         0 for my $col (@cols) { 363 0   0     0 $data->{$row}{$col} ||= undef; # causes autoviv 364             #if (ref $data->{$row}{$col} && 365             #ref $data->{$row}{$col} ne 'ARRAY') 366             #{ 367             # recursively call for refs 368             #push @thisrow, $self->render($data->{$row}{$col}); 369             #} else { 370             #my $val = ref $data->{$row}{$col} eq 'ARRAY' 371             #? $data->{$row}{$col} : [$data->{$row}{$col}]; 372             #push @thisrow, [$row, @$val]; 373 0         0 push @thisrow, $data->{$row}{$col}; 374             #} 375             } 376 0         0 push @new, [$row, @thisrow]; 377             } 378 0   0     0 my $keylabel = $self->{keylabel} || ''; 379 0         0 unshift @new, [$keylabel, @cols]; 380             } 381             elsif (ref $data->{$key} eq 'ARRAY' || ! ref $data->{$key}) { 382             # keylabel => [value, value, value] or keylabel => value 383               384 3         12 for my $row (sort keys %$data) { 385 3 50       13 my $val = ref $data->{$row} eq 'ARRAY' ? $data->{$row} : [$data->{$row}]; 386 3         14 push @new, [$row, @$val]; 387             } 388             } 389             # both methods above will fill up @new 390 3         26 $html .= $self->render(\@new); 391             } 392             elsif ($ref && UNIVERSAL::can($ref, 'param')) { 393             # object with param method 394 0         0 my @keys = $data->param; 395 0         0 $self->{labels} = 1; 396 0         0 my @new = (); 397 0         0 for my $key (@keys) { 398 0         0 my(@val) = $data->param($key); 399 0 0       0 my $val = @val > 1 ? \@val : $val[0]; 400 0         0 push @new, $val; 401             } 402 0         0 $data = [\@keys, \@new]; 403 0         0 $html .= $self->render($data); 404             } 405             } 406               407 41 100 100     154 if ($self->{header} && ! $self->{_level} && ! $self->{_sentheader}++) {       66         408 3 100       14 my $title = $self->{title} ? (''._escapehtml($self->{title})."\n") : ''; 409 3 100       44 my $h3 = $self->{title} ? "

$self->{title}

\n" : ''; 410 3 100 66     17 my $style = ($self->{stylesheet} && $self->{stylesheet} ne 1) 411             ? qq(\n) : ''; 412 3 100       11 my $text = $self->{text} ? "$self->{text}\n" : ''; 413               414 3         12 $html = "Content-type: text/html; charset=iso-8859-1\n\n" . '' # fuck doctypes, really 415 3         12 . "\n" . _tag('head', %{$self->{head}}) . "\n" . $style . $title . "\n" 416 3         5 . _tag('body', %{$self->{body}}) . $self->{_fo} . "\n" 417             . $h3 . $text . $html . $self->{_fc} . "\n"; 418             } 419               420             # detect what row we're in by counting down and up 421 41         73 $self->{_notfirstrow} = $self->{_level}; 422               423 41         130 return $html; 424             } 425               426             1; 427               428             __END__