File Coverage

blib/lib/App/ansifold.pm
Criterion Covered Total %
statement 127 147 86.3
branch 33 54 61.1
condition 10 21 47.6
subroutine 22 24 91.6
pod 0 6 0.0
total 192 252 76.1


line stmt bran cond sub pod time code
1             package App::ansifold;
2             our $VERSION = "1.19";
3              
4 54     54   4969604 use v5.14;
  54         661  
5 54     54   303 use warnings;
  54         110  
  54         1757  
6              
7 54     54   860 use open IO => 'utf8', ':std';
  54         1345  
  54         300  
8 54     54   35256 use Encode;
  54         533607  
  54         3948  
9              
10 54     54   29934 use Pod::Usage;
  54         2241174  
  54         8134  
11 54     54   553 use List::Util qw(min);
  54         153  
  54         6145  
12 54     54   32019 use Hash::Util qw(lock_keys);
  54         157009  
  54         536  
13 54     54   33142 use Text::ANSI::Fold qw(:constants);
  54         3405035  
  54         10203  
14 54     54   23114 use Text::ANSI::Fold::Util qw(ansi_width);
  54         38349  
  54         3504  
15 54     54   420 use Unicode::EastAsianWidth;
  54         148  
  54         3655  
16 54     54   342 use Data::Dumper;
  54         128  
  54         4886  
17              
18             our $DEFAULT_WIDTH //= 72;
19             our $DEFAULT_SEPARATE //= "\n";
20             our $DEFAULT_EXPAND //= 0;
21             our $DEFAULT_COLRM //= 0;
22              
23 54     54   28672 use Getopt::EX::Hashed 'has'; {
  54         198607  
  54         332  
24              
25             Getopt::EX::Hashed->configure(DEFAULT => [ is => 'rw' ]);
26              
27             has width => ' w =s@ ' , default => [];
28             has boundary => ' =s ' , default => 'none';
29             has padding => ' :s ' , action => sub {
30             $_->padding = 1;
31             $_->padchar = $_[1] if $_[1] ne '';
32             };
33             has padchar => ' =s ' ;
34             has prefix => ' =s ' ;
35             has autoindent => ' =s ' ;
36             has ambiguous => ' =s ' ;
37             has paragraph => ' p + ' , default => 0;
38             has refill => ' r + ' , default => 0;
39             has separate => ' =s ' , default => $DEFAULT_SEPARATE;
40             has linebreak => ' =s ' , alias => 'lb';
41             has runin => ' =i ' , min => 1, default => 4;
42             has runout => ' =i ' , min => 1, default => 4;
43             has nonewline => ' n ' ;
44             has smart => ' s ! ' ;
45             has expand => ' x :-1 ' , default => $DEFAULT_EXPAND;
46             has tabstop => ' =i ' , min => 1;
47             has tabhead => ' =s ' ;
48             has tabspace => ' =s ' ;
49             has tabstyle => ' =s ' ;
50             has discard => ' =s@ ' , default => [];
51             has colrm => ' ' , default => $DEFAULT_COLRM;
52             has help => ' h ' ;
53             has version => ' v ' ;
54              
55             has '+boundary' => any => [ qw(none word space) ];
56             has '+ambiguous' => any => [ qw(wide narrow) ] ;
57              
58             has '+help' => sub {
59             pod2usage
60             -verbose => 99,
61             -sections => [ qw(SYNOPSIS VERSION) ];
62             };
63              
64             has '+version' => sub {
65             print "Version: $VERSION\n";
66             exit;
67             };
68              
69             has '+nonewline' => sub {
70             $_->separate = "";
71             };
72              
73             has '+linebreak' =>
74             default => LINEBREAK_NONE,
75             action => sub {
76             my($name, $value) = @_;
77             $_->$name = do {
78             local $_ = $value;
79             my $v = LINEBREAK_NONE;
80             $v |= LINEBREAK_ALL if /all/i;
81             $v |= LINEBREAK_RUNIN if /runin/i;
82             $v |= LINEBREAK_RUNOUT if /runout/i;
83             $v;
84             };
85             };
86              
87             has '+smart' => sub {
88             my $smart = $_->{$_[0]} = $_[1];
89             ($_->boundary, $_->linebreak) = do {
90             if ($smart) {
91             ('word', LINEBREAK_ALL);
92             } else {
93             ('none', LINEBREAK_NONE);
94             }
95             };
96             };
97              
98             # internal use
99             has width_index => default => [];
100             has indent_pat => ;
101              
102 54     54   39917 } no Getopt::EX::Hashed;
  54         130  
  54         238  
