File Coverage

blib/lib/App/ansifold.pm
Criterion Covered Total %
statement 122 141 86.5
branch 31 52 59.6
condition 5 16 31.2
subroutine 22 24 91.6
pod 0 6 0.0
total 180 239 75.3


line stmt bran cond sub pod time code
1             package App::ansifold;
2             our $VERSION = "1.18";
3              
4 51     51   3683198 use v5.14;
  51         531  
5 51     51   215 use warnings;
  51         88  
  51         1286  
6              
7 51     51   646 use open IO => 'utf8', ':std';
  51         1123  
  51         263  
8 51     51   26252 use Encode;
  51         393353  
  51         3022  
9              
10 51     51   20494 use Pod::Usage;
  51         1642246  
  51         6147  
11 51     51   414 use List::Util qw(min);
  51         95  
  51         4649  
12 51     51   24668 use Hash::Util qw(lock_keys);
  51         114749  
  51         255  
13 51     51   23898 use Text::ANSI::Fold qw(:constants);
  51         2553887  
  51         8656  
14 51     51   17529 use Text::ANSI::Fold::Util qw(ansi_width);
  51         28163  
  51         2516  
15 51     51   324 use Unicode::EastAsianWidth;
  51         98  
  51         2890  
16 51     51   248 use Data::Dumper;
  51         103  
  51         3537  
17              
18             our $DEFAULT_WIDTH //= 72;
19             our $DEFAULT_SEPARATE //= "\n";
20             our $DEFAULT_EXPAND //= 0;
21             our $DEFAULT_COLRM //= 0;
22              
23 51     51   20590 use Getopt::EX::Hashed 'has'; {
  51         145846  
  51         287  
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 51     51   28745 } no Getopt::EX::Hashed;
  51         118  
  51         210  
