File Coverage

blib/lib/Text/ANSI/BaseUtil.pm
Criterion Covered Total %
statement 410 575 71.3
branch 186 352 52.8
condition 82 123 66.6
subroutine 28 37 75.6
pod 0 22 0.0
total 706 1109 63.6


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