File Coverage

blib/lib/Perl6/Form.pm
Criterion Covered Total %
statement 458 951 48.1
branch 158 542 29.1
condition 89 332 26.8
subroutine 52 96 54.1
pod 0 57 0.0
total 757 1978 38.2


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