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   782489 use v5.14;
  12         180  
4 12     12   68 use warnings;
  12         25  
  12         316  
5 12     12   1222 use utf8;
  12         49  
  12         57  
6              
7             our $VERSION = "2.20";
8              
9 12     12   5343 use Data::Dumper;
  12         54631  
  12         820  
10             $Data::Dumper::Sortkeys = 1;
11 12     12   82 use Carp;
  12         24  
  12         785  
12 12     12   81 use List::Util qw(pairmap pairgrep);
  12         23  
  12         1367  
13 12     12   81 use Scalar::Util qw(looks_like_number);
  12         31  
  12         565  
14 12     12   5263 use Text::VisualWidth::PP 'vwidth';
  12         292305  
  12         1706  
15 23     23 0 168 sub pwidth { vwidth $_[0] =~ s/\X\cH{1,2}//gr }
16              
17             ######################################################################
18 12     12   124 use Exporter 'import';
  12         24  
  12         2111  
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 138610 my($text, $width, @option) = @_;
42 422         1320 __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   28800 use constant SGR_RESET => "\e[m";
  12         26  
  12         1880  
66              
67             sub IsPrintableLatin {
68 2     2 0 1003 return <<"END";
69             +utf8::ASCII
70             +utf8::Latin
71             -utf8::White_Space
72             END
73             }
74              
75             sub IsWideSpacing {
76 5     5 0 666 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 123 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 371 100   371   657 if ($Text::VisualWidth::PP::EastAsian) {
95 2         16 /^\p{IsWideAmbiguousSpacing}/;
96             } else {
97 369         1588 /^\p{IsWideSpacing}/;
98             }
99             }
100              
101             use constant {
102 12         1816 LINEBREAK_NONE => 0,
103             LINEBREAK_RUNIN => 1,
104             LINEBREAK_RUNOUT => 2,
105             LINEBREAK_ALL => 3,
106 12     12   2763 };
  12         29  
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   1072 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   6468 symbol => [ "\N{SYMBOL FOR HORIZONTAL TABULATION}", # ␉
  12         102840  
  12         91  
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 1833 my $class = shift;
174 13         203 my $obj = bless { @default }, $class;
175 13 100       67 $obj->configure(@_) if @_;
176 13         44 $obj;
177             }
178              
179             INTERNAL_METHODS: {
180             sub spawn {
181 616     616 0 901 my $obj = shift;
182 616         959 my $class = ref $obj;
183 616     655   5204 my %new = ( %$obj, pairgrep { defined $b } @_ );
  655         5347  
184 616         2954 bless \%new, $class;
185             }
186 616 100   616 0 1877 sub do_runin { $_[0]->{linebreak} & LINEBREAK_RUNIN && $_[0]->{runin} > 0 }
187 523 100   523 0 2240 sub do_runout { $_[0]->{linebreak} & LINEBREAK_RUNOUT && $_[0]->{runout} > 0 }
188             }
189              
190 12     12   227255 use Text::ANSI::Fold::Japanese::W3C qw(%prohibition);
  12         47  
  12         12038  
191              
192             sub chars_to_regex {
193 24     24 0 7158 my $chars = join '', @_;
194 24         62 my($c, @s);
195 24         1120 for ($chars =~ /\X/g) {
196 1860 100       3330 if (length == 1) {
197 1848         2555 $c .= $_;
198             } else {
199 12         34 push @s, $_;
200             }
201             }
202 24 100       247 if (@s) {
203 12         37 local $" = '|';
204 12         903 qr/(?:[\Q$c\E]|@s)/;
205             } else {
206 12         349 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 14388 my $obj = shift;
220 47 100       154 if (not ref $obj) {
221 12         32 $obj = state $private = __PACKAGE__->new;
222             }
223 47 50       152 croak "invalid parameter" if @_ % 2;
224 47         140 while (@_ >= 2) {
225 71         183 my($a, $b) = splice @_, 0, 2;
226              
227 71 100       180 if ($a eq 'tabstyle') {
228 1   50     4 $b // next;
229 1 50       11 my($h, $s) = $b =~ /([-\w]+)/g or croak "$b: invalid tabstyle";
230 1   33     7 $s ||= $h;
231             my %style = (
232             h => ($TABSTYLE{$h} or croak "$h: invalid tabstyle"),
233 1   33     19 s => ($TABSTYLE{$s} or croak "$s: invalid tabstyle"),
      33        
234             );
235             unshift @_,
236             tabhead => $style{h}->[0],
237 1         6 tabspace => $style{s}->[1];
238 1         5 next;
239             }
240              
241 70 50       187 croak "$a: invalid parameter" if not exists $obj->{$a};
242 70         188 $obj->{$a} = $b;
243             }
244 47 50       164 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 360 sub put_reset { @reset = shift };
254             sub pop_reset {
255 167 100   167 0 377 @reset ? do { @color_stack = (); pop @reset } : '';
  138         247  
  138         266  
256             }
257 68     68 0 221 sub vlength { length $_[0] =~ s/.\cH//gr }
258              
259 12     12   102 use constant MAX_INT => ~0 >> 1;
  12         32  
  12         10083  
260              
261             sub fold {
262 616     616 0 65124 my $obj = shift;
263 616   50     1538 local $_ = shift // '';
264              
265 616 100       1335 if (not ref $obj) {
266 422         716 $obj = state $private = configure();
267             }
268 616         1597 my $opt = $obj->spawn(splice @_);
269              
270 616         1161 my $width = $opt->{width};
271 616 50       1775 croak "invalid width" if not looks_like_number $width;
272 616 100       1234 $width = MAX_INT if $width < 0;
273 616 50       1461 ($width -= $opt->{margin}) > 0 or croak "margin too big";
274              
275             my $word_char_re =
276             { word => $alphanum_re, space => $nonspace_re }
277 616   50     2266 ->{$opt->{boundary} // ''};
278              
279 616         1592 $Text::VisualWidth::PP::EastAsian = $opt->{ambiguous} eq 'wide';
280              
281 616         937 my $folded = '';
282 616         814 my $eol = '';
283 616         851 my $room = $width;
284 616         1120 @bg_stack = @color_stack = @reset = ();
285 616 100       2286 my $yield_re = $opt->{expand} ? qr/[^\e\n\f\r\t]/ : qr/[^\e\n\f\r]/;
286              
287             FOLD:
288 616         1523 while (length) {
289              
290             # newline
291 1770 50       4304 if (s/\A(\r*\n)//) {
292 0         0 $eol = $1;
293 0         0 last;
294             }
295             # formfeed / carriage return
296 1770 50       4784 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 1770 50       7388 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 1770 100       5550 if (s/\A($erase_re)//) {
309 1 50       5 $folded .= $1 unless $obj->{discard}->{EL};
310 1         3 @bg_stack = @color_stack;
311 1         3 next;
312             }
313             # reset
314 1769 100       6034 if (s/\A($reset_re+($erase_re*))//) {
315 138         384 put_reset($1);
316 138 50       314 @bg_stack = () if $2;
317 138         340 next;
318             }
319              
320 1631 100       3337 last if $room < 1;
321 1123 100 100     2410 last if $room != $width and &_startWideSpacing and $room < 2;
      100        
322              
323 1115 100       4003 if (@reset) {
324 97         178 $folded .= pop_reset();
325             }
326              
327             # ANSI color sequence
328 1115 100       3825 if (s/\A($color_re)//) {
329 182         467 $folded .= $1;
330 182         393 push @color_stack, $1;
331 182         511 next;
332             }
333              
334             # tab
335 933 100 100     2978 if ($opt->{expand} and s/\A\t//) {
336 112         257 my $space = $opt->{tabstop} - ($width - $room) % $opt->{tabstop};
337 112         360 $_ = $opt->{tabhead} . $opt->{tabspace} x ($space - 1) . $_;
338 112         330 next;
339             }
340              
341             # backspace
342 821         1146 my $bs = 0;
343 821         2160 while (s/\A(?:\X\cH+)++(?\X|\z)//p) {
344 12     12   5551 my $w = vwidth($+{c});
  12         5476  
  12         21508  
  839         2532  
345 839 100       29505 if ($w > $room) {
346 7 100       17 if ($folded eq '') {
347 4         11 $folded .= ${^MATCH};
348 4         8 $room -= $w;
349             } else {
350 3         8 $_ = ${^MATCH} . $_;
351             }
352 7         22 last FOLD;
353             }
354 832         1418 $folded .= ${^MATCH};
355 832         1009 $room -= $w;
356 832         1032 $bs++;
357 832 100       4168 last if $room < 1;
358             }
359 814 100       1541 next if $bs;
360              
361 758 50       5234 if (s/\A(\e*(?:${yield_re}(?!\cH))+)//) {
362 758         1733 my $s = $1;
363 758 100       1980 if ((my $w = vwidth($s)) <= $room) {
364 326         21949 $folded .= $s;
365 326         458 $room -= $w;
366 326         1023 next;
367             }
368 432         134467 my($a, $b, $w) = simple_fold($s, $room);
369 432 50 66     1122 if ($w > $room and $room < $width) {
370 0         0 $_ = $s . $_;
371 0         0 last;
372             }
373 432         1194 ($folded, $_) = ($folded . $a, $b . $_);
374 432         1344 $room -= $w;
375             } else {
376 0         0 die "panic ($_)";
377             }
378             }
379              
380             ##
381             ## --boundary=word
382             ##
383 616 100 100     6971 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 does not make 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         3856 my $l = vlength($w1);
394             ## prefix length
395 34 100       125 my $p = $opt->{prefix} eq '' ? 0 : vwidth($opt->{prefix});
396 34 100 66     212 if ($room + $l < $width - $p and $l + vlength($w2) <= $width - $p) {
397 29         57 $folded = $lead;
398 29         65 $_ = $w1 . pop_reset() . $_;
399 29         58 $room += $l;
400             }
401             }
402              
403             ##
404             ## RUN-OUT
405             ##
406 616 100 100     2082 if ($_ ne '' and $opt->do_runout) {
407 83 100 66     1771 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         328 $folded = ${^PREMATCH};
416 18         64 $_ = join '', ${^MATCH}, @reset, $_;
417 18 100       91 pop_reset() if $+{color};
418 18         55 $room += $w;
419             }
420             }
421              
422 616 100       1535 $folded .= pop_reset() if @reset;
423              
424 616         1029 $room += $opt->{margin};
425              
426             ##
427             ## RUN-IN
428             ##
429 616 100       1294 if ($opt->do_runin) {
430 87         136 my @runin;
431 87         136 my $m = $opt->{runin};
432 87   100     1209 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         125 my $w = vwidth $+{runin};
439 28 50       574 last if ($m -= $w) < 0;
440 28 100       125 $+{color} and do { push @color_stack, $+{color} };
  2         7  
441 28 100       121 $+{reset} and do { @color_stack = () };
  2         5  
442 28         58 $room -= $w;
443 28         64 push @runin, ${^MATCH};
444 28         167 $_ = ${^POSTMATCH};
445             }
446 87 100       853 $folded .= join '', @runin if @runin;
447             }
448              
449 616 100       1190 if (@color_stack) {
450 44         96 $folded .= SGR_RESET;
451 44 50       160 $_ = join '', @color_stack, $_ if $_ ne '';
452             }
453              
454 616 100 100     1587 if ($opt->{padding} and $room > 0) {
455 12         47 my $padding = $opt->{padchar} x $room;
456 12 100       38 if (@bg_stack) {
457 1         4 $padding = join '', @bg_stack, $padding, SGR_RESET;
458             }
459 12         31 $folded .= $padding;
460             }
461              
462 616 100 100     2084 if (length and my $p = $opt->{prefix}) {
463 19 100       52 my $s = ref $p eq 'CODE' ? &$p : $p;
464 19         52 $_ = $s . $_;
465             }
466              
467 616         5825 ($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 670 my $orig = shift;
475 432         671 my $width = shift;
476 432 50       831 $width <= 0 and croak "parameter error";
477              
478 432 50       6931 my($left, $right) = $orig =~ m/^(\X{0,$width}+)(.*)/ or die;
479              
480 432         1299 my $w = vwidth($left);
481 432         6459 while ($w > $width) {
482 76         108 my $trim = do {
483             # use POSIX qw(ceil);
484             # ceil(($w - $width) / 2) || 1;
485 76 50       292 int(($w - $width) / 2 + 0.5) || 1;
486             };
487 76 100       2125 $left =~ s/\X \K ( \X{$trim} ) \z//x or last;
488 73         278 $right = $1 . $right;
489 73         190 $w -= vwidth($1);
490             }
491              
492 432         2558 ($left, $right, $w);
493             }
494              
495             ######################################################################
496             # EXTERNAL METHODS
497              
498             sub text :lvalue {
499 7     7 0 559 my $obj = shift;
500 7 100       24 if (@_ == 0) {
501 6         45 $obj->{text};
502             } else {
503 1 50       5 croak "Invalid argument" if @_ > 1;
504 1         3 $obj->{text} = shift;
505 1         34 $obj;
506             }
507             }
508              
509             sub retrieve {
510 65     65 0 95 my $obj = shift;
511 65         142 local *_ = \$obj->{text};
512 65 100       146 return '' if not defined $_;
513 56         141 (my $folded, $_) = $obj->fold($_, @_);
514 56 100       128 $_ = undef if length == 0;
515 56         196 $folded;
516             }
517              
518             sub chops {
519 15     15 0 35 my $obj = shift;
520 15         28 my %opt = @_;
521 15   66     61 my $width = $opt{width} // $obj->{width};
522              
523 15         34 my @chops;
524              
525 15 100       49 if (ref $width eq 'ARRAY') {
526 8         14 for my $w (@{$width}) {
  8         17  
527 40 100       112 if ($w == 0) {
    100          
528 2         7 push @chops, '';
529             }
530             elsif ((my $chop = $obj->retrieve(width => $w)) ne '') {
531 35         87 push @chops, $chop;
532             }
533             else {
534 3         5 last;
535             }
536             }
537             }
538             else {
539 7         22 while ((my $chop = $obj->retrieve(width => $width)) ne '') {
540 19         51 push @chops, $chop;
541             }
542             }
543              
544 15         146 @chops;
545             }
546              
547             1;
548              
549             __END__