File Coverage

blib/lib/String/Elide/Lines.pm
Criterion Covered Total %
statement 98 101 97.0
branch 39 48 81.2
condition 13 27 48.1
subroutine 6 6 100.0
pod 1 1 100.0
total 157 183 85.7


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