File Coverage

blib/lib/Text/ANSI/BaseUtil.pm
Criterion Covered Total %
statement 404 470 85.9
branch 186 256 72.6
condition 82 115 71.3
subroutine 26 33 78.7
pod 0 20 0.0
total 698 894 78.0


line stmt bran cond sub pod time code
1             package Text::ANSI::BaseUtil;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2021-08-08'; # DATE
5             our $DIST = 'Text-ANSI-Util'; # DIST
6             our $VERSION = '0.233'; # VERSION
7              
8 1     1   16 use 5.010001;
  1         2  
9 1     1   4 use strict;
  1         2  
  1         15  
10 1     1   4 use warnings;
  1         2  
  1         22  
11              
12 1     1   5 use List::Util qw(min max);
  1         2  
  1         4230  
13              
14             our $re = qr/\e\[[0-9;]+m/s;
15             our $re_mult = qr/(?:\e\[[0-9;]+m)+/s;
16              
17             sub ta_detect {
18 4     4 0 1070 my $text = shift;
19 4 100       36 $text =~ $re ? 1:0;
20             }
21              
22             sub ta_length {
23 30     30 0 2318 my $text = shift;
24 30         52 length(ta_strip($text));
25             }
26              
27             sub _ta_length_height {
28 5     5   11 my ($is_mb, $text) = @_;
29 5         8 my $num_lines = 0;
30 5         8 my @lens;
31 5         10 for my $e (split /(\r?\n)/, ta_strip($text)) {
32 7 100       17 if ($e =~ /\n/) {
33 3         5 $num_lines++;
34 3         4 next;
35             }
36 4 100       10 $num_lines = 1 if $num_lines == 0;
37 4 50       9 push @lens, $is_mb ? Text::WideChar::Util::mbswidth($e) : length($e);
38             }
39 5   100     42 [max(@lens) // 0, $num_lines];
40             }
41              
42             sub ta_length_height {
43 5     5 0 2255 _ta_length_height(0, @_);
44             }
45              
46             sub ta_mbswidth_height {
47 0     0 0 0 _ta_length_height(1, @_);
48             }
49              
50             sub ta_strip {
51 38     38 0 2183 my $text = shift;
52 38         183 $text =~ s/$re//go;
53 38         121 $text;
54             }
55              
56             sub ta_extract_codes {
57 6     6 0 2262 my $text = shift;
58 6         10 my $res = "";
59 6         78 $res .= $1 while $text =~ /($re_mult)/go;
60 6         19 $res;
61             }
62              
63             sub ta_split_codes {
64 7     7 0 2167 my $text = shift;
65 7         112 return split(/($re_mult)/o, $text);
66             }
67              
68             sub ta_split_codes_single {
69 31     31 0 2345 my $text = shift;
70 31         250 return split(/($re)/o, $text);
71             }
72              
73             # same like _ta_mbswidth, but without handling multiline text
74             sub _ta_mbswidth0 {
75 0     0   0 my $text = shift;
76 0         0 Text::WideChar::Util::mbswidth(ta_strip($text));
77             }
78              
79             sub ta_mbswidth {
80 0     0 0 0 my $text = shift;
81 0         0 ta_mbswidth_height($text)->[0];
82             }
83              
84             sub _indent_width {
85 4     4   6 my ($indent, $tab_width) = @_;
86 4         6 my $w = 0;
87 4         10 for (split //, $indent) {
88 6 50       10 if ($_ eq "\t") {
89             # go to the next tab
90 0         0 $w = $tab_width * (int($w/$tab_width) + 1);
91             } else {
92 6         9 $w += 1;
93             }
94             }
95 4         6 $w;
96             }
97              
98             sub _ta_wrap {
99 10     10   24 my ($is_mb, $text, $width, $opts) = @_;
100 10   50     27 $width //= 80;
101 10   100     42 $opts //= {};
102              
103             # basically similar to Text::WideChar::Util's algorithm. we adjust for
104             # dealing with ANSI codes by splitting codes first (to easily do color
105             # resets/replays), then grouping into words and paras, then doing wrapping.
106              
107 10 50       46 my $_re1 = $is_mb ?
108             qr/($Text::WideChar::Util::re_cjk+)|(\S+)|(\s+)/ :
109             qr/()(\S+)|(\s+)/;
110              
111 10 50       28 my $_re2 = $is_mb ?
112             qr/($Text::WideChar::Util::re_cjk_class+)|
113             ($Text::WideChar::Util::re_cjk_negclass+)/x : undef;
114              
115 10         40 my @termst; # store term type, 's' (spaces), 'w' (word), 'c' (cjk word) or
116             # 'p' (parabreak)
117             my @terms; # store the text (w/ codes); for ws, only store the codes
118 10         0 my @pterms; # store the plaintext ver, but only for ws to check parabreak
119 10         0 my @termsw; # store width of each term, only for non-ws
120 10         0 my @termsc; # store color replay code
121             {
122 10         19 my @ch = ta_split_codes_single($text);
123 10         20 my $crcode = ""; # code for color replay to be put at the start of line
124 10         13 my $term = '';
125 10         13 my $pterm = '';
126 10         14 my $prev_type = '';
127 10         44 while (my ($pt, $c) = splice(@ch, 0, 2)) {
128             #use Data::Dump; print "D:chunk: "; dd [$pt, $c];
129              
130             # split into (CJK and non-CJK) words and spaces.
131              
132 25         33 my @s; # (WORD1, TYPE, ...) where type is 's' for space, 'c' for
133             # CJK word, or 'w' for non-CJK word
134 25         133 while ($pt =~ /$_re1/g) {
135 237 50 33     558 if ($is_mb && $1) {
    100          
136 0         0 push @s, $1, 'c';
137             } elsif ($3) {
138 114         390 push @s, $3, 's';
139             } else {
140 123 50       181 if ($is_mb) {
141 0         0 my $pt2 = $2;
142 0         0 while ($pt2 =~ /$_re2/g) {
143 0 0       0 if ($1) {
144 0         0 push @s, $1, 'c';
145             } else {
146 0         0 push @s, $2, 'w';
147             }
148             }
149             } else {
150 123         443 push @s, $2, 'w';
151             }
152             }
153             }
154              
155             #use Data::Dump; say "D:s=",Data::Dump::dump(\@s);
156              
157 25 100       34 my $only_code; $only_code = 1 if !@s;
  25         49  
158 25         28 while (1) {
159 262         387 my ($s, $s_type) = splice @s, 0, 2;
160 262   100     433 $s_type //= '';
161 262 100 100     592 last unless $only_code || defined($s);
162             # empty text, only code
163 242 100       319 if ($only_code) {
164 5         10 $s = "";
165 5 50       14 $term .= $c if defined $c;
166             }
167             #say "D:s=[$s] prev_type=$prev_type \@ch=",~~@ch," \@s=",~~@s;
168              
169 242 100 100     526 if ($s_type && $s_type ne 's') {
    100          
170 123 100 66     196 if ($prev_type eq 's') {
    50          
171             #say "D:found word, completed previous ws [$term]";
172 111         132 push @termst, 's';
173 111         131 push @terms , $term;
174 111         134 push @pterms, $pterm;
175 111         127 push @termsw, undef;
176 111         165 push @termsc, $crcode;
177             # start new word
178 111         126 $pterm = ''; $term = '';
  111         121  
179             } elsif ($prev_type && $prev_type ne $s_type) {
180             #say "D:found a ".($s_type eq 'c' ? 'CJK':'non-CJK')." word, completed previous ".($prev_type eq 'c' ? 'CJK':'non-CJK')." word [$term]";
181 0         0 push @termst, $prev_type;
182 0         0 push @terms , $term;
183 0         0 push @pterms, $pterm;
184 0 0       0 push @termsw, $is_mb ? Text::WideChar::Util::mbswidth($pterm):length($pterm);
185 0         0 push @termsc, $crcode;
186             # start new word
187 0         0 $pterm = ''; $term = '';
  0         0  
188             }
189 123         143 $pterm .= $s;
190 123 100 100     137 $term .= $s; $term .= $c if defined($c) && !@s;
  123         206  
191 123 100 100     217 if (!@s && !@ch) {
192             #say "D:complete word because this is the last token";
193 8         11 push @termst, $s_type;
194 8         10 push @terms , $term;
195 8         11 push @pterms, "";
196 8 50       16 push @termsw, $is_mb ? Text::WideChar::Util::mbswidth($pterm):length($pterm);
197 8         11 push @termsc, $crcode;
198             }
199             } elsif (length($s)) {
200 114 100       168 if ($prev_type ne 's') {
201             #say "D:found ws, completed previous word [$term]";
202 113         147 push @termst, $prev_type;
203 113         139 push @terms , $term;
204 113         126 push @pterms, "";
205 113 50       153 push @termsw, $is_mb ? Text::WideChar::Util::mbswidth($pterm):length($pterm);
206 113         151 push @termsc, $crcode;
207             # start new ws
208 113         126 $pterm = ''; $term = '';
  113         124  
209             }
210 114         128 $pterm .= $s;
211 114 100 100     201 $term .= $c if defined($c) && !@s;
212 114 100 100     176 if (!@s && !@ch) {
213             #say "D:complete ws because this is the last token";
214 2         3 push @termst, 's';
215 2         4 push @terms , $term;
216 2         2 push @pterms, $pterm;
217 2         3 push @termsw, undef;
218 2         5 push @termsc, $crcode;
219             }
220             }
221 242         278 $prev_type = $s_type;
222              
223 242 100       364 if (!@s) {
224 25 100 66     88 if (defined($c) && $c =~ /m\z/) {
225 17 100       85 if ($c eq "\e[0m") {
    50          
226             #say "D:found color reset, emptying crcode";
227 8         11 $crcode = "";
228             } elsif ($c =~ /m\z/) {
229             #say "D:adding to crcode";
230 9         15 $crcode .= $c;
231             }
232             }
233 25 100       60 last if $only_code;
234             }
235              
236             } # splice @s
237             } # splice @ch
238             }
239              
240             # mark parabreaks
241             {
242 10         13 my $i = 0;
  10         13  
  10         15  
243 10         24 while ($i < @pterms) {
244 234 100       319 if ($termst[$i] eq 's') {
245 113 100       166 if ($pterms[$i] =~ /[ \t]*(\n(?:[ \t]*\n)+)([ \t]*)/) {
246             #say "D:found parabreak";
247 2         7 $pterms[$i] = $1;
248 2         6 $termst[$i] = 'p';
249 2 50       6 if ($i < @pterms-1) {
250             # stick color code to the beginning of next para
251 2         9 $terms [$i+1] = $terms[$i] . $terms [$i+1];
252 2         4 $terms [$i] = "";
253             }
254 2 50       6 if (length $2) {
255             #say "D:found space after parabreak, splitting";
256 0         0 splice @termst, $i+1, 0, "s";
257 0         0 splice @terms , $i+1, 0, "";
258 0         0 splice @pterms, $i+1, 0, $2;
259 0         0 splice @termsw, $i+1, 0, undef;
260 0         0 splice @termsc, $i+1, 0, $termsc[$i];
261 0         0 $i += 2;
262 0         0 next;
263             }
264             }
265             }
266 234         312 $i++;
267             }
268             }
269              
270             #use Data::Dump::Color; my @d; for (0..$#terms) { push @d, {type=>$termst[$_], term=>$terms[$_], pterm=>$pterms[$_], termc=>$termsc[$_], termw=>$termsw[$_], } } dd \@d;
271             #return;
272              
273             #use Data::Dump; say "D:termst=".Data::Dump::dump(\@termst);
274             #use Data::Dump; say "D:terms =".Data::Dump::dump(\@terms);
275             #use Data::Dump; say "D:pterms=".Data::Dump::dump(\@pterms);
276             #use Data::Dump; say "D:termsw=".Data::Dump::dump(\@termsw);
277             #use Data::Dump; say "D:termsc=".Data::Dump::dump(\@termsc);
278              
279 10         50 my ($maxww, $minww);
280              
281             # now we perform wrapping
282              
283 10         0 my @res;
284             {
285 10   50     12 my $tw = $opts->{tab_width} // 8;
  10         29  
286 10 50       19 die "Please specify a positive tab width" unless $tw > 0;
287 10         13 my $optfli = $opts->{flindent};
288 10 50       27 my $optfliw; $optfliw = $is_mb ? Text::WideChar::Util::_mbs_indent_width($optfli, $tw) : _indent_width($optfli, $tw) if defined $optfli;
  10 100       22  
289 10         14 my $optsli = $opts->{slindent};
290 10 50       13 my $optsliw; $optsliw = $is_mb ? Text::WideChar::Util::_mbs_indent_width($optsli, $tw) : _indent_width($optsli, $tw) if defined $optsli;
  10 100       57  
291 10   50     37 my $optkts = $opts->{keep_trailing_space} // 0;
292 10         16 my $pad = $opts->{pad};
293 10         15 my $x = 0;
294 10         12 my $y = 0;
295 10         14 my ($fli, $sli, $fliw, $sliw);
296 10         10 my $is_parastart = 1;
297 10         16 my $line_has_word = 0;
298 10         12 my ($termt, $prev_t);
299             TERM:
300 10         30 for my $i (0..$#terms) {
301 234 100       331 $prev_t = $termt if $i;
302 234         276 $termt = $termst[$i];
303 234         260 my $term = $terms[$i];
304 234         259 my $pterm = $pterms[$i];
305 234         240 my $termw = $termsw[$i];
306 234 100       348 my $crcode = $i > 0 ? $termsc[$i-1] : "";
307             #say "D:term=[", ($termt eq 'w' ? $term : $pterm), "] ($termt)";
308              
309             # end of paragraph
310 234 100       333 if ($termt eq 'p') {
311 2         5 my $numnl = 0;
312 2         10 $numnl++ while $pterm =~ /\n/g;
313 2         8 for (1..$numnl) {
314 4 50 33     9 push @res, "\e[0m" if $crcode && $_ == 1;
315 4 50       18 push @res, " " x ($width-$x) if $pad;
316 4         7 push @res, "\n";
317 4         6 $x = 0;
318 4         5 $y++;
319             }
320 2         4 $line_has_word = 0;
321 2         3 $x = 0;
322 2         5 $is_parastart = 1;
323 2         5 next TERM;
324             }
325              
326 232 100       292 if ($is_parastart) {
327 12 100       26 unless (defined $fli) {
328             # this is the start of paragraph, determine indents
329              
330 10         15 $fli = $optfli;
331 10         13 $fliw = $optfliw;
332 10 50       17 if ($termt eq 's') {
333 0   0     0 $fli //= $pterm;
334 0 0 0     0 $fliw //= $is_mb ? Text::WideChar::Util::_mbs_indent_width($fli, $tw) : _indent_width($fli, $tw);
335             } else {
336 10   100     29 $fli //= "";
337 10   100     19 $fliw //= 0;
338             }
339              
340 10         15 $sli = $optsli;
341 10         12 $sliw = $optsliw;
342 10 100       17 unless (defined $sli) {
343 8         11 my $j = $i;
344 8   100     26 while ($j < @terms && $termst[$j] ne 'p') {
345 171 100       239 if ($termst[$j] eq 's') {
346 82 50       110 if ($pterms[$j] =~ /\n([ \t]+)/) {
347 0         0 $sli = $1;
348 0 0       0 $sliw = $is_mb ? Text::WideChar::Util::_mbs_indent_width($sli, $tw) : _indent_width($sli, $tw);
349 0         0 last;
350             }
351             }
352 171         354 $j++;
353             }
354             }
355 10   100     31 $sli //= "";
356 10   100     37 $sliw //= 0;
357              
358 10 50       21 die "Subsequent indent must be less than width" if $sliw >= $width;
359             }
360              
361             #say "D:inserting the fli [$fli] ($fliw)";
362 12         18 push @res, $fli;
363 12         16 $x += $fliw;
364             } # parastart
365              
366 232         231 $is_parastart = 0;
367              
368 232 100       317 if ($termt eq 's') {
369             # just print the codes
370 111         133 push @res, $term;
371              
372             # maintain terminating newline
373 111 100 100     217 if ($pterm =~ /\n/ && $i == $#terms) {
374 2 50       14 push @res, "\e[0m" if $crcode;
375 2 50       5 push @res, " " x ($width-$x) if $pad;
376 2         4 push @res, "\n";
377 2         4 $line_has_word = 0;
378             }
379             }
380              
381 232 100       349 if ($termt ne 's') {
382             # we need to chop long words
383 121         288 my @words;
384             my @wordsw;
385 121         0 my @wordst; # c if cjk, w if not
386 121         0 my @wordswsb; # whether there are ws before the word
387 121         123 my $j = 0;
388 121         124 my $c = ""; # see below for explanation
389 121         124 while (1) {
390 124         124 $j++;
391             # most words shouldn't be that long. and we don't need to
392             # truncate long CJK word first here because it will get
393             # truncated later.
394 124 100 66     199 if ($termw <= $width-$sliw || $termt eq 'c') {
395 121         243 push @words , $c . $term;
396 121         129 push @wordsw , $termw;
397 121         133 push @wordst , $termt;
398 121 100 100     268 push @wordswsb, ($prev_t && $prev_t eq 's')?1:0;
399 121         137 last;
400             }
401             #use Data::Dump; print "D:truncating long word "; dd $term;
402 3 50       11 my $res = $is_mb ? ta_mbtrunc($term, $width-$sliw, 1) :
403             ta_trunc($term, $width-$sliw, 1);
404              
405 3         7 my ($tword, $twordw);
406 3 50       5 if ($j == 1) {
407 3         4 $tword = $res->[0];
408 3         6 $twordw = $res->[1];
409             } else {
410             # since ta_{,mb}trunc() adds the codes until the end of
411             # the word, to avoid messing colors, for the second word
412             # and so on we need to replay colors by prefixing with:
413             # \e[0m (reset) + $crcode + (all the codes from the
414             # start of the long word up until the truncated
415             # position, stored in $c).
416             #
417             # there might be faster way, but it is expected that
418             # long words are not that common.
419 0 0       0 $tword = ($crcode ? "\e[0m" . $crcode : "") .
420             $c . $res->[0];
421 0         0 $twordw = $res->[1];
422             }
423 3         8 $c .= ta_extract_codes(substr($term, 0, $res->[2]));
424             #use Data::Dump; print "D:truncated word is "; dd $tword;
425              
426 3         6 push @words , $tword;
427 3         4 push @wordsw , $twordw;
428 3         5 push @wordst , $termt;
429 3 100 66     14 push @wordswsb, $j == 1 ? (($prev_t && $prev_t eq 's')?1:0) : 0;
    50          
430 3         5 $term = substr($term, $res->[2]);
431 3 50       6 $termw = $is_mb ? _ta_mbswidth0($term) : ta_length($term);
432             }
433              
434             #use Data::Dump; print "D:words="; dd \@words; print "D:wordsw="; dd \@wordsw; print "D:wordswsb="; dd \@wordswsb;
435              
436             # the core of the wrapping algo
437 121         191 for my $word (@words) {
438 124         144 my $wordw = shift @wordsw;
439 124         149 my $wordt = shift @wordst;
440 124         140 my $ws_before = shift @wordswsb;
441             #say "D:x=$x word=$word wordw=$wordw wordt=$wordt ws_before=$ws_before line_has_word=$line_has_word width=$width";
442              
443 124 100 100     263 $maxww = $wordw if !defined($maxww) || $maxww < $wordw;
444 124 100 100     247 $minww = $wordw if !defined($minww) || $minww > $wordw;
445              
446 124 100       198 if ($x + ($line_has_word ? 1:0) + $wordw <= $width) {
    100          
447 106 100 66     220 if ($line_has_word && $ws_before) {
448 94         115 push @res, " ";
449 94         94 $x++;
450             }
451 106         154 push @res, $word;
452 106         109 $x += $wordw;
453             } else {
454             # line break
455 18         20 while (1) {
456 18 50       31 if ($wordt eq 'c') {
457             # a CJK word can be line-broken
458 0         0 my $res;
459 0 0       0 if ($ws_before) {
460 0         0 $res = ta_mbtrunc($word, $width-$x-1, 1);
461 0         0 push @res, " ", $res->[0];
462             } else {
463 0         0 $res = ta_mbtrunc($word, $width-$x, 1);
464 0         0 push @res, $res->[0];
465             }
466             #say "D:truncated CJK word: $word (".length($word)."), ".($width-$x)." -> $res->[0] (".length($res->[0]).") & $res->[1], remaining=$res->[3] (".length($res->[3]).")";
467 0         0 $word = $res->[3];
468 0         0 $wordw = _ta_mbswidth0($res->[3]);
469             } else {
470 18 100       29 push @res, "\e[0m" if $crcode;
471             }
472 18 50       27 push @res, " " x ($width-$x) if $pad;
473 18 50 66     48 push @res, " " if $ws_before && $optkts;
474 18         26 push @res, "\n";
475 18         19 $y++;
476 18         26 push @res, $crcode;
477 18         21 push @res, $sli;
478              
479 18 50       27 if ($sliw + $wordw <= $width) {
480 18         21 push @res, $word;
481 18         19 $x = $sliw + $wordw;
482 18         23 last;
483             } else {
484             # word still too long, break again
485 0         0 $x = $sliw;
486 0         0 $ws_before = 0;
487             }
488             }
489             }
490 124         234 $line_has_word++;
491             }
492              
493             }
494             } # for term
495 10 50 66     37 push @res, " " x ($width-$x) if $line_has_word && $pad;
496             }
497              
498 10 100       22 if ($opts->{return_stats}) {
499 1         10 return [join("", @res), {
500             max_word_width => $maxww,
501             min_word_width => $minww,
502             }];
503             } else {
504 9         143 return join("", @res);
505             }
506             }
507              
508             sub ta_wrap {
509 10     10 0 8067 _ta_wrap(0, @_);
510             }
511              
512             sub ta_mbwrap {
513 0     0 0 0 _ta_wrap(1, @_);
514             }
515              
516             sub _ta_pad {
517 7     7   14 my ($is_mb, $text, $width, $which, $padchar, $is_trunc) = @_;
518 7 100       16 if ($which) {
519 3         5 $which = substr($which, 0, 1);
520             } else {
521 4         7 $which = "r";
522             }
523 7   100     28 $padchar //= " ";
524              
525 7 50       15 my $w = $is_mb ? _ta_mbswidth0($text) : ta_length($text);
526 7 100 66     19 if ($is_trunc && $w > $width) {
527 1 50       6 my $res = $is_mb ?
528             ta_mbtrunc($text, $width, 1) : ta_trunc($text, $width, 1);
529 1         4 $text = $res->[0] . ($padchar x ($width-$res->[1]));
530             } else {
531 6 100       17 if ($which eq 'l') {
    100          
532 1         5 $text = ($padchar x ($width-$w)) . $text;
533             } elsif ($which eq 'c') {
534 1         4 my $n = int(($width-$w)/2);
535 1         5 $text = ($padchar x $n) . $text . ($padchar x ($width-$w-$n));
536             } else {
537 4 100       16 $text .= ($padchar x ($width-$w)) if $width > $w;
538             }
539             }
540 7         31 $text;
541             }
542              
543             sub ta_pad {
544 7     7 0 2346 _ta_pad(0, @_);
545             }
546              
547             sub ta_mbpad {
548 0     0 0 0 _ta_pad(1, @_);
549             }
550              
551             sub _ta_trunc {
552 16     16   35 my ($is_mb, $text, $width, $return_extra) = @_;
553              
554             # return_extra (undocumented): if set to 1, will return [truncated_text,
555             # visual width, length(chars) up to truncation point, rest of the text not
556             # included]
557              
558 16 50       38 my $w = $is_mb ? _ta_mbswidth0($text) : ta_length($text);
559 16 100       39 if ($w <= $width) {
560 2 50       12 return $return_extra ? [$text, $w, length($text), ''] : $text;
561             }
562 14         27 my @p = ta_split_codes_single($text);
563 14         23 my $res = '';
564 14         19 my $append = 1; # whether we should add more text
565 14         19 my $code4rest = '';
566 14         18 my $rest = '';
567 14         17 $w = 0;
568 14         19 my $c = 0;
569             #use Data::Dump; dd \@p;
570 14         40 while (my ($t, $ansi) = splice @p, 0, 2) {
571             #say "D: t=<$t>, \@p=", ~~@p, ", code4rest=<$code4rest>, rest=<$rest>";
572 45 100       74 if ($append) {
573 32 50       48 my $tw = $is_mb ? Text::WideChar::Util::mbswidth($t) : length($t);
574             #say "D: tw=$tw";
575 32 100       50 if ($w+$tw <= $width) {
576 22         30 $res .= $t;
577 22         27 $w += $tw;
578 22         28 $c += length($t);
579 22 100       40 $append = 0 if $w == $width;
580             #say "D:end1" unless $append;
581             } else {
582 10 50       32 my $tres = $is_mb ?
583             Text::WideChar::Util::mbtrunc($t, $width-$w, 1) :
584             [substr($t, 0, $width-$w), $width-$w, $width-$w];
585             #use Data::Dump; dd $tres;
586 10         18 $res .= $tres->[0];
587 10         17 $w += $tres->[1];
588 10         27 $c += $tres->[2];
589 10         22 $rest = substr($t, $tres->[2]);
590 10         14 $append = 0;
591             #say "D:end2";
592             }
593             } else {
594 13         17 $rest .= $t;
595             }
596 45 100       72 if (defined $ansi) {
597 39 100       54 if ($append) {
598 18 100       27 if ($ansi eq "\e[0m") {
599             #say "D:found color reset, resetting code4rest";
600 1         2 $c = length($ansi);
601 1         2 $code4rest = $ansi;
602             } else {
603 17         22 $c += length($ansi);
604 17         22 $code4rest .= $ansi;
605             }
606 18         42 $res .= $ansi;
607             } else {
608 21         25 $res .= $ansi;
609 21         45 $rest .= $ansi;
610             }
611             }
612             }
613              
614             # ta_trunc/ta_mbtrunc currently adds unpruned color codes at the end of
615             # truncated string. pruned meaning strings of color codes right before reset
616             # code is removed, e.g. \e[1m\e[30m...\e[0m becomes \e[0m. you might want to
617             # prune the result of trunc using _ta_prune_codes.
618              
619 14 100       23 if ($return_extra) {
620 10         33 return [$res, $w, $c, $code4rest . $rest];
621             } else {
622 4         18 return $res;
623             }
624             }
625              
626             sub _ta_prune_codes {
627 3     3   7 my $text = shift;
628 3         46 $text =~ s/($re_mult)\e\[0m/\e\[0m/g;
629 3         21 $text;
630             }
631              
632             sub ta_trunc {
633 10     10 0 2488 _ta_trunc(0, @_);
634             }
635              
636             sub ta_mbtrunc {
637 0     0 0 0 _ta_trunc(1, @_);
638             }
639              
640             sub _ta_highlight {
641 2     2   11 my ($is_all, $text, $needle, $color) = @_;
642              
643             # break into chunks
644 2         5 my (@chptext, @chcode, @chsavedc); # chunk plain texts, codes, saved codes
645 2         4 my $sc = "";
646 2         4 my $plaintext = "";
647 2         8 my @ch = ta_split_codes_single($text);
648 2         13 while (my ($pt, $c) = splice(@ch, 0, 2)) {
649 8         14 push @chptext , $pt;
650 8         13 push @chcode , $c;
651 8         10 push @chsavedc, $sc;
652 8         11 $plaintext .= $pt;
653 8 50 33     33 if (defined($c) && $c =~ /m\z/) {
654 8 100       27 if ($c eq "\e[0m") {
    50          
655 2         7 $sc = "";
656             } elsif ($c =~ /m\z/) {
657 6         16 $sc .= $c;
658             }
659             }
660             }
661             #use Data::Dump; print "\@chptext: "; dd \@chptext; print "\@chcode: "; dd \@chcode; print "\@chsavedc: "; dd \@chsavedc;
662              
663             # gather a list of needles to highlight, with their positions
664 2         4 my (@needle, @npos);
665 2 50       6 if (ref($needle) eq 'Regexp') {
666 0         0 my @m = $plaintext =~ /$needle/g;
667 0 0       0 return $text unless @m;
668 0         0 my $pos = 0;
669 0         0 while ($pos < length($plaintext)) {
670 0         0 my @pt;
671 0         0 for (@m) {
672 0         0 my $p = index($plaintext, $_, $pos);
673 0 0       0 push @pt, [$p, $_] if $p >= 0;
674             }
675 0 0       0 last unless @pt;
676 0         0 my $pmin = $pt[0][0];
677 0         0 my $t = $pt[0][1];
678 0         0 for (@pt) {
679 0 0 0     0 if ($pmin > $_->[0] ||
      0        
680             $pmin==$_->[0] && length($t) < length($_->[1])) {
681 0         0 $pmin = $_->[0];
682 0         0 $t = $_->[1];
683             }
684             }
685 0         0 push @needle, $t;
686 0         0 push @npos , $pmin;
687 0 0       0 last unless $is_all;
688 0         0 $pos = $pmin + length($t);
689             }
690             } else {
691 2         4 my $pos = 0;
692 2         2 while (1) {
693             #say "D:finding '$needle' in '$plaintext' from pos '$pos'";
694 4         9 my $p = index($plaintext, $needle, $pos);
695 4 100       8 last if $p < 0;
696 3         7 push @needle, $needle;
697 3         4 push @npos , $p;
698 3 100       6 last unless $is_all;
699 2         3 $pos = $p + length($needle);
700 2 50       5 last if $pos >= length($plaintext);
701             }
702 2 50       5 return $text unless @needle;
703             }
704             #use Data::Dump; print "\@needle: "; dd \@needle; print "\@npos: "; dd \@npos;
705              
706 2         4 my @res;
707 2         16 my $found = 1;
708 2         5 my $pos = 0;
709 2         4 my $i = 0;
710 2         4 my $curneed = shift @needle;
711 2         3 my $npos = shift @npos;
712             CHUNK:
713 2         5 while (1) {
714 11 100       19 last if $i >= @chptext;
715 9         14 my $pos2 = $pos+length($chptext[$i])-1;
716 9         11 my $npos2 = $npos+length($curneed)-1;
717             #say "D: chunk=[$chptext[$i]], npos=$npos, npos2=$npos2, pos=$pos, pos2=$pos2";
718 9 100 66     40 if ($pos > $npos2 || $pos2 < $npos || !$found) {
      66        
719             #say "D:inserting chunk: [$chptext[$i]]";
720             # no need to highlight
721 4         6 push @res, $chptext[$i];
722 4 50       11 push @res, $chcode[$i] if defined $chcode[$i];
723 4         16 goto L1;
724             }
725              
726             # there is chunk text at the left of needle?
727 5 100       11 if ($pos < $npos) {
728 3         6 my $pre = substr($chptext[$i], 0, $npos-$pos);
729             #say "D:inserting pre=[$pre]";
730 3         6 push @res, $pre;
731             }
732              
733 5         22 my $npart = substr($curneed,
734             max(0, $pos-$npos),
735             min($pos2, $npos2)-max($pos, $npos)+1);
736 5 50       9 if (length($npart)) {
737             #say "D:inserting npart=[$npart]";
738 5         10 push @res, $color, $npart;
739 5         14 push @res, "\e[0m";
740             #use Data::Dump; dd [$chsaved[$i], $chcode[$i]];
741 5         8 push @res, $chsavedc[$i];
742             }
743              
744             # is there chunk text at the right of needle?
745 5 100       11 if ($npos2 <= $pos2) {
746             #say "D:We have run past current needle [$curneed]";
747 3         55 my $post = substr($chptext[$i], $npos2-$pos+1);
748              
749 3 100       8 if (@needle) {
750 1         3 $curneed = shift @needle;
751 1         2 $npos = shift @npos;
752             #say "D:Finding the next needle ($curneed) at pos $npos";
753 1         3 $pos = $npos2+1;
754 1         2 $chptext[$i] = $post;
755 1         2 $found = 1;
756 1         3 redo CHUNK;
757             } else {
758             # we're done finding needle
759 2         3 $found = 0;
760             }
761              
762 2 50       5 if (!$found) {
763             #say "D:inserting post=[$post]";
764 2         4 push @res, $post;
765 2 50       6 push @res, $chcode[$i] if defined $chcode[$i];
766             }
767             }
768              
769             L1:
770 8         9 $pos = $pos2+1;
771 8         11 $i++;
772             }
773              
774 2         19 join "", @res;
775             }
776              
777             sub ta_highlight {
778 1     1 0 2300 _ta_highlight(0, @_);
779             }
780              
781             sub ta_highlight_all {
782 1     1 0 2382 _ta_highlight(1, @_);
783             }
784              
785             sub ta_add_color_resets {
786 1     1 0 2268 my (@text) = @_;
787              
788 1         2 my @res;
789 1         2 my $i = 0;
790 1         4 my $savedc = "";
791 1         3 for my $text (@text) {
792 4         6 $i++;
793 4 100 100     15 my $newt = $i > 1 && !$savedc ? "\e[0m" : $savedc;
794              
795             # break into chunks
796 4         8 my @ch = ta_split_codes_single($text);
797 4         12 while (my ($t, $c) = splice(@ch, 0, 2)) {
798 7         10 $newt .= $t;
799 7 100 66     26 if (defined($c) && $c =~ /m\z/) {
800 3         4 $newt .= $c;
801 3 100       22 if ($c eq "\e[0m") {
    50          
802 1         6 $savedc = "";
803             } elsif ($c =~ /m\z/) {
804 2         7 $savedc .= $c;
805             }
806             }
807             }
808              
809 4 100 66     14 $newt .= "\e[0m" if $savedc && $i < @text;
810 4         7 push @res, $newt;
811             }
812              
813 1         9 @res;
814             }
815              
816             sub _ta_substr {
817 3     3   5 my $is_mb = shift;
818 3         5 my $str = shift;
819 3         5 my $pos = shift;
820 3         4 my $len = shift;
821              
822 3         8 my $res1 = _ta_trunc($is_mb, $str, $pos, 1);
823 3         5 my $res2 = _ta_trunc($is_mb, $res1->[3], $len, 1);
824              
825 3 100       6 if (@_) {
826             # left + replacement + right
827 2         8 return _ta_prune_codes($res1->[0] . $_[0] . $res2->[3]);
828             } else {
829 1         3 return _ta_prune_codes($res2->[0]);
830             }
831             }
832              
833             sub ta_substr {
834 3     3 0 2794 _ta_substr(0, @_);
835             }
836              
837             sub ta_mbsubstr {
838 0     0 0   _ta_substr(1, @_);
839             }
840              
841              
842             1;
843             # ABSTRACT: Base for Text::ANSI::{Util,WideUtil}
844              
845             __END__