File Coverage

blib/lib/Perl6/Form.pm
Criterion Covered Total %
statement 25 188 13.3
branch 0 92 0.0
condition 1 45 2.2
subroutine 10 43 23.2
pod n/a
total 36 368 9.7


line stmt bran cond sub pod time code
1             package Perl6::Form;
2 2     2   27887 use 5.008;
  2         4  
3              
4             our $VERSION = '0.05';
5              
6 2     2   1270 use Perl6::Export;
  2         36568  
  2         9  
7 2 0 50 2   12902 use Scalar::Util qw( readonly );
  2 0 0 2   9  
  2   0 2   444  
  2   0 0   45  
  2         8  
  2         3  
  2         155  
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
8 2     2   8 use List::Util qw( max min first );
  2         3  
  2         140  
9 2     2   8 use Carp;
  2         2  
  2         80  
10 2     2   1120 use charnames ':full';
  2         45996  
  2         425  
11              
12             my %caller_opts;
13              
14             sub fatal {
15 0     0     croak @_, "\nin call to &form";
16             }
17              
18             sub defined_or_space {
19 0 0 0 0     return " " if !defined $_[0] || length $_[0] == 0;
20 0           return $_[0];
21             }
22              
23             sub boolean {
24 0 0   0     return $_[0] ? 1 : 0;
25             }
26              
27             sub pattern {
28 0 0   0     return $_[0] if ref $_[0] eq 'Regexp';
29 0 0         return $_[0] ? qr/(?=)/ : qr/(?!)/;
30             }
31              
32             sub code {
33 0     0     my ($newval, $name ) = @_;
34 0   0       my $type = ref($newval) || "'$newval'";
35 0 0         fatal "Value for '$name' option must be code reference (not $type)"
36             unless $type eq 'CODE';
37 0           return $newval;
38             }
39              
40             my %std_one = (
41             '=' => '{=[{1}[=}',
42             '_' => '{_[{1}[_}',
43             );
44              
45             sub one_char {
46 0     0     my ($newval, undef, $opts ) = @_;
47 0 0         $newval = [ $newval ] unless ref $newval eq 'ARRAY';
48 0           for (@$newval) {
49 0 0         fatal "Value for 'single' option must be single character (not '$_')"
50             if length() != 1;
51             $opts->{field} =
52 0   0       user_def([qr/\Q$_\E/, $std_one{$_}||'{[{1}[}'], 'single', $opts);
53             }
54 0           return;
55             }
56              
57             sub layout_word {
58 0 0   0     fatal "Value for layout option must be 'across', 'down', 'balanced', ",
59             "or 'tabular\n(not '$_[0]')"
60             unless $_[0] =~ /^(across|down|balanced|tabular)$/;
61 0           return $_[0];
62             }
63              
64             sub pos_integer {
65 0 0 0 0     fatal "Value for '$_[1]' option must be positive integer (not $_[0])"
66             unless int($_[0]) eq $_[0] && $_[0] > 0;
67 0           return $_[0];
68             }
69              
70             sub strings_or_undef {
71 0     0     my ($val, $name) = @_;
72 0           my $type = ref $val;
73 0 0         if (!defined $val) { $val = [] }
  0 0          
74 0           elsif (!$type) { $val = [ "$val" ] }
75 0 0         fatal "Value for '$name' option must be string, array of strings, or undef (not \L$type\E)"
76             unless ref $val eq 'ARRAY';
77 0           return $val;
78             }
79              
80             my $unlimited = ~0>>1; # Ersatz infinity
81              
82             sub height_vals {
83 0     0     my ($vals) = @_;
84 0           my $type = ref $vals;
85 0 0 0       if (!defined $vals) { $vals = {min=>0, max=>$unlimited} }
  0 0          
    0          
    0          
86             elsif (!$type && $vals eq 'minimal')
87 0           { $vals = {min=>0, max=>$unlimited, minimal=>1} }
88 0           elsif (!$type) { $vals = {min=>$vals, max=>$vals} }
89 0   0       elsif ($type eq 'HASH') { $vals->{min}||=0;
90 0 0         defined $vals->{max} or $vals->{max}=$unlimited;
91             }
92             fatal "Values for height option must be positive integers (not $_[0])"
93             unless ref $vals eq 'HASH'
94 0 0 0       && !grep {int($vals->{$_}) ne $vals->{$_}} qw(min max);
  0            
95 0           return $vals;
96             }
97              
98             my %nothing = map {$_=>sub{""}} qw(first even odd other);
99              
100             sub std_body {
101 0     0     my ($rows, $fill, $opt) = @_;
102 0           join("", @$rows, @$fill);
103             }
104             my %std_body = (other =>\&std_body);
105              
106             my %def_page = (
107             length => $unlimited,
108             width => 78,
109             header => \%nothing, # Args: ($opts)
110             footer => \%nothing, # Args: ($opts)
111             body => \%std_body, # Args: ($body_rows, $body_len, $opts)
112             feed => \%nothing, # Args: ($opts)
113             number => undef,
114             );
115              
116             sub form_body {
117 0     0     my ($format) = @_;
118 0 0         $format = '{[{*}[}' unless defined $format;
119             return sub {
120 0     0     my ($rows, $fill, $opt) = @_;
121 0           my %form_opts = ( page=>{width => $opt->{page}{width}} );
122 0           @{$form_opts{height}}{qw(min max)} = (@$rows+@$fill) x 2
123 0 0         unless $opt->{page}{length} == $unlimited;
124 0           return form(\%form_opts, $format, $rows);
125             }
126 0           }
127              
128             sub hashify {
129 0     0     my ($what, $val, $default_undef, $default_val) = @_;
130 0 0         if (!defined $val) {
131 0           return { other => $default_undef};
132             }
133 0 0         if (!ref $val) {
134 0           return { other => $default_val->($val) };
135             }
136 0 0         if (ref $val eq 'CODE') {
137 0           return { other => $val };
138             }
139 0 0         if (ref $val eq 'HASH') {
140             fatal "Invalid key for $what: '$_'"
141 0           for grep { !/^(first|last|even|odd|other)$/ } keys %$val;
  0            
142 0           my %hash;
143 0           for (keys %$val) {
144 0 0         if (!ref $val->{$_}) {
    0          
145 0           $hash{$_} = $default_val->($val->{$_})
146             }
147             elsif (ref $val->{$_} ne 'CODE') {
148 0           fatal "Value for $what '$_' must be string or subroutine";
149             }
150             else {
151 0           $hash{$_} = $val->{$_};
152             }
153             }
154 0           return \%hash;
155             }
156 0           fatal "Value for $what must be string, subroutine, or hash";
157             }
158              
159             sub page_hash {
160 0     0     my ($h, undef, $opts) = @_;
161             fatal "Value for 'page' option must be hash reference (not $_)"
162 0           for grep $_ ne 'HASH', ref $h;
163 0           $h = { %{$opts->{page}}, %$h };
  0            
164             fatal "Unknown page sub-option ('$_')"
165 0           for grep {!exists $def_page{$_}} keys %$h;
  0            
166             fatal "Page $_ must be greater than zero"
167 0           for grep $h->{$_} <= 0, qw(length width);
168             $h->{body} =
169 0           hashify("body preprocessor", $h->{body}, \&std_body, \&form_body);
170 0           for (qw( header footer feed )) {
171 0     0     $h->{$_} = hashify($_, $h->{$_}, sub{""}, sub{my($str)=@_; sub{$str}});
  0            
  0            
  0            
  0            
172             }
173 0           return $h;
174             }
175              
176             sub filehandle {
177             fatal "Value for 'out' option must be filehandle (not '$_')"
178 0     0     for grep {$_ ne 'GLOB' } ref $_[0];
  0            
179 0           return $_[0];
180             }
181              
182             sub user_def {
183 0     0     my ($spec, $name, $opts) = @_;
184 0           my $type = ref $spec;
185 0 0 0       fatal "Value of 'field' option must be an array of pairs or a hash (not ",
186             $type||"'$spec'", ")"
187             unless $type =~ /^(ARRAY|HASH)$/;
188 0 0         if ($type eq 'ARRAY') {
189 0 0         fatal "Missing value for last user-defined field ('$spec->[-1]')"
190             if @$spec % 2;
191             }
192             else {
193 0           $spec = [%$spec];
194             }
195 0   0       my @from = @{$opts->{field}{from}||=[]};
  0            
196 0   0       my @to = @{$opts->{field}{to}||=[]};
  0            
197 0           my $count = @from;
198 0           for (my $i=0; $i<@$spec; $i+=2, $count++) {
199 0           my ($pat, $fld) = @{$spec}[$i,$i+1];
  0            
200 0           push @from, "$pat(?{$count})";
201 0 0   0     push @to, (ref $fld eq 'CODE' ? $fld : sub{$fld});
  0            
202             }
203 0           return {from=>\@from, to=>\@to};
204             }
205              
206             my %std_opt = (
207             out => { set => \&filehandle, def => \*STDOUT, },
208             ws => { set => \&pattern, def => undef, },
209             fill => { set => \&defined_or_space, def => " ", },
210             lfill => { set => \&defined_or_space, def => undef, },
211             rfill => { set => \&defined_or_space, def => undef, },
212             hfill => { set => \&defined_or_space, def => undef, },
213             tfill => { set => \&defined_or_space, def => undef, },
214             bfill => { set => \&defined_or_space, def => undef, },
215             vfill => { set => \&defined_or_space, def => undef, },
216             single => { set => \&one_char, def => undef, },
217             field => { set => \&user_def, def => {from=>[],to=>[]} },
218             bullet => { set => \&strings_or_undef, def => [] },
219             height => { set => \&height_vals, def => {min=>0, max=>$unlimited} },
220             layout => { set => \&layout_word, def => 'balanced', },
221             break => { set => \&code, def => break_at('-'), },
222             page => { set => \&page_hash, def => {%def_page}, },
223             under => { set => sub {"$_[0]"}, def => undef },
224             interleave => { set => \&boolean, def => 0 },
225             untrimmed => { set => \&boolean, def => 0, },
226             locale => { set => \&boolean, def => 0, },
227             );
228              
229             my %def_opts = map {$_=>$std_opt{$_}{def}} keys %std_opt;
230              
231             sub get_locale_vals { # args: $dec_mark, $thou_sep, $thou_group
232 2     2   4780 use POSIX;
  2         9328  
  2         7  
233 0     0     $lconv = POSIX::localeconv();
234 0 0         $_[0] = exists $lconv->{decimal_point} ? $lconv->{decimal_point} : "?";
235 0 0         $_[1] = exists $lconv->{thousands_sep} ? $lconv->{thousands_sep} : "";
236 0 0         $_[2] = exists $lconv->{grouping} ? [unpack "c*", $lconv->{grouping}] : [0];
237             }
238              
239             my %std_literal = (
240             break => \&break_lit,
241             literal => 1,
242             vjust => \&jverlit,
243             hjust => \&jhorlit,
244             );
245              
246             sub update(\%\%;$) {
247 0     0     my ($old, $new, $croak) = @_;
248 0           my @bad;
249 0           for my $opt (keys %$new) {
250 0           my $std = $std_opt{$opt};
251 0 0 0       push @bad, "Unknown option: $opt=>'$new->{$opt}" and next unless $std;
252 0           $old->{$opt} = $std->{set}->($new->{$opt}, $opt, $old);
253             }
254 0 0 0       if (@bad && $croak) { croak join "\n", @bad }
  0 0          
255 0           elsif (@bad) { fatal join "\n", @bad }
256             }
257              
258              
259              
260             # Horizontal justifiers
261              
262             sub fillpat {
263 0     0     my ($pos, $fill, $len) = @_;
264 0 0         return "" if $len < 0;
265 0           return substr($fill x (($pos+$len)/length($fill)+1), $pos, $len);
266             }
267              
268       0     sub jhorlit {} # literals don't need any justification
269              
270             sub jverbatim {
271 0     0     jleft(@_, precropped=>1);
272             }
273              
274             sub jleft {
275 0     0     my (undef, %val) = @_;
276 0 0         $_[0] =~ s/^\s+// unless $val{precropped};
277 0           my $len = length $_[0];
278 0           $_[0] .= fillpat($val{pos}+$len, $val{post}, $val{width}-$len);
279 0 0         substr($_[0],$val{width}) = "" unless $val{stretch};
280             }
281              
282             sub jright {
283 0     0     my (undef, %val) = @_;
284 0 0         $_[0] =~ s/\s+$// unless $val{precropped};
285 0           $_[0] = fillpat($val{pos}, $val{pre}, $val{width}-length($_[0])) . $_[0];
286 0 0         substr($_[0],0,-$val{width}) = "" unless $val{stretch};
287             }
288              
289             sub jcentre {
290 0     0     my (undef, %val) = @_;
291 0           $_[0] =~ s/^\s+|\s+$//g;
292 0           $val{precropped} = 1;
293 0           my $indent = int( ($val{width}-length $_[0])/2 );
294 0           jleft($_[0], %val, stretch=>0, pos=>$val{pos}+$indent, width=>$val{width}-$indent);
295 0           jright($_[0], %val);
296             }
297              
298             sub jfull {
299 0     0     my ($str, %val) = @_;
300 0           my $rem = $val{width};
301 0           $str =~ s/^\s+|\s+$//g;
302 0 0         unless ($val{last}) {
303 0           my $rem = $val{width}-length($str);
304 0           $str = reverse $str;
305 0 0 0       1 while $rem>0 && $str =~ s/( +)/($rem-->0?" ":"").$1/ge;
  0            
306 0           $_[0] = reverse $str;
307             }
308 0           &jleft;
309             }
310              
311             sub jsingle {
312 0     0     my (undef, %val) = @_;
313 0 0         $_[0] = length $_[0] ? substr($_[0],0,1) : fillpat($val{pos}, $val{pre},1);
314             }
315              
316             sub jfatal {
317 0     0     die "Internal error in &form."
318             }
319              
320             sub joverflow (\%\%) {
321 0     0     $_[0]{overflow} = 1;
322 0           %{$_[1]} = ();
  0            
323 0           return \&jfatal;
324             }
325              
326             sub jbullet {
327 0     0     return ($_[0],1);
328             }
329              
330 2     2   6489 use Data::Dump 'ddx';
  0            
  0            
