File Coverage

blib/lib/String/Print.pm
Criterion Covered Total %
statement 318 340 93.5
branch 191 270 70.7
condition 83 147 56.4
subroutine 42 46 91.3
pod 9 10 90.0
total 643 813 79.0


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution String-Print version 1.02.
2             # The POD got stripped from this file by OODoc version 3.05.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2016-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package String::Print;{
13             our $VERSION = '1.02';
14             }
15              
16              
17 19     19   2583093 use warnings;
  19         95  
  19         1301  
18 19     19   216 use strict;
  19         48  
  19         546  
19 19     19   3421 use utf8;
  19         2112  
  19         147  
20              
21             #use Log::Report::Optional 'log-report';
22              
23 19     19   9137 use Unicode::GCString ();
  19         717863  
  19         592  
24 19     19   11781 use Data::Dumper ();
  19         160693  
  19         881  
25 19     19   9677 use Date::Parse qw/str2time/;
  19         160176  
  19         1892  
26 19     19   151 use Encode qw/is_utf8 decode/;
  19         51  
  19         1127  
27 19     19   9722 use HTML::Entities qw/encode_entities/;
  19         108074  
  19         2029  
28 19     19   9818 use POSIX qw/strftime/;
  19         135045  
  19         118  
29 19     19   31253 use Scalar::Util qw/blessed reftype/;
  19         64  
  19         23839  
