File Coverage

blib/lib/App/ansifold.pm
Criterion Covered Total %
statement 170 207 82.1
branch 59 90 65.5
condition 17 39 43.5
subroutine 30 36 83.3
pod 0 16 0.0
total 276 388 71.1


line stmt bran cond sub pod time code
1             package App::ansifold;
2             our $VERSION = "1.35";
3              
4 90     90   11295568 use v5.14;
  90         329  
5 90     90   403 use warnings;
  90         156  
  90         6012  
6              
7 90     90   1311 use open IO => 'utf8', ':std';
  90         1748  
  90         611  
8 90     90   56416 use Encode;
  90         1637111  
  90         10534  
9              
10 90     90   65938 use Pod::Usage;
  90         4888046  
  90         19650  
11 90     90   1010 use List::Util qw(min);
  90         200  
  90         6330  
12 90     90   56403 use Hash::Util qw(lock_keys);
  90         341044  
  90         762  
13 90     90   63576 use Text::ANSI::Fold qw(:constants);
  90         5366095  
  90         20396  
14 90     90   55817 use Text::ANSI::Fold::Util qw(ansi_width); {
  90         78577  
  90         9646  
15             Text::ANSI::Fold->configure(expand => 1);
16             }
17 90     90   755 use Unicode::EastAsianWidth;
  90         201  
  90         8015  
18 90     90   557 use Data::Dumper;
  90         161  
  90         4332  
19              
20 90     90   445 use Exporter 'import';
  90         113  
  90         27612  
21             our @EXPORT_OK = qw(ansifold ansiexpand ansicolrm ansicut);
22              
23             our $DEFAULT_WIDTH //= 72;
24             our $DEFAULT_SEPARATE //= "\n";
25             our $DEFAULT_EXPAND //= 0;
26             our $DEFAULT_COLRM //= 0;
27             our $DEFAULT_CUT //= 0;
28             our $DEFAULT_SPLITWIDE;
29              
30             sub ansifold {
31 0     0 0 0 __PACKAGE__->new->perform(@_);
32             }
33              
34             sub ansiexpand {
35 0     0 0 0 local $DEFAULT_WIDTH = -1;
36 0         0 local $DEFAULT_EXPAND = -1;
37 0         0 goto &ansifold;
38             }
39              
40             sub ansicolrm {
41 0     0 0 0 local $DEFAULT_SEPARATE = "";
42 0         0 local $DEFAULT_COLRM = 1;
43 0         0 goto &ansifold;
44             }
45              
46             sub ansicut {
47 0     0 0 0 local $DEFAULT_SEPARATE = "";
48 0         0 goto &ansifold;
49             }
50              
51 90     90   54381 use Getopt::EX::Hashed 'has'; {
  90         427083  
  90         745  
52              
53             Getopt::EX::Hashed->configure(DEFAULT => [ is => 'rw' ]);
54              
55             has width => ' w =s@ ' , default => [];
56             has boundary => ' =s ' , default => 'none';
57             has padding => ' :s ' , action => sub {
58             $_->padding = 1;
59             $_->padchar = $_[1] if $_[1] ne '';
60             };
61             has padchar => ' =s ' ;
62             has prefix => ' =s ' ;
63             has autoindent => ' =s ' ;
64             has keepindent => ' ! ' ;
65             has indentchar => ' =s ' , default => ' ';
66             has ambiguous => ' =s ' ;
67             has paragraph => ' p + ' , default => 0;
68             has refill => ' r + ' , default => 0;
69             has separate => ' =s ' , default => $DEFAULT_SEPARATE;
70             has linebreak => ' =s ' , alias => 'lb';
71             has runin => ' =i ' , min => 0, default => 4;
72             has runout => ' =i ' , min => 0, default => 4;
73             has runlen => ' =i ' , min => 0;
74             has margin => ' =i ' , min => 0;
75             has nonewline => ' n ' ;
76             has splitwide => ' :s ' , default => $DEFAULT_SPLITWIDE;
77 90     90   36640 has lefthalf => ' =s ' , default => "\N{LEFT HALF BLACK CIRCLE}";
  90         224  
  90         756  
78             has righthalf => ' =s ' , default => "\N{RIGHT HALF BLACK CIRCLE}";
79             has smart => ' s ! ' ;
80             has crmode => ' ! ' ;
81              
82             has '+crmode' => sub {
83             $_->{$_[0]} = $_[1];
84             $_->separate = "\r" if $_[1];
85             };
86             has expand => ' x :-1 ' , default => $DEFAULT_EXPAND;
87             has tabstop => ' =i ' , min => 1;
88             has tabhead => ' =s ' ;
89             has tabspace => ' =s ' ;
90             has tabstyle => ' =s ' ;
91             has discard => ' =s@ ' , default => [];
92             has colrm => ' ' , default => $DEFAULT_COLRM;
93             has cut => ' c =s ' ;
94             has debug => ' d ' ;
95             has help => ' h ' ;
96             has version => ' v ' ;
97              
98             has '+boundary' => any => [ qw(none word space) ];
99             has '+ambiguous' => any => [ qw(wide narrow) ] ;
100              
101             has '+help' => sub {
102             pod2usage
103             -verbose => 99,
104             -sections => [ qw(SYNOPSIS VERSION) ];
105             };
106              
107             has '+version' => sub {
108             print "Version: $VERSION\n";
109             exit;
110             };
111              
112             has '+nonewline' => sub {
113             $_->separate = "";
114             };
115              
116             has '+linebreak' =>
117             default => LINEBREAK_NONE,
118             action => sub {
119             my($name, $value) = @_;
120             $_->$name = do {
121             local $_ = $value;
122             my $v = LINEBREAK_NONE;
123             $v |= LINEBREAK_ALL if /all/i;
124             $v |= LINEBREAK_RUNIN if /runin/i;
125             $v |= LINEBREAK_RUNOUT if /runout/i;
126             $v;
127             };
128             };
129              
130             ### --runlen
131             has '+runlen' => sub {
132             $_->runin = $_->runout = $_[1];
133             };
134             # for backward compatibility, would be deplicated
135             has run => '=i';
136             has '+run' => sub {
137             $_->runin = $_->runout = $_[1];
138             };
139              
140             has '+smart' => sub {
141             my $smart = $_->{$_[0]} = $_[1];
142             ($_->boundary, $_->linebreak) = do {
143             if ($smart) {
144             ('word', LINEBREAK_ALL);
145             } else {
146             ('none', LINEBREAK_NONE);
147             }
148             };
149             };
150              
151             # internal use
152             has width_index => default => [];
153             has indent_pat => ;
154             has min_width => ;
155              
156 90     90   166033 } no Getopt::EX::Hashed;
  90         251  
  90         435  
