File Coverage

blib/lib/HTML/Tabulate.pm
Criterion Covered Total %
statement 668 763 87.5
branch 351 500 70.2
condition 188 307 61.2
subroutine 63 69 91.3
pod 10 56 17.8
total 1280 1695 75.5


line stmt bran cond sub pod time code
1             package HTML::Tabulate;
2             $HTML::Tabulate::VERSION = '0.46';
3 26     26   3146393 use 5.005;
  26         130  
4 26     26   178 use Carp;
  26         158  
  26         2087  
5 26     26   12844 use URI::Escape;
  26         54634  
  26         2223  
6 26     26   238 use Scalar::Util qw(blessed);
  26         107  
  26         1527  
7 26     26   14781 use HTML::Entities qw(encode_entities);
  26         207083  
  26         2507  
8 26     26   190 use strict;
  26         61  
  26         972  
9 26     26   143 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK $TITLE_HEADING_LEVEL);
  26         41  
  26         327686  
10              
11             require Exporter;
12             @ISA = qw(Exporter);
13             @EXPORT = qw();
14             @EXPORT_OK = qw(&render);
15              
16             $VERSION = '0.46';
17             my $DEFAULT_TEXT_FORMAT = "

%s

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

%s

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