File Coverage

blib/lib/String/Elide/Lines.pm
Criterion Covered Total %
statement 99 102 97.0
branch 39 48 81.2
condition 15 29 51.7
subroutine 6 6 100.0
pod 1 1 100.0
total 160 186 86.0


line stmt bran cond sub pod time code
1             package String::Elide::Lines;
2              
3             our $DATE = '2017-01-29'; # DATE
4             our $VERSION = '0.003'; # VERSION
5              
6 1     1   15246 use 5.010001;
  1         3  
7 1     1   3 use strict;
  1         2  
  1         20  
8 1     1   4 use warnings;
  1         2  
  1         23  
9              
10 1     1   3 use Exporter;
  1         1  
  1         904  
11             our @ISA = qw(Exporter);
12             our @EXPORT_OK = qw(elide);
13              
14             sub _elide_lines {
15 21     21   22 my ($lines, $len, $marker, $truncate) = @_;
16              
17 21 50       50 $marker = ["$marker\n"] unless ref $marker eq 'ARRAY';
18              
19 21 50       29 if (@$lines <= $len) {
20 0         0 return $lines;
21             }
22              
23 21         17 my $len_marker = @$marker;
24 21 100       28 if ($len <= $len_marker) {
25 8         9 return [ @{$marker}[0..$len-1] ];
  8         14  
26             }
27              
28 13 100       31 if ($truncate eq 'top') {
    100          
    100          
29 1         3 return [ @$marker, @{$lines}[(@$lines - $len + $len_marker) .. $#$lines] ];
  1         3  
30             } elsif ($truncate eq 'middle') {
31 1         4 my @top_lines = @{$lines}[0 .. ($len-$len_marker)/2-1];
  1         3  
32 1         3 my @bottom_lines = @{$lines}[(@$lines - ($len-$len_marker-@top_lines)) .. $#$lines];
  1         2  
33 1         4 return [ @top_lines, @$marker, @bottom_lines ];
34             } elsif ($truncate eq 'ends') {
35 1 50       3 if ($len <= 2*$len_marker) {
36 0         0 my @marker2 = (@$marker, @$marker);
37 0         0 return [@marker2[0..$len-1]];
38             }
39 1         4 my $offset = (@$lines-$len)/2 + $len_marker;
40 1         4 return [ @$marker, @{$lines}[$offset .. $offset + ($len-2*$len_marker)-1], @$marker ];
  1         4  
41             } else { # bottom
42 10         11 return [ @{$lines}[0 .. $len-$len_marker-1], @$marker ];
  10         29  
43             }
44             }
45              
46             sub elide {
47 21     21 1 5431 my ($str, $len, $opts) = @_;
48              
49 21   100     55 $opts //= {};
50 21   100     63 my $truncate = $opts->{truncate} // 'bottom';
51 21   100     57 my $marker = $opts->{marker} // '..';
52 21   100     48 my $default_prio = $opts->{default_prio} // 1;
53              
54             # split into parts by priority
55 21         17 my @parts;
56             my @parts_attrs;
57 21         81 while ($str =~ m#]*)>(.*?)|(.*?)(?=
58 64 100       159 if (defined $1) {
    100          
    50          
59 11 50       15 next unless length $2;
60 11         12 push @parts, $2;
61 11         26 push @parts_attrs, $1;
62             } elsif (defined $3) {
63 11 50       20 next unless length $3;
64 11         13 push @parts, $3;
65 11         33 push @parts_attrs, undef;
66             } elsif (defined $4) {
67 42 100       102 next unless length $4;
68 21         27 push @parts, $4;
69 21         51 push @parts_attrs, undef;
70             }
71             }
72 21 100 66     82 return "" unless @parts && $len > 0;
73 19         40 for my $i (0..$#parts) {
74 39         86 $parts[$i] = [split /^/, $parts[$i]];
75 39 100       48 if (defined $parts_attrs[$i]) {
76 10         33 my $attrs = {};
77 10 50 0     15 $attrs->{truncate} = $1 // $2
78             if $parts_attrs[$i] =~ /\btruncate=(?:"([^"]*)"|(\S+))/;
79 10 50 0     13 $attrs->{marker} = $1 // $2
80             if $parts_attrs[$i] =~ /\bmarker=(?:"([^"]*)"|(\S+))/;
81 10 50 33     62 $attrs->{prio} = $1 // $2
82             if $parts_attrs[$i] =~ /\bprio(?:rity)?=(?:"([^"]*)"|(\S+))/;
83 10         15 $parts_attrs[$i] = $attrs;
84             } else {
85 29         52 $parts_attrs[$i] = {prio=>$default_prio};
86             }
87             }
88              
89             #use DD; dd \@parts; dd \@parts_attrs;
90              
91             # elide and truncate prio by prio until str is short enough
92             PRIO:
93 19         19 while (1) {
94             # (re)calculate total len of all parts
95 37         25 my $all_parts_len = 0;
96 37         64 $all_parts_len += @$_ for @parts;
97              
98             # total len of all parts is short enough, we're done
99 37 100       48 if ($all_parts_len <= $len) {
100 19         25 return join("", map { join "", @$_ } @parts);
  34         133  
101             }
102              
103             # we still need to elide some parts. first collect part indexes that
104             # have the current largest priority.
105 18         15 my $highest_prio;
106 18         16 for (@parts_attrs) {
107             $highest_prio = $_->{prio} if !defined($highest_prio) ||
108 37 100 66     101 $highest_prio < $_->{prio};
109             }
110 18         14 my @high_indexes;
111 18         16 my $high_parts_len = 0;
112 18         22 for my $i (0..$#parts_attrs) {
113 37 100       49 if ($parts_attrs[$i]{prio} == $highest_prio) {
114 29         19 $high_parts_len += @{ $parts[$i] };
  29         29  
115 29         30 push @high_indexes, $i;
116             }
117             }
118              
119 18 100       33 if ($all_parts_len - $high_parts_len >= $len) {
120             # we need to fully eliminate all the highest parts part then search
121             # for another set of parts
122 2         3 for (reverse @high_indexes) {
123 2         3 splice @parts, $_, 1;
124 2         3 splice @parts_attrs, $_, 1;
125 2         3 next PRIO;
126             }
127             }
128              
129             # elide all to-be-elided parts equally
130              
131             # after this position, must elide a total of this number of lines after
132             # this position
133 16         12 my @must_elide_total_len_after_this;
134 16         14 my $tot_to_elide = $all_parts_len - $len;
135 16         19 for my $i (0..$#high_indexes) {
136 26         47 $must_elide_total_len_after_this[$i] =
137             int( ($i+1)/@high_indexes * $tot_to_elide );
138             }
139             # calculate how many characters to truncate for each part
140 16         12 my $tot_already_elided = 0;
141 16         14 my $tot_still_to_elide = 0;
142 16         22 for my $i (reverse 0..$#high_indexes) {
143 26         37 my $idx = $high_indexes[$i];
144 26         18 my $part_len = @{ $parts[$idx] };
  26         22  
145 26         25 my $to_elide = $must_elide_total_len_after_this[$#high_indexes - $i] -
146             $tot_already_elided;
147 26 100       39 if ($to_elide <= 0) {
    100          
148             # leave this part alone
149             } elsif ($part_len <= $to_elide) {
150             # we need to eliminate this part
151 3         3 splice @parts, $idx, 1;
152 3         4 splice @parts_attrs, $idx, 1;
153 3         3 $tot_already_elided += $part_len;
154 3         10 $tot_still_to_elide += ($to_elide - $part_len);
155             } else {
156             $parts[$idx] = _elide_lines(
157             $parts[$idx],
158             $part_len - $to_elide,
159             $parts_attrs[$idx]{marker} // $marker,
160 21   33     98 $parts_attrs[$idx]{truncate} // $truncate,
      33        
161             );
162 21         24 $tot_already_elided += $to_elide;
163 21         33 $tot_still_to_elide = 0;
164             }
165             }
166              
167             } # while 1
168             }
169              
170             1;
171             # ABSTRACT: Elide lines from a string, with options
172              
173             __END__