157              
158             sub perform {
159 80     80 0 267977976 my $app = shift;
160 80         4834 local @ARGV = @_;
161 80         4913 $app->options->params->doit;
162 74         1396 return 0;
163             }
164              
165             sub options {
166 80     80 0 1364 my $app = shift;
167              
168 80         1387 for (@ARGV) {
169 243 50       17727 $_ = decode 'utf8', $_ unless utf8::is_utf8($_);
170             }
171              
172 90     90   72594 use Getopt::EX::Long qw(:DEFAULT ExConfigure Configure);
  90         9616953  
  90         65929  
173 80         8269 ExConfigure BASECLASS => [ __PACKAGE__, 'Getopt::EX' ];
174 80         19303 Configure "bundling";
175 80 100       13779 $app->getopt || pod2usage();
176              
177             ## --colrm
178 74 100       241465 if ($app->colrm) {
    100          
179 7         66 my @params;
180 7   66     343 while (@ARGV > 0 and $ARGV[0] =~ /^\d+$/) {
181 13         173 push @params, shift @ARGV;
182             }
183 7         353 @{$app->width} = colrm_to_width(@params);
  7         73  
184             }
185             ## --cut
186             elsif ($app->cut) {
187 6         143 @{$app->width} = cut_to_width($app->cut);
  6         36  
188             }
189              
190 74 50       3190 if ($app->expand > 0) {
191 0         0 $app->tabstop = $app->expand;
192             }
193              
194 74   33     956 $app->separate //= $DEFAULT_SEPARATE;
195              
196 90     90   57029 use charnames ':loose';
  90         75768  
  90         592  
197 74         874 for (@{$app}{qw(tabhead tabspace)}) {
  74         850  
198 148 50 33     978 defined && length > 1 or next;
199 0   0     0 $_ = charnames::string_vianame($_) || die "$_: invalid name\n";
200             }
201              
202 74 50       544 if (my $indent = $app->autoindent) {
203 0         0 $app->indent_pat = qr/$indent/;
204             }
205              
206 74 100       1340 if (defined (my $char = $app->splitwide)) {
207 4         42 $app->splitwide = 1;
208 4 50       120 if ($char =~ s/\A(\X)(\X)?//) {
209 0         0 $app->lefthalf = $1;
210 0   0     0 $app->righthalf = $2 // $1;
211             }
212             }
213              
214 74         4172 return $app;
215             }
216              
217             sub params {
218 74     74 0 493 my $app = shift;
219              
220 90     90   115679 use Getopt::EX::Numbers;
  90         142378  
  90         158331  
221 74         6446 my $numbers = Getopt::EX::Numbers->new;
222              
223 74         8471 my @width = do {
224             map {
225 135 100       3262 if (/^$/) { 0 } # empty
  26 50       477  
    0          
    0          
226 109         918 elsif (/^-?\d+$/) { $_ } # number
227             elsif (/^(-?[-\d:]+) (?:\{(\d+)\})? $/x) { # a:b:c:d{e}
228 0   0     0 ($numbers->parse($1)->sequence) x ($2 // 1);
229             }
230             elsif (/^=(.*)/) {
231 0         0 require Getopt::EX::RPN;
232 0         0 int Getopt::EX::RPN::rpn_calc(terminal_width(), $1);
233             }
234 0         0 else { die "$_: width format error.\n" }
235             }
236 66         734 map { split /,/, $_, -1 }
237 74         262 @{$app->width};
  74         584  
238             };
239              
240 74         412 $app->width = do {
241 74 100       668 if (@width == 0) { $DEFAULT_WIDTH }
  8 100       43  
242 26         198 elsif (@width == 1) { $width[0] }
243             else {
244 40         551 my @map = map [ abs $_ => $_ >= 0 ], @width;
245 40         154 $map[-1] = [ $width[-1] => $width[-1] ];
246 40         276 @width = map $_->[0], @map;
247 40         193 $app->width_index = [ grep { $map[$_][1] != 0 } keys @map ];
  109         728  
248 40         500 \@width;
249             }
250             };
251              
252 74 100       1548 $app->min_width = ref $app->width ? min @{$app->width} : $app->width;
  40         1062  
253              
254 74         5825 return $app;
255             }
256              
257             sub doit {
258 74     74 0 512 my $app = shift;
259              
260             my $fold = Text::ANSI::Fold->new(
261 680         7477 map { $_ => $app->$_ }
262 74         559 grep { defined $app->$_ }
  1406         9774  
263             qw(width boundary padding padchar prefix ambiguous
264             margin linebreak runin runout
265             splitwide lefthalf righthalf
266             expand tabstyle tabstop tabhead tabspace discard)
267             );
268              
269 74   33 1   27130 my $separator = eval sprintf(qq["%s"], $app->separate) // do {
  1         231  
  1         3  
  1         31  
270 0         0 warn $@ =~ s/ at .*//r;
271 0         0 $DEFAULT_SEPARATE;
272             };
273              
274 74         851 my @index = @{$app->width_index};
  74         470  
275              
276 74 50       928 local $/ = "\n\n" if $app->refill;
277 74         7170 while (<>) {
278 73 50 66     1379 if (not $app->padding and s/\A(\n+)//) {
279 0         0 print $1;
280 0 0       0 next if length == 0;
281             }
282             # chomp() does not remove single "\n" when $/ is "\n\n"
283 73 100       3620 my $chomped = s/(\R+)\z// ? $1 : '';
284 73 50       465 my $eol = $chomped =~ /\r/ ? "\r\n" : "\n";
285 73 50       351 fill_paragraph() if $app->refill;
286 73 100       968 fill_cr() if $app->crmode;
287 73 50 33     1098 if ($app->{indent_pat} && /^$app->{indent_pat}/p) {
288 0         0 my $matched = ${^MATCH};
289 0         0 my $indent = ansi_width $matched;
290 0 0       0 if ($indent >= $app->min_width) {
291 0         0 die sprintf("%s\n%s\n%s\n", $_, ("^" x $indent),
292             "ERROR: Autoindent pattern is longer than folding width.");
293             }
294 0         0 my $prefix = do {
295 0 0       0 if ($app->keepindent) {
296 0         0 $matched;
297             } else {
298 0         0 $app->indentchar x $indent;
299             }
300             };
301 0         0 $fold->configure(prefix => $prefix);
302             }
303 73         2196 my @chops = $fold->text($_)->chops;
304 73 100       1082400 if (ref $app->width eq 'ARRAY') {
305 40         515 while (my($i, $w) = each @{$app->width}) {
  149         464  
306 109 50 33     2650 $w = 0 if not $app->padding or $w < 0;
307 109   66     895 $chops[$i] //= ' ' x $w;
308             }
309             }
310 73 100       834 if (@index > 0) {
311 40         182 @chops = grep { defined } @chops[@index];
  65         243  
312             }
313 73         6093 print join $separator, @chops;
314 73 100       780 print $chomped if $chomped;
315 73 50       510 print $eol x $app->paragraph if $app->paragraph > 0;
316             }
317              
318 74         4998 return $app;
319             }
320              
321             sub InConcatScript {
322 14     14 0 1286 return <<"END";
323             +App::ansifold::InFullwidth
324             -utf8::Hangul
325             END
326             }
327              
328             sub InFullwidthPunctuation {
329 14     14 0 1306 return <<"END";
330             +App::ansifold::InFullwidth
331             &utf8::Punctuation
332             END
333             }
334              
335             sub fill_up {
336 18   33 18 0 81 my $pat = shift // qr/\R/;
337 18         552 s/(?<=\p{InFullwidthPunctuation})$pat//g;
338 18         15786 s/(?<=\p{InConcatScript})$pat(?=\p{InConcatScript})//g;
339 18         445 s/[ ]*$pat[ ]*/ /g;
340             }
341              
342             sub fill_paragraph {
343 0     0 0 0 fill_up(qr/\R/);
344             }
345              
346             sub fill_cr {
347 18     18 0 545 fill_up(qr/\r/);
348             }
349              
350             sub terminal_width {
351 90     90   52574 use Term::ReadKey;
  90         230875  
  90         78003  
352 0     0 0 0 my $default = 80;
353 0         0 my @size;
354 0 0       0 if (open my $tty, ">", "/dev/tty") {
355             # Term::ReadKey 2.31 on macOS 10.15 has a bug in argument handling
356             # and the latest version 2.38 fails to install.
357             # This code should work on both versions.
358 0         0 @size = GetTerminalSize $tty, $tty;
359             }
360 0 0       0 $size[0] or $default;
361             }
362              
363             sub colrm_to_width {
364 7     7 0 25 my @width;
365 7         75 my $pos = 0;
366 7         68 while (my($start, $end) = splice @_, 0, 2) {
367 8 50       38 $pos < $start or die "$start: invalid arg\n";
368 8         24 $start--;
369 8 100       125 push @width,
370             $start - $pos,
371             defined $end ? $start - $end : '';
372 8   100     44 $pos = $end // last;
373             }
374 7 100 100     178 push @width, -1 if @width == 0 or $width[-1] ne '';
375 7         49 join ',', @width;
376             }
377              
378             sub cut_to_width {
379 6     6 0 327 my $list = shift;
380 6         277 my @params = split /[\s,]+/, $list;
381 6         73 my @width;
382 6         20 my $pos = 1;
383 6         18 for (@params) {
384 11 50       103 next if $_ eq '';
385 11 50       381 my($start, $end) =
    100          
    100          
    100          
386             /^(\d+)$/ ? ( $1, $1 ) :
387             /^-(\d+)/ ? ( $pos, $1 ) :
388             /^(\d+)-$/ ? ( $1, -1 ) :
389             /^(\d+)-(\d+)$/ ? ( $1, $2 ) : die "$list: format error";
390 11 50       61 $pos <= $start or die "$start: invalid arg\n";
391 11 100       46 if ($start > $pos) {
392 7         18 push @width, $pos - $start;
393             }
394 11 100       31 if ($end < 0) {
395 1         12 push @width, -1;
396 1         3 last;
397             } else {
398 10         25 push @width, $end - $start + 1;
399             }
400 10         21 $pos = $end + 1;
401             }
402 6 100       21 push @width, 0 if $width[-1] != -1;
403 6         46 join ',', @width;
404             }
405              
406             1;
407              
408             __END__