File Coverage

blib/lib/App/ansifold.pm
Criterion Covered Total %
statement 153 173 88.4
branch 50 74 67.5
condition 10 21 47.6
subroutine 24 26 92.3
pod 0 8 0.0
total 237 302 78.4


line stmt bran cond sub pod time code
1             package App::ansifold;
2             our $VERSION = "1.20";
3              
4 61     61   5849177 use v5.14;
  61         916  
5 61     61   448 use warnings;
  61         106  
  61         3553  
6              
7 61     61   1129 use open IO => 'utf8', ':std';
  61         1588  
  61         565  
8 61     61   48605 use Encode;
  61         621131  
  61         4453  
9              
10 61     61   35016 use Pod::Usage;
  61         2604421  
  61         8896  
11 61     61   595 use List::Util qw(min);
  61         122  
  61         7734  
12 61     61   36281 use Hash::Util qw(lock_keys);
  61         181939  
  61         399  
13 61     61   39352 use Text::ANSI::Fold qw(:constants);
  61         4051851  
  61         12115  
14 61     61   26150 use Text::ANSI::Fold::Util qw(ansi_width);
  61         44586  
  61         3865  
15 61     61   635 use Unicode::EastAsianWidth;
  61         155  
  61         4478  
16 61     61   425 use Data::Dumper;
  61         119  
  61         6147  
17              
18             our $DEFAULT_WIDTH //= 72;
19             our $DEFAULT_SEPARATE //= "\n";
20             our $DEFAULT_EXPAND //= 0;
21             our $DEFAULT_COLRM //= 0;
22             our $DEFAULT_CUT //= 0;
23              
24 61     61   33277 use Getopt::EX::Hashed 'has'; {
  61         234273  
  61         569  
25              
26             Getopt::EX::Hashed->configure(DEFAULT => [ is => 'rw' ]);
27              
28             has width => ' w =s@ ' , default => [];
29             has boundary => ' =s ' , default => 'none';
30             has padding => ' :s ' , action => sub {
31             $_->padding = 1;
32             $_->padchar = $_[1] if $_[1] ne '';
33             };
34             has padchar => ' =s ' ;
35             has prefix => ' =s ' ;
36             has autoindent => ' =s ' ;
37             has ambiguous => ' =s ' ;
38             has paragraph => ' p + ' , default => 0;
39             has refill => ' r + ' , default => 0;
40             has separate => ' =s ' , default => $DEFAULT_SEPARATE;
41             has linebreak => ' =s ' , alias => 'lb';
42             has runin => ' =i ' , min => 1, default => 4;
43             has runout => ' =i ' , min => 1, default => 4;
44             has nonewline => ' n ' ;
45             has smart => ' s ! ' ;
46             has expand => ' x :-1 ' , default => $DEFAULT_EXPAND;
47             has tabstop => ' =i ' , min => 1;
48             has tabhead => ' =s ' ;
49             has tabspace => ' =s ' ;
50             has tabstyle => ' =s ' ;
51             has discard => ' =s@ ' , default => [];
52             has colrm => ' ' , default => $DEFAULT_COLRM;
53             has cut => ' c =s ' ;
54             has debug => ' d ' ;
55             has help => ' h ' ;
56             has version => ' v ' ;
57              
58             has '+boundary' => any => [ qw(none word space) ];
59             has '+ambiguous' => any => [ qw(wide narrow) ] ;
60              
61             has '+help' => sub {
62             pod2usage
63             -verbose => 99,
64             -sections => [ qw(SYNOPSIS VERSION) ];
65             };
66              
67             has '+version' => sub {
68             print "Version: $VERSION\n";
69             exit;
70             };
71              
72             has '+nonewline' => sub {
73             $_->separate = "";
74             };
75              
76             has '+linebreak' =>
77             default => LINEBREAK_NONE,
78             action => sub {
79             my($name, $value) = @_;
80             $_->$name = do {
81             local $_ = $value;
82             my $v = LINEBREAK_NONE;
83             $v |= LINEBREAK_ALL if /all/i;
84             $v |= LINEBREAK_RUNIN if /runin/i;
85             $v |= LINEBREAK_RUNOUT if /runout/i;
86             $v;
87             };
88             };
89              
90             has '+smart' => sub {
91             my $smart = $_->{$_[0]} = $_[1];
92             ($_->boundary, $_->linebreak) = do {
93             if ($smart) {
94             ('word', LINEBREAK_ALL);
95             } else {
96             ('none', LINEBREAK_NONE);
97             }
98             };
99             };
100              
101             # internal use
102             has width_index => default => [];
103             has indent_pat => ;
104              
105 61     61   49426 } no Getopt::EX::Hashed;
  61         643  
  61         455  