103              
104             sub run {
105 47     47 0 89329903 my $app = shift;
106 47         1716 local @ARGV = @_;
107 47         2077 $app->options->params->doit;
108 42         289 return 0;
109             }
110              
111             sub options {
112 47     47 0 845 my $app = shift;
113              
114 47         338 for (@ARGV) {
115 151 50       9032 $_ = decode 'utf8', $_ unless utf8::is_utf8($_);
116             }
117              
118 54     54   33733 use Getopt::EX::Long qw(:DEFAULT ExConfigure Configure);
  54         2020112  
  54         23909  
119 47         6288 ExConfigure BASECLASS => [ __PACKAGE__, 'Getopt::EX' ];
120 47         5876 Configure "bundling";
121 47 100       7434 $app->getopt || pod2usage();
122              
123 42 100       217863 if ($app->colrm) {
124 7         298 $app->separate = '';
125 7         165 @{$app->width} = do {
  7         140  
126 7         145 my @params;
127             my @width;
128 7   66     299 while (@ARGV > 0 and $ARGV[0] =~ /^\d+$/) {
129 13         173 push @params, shift @ARGV;
130             }
131 7         56 my $pos = 0;
132 7         88 while (my($start, $end) = splice @params, 0, 2) {
133 8 50       34 $pos < $start or die "$start: invalid arg\n";
134 8         56 $start--;
135 8 100       166 push @width,
136             $start - $pos,
137             defined $end ? $start - $end : '';
138 8   100     145 $pos = $end // last;
139             }
140 7 100 100     191 push @width, -1 if @width == 0 or $width[-1] ne '';
141 7         65 join ',', @width;
142             }
143             }
144              
145 42 50       1518 if ($app->expand > 0) {
146 0         0 $app->tabstop = $app->expand;
147             }
148              
149 54     54   28211 use charnames ':loose';
  54         39961  
  54         418  
150 42         699 for (@{$app}{qw(tabhead tabspace)}) {
  42         370  
151 84 50 33     609 defined && length > 1 or next;
152 0   0     0 $_ = charnames::string_vianame($_) || die "$_: invalid name\n";
153             }
154              
155 42 50       285 if (my $indent = $app->autoindent) {
156 0         0 $app->indent_pat = qr/$indent/;
157             }
158              
159 42         1493 return $app;
160             }
161              
162             sub params {
163 42     42 0 600 my $app = shift;
164              
165 54     54   45990 use Getopt::EX::Numbers;
  54         67894  
  54         54897  
166 42         1361 my $numbers = Getopt::EX::Numbers->new;
167              
168 42         4133 my @width = do {
169             map {
170 77 100       1563 if (/^$/) { 0 } # empty
  18 50       212  
    0          
    0          
171 59         383 elsif (/^-?\d+$/) { $_ } # number
172             elsif (/^(-?[-\d:]+) (?:\{(\d+)\})? $/x) { # a:b:c:d{e}
173 0   0     0 ($numbers->parse($1)->sequence) x ($2 // 1);
174             }
175             elsif (/^(term|tty)$/) {
176 0         0 terminal_width();
177             }
178 0         0 else { die "$_: width format error.\n" }
179             }
180 35         414 map { split /,/, $_, -1 }
181 42         195 @{$app->width};
  42         323  
182             };
183              
184 42         283 $app->width = do {
185 42 100       475 if (@width == 0) { $DEFAULT_WIDTH }
  7 100       61  
186 9         49 elsif (@width == 1) { $width[0] }
187             else {
188 26         181 my @map = [ (int(pop @width)) x 2 ];
189 26 100       122 unshift @map, map { [ $_ < 0 ? (-$_, 0) : ($_, 1) ] } @width;
  42         268  
190 26         89 @width = map { $_->[0] } @map;
  68         235  
191 26         140 $app->width_index = [ grep { $map[$_][1] } 0 .. $#map ];
  68         261  
192 26         392 \@width;
193             }
194             };
195              
196 42         1391 return $app;
197             }
198              
199             sub doit {
200 42     42 0 154 my $app = shift;
201              
202             my $fold = Text::ANSI::Fold->new(
203 300         4014 map { $_ => $app->$_ }
204 42         254 grep { defined $app->$_ }
  630         3661  
205             qw(width boundary padding padchar prefix ambiguous
206             linebreak runin runout
207             expand tabstyle tabstop tabhead tabspace discard)
208             );
209              
210 42   33 1   10892 my $separator = eval sprintf(qq["%s"], $app->separate) // do {
  1         219  
  1         3  
  1         30  
211 0         0 warn $@ =~ s/ at .*//r;
212 0         0 $DEFAULT_SEPARATE;
213             };
214              
215 42         237 my @index = @{$app->width_index};
  42         668  
216              
217 42 50       465 local $/ = "\n\n" if $app->refill;
218 42         4125 while (<>) {
219 38 50       1063 if (s/\A(\n+)//) {
220 0         0 print $1;
221 0 0       0 next if length == 0;
222             }
223             # chomp() does not remove single "\n" when $/ is "\n\n"
224 38 100       543 my $chomped = s/(\n+)\z// ? length $1 : 0;
225 38 50       247 fill_paragraph() if $app->refill;
226 38         357 my @opt;
227 38 50 33     185 if ($app->{indent_pat} && /^$app->{indent_pat}/p) {
228 0         0 my $indent = ansi_width ${^MATCH};
229 0         0 my $prefix = ' ' x $indent;
230 0         0 $fold->configure(prefix => $prefix);
231             }
232 38         1272 my @chops = $fold->text($_)->chops;
233 38 100       732944 @chops = grep { defined } @chops[@index] if @index > 0;
  44         332  
234 38         2360 print join $separator, @chops;
235 38 100       522 print "\n" x $chomped if $chomped;
236 38 50       273 print "\n" x $app->paragraph if $app->paragraph > 0;
237             }
238              
239 42         1820 return $app;
240             }
241              
242             sub fill_paragraph {
243 0     0 0 0 s/(?<=\p{InFullwidth})\R(?=\p{InFullwidth})//g;
244 0         0 s/[ ]*\R[ ]*/ /g;
245             }
246              
247             sub terminal_width {
248 54     54   43964 use Term::ReadKey;
  54         110198  
  54         10271  
249 0     0 0 0 my $default = 80;
250 0         0 my @size;
251 0 0       0 if (open my $tty, ">", "/dev/tty") {
252             # Term::ReadKey 2.31 on macOS 10.15 has a bug in argument handling
253             # and the latest version 2.38 fails to install.
254             # This code should work on both versions.
255 0         0 @size = GetTerminalSize $tty, $tty;
256             }
257 0 0       0 $size[0] or $default;
258             }
259              
260             1;
261              
262             __END__