File Coverage

blib/lib/HTML/Tabulate.pm
Criterion Covered Total %
statement 666 761 87.5
branch 350 498 70.2
condition 186 307 60.5
subroutine 62 68 91.1
pod 10 56 17.8
total 1274 1690 75.3


line stmt bran cond sub pod time code
1             package HTML::Tabulate;
2              
3 26     26   959260 use 5.005;
  26         110  
  26         1173  
4 26     26   236 use Carp;
  26         61  
  26         2656  
5 26     26   28621 use URI::Escape;
  26         58772  
  26         2268  
6 26     26   208 use Scalar::Util qw(blessed);
  26         54  
  26         3431  
7 26     26   154 use strict;
  26         52  
  26         1234  
8 26     26   157 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK $TITLE_HEADING_LEVEL);
  26         72  
  26         426594  
9              
10             require Exporter;
11             @ISA = qw(Exporter);
12             @EXPORT = qw();
13             @EXPORT_OK = qw(&render);
14              
15             $VERSION = '0.44';
16             my $DEFAULT_TEXT_FORMAT = "

%s

\n";
17             my %DEFAULT_DEFN = (
18             style => 'down',
19             table => {},
20             title => { format => "

%s

\n" },
21             text => { format => $DEFAULT_TEXT_FORMAT },
22             caption => { type => 'caption', format => $DEFAULT_TEXT_FORMAT },
23             field_attr => { -defaults => {}, },
24             );
25             my %VALID_ARG = (
26             table => 'HASH/SCALAR',
27             thead => 'HASH/SCALAR',
28             tbody => 'HASH/SCALAR',
29             tfoot => 'HASH/SCALAR',
30             tr => 'HASH/CODE',
31             thtr => 'HASH',
32             th => 'HASH',
33             td => 'HASH',
34             fields => 'ARRAY',
35             fields_add => 'HASH',
36             fields_omit => 'ARRAY',
37             in_fields => 'ARRAY',
38             labels => 'SCALAR/HASH',
39             label_links => 'HASH',
40             stripe => 'ARRAY/SCALAR/HASH',
41             null => 'SCALAR',
42             trim => 'SCALAR',
43             style => 'SCALAR',
44             # limit => 'SCALAR',
45             # output => 'SCALAR',
46             # first => 'SCALAR',
47             # last => 'SCALAR',
48             field_attr => 'HASH',
49             # xhtml: boolean indicating whether to use xhtml-style tagging
50             xhtml => 'SCALAR',
51             # title: title/heading to be rendered above table
52             title => 'SCALAR/HASH/CODE',
53             # text: text to be rendered above table, after title
54             text => 'SCALAR/HASH/CODE',
55             # caption: text to be rendered below table
56             caption => 'SCALAR/HASH/CODE',
57             # data_prepend: data rows to be inserted before main dataset
58             data_prepend => 'ARRAY',
59             # data_append: data rows to be appended to main dataset
60             data_append => 'ARRAY',
61             # colgroups: array of hashrefs to be inserted as individual colgroups
62             colgroups => 'ARRAY',
63             # labelgroups: named groupings of labels used to create two-tier headers
64             labelgroups => 'HASH',
65             # derived: fields not present in the underlying data, to skip unnecessary lookups
66             derived => 'ARRAY',
67             );
68             my %VALID_FIELDS = (
69             -defaults => 'HASH',
70             );
71             my %FIELD_ATTR = (
72             escape => 'SCALAR',
73             value => 'SCALAR/CODE',
74             format => 'SCALAR/CODE',
75             link => 'SCALAR/CODE',
76             label => 'SCALAR/CODE',
77             label_format => 'SCALAR/CODE',
78             label_link => 'SCALAR/CODE',
79             label_escape => 'SCALAR',
80             default => 'SCALAR',
81             composite => 'ARRAY',
82             composite_join => 'SCALAR/CODE',
83             derived => 'SCALAR',
84             );
85             my %MINIMISED_ATTR = map { $_ => 1 } qw(
86             checked compact declare defer disabled ismap multiple
87             nohref noresize noshade nowrap readonly selected
88             );
89             my $URI_ESCAPE_CHARS = "^A-Za-z0-9\-_.!~*'()?&;:/=";
90             $TITLE_HEADING_LEVEL = 'h2'; # TODO: deprecated
91              
92             # -------------------------------------------------------------------------
93             # Provided for subclassing
94             sub get_valid_arg
95             {
96 123 50   123 0 3098 return wantarray ? %VALID_ARG : \%VALID_ARG;
97             }
98              
99             # Provided for subclassing
100             sub get_valid_fields
101             {
102 123 50   123 0 935 return wantarray ? %VALID_FIELDS : \%VALID_FIELDS;
103             }
104              
105             # Provided for subclassing
106             sub get_field_attributes
107             {
108 31 50   31 0 198 return wantarray ? %FIELD_ATTR : \%FIELD_ATTR;
109             }
110              
111             #
112             # Check $self->{defn} for invalid arguments or types
113             #
114             sub check_valid
115             {
116 123     123 0 241 my ($self, $defn) = @_;
117              
118             # Check top-level args
119 123         378 my %valid = $self->get_valid_arg();
120 123         489 my (@invalid, @badtype);
121 123         632 for (sort keys %$defn) {
122 241 50       579 if (! exists $valid{$_}) {
123 0         0 push @invalid, $_;
124 0         0 next;
125             }
126 241         959 my $type = ref $defn->{$_};
127 241 50 66     2726 push @badtype, $_
      66        
128             if $type && $type ne 'SCALAR' && $valid{$_} !~ m/$type/;
129 241 50 66     1051 push @badtype, $_
130             if ! $type && $valid{$_} !~ m/SCALAR/;
131             }
132 123 50       371 croak "[check_valid] invalid argument found: " . join(',',@invalid)
133             if @invalid;
134 123 50       358 croak "[check_valid] invalid types for argument: " . join(',',@badtype)
135             if @badtype;
136              
137             # Check special fields
138 123         393 %valid = $self->get_valid_fields();
139 123         247 @invalid = ();
140 123         446 @badtype = ();
141 123         205 for (sort grep(/^-/, keys(%{$defn->{field_attr}})) ) {
  123         694  
142 6 50       25 if (! exists $valid{$_}) {
143 0         0 push @invalid, $_;
144 0         0 next;
145             }
146 6         19 my $type = ref $defn->{field_attr}->{$_};
147 6 50 33     130 push @badtype, $_
      33        
148             if $type && $type ne 'SCALAR' && $valid{$_} !~ m/$type/;
149 6 0 33     31 push @badtype, $_
150             if ! $type && $valid{$_} !~ m/SCALAR/;
151             }
152 123 50       461 croak "[check_valid] invalid field argument found: " . join(',',@invalid)
153             if @invalid;
154 123 50       296 croak "[check_valid] invalid types for field argument: " . join(',',@badtype)
155             if @badtype;
156              
157             # Check field attributes
158 123   66     655 $self->{field_attr} ||= $self->get_field_attributes();
159 123         156 %valid = %{$self->{field_attr}};
  123         1485  
160 123         430 @badtype = ();
161 123         186 for my $field (keys %{$defn->{field_attr}}) {
  123         845  
162 55 50       204 croak "[check_valid] invalid field argument entry '$field': " .
163             $defn->{field_attr}->{$field}
164             if ref $defn->{field_attr}->{$field} ne 'HASH';
165 55         78 for (sort keys %{$defn->{field_attr}->{$field}}) {
  55         212  
166 91 100       251 next if ! exists $valid{$_};
167 46 50       98 next if ! $valid{$_};
168 46         96 my $type = ref $defn->{field_attr}->{$field}->{$_};
169 46 50       99 if (! ref $valid{$_}) {
    0          
170 46 50 66     336 push @badtype, $_
      66        
171             if $type && $type ne 'SCALAR' && $valid{$_} !~ m/$type/;
172 46 50 66     216 push @badtype, $_
173             if ! $type && $valid{$_} !~ m/SCALAR/;
174             }
175             elsif (ref $valid{$_} eq 'ARRAY') {
176 0 0       0 if ($type) {
177 0         0 push @badtype, $_;
178             }
179             else {
180 0         0 my $val = $defn->{field_attr}->{$field}->{$_};
181 0 0       0 push @badtype, "$_ ($val)" if ! grep /^$val$/, @{$valid{$_}};
  0         0  
182             }
183             }
184             else {
185 0         0 croak "[check_valid] invalid field attribute entry for '$_': " .
186             ref $valid{$_};
187             }
188             }
189 55 50       231 croak "[check_valid] invalid type for '$field' field attribute: " .
190             join(',',@badtype) if @badtype;
191             }
192             }
193              
194             #
195             # Merge $hash1 and $hash2 together, returning the result (or, in void
196             # context, merging into $self->{defn}). Performs a shallow (one-level deep)
197             # hash merge unless the field is defined in the @recurse_keys array, in
198             # which case we do a full recursive merge.
199             #
200             sub merge
201             {
202 246     246 1 3672 my $self = shift;
203 246   50     797 my $hash1 = shift || {};
204 246         299 my $hash2 = shift;
205 246         315 my $arg = shift;
206              
207 246 50       720 croak "[merge] invalid hash1 '$hash1'" if ref $hash1 ne 'HASH';
208 246 50 66     1153 croak "[merge] invalid hash2 '$hash2'" if $hash2 && ref $hash2 ne 'HASH';
209              
210 246         422 my $single_arg = ! $hash2;
211              
212             # Use $self->{defn} as $hash1 if only one argument
213 246 100       702 if ($single_arg) {
214 2         4 $hash2 = $hash1;
215 2         5 $hash1 = $self->{defn};
216             }
217              
218             # Check hash2 for valid args (except when recursive)
219 246   100     1373 my $sub = (caller(1))[3] || '';
220 246 100       1118 $self->check_valid($hash2) unless substr($sub, -7) eq '::merge';
221              
222 246         673 my $merge = $self->deepcopy($hash1);
223              
224             # Add hash2 to $merge
225 246         719 my @recurse_keys = qw(field_attr);
226 246         660 for my $key (keys %$hash2) {
227             # If this value is a hashref on both sides, do a shallow hash merge
228             # unless we need to do a proper recursive merge
229 395 100 100     1876 if (ref $hash2->{$key} eq 'HASH' && ref $merge->{$key} eq 'HASH') {
230             # Recursive merge
231 153 100       1942 if (grep /^$key$/, @recurse_keys) {
232 123         539 $merge->{$key} = $self->merge($hash1->{$key}, $hash2->{$key});
233             }
234             # Shallow hash merge
235             else {
236 30         50 @{$merge->{$key}}{ keys %{$hash1->{$key}}, keys %{$hash2->{$key}} } = (values %{$hash1->{$key}}, values %{$hash2->{$key}});
  30         133  
  30         67  
  30         59  
  30         73  
  30         79  
237             }
238             }
239             # Otherwise (scalars, arrayrefs etc) just copy the value
240             else {
241 242         643 $merge->{$key} = $hash2->{$key};
242             }
243             }
244              
245             # In void context update $self->{defn}
246 246 100       566 if (! defined wantarray) {
247 2         5 $self->{defn} = $merge;
248             # Must invalidate transient $self->{defn_t} when $self->{defn} changes
249 2 50       14 delete $self->{defn_t} if exists $self->{defn_t};
250             }
251             else {
252 244         925 return $merge;
253             }
254             }
255              
256             sub defn
257             {
258 8     8 0 1245 my $self = shift;
259 8         21 return $self->{defn};
260             }
261              
262             # Initialisation
263             sub init
264             {
265 31     31 0 62 my $self = shift;
266 31   100     145 my $defn = shift || {};
267 31 50 33     323 croak "[init] invalid defn '$defn'" if $defn && ref $defn ne 'HASH';
268              
269             # Map $defn table => 1 to table => {} for cleaner merging
270 31 50 66     173 $defn->{table} = {} if $defn->{table} && ! ref $defn->{table};
271              
272             # Initialise $self->{defn} by merging defaults and $defn
273 31         163 $self->{defn} = $self->merge(\%DEFAULT_DEFN, $defn);
274              
275 31         110 return $self;
276             }
277              
278             sub new
279             {
280 31     31 1 13276 my $class = shift;
281 31         68 my $self = {};
282 31         91 bless $self, $class;
283 31         152 $self->init(@_);
284             }
285              
286             # -------------------------------------------------------------------------
287             #
288             # If deriving field names, also derive labels (if not already defined)
289             #
290             sub derive_label
291             {
292 356     356 0 540 my ($self, $field) = @_;
293 356         1383 $field =~ s/_+/ /g;
294 356         1089 $field = join ' ', map { ucfirst($_) } split(/\s+/, $field);
  668         8940  
295 356         1288 $field =~ s/(Id)$/\U$1/;
296 356         831 return $field;
297             }
298              
299             #
300             # Try and derive a reasonable field list from $self->{defn_t} using the set data.
301             # Croaks on failure.
302             #
303             sub derive_fields
304             {
305 7     7 0 11 my ($self, $set) = @_;
306 7         13 my $defn = $self->{defn_t};
307              
308             # For iterators, prefetch the first row and use its keys
309 7 50       17 croak "invalid Tabulate data type '$set'" unless ref $set;
310 7 50 33     69 if (ref $set eq 'CODE') {
    50 33        
    50 33        
    100 33        
    50          
    50          
311 0         0 my $row = $set->();
312 0         0 $self->{prefetch} = $row;
313 0 0       0 $defn->{fields} = [ sort keys %$row ] if eval { keys %$row };
  0         0  
314             }
315             elsif (blessed $set and $set->can('Next')) {
316 0 0       0 my $row = $set->can('First') ? $set->First : $set->Next;
317 0         0 $self->{prefetch} = $row;
318 0 0       0 $defn->{fields} = [ sort keys %$row ] if eval { keys %$row };
  0         0  
319             }
320             elsif (blessed $set and $set->can('next')) {
321 0 0       0 my $row = $set->can('first') ? $set->first : $set->next;
322 0         0 $self->{prefetch} = $row;
323 0 0       0 $defn->{fields} = [ sort keys %$row ] if eval { keys %$row };
  0         0  
324             }
325             # For arrays
326             elsif (ref $set eq 'ARRAY') {
327 6 50       15 if (! @$set) {
328 0         0 $defn->{fields} = [];
329 0         0 return;
330             }
331 6         9 my $obj = $set->[0];
332             # Arrayref of hashrefs
333 6 100       16 if (ref $obj eq 'HASH') {
    50          
    0          
334 3         21 $defn->{fields} = [ sort keys %$obj ];
335             }
336             # Arrayref of arrayrefs - access via subscripts unless labels are defined
337             elsif (ref $obj eq 'ARRAY') {
338 3 50       9 if ($defn->{labels}) {
339 0         0 croak "[derive_fields] no fields found and cannot derive fields from data arrayrefs";
340             }
341             # Arrayref of arrayrefs, labels off
342             else {
343 3         11 $defn->{fields} = [ 0 .. $#$obj ];
344             }
345             }
346             # For Class::DBI objects, derive via columns groups
347             elsif ($obj->isa('Class::DBI')) {
348 0         0 my @col = $obj->columns('Tabulate');
349 0 0 0     0 @col = ( $obj->columns('Essential'), $obj->columns('Others') )
350             if ! @col && $obj->columns('Essential');
351 0 0       0 @col = $obj->columns('All') if ! @col;
352 0 0       0 $defn->{fields} = [ @col ] if @col;
353             }
354             # If all else fails, try treating as a hash
355 6 50 33     25 unless (ref $defn->{fields} && @{$defn->{fields}}) {
  6         26  
356 0 0       0 if (! defined eval { $defn->{fields} = [ sort keys %$obj ] }) {
  0         0  
357 0         0 croak "[derive_fields] no fields found and initial object '$obj' is strange type";
358             }
359             }
360             }
361             # Else looks like a single object - check for Class::DBI
362             elsif (ref $set && ref $set ne 'HASH' && $set->isa('Class::DBI')) {
363 0         0 my @col = $set->columns('Tabulate');
364 0 0 0     0 @col = ( $set->columns('Essential'), $set->columns('Others') )
365             if ! @col && $set->columns('Essential');
366 0 0       0 @col = $set->columns('All') if ! @col;
367 0 0       0 $defn->{fields} = [ @col ] if @col;
368             }
369             # Otherwise try treating as a hash
370 1         3 elsif (defined eval { keys %$set }) {
371 1         6 my $first = (sort keys %$set)[0];
372 1 50       4 my $ref = ref $set->{$first} if defined $first;
373             # Check whether first value is reference
374 1 50       3 if ($ref) {
375             # Hashref of hashrefs
376 0 0       0 if ($ref eq 'HASH') {
    0          
    0          
377 0         0 $defn->{fields} = [ sort keys %{$set->{$first}} ];
  0         0  
378             }
379             elsif (ref $set->[0] ne 'ARRAY') {
380 0         0 croak "[derive_fields] no fields found and first row '" . $set->[0] . "' is strange type";
381             }
382             # Hashref of arrayrefs - fatal only if labels => 1
383             elsif ($defn->{labels}) {
384 0         0 croak "[derive_fields] no fields found and cannot derive fields from data arrayrefs";
385             }
386             # Hashref of arrayrefs, labels off
387             else {
388 0         0 $defn->{fields} = [ 0 .. $#{$set->[$first]} ];
  0         0  
389             }
390             }
391             else {
392 1         6 $defn->{fields} = [ sort keys %$set ];
393             }
394             }
395             else {
396 0         0 croak "[derive_fields] no fields found and set '$set' is strange type: $@";
397             }
398            
399 7 50       31 croak sprintf "[derive_fields] field derivation failed (fields: %s)",
400             $defn->{fields}
401             unless ref $defn->{fields} eq 'ARRAY';
402             }
403              
404             # Derive a fields list if none is defined
405             sub check_fields
406             {
407 107     107 0 193 my $self = shift;
408 107         166 my ($set) = @_;
409 100         593 $self->derive_fields($set)
410             if ! $self->{defn_t}->{fields} ||
411             ref $self->{defn_t}->{fields} ne 'ARRAY' ||
412 107 100 66     1056 ! @{$self->{defn_t}->{fields}};
      66        
413             }
414              
415             # Splice additional fields into the fields array
416             sub splice_fields
417             {
418 1     1 0 2 my $self = shift;
419 1         2 my $defn = $self->{defn_t};
420 1         2 my $add = $defn->{fields_add};
421 1 50 33     14 return unless ref $defn->{fields} eq 'ARRAY' && ref $add eq 'HASH';
422              
423 1         2 for (my $i = $#{$defn->{fields}}; $i >= 0; $i--) {
  1         5  
424 6         10 my $f = $defn->{fields}->[$i];
425 6 100       20 next unless $add->{$f};
426 2 100       5 if (ref $add->{$f} eq 'ARRAY') {
427 1         2 splice @{$defn->{fields}}, $i+1, 0, @{$add->{$f}};
  1         2  
  1         5  
428             }
429             else {
430 1         2 splice @{$defn->{fields}}, $i+1, 0, $add->{$f};
  1         7  
431             }
432             }
433             }
434              
435             # Omit/remove fields from the fields array
436             sub omit_fields
437             {
438 26     26 0 40 my $self = shift;
439 26         40 my $defn = $self->{defn_t};
440 26         36 my %omit = map { $_ => 1 } @{$defn->{fields_omit}};
  52         126  
  26         52  
441 26         174 $defn->{fields} = [ grep { ! exists $omit{$_} } @{$defn->{fields}} ];
  134         308  
  26         53  
442             }
443              
444             #
445             # Deep copy routine, originally swiped from a Randal Schwartz column
446             #
447             sub deepcopy
448             {
449 5304     5304 0 15549 my ($self, $this) = @_;
450 5304 100       12842 if (! ref $this) {
    100          
    100          
    50          
    0          
451 2488         9582 return $this;
452             } elsif (ref $this eq "ARRAY") {
453 249         647 return [map $self->deepcopy($_), @$this];
454             } elsif (ref $this eq "HASH") {
455 2458         8027 return {map { $_ => $self->deepcopy($this->{$_}) } keys %$this};
  3583         12572  
456             } elsif (ref $this eq "CODE") {
457 109         458 return $this;
458             } elsif (sprintf $this) {
459             # Object! As a last resort, try copying the stringification value
460 0         0 return sprintf $this;
461             } else {
462 0         0 die "what type is $_? (" . ref($this) . ")";
463             }
464             }
465              
466             #
467             # Create a transient presentation definition (defn_t) by doing a set of one-off
468             # or dataset-specific mappings on the current table definition e.g. deriving
469             # a field list if none is set, setting up a field map for arrayref-of-
470             # arrayref sets, and mapping top-level shortcuts into their field
471             # attribute equivalents.
472             #
473             sub prerender_munge
474             {
475 107     107 0 186 my $self = shift;
476 107         205 my ($set, $defn) = @_;
477              
478             # Use $self->{defn} if $defn not passed
479 107   66     348 $defn ||= $self->{defn};
480              
481             # If already done, return unless we require any dataset-specific mappings
482             # if ($self->{defn_t}) {
483             # return unless
484             # ref $defn->{fields} ne 'ARRAY' ||
485             # ! @{$defn->{fields}} ||
486             # (ref $set eq 'ARRAY' && @$set && ref $set->[0] eq 'ARRAY');
487             # }
488              
489             # Copy $defn to $self->{defn_t}
490 107         259 $self->{defn_t} = $self->deepcopy($defn);
491              
492             # Try to derive field list if not set
493 107         1922 $self->check_fields($set);
494              
495             # Set up a field map in case we have arrayref based data
496 107         230 my $defn_t = $self->{defn_t};
497 107         168 my $pos = 0;
498 107 100       355 my $fields = ref $defn_t->{in_fields} eq 'ARRAY' ? $defn_t->{in_fields} : $defn_t->{fields};
499 107         278 $defn_t->{field_map} = { map { $_ => $pos++; } @$fields };
  437         1225  
500              
501             # Splice any additional fields into the fields array
502 107 100       366 $self->splice_fields if $defn_t->{fields_add};
503 107 100       344 $self->omit_fields if $defn_t->{fields_omit};
504              
505             # Map top-level 'labels' and 'label_links' hashrefs into fields
506 107 100       323 if (ref $defn_t->{labels} eq 'HASH') {
507 10         20 for (keys %{$defn_t->{labels}}) {
  10         37  
508 23   100     94 $defn_t->{field_attr}->{$_} ||= {};
509 23         87 $defn_t->{field_attr}->{$_}->{label} = $defn_t->{labels}->{$_};
510             }
511             }
512 107 100       341 if (ref $defn_t->{label_links} eq 'HASH') {
513 1         2 for (keys %{$defn_t->{label_links}}) {
  1         5  
514 1   50     8 $defn_t->{field_attr}->{$_} ||= {};
515 1         5 $defn_t->{field_attr}->{$_}->{label_link} = $defn_t->{label_links}->{$_};
516             }
517             }
518              
519             # Map top-level 'derived' field list into fields
520 107 50       306 if ($defn_t->{derived}) {
521 0         0 for (@{ $defn_t->{derived} }) {
  0         0  
522 0         0 $defn_t->{field_attr}->{$_}->{derived} = 1;
523             }
524             }
525              
526             # If style across, map top-level 'thtr' hashref into -defaults label_ attributes
527 107 100 100     458 if ($self->{defn_t}->{style} eq 'across' && ref $defn_t->{thtr} eq 'HASH') {
528 1         2 for (keys %{$defn_t->{thtr}}) {
  1         5  
529 1 50       12 $defn_t->{field_attr}->{-defaults}->{"label_$_"} = $defn_t->{thtr}->{$_}
530             if ! exists $defn_t->{field_attr}->{-defaults}->{"label_$_"};
531             }
532             }
533             # Map top-level 'th' hashref into -defaults label_ attributes
534 107 100       343 if (ref $defn_t->{th} eq 'HASH') {
535 8         18 for (keys %{$defn_t->{th}}) {
  8         30  
536 8 50       80 $defn_t->{field_attr}->{-defaults}->{"label_$_"} = $defn_t->{th}->{$_}
537             if ! exists $defn_t->{field_attr}->{-defaults}->{"label_$_"};
538             }
539             }
540             # Map top-level 'td' hashref into -defaults
541 107 100       411 if (ref $defn_t->{td} eq 'HASH') {
542 4         8 $defn_t->{field_attr}->{-defaults} = { %{$defn_t->{td}}, %{$defn_t->{field_attr}->{-defaults}} };
  4         13  
  4         24  
543             }
544              
545             # Move regex field_attr definitions into a -regex hash
546 107         374 $defn_t->{field_attr}->{-regex} = {};
547 107         180 for (keys %{$defn_t->{field_attr}}) {
  107         403  
548             # The following test is an ugly hack, but the regex is stringified now
549 289 100       851 next unless m/^\(\?.*\)$/;
550 4         13 $defn_t->{field_attr}->{-regex}->{$_} = $defn_t->{field_attr}->{$_};
551 4         11 delete $defn_t->{field_attr}->{$_};
552             }
553              
554             # Force a non-array stripe to be a binary array
555 107 100 100     651 if ($defn_t->{stripe} && ref $defn_t->{stripe} ne 'ARRAY') {
556 6         38 $defn_t->{stripe} = [ undef, $defn_t->{stripe} ];
557             }
558              
559             # thead and tfoot imply tbody
560 107 100       312 if ($defn_t->{thead}) {
561 7   100     37 $defn_t->{tbody} ||= 1;
562 7 100       28 $defn_t->{thead} = {} if ! ref $defn_t->{thead};
563             }
564 107 100       339 if ($defn_t->{tfoot}) {
565 3   100     9 $defn_t->{tbody} ||= 1;
566 3 100       8 $defn_t->{tfoot} = {} if ! ref $defn_t->{tfoot};
567             }
568              
569             # Setup tbody attributes hash for hashref tbodies
570 107 100       503 if ($defn_t->{tbody}) {
571 23 100       53 if (ref $defn_t->{tbody}) {
572 13         34 $defn_t->{tbody_attr} = $self->deepcopy($defn_t->{tbody});
573 13         23 for (keys %{$defn_t->{tbody_attr}}) {
  13         43  
574 17 100       99 delete $defn_t->{tbody_attr}->{$_} if m/^-/;
575             }
576             }
577             else {
578 10         35 $defn_t->{tbody_attr} = {};
579             }
580             }
581              
582             }
583              
584             # Split fields up according to labelgroups into two field lists
585             # labelgroup entries look like LabelGroup => [ qw(field1 field2 field3) ]
586             sub labelgroup_fields
587             {
588 1     1 0 10 my $self = shift;
589              
590 1         2 my @fields = @{$self->{defn_t}->{fields}};
  1         5  
591 1         2 my $labelgroups = $self->{defn_t}->{labelgroups};
592              
593             # Map first field of each labelgroup into a hash
594 1         2 my %grouped_fields;
595 1         11 for my $label (keys %$labelgroups) {
596 1         3 my $field1 = $labelgroups->{$label}->[0];
597 1         5 $grouped_fields{ $field1 } = $label;
598             }
599              
600             # Process all fields looking for label groups, and splitting out if found
601 1         2 my (@fields1, @fields2);
602 1         6 while (my $f = shift @fields) {
603 3 100       10 if (my $label = $grouped_fields{ $f }) {
604             # Found a grouped label - splice labelled fields into fields2
605 1         2 my @gfields = @{ $labelgroups->{$label} };
  1         3  
606 1         2 shift @gfields; # discard $f
607              
608             # Check all fields match
609 1         2 my @next_group;
610 1         5 while (my $g = shift @gfields) {
611 1         2 my $fn = shift @fields;
612 1 50       8 push @next_group, $fn if $fn eq $g;
613             }
614              
615             # If we have as many as we're expecting, we're good
616 1 50       2 if (@next_group == @{ $labelgroups->{$label} } - 1) {
  1         39  
617 1         4 push @fields2, $f, @next_group;
618 1         5 push @fields1, $label;
619             }
620             # Otherwise our field list doesn't exactly match the label group - omit
621             else {
622 0         0 push @fields1, $f;
623             # Push @next_group back into @fields for reprocessing
624 0         0 unshift @fields, @next_group;
625             }
626             }
627              
628             # Not a labelgroup
629             else {
630 2         8 push @fields1, $f;
631             }
632             }
633              
634             # Setup $field1_tx_attr map if we have any @fields2 fields
635 1         2 my $field1_tx_attr = {};
636 1 50       5 if (@fields2) {
637 1         2 for my $f (@fields1) {
638 3 100       11 if (my $grouped_fields = $labelgroups->{$f}) {
639 1         4 $field1_tx_attr->{$f} = { colspan => scalar(@$grouped_fields) };
640             }
641             else {
642 2         9 $field1_tx_attr->{$f} = { rowspan => 2 };
643             }
644             }
645             }
646              
647 1         5 return (\@fields1, \@fields2, $field1_tx_attr);
648             }
649              
650             # -------------------------------------------------------------------------
651             #
652             # Return the given HTML $tag with attributes from the $attr hashref.
653             # An attribute with a non-empty value (i.e. not '' or undef) is rendered
654             # attr="value"; one with a value of '' is rendered as a 'bare' attribute
655             # (i.e. no '=') in non-xhtml mode; one with undef is simply ignored
656             # (e.g. allowing unset CGI parameters to be ignored).
657             #
658             sub start_tag
659             {
660 2279     2279 0 3788 my ($self, $tag, $attr, $close) = @_;
661 2279         3837 my $xhtml = $self->{defn_t}->{xhtml};
662 2279         3386 my $str = "<$tag";
663 2279 100       5361 if (ref $attr eq 'HASH') {
664 2245         8183 for my $a (sort keys %$attr) {
665 364 100       840 next if ! defined $attr->{$a};
666 340 100       759 if ($attr->{$a} ne '') {
667 316         978 $str .= qq( $a="$attr->{$a}");
668             }
669             else {
670 24 100       44 if ($MINIMISED_ATTR{$a}) {
671 12 100       39 $str .= $xhtml ? qq( $a="$a") : qq( $a);
672             }
673             else {
674 12         29 $str .= qq( $a="");
675             }
676             }
677             }
678             }
679 2279 100 100     5779 $str .= ' /' if $close && $xhtml;
680 2279         3142 $str .= ">";
681 2279         7415 return $str;
682             }
683              
684             sub end_tag
685             {
686 2270     2270 0 3312 my ($self, $tag) = @_;
687 2270         6907 return "";
688             }
689              
690             # ------------------------------------------------------------------------
691             # Pre- and post-table content
692              
693             # Title, text, and caption elements may be:
694             # - hashref, containing 'value' (scalar) and 'format' (scalar or subref)
695             # elements that are rendered like table cells
696             # - scalar, that is treated as a scalar 'value' as above with a default
697             # 'format'
698             # - subref, that is executed and the results used verbatim (i.e. no default
699             # 'format' applies
700             sub text_element
701             {
702 321     321 0 405 my $self = shift;
703 321         522 my ($type, $dataset) = @_;
704 321 50       6497 return '' unless grep /^$type$/, qw(title text caption);
705              
706 321         689 my $elt = $self->{defn_t}->{$type};
707              
708             # Subref - execute and return results
709 321 100       1172 if (ref $elt eq 'CODE') {
    100          
710 2         12 return $elt->($dataset, $type);
711             }
712              
713             # Scalar - convert to hashref
714             elsif (! ref $elt) {
715 16         22 my $value = $elt;
716 16         73 $elt = {};
717             # If there's a DEFAULT_DEFN $elt entry, use that as defaults
718 16 50 33     97 if ($DEFAULT_DEFN{$type} && ref $DEFAULT_DEFN{$type} eq 'HASH') {
719 16         16 $elt = { %{$DEFAULT_DEFN{$type}} };
  16         67  
720             }
721 16         43 $elt->{value} = $value;
722             }
723              
724             # Hashref - render and return
725 319 50       704 if (ref $elt eq 'HASH') {
726 319 100 100     2362 return '' unless defined $elt->{value} or defined $elt->{title};
727              
728             # Omit formatting if tag-wrapped
729 25 100 100     165 return $elt->{value}
730             if defined $elt->{value} && $elt->{value} =~ m/^\s*\<.*\>\s*$/s;
731 19 50 66     70 return $elt->{title}
732             if defined $elt->{title} && $elt->{title} =~ m/^\s*\<.*\>\s*$/s;
733              
734             # sprintf format pattern
735 19 100 100     235 return sprintf $elt->{format}, $elt->{value}
      100        
736             if defined $elt->{value} && defined $elt->{format} &&
737             ! ref $elt->{format};
738              
739             # subref format pattern
740 6 100 100     53 return $elt->{format}->($elt->{value}, $dataset, $type)
      66        
741             if defined $elt->{value} && defined $elt->{format} &&
742             ref $elt->{format} eq 'CODE';
743            
744             # Deprecated formatting style
745 3 100       12 if ($elt->{title}) {
746 2         5 my $title = $elt->{title};
747 2   100     10 my $tag = $elt->{tag} || 'h2';
748 2         4 delete $elt->{title};
749 2         3 delete $elt->{tag};
750 2         5 delete $elt->{format};
751 2         5 return $self->start_tag($tag, $elt) . $title .
752             $self->end_tag($tag, $elt) . "\n";
753             }
754              
755             # fallthru: return 'value'
756 1         7 return $elt->{value};
757             }
758              
759 0         0 return '';
760             }
761              
762             # unchomp: ensure (non-empty) elements end with a newline
763             sub unchomp
764             {
765 321     321 0 748 my $self = shift;
766 321         381 my $data = shift;
767 321 100 66     1782 $data .= "\n" if defined $data && $data ne '' && substr($data,-1) ne "\n";
      100        
768 321         764 $data
769             }
770              
771             # title: title/heading preceding the table
772 107     107 1 165 sub title { my $self = shift; $self->unchomp($self->text_element('title', @_)) }
  107         365  
773             # text: text preceding begin table tag (after title, if any)
774 107     107 1 166 sub text { my $self = shift; $self->unchomp($self->text_element('text', @_)) }
  107         366  
775              
776             # caption: either new-style
text, or legacy text after end table tag
777             sub caption {
778 214     214 1 287 my $self = shift;
779 214         307 my ($set, $post_table) = @_;
780 214         347 my $defn_t = $self->{defn_t};
781              
782             # Legacy text must have a 'format' element
783 214 100 100     2674 if ($post_table &&
    100 66        
      66        
      100        
      66        
784             (ref $defn_t->{caption} ne 'HASH' ||
785             ! $defn_t->{caption}->{type} ||
786             $defn_t->{caption}->{type} ne 'caption_caption')) {
787 105         294 $self->unchomp($self->text_element('caption', $set));
788             }
789             elsif (! $post_table &&
790             (ref $defn_t->{caption} eq 'HASH' &&
791             $defn_t->{caption}->{type} &&
792             $defn_t->{caption}->{type} eq 'caption_caption')) {
793 2 100 50     12 delete $defn_t->{caption}->{format}
794             if ($defn_t->{caption}->{format} || '') eq $DEFAULT_TEXT_FORMAT;
795 2         5 $self->unchomp(
796             $self->start_tag('caption') .
797             $self->text_element('caption', $set) .
798             $self->end_tag('caption')
799             )
800             }
801             }
802              
803             sub colgroups {
804 107     107 1 169 my $self = shift;
805 107         155 my ($set) = @_;
806 107         191 my $defn_t = $self->{defn_t};
807              
808 107 100       418 return '' unless $self->{defn_t}->{colgroups};
809              
810 3         4 my $content = '';
811 3         4 for my $cg (@{$self->{defn_t}->{colgroups}}) {
  3         6  
812 8 100 66     49 if ($cg->{cols} && ref $cg->{cols} && ref $cg->{cols} eq 'ARRAY') {
      66        
813 1         3 my $cols = delete $cg->{cols};
814 1         4 $content .= $self->start_tag('colgroup', $cg, 0) . "\n";
815 1         3 for my $col (@$cols) {
816 2         7 $content .= $self->start_tag('col', $col, 1) . "\n";
817             }
818 1         5 $content .= $self->end_tag('colgroup') . "\n";
819             }
820             else {
821 7         12 $content .= $self->start_tag('colgroup', $cg, 1) . "\n";
822             }
823             }
824 3         7 return $content;
825             }
826              
827             # ------------------------------------------------------------------------
828             # Content before begin table tag
829             sub pre_table
830             {
831 107     107 0 167 my $self = shift;
832 107         176 my ($set) = @_;
833 107         169 my $content = '';
834 107 50       642 $content .= $self->title($set) if $self->{defn_t}->{title};
835 107 50       665 $content .= $self->text($set) if $self->{defn_t}->{text};
836 107         286 return $content;
837             }
838              
839             # Provided for subclassing
840             sub start_table
841             {
842 107     107 0 182 my $self = shift;
843 107 100 66     721 return '' if exists $self->{defn_t}->{table} && ! $self->{defn_t}->{table};
844 106         340 return $self->start_tag('table',$self->{defn_t}->{table}) . "\n";
845             }
846              
847             # Provided for subclassing
848             sub end_table
849             {
850 107     107 0 168 my $self = shift;
851 107 100 66     668 return '' if exists $self->{defn_t}->{table} && ! $self->{defn_t}->{table};
852 106         274 return $self->end_tag('table') . "\n";
853             }
854              
855             # Content after end table tag
856             sub post_table
857             {
858 107     107 0 155 my $self = shift;
859 107         159 my ($set) = @_;
860 107         202 my $content = '';
861 107         311 $content .= $self->caption($set, 'post_table');
862 107         242 return $content;
863             }
864              
865             # ------------------------------------------------------------------------
866             # Apply 'format' formatting
867             sub cell_format_format
868             {
869 28     28 0 43 my ($self, $data, $fattr, $row, $field) = @_;
870 28         44 my $ref = ref $fattr->{format};
871 28 50 66     104 croak "[cell_format] invalid '$field' format: $ref" if $ref && $ref ne 'CODE';
872 28 100 50     118 $data = $fattr->{format}->($data, $row || {}, $field) if $ref eq 'CODE';
873 28 100       183 $data = sprintf $fattr->{format}, $data if ! $ref;
874 28         50 return $data;
875             }
876              
877             # Simple tag escaping
878             sub cell_format_escape
879             {
880 1515     1515 0 2164 my ($self, $data) = @_;
881 1515         2336 $data =~ s/
882 1515         1741 $data =~ s/>/>/g;
883 1515         2845 return $data;
884             }
885              
886             # Link formatting
887             sub cell_format_link
888             {
889 30     30 0 77 my ($self, $data, $fattr, $row, $field, $data_unformatted) = @_;
890 30         31 my $ldata;
891 30         52 my $ref = ref $fattr->{link};
892 30 50 66     154 croak "[cell_format] invalid '$field' link: $ref"
893             if $ref && $ref ne 'CODE';
894 30 100 100     125 $ldata = $fattr->{link}->($data_unformatted, $row || {}, $field)
895             if $ref eq 'CODE';
896 30 100       185 $ldata = sprintf $fattr->{link}, $data_unformatted
897             if ! $ref;
898 30 50       60 if ($ldata) {
899             # $data = sprintf qq(%s),
900             # uri_escape($ldata, $URI_ESCAPE_CHARS), $data;
901 30         122 my $link_attr = { href => uri_escape($ldata, $URI_ESCAPE_CHARS)};
902 30         2376 for my $attr (keys %$fattr) {
903 151 100       411 if ($attr =~ m/^link_/) {
904 34         54 my $val = $fattr->{$attr};
905 34         85 $attr =~ s/^link_//;
906 34 100 100     125 $link_attr->{$attr} = ref $val eq 'CODE' ?
907             $val->($data_unformatted, $row || {}, $field) :
908             $val;
909             }
910             }
911 30         96 $data = $self->start_tag('a', $link_attr) . $data . $self->end_tag('a');
912             }
913 30         79 return $data;
914             }
915              
916             #
917             # Format the given data item using formatting field attributes (e.g. format,
918             # link, escape etc.)
919             #
920             sub cell_format
921             {
922 1630     1630 0 1953 my $self = shift;
923 1630         2415 my ($data, $fattr, $row, $field) = @_;
924 1630         2139 my $defn = $self->{defn_t};
925              
926             # Trim
927 1630 100 100     7492 $data =~ s/^\s*(.*?)\s*$/$1/ if $data ne '' && $defn->{trim};
928              
929 1630         2001 my $data_unformatted = $data;
930              
931             # 'escape' boolean for simple tag escaping (defaults to on)
932 1630 50 33     10363 $data = $self->cell_format_escape($data)
      66        
933             if $data ne '' && ($fattr->{escape} || ! exists $fattr->{escape});
934              
935             # 'format' subroutine or sprintf pattern
936 1630 100       3573 $data = $self->cell_format_format(@_)
937             if $fattr->{format};
938              
939             # 'link' subroutine or sprintf pattern
940 1630 100 100     6348 $data = $self->cell_format_link($data, $fattr, $row, $field, $data_unformatted)
941             if $data ne '' && $fattr->{link};
942              
943             # 'null' defaults
944 1630 100 100     4429 $data = $defn->{null}
945             if defined $defn->{null} && $data eq '';
946              
947 1630         3287 return $data;
948             }
949              
950             sub label
951             {
952 391     391 1 1283 my ($self, $label, $field) = @_;
953              
954             # Use first label if arrayref
955 391         421 my $l;
956 391 100       800 if (ref $label eq 'CODE') {
957 2         7 $l = $label->($field);
958             }
959             else {
960 389         526 $l = $label;
961             }
962 391 100       1273 $l = $self->derive_label($field) unless defined $l;
963 391 100 66     1110 $l = $self->{defn_t}->{null} if $l eq '' && defined $self->{defn_t}->{null};
964 391         1706 return $l;
965             }
966              
967             #
968             # Add in any extra (conditional) defaults for this field.
969             # Provided for subclassing.
970             #
971             sub cell_merge_extras
972             {
973 391     391 0 769 return ();
974             }
975              
976             #
977             # Split field attr data into label, tfoot, and data buckets
978             sub cell_split_label_tfoot_data {
979 391     391 0 559 my ($self, $fattr, $field) = @_;
980              
981 391   50     2107 $self->{defn_t}->{label_attr}->{$field} ||= {};
982 391   50     1782 $self->{defn_t}->{tfoot_attr}->{$field} ||= {};
983 391   50     1699 $self->{defn_t}->{data_attr}->{$field} ||= {};
984              
985 391         1144 for (keys %$fattr) {
986 202 100       608 if (substr($_,0,6) eq 'label_') {
    100          
987 52         209 $self->{defn_t}->{label_attr}->{$field}->{substr($_,6)} = $fattr->{$_};
988             }
989             elsif (substr($_,0,6) eq 'tfoot_') {
990 18         51 $self->{defn_t}->{tfoot_attr}->{$field}->{substr($_,6)} = $fattr->{$_};
991             }
992             else {
993 132         472 $self->{defn_t}->{data_attr}->{$field}->{$_} = $fattr->{$_};
994             }
995             }
996 391         1264 $self->{defn_t}->{label_attr}->{$field}->{value} = $self->label(delete $fattr->{label}, $field);
997             }
998              
999             #
1000             # Create tx_attr for each attr bucket by removing attributes in $field_attr
1001             #
1002             sub cell_split_out_tx_attr {
1003 391     391 0 571 my ($self, $field) = @_;
1004              
1005 391         637 for my $attr (qw(label_attr tfoot_attr data_attr)) {
1006 1173         1373 my %tx_attr = %{ $self->{defn_t}->{$attr}->{$field} };
  1173         5535  
1007 1173         1655 my $tx_code = 0;
1008 1173         2712 for (keys %tx_attr) {
1009 593 100       1745 delete $tx_attr{$_} if exists $self->{field_attr}->{$_};
1010 593 100       1334 delete $tx_attr{$_} if m/^link_/;
1011 593 100       1827 $tx_code = 1 if ref $tx_attr{$_} eq 'CODE';
1012             }
1013 1173         3236 $self->{defn_t}->{$attr}->{$field}->{tx_attr} = \%tx_attr;
1014 1173         3899 $self->{defn_t}->{$attr}->{$field}->{tx_code} = $tx_code;
1015             }
1016             }
1017              
1018             #
1019             # Merge default and field attributes once each per-field for labels and data
1020             #
1021             sub cell_merge_defaults
1022             {
1023 391     391 0 550 my ($self, $row, $field) = @_;
1024              
1025 391 50       1047 return if $self->{defn_t}->{data_attr}->{$field};
1026              
1027             # Create a temp $fattr hash merging defaults, regexes, and field attrs
1028 391         483 my $fattr = { %{$self->{defn_t}->{field_attr}->{-defaults}},
  391         2267  
1029             $self->cell_merge_extras($row, $field) };
1030 391         560 for my $regex (sort keys %{$self->{defn_t}->{field_attr}->{-regex}}) {
  391         1569  
1031 15 100       163 next unless $field =~ $regex;
1032 6         31 @$fattr{ keys %{$self->{defn_t}->{field_attr}->{-regex}->{$regex}} } =
  6         21  
1033 6         8 values %{$self->{defn_t}->{field_attr}->{-regex}->{$regex}};
1034             }
1035 391         1032 @$fattr{ keys %{$self->{defn_t}->{field_attr}->{$field}} } =
  391         1242  
1036 391         892 values %{$self->{defn_t}->{field_attr}->{$field}};
1037            
1038             # Split out label, data, and tfoot attributes
1039 391         1116 $self->cell_split_label_tfoot_data($fattr, $field);
1040              
1041             # Remove tx_attr for label, data, and tfoot attr buckets
1042 391         977 $self->cell_split_out_tx_attr($field);
1043             }
1044              
1045             #
1046             # Set and format the data for a single (data) cell or item
1047             #
1048             sub cell_value
1049             {
1050 1666     1666 0 1929 my $self = shift;
1051 1666         2449 my ($row, $field, $fattr) = @_;
1052 1666         2337 my $defn = $self->{defn_t};
1053 1666         1568 my $value;
1054              
1055             # 'value' literal takes precedence over row
1056 1666 100 100     11016 if (exists $fattr->{value} && ! ref $fattr->{value}) {
    100 66        
1057 175 50       433 $value = defined $fattr->{value} ? $fattr->{value} : '';
1058             }
1059              
1060             # Get value from $row (but skip 'derived' fields)
1061             elsif (ref $row && ! $fattr->{derived}) {
1062 1482 50 66     6414 if (blessed $row) {
    100          
    100          
1063             # Field-methods e.g. Class::DBI, DBIx::Class
1064 0 0 0     0 if (eval { $row->can($field) }
  0 0       0  
1065             && $field ne 'delete') { # special DBIx::Class protection :-)
1066 0         0 $value = eval { $row->$field() };
  0         0  
1067             }
1068             # For DBIx::Class we need to check both methods and get_column() values,
1069             # since joined fields (+columns/+select) are only available via the latter
1070 0         0 elsif (eval { $row->can('get_column') }) {
1071 0         0 $value = eval { $row->get_column($field) };
  0         0  
1072             }
1073             }
1074             elsif (ref $row eq 'ARRAY') {
1075 1149 50       1111 my $i = keys %{$defn->{field_map}} ? $defn->{field_map}->{$field} : $field;
  1149         3490  
1076 1149 100       3611 $value = $row->[ $i ] if defined $i;
1077             }
1078             elsif (ref $row eq 'HASH' && exists $row->{$field}) {
1079 295         517 $value = $row->{$field};
1080             }
1081             }
1082              
1083             # Handle 'value' subref
1084 1666 100 100     4629 if (exists $fattr->{value} && ref $fattr->{value}) {
1085 37         66 my $ref = ref $fattr->{value};
1086 37 50       72 if ($ref eq 'CODE') {
1087 37         114 $value = $fattr->{value}->($value, $row, $field);
1088             }
1089             else {
1090 0         0 croak "[cell_value] invalid '$field' value (not scalar or code ref): $ref";
1091             };
1092             }
1093              
1094 1666 50 66     4028 $value = $fattr->{default} if ! defined $value && exists $fattr->{default};
1095              
1096 1666 100       5382 return defined $value ? $value : '';
1097             }
1098              
1099             #
1100             # Return a cell value created from one or more other cells
1101             #
1102             sub cell_composite
1103             {
1104 6     6 0 10 my $self = shift;
1105 6         10 my ($row, $field, $fattr) = @_;
1106              
1107 6 50       18 my $composite = $fattr->{composite}
1108             or die "Missing composite field attribute";
1109 6         8 my @composite = ();
1110 6         12 for my $f (@$composite) {
1111 12         34 push @composite, $self->cell_single(row => $row, field => $f, tags => 0);
1112             }
1113              
1114 6   50     30 my $composite_join = $fattr->{composite_join} || ' ';
1115 6 50       14 if (ref $composite_join eq 'CODE') {
1116 0         0 return $composite_join->(\@composite, $row, $field);
1117             }
1118             else {
1119 6         26 return join ' ', @composite;
1120             }
1121             }
1122              
1123             #
1124             # Set and format the data for a single (data) cell or item
1125             #
1126             sub cell_content
1127             {
1128 1630     1630 0 1940 my $self = shift;
1129 1630         2438 my ($row, $field, $fattr) = @_;
1130 1630         1672 my $value;
1131              
1132             # Composite fields - concatenate members together
1133 1630 100       3083 if ($fattr->{composite}) {
1134 6         13 $value = $self->cell_composite(@_);
1135             }
1136              
1137             # Standard field - get value from $row
1138             else {
1139 1624         3395 $value = $self->cell_value(@_);
1140             }
1141              
1142             # Format
1143 1630         3694 my $fvalue = $self->cell_format($value, $fattr, $row, $field);
1144              
1145 1630 50       5361 return wantarray ? ($fvalue, $value) : $fvalue;
1146             }
1147              
1148             #
1149             # Wrap cell in or table tags
1150             #
1151             sub cell_tags
1152             {
1153 1610     1610 0 14843 my ($self, $data, $row, $field, $tx_attr) = @_;
1154              
1155 1610 100       2836 my $tag = ! defined $row ? 'th' : 'td';
1156 1610 50       3086 $data = '' unless defined $data;
1157 1610         3035 return $self->start_tag($tag, $tx_attr) . $data . $self->end_tag($tag);
1158             }
1159              
1160             #
1161             # Execute any th or td attribute subrefs
1162             #
1163             sub cell_tx_execute
1164             {
1165 63     63 0 83 my $self = shift;
1166 63         103 my ($tx_attr, $value, $row, $field) = @_;
1167 63         92 my %tx2 = ();
1168 63         208 while (my ($k,$v) = each %$tx_attr) {
1169 82 100       313 if (ref $v eq 'CODE') {
1170 79         199 $tx2{$k} = $v->($value, $row, $field);
1171             }
1172             else {
1173 3         11 $tx2{$k} = $v;
1174             }
1175             }
1176 63         669 return \%tx2;
1177             }
1178              
1179             #
1180             # Render a single table cell or item
1181             #
1182             sub cell_single
1183             {
1184 1630     1630 0 5988 my ($self, %args) = @_;
1185 1630         3003 my $row = delete $args{row};
1186 1630         2610 my $field = delete $args{field};
1187 1630         2311 my $fattr = delete $args{field_attr};
1188 1630         2106 my $tx_attr = delete $args{tx_attr};
1189 1630         1906 my $tx_attr_extra = delete $args{tx_attr_extra};
1190 1630         2459 my $skip_count = delete $args{skip_count};
1191 1630         2812 my $tags = delete $args{tags};
1192 1630 100       3300 $tags = 1 unless defined $tags;
1193 1630 50       3136 die "Unknown arguments to cell_single: " . join(',', keys %args) if %args;
1194              
1195             # Merge default and field attributes once for each field
1196 1630 100       5459 $self->cell_merge_defaults($row, $field)
1197             if ! $self->{defn_t}->{data_attr}->{$field};
1198              
1199 1630         1990 my $tx_code = 0;
1200 1630 50 33     3713 unless ($fattr && $tx_attr) {
1201 1630 100 66     8765 if (! defined $row || $row eq 'thead') {
    100          
1202 161         372 $fattr = $self->{defn_t}->{label_attr}->{$field};
1203             }
1204             elsif ($row eq 'tfoot') {
1205 12         19 $fattr = $self->{defn_t}->{tfoot_attr}->{$field};
1206             }
1207             else {
1208 1457         3315 $fattr = $self->{defn_t}->{data_attr}->{$field};
1209             }
1210 1630         3198 $tx_attr = $fattr->{tx_attr};
1211 1630         3114 $tx_code = $fattr->{tx_code};
1212             }
1213              
1214             # Standard (non-composite) fields
1215 1630         3269 my ($fvalue, $value) = $self->cell_content($row, $field, $fattr);
1216              
1217             # If $tx_attr includes coderefs, execute them
1218 1630 100       3538 $tx_attr = $self->cell_tx_execute($tx_attr, $value, $row, $field)
1219             if $tx_code;
1220              
1221 1630         1909 my $tx_attr_merged = $tx_attr;
1222 1630 100 66     3407 $tx_attr_merged = { %$tx_attr, %{$tx_attr_extra->{$field}} }
  3         12  
1223             if $tx_attr_extra && $tx_attr_extra->{$field};
1224              
1225             # Generate tags
1226 1630 100       4088 my $cell = $tags ? $self->cell_tags($fvalue, $row, $field, $tx_attr_merged) : $fvalue;
1227              
1228 1630 100 66     13196 $$skip_count = $tx_attr->{colspan} ? ($tx_attr->{colspan}-1) : 0
    50 66        
1229             if $skip_count && ref $skip_count && ref $skip_count eq 'SCALAR';
1230              
1231 1630         6650 return $cell;
1232             }
1233              
1234             #
1235             # Legacy interface (deprecated)
1236             #
1237             sub cell_wantarray
1238             {
1239 0     0 0 0 my ($self, $row, $field, $fattr, $tx_attr, %opts) = @_;
1240              
1241 0         0 my $skip_count;
1242 0         0 my $cell = $self->cell_single(
1243             %opts,
1244             row => $row,
1245             field => $field,
1246             field_attr => $fattr,
1247             tx_attr => $tx_attr,
1248             skip_count => \$skip_count,
1249             );
1250              
1251 0         0 return ($cell, $skip_count);
1252             }
1253              
1254             #
1255             # Render a single table cell (legacy interface)
1256             #
1257             sub cell
1258             {
1259 0     0 0 0 my ($self, $row, $field, $fattr, $tx_attr, %opts) = @_;
1260              
1261 0         0 $self->cell_single(
1262             %opts,
1263             row => $row,
1264             field => $field,
1265             field_attr => $fattr,
1266             tx_attr => $tx_attr,
1267             );
1268             }
1269              
1270             #
1271             # Modify the $tr hashref for striping. If $type is 'SCALAR', the stripe is
1272             # a HTML colour string for a bgcolor attribute for the relevant row; if
1273             # $type is 'HASH' the stripe is a set of attributes to be merged.
1274             # $stripe has already been coerced to an arrayref if something else.
1275             #
1276             sub stripe
1277             {
1278 459     459 1 665 my ($self, $tr, $rownum) = @_;
1279 459         790 my $stripe = $self->{defn_t}->{stripe};
1280 459 100       2066 return $tr unless $stripe;
1281            
1282 31         72 my $r = int($rownum % scalar(@$stripe)) - 1;
1283 31 100       77 if (defined $stripe->[$r]) {
1284 22 100       135 if (! ref $stripe->[$r]) {
    50          
1285             # Set bgcolor to stripe (exception: header where bgcolor already set)
1286 13 100 100     74 $tr->{bgcolor} = $stripe->[$r]
1287             unless $rownum == 0 && exists $tr->{bgcolor};
1288             }
1289             elsif (ref $stripe->[$r] eq 'HASH') {
1290             # Class attributes are special in that they're additive,
1291             # so we can merge instead of overwriting
1292 9 100 66     65 if ($stripe->[$r]->{class} && $tr->{class}) {
    100          
1293 5         18 $tr->{class} = "$stripe->[$r]->{class} $tr->{class}";
1294             }
1295              
1296             # Existing attributes take precedence over stripe ones for header
1297             elsif ($rownum == 0) {
1298 1         2 for (keys %{$stripe->[$r]}) {
  1         5  
1299 1 50       7 $tr->{$_} = $stripe->[$r]->{$_} unless exists $tr->{$_};
1300             }
1301             }
1302              
1303             # For non-header rows, merge attributes straight into $tr
1304             else {
1305 3         4 @$tr{keys %{$stripe->[$r]}} = values %{$stripe->[$r]};
  3         10  
  3         8  
1306             }
1307             }
1308             # Else silently ignore
1309             }
1310 31         118 return $tr;
1311             }
1312              
1313             #
1314             # Return tbody close and/or open tags if appropriate, '' otherwise
1315             #
1316             sub tbody
1317             {
1318 407     407 1 479 my $self = shift;
1319 407         525 my ($row, $rownum) = @_;
1320 407         469 my $generate = 0;
1321              
1322 407 100       1457 return '' unless $self->{defn_t}->{tbody};
1323              
1324             # Scalar tbody - generate once only
1325 121 100       477 if (! ref $self->{defn_t}->{tbody}) {
    100          
    100          
1326 36 100       90 $generate++ if ! $self->{defn_t}->{tbody_open};
1327             }
1328            
1329             # tbody with -field - generate when field value changes
1330             elsif ($self->{defn_t}->{tbody}->{'-field'}) {
1331 18         47 my $value = $self->cell_value($row, $self->{defn_t}->{tbody}->{'-field'});
1332 18 100       49 if (exists $self->{defn_t}->{tbody_field_value}) {
1333 15 100 33     68 if ($value eq $self->{defn_t}->{tbody_field_value} ||
      66        
1334             (! defined $value &&
1335             ! defined $self->{defn_t}->{tbody_field_value})) {
1336 8         19 return '';
1337             }
1338             else {
1339 7         11 $generate++;
1340             }
1341             }
1342             else {
1343 3         7 $generate++;
1344             }
1345 10         20 $self->{defn_t}->{tbody_field_value} = $value;
1346             }
1347              
1348             # tbody with -rows - generate when $rownum == $r ** n + 1
1349             elsif (my $r = $self->{defn_t}->{tbody}->{'-rows'}) {
1350 54 100       199 $generate++ if int(($rownum-1) % $r) == 0;
1351             }
1352              
1353             # else a hashref - treat like a scalar
1354             else {
1355 13 100       35 $generate++ if ! $self->{defn_t}->{tbody_open};
1356             }
1357              
1358 113         148 my $tbody = '';
1359 113 100       223 if ($generate) {
1360 50 100       158 if ($self->{defn_t}->{tbody_open}) {
1361 27         56 $tbody .= $self->end_tag('tbody') . "\n";
1362             }
1363 50         137 $tbody .= $self->start_tag('tbody', $self->{defn_t}->{tbody_attr}) . "\n";
1364 50         125 $self->{defn_t}->{tbody_open} = 1;
1365             }
1366 113         228 return $tbody;
1367             }
1368              
1369             #
1370             # Return an attribute hash for table rows
1371             #
1372             sub tr_attr
1373             {
1374 459     459 0 843 my ($self, $rownum, $row, $dataset) = @_;
1375 459         751 my $defn_t = $self->{defn_t};
1376 459         714 my $tr = undef;
1377 459 100       846 if ($rownum == 0) {
1378 38 100       141 $tr = $defn_t->{thtr} if $defn_t->{thtr};
1379 38   66     275 $tr ||= $self->deepcopy($defn_t->{tr_base});
1380             }
1381             else {
1382 421 100 66     1396 if (ref $defn_t->{tr} eq 'CODE' && $row) {
1383 3         15 $tr = $defn_t->{tr}->($row, $dataset);
1384             }
1385             else {
1386 418 100       1144 $defn_t->{tr} = {} unless ref $defn_t->{tr} eq 'HASH';
1387 418         1107 $tr = $self->deepcopy($defn_t->{tr});
1388             # Evaluate any code attributes
1389 418   50     910 $tr ||= {};
1390 418         2351 while (my ($k,$v) = each %$tr) {
1391 29 100       126 $tr->{$k} = $v->($row, $dataset) if ref $v eq 'CODE';
1392             }
1393             }
1394             }
1395             # Stripe and return
1396 459         1268 return $self->stripe($tr, $rownum);
1397             }
1398              
1399             #
1400             # Render a single table row (style 'down')
1401             #
1402             sub row_down
1403             {
1404 451     451 0 815 my ($self, $row, $rownum, %args) = @_;
1405 451         655 my $fields = delete $args{fields};
1406 451   66     2026 $fields ||= $self->{defn_t}->{fields};
1407 451         751 my $tx_attr_extra = delete $args{tx_attr_extra};
1408 451 100       951 my %tx_attr_extra = $tx_attr_extra ? ( tx_attr_extra => $tx_attr_extra ) : ();
1409              
1410             # Open tr
1411 451         541 my $out = '';
1412 451         1048 $out .= $self->start_tag('tr', $self->tr_attr($rownum, $row));
1413              
1414             # Render cells
1415 451         953 my @cells = ();
1416 451         555 my $skip_count = 0;
1417 451         1045 for my $f (@$fields) {
1418 1588 100       3152 if ($skip_count > 0) {
1419 10         12 $skip_count--;
1420 10         15 next;
1421             }
1422              
1423 1578 100       2579 if (! $row) {
1424 145         480 $out .= $self->cell_single(field => $f, skip_count => \$skip_count, %tx_attr_extra);
1425             }
1426             else {
1427 1433         3760 $out .= $self->cell_single(row => $row, field => $f, skip_count => \$skip_count, , %tx_attr_extra);
1428             }
1429             }
1430              
1431 451         1070 $out .= $self->end_tag('tr') . "\n";
1432 451         1845 return $out;
1433             }
1434              
1435             #
1436             # Return a generalised iterator function to walk the set, returning undef at eod
1437             #
1438             sub data_iterator
1439             {
1440 105     105 0 188 my ($self, $set, $fields) = @_;
1441 105         238 my $row = 0;
1442              
1443 105 50       287 croak "invalid Tabulate data type '$set'" unless ref $set;
1444 105 50 33     1361 if (ref $set eq 'CODE') {
    50 33        
    50 33        
    100          
    50          
1445             return sub {
1446 0 0 0 0   0 $row = $row ? $set->() : ($self->{prefetch} || $set->());
1447 0         0 };
1448             }
1449             elsif (blessed $set and $set->can('Next')) {
1450             return sub {
1451 0 0 0 0   0 $row = $row ? $set->Next : ($self->{prefetch} || eval { $set->First } || $set->Next);
1452 0         0 };
1453             }
1454             elsif (blessed $set and $set->can('next')) {
1455             return sub {
1456 0 0 0 0   0 $row = $row ? $set->next : ($self->{prefetch} || eval { $set->first } || $set->next);
1457 0         0 };
1458             }
1459             elsif (ref $set eq 'ARRAY') {
1460             return sub {
1461 489 100   489   1926 return undef if $row > $#$set;
1462 390         1394 $set->[$row++];
1463 99         739 };
1464             }
1465 0         0 elsif (ref $set eq 'HASH' || eval { keys %$set }) {
1466             # Check first value - drill down further unless non-reference
1467 6   33     34 my $k = $fields->[0] || (sort keys %$set)[0];
1468             # For hashes of scalars, just return the hash once-only
1469 6 50       24 if (! ref $set->{$k}) {
1470             return sub {
1471 12 100   12   46 return undef if $row++;
1472 6         22 $set;
1473 6         47 };
1474             }
1475             # For hashes of refs, return the refs in key order
1476             else {
1477             return sub {
1478 0     0   0 my @k = sort keys %$set;
1479 0 0       0 return undef if $row > $#k;
1480 0         0 return $k[$row++];
1481 0         0 };
1482             }
1483             }
1484             else {
1485 0         0 croak "invalid Tabulate data type '$set'";
1486             }
1487             }
1488              
1489             #
1490             # Render the table body with successive records down the page
1491             #
1492             sub body_down
1493             {
1494 105     105 0 167 my ($self, $set) = @_;
1495              
1496             # Get data_iterator
1497 105 50       521 my @fields = @{$self->{defn_t}->{fields}}
  105         332  
1498             if ref $self->{defn_t}->{fields} eq 'ARRAY';
1499 105         472 my $data_next = $self->data_iterator($set, \@fields);
1500 105         251 my $data_prepend = $self->{defn_t}->{data_prepend};
1501              
1502             # Labels/headings
1503 105         163 my $thead = '';
1504 105 100 66     1707 if ($self->{defn_t}->{labels} && @fields) {
    100          
1505 37 100       153 $thead .= $self->start_tag('thead', $self->{defn_t}->{thead}) . "\n"
1506             if $self->{defn_t}->{thead};
1507              
1508 37 100       140 if ($self->{defn_t}->{labelgroups}) {
1509 1         5 my ($fields1, $fields2, $field1_tx_attr) = $self->labelgroup_fields;
1510 1         7 $thead .= $self->row_down(undef, 0, fields => $fields1, tx_attr_extra => $field1_tx_attr);
1511 1 50       17 $thead .= $self->row_down(undef, 0, fields => $fields2) if @$fields2;
1512             }
1513             else {
1514 36         146 $thead .= $self->row_down(undef, 0);
1515             }
1516              
1517 37 100       176 if ($self->{defn_t}->{thead}) {
1518 4         10 $thead .= $self->end_tag('thead') . "\n";
1519 4         12 $self->{defn_t}->{thead} = 0;
1520             }
1521             }
1522             elsif ($self->{defn_t}->{thead}) {
1523             # If thead set and labels isn't, use the first data row
1524 3 50 33     19 my $row = $data_prepend && @$data_prepend ? shift @$data_prepend : $data_next->();
1525 3 50       10 if ($row) {
1526 3         13 $thead .= $self->start_tag('thead', $self->{defn_t}->{thead}) . "\n";
1527 3         13 $thead .= $self->row_down($row, 1);
1528 3         9 $thead .= $self->end_tag('thead') . "\n";
1529             }
1530             }
1531              
1532             # Table body
1533 105         199 my $tbody = '';
1534 105         158 my $rownum = 1;
1535 105 100 100     336 if ($data_prepend && @$data_prepend) {
1536 3         8 for my $row (@$data_prepend) {
1537 7         19 $tbody .= $self->tbody($row, $rownum);
1538 7         20 $tbody .= $self->row_down($row, $rownum);
1539 7         16 $rownum++;
1540             }
1541             }
1542 105         258 while (my $row = $data_next->()) {
1543 393         976 $tbody .= $self->tbody($row, $rownum);
1544 393         917 $tbody .= $self->row_down($row, $rownum);
1545 393         1081 $rownum++;
1546             }
1547 105 100       535 if (my $data_append = $self->{defn_t}->{data_append}) {
1548 4         7 for my $row (@$data_append) {
1549 7         16 $tbody .= $self->tbody($row, $rownum);
1550 7         15 $tbody .= $self->row_down($row, $rownum);
1551 7         13 $rownum++;
1552             }
1553             }
1554              
1555 105 100       382 $tbody .= $self->end_tag('tbody') . "\n" if $self->{defn_t}->{tbody_open};
1556              
1557 105         182 my $tfoot = '';
1558 105 100       342 if ($self->{defn_t}->{tfoot}) {
1559 3         7 $tfoot .= $self->start_tag('tfoot', $self->{defn_t}->{tfoot}) . "\n";
1560 3         7 $tfoot .= $self->row_down('tfoot', $rownum);
1561 3         14 $tfoot .= $self->end_tag('tfoot') . "\n";
1562             }
1563              
1564 105         1070 return $thead . $tfoot . $tbody;
1565             }
1566              
1567             #
1568             # Render a single table row (style 'across')
1569             #
1570             sub row_across
1571             {
1572 8     8 0 14 my ($self, $data, $rownum, $field) = @_;
1573 8         18 my @cells = ();
1574 8         12 my @across_row = ();
1575 8         12 my $skip_count = 0;
1576              
1577             # Label/heading
1578 8 50       29 if ($self->{defn_t}->{labels}) {
1579 8         24 push @cells, $self->cell_single(field => $field, skip_count => \$skip_count);
1580 8         25 push @across_row, $self->cell_single(field => $field, tags => 0);
1581             }
1582              
1583             # Data
1584 8         18 for my $row (@$data) {
1585 24 50       51 if ($skip_count > 0) {
1586 0         0 $skip_count--;
1587 0         0 next;
1588             }
1589              
1590 24         59 push @cells, $self->cell_single(row => $row, field => $field, skip_count => \$skip_count);
1591 24         61 push @across_row, $self->cell_value($row, $field);
1592             }
1593              
1594             # Build row
1595 8         28 my $out = $self->start_tag('tr', $self->tr_attr($rownum, $data, \@across_row));
1596 8         42 $out .= join('', @cells);
1597 8         19 $out .= $self->end_tag('tr') . "\n";
1598             }
1599              
1600             sub get_dataset
1601             {
1602 2     2 0 6 my ($self, $set) = @_;
1603              
1604             # Fetch the full data set
1605 2         5 my @data = ();
1606 2 50       10 croak "invalid Tabulate data type '$set'" unless ref $set;
1607 2 50 33     37 if (ref $set eq 'CODE') {
    50 33        
    50 0        
    50          
    0          
1608 0         0 while (my $row = $set->()) {
1609 0         0 push @data, $row;
1610             }
1611             }
1612             elsif (blessed $set and $set->can('Next')) {
1613 0   0     0 my $row = eval { $set->First } || $set->Next;
1614 0 0       0 if (ref $row) {
1615 0         0 do {
1616 0         0 push @data, $row;
1617             }
1618             while ($row = $set->Next);
1619             }
1620             }
1621             elsif (blessed $set and $set->can('next')) {
1622 0   0     0 my $row = eval { $set->first } || $set->next;
1623 0 0       0 if (ref $row) {
1624 0         0 do {
1625 0         0 push @data, $row;
1626             }
1627             while ($row = $set->next);
1628             }
1629             }
1630             elsif (ref $set eq 'ARRAY') {
1631 2         6 @data = @$set;
1632             }
1633 0         0 elsif (ref $set eq 'HASH' || eval { keys %$set }) {
1634 0         0 @data = ( $set );
1635             }
1636             else {
1637 0         0 croak "[body_across] invalid Tabulate data type '$set'";
1638             }
1639              
1640 2         44 return @data;
1641             }
1642              
1643             #
1644             # Render the table body with successive records across the page
1645             # (i.e. fields down the page)
1646             #
1647             sub body_across
1648             {
1649 2     2 0 20 my ($self, $set) = @_;
1650              
1651             # Iterate over fields (instead of data rows)
1652 2         11 my @data = $self->get_dataset($set);
1653 2         5 my $rownum = 1;
1654 2         5 my $body = '';
1655 2         4 for my $field (@{$self->{defn_t}->{fields}}) {
  2         7  
1656 8         28 $body .= $self->row_across(\@data, $rownum, $field);
1657 8         23 $rownum++;
1658             }
1659              
1660 2         9 return $body;
1661             }
1662              
1663             # -------------------------------------------------------------------------
1664             sub render_table
1665             {
1666 107     107 0 181 my ($self, $set) = @_;
1667 107         201 my $defn_t = $self->{defn_t};
1668              
1669             # Style-specific bodies (default is 'down')
1670 107         148 my $body;
1671 107 100       347 if ($defn_t->{style} eq 'down') {
    50          
1672 105         440 $body .= $self->body_down($set);
1673             }
1674             elsif ($defn_t->{style} eq 'across') {
1675 2         11 $body .= $self->body_across($set);
1676             }
1677             else {
1678 0         0 croak sprintf "[render] invalid style '%s'", $defn_t->{style};
1679             }
1680              
1681             # Build table
1682 107         269 my $table = '';
1683 107         460 $table .= $self->pre_table($set);
1684 107         390 $table .= $self->start_table();
1685 107         364 $table .= $self->caption($set);
1686 107         498 $table .= $self->colgroups($set);
1687 107         223 $table .= $body;
1688 107         322 $table .= $self->end_table();
1689 107         345 $table .= $self->post_table($set);
1690            
1691 107         1244 return $table;
1692             }
1693              
1694             #
1695             # Render the data set $set using the settings in $self->{defn} + $defn,
1696             # returning the resulting string.
1697             #
1698             sub render
1699             {
1700 107     107 1 69162 my ($self, $set, $defn) = @_;
1701 107 50       395 $set = {} unless ref $set;
1702              
1703             # If $self is not a subclass of HTML::Tabulate, this is a procedural call, $self is $set
1704 107 100 33     1678 if (! ref $self || ! blessed $self || ! $self->isa('HTML::Tabulate')) {
      66        
1705 6         11 $defn = $set;
1706 6         9 $set = $self;
1707 6         33 $self = __PACKAGE__->new($defn);
1708 6         11 undef $defn;
1709             }
1710            
1711             # If $defn defined, merge with $self->{defn} for this render only
1712 107 100 66     818 if (ref $defn eq 'HASH' && keys %$defn) {
1713 90         373 $defn = $self->merge($self->{defn}, $defn);
1714 90         309 $self->prerender_munge($set, $defn);
1715             }
1716             else {
1717 17         56 $self->prerender_munge($set);
1718             }
1719              
1720 107         787 $self->render_table($set);
1721             }
1722              
1723             # -------------------------------------------------------------------------
1724              
1725             1;
1726              
1727             __END__