103              
104             sub run {
105 44     44 0 64452178 my $app = shift;
106 44         1196 local @ARGV = @_;
107 44         1434 $app->options->params->doit;
108 39         224 return 0;
109             }
110              
111             sub options {
112 44     44 0 438 my $app = shift;
113              
114 44         326 for (@ARGV) {
115 143 50       6389 $_ = decode 'utf8', $_ unless utf8::is_utf8($_);
116             }
117              
118 51     51   24465 use Getopt::EX::Long qw(:DEFAULT ExConfigure Configure);
  51         1508826  
  51         16570  
119 44         4013 ExConfigure BASECLASS => [ __PACKAGE__, 'Getopt::EX' ];
120 44         3661 Configure "bundling";
121 44 100       4642 $app->getopt || pod2usage();
122              
123 39 100       137923 if ($app->colrm) {
124 4         113 $app->separate = '';
125 4         34 @{$app->width} = do {
  4         18  
126 4 50 33     174 unless (@ARGV > 0 and $ARGV[0] =~ /^\d+$/) {
127 0         0 "-1";
128             } else {
129 4         43 my $start = shift(@ARGV) - 1;
130 4 100 66     74 if (@ARGV > 0 and $ARGV[0] =~ /^\d+$/) {
131 2         6 my $end = shift(@ARGV);
132 2         11 sprintf '%d,-%d,-1', $start, $end - $start;
133             } else {
134 2         11 sprintf '%d,', $start;
135             }
136             }
137             }
138             }
139              
140 39 50       1081 if ($app->expand > 0) {
141 0         0 $app->tabstop = $app->expand;
142             }
143              
144 51     51   21056 use charnames ':loose';
  51         28901  
  51         291  
145 39         548 for (@{$app}{qw(tabhead tabspace)}) {
  39         277  
146 78 50 33     517 defined && length > 1 or next;
147 0   0     0 $_ = charnames::string_vianame($_) || die "$_: invalid name\n";
148             }
149              
150 39 50       319 if (my $indent = $app->autoindent) {
151 0         0 $app->indent_pat = qr/$indent/;
152             }
153              
154 39         1013 return $app;
155             }
156              
157             sub params {
158 39     39 0 241 my $app = shift;
159              
160 51     51   36152 use Getopt::EX::Numbers;
  51         52095  
  51         38509  
161 39         1256 my $numbers = Getopt::EX::Numbers->new;
162              
163 39         3553 my @width = do {
164             map {
165 67 100       888 if (/^$/) { 0 } # empty
  17 50       67  
    0          
    0          
166 50         162 elsif (/^-?\d+$/) { $_ } # number
167             elsif (/^(-?[-\d:]+) (?:\{(\d+)\})? $/x) { # a:b:c:d{e}
168 0   0     0 ($numbers->parse($1)->sequence) x ($2 // 1);
169             }
170             elsif (/^(term|tty)$/) {
171 0         0 terminal_width();
172             }
173 0         0 else { die "$_: width format error.\n" }
174             }
175 32         321 map { split /,/, $_, -1 }
176 39         308 @{$app->width};
  39         307  
177             };
178              
179 39         190 $app->width = do {
180 39 100       206 if (@width == 0) { $DEFAULT_WIDTH }
  7 100       60  
181 8         49 elsif (@width == 1) { $width[0] }
182             else {
183 24         179 my @map = [ (int(pop @width)) x 2 ];
184 24 100       78 unshift @map, map { [ $_ < 0 ? (-$_, 0) : ($_, 1) ] } @width;
  35         302  
185 24         73 @width = map { $_->[0] } @map;
  59         149  
186 24         101 $app->width_index = [ grep { $map[$_][1] } 0 .. $#map ];
  59         255  
187 24         267 \@width;
188             }
189             };
190              
191 39         1063 return $app;
192             }
193              
194             sub doit {
195 39     39 0 327 my $app = shift;
196              
197             my $fold = Text::ANSI::Fold->new(
198 279         3218 map { $_ => $app->$_ }
199 39         209 grep { defined $app->$_ }
  585         3129  
200             qw(width boundary padding padchar prefix ambiguous
201             linebreak runin runout
202             expand tabstyle tabstop tabhead tabspace discard)
203             );
204              
205 39     1   8324 my $separator = eval sprintf qq["%s"], $app->separate;
  1         176  
  1         3  
  1         27  
206              
207 39         209 my @index = @{$app->width_index};
  39         175  
208              
209 39 50       350 local $/ = "\n\n" if $app->refill;
210 39         3108 while (<>) {
211 35 50       980 if (s/\A(\n+)//) {
212 0         0 print $1;
213 0 0       0 next if length == 0;
214             }
215             # chomp() does not remove single "\n" when $/ is "\n\n"
216 35 100       624 my $chomped = s/(\n+)\z// ? length $1 : 0;
217 35 50       211 fill_paragraph() if $app->refill;
218 35         243 my @opt;
219 35 50 33     132 if ($app->{indent_pat} && /^$app->{indent_pat}/p) {
220 0         0 my $indent = ansi_width ${^MATCH};
221 0         0 my $prefix = ' ' x $indent;
222 0         0 $fold->configure(prefix => $prefix);
223             }
224 35         690 my @chops = $fold->text($_)->chops;
225 35 100       594115 @chops = grep { defined } @chops[@index] if @index > 0;
  39         131  
226 35         1592 print join $separator, @chops;
227 35 100       429 print "\n" x $chomped if $chomped;
228 35 50       266 print "\n" x $app->paragraph if $app->paragraph > 0;
229             }
230              
231 39         1669 return $app;
232             }
233              
234             sub fill_paragraph {
235 0     0 0 0 s/(?<=\p{InFullwidth})\R(?=\p{InFullwidth})//g;
236 0         0 s/[ ]*\R[ ]*/ /g;
237             }
238              
239             sub terminal_width {
240 51     51   30653 use Term::ReadKey;
  51         82671  
  51         7412  
241 0     0 0 0 my $default = 80;
242 0         0 my @size;
243 0 0       0 if (open my $tty, ">", "/dev/tty") {
244             # Term::ReadKey 2.31 on macOS 10.15 has a bug in argument handling
245             # and the latest version 2.38 fails to install.
246             # This code should work on both versions.
247 0         0 @size = GetTerminalSize $tty, $tty;
248             }
249 0 0       0 $size[0] or $default;
250             }
251              
252             1;
253              
254             __END__