331             sub jnum {
332             my ($fld,$precurr,$incurr,$postcurr,$width,$opts,$setplaces,$checkplaces)
333             = @_;
334             my $orig = $fld;
335             $incurr ||= qr/(?!)/;
336              
337             my $comma_count = $fld=~tr/,//;
338             my $period_count = $fld=~tr/.//;
339             my $apost_count = $fld=~tr/ '//;
340             my $integral = $comma_count > 1 && !($period_count || $apost_count)
341             || $period_count > 1 && !($comma_count || $apost_count)
342             || $apost_count > 1 && !($comma_count || $period_count);
343             my ($whole, $point, $places) =
344             $integral ? ($fld, "", "")
345             : ($fld =~ /^([]{>,.' 0]*)([.,]|\Q$incurr\E)([[}<0]*)/g);
346              
347             my $missing = $width-length($fld);
348             if ($missing>0) { $fld = substr($fld,0,1) x $missing . $fld }
349              
350             $opts->{lfill} = '0' if $whole =~ m/^0+/;
351             $opts->{rfill} = '0' if $places =~ m/0+$/;
352             my $comma = $whole =~ /([,.' ])/ ? $1 : '';
353             my $grouping;
354             if ($comma) {
355             $grouping = $whole =~ /,(?:\]{2},\]{3}|>{2},>{3})\z/ ? [3,2] # Subcont
356             : $whole =~ /[,.' ](\]+|>+)\z/ ? [length($1)]
357             : undef;
358             }
359             if (defined $setplaces) {
360             $places = $setplaces;
361             $whole = $width - length($point) - $setplaces;
362             }
363             else {
364             $_ = length for $whole, $places;
365             }
366             fatal "Inconsistent number of decimal places in numeric field.\n",
367             "Specified as $checkplaces but found $places"
368             if $checkplaces && $places != $checkplaces;
369             my $huh = substr( ('?'x$whole).$point.('?'x$places), 0, $width);
370             my $duh = substr( ('#'x$whole).$point.('#'x$places), 0, $width);
371              
372             $places -= length($postcurr);
373              
374             get_locale_vals($point, $comma, $grouping) if $opts->{locale};
375              
376             return sub {
377             my ($orig, %val) = @_;
378             $_[0] = " "x$val{width} and return if $orig =~ /^\s*$/;
379             $orig =~ s/,|\Q$incurr\E/./ if $point ne '.';
380             my ($pre,$post) = ($precurr,$postcurr);
381             if ($orig !~ /^\s*-/ || $orig == -$orig) {
382             $pre =~ s/^[(-]|[(-]$/ /g;
383             $post =~ s/^[)-]|[)-]$/ /g;
384             }
385             my ($fail, $str);
386             if ($integral) {
387             local $SIG{__WARN__} = sub { $fail = 1 };
388             $str = sprintf('%*d',$val{width},int($orig));
389             }
390             else {
391             local $SIG{__WARN__} = sub { $fail = 1 };
392             $str = sprintf('%*.*f',$val{width},$places,$orig);
393             }
394             if ($fail) {
395             $_[0] = $huh;
396             }
397             else {
398             my ($w,$p) = ($str =~ /^\s*(.*)\.(.*)$/g); # floating point
399             ($w,$p) = ($str =~ /^\s*(.*)$/,"") if !$w; # integer
400             if ($grouping) {
401             my @groups = @$grouping;
402             my $group = shift @groups;
403             if ($group) {
404             $w =~ s/(\d)(\d{$group})\z/$1$comma$2/;
405             do {
406             $group = shift @groups if @groups;
407             } while $group && $w =~ s/(?
408             }
409             }
410             if (length($w) > $width || !$val{stretch} && ($w ? length($w) : 0)+length($pre) > $whole) {
411             $_[0] = $duh;
412             }
413             else {
414             $str = $integral ? $w : $w.q(.).$p;
415             $str =~ s/(\.\d+?)(0+)$/$1/
416             unless $orig =~ /\.\d{$places,}[1-9]/;
417             if ($integral && $str < 0) {
418             if ($pre =~ /[(]/ || $post =~ /[)]/) {
419             $str =~ s/-//;
420             }
421             else {
422             s/-/ / for $pre, $post;
423             }
424             }
425             $str =~ s/^(?:\Q$pre\E)?/$pre/;
426             if ($val{pre} =~ /^0+$/) {
427             $str =~ s{^((\D*)(\d.*))\.}
428             {$2 . ("0" x ($whole-length $1)) . "$3."}e;
429             $val{pre} = " ";
430             }
431             my $postlen = length($post);
432             if (!$integral) {
433             $str =~ s/^(.*)\./$1$point/;
434             my $width = $val{width}-$whole+length($1);
435             jleft($str, %val, width=>$width, precropped=>1);
436             jright($str, %val, precropped=>1);
437             }
438             if ($integral) {
439             $str = substr((q{ } x $width) . $str . $post, -$width);
440             $str =~ s/(?:[ ]{$postlen}([ ]*))$/$post$+/;
441             }
442             elsif ($postlen) {
443             $str =~ s/(?:[ ]{$postlen}([ ]*)|.{$postlen}())$/$post$+/;
444             }
445             $_[0] = $str;
446             }
447             }
448             }
449             }
450              
451              
452             # Vertical justifiers
453              
454             sub jverlit {
455             my ($height, $above, $below, $column) = @_;
456             push @$column, ($column->[0]||"") while @$column < $height;
457             }
458              
459             sub jmiddle {
460             my ($height, $above, $below, $column) = @_;
461             my $add = int(($height-@$column)/2);
462             splice @$column, 0, 0, ($above)x$add;
463             $add = $height-@$column;
464             push @$column, ($below)x$add;
465             }
466              
467             sub jbottom {
468             my ($height, $above, $below, $column) = @_;
469             my $pre = $height-@$column;
470             splice @$column, 0, 0, ($above)x$pre;
471             }
472              
473             sub jtop {
474             my ($height, $above, $below, $column) = @_;
475             my $post = $height-@$column;
476             push @$column, ($below)x$post;
477             }
478              
479              
480             my $precurrpat = qr/^(\{) ([^]0>[<,']+?) ([]>,'0])/x;
481             my $incurrpat = qr/([]>0]) ([^]0>[<,'. ]+?) ([[<0]) /x;
482             my $postcurrpat = qr/([[<>0]) ([^]0>[<]+) (\}$) /x;
483              
484             sub perl6_match {
485             my ($str, $pat) = @_;
486             use re 'eval';
487             if (my @vals = $str =~ /$pat/) {
488             unshift @vals, $&;
489             bless \@vals, 'Perl6::Form::Rule::Okay';
490             }
491             else {
492             bless [], 'Perl6::Form::Rule::Fail';
493             }
494             }
495              
496             my $litval;
497             sub litval {
498             ($litval) = @_ if @_;
499             return $litval;
500             }
501              
502             my ($fld, $udnum);
503             sub fldvals {
504             ($fld, $udnum) = @_ if @_;
505             return ($fld, $udnum);
506             }
507              
508             our $nestedbraces = qr/ \{ (?: (?> ((?!\{|\}).)+ ) | (??{ $nestedbraces }) )* \} /sx;
509              
510             sub segment ($\@\%$\%) {
511             my ($format, $args, $opts, $fldcnt, $argcache) = @_;
512             my $width =
513             defined $opts->{page}{width} ? $opts->{page}{width} : length($format);
514             my $userdef = join("|", @{$opts->{field}{from}}) || qr/(?!)/;
515             my $bullet = join("|", map quotemeta, @{$opts->{bullet}}) || qr/(?!)/;
516             use re 'eval';
517             my @format;
518             while ($format =~ /\G ((?>(?:\\.|(?!$userdef|$bullet|\{).)*))
519             (?{litval($^N)})
520             (?: ($userdef) (?{fldvals($^N,$^R)})
521             | ($bullet) (?{fldvals($^N,-1)})
522             | ($nestedbraces) (?{fldvals($^N,undef)})
523             )
524             /gcsx) {
525             push @format, litval(), fldvals();
526             }
527             push @format, substr ($format, pos($format)||0);
528             my $args_req = int(@format/3);
529             my (@formatters,@starred,@vstarred);
530             for my $i (0..$args_req) {
531             my ($literal,$field,$userdef) = @format[3*$i..3*$i+2];
532             $literal =~ s/\\\{/{/g;
533             push @formatters, { %std_literal,
534             width => length($literal),
535             src => \$literal,
536             };
537             $width -= length($literal);
538             if (defined $field) {
539             my %form;
540             my %fldopts = %$opts;
541             $fldcnt++;
542             my ($setwidth, $setplaces, $checkwidth, $checkplaces);
543             if (defined $userdef) {
544             if ($userdef < 0) {
545             $form{isbullet} = \"$field";
546             }
547             else {
548             my ($from,$to) =
549             map $_->[$userdef], @{$opts->{field}}{'from','to'};
550             $field = $to->(perl6_match($field,$from),\%fldopts);
551             }
552             }
553             my $fld = $field;
554             my ($precurr, $incurr, $postcurr) = ("")x3;
555             $form{width} = length $field;
556             if ($form{isbullet}) {
557             $form{vjust} = \&jtop;
558             $form{hjust} = \&jbullet;
559             $form{break} = \&break_bullet;
560             $form{src} = [];
561             ($form{bullethole} = $field) =~ s/./ /gs;
562             }
563             else {
564             $form{stretch} = !$form{isbullet} && $fld =~ s/[+]//;
565             @form{qw(verbatim break hjust)}
566             = (1, \&break_verbatim, \&jverbatim)
567             if $fld =~ /["']/ && $fld !~ /[][><]/;
568             # was: if $fld =~ /["']/ && $fld !~ /[][]/;
569             $form{trackpos} = $fld =~ s/(\{):|:(\})/$+/g;
570             $form{vjust} = $fld =~ s/=//g ? \&jmiddle
571             : $fld =~ s/_//g ? \&jbottom
572             : \&jtop
573             ;
574              
575             ($checkwidth, $extras) = $fld =~ m/\(\s*(\d+[.,]?\d*)\s*\)/g;
576             fatal "Too many width specifications in $field" if $extras;
577             if ($checkwidth) {
578             $checkplaces = $checkwidth =~ s/[.,](\d+)// && $1;
579             for ($fld) {
580             s{([][>
581             { $1 . ($1 x length $2) }xe and last;
582             s{(\(\s*\d+[.,]?\d*\s*\)) ([][>
583             { ($2 x length $1) . $2 }xe and last;
584             s{(> [.,]) (\(\s*\d+[.,]?\d*\s*\))}
585             { $1 . ('<' x length $2) }xe and last;
586             s{(\(\s*\d+[.,]?\d*\s*\)) ([.,] <)}
587             { ('>' x length $1) . $2 }xe and last;
588             s{(\(\s*\d+[.,]?\d*\s*\)) ([.,] \[)}
589             { (']' x length $1) . $2 }xe and last;
590             s{(\(\s*\d+[.,]?\d*\s*\))}
591             { '[' x length $1 }xe and last;
592             }
593             }
594              
595             ($setwidth, $extras) = $fld =~ m/\{\s*(\d+[.,]?\d*|\*)\s*\}/g
596             and $fld =~ s/\{\s*(\d+[.,]?\d*|\*)\s*\}//;
597             fatal "Too many width specifications in $field"
598             if $extras || $setwidth && $checkwidth;
599             if ($setwidth && $setwidth =~ s/[.,](\d+)//) {
600             $setplaces = $1 || 0;
601             }
602              
603             for ([$checkwidth, $checkplaces], [$setwidth, $setplaces]) {
604             fatal "Can't fit $_->[1] decimal place",($_->[1]!=1?'s':''),
605             " in a $_->[0]-character field"
606             if defined($_->[0]) && defined($_->[1])
607             && $_->[0] ne '*'
608             && $_->[0] <= $_->[1];
609             }
610              
611             $precurr =
612             $fld =~ s/$precurrpat/$1.($3 x length $2).$3/e ? "$2" : "";
613             $incurr =
614             $fld =~ m/$incurrpat/ ? "$2" : "";
615             $postcurr =
616             $fld =~ s/$postcurrpat/$1.($1 x length $2).$3/e ? "$2" : "";
617              
618             if ($form{width} == 2) {
619             $fld = '[[';
620             }
621             elsif ($form{width} == 3) {
622             $fld =~ s/^ \{ ([.,]) \} $/].[/x;
623             $fld =~ s/^ \{ (.) \} $/$+$+$+/x;
624             }
625             elsif ($form{width} > 3) {
626             $fld =~ s/^ \{ ([]>,]+ ([]>])) \} $/$2$2$1/x; # Integral comma'd field
627             $fld =~ s/^ \{ ([.,] \[) /]$1/x;
628             $fld =~ s/^ \{ ([.,] \<) />$1/x;
629             $fld =~ s/(\] .* [.,]) \} $/$1\[/x;
630             $fld =~ s/(\> .* [.,]) \} $/$1
631             $fld =~ s/^ \{ (.) | (.) \} $/$+$+/gx;
632             }
633              
634             $form{width} = $setwidth
635             if defined $setwidth && $setwidth ne '*';
636              
637             if ($form{width} == 2) {
638             $fld = substr($fld,0,1) x 2;
639             }
640             elsif ($form{width} == 3) {
641             $fld =~ s/^ \{ ([.,]) \} $/].[/x;
642             $fld =~ s/^ \{ (.) \} $/$+$+$+/x;
643             }
644             elsif ($form{width} > 3) {
645             $fld =~ s/^ \{ ([.,] \[) /]$1/x;
646             $fld =~ s/^ \{ ([.,] \<) />$1/x;
647             $fld =~ s/(\] .* [.,]) \} $/$1\[/x;
648             $fld =~ s/(\> .* [.,]) \} $/$1
649             $fld =~ s/^ \{ (.) | (.) \} $/$+$+/gx;
650             }
651              
652             $form{width} = $setwidth
653             if defined $setwidth && $setwidth ne '*';
654             }
655              
656             if ($setwidth && $setwidth eq '*') {
657             push @{$form{verbatim} ? \@vstarred : \@starred}, \%form;
658             }
659             else {
660             $width -= $form{width}
661             }
662              
663             $form{line} = 1 unless $form{isbullet} || $fld =~ /[][IV"]/;
664              
665             $form{hjust} ||= $form{width} == 1 ? \&jsingle
666             : ($fld =~ /^(?:<+|\[+)$/) ? \&jleft
667             : ($fld =~ /^(?:>+|\]+)$/) ? \&jright
668             : ($fld =~ /^(?:I+|\|+|>+<+|\]+\[+)$/)? \&jcentre
669             : ($fld =~ /^(?:<+>+|\[+\]+)$/) ? \&jfull
670             : ($fld =~ /^(?:V+)$/) ? joverflow(%form, %fldopts)
671             : ($fld =~ /^(?: [>,' 0]* \. [<0]*
672             | [],' 0]* \. [[0]*
673             | [>.' 0]* \, [<0]*
674             | [].' 0]* \, [[0]*
675             | [>.,' 0]* \Q$incurr\E [<0]*
676             | [].,' 0]* \Q$incurr\E [[0]*
677             | [].' 0]* \, [[0]*
678             )$/x) ? do {
679             $form{break}=\&break_nl;
680             jnum($fld,$precurr,$incurr,$postcurr,
681             $form{width},\%fldopts,
682             $setplaces, $checkplaces)
683             }
684             : fatal "Field $fldcnt is of unknown type: $field"
685             ;
686              
687             $form{break}=\&break_nl if $form{stretch};
688              
689             fatal "Inconsistent width for field $fldcnt.\n",
690             "Specified as '$field' but actual width is $form{width}"
691             if defined $checkwidth && $form{width} != $checkwidth;
692              
693             splice @$args, $i, 0, "" if $form{isbullet}; # BEFORE ANY OPTIONS
694              
695             while (ref $args->[$i] eq 'HASH') {
696             update %fldopts, %{splice @$args, $i, 1};
697             }
698             $form{opts} = \%fldopts;
699              
700             splice @$args, $i, 0, "" if $form{overflow}; # AFTER ANY OPTIONS
701              
702             fatal "Missing data value for field ", $i, " ($field)"
703             unless defined $args->[$i];
704              
705             for ($args->[$i]) {
706             next if $form{isbullet};
707             $form{src} ||=
708             ref eq 'ARRAY' ? do {
709             my $s = join "", map { my $val = $_; $val =~ s/\n(?!\z)/\r/g; $val }
710             map {!defined() ? "\n"
711             : /\n\z/ ? $_
712             : "$_\n"} @$_;
713             $form{trackpos} ? ($argcache->{$_} ||= \$s) : \$s;
714             }
715             : (readonly $_ || !$form{trackpos}) ? \(my$s=$_)
716             : \$_;
717             }
718              
719             $form{break} ||= $fldopts{break} || $opts->{break};
720              
721             push @formatters, \%form;
722             }
723             }
724             splice @$args, 0, $args_req;
725             $_[-1] = $fldcnt; # remember field count
726             # Distribute {*} widths...
727             for my $f (@vstarred) {
728             $f->{maxwidth} = max 0, map {length} split "\n", ${$f->{src}};
729             }
730             if (@starred||@vstarred) {
731             my $fldwidth = int($width/(@starred+@vstarred));
732             for my $f (@vstarred) {
733             $f->{width} = @starred ? $f->{maxwidth}
734             : min $fldwidth, $f->{maxwidth};
735             $width += $fldwidth - $f->{width};
736             }
737             $fldwidth = int($width/(@starred+@vstarred)) if @starred;
738             $_->{width} = $fldwidth for @starred;
739             }
740              
741             # Attach bullets to neighbouring fields,
742             # and compute offsets from left margin...
743             my $offset = 0;
744             my $lastbullet;
745             for my $f (@formatters) {
746             $f->{pos} = $offset;
747             $offset += $f->{width};
748             if ($lastbullet) {
749             if ($f->{literal}) { # IGNORE IT
750             }
751             elsif ($f->{isbullet}) {
752             my $literal = ${$lastbullet->{isbullet}};
753             %$lastbullet = (%std_literal, width=>length($literal), src=>\$literal);
754             $lastbullet = undef;
755             }
756             else {
757             $f->{hasbullet} = $lastbullet;
758             $lastbullet = undef;
759             }
760             }
761             $lastbullet = $f if $f->{isbullet};
762             }
763             if ($lastbullet) {
764             my $literal = ${$lastbullet->{isbullet}};
765             %$lastbullet = (%std_literal, width=>length($literal), src=>\$literal);
766             }
767              
768             return \@formatters;
769             }
770              
771             sub layout_groups {
772             my @groups;
773             my $i = 0;
774             FORMATTER: for my $f (@_) {
775             $f->{index} = $i++;
776             for my $group (@groups) {
777             if ($f->{src} == $group->[0]{src}) {
778             push @$group, $f;
779             next FORMATTER;
780             }
781             }
782             push @groups, [$f];
783             }
784             return @groups;
785             }
786              
787             sub make_col {
788             my ($f, $opts, $maxheight, $tabular) = @_;
789             $maxheight = min $unlimited,
790             grep defined(), $maxheight, $f->{opts}{height}{max};
791             my ($str_ref, $width) = @{$f}{qw(src width)};
792             my @col;
793             my ($more, $text) = (1,"");
794             my $bullet = $f->{hasbullet};
795             $bullet->{bullets} = [] if $bullet;
796             my $bulleted = 1;
797             until ($f->{done}) {
798             my $skipped = 0;
799             unless ($f->{isbullet} || $f->{width} == 1 || $f->{verbatim}) {
800             ($skipped) = ($$str_ref =~ /\G(\s*)/gc);
801             if ($skipped||=0) {
802             $bulleted = ($skipped =~ /\n/);
803             $skipped=~s/\r\Z//;
804             $skipped = ($skipped=~tr/\r//);
805             push @col, ("") x $skipped;
806             last if $tabular && $bulleted && @col;
807             }
808             }
809             ($text,$more,$eol) = $f->{break}->($str_ref,$width,$f->{opts}{ws});
810             if ($f->{opts}{ws}) {
811             $text =~ s{($f->{opts}{ws})}
812             { @caps = grep { defined $$_ } 2..$#+;
813             @caps = length($1) ? " " : "" unless @caps;
814             join "", @caps;
815             }ge;
816             }
817             $text .= "\r" if $eol;
818             push @col, $text;
819             if ($bullet && $text =~ /\S/) {
820             push @{$bullet->{bullets}}, ($bullet->{bullethole}) x $skipped;
821             push @{$bullet->{bullets}}, $bulleted ? ${$bullet->{isbullet}}
822             : $bullet->{bullethole};
823             }
824             $f->{done} = 1
825             if defined $f->{opts}{height}{max} && @col==$f->{opts}{height}{max};
826             last if !$more || @col==$maxheight;
827             $f->{done} = 1 if $f->{line};
828             $bulleted = 0;
829             }
830             @col = () if @col == 1 && $col[0] eq "";
831             $_[3] = $more && !$f->{done} if @_>3;
832             return \@col;
833             }
834              
835             my $count = 0;
836              
837             sub balance_cols {
838             my ($group, $opts, $maxheight) = @_;
839             my ($first, $src) = ($group->[0], $group->[0]{src});
840             if (@$group<=1) {
841             $first->{formcol} = make_col($first,$opts,$maxheight);
842             return;
843             }
844             my $pos = pos($$src) || 0;
845             my $minheight = 0;
846             while (1) {
847             my @cols;
848             pos($$src) = $pos;
849             my $medheight = int(($maxheight+$minheight+1)/2);
850             for my $f (@$group) {
851             $f->{done} = 0;
852             push @cols, make_col($f,$opts,$medheight)
853             }
854             if ($maxheight <= $minheight+1) {
855             for (0..$#cols) {
856             $group->[$_]{formcol} = $cols[$_];
857             }
858             return;
859             }
860             (substr($$src,pos$$src) =~ /\S/) ? $minheight : $maxheight = $medheight;
861             }
862             }
863              
864             sub delineate_overflows {
865             for my $formats (@_) {
866             # Is there a block field on the line?
867             next if grep { !( $_->{line}
868             || $_->{literal}
869             || $_->{notlastoverflow}
870             )
871             } @$formats;
872             for (@$formats) {
873             next unless $_->{overflow};
874             if ($_->{notlastoverflow}) {
875             $_->{line} = 1;
876             }
877             }
878             }
879             for my $formats (@_) {
880             for (@$formats) {
881             next if !$_->{overflow} || $_->{notlastoverflow};
882             $_->{opts}{height}{max} = $unlimited;
883             $_->{opts}{height}{minimal} = 0;
884             }
885             }
886             }
887              
888             sub resolve_overflows {
889             my ($formatters,$prevformatters) = @_;
890             FORMATTER: for my $fld (@$formatters) {
891             next unless $fld->{overflow};
892             my $left = $fld->{pos};
893             my $right = $left + $fld->{width} - 1;
894             my $overflowed;
895             for my $prev (@$prevformatters) {
896             next if $prev->{literal};
897             my $prevleft = $prev->{pos};
898             my $prevright = $prevleft + $prev->{width} - 1;
899             if ($right >= $prevleft && $left <= $prevright) { # overlap
900             if ($overflowed) {
901             $prev->{notlastoverflow} = 1
902             if $prev->{overflow} && $prev->{src} == $fld->{src};
903             next;
904             }
905             my %newfld = ( %$prev, opts=>{}, overflow=>1 );
906             my @keep = qw( width pos complete done line );
907             @newfld{@keep} = @{$fld}{@keep};
908             update %{$newfld{opts}}, %{$fld->{opts}};
909             $newfld{opts}{height} = {min=>0, max=>undef, minimal=>1};
910             $fld = \%newfld;
911             $prev->{notlastoverflow} = 1 if $prev->{overflow};
912             $overflowed = 1;
913             }
914             }
915             croak "Useless overflow field (no field above it)"
916             unless $overflowed;
917             }
918             }
919              
920             sub make_cols($$\@\%$) {
921             my ($formatters,$prevformatters,$parts, $opts, $maxheight) = @_;
922             my (@bullets, @max, @min);
923             for my $f (@$formatters) {
924             if ($f->{isbullet}) { push @bullets, $f }
925             elsif ($f->{opts}{height}{minimal}) { push @min, $f }
926             else { push @max, $f }
927             }
928             my @maxgroups = layout_groups(@max);
929             my @mingroups = layout_groups(@min);
930             my $has_nonminimal = grep {!$_->{literal} && !$_->{line}} @max;
931             if ($opts->{layout} eq 'balanced') { # balanced column-by-column
932             for my $g (@maxgroups) {
933             balance_cols($g,$opts, $maxheight);
934             }
935             $maxheight = map 0+@{$_->{formcol}||[]}, @$formatters
936             if grep {!$_->{literal} && !$_->{opts}{height}{minimal}} @$formatters;
937             for my $g (@mingroups) {
938             balance_cols($g, $opts, $maxheight);
939             }
940             for my $f (@$formatters) {
941             push @$parts, $f->{formcol}||$f->{bullets}||[];
942             }
943             }
944             elsif ($opts->{layout} eq 'down') { # column-by-column
945             for my $col (0..$#$formatters) {
946             my $f = $formatters->[$col];
947             next if $f->{isbullet} || $f->{opts}{height}{minimal};
948             $parts->[$col] = make_col($f,$opts, $maxheight);
949             }
950             $maxheight = min $maxheight,
951             max map { defined() ? scalar @$_ : 0 } @$parts
952             if $has_nonminimal;
953             for my $col (0..$#$formatters) {
954             my $f = $formatters->[$col];
955             next if $f->{isbullet} || !$f->{opts}{height}{minimal};
956             $parts->[$col] = make_col($f,$opts, $maxheight);
957             }
958             for my $col (0..$#$formatters) {
959             my $f = $formatters->[$col];
960             next unless $f->{isbullet};
961             $parts->[$col] = $f->{bullets}||[];
962             }
963             }
964             elsif ($opts->{layout} eq 'across') { # across row-by-row
965             my %incomplete = (first=>1);
966             for (my $row=0;$row<$maxheight && grep {$_} values %incomplete;$row++) {
967             %incomplete = ();
968             for my $col (0..$#$formatters) {
969             $parts->[$col] ||= [];
970             }
971             for my $col (0..$#$formatters) {
972             my $f = $formatters->[$col];
973             next if $f->{isbullet} || $f->{opts}{height}{minimal};
974             next if $f->{line} && $row>0 || $f->{done};
975             my ($str_ref, $width) = @{$f}{qw(src width)};
976             $$str_ref =~ /\G\s+/gc unless $f->{verbatim};
977             ($parts->[$col][$row], my $more) =
978             $f->{break}->($str_ref,$width,$f->{opts}{ws});
979             $parts->[$col][$row] =~ s/$f->{opts}{ws}/ /g if $f->{opts}{ws};
980             $f->{done} = 1 if !$f->{literal}
981             && $row+1 >= ($f->{opts}{height}{max}||$maxheight);
982             $incomplete{$str_ref} = $more
983             unless $f->{literal} || $f->{line} || $f->{done};
984             }
985             for my $col (0..$#$formatters) {
986             my $f = $formatters->[$col];
987             next if $f->{isbullet} || !$f->{opts}{height}{minimal};
988             next if $f->{line} && $row>0 || $f->{done};
989             my ($str_ref, $width) = @{$f}{qw(src width)};
990             $$str_ref =~ /\G\s+/gc unless $f->{verbatim};
991             ($parts->[$col][$row], my $more) =
992             $f->{break}->($str_ref,$width,$f->{opts}{ws});
993             $parts->[$col][$row] =~ s/$f->{opts}{ws}/ /g if $f->{opts}{ws};
994             $f->{done} = 1 if !$f->{literal}
995             && $row+1 >= ($f->{opts}{height}{max}||$maxheight);
996             $incomplete{$str_ref} = $more
997             unless $has_nonminimal || $f->{done};
998             }
999             for my $col (0..$#$formatters) {
1000             my $f = $formatters->[$col];
1001             next unless $f->{isbullet};
1002             $parts->[$col][$row] = shift @{$f->{bullets}};
1003             }
1004             }
1005             }
1006             else { # tabular layout: down to the first \n, then across, then fill
1007             my $finished = 0;
1008             for my $col (0..$#$formatters) { $parts->[$col] = []; }
1009             while (!$finished) {
1010             $finished = 1;
1011             for my $col (0..$#$formatters) {
1012             my $tabular_more = 1;
1013             my $f = $formatters->[$col];
1014             next if $f->{isbullet} || $f->{opts}{height}{minimal};
1015             push @{$parts->[$col]},
1016             @{make_col($f,$opts, $maxheight, $tabular_more)};
1017             $finished &&= !$tabular_more;
1018             }
1019             my $minimaxheight = min $maxheight,
1020             max map { defined() ? scalar @$_ : 0 } @$parts
1021             if $has_nonminimal;
1022             for my $col (0..$#$formatters) {
1023             my $tabular = 1;
1024             my $f = $formatters->[$col];
1025             next if $f->{isbullet} || !$f->{opts}{height}{minimal};
1026             push @{$parts->[$col]},
1027             @{make_col($f,$opts, $maxheight, $tabular)};
1028             }
1029             for my $col (0..$#$formatters-1) {
1030             my $f = $formatters->[$col];
1031             if ($f->{isbullet}) {
1032             push @{$parts->[$col]}, @{$f->{bullets}||[]};
1033             push @{$parts->[$col]},
1034             ($f->{bullethole})x($minimaxheight-@{$parts->[$col]});
1035             }
1036             elsif ($f->{literal}) {
1037             push @{$parts->[$col]},
1038             (${$f->{src}})x($minimaxheight-@{$parts->[$col]});
1039             }
1040             else {
1041             push @{$parts->[$col]},
1042             ("")x($minimaxheight-@{$parts->[$col]});
1043             }
1044             }
1045             $maxheight -= $minimaxheight||0;
1046             }
1047             }
1048             for my $g (@maxgroups, @mingroups) {
1049             my $text = $g->[-1]{src};
1050             next if substr($$text,pos($$text)||0) =~ /\S/;
1051             for (1..@$g) {
1052             next unless @{$parts->[$g->[-$_]{index}]};
1053             $g->[-$_]{final} = 1;
1054             last;
1055             }
1056             }
1057             for my $i (1..@$parts) {
1058             $formatters->[-$i]{complete} = 0
1059             }
1060             for my $f (grep {!($_->{literal}||$_->{line})} @$formatters) {
1061             next if $f->{done} || $f->{isbullet} || $f->{opts}{height}{minimal};
1062             return 1 if substr(${$f->{src}},pos(${$f->{src}})||0) =~ /\S/;
1063             }
1064             return 0;
1065             }
1066              
1067             sub make_underline {
1068             my ($under, $prevline, $nextline) = @_;
1069             $under =~ s/(\n*)\z//;
1070             my $trail = "$1"|"\n";
1071             for my $l ($nextline, $prevline) {
1072             $l = join "", map {$_->{literal} ? ${$_->{src}} : '*'x$_->{width} } @$l;
1073             $l =~ s{(.)}{$1 =~ /\s/ ? "\0" : "\1"}ges;
1074             }
1075             $nextline |= $prevline;
1076             $nextline =~ s{\0}{ }g;
1077             $nextline =~ s{(\cA+)}{my $len=length($1); substr($under x $len,0,$len)}ge;
1078             $nextline .= $trail;
1079             return [{ %std_literal, width => length($nextline), src => \$nextline }];
1080             }
1081              
1082             sub linecount($) {
1083             return tr/\n// + (/[^\n]\z/?1:0) for @_;
1084             }
1085              
1086             use warnings::register;
1087              
1088             sub form is export(:MANDATORY) {
1089             croak "Useless call to &form in void context" unless defined wantarray;
1090              
1091             # Handle formatting calls...
1092             my ($package, $file, $line) = caller;
1093             my $caller_opts = $caller_opts{$package,$file} ||= {};
1094             if (keys %$caller_opts) {
1095             $line = first { $_ < $line } sort {$b<=>$a} keys %$caller_opts;
1096             $caller_opts = $caller_opts->{$line} || {}
1097             if defined $line;
1098             }
1099             my %opts = (%def_opts, %$caller_opts);
1100             my $fldcnt = 0;
1101             my @section = {opts=>{%opts}, text=>[]};
1102             my $formats = \@_;
1103             my $first = 1;
1104             my %argcache;
1105             my ($prevformat,$currformat,@allformats);
1106             while (@$formats) {
1107             my $format = shift @$formats;
1108             if (ref $format eq 'HASH') {
1109             update %opts, %$format;
1110             $opts{page}{number} = undef unless defined $format->{page}{number};
1111             push @section, {opts=>{%opts}};
1112             redo;
1113             }
1114             if ($first) { # Change format lists if data first or last
1115             if ($opts{interleave}) {
1116             $formats = [$format =~ /.*(?:\n|\z)/g];
1117             $format = shift @$formats;
1118             }
1119             $first = 0;
1120             }
1121             $format =~ s/\n?\z/\n/;
1122             $prevformat = $currformat;
1123             $currformat = segment($format, @_, %opts, $fldcnt, %argcache);
1124             resolve_overflows($currformat, $prevformat);
1125             if (defined $opts{under}) {
1126             push @{$section[-1]{formatters}},
1127             make_underline($opts{under}, $prevformat, $currformat);
1128             $opts{under} = undef;
1129             }
1130             push @{$section[-1]{formatters}}, $currformat;
1131             push @allformats, $currformat;
1132             }
1133             croak scalar(@_), " too many data values after last format" if @_;
1134             delineate_overflows(@allformats);
1135              
1136             my $text = "";
1137             my $pagetype = 'first';
1138             my $pagenum = 1;
1139             for my $section (@section) {
1140             next unless $section->{formatters};
1141             my $sect_opts = $section->{opts};
1142             my $page = $sect_opts->{page};
1143             $page->{number} = $pagenum unless defined $page->{number};
1144             my $pagelen = $page->{length};
1145             while (1) {
1146             my $parity = $page->{number}%2 ? 'odd' : 'even';
1147             my $header =
1148             $page->{header}{$pagetype} ? $page->{header}{$pagetype}($sect_opts)
1149             : $page->{header}{$parity} ? $page->{header}{$parity}($sect_opts)
1150             : $page->{header}{other} ? $page->{header}{other}($sect_opts)
1151             : "";
1152             my $footer =
1153             $page->{footer}{$pagetype} ? $page->{footer}{$pagetype}($sect_opts)
1154             : $page->{footer}{$parity} ? $page->{footer}{$parity}($sect_opts)
1155             : $page->{footer}{other} ? $page->{footer}{other}($sect_opts)
1156             : "";
1157             my $feed =
1158             $page->{feed}{$pagetype} ? $page->{feed}{$pagetype}($sect_opts)
1159             : $page->{feed}{$parity} ? $page->{feed}{$parity}($sect_opts)
1160             : $page->{feed}{other} ? $page->{feed}{other}($sect_opts)
1161             : "";
1162             length and s/\n?\z/\n/ for $header, $footer; # NOT for $feed
1163             my $bodyfn = $page->{body}{$pagetype}
1164             || $page->{body}{$parity}
1165             || $page->{body}{other}
1166             || \&std_body;
1167             my $bodylen = max 1, $pagelen - linecount($header) - linecount($footer);
1168             my ($pagetext, $more) = make_page($section, $sect_opts, $bodylen);
1169             if (!$more && $section == $section[-1]) {
1170             my $lastheader =
1171             $page->{header}{last} ? $page->{header}{last}($sect_opts)
1172             : $header;
1173             my $lastfooter =
1174             $page->{footer}{last} ? $page->{footer}{last}($sect_opts)
1175             : $footer;
1176             length and s/\n?\z/\n/ for $lastheader, $lastfooter;
1177             my $lastlen = max 1, $pagelen-linecount($lastheader)-linecount($lastfooter);
1178             if (@$pagetext <= $lastlen) {
1179             $pagetype = 'last';
1180             ($header, $footer, $bodylen)
1181             = ($lastheader, $lastfooter, $lastlen);
1182             $feed = $page->{feed}{last}($sect_opts)
1183             if $page->{feed}{last};
1184             $bodyfn = $page->{body}{last}
1185             if $page->{body}{last};
1186             }
1187             }
1188             my $fill = $pagelen < $unlimited ? [("\n") x ($bodylen-@$pagetext)]
1189             : [];
1190              
1191             my $body = $bodyfn->($pagetext, $fill, \%opts);
1192              
1193             $text .= $header . $body . $footer . $feed;
1194             $page->{number}++;
1195              
1196             # Handle special case of empty last page...
1197             last unless $more || $section == $section[-1] && $pagetype ne 'last';
1198             $pagetype = $page->{number}%2 ? 'odd' : 'even';
1199             }
1200             $pagenum = $page->{number};
1201             }
1202              
1203             $text =~ s/[^\S\n]+\n/\n/g unless $opts{untrimmed};
1204             return $text;
1205             }
1206              
1207             sub make_page {
1208             my ($section, $sect_opts, $bodylen) = @_;
1209             my (@text, $more);
1210             my ($prevformatters, $formatters);
1211             while (@text < $bodylen && @{$section->{formatters}}) {
1212             $prevformatters = $formatters;
1213             $formatters = $section->{formatters}[0];
1214             $more = make_cols($formatters,$prevformatters,my @parts, %$sect_opts, $bodylen-@text);
1215             shift @{$section->{formatters}} unless $more;
1216             my $maxheight = 0;
1217             my $maxwidth = 0;
1218             for my $col (0..$#parts) {
1219             local $_ = $parts[$col];
1220             pop @$_ while @$_ && ! length($_->[-1]);
1221             $maxheight = max($maxheight, scalar(@$_), $formatters->[$col]{opts}{height}{min}||0);
1222             # $formatters->[$col]{pos} = $maxwidth;
1223             # $maxwidth += $formatters->[$col]{width};
1224             }
1225             for my $col (0..$#parts) {
1226             my $f = $formatters->[$col];
1227             push @{$parts[$col]}, ("") x (($f->{height}{min}||0)-@{$parts[$col]});
1228             my $fopts = $f->{opts};
1229             my $tfill = first {defined $_} @{$fopts}{qw(tfill vfill fill)}, " ";
1230             my $bfill = first {defined $_} @{$fopts}{qw(bfill vfill fill)}, " ";
1231             my $lfill = first {defined $_} @{$fopts}{qw(lfill hfill fill)}, " ";
1232             my $rfill = first {defined $_} @{$fopts}{qw(rfill hfill fill)}, " ";
1233             $f->{vjust}->($maxheight,$tfill,$bfill,$parts[$col]);
1234             for my $row (0..$#{$parts[$col]}) {
1235             my $last = $parts[$col][$row] =~ s/\r//;
1236             $f->{hjust}->($parts[$col][$row], pre=>$lfill, post=>$rfill,
1237             last=>$last, pos=>$f->{pos},
1238             stretch=>$f->{stretch}, width=>$f->{width},
1239             );
1240             }
1241             }
1242             for my $row (0..$maxheight) {
1243             push @text, join "",
1244             grep { defined } map $parts[$_][$row],0..$#parts;
1245             }
1246             }
1247             return (\@text, $more);
1248             }
1249              
1250             # Extract perpendicular cross-sections from an AoA, AoH, HoA, HoH, AoHoA, etc.
1251              
1252             sub section {
1253             my ($structure, @index) = @_;
1254             $structure = [ values %$structure ] if ref $structure eq 'HASH';
1255             my @section;
1256             for my $row ( @$structure ) {
1257             local $,=",";
1258             my $type = ref $row or croak "Too many indices (starting with [@index])";
1259             if ($type eq 'HASH') {
1260             @index = keys %$row unless @index;
1261             push @{$section[$_]}, $row->{$index[$_]} for 0..$#index;
1262             }
1263             elsif ($type eq 'ARRAY') {
1264             @index = (0..$#$row) unless @index;
1265             push @{$section[$_]}, $row->[$index[$_]] for 0..$#index;
1266             }
1267             else {
1268             my $what = ref $structure;
1269             croak "Can't drill ", ($what ? lc $what : $structure) , " of $type";
1270             }
1271             }
1272             return @section;
1273             }
1274              
1275             sub slice {
1276             my ($structure, @indices) = @_;
1277             return ref eq 'HASH' ? @{$_}{@indices} : @{$_}[@indices] for $structure;
1278             }
1279              
1280             sub vals { return ref eq 'HASH' ? values %$_ : @$_ for $_[0] }
1281              
1282             sub drill (\[@%];@) is export {
1283             my ($structure, @indices) = @_;
1284             return $structure unless @indices;
1285             my $index = shift @indices;
1286             my @section = [ @$index ? slice($structure,@$index) : vals($structure) ];
1287             return @section unless @indices;
1288             for my $index (@indices) {
1289             @section = map {section $_, @$index} @section;
1290             }
1291             return @section;
1292             }
1293              
1294             sub break_lit {
1295             return (${$_[0]},0,0);
1296             }
1297              
1298             sub break_bullet {
1299             my ($src) = @_;
1300             my $next = pop @$src || "";
1301             return ($next,@$src>0,0);
1302             }
1303              
1304             sub break_verbatim {
1305             my ($str,$rem) = @_;
1306             $$str =~ m/ \G ([^\n\r]*) (?:\r|\n|\z) /gcx or return ("",0);
1307             return (substr("$1",0,$rem), $$str =~ m/ \G (?=.) /sgcx ? 1 : 0,0);
1308             }
1309              
1310             sub break_nl {
1311             my ($str) = @_;
1312             if ($$str =~ m/\G [^\S\n\r]* ([^\n\r]*?) [^\S\r\n]* (?:\r|$)/gcxm) {
1313             return ("$1", $$str =~ /\G(?=.*\S)/sgc?1:0, 1);
1314             }
1315             else {
1316             return ("",0,0);
1317             }
1318             }
1319              
1320             my $wsnzw = q{ (??{length($^N)?'(?=)':'(?!)'}) };
1321              
1322             sub break_at is export {
1323             my ($hyphen) = @_;
1324             my ($lit_hy) = qr/\Q$hyphen\E/;
1325             my $hylen = length($hyphen);
1326             my @ret;
1327             return sub {
1328             my ($str,$rem,$ws) = @_;
1329             my ($last_breakable, $res) = ($rem+1,"", 0);
1330             for ($$str) {
1331             use re 'eval';
1332             while ($rem > 0 && (pos()||0) < length()) {
1333             if ($ws && /\G ($ws) $wsnzw/gcx) {
1334             my $captured;
1335             if ($#+ > 1) { # may be extra captures...
1336             for (2..$#+) {
1337             next unless defined $$_;
1338             $captured++;
1339             $res .= $$_;
1340             $rem -= length $$_;
1341             }
1342             }
1343             unless ($captured) {
1344             $res .= $1;
1345             $rem--;
1346             }
1347             $last_breakable = length $res;
1348             }
1349             elsif ($rem>=$hylen && /\G $lit_hy /gcx) {
1350             $res .= $hyphen;
1351             $rem -= $hylen;
1352             $last_breakable = length $res;
1353             }
1354             elsif (/\G ((?!$lit_hy)[^\n\r]) /gcx) {
1355             $res .= $1;
1356             $rem--;
1357             $last_breakable = length $res if $res =~ /\s$/;
1358             }
1359             else { last }
1360             }
1361             my $reslen = length $res;
1362             $ws ||= qr/\s/;
1363             unless (/\G (?=$lit_hy|($ws)$wsnzw|\z|\n|\r) /gcx) {
1364             if ($last_breakable <= $reslen) {
1365             pos() -= $reslen-$last_breakable;
1366             substr($res,$last_breakable) = "";
1367             }
1368             elsif ($reslen > $hylen) {
1369             if ($res =~ /\S\S\S{$hylen}$/) {
1370             pos() -= $hylen;
1371             substr($res,-$hylen) = $hyphen;
1372             }
1373             elsif ($res =~ s/(\S+)$//) {
1374             pos() -= length($1);
1375             }
1376             }
1377             }
1378             my $rem = substr($$str, pos $$str);
1379             return ($res, $rem=~/\S/?1:0, $rem =~ /^\s*(?:\z|\n|\r)/);
1380             }
1381             };
1382             }
1383              
1384             sub import {
1385             my $class = shift;
1386             my ($package, $file, $line) = caller;
1387             my %opts;
1388             for (@_) {
1389             croak "Options for $class must be specified in a hash"
1390             unless ref eq 'HASH';
1391             update(%opts, %$_, 'croak');
1392             }
1393             $caller_opts{$package,$file}{$line} = \%opts;
1394             }
1395              
1396             package Perl6::Form::Rule::Fail;
1397             use overload
1398             '""' => sub{ undef },
1399             '0+' => sub{ undef },
1400             'bool' => sub{ 0 },
1401             ;
1402              
1403             package Perl6::Form::Rule::Okay;
1404             use overload
1405             '""' => sub{ $_[0][0] },
1406             '0+' => sub{ $_[0][0] },
1407             'bool' => sub{ 1 },
1408             ;
1409              
1410             1;
1411             __END__