30              
31             my @default_modifiers = (
32             qr/\% ?\S+/ => \&_modif_format,
33             qr/BYTES\b/ => \&_modif_bytes,
34             qr/HTML\b/ => \&_modif_html,
35             qr/YEAR\b/ => \&_modif_year,
36             qr/TIME\b/ => \&_modif_time,
37             qr/\=/ => \&_modif_name,
38             qr/DATE\([^)]*\)|DATE\b/ => \&_modif_date,
39             qr/DT\([^)]*\)|DT\b/ => \&_modif_dt,
40             qr!UNKNOWN\([0-9]+\)|UNKNOWN\b! => \&_modif_unknown,
41             qr!CHOP\([0-9]+(?:\,?[^)]*)\)|CHOP\b! => \&_modif_chop,
42             qr!EL\([0-9]+(?:\,?[^)]*)\)|EL\b! => \&_modif_ellipsis,
43             qr!//(?:\"[^"]*\"|\'[^']*\'|\w+)! => \&_modif_undef,
44             );
45              
46             # Be warned: %F and %T (from C99) are not always supported on Windows
47             my %dt_format = (
48             ASC => '%a %b %e %H:%M:%S %Y',
49             ISO => '%Y-%m-%dT%H:%M:%S%z',
50             RFC822 => '%a, %d %b %y %H:%M:%S %z',
51             RFC2822 => '%a, %d %b %Y %H:%M:%S %z',
52             RFC5322 => '%a, %d %b %Y %H:%M:%S %z',
53             FT => '%Y-%m-%d %H:%M:%S',
54             );
55              
56             my %date_format = (
57             '-' => '%Y-%m-%d',
58             '/' => '%Y/%m/%d',
59             );
60              
61             my %defaults = (
62             CHOP => +{ width => 30, head => '[', units => '', tail => ']' },
63             DATE => +{ format => $date_format{'-'}, },
64             DT => +{ format => $dt_format{FT}, },
65             EL => +{ width => 30, replace => '⋯ '},
66             FORMAT => +{ thousands => '' },
67             UNKNOWN => +{ width => 30, trim => 'EL' },
68             );
69              
70             my %default_serializers = (
71             UNDEF => sub { 'undef' },
72             '' => sub { $_[1] },
73             SCALAR => sub { ${$_[1]} // shift->{SP_seri}{UNDEF}->(@_) },
74             ARRAY => sub { my $v = $_[1]; my $join = $_[2]{_join} // ', '; join $join, map +($_ // 'undef'), @$v },
75             HASH => sub { my $v = $_[1]; join ', ', map "$_ => ".($v->{$_} // 'undef'), sort keys %$v },
76             # CODE value has different purpose
77             );
78              
79             my %predefined_encodings = (
80             HTML => {
81             exclude => [ qr/html$/i ],
82             encode => sub { encode_entities $_[0] },
83             },
84             );
85              
86              
87 34     34 1 2789857 sub new(@) { my $class = shift; (bless {}, $class)->init( +{@_} ) }
  34         208  
88              
89             sub init($)
90 34     34 0 100 { my ($self, $args) = @_;
91              
92 34         390 my $modif = $self->{SP_modif} = [ @default_modifiers ];
93 34 100       145 if(my $m = $args->{modifiers})
94 3         30 { unshift @$modif, @$m;
95             }
96              
97 34   100     197 my $s = $args->{serializers} || {};
98 34 100       356 my $seri = $self->{SP_seri} = +{ %default_serializers, (ref $s eq 'ARRAY' ? @$s : %$s) };
99              
100 34         237 $self->{SP_defs} = +{ %defaults }; # the HASHes get copied when changed.
101 34 50       158 $self->setDefaults($args->{defaults}) if $args->{defaults};
102              
103 34         203 $self->encodeFor($args->{encode_for});
104 34   50     233 $self->{SP_missing} = $args->{missing_key} || \&_reportMissingKey;
105 34         137 $self;
106             }
107              
108             sub import(@)
109 19     19   232 { my $class = shift;
110 19         38 my ($oo, %func);
111 19         92 while(@_)
112 5 100       34 { last if $_[0] !~ m/^s?print[ip]$/;
113 3         17 $func{shift()} = 1;
114             }
115              
116 19 100 100     81 if(@_ && $_[0] eq 'oo')
117             { # import only object oriented interface
118 1         2 shift @_;
119 1 50       1 @_ and die "no options allowed at import with oo interface";
120 1         44 return;
121             }
122              
123 18         53 my $all = !keys %func;
124 18         68 my $f = $class->new(@_); # OO encapsulated
125 18         69 my ($pkg) = caller;
126 19     19   156 no strict 'refs';
  19         36  
  19         65339  
127 18 100 66 0   104 *{"$pkg\::printi"} = sub { $f->printi(@_) } if $all || $func{printi};
  16         102  
  0         0  
128 18 100 100 3   99 *{"$pkg\::sprinti"} = sub { $f->sprinti(@_) } if $all || $func{sprinti};
  17         65  
  3         324289  
129 18 100 66 0   77 *{"$pkg\::printp"} = sub { $f->printp(@_) } if $all || $func{printp};
  16         163  
  0         0  
130 18 50 66 4   106 *{"$pkg\::sprintp"} = sub { $f->sprintp(@_) } if $all || $func{sprintp};
  18         59  
  4         240038  
131 18         32826 $class;
132             }
133              
134             #--------------------
135              
136 2     2 1 8 sub addModifiers(@) { my $s = shift; unshift @{$s->{SP_modif}}, @_ }
  2         6  
  2         17  
137              
138              
139             sub encodeFor($)
140 36     36 1 146 { my ($self, $type) = (shift, shift);
141             defined $type
142 36 100       167 or return $self->{SP_enc} = undef;
143              
144 2         5 my %def;
145 2 50       8 if(ref $type eq 'HASH')
146 0         0 { %def = %$type;
147             }
148             else
149 2 50       10 { my $def = $predefined_encodings{$type} or die "ERROR: unknown output encoding type $type\n";
150 2         11 %def = (%$def, @_);
151             }
152              
153 2   50     10 my $excls = $def{exclude} || [];
154 2 50       20 my $regexes = join '|',
    50          
155             map +(ref $_ eq 'Regexp' ? $_ : qr/(?:^|\.)\Q$_\E$/),
156             ref $excls eq 'ARRAY' ? @$excls : $excls;
157 2         25 $def{SP_exclude} = qr/$regexes/o;
158              
159 2         9 $self->{SP_enc} = \%def;
160             }
161              
162              
163             sub setDefaults(@)
164 5     5 1 47 { my $self = shift;
165 5         13 my $default = $self->{SP_defs};
166              
167 5 50 33     38 my @set = @_==1 && ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
  0         0  
168 5         17 while(@set)
169 5         16 { my ($modif, $defs) = (shift @set, shift @set);
170 5 50       57 my $was = $defaults{$modif} or die "No defaults available for $modif.";
171 5         43 $default->{$modif} = +{ %$was, %$defs };
172             }
173              
174 5         14 $self;
175             }
176              
177              
178 87     87 1 264 sub defaults($) { $_[0]->{SP_defs}{$_[1]} }
179              
180             #--------------------
181             #XXX OODoc does not like it when we have methods and functions with the same name.
182              
183              
184             #--------------------
185              
186             sub sprinti($@)
187 162     162 1 32202 { my ($self, $format) = (shift, shift);
188 162 100       775 my $args = @_==1 ? shift : +{ @_ };
189             # $args may be a blessed HASH, for instance a Log::Report::Message
190              
191 162   100     1110 $args->{_join} //= ', ';
192 162         484 local $args->{_format} = $format;
193              
194 162 100       1507 my @frags = split /\{([^}]*)\}/, # enforce unicode
195             is_utf8($format) ? $format : decode(latin1 => $format);
196              
197 162         18158 my @parts;
198              
199             # Code parially duplicated for performance!
200 162 100       635 if(my $enc = $self->{SP_enc})
201 5         11 { my $encode = $enc->{encode};
202 5         12 my $exclude = $enc->{SP_exclude};
203 5 50       15 push @parts, $encode->($args->{_prepend}) if defined $args->{_prepend};
204 5         17 push @parts, $encode->(shift @frags);
205 5         199 while(@frags) {
206 4 50       36 my ($name, $tricks) = (shift @frags) =~ m!^\s*([\pL\p{Pc}\pM][\w.]*)\s*(.*?)\s*$!o or die $format;
207              
208 4 100       42 push @parts, $name =~ $exclude
209             ? $self->_expand($name, $tricks, $args)
210             : $encode->($self->_expand($name, $tricks, $args));
211              
212 4 50       73 push @parts, $encode->(shift @frags) if @frags;
213             }
214 5 50       158 push @parts, $encode->($args->{_append}) if defined $args->{_append};
215             }
216             else
217 157 50       503 { push @parts, $args->{_prepend} if defined $args->{_prepend};
218 157         361 push @parts, shift @frags;
219 157         420 while(@frags) {
220 161 50       1230 (shift @frags) =~ /^\s*([\pL\p{Pc}\pM][\w.]*)\s*(.*?)\s*$/o or die $format;
221 161         716 push @parts, $self->_expand($1, $2, $args);
222 161 100       1252 push @parts, shift @frags if @frags;
223             }
224 157 50       469 push @parts, $args->{_append} if defined $args->{_append};
225             }
226              
227 162         1542 join '', @parts;
228             }
229              
230             sub _expand($$$)
231 165     165   762 { my ($self, $key, $modifier, $args) = @_;
232 165         573 local $args->{varname} = $key;
233              
234 165         258 my $value;
235 165 100       539 if(index($key, '.') == -1)
236             { # simple value
237 159 100       480 $value = exists $args->{$key} ? $args->{$key} : $self->_missingKey($key, $args);
238 159         518 $value = $value->($self, $key, $args)
239             while ref $value eq 'CODE';
240             }
241             else
242 6         14 { my @parts = split /\./, $key;
243 6         9 my $key = shift @parts;
244 6 50       14 $value = exists $args->{$key} ? $args->{$key} : $self->_missingKey($key, $args);
245              
246 6         12 $value = $value->($self, $key, $args)
247             while ref $value eq 'CODE';
248              
249 6   66     21 while(defined $value && @parts)
250 8 100 66     30 { if(blessed $value)
    100 33        
    50          
251 1         3 { my $method = shift @parts;
252 1 50       8 $value->can($method) or die "object $value cannot $method\n";
253 1         3 $value = $value->$method; # parameters not supported here
254             }
255             elsif(ref $value && reftype $value eq 'HASH')
256 6         11 { $value = $value->{shift @parts};
257             }
258             elsif(index($value, ':') != -1 || $::{$value.'::'})
259 1         2 { my $method = shift @parts;
260 1 50       6 $value->can($method) or die "class $value cannot $method\n";
261 1         3 $value = $value->$method; # parameters not supported here
262             }
263             else
264 0         0 { die "not a HASH, object, or class at $parts[0] in $key\n";
265             }
266              
267 8         29 $value = $value->($self, $key, $args)
268             while ref $value eq 'CODE';
269             }
270             }
271              
272 165         270 my $mod;
273             STACKED:
274 165         398 while(length $modifier)
275 138         209 { my @modif = @{$self->{SP_modif}};
  138         644  
276 138         339 while(@modif)
277 670         1646 { my ($regex, $callback) = (shift @modif, shift @modif);
278 670 100       27954 $modifier =~ s/^($regex)\s*// or next;
279              
280 138         602 $value = $callback->($self, $1, $value, $args);
281 138         1006 next STACKED;
282             }
283 0         0 return "{unknown modifier '$modifier'}";
284             }
285              
286 165 100       577 my $seri = $self->{SP_seri}{defined $value ? ref $value : 'UNDEF'};
287 165 50       520 $seri ? $seri->($self, $value, $args) : "$value";
288             }
289              
290             sub _missingKey($$)
291 1     1   4 { my ($self, $key, $args) = @_;
292 1         5 $self->{SP_missing}->($self, $key, $args);
293             }
294              
295             sub _reportMissingKey($$)
296 1     1   4 { my ($self, $key, $args) = @_;
297              
298 1         2 my $depth = 0;
299 1         3 my ($filename, $linenr);
300 1         10 while((my $pkg, $filename, $linenr) = caller $depth++)
301 4 100 66     44 { last unless $pkg->isa(__PACKAGE__) || $pkg->isa('Log::Report::Minimal::Domain');
302             }
303              
304             warn $self->sprinti(
305             "Missing key '{key}' in format '{format}', file {fn} line {line}\n",
306 1         8 key => $key, format => $args->{_format}, fn => $filename, line => $linenr
307             );
308              
309 1         10 undef;
310             }
311              
312             # See dedicated section in explanation in DETAILS
313             sub _modif_format_s($$$$$)
314 13     13   101 { my ($value, $padding, $width, $max, $u) = @_;
315              
316             # String formats like %10s or %-3.5s count characters, not width.
317             # String formats like %10S or %-3.5S are subject to column width.
318             # The latter means: minimal 3 chars, max 5, padding right with blanks.
319             # All inserted strings are upgraded into utf8.
320              
321 13 100       106 my $s = Unicode::GCString->new(is_utf8($value) ? $value : decode(latin1 => $value));
322              
323 13         1229 my $pad;
324 13 50       47 if($u eq 'S')
325             { # too large to fit
326 0 0 0     0 return $value if !$max && $width && $width <= $s->columns;
      0        
327              
328             # wider than max. Waiting for $s->trim($max) if $max, see
329             # https://rt.cpan.org/Public/Bug/Display.html?id=84549
330 0   0     0 $s->substr(-1, 1, '')
331             while $max && $s->columns > $max;
332              
333 0 0       0 $pad = $width ? $width - $s->columns : 0;
334             }
335             else # $u eq 's'
336 13 100 100     323 { return $value if !$max && $width && $width <= length $s;
      100        
337 12 100 66     95 $s->substr($max, length($s)-$max, '') if $max && length $s > $max;
338 12 100       51 $pad = $width ? $width - length $s : 0;
339             }
340              
341 12 100       163 $pad==0 ? $s->as_string
    100          
342             : $padding eq '-' ? $s->as_string . (' ' x $pad)
343             : (' ' x $pad) . $s->as_string;
344             }
345              
346             sub _modif_format_d($$$$)
347 27     27   114 { my ($value, $padding, $max, $sep) = @_;
348 27         146 my $d = sprintf "%d", $value; # what perl usually does with floats etc
349 27 100       257 my $v = length $sep ? reverse(reverse($d) =~ s/([0-9][0-9][0-9])/$1$sep/gr) : $d;
350 27         343 $v =~ s/^\Q$sep//;
351              
352 27 100       84 if($d !~ /^\-/)
353 23 100       60 { $v = "+$v" if $padding eq '+';
354 23 100       73 $v = " $v" if $padding eq ' ';
355             }
356 27 100       121 $max or return $v;
357              
358 6         21 my $pad = $max - length $v;
359              
360 6 50       55 $pad <= 0 ? $v
    100          
    100          
    50          
361             : $padding eq '-' ? $v . (' ' x $pad)
362             : $padding eq '0' ? ('0' x $pad) . $v
363             : $padding eq '' ? (' ' x $pad) . $v
364             : $v;
365             }
366              
367             sub _modif_format($$$$)
368 41     41   180 { my ($self, $format, $value, $args) = @_;
369 41 50 33     320 defined $value && length $value or return undef;
370              
371 41         143 my $defaults = $self->defaults('FORMAT');
372              
373 19     19   9042 use locale;
  19         14816  
  19         151  
374 41 50       164 if(ref $value eq 'ARRAY')
    50          
375 0 0       0 { @$value or return '(none)';
376 0         0 return +[ map $self->_format_print($format, $_, $args), @$value ];
377             }
378             elsif(ref $value eq 'HASH')
379 0 0       0 { keys %$value or return '(none)';
380 0         0 return +{ map +($_ => $self->_format_print($format, $value->{$_}, $args)), keys %$value } ;
381             }
382              
383             $format =~ m/^\%(\-?)([0-9]*)(?:\.([0-9]*))?([sS])$/ ? _modif_format_s($value, $1, $2, $3, $4)
384             : $format =~ m/^\%([+\ \-0]?)([0-9]*)([_,.])?d$/ ? _modif_format_d($value, $1, $2, $3 // $defaults->{thousands})
385 41 100 100     472 : return sprintf $format, $value; # simple: standard perl sprintf()
    100          
386             }
387              
388             # See dedicated section in explanation in DETAILS
389             sub _modif_bytes($$$)
390 19     19   49 { my ($self, $format, $value, $args) = @_;
391 19 50 33     177 defined $value && length $value or return undef;
392              
393 19 100       51 return sprintf("%3d B", $value) if $value < 1000;
394              
395 14         48 my @scale = qw/kB MB GB TB PB EB ZB/;
396 14         21 $value /= 1024;
397              
398 14   100     50 while(@scale > 1 && $value > 999)
399 18         17 { shift @scale;
400 18         38 $value /= 1024;
401             }
402              
403 14 100       43 return sprintf "%3d$scale[0]", $value + 0.5
404             if $value > 9.949;
405              
406 8         62 sprintf "%3.1f$scale[0]", $value;
407             }
408              
409             sub _modif_html($$$)
410 1     1   6 { my ($self, $format, $value, $args) = @_;
411 1 50       9 defined $value ? (encode_entities $value) : undef;
412             }
413              
414             sub _modif_year($$$)
415 5     5   17 { my ($self, $format, $value, $args) = @_;
416 5 50       13 defined $value or return undef;
417              
418 5 50 33     43 blessed $value && $value->isa('DateTime')
419             and return $value->year;
420              
421 5 50       13 length $value or return undef;
422              
423 5 100 66     47 return $1
424             if $value =~ /^\s*([0-9]{4})\s*$/ && $1 < 2200;
425              
426 2 100       12 my $stamp = $value =~ /^\s*([0-9]+)\s*$/ ? $1 : str2time($value);
427 2 50       305 defined $stamp or return "year not found in '$value'";
428              
429 2         52 strftime "%Y", localtime($stamp);
430             }
431              
432             sub _modif_date($$$)
433 9     9   25 { my ($self, $format, $value, $args) = @_;
434 9 50       33 defined $value or return undef;
435              
436 9         22 my $defaults = $self->defaults('DATE');
437 9   66     47 my $kind = ($format =~ m/^DATE\(([^)]*)\)/ ? $1 : undef) || $defaults->{format};
438 9   66     28 my $pattern = $date_format{$kind} // $kind;
439              
440 9         11 my ($y, $m, $d);
441 9 50 33     78 if(blessed $value && $value->isa('DateTime'))
    100 100        
442 0         0 { ($y, $m, $d) = ($value->year, $value->month, $value->day);
443             }
444             elsif( $value =~ m!^\s*([0-9]{4})[:/.-]([0-9]?[0-9])[:/.-]([0-9]?[0-9])\s*$!
445             || $value =~ m!^\s*([0-9]{4})([0-9][0-9])([0-9][0-9])\s*$!)
446 8         23 { ($y, $m, $d) = ($1, $2, $3);
447             }
448             else
449 1 50       9 { my $stamp = $value =~ /\D/ ? str2time($value) : $value;
450 1 50       181 defined $stamp or return "date not found in '$value'";
451 1         8 ($y, $m, $d) = (localtime $stamp)[5, 4, 3];
452 1         2 $y += 1900; $m++;
  1         2  
453             }
454              
455 9         46 $pattern
456             =~ s/\%Y/$y/r
457 9         58 =~ s/\%m/sprintf "%02d", $m/re
458 9         53 =~ s/\%d/sprintf "%02d", $d/re;
459             }
460              
461             sub _modif_time($$$)
462 4     4   13 { my ($self, $format, $value, $args) = @_;
463 4 50       8 defined $value or return undef;
464              
465 4 50 33     18 blessed $value && $value->isa('DateTime')
466             and return $value->hms;
467              
468 4 50       7 length $value or return undef;
469              
470 4 100 100     49 return sprintf "%02d:%02d:%02d", $1, $2, $3||0
      66        
471             if $value =~ m!^\s*(0?[0-9]|1[0-9]|2[0-3])\:([0-5]?[0-9])(?:\:([0-5]?[0-9]))?\s*$!
472             || $value =~ m!^\s*(0[0-9]|1[0-9]|2[0-3])([0-5][0-9])(?:([0-5][0-9]))?\s*$!;
473              
474 2 50       11 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
475 2 50       347 defined $stamp or return "time not found in '$value'";
476              
477 2         33 strftime "%H:%M:%S", localtime($stamp);
478             }
479              
480             sub _modif_dt($$$)
481 3     3   15 { my ($self, $format, $value, $args) = @_;
482 3 100       13 defined $value or return undef;
483              
484 1 50 33     5 blessed $value && $value->isa('DateTime')
485             and $value = $value->epoch;
486              
487 1 50       5 length $value or return undef;
488              
489 1         7 my $defaults = $self->defaults('DT');
490 1   33     12 my $kind = ($format =~ m/^DT\(([^)]*)\)/ ? $1 : undef) || $defaults->{format};
491 1   33     7 my $pattern = $dt_format{$kind} // $kind;
492              
493 1 50       12 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
494 1 50       480 defined $stamp or return "dt not found in '$value'";
495              
496 1         35 strftime $pattern, localtime($stamp);
497             }
498              
499             sub _modif_undef($$$)
500 12     12   56 { my ($self, $format, $value, $args) = @_;
501 12 100 66     114 return $value if defined $value && length $value;
502 7 50       88 $format =~ m!//"([^"]*)"|//'([^']*)'|//(\w*)! ? $+ : undef;
503             }
504              
505             sub _modif_name($$$)
506 3     3   13 { my ($self, $format, $value, $args) = @_;
507 3         13 "$args->{varname}$format$value";
508             }
509              
510             sub _modif_chop($$$)
511 10     10   43 { my ($self, $format, $value, $args) = @_;
512 10 50 33     55 defined $value && length $value or return undef;
513              
514 10         33 my $defaults = $self->defaults('CHOP');
515 10 50       60 $format =~ m/^ CHOP\( ([0-9]+) \,? ([^)]+)? \) | CHOP\b /x or die $format;
516 10   66     47 my $width = $1 // $args->{width} // $defaults->{width};
      66        
517 10   33     51 my $units = $2 // $args->{units} // $defaults->{units};
      66        
518 10 50       80 $width != 0 or return $value;
519              
520             # max width of a char is 2
521 10 50       28 return $value if 2 * length $value < $width; # surely small enough?
522              
523 10 50       84 my $v = Unicode::GCString->new(is_utf8($value) ? $value : decode(latin1 => $value));
524 10 100       743 return $value if $width >= $v->columns; # small enough after counting
525              
526 9         22 my $head = $defaults->{head};
527 9         20 my $tail = $defaults->{tail};
528              
529             #XXX This is expensive for long texts, but the value could be filled with many zero-widths
530 9         28 my ($shortened, $append) = (0, $head . '+0' . $units . $tail);
531 9         38 while($v->columns > $width - length $append)
532 194         653 { my $chopped = $v->substr(-1, 1, '');
533              
534 194 100       730 unless($chopped->length)
535             { # nothing left
536 2         5 $append = $head . $shortened . $units . $tail;
537 2         8 last;
538             }
539              
540 192 50       481 $chopped->columns > 0 or next;
541 192         276 $shortened++;
542 192         793 $append = $head . '+' . $shortened . $units . $tail;
543             }
544              
545             # might be one column short
546 9 50       37 my $pad = $v->columns < $width - (length $append) ? ' ' : '';
547 9         81 $v->as_string . $pad . $append;
548             }
549              
550             sub _modif_ellipsis($$$)
551 16     16   66 { my ($self, $format, $value, $args) = @_;
552 16 50 33     90 defined $value && length $value or return undef;
553              
554 16         55 my $defaults = $self->defaults('EL');
555 16 50       86 $format =~ m/^ EL\( ([0-9]+) \,? ([^)]+)? \) | EL\b /x or die $format;
556 16   66     109 my $width = $1 // $args->{width} // $defaults->{width};
      66        
557 16   33     81 my $replace = $2 // $args->{replace} // $defaults->{replace};
      33        
558 16 50       53 $width != 0 or return $value;
559              
560             # max width of a char is 2
561 16 100       58 return $value if 2 * length($value) < $width; # surely small enough?
562              
563 14 50       125 my $v = Unicode::GCString->new(is_utf8($value) ? $value : decode(latin1 => $value));
564 14 100       4976 return $value if $width >= $v->columns; # small enough after counting
565              
566 11 50       49 my $r = Unicode::GCString->new(is_utf8($replace) ? $replace : decode(latin1 => $replace));
567 11 100       218 $r->columns < $width or return $replace;
568              
569             #XXX This is expensive for long texts, but the value could be filled with many zero-widths
570 10         34 my $take = $width - $r->columns;
571 10         733 $v->substr(-1, 1, '') while $v->columns > $take;
572              
573             # might be one column short
574 10 50       55 my $pad = $v->columns + $r->columns < $width ? ' ' : '';
575 10         163 $v->as_string . $pad . $replace;
576             }
577              
578             sub _modif_unknown($$$)
579 10     10   46 { my ($self, $format, $value, $args) = @_;
580 10 50       23 defined $value or return undef;
581              
582 10         36 my $defaults = $self->defaults('UNKNOWN');
583 10 50       50 $format =~ m/^ UNKNOWN\( ([0-9]+) \) | UNKNOWN\b /x or die $format;
584 10   33     59 $args->{width} = $1 // $args->{width} // $defaults->{width};
      33        
585              
586 10 100       28 return ref $value
587             if blessed $value;
588              
589 8   33     103 my $trim = $args->{trim} // $defaults->{trim};
590 8 50       26 my $trimmer = $trim eq 'EL' ? '_modif_ellipsis' : $trim eq 'CHOP' ? '_modif_chop' : die $trim;
    100          
591 8     8   48 my $shorten = sub { $self->$trimmer($trim, $_[0], $args) };
  8         44  
592              
593 8         55 my $serial = Data::Dumper->new([$value])->Quotekeys(0)->Terse(1)->Useqq(1)->Indent(0)->Sortkeys(1)->Dump;
594              
595 8 50       932 ! reftype $value ? '"' . $shorten->($serial =~ s/^\"//r =~ s/\"$//r) . '"'
    100          
    100          
596             : reftype $value eq 'ARRAY' ? '[' . $shorten->($serial =~ s/^\[//r =~ s/\]$//r) . ']'
597             : reftype $value eq 'HASH' ? '{' . $shorten->($serial =~ s/^\{//r =~ s/\}$//r) . '}'
598             : $shorten->($serial);
599             }
600              
601              
602             sub printi($$@)
603 0     0 1 0 { my $self = shift;
604 0 0       0 my $fh = ref $_[0] eq 'GLOB' ? shift : select;
605 0         0 $fh->print($self->sprinti(@_));
606             }
607              
608              
609              
610             sub printp($$@)
611 0     0 1 0 { my $self = shift;
612 0 0       0 my $fh = ref $_[0] eq 'GLOB' ? shift : select;
613 0         0 $fh->print($self->sprintp(@_));
614             }
615              
616              
617             sub _printp_rewrite($)
618 22     22   248532 { my @params = @{$_[0]};
  22         69  
619 22         40 my $printp = $params[0];
620 22         37 my ($printi, @iparam);
621 22         50 my ($pos, $maxpos) = (1, 1);
622              
623 22         71 while(length $printp)
624 47         202 { $printp =~ s/^([^%]*)//s; # take printables
625 47         104 $printi .= $1;
626 47 100       107 length $printp or last;
627              
628 27 50       78 if($printp =~ s/^\%\%//) # %% means real %
629 0         0 { $printi .= '%';
630 0         0 next;
631             }
632              
633 27 50       182 $printp =~ s/
634             \%
635             (?:([0-9]+)\$)? # 1=positional
636             ([-+0 \#]*) # 2=flags
637             ([0-9]*|\*)? # 3=width
638             (?:\.([0-9]*|\*))? # 4=precission
639             (?:\{ ([^}]*) \})? # 5=modifiers
640             (\w) # 6=conversion
641             //x
642             or die "format error at '$printp' in '$params[0]'";
643              
644 27 100       102 $pos = $1 if $1;
645 27 100       118 my $width = !defined $3 ? '' : $3 eq '*' ? $params[$pos++] : $3;
    50          
646 27 100       89 my $prec = !defined $4 ? '' : $4 eq '*' ? $params[$pos++] : $4;
    100          
647 27 100       94 my $modif = !defined $5 ? '' : $5;
648 27         41 my $valpos = $pos++;
649 27 100       61 $maxpos = $pos if $pos > $maxpos;
650 27         78 push @iparam, "_$valpos" => $params[$valpos];
651 27 100 100     207 my $format = '%'.$2.($width || '').($prec ? ".$prec" : '').$6;
652 27 100       58 $format = '' if $format eq '%s';
653 27 100       81 my $sep = $modif.$format =~ m/^\w/ ? ' ' : '';
654 27         75 $printi .= "{_$valpos$sep$modif$format}";
655             }
656 22         68 splice @params, 0, $maxpos, @iparam;
657 22         113 ($printi, \@params);
658             }
659              
660             sub sprintp(@)
661 4     4 1 11 { my $self = shift;
662 4         18 my ($i, $iparam) = _printp_rewrite \@_;
663 4         27 $self->sprinti($i, +{@$iparam});
664             }
665              
666             #--------------------
667              
668             1;