File Coverage

blib/lib/Text/ANSI/Fold.pm
Criterion Covered Total %
statement 240 256 93.7
branch 114 138 82.6
condition 43 57 75.4
subroutine 37 37 100.0
pod 0 19 0.0
total 434 507 85.6


line stmt bran cond sub pod time code
1             package Text::ANSI::Fold;
2              
3 12     12   792910 use v5.14;
  12         172  
4 12     12   68 use warnings;
  12         32  
  12         290  
5 12     12   1235 use utf8;
  12         45  
  12         61  
6              
7             our $VERSION = "2.2101";
8              
9 12     12   5403 use Data::Dumper;
  12         57109  
  12         807  
10             $Data::Dumper::Sortkeys = 1;
11 12     12   100 use Carp;
  12         44  
  12         747  
12 12     12   83 use List::Util qw(pairmap pairgrep);
  12         21  
  12         1241  
13 12     12   85 use Scalar::Util qw(looks_like_number);
  12         20  
  12         616  
14 12     12   5364 use Text::VisualWidth::PP 'vwidth';
  12         296042  
  12         1594  
15 23     23 0 173 sub pwidth { vwidth $_[0] =~ s/\X\cH{1,2}//gr }
16              
17             ######################################################################
18 12     12   117 use Exporter 'import';
  12         25  
  12         1964  
19             our %EXPORT_TAGS = (
20             constants => [ qw(
21             &LINEBREAK_NONE
22             &LINEBREAK_ALL
23             &LINEBREAK_RUNIN
24             &LINEBREAK_RUNOUT
25             ) ],
26             regex => [ qw(
27             $reset_re
28             $color_re
29             $erase_re
30             $csi_re
31             $osc_re
32             ) ],
33             );
34              
35             our @EXPORT_OK = ( qw(&ansi_fold),
36             @{$EXPORT_TAGS{constants}},
37             @{$EXPORT_TAGS{regex}},
38             );
39              
40             sub ansi_fold {
41 422     422 0 140865 my($text, $width, @option) = @_;
42 422         1372 __PACKAGE__->fold($text, width => $width, @option);
43             }
44             ######################################################################
45              
46             our $alphanum_re = qr{ [_\d\p{Latin}\p{Greek}\p{Cyrillic}\p{Hangul}] }x;
47             our $nonspace_re = qr{ \p{IsPrintableLatin} }x;
48             our $reset_re = qr{ \e \[ [0;]* m }x;
49             our $color_re = qr{ \e \[ [\d;]* m }x;
50             our $erase_re = qr{ \e \[ [\d;]* K }x;
51             our $csi_re = qr{
52             # see ECMA-48 5.4 Control sequences
53             (?: \e\[ | \x9b ) # csi
54             [\x30-\x3f]* # parameter bytes
55             [\x20-\x2f]* # intermediate bytes
56             [\x40-\x7e] # final byte
57             }x;
58             our $osc_re = qr{
59             # see ECMA-48 8.3.89 OSC - OPERATING SYSTEM COMMAND
60             (?: \e\] | \x9d ) # osc
61             [\x08-\x13\x20-\x7d]*+ # command
62             (?: \e\\ | \x9c | \a ) # st: string terminator
63             }x;
64              
65 12     12   28645 use constant SGR_RESET => "\e[m";
  12         29  
  12         1943  
66              
67             sub IsPrintableLatin {
68 2     2 0 1125 return <<"END";
69             +utf8::ASCII
70             +utf8::Latin
71             -utf8::White_Space
72             END
73             }
74              
75             sub IsWideSpacing {
76 5     5 0 762 return <<"END";
77             +utf8::East_Asian_Width=Wide
78             +utf8::East_Asian_Width=FullWidth
79             -utf8::Nonspacing_Mark
80             END
81             }
82              
83             sub IsWideAmbiguousSpacing {
84 1     1 0 128 return <<"END";
85             +utf8::East_Asian_Width=Wide
86             +utf8::East_Asian_Width=FullWidth
87             +utf8::East_Asian_Width=Ambiguous
88             -utf8::Nonspacing_Mark
89             END
90             }
91              
92             sub _startWideSpacing {
93             # look at $_
94 372 100   372   736 if ($Text::VisualWidth::PP::EastAsian) {
95 2         17 /^\p{IsWideAmbiguousSpacing}/;
96             } else {
97 370         1643 /^\p{IsWideSpacing}/;
98             }
99             }
100              
101             use constant {
102 12         1777 LINEBREAK_NONE => 0,
103             LINEBREAK_RUNIN => 1,
104             LINEBREAK_RUNOUT => 2,
105             LINEBREAK_ALL => 3,
106 12     12   2864 };
  12         26  
107              
108             our $DEFAULT_LINEBREAK = LINEBREAK_NONE;
109             our $DEFAULT_RUNIN_WIDTH = 2;
110             our $DEFAULT_RUNOUT_WIDTH = 2;
111              
112             BEGIN {
113 12 50   12   1062 if ($] < 5.016) {
114 0         0 require charnames;
115 0         0 charnames->import(':full');
116             }
117             }
118              
119             our %TABSTYLE = (
120             pairmap {
121             ( $a =~ s/_/-/gr => ref $b ? $b : [ $b, $b ] );
122             }
123 12     12   6444 symbol => [ "\N{SYMBOL FOR HORIZONTAL TABULATION}", # ␉
  12         102649  
  12         82  
124             "\N{SYMBOL FOR SPACE}" ], # ␠
125             shade => [ "\N{MEDIUM SHADE}", # ▒
126             "\N{LIGHT SHADE}" ], # ░
127             block => [ "\N{LOWER ONE QUARTER BLOCK}", # ▂
128             "\N{LOWER ONE EIGHTH BLOCK}" ], # ▁
129             bar => [ "\N{BOX DRAWINGS HEAVY RIGHT}", # ╺
130             "\N{BOX DRAWINGS LIGHT HORIZONTAL}" ], # ─
131             dash => [ "\N{BOX DRAWINGS HEAVY RIGHT}", # ╺
132             "\N{BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL}" ], # ╌
133              
134             dot => '.',
135             space => ' ',
136             emspace => "\N{EM SPACE}", #  
137             middle_dot => "\N{MIDDLE DOT}", # ·
138             arrow => "\N{RIGHTWARDS ARROW}", # →
139             double_arrow => "\N{RIGHTWARDS DOUBLE ARROW}", # ⇒
140             triple_arrow => "\N{RIGHTWARDS TRIPLE ARROW}", # ⇛
141             white_arrow => "\N{RIGHTWARDS WHITE ARROW}", # ⇨
142             wave_arrow => "\N{RIGHTWARDS WAVE ARROW}", # ↝
143             circle_arrow => "\N{CIRCLED HEAVY WHITE RIGHTWARDS ARROW}", # ➲
144             curved_arrow => "\N{HEAVY BLACK CURVED DOWNWARDS AND RIGHTWARDS ARROW}",# ➥
145             shadow_arrow => "\N{HEAVY UPPER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW}",# ➮
146             squat_arrow => "\N{SQUAT BLACK RIGHTWARDS ARROW}", # ➧
147             squiggle => "\N{RIGHTWARDS SQUIGGLE ARROW}", # ⇝
148             harpoon => "\N{RIGHTWARDS HARPOON WITH BARB UPWARDS}", # ⇀
149             cuneiform => "\N{CUNEIFORM SIGN TAB}", # 𒋰
150              
151             );
152              
153             my @default = (
154             text => '',
155             width => undef,
156             padding => 0,
157             boundary => '',
158             padchar => ' ',
159             prefix => '',
160             ambiguous => 'narrow',
161             margin => 0,
162             linebreak => $DEFAULT_LINEBREAK,
163             runin => $DEFAULT_RUNIN_WIDTH,
164             runout => $DEFAULT_RUNOUT_WIDTH,
165             expand => 0,
166             tabstop => 8,
167             tabhead => ' ',
168             tabspace => ' ',
169             discard => {},
170             );
171              
172             sub new {
173 13     13 0 1781 my $class = shift;
174 13         220 my $obj = bless { @default }, $class;
175 13 100       80 $obj->configure(@_) if @_;
176 13         43 $obj;
177             }
178              
179             INTERNAL_METHODS: {
180             sub spawn {
181 619     619 0 905 my $obj = shift;
182 619         996 my $class = ref $obj;
183 619     658   5308 my %new = ( %$obj, pairgrep { defined $b } @_ );
  658         5474  
184 619         3017 bless \%new, $class;
185             }
186 619 100   619 0 1877 sub do_runin { $_[0]->{linebreak} & LINEBREAK_RUNIN && $_[0]->{runin} > 0 }
187 523 100   523 0 2330 sub do_runout { $_[0]->{linebreak} & LINEBREAK_RUNOUT && $_[0]->{runout} > 0 }
188             }
189              
190 12     12   228882 use Text::ANSI::Fold::Japanese::W3C qw(%prohibition);
  12         37  
  12         11792  
191              
192             sub chars_to_regex {
193 24     24 0 7291 my $chars = join '', @_;
194 24         55 my($c, @s);
195 24         1136 for ($chars =~ /\X/g) {
196 1860 100       3204 if (length == 1) {
197 1848         2544 $c .= $_;
198             } else {
199 12         38 push @s, $_;
200             }
201             }
202 24 100       257 if (@s) {
203 12         33 local $" = '|';
204 12         935 qr/(?:[\Q$c\E]|@s)/;
205             } else {
206 12         410 qr/[\Q$c\E]/;
207             }
208             }
209              
210             my %prohibition_re = do {
211             head => do {
212             my $re = chars_to_regex @prohibition{qw(head postfix)};
213             qr/(?: $re | \p{Space_Separator} )/x;
214             },
215             end => chars_to_regex @prohibition{qw(end prefix)},
216             };
217              
218             sub configure {
219 47     47 0 13701 my $obj = shift;
220 47 100       150 if (not ref $obj) {
221 12         40 $obj = state $private = __PACKAGE__->new;
222             }
223 47 50       176 croak "invalid parameter" if @_ % 2;
224 47         151 while (@_ >= 2) {
225 71         236 my($a, $b) = splice @_, 0, 2;
226              
227 71 100       162 if ($a eq 'tabstyle') {
228 1   50     6 $b // next;
229 1 50       10 my($h, $s) = $b =~ /([-\w]+)/g or croak "$b: invalid tabstyle";
230 1   33     9 $s ||= $h;
231             my %style = (
232             h => ($TABSTYLE{$h} or croak "$h: invalid tabstyle"),
233 1   33     17 s => ($TABSTYLE{$s} or croak "$s: invalid tabstyle"),
      33        
234             );
235             unshift @_,
236             tabhead => $style{h}->[0],
237 1         7 tabspace => $style{s}->[1];
238 1         4 next;
239             }
240              
241 70 50       230 croak "$a: invalid parameter" if not exists $obj->{$a};
242 70         187 $obj->{$a} = $b;
243             }
244 47 50       166 if (ref $obj->{discard} eq 'ARRAY') {
245 0         0 $obj->{discard} = { map { uc $_ => 1 } @{$obj->{discard}} };
  0         0  
  0         0  
246             }
247 47         104 $obj;
248             }
249              
250             my @color_stack;
251             my @bg_stack;
252             my @reset;
253 138     138 0 368 sub put_reset { @reset = shift };
254             sub pop_reset {
255 167 100   167 0 396 @reset ? do { @color_stack = (); pop @reset } : '';
  138         252  
  138         273  
256             }
257 68     68 0 223 sub vlength { length $_[0] =~ s/.\cH//gr }
258              
259 12     12   106 use constant MAX_INT => ~0 >> 1;
  12         26  
  12         10428  
260              
261             sub fold {
262 619     619 0 65506 my $obj = shift;
263 619   50     1627 local $_ = shift // '';
264              
265 619 100       1354 if (not ref $obj) {
266 422         686 $obj = state $private = configure();
267             }
268 619         1504 my $opt = $obj->spawn(splice @_);
269              
270 619         1185 my $width = $opt->{width};
271 619 50       1752 croak "invalid width" if not looks_like_number $width;
272 619 100       1243 $width = MAX_INT if $width < 0;
273 619 50       1356 ($width -= $opt->{margin}) > 0 or croak "margin too big";
274              
275             my $word_char_re =
276             { word => $alphanum_re, space => $nonspace_re }
277 619   50     2148 ->{$opt->{boundary} // ''};
278              
279 619         1897 $Text::VisualWidth::PP::EastAsian = $opt->{ambiguous} eq 'wide';
280              
281 619         989 my $folded = '';
282 619         801 my $eol = '';
283 619         854 my $room = $width;
284 619         1090 @bg_stack = @color_stack = @reset = ();
285 619 100       2302 my $yield_re = $opt->{expand} ? qr/[^\e\n\f\r\t]/ : qr/[^\e\n\f\r]/;
286              
287             FOLD:
288 619         1565 while (length) {
289              
290             # newline
291 1774 50       4201 if (s/\A(\r*\n)//) {
292 0         0 $eol = $1;
293 0         0 last;
294             }
295             # formfeed / carriage return
296 1774 50       4873 if (s/\A([\f\r]+)//) {
297 0 0       0 last if length == 0;
298 0         0 $folded .= $1;
299 0         0 $room = $width;
300 0         0 next;
301             }
302             # ECMA-48 OPERATING SYSTEM COMMAND
303 1774 50       7430 if (s/\A($osc_re)//) {
304 0 0       0 $folded .= $1 unless $obj->{discard}->{OSC};
305 0         0 next;
306             }
307             # erase line (assume 0)
308 1774 100       5630 if (s/\A($erase_re)//) {
309 1 50       5 $folded .= $1 unless $obj->{discard}->{EL};
310 1         4 @bg_stack = @color_stack;
311 1         2 next;
312             }
313             # reset
314 1773 100       6027 if (s/\A($reset_re+($erase_re*))//) {
315 138         390 put_reset($1);
316 138 50       322 @bg_stack = () if $2;
317 138         355 next;
318             }
319              
320 1635 100       3409 last if $room < 1;
321 1127 100 100     2425 last if $room != $width and &_startWideSpacing and $room < 2;
      100        
322              
323 1119 100       5121 if (@reset) {
324 97         187 $folded .= pop_reset();
325             }
326              
327             # ANSI color sequence
328 1119 100       3915 if (s/\A($color_re)//) {
329 182         479 $folded .= $1;
330 182         398 push @color_stack, $1;
331 182         599 next;
332             }
333              
334             # tab
335 937 100 100     3083 if ($opt->{expand} and s/\A\t//) {
336 112         260 my $space = $opt->{tabstop} - ($width - $room) % $opt->{tabstop};
337 112         315 $_ = $opt->{tabhead} . $opt->{tabspace} x ($space - 1) . $_;
338 112         343 next;
339             }
340              
341             # backspace
342 825         1182 my $bs = 0;
343 825         2218 while (s/\A(?:\X\cH+)++(?\X|\z)//p) {
344 12     12   5415 my $w = vwidth($+{c});
  12         5761  
  12         22145  
  839         2507  
345 839 100       30412 if ($w > $room) {
346 7 100       18 if ($folded eq '') {
347 4         11 $folded .= ${^MATCH};
348 4         6 $room -= $w;
349             } else {
350 3         8 $_ = ${^MATCH} . $_;
351             }
352 7         23 last FOLD;
353             }
354 832         1489 $folded .= ${^MATCH};
355 832         1018 $room -= $w;
356 832         1041 $bs++;
357 832 100       4402 last if $room < 1;
358             }
359 818 100       1508 next if $bs;
360              
361 762 50       5227 if (s/\A(\e+|(?:${yield_re}(?!\cH))+)//) {
362 762         2173 my $s = $1;
363 762 100       2068 if ((my $w = vwidth($s)) <= $room) {
364 330         40037 $folded .= $s;
365 330         539 $room -= $w;
366 330         1073 next;
367             }
368 432         121439 my($a, $b, $w) = simple_fold($s, $room);
369 432 50 66     1153 if ($w > $room and $room < $width) {
370 0         0 $_ = $s . $_;
371 0         0 last;
372             }
373 432         1275 ($folded, $_) = ($folded . $a, $b . $_);
374 432         1350 $room -= $w;
375             } else {
376 0         0 die "panic ($_)";
377             }
378             }
379              
380             ##
381             ## --boundary=word
382             ##
383 619 100 100     5403 if ($word_char_re
      66        
384             and my($w2) = /\A( (?: ${word_char_re} \cH ? ) + )/x
385             and my($lead, $w1) = $folded =~ m{
386             \A ## avoid CSI final char making a word
387             ( (?: [^\e]* ${csi_re}++ ) *+ .*? )
388             ( (?: ${word_char_re} \cH ? ) + )
389             \z }x
390             ) {
391             ## Break line before word only when enough space will be
392             ## provided for the word in the next turn.
393 34         3947 my $l = vlength($w1);
394             ## prefix length
395 34 100       138 my $p = $opt->{prefix} eq '' ? 0 : vwidth($opt->{prefix});
396 34 100 66     258 if ($room + $l < $width - $p and $l + vlength($w2) <= $width - $p) {
397 29         50 $folded = $lead;
398 29         74 $_ = $w1 . pop_reset() . $_;
399 29         82 $room += $l;
400             }
401             }
402              
403             ##
404             ## RUN-OUT
405             ##
406 619 100 100     2320 if ($_ ne '' and $opt->do_runout) {
407 83 100 66     1783 if ($folded =~ m{ (? (?! ${reset_re}) ${color_re}*+ )
      100        
408             (?
409             (?: ($prohibition_re{end}) (?: \cH{1,2} \g{-1})* )+
410             ) \z
411             }xp
412             and ${^PREMATCH} ne ''
413             and (my $w = pwidth $+{runout}) <= $opt->{runout}) {
414              
415 18         324 $folded = ${^PREMATCH};
416 18         66 $_ = join '', ${^MATCH}, @reset, $_;
417 18 100       103 pop_reset() if $+{color};
418 18         48 $room += $w;
419             }
420             }
421              
422 619 100       1450 $folded .= pop_reset() if @reset;
423              
424 619         1088 $room += $opt->{margin};
425              
426             ##
427             ## RUN-IN
428             ##
429 619 100       1288 if ($opt->do_runin) {
430 87         135 my @runin;
431 87         143 my $m = $opt->{runin};
432 87   100     1140 while ($m > 0 and
433             m{\A (? ${color_re}*+)
434             (? $prohibition_re{head} )
435             ( \cH{1,2} \g{runin} )* # multiple strike
436             (? (?: $erase_re* $reset_re+ $erase_re* )? )
437             }xp) {
438 28         119 my $w = vwidth $+{runin};
439 28 50       570 last if ($m -= $w) < 0;
440 28 100       125 $+{color} and do { push @color_stack, $+{color} };
  2         9  
441 28 100       114 $+{reset} and do { @color_stack = () };
  2         5  
442 28         85 $room -= $w;
443 28         66 push @runin, ${^MATCH};
444 28         167 $_ = ${^POSTMATCH};
445             }
446 87 100       789 $folded .= join '', @runin if @runin;
447             }
448              
449 619 100       1305 if (@color_stack) {
450 44         87 $folded .= SGR_RESET;
451 44 50       161 $_ = join '', @color_stack, $_ if $_ ne '';
452             }
453              
454 619 100 100     1453 if ($opt->{padding} and $room > 0) {
455 12         53 my $padding = $opt->{padchar} x $room;
456 12 100       49 if (@bg_stack) {
457 1         5 $padding = join '', @bg_stack, $padding, SGR_RESET;
458             }
459 12         27 $folded .= $padding;
460             }
461              
462 619 100 100     2110 if (length and my $p = $opt->{prefix}) {
463 19 100       53 my $s = ref $p eq 'CODE' ? &$p : $p;
464 19         56 $_ = $s . $_;
465             }
466              
467 619         5838 ($folded . $eol, $_, $width - $room);
468             }
469              
470             ##
471             ## Trim off one or more *logical* characters from the top.
472             ##
473             sub simple_fold {
474 432     432 0 699 my $orig = shift;
475 432         900 my $width = shift;
476 432 50       858 $width <= 0 and croak "parameter error";
477              
478 432 50       7675 my($left, $right) = $orig =~ m/^(\X{0,$width}+)(.*)/ or die;
479              
480 432         1321 my $w = vwidth($left);
481 432         6441 while ($w > $width) {
482 76         150 my $trim = do {
483             # use POSIX qw(ceil);
484             # ceil(($w - $width) / 2) || 1;
485 76 50       328 int(($w - $width) / 2 + 0.5) || 1;
486             };
487 76 100       2046 $left =~ s/\X \K ( \X{$trim} ) \z//x or last;
488 73         303 $right = $1 . $right;
489 73         185 $w -= vwidth($1);
490             }
491              
492 432         2645 ($left, $right, $w);
493             }
494              
495             ######################################################################
496             # EXTERNAL METHODS
497              
498             sub text :lvalue {
499 7     7 0 560 my $obj = shift;
500 7 100       21 if (@_ == 0) {
501 6         44 $obj->{text};
502             } else {
503 1 50       5 croak "Invalid argument" if @_ > 1;
504 1         3 $obj->{text} = shift;
505 1         3 $obj;
506             }
507             }
508              
509             sub retrieve {
510 65     65 0 94 my $obj = shift;
511 65         148 local *_ = \$obj->{text};
512 65 100       154 return '' if not defined $_;
513 56         136 (my $folded, $_) = $obj->fold($_, @_);
514 56 100       137 $_ = undef if length == 0;
515 56         179 $folded;
516             }
517              
518             sub chops {
519 15     15 0 36 my $obj = shift;
520 15         30 my %opt = @_;
521 15   66     64 my $width = $opt{width} // $obj->{width};
522              
523 15         24 my @chops;
524              
525 15 100       40 if (ref $width eq 'ARRAY') {
526 8         10 for my $w (@{$width}) {
  8         19  
527 40 100       114 if ($w == 0) {
    100          
528 2         4 push @chops, '';
529             }
530             elsif ((my $chop = $obj->retrieve(width => $w)) ne '') {
531 35         82 push @chops, $chop;
532             }
533             else {
534 3         6 last;
535             }
536             }
537             }
538             else {
539 7         20 while ((my $chop = $obj->retrieve(width => $width)) ne '') {
540 19         52 push @chops, $chop;
541             }
542             }
543              
544 15         147 @chops;
545             }
546              
547             1;
548              
549             __END__