File Coverage

blib/lib/Text/ANSI/Fold.pm
Criterion Covered Total %
statement 239 255 93.7
branch 114 138 82.6
condition 44 57 77.1
subroutine 36 36 100.0
pod 0 18 0.0
total 433 504 85.9


line stmt bran cond sub pod time code
1             package Text::ANSI::Fold;
2              
3 12     12   784914 use v5.14;
  12         158  
4 12     12   68 use warnings;
  12         25  
  12         300  
5 12     12   1203 use utf8;
  12         48  
  12         57  
6              
7             our $VERSION = "2.2102";
8              
9 12     12   5333 use Data::Dumper;
  12         55178  
  12         813  
10             $Data::Dumper::Sortkeys = 1;
11 12     12   82 use Carp;
  12         26  
  12         754  
12 12     12   76 use List::Util qw(pairmap pairgrep);
  12         23  
  12         1223  
13 12     12   85 use Scalar::Util qw(looks_like_number);
  12         23  
  12         576  
14 12     12   5308 use Text::VisualWidth::PP 'vwidth';
  12         293452  
  12         1634  
15 95     95 0 402 sub pwidth { vwidth $_[0] =~ s/\X\cH{1,2}//gr }
16              
17             ######################################################################
18 12     12   116 use Exporter 'import';
  12         22  
  12         1954  
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 429     429 0 142888 my($text, $width, @option) = @_;
42 429         1356 __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   27595 use constant SGR_RESET => "\e[m";
  12         27  
  12         1938  
66              
67             sub IsPrintableLatin {
68 2     2 0 1043 return <<"END";
69             +utf8::ASCII
70             +utf8::Latin
71             -utf8::White_Space
72             END
73             }
74              
75             sub IsWideSpacing {
76 5     5 0 746 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 135 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 374 100   374   658 if ($Text::VisualWidth::PP::EastAsian) {
95 2         18 /^\p{IsWideAmbiguousSpacing}/;
96             } else {
97 372         1693 /^\p{IsWideSpacing}/;
98             }
99             }
100              
101             use constant {
102 12         1725 LINEBREAK_NONE => 0,
103             LINEBREAK_RUNIN => 1,
104             LINEBREAK_RUNOUT => 2,
105             LINEBREAK_ALL => 3,
106 12     12   2876 };
  12         28  
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   1056 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   6399 symbol => [ "\N{SYMBOL FOR HORIZONTAL TABULATION}", # ␉
  12         101383  
  12         80  
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 1823 my $class = shift;
174 13         290 my $obj = bless { @default }, $class;
175 13 100       73 $obj->configure(@_) if @_;
176 13         47 $obj;
177             }
178              
179             INTERNAL_METHODS: {
180             sub spawn {
181 626     626 0 921 my $obj = shift;
182 626         986 my $class = ref $obj;
183 626     672   5400 my %new = ( %$obj, pairgrep { defined $b } @_ );
  672         5392  
184 626         3030 bless \%new, $class;
185             }
186 626 100   626 0 1861 sub do_runin { $_[0]->{linebreak} & LINEBREAK_RUNIN && $_[0]->{runin} > 0 }
187 530 100   530 0 2408 sub do_runout { $_[0]->{linebreak} & LINEBREAK_RUNOUT && $_[0]->{runout} > 0 }
188             }
189              
190 12     12   225402 use Text::ANSI::Fold::Japanese::W3C qw(%prohibition);
  12         37  
  12         11107  
191              
192             sub chars_to_regex {
193 24     24 0 7029 my $chars = join '', @_;
194 24         53 my($c, @s);
195 24         1150 for ($chars =~ /\X/g) {
196 1860 100       3249 if (length == 1) {
197 1848         2512 $c .= $_;
198             } else {
199 12         50 push @s, $_;
200             }
201             }
202 24 100       245 if (@s) {
203 12         34 local $" = '|';
204 12         905 qr/(?:[\Q$c\E]|@s)/;
205             } else {
206 12         341 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 13856 my $obj = shift;
220 47 100       155 if (not ref $obj) {
221 12         38 $obj = state $private = __PACKAGE__->new;
222             }
223 47 50       156 croak "invalid parameter" if @_ % 2;
224 47         151 while (@_ >= 2) {
225 71         187 my($a, $b) = splice @_, 0, 2;
226              
227 71 100       153 if ($a eq 'tabstyle') {
228 1   50     4 $b // next;
229 1 50       20 my($h, $s) = $b =~ /([-\w]+)/g or croak "$b: invalid tabstyle";
230 1   33     8 $s ||= $h;
231             my %style = (
232             h => ($TABSTYLE{$h} or croak "$h: invalid tabstyle"),
233 1   33     16 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         5 next;
239             }
240              
241 70 50       196 croak "$a: invalid parameter" if not exists $obj->{$a};
242 70         206 $obj->{$a} = $b;
243             }
244 47 50       181 if (ref $obj->{discard} eq 'ARRAY') {
245 0         0 $obj->{discard} = { map { uc $_ => 1 } @{$obj->{discard}} };
  0         0  
  0         0  
246             }
247 47         99 $obj;
248             }
249              
250             my @color_stack;
251             my @bg_stack;
252             my @reset;
253 138     138 0 441 sub put_reset { @reset = shift };
254             sub pop_reset {
255 169 100   169 0 393 @reset ? do { @color_stack = (); pop @reset } : '';
  138         231  
  138         278  
256             }
257              
258 12     12   106 use constant MAX_INT => ~0 >> 1;
  12         27  
  12         10092  
259              
260             sub fold {
261 626     626 0 65319 my $obj = shift;
262 626   50     1552 local $_ = shift // '';
263              
264 626 100       1368 if (not ref $obj) {
265 429         742 $obj = state $private = configure();
266             }
267 626         1957 my $opt = $obj->spawn(splice @_);
268              
269 626         1111 my $width = $opt->{width};
270 626 50       1807 croak "invalid width" if not looks_like_number $width;
271 626 100       1268 $width = MAX_INT if $width < 0;
272 626 50       1345 ($width -= $opt->{margin}) > 0 or croak "margin too big";
273              
274             my $word_char_re =
275             { word => $alphanum_re, space => $nonspace_re }
276 626   50     2222 ->{$opt->{boundary} // ''};
277              
278 626         1596 $Text::VisualWidth::PP::EastAsian = $opt->{ambiguous} eq 'wide';
279              
280 626         939 my $folded = '';
281 626         842 my $eol = '';
282 626         829 my $room = $width;
283 626         1069 @bg_stack = @color_stack = @reset = ();
284 626 100       2281 my $yield_re = $opt->{expand} ? qr/[^\e\n\f\r\t]/ : qr/[^\e\n\f\r]/;
285              
286             FOLD:
287 626         1560 while (length) {
288              
289             # newline
290 1788 50       4372 if (s/\A(\r*\n)//) {
291 0         0 $eol = $1;
292 0         0 last;
293             }
294             # formfeed / carriage return
295 1788 50       4786 if (s/\A([\f\r]+)//) {
296 0 0       0 last if length == 0;
297 0         0 $folded .= $1;
298 0         0 $room = $width;
299 0         0 next;
300             }
301             # ECMA-48 OPERATING SYSTEM COMMAND
302 1788 50       7826 if (s/\A($osc_re)//) {
303 0 0       0 $folded .= $1 unless $obj->{discard}->{OSC};
304 0         0 next;
305             }
306             # erase line (assume 0)
307 1788 100       5594 if (s/\A($erase_re)//) {
308 1 50       6 $folded .= $1 unless $obj->{discard}->{EL};
309 1         4 @bg_stack = @color_stack;
310 1         2 next;
311             }
312             # reset
313 1787 100       6038 if (s/\A($reset_re+($erase_re*))//) {
314 138         380 put_reset($1);
315 138 50       315 @bg_stack = () if $2;
316 138         405 next;
317             }
318              
319 1649 100       3401 last if $room < 1;
320 1136 100 100     2478 last if $room != $width and &_startWideSpacing and $room < 2;
      100        
321              
322 1126 100       5265 if (@reset) {
323 97         182 $folded .= pop_reset();
324             }
325              
326             # ANSI color sequence
327 1126 100       3993 if (s/\A($color_re)//) {
328 182         461 $folded .= $1;
329 182         456 push @color_stack, $1;
330 182         578 next;
331             }
332              
333             # tab
334 944 100 100     3008 if ($opt->{expand} and s/\A\t//) {
335 112         266 my $space = $opt->{tabstop} - ($width - $room) % $opt->{tabstop};
336 112         322 $_ = $opt->{tabhead} . $opt->{tabspace} x ($space - 1) . $_;
337 112         331 next;
338             }
339              
340             # backspace
341 832         1170 my $bs = 0;
342 832         2204 while (s/\A(?:\X\cH+)++(?\X|\z)//p) {
343 12     12   5449 my $w = vwidth($+{c});
  12         5686  
  12         21766  
  839         2533  
344 839 100       30128 if ($w > $room) {
345 7 100       17 if ($folded eq '') {
346 4         9 $folded .= ${^MATCH};
347 4         6 $room -= $w;
348             } else {
349 3         8 $_ = ${^MATCH} . $_;
350             }
351 7         23 last FOLD;
352             }
353 832         1491 $folded .= ${^MATCH};
354 832         1071 $room -= $w;
355 832         980 $bs++;
356 832 100       4447 last if $room < 1;
357             }
358 825 100       1516 next if $bs;
359              
360 769 50       5270 if (s/\A(\e+|(?:${yield_re}(?!\cH))+)//) {
361 769         1891 my $s = $1;
362 769 100       1957 if ((my $w = vwidth($s)) <= $room) {
363 330         41121 $folded .= $s;
364 330         479 $room -= $w;
365 330         1044 next;
366             }
367 439         117490 my($a, $b, $w) = simple_fold($s, $room);
368 439 50 66     1225 if ($w > $room and $room < $width) {
369 0         0 $_ = $s . $_;
370 0         0 last;
371             }
372 439         1213 ($folded, $_) = ($folded . $a, $b . $_);
373 439         1431 $room -= $w;
374             } else {
375 0         0 die "panic ($_)";
376             }
377             }
378              
379             ##
380             ## --boundary=word
381             ##
382 626 100 100     5400 if ($word_char_re
      100        
383             and my($w2) = /\A( (?: ${word_char_re} \cH ? ) + )/x
384             and my($lead, $w1) = $folded =~ m{
385             \A ## avoid CSI final char making a word
386             ( (?: [^\e]* ${csi_re}++ ) *+ .*? )
387             ( (?: ${word_char_re} \cH ? ) + )
388             \z }x
389             ) {
390             ## Break line before word only when enough space will be
391             ## provided for the word in the next turn.
392 36         3861 my $l = pwidth($w1);
393             ## prefix length
394 36 100       607 my $p = $opt->{prefix} eq '' ? 0 : vwidth($opt->{prefix});
395 36 100 66     210 if ($room + $l < $width - $p and $l + pwidth($w2) <= $width - $p) {
396 31         527 $folded = $lead;
397 31         74 $_ = $w1 . pop_reset() . $_;
398 31         57 $room += $l;
399             }
400             }
401              
402             ##
403             ## RUN-OUT
404             ##
405 626 100 100     2232 if ($_ ne '' and $opt->do_runout) {
406 83 100 66     1795 if ($folded =~ m{ (? (?! ${reset_re}) ${color_re}*+ )
      100        
407             (?
408             (?: ($prohibition_re{end}) (?: \cH{1,2} \g{-1})* )+
409             ) \z
410             }xp
411             and ${^PREMATCH} ne ''
412             and (my $w = pwidth $+{runout}) <= $opt->{runout}) {
413              
414 18         314 $folded = ${^PREMATCH};
415 18         77 $_ = join '', ${^MATCH}, @reset, $_;
416 18 100       104 pop_reset() if $+{color};
417 18         49 $room += $w;
418             }
419             }
420              
421 626 100       1499 $folded .= pop_reset() if @reset;
422              
423 626         1057 $room += $opt->{margin};
424              
425             ##
426             ## RUN-IN
427             ##
428 626 100       1356 if ($opt->do_runin) {
429 87         125 my @runin;
430 87         134 my $m = $opt->{runin};
431 87   100     1176 while ($m > 0 and
432             m{\A (? ${color_re}*+)
433             (? $prohibition_re{head} )
434             ( \cH{1,2} \g{runin} )* # multiple strike
435             (? (?: $erase_re* $reset_re+ $erase_re* )? )
436             }xp) {
437 28         129 my $w = vwidth $+{runin};
438 28 50       583 last if ($m -= $w) < 0;
439 28 100       123 $+{color} and do { push @color_stack, $+{color} };
  2         8  
440 28 100       115 $+{reset} and do { @color_stack = () };
  2         5  
441 28         51 $room -= $w;
442 28         65 push @runin, ${^MATCH};
443 28         155 $_ = ${^POSTMATCH};
444             }
445 87 100       840 $folded .= join '', @runin if @runin;
446             }
447              
448 626 100       1284 if (@color_stack) {
449 44         96 $folded .= SGR_RESET;
450 44 50       149 $_ = join '', @color_stack, $_ if $_ ne '';
451             }
452              
453 626 100 100     1444 if ($opt->{padding} and $room > 0) {
454 12         47 my $padding = $opt->{padchar} x $room;
455 12 100       37 if (@bg_stack) {
456 1         5 $padding = join '', @bg_stack, $padding, SGR_RESET;
457             }
458 12         29 $folded .= $padding;
459             }
460              
461 626 100 100     2204 if (length and my $p = $opt->{prefix}) {
462 19 100       46 my $s = ref $p eq 'CODE' ? &$p : $p;
463 19         57 $_ = $s . $_;
464             }
465              
466 626         5923 ($folded . $eol, $_, $width - $room);
467             }
468              
469             ##
470             ## Trim off one or more *logical* characters from the top.
471             ##
472             sub simple_fold {
473 439     439 0 711 my $orig = shift;
474 439         696 my $width = shift;
475 439 50       872 $width <= 0 and croak "parameter error";
476              
477 439 50       7147 my($left, $right) = $orig =~ m/^(\X{0,$width}+)(.*)/ or die;
478              
479 439         1319 my $w = vwidth($left);
480 439         6730 while ($w > $width) {
481 86         199 my $trim = do {
482             # use POSIX qw(ceil);
483             # ceil(($w - $width) / 2) || 1;
484 86 50       314 int(($w - $width) / 2 + 0.5) || 1;
485             };
486 86 100       2232 $left =~ s/\X \K ( \X{$trim} ) \z//x or last;
487 83         285 $right = $1 . $right;
488 83         203 $w -= vwidth($1);
489             }
490              
491 439         2739 ($left, $right, $w);
492             }
493              
494             ######################################################################
495             # EXTERNAL METHODS
496              
497             sub text :lvalue {
498 7     7 0 592 my $obj = shift;
499 7 100       55 if (@_ == 0) {
500 6         46 $obj->{text};
501             } else {
502 1 50       5 croak "Invalid argument" if @_ > 1;
503 1         3 $obj->{text} = shift;
504 1         2 $obj;
505             }
506             }
507              
508             sub retrieve {
509 65     65 0 125 my $obj = shift;
510 65         148 local *_ = \$obj->{text};
511 65 100       147 return '' if not defined $_;
512 56         141 (my $folded, $_) = $obj->fold($_, @_);
513 56 100       125 $_ = undef if length == 0;
514 56         183 $folded;
515             }
516              
517             sub chops {
518 15     15 0 35 my $obj = shift;
519 15         30 my %opt = @_;
520 15   66     61 my $width = $opt{width} // $obj->{width};
521              
522 15         24 my @chops;
523              
524 15 100       38 if (ref $width eq 'ARRAY') {
525 8         13 for my $w (@{$width}) {
  8         18  
526 40 100       114 if ($w == 0) {
    100          
527 2         4 push @chops, '';
528             }
529             elsif ((my $chop = $obj->retrieve(width => $w)) ne '') {
530 35         85 push @chops, $chop;
531             }
532             else {
533 3         6 last;
534             }
535             }
536             }
537             else {
538 7         17 while ((my $chop = $obj->retrieve(width => $width)) ne '') {
539 19         52 push @chops, $chop;
540             }
541             }
542              
543 15         139 @chops;
544             }
545              
546             1;
547              
548             __END__