106              
107             sub run {
108 53     53 0 109868022 my $app = shift;
109 53         2774 local @ARGV = @_;
110 53         3123 $app->options->params->doit;
111 48         283 return 0;
112             }
113              
114             sub options {
115 53     53 0 866 my $app = shift;
116              
117 53         1033 for (@ARGV) {
118 163 50       16490 $_ = decode 'utf8', $_ unless utf8::is_utf8($_);
119             }
120              
121 61     61   38717 use Getopt::EX::Long qw(:DEFAULT ExConfigure Configure);
  61         2346627  
  61         22678  
122 53         8600 ExConfigure BASECLASS => [ __PACKAGE__, 'Getopt::EX' ];
123 53         9931 Configure "bundling";
124 53 100       11561 $app->getopt || pod2usage();
125              
126             ## --colrm
127 48 100       319245 if ($app->colrm) {
    100          
128 7         104 $app->separate = '';
129 7         310 my @params;
130 7   66     221 while (@ARGV > 0 and $ARGV[0] =~ /^\d+$/) {
131 13         186 push @params, shift @ARGV;
132             }
133 7         304 @{$app->width} = colrm_to_width(@params);
  7         90  
134             }
135             ## --cut
136             elsif ($app->cut) {
137 6         196 $app->separate = '';
138 6         212 @{$app->width} = cut_to_width($app->cut);
  6         60  
139             }
140              
141 48 50       1791 if ($app->expand > 0) {
142 0         0 $app->tabstop = $app->expand;
143             }
144              
145 61     61   33594 use charnames ':loose';
  61         43352  
  61         441  
146 48         1083 for (@{$app}{qw(tabhead tabspace)}) {
  48         292  
147 96 50 33     1217 defined && length > 1 or next;
148 0   0     0 $_ = charnames::string_vianame($_) || die "$_: invalid name\n";
149             }
150              
151 48 50       613 if (my $indent = $app->autoindent) {
152 0         0 $app->indent_pat = qr/$indent/;
153             }
154              
155 48         1767 return $app;
156             }
157              
158             sub params {
159 48     48 0 556 my $app = shift;
160              
161 61     61   56225 use Getopt::EX::Numbers;
  61         83427  
  61         64242  
162 48         1963 my $numbers = Getopt::EX::Numbers->new;
163              
164 48         6291 my @width = do {
165             map {
166 100 100       1744 if (/^$/) { 0 } # empty
  18 50       132  
    0          
    0          
167 82         570 elsif (/^-?\d+$/) { $_ } # number
168             elsif (/^(-?[-\d:]+) (?:\{(\d+)\})? $/x) { # a:b:c:d{e}
169 0   0     0 ($numbers->parse($1)->sequence) x ($2 // 1);
170             }
171             elsif (/^(term|tty)$/) {
172 0         0 terminal_width();
173             }
174 0         0 else { die "$_: width format error.\n" }
175             }
176 41         596 map { split /,/, $_, -1 }
177 48         215 @{$app->width};
  48         372  
178             };
179              
180 48         555 $app->width = do {
181 48 100       798 if (@width == 0) { $DEFAULT_WIDTH }
  7 100       83  
182 9         101 elsif (@width == 1) { $width[0] }
183             else {
184 32         383 my @map = [ (int(pop @width)) x 2 ];
185 32 100       188 unshift @map, map { [ $_ < 0 ? (-$_, 0) : ($_, 1) ] } @width;
  59         357  
186 32         132 @width = map { $_->[0] } @map;
  91         339  
187 32         209 $app->width_index = [ grep { $map[$_][1] } 0 .. $#map ];
  91         307  
188 32         405 \@width;
189             }
190             };
191              
192 48         2110 return $app;
193             }
194              
195             sub doit {
196 48     48 0 271 my $app = shift;
197              
198             my $fold = Text::ANSI::Fold->new(
199 342         4962 map { $_ => $app->$_ }
200 48         631 grep { defined $app->$_ }
  720         4575  
201             qw(width boundary padding padchar prefix ambiguous
202             linebreak runin runout
203             expand tabstyle tabstop tabhead tabspace discard)
204             );
205              
206 48   33 1   13771 my $separator = eval sprintf(qq["%s"], $app->separate) // do {
  1         197  
  1         4  
  1         42  
207 0         0 warn $@ =~ s/ at .*//r;
208 0         0 $DEFAULT_SEPARATE;
209             };
210              
211 48         403 my @index = @{$app->width_index};
  48         311  
212              
213 48 50       582 local $/ = "\n\n" if $app->refill;
214 48         5249 while (<>) {
215 44 50       1632 if (s/\A(\n+)//) {
216 0         0 print $1;
217 0 0       0 next if length == 0;
218             }
219             # chomp() does not remove single "\n" when $/ is "\n\n"
220 44 100       744 my $chomped = s/(\n+)\z// ? length $1 : 0;
221 44 50       248 fill_paragraph() if $app->refill;
222 44         805 my @opt;
223 44 50 33     620 if ($app->{indent_pat} && /^$app->{indent_pat}/p) {
224 0         0 my $indent = ansi_width ${^MATCH};
225 0         0 my $prefix = ' ' x $indent;
226 0         0 $fold->configure(prefix => $prefix);
227             }
228 44         1337 my @chops = $fold->text($_)->chops;
229 44 100       941847 @chops = grep { defined } @chops[@index] if @index > 0;
  55         313  
230 44         2642 print join $separator, @chops;
231 44 100       766 print "\n" x $chomped if $chomped;
232 44 50       986 print "\n" x $app->paragraph if $app->paragraph > 0;
233             }
234              
235 48         2148 return $app;
236             }
237              
238             sub fill_paragraph {
239 0     0 0 0 s/(?<=\p{InFullwidth})\R(?=\p{InFullwidth})//g;
240 0         0 s/[ ]*\R[ ]*/ /g;
241             }
242              
243             sub terminal_width {
244 61     61   70681 use Term::ReadKey;
  61         157008  
  61         45720  
245 0     0 0 0 my $default = 80;
246 0         0 my @size;
247 0 0       0 if (open my $tty, ">", "/dev/tty") {
248             # Term::ReadKey 2.31 on macOS 10.15 has a bug in argument handling
249             # and the latest version 2.38 fails to install.
250             # This code should work on both versions.
251 0         0 @size = GetTerminalSize $tty, $tty;
252             }
253 0 0       0 $size[0] or $default;
254             }
255              
256             sub colrm_to_width {
257 7     7 0 106 my @width;
258 7         37 my $pos = 0;
259 7         95 while (my($start, $end) = splice @_, 0, 2) {
260 8 50       82 $pos < $start or die "$start: invalid arg\n";
261 8         79 $start--;
262 8 100       191 push @width,
263             $start - $pos,
264             defined $end ? $start - $end : '';
265 8   100     53 $pos = $end // last;
266             }
267 7 100 100     226 push @width, -1 if @width == 0 or $width[-1] ne '';
268 7         57 join ',', @width;
269             }
270              
271             sub cut_to_width {
272 6     6 0 209 my $list = shift;
273 6         211 my @params = split /[\s,]+/, $list;
274 6         68 my @width;
275 6         24 my $pos = 1;
276 6         47 while (my $col = shift @params) {
277 11 50       125 next if $col eq '';
278 11         41 my($start, $end);
279 11 100       232 if ($col =~ /^(\d+)$/) { ($start, $end) = ($1, $1); }
  6 100       32  
    100          
    50          
280 1         21 elsif ($col =~ /^-(\d+)/) { ($start, $end) = ($pos, $1); }
281 1         7 elsif ($col =~ /^(\d+)-$/) { ($start, $end) = ($1, -1); }
282 3         15 elsif ($col =~ /^(\d+)-(\d+)$/) { ($start, $end) = ($1, $2); }
283 11 50       164 $pos <= $start or die "$start: invalid arg\n";
284 11 100       116 if ($start > $pos) {
285 7         32 push @width, $pos - $start;
286             }
287 11 100       39 if ($end < 0) {
288 1         3 push @width, -1;
289 1         3 last;
290             } else {
291 10         31 push @width, $end - $start + 1;
292             }
293 10         93 $pos = $end + 1;
294             }
295 6 100       88 push @width, 0 if $width[-1] != -1;
296 6         100 join ',', @width;
297             }
298              
299             1;
300              
301             __END__