Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl-Sensitive Sunglasses
 
PerlMonks  

Text::ANSI::Util for wrapping "colorful" text

by ibm1620 (Pilgrim)
on May 08, 2022 at 22:59 UTC ( #11143680=perlquestion: print w/replies, xml ) Need Help??

ibm1620 has asked for the wisdom of the Perl Monks concerning the following question:

I'm using Term::ANSIColor to apply colors and styles (boldface, underline) to ASCII text strings, which I then want to wrap to a certain width for printing to the terminal. I found Text::ANSI::Util qw/ta_wrap/ which purports to do this properly for strings with these escape sequences, but I'm encountering what appear to be bugs. On the other hand, am I just not using the module correctly?

Here's a test program that illustrates some problems. Since I can't show the colors or styling I'll try to add some commentary to the output.

#!/usr/bin/env perl use 5.010; use warnings; use strict; use Term::ANSIColor qw(color :constants); use Text::ANSI::Util qw/ta_wrap/; sub show { my $line = shift; my $wrapped = ta_wrap($line, 30, {flindent => q[ ] x 5, slindent => q[ ] x 5 } ); say "BEFORE:"; say $line; # print unpack('H*', $_) . ' ' for split //, $line; say ''; say "AFTER:"; say $wrapped; # print unpack('H*', $_) . ' ' for split //, $wrapped; say "\n"; } show "Marley was " . BOLD . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . BOLD . "dead, " . RESET . "to be +gin with."; show "Marley was " . UNDERLINE . color('green') . "dead, to begin with +. " . RESET . "There is no doubt..."; show "Marley was " . UNDERLINE . "dead, to begin with +. " . RESET . "There is no doubt...";
Output:
BEFORE: Marley was dead, to begin with. AFTER: ## 'dead,' is bold and green but we lost the space preceding it + ## Marley wasdead, to begin with. BEFORE: Marley was dead, to begin with. AFTER: ## 'dead,' is green. This is correctly rendered. ## Marley was dead, to begin with. BEFORE: Marley was dead, to begin with. AFTER: ## 'dead,' is bold. This is correctly rendered. ## Marley was dead, to begin with. BEFORE: Marley was dead, to begin with. There is no doubt... AFTER: ## 'dead, to begin with. ' is underlined/green but the precedin +g space was lost, and underlining between "begin" and "with" persiste +d across the margin ## Marley wasdead, to begin with. There is no doubt... BEFORE: Marley was dead, to begin with. There is no doubt... AFTER: ## Underlining only, but again the margin between "begin" and " +with" is underlined. ## Marley was dead, to begin with. There is no doubt...

Replies are listed 'Best First'.
Re: Text::ANSI::Util for wrapping "colorful" text (updated)
by vr (Curate) on May 09, 2022 at 07:46 UTC

    Looks like qualified bugs to me, because of duck test. Vanishing space character can be fixed in line #160, it should be $prev_type instead of '', I think. As to other issues, there are quite a few hundred lines of code, I gave up after some poking around. In the very next line, #161, condition never evaluates to false (or does it?), I don't understand what its purpose would be, etc.

    I think you missed an issue (easily seen with underline) of ANSI code leaking into preceding whitespace. If debugging output is un-commented in #273-277, I think it's clear it is a matter of a space char either being prepended or appended to a term in further processing, but again I'm not ready to find where exactly, in code, it happens. I think something similar would fix the leading indentation issue.

    To add to things to fix for the author, I tried to use Text::ANSI::WideUtil qw/ta_mbwrap/;, it fails with call to non-existent Text::WideChar::Util::_mbs_indent_width

    Update 22/05/10. Here's a patch, seems OK with limited testing. Issues mentioned in this thread are fixed, tests are passed. As strange as it is, color continuing in next line's left margin is "a feature", see #153. For now, there's ugly condition to both satisfy existing test and provide clean white-space margin. As I expected, if term is ANSI-code only, it must have originated from code following WS originally (of course not including the case of code in the very start of text), therefore a space char should be prepended, not appended, to it.

    Update#2 22/05/10. Sadly, there still was leak of formatting into preceding WS in case of several adjacent codes (as "Marley " . BOLD . GREEN . "was" . RESET). I have updated patch, replacing $re with $re_mult. Damn, I thought I checked that :(. Well, there's still a convoluted/artificial case such as " ". UNDERLINE . " was" . RESET;. Multiple spaces being compressed to one, where should code go? Result may look OK if we agree on "where", but I don't like that debugging output says @termsw stores "0" and "undef" for these 2 spaces. I think it's already for the author to unravel :(

    --- BaseUtil.pm.original Sun Aug 8 19:50:33 2021 +++ BaseUtil.pm Tue May 10 12:16:22 2022 @@ -157,7 +157,7 @@ my $only_code; $only_code = 1 if !@s; while (1) { my ($s, $s_type) = splice @s, 0, 2; - $s_type //= ''; + $s_type //= $prev_type; last unless $only_code || defined($s); # empty text, only code if ($only_code) { @@ -445,7 +445,7 @@ if ($x + ($line_has_word ? 1:0) + $wordw <= $widt +h) { if ($line_has_word && $ws_before) { - push @res, " "; + splice @res, $#res + $res[-1] !~ /^$re_mu +lt$/, 0, ' '; $x++; } push @res, $word; @@ -474,7 +474,7 @@ push @res, "\n"; $y++; push @res, $crcode; - push @res, $sli; + splice @res, $#res + $sli =~ /\S/, 0, $sl +i; if ($sliw + $wordw <= $width) { push @res, $word;

        Yes, it does. But, unless you want to familiarize yourself with "patching" (please do, either now or at leisure), in this trivial case: if as I understand you have the distribution already installed, it's a matter of simple replacement of just 2 lines in a text editor. Third line edit, in "patch", is to pacify a (dubious -- I hope Perlancar would agree) test during installation. Just make a backup copy and edit BaseUtil.pm. Task at hand is kind of borderline in whether sticking to maxima "do not re-invent the wheel" would ultimately benefit you or not. Maybe you'll later encounter unexpected cases of "wrapping text" foreseen by Perlancar many years ago, but not by you with home-made implementation. Maybe not. Sorry for mentoring :)

Re: Text::ANSI::Util for wrapping "colorful" text
by tybalt89 (Monsignor) on May 10, 2022 at 00:49 UTC

    First pass at trying to write my own.

    #!/usr/bin/perl use strict; # https://perlmonks.org/?node_id=11143680 use warnings; use 5.010; use Term::ANSIColor qw(color :constants); sub ta_wrap { my ($text, $width, $optionsref) = @_; my %pads = (flindent => '', slindent => '', %{ $optionsref // {} } ) +; $text = $pads{flindent} . $text; my $out = ''; my $save = ''; while( $text =~ s/^(.+)(?: +|$)(??{ $width < length join '', split m{\e[[\d;]*m}, $1 and '(*FAIL)' })// ) { $out .= $1 . "\e[m\n"; $save .= join '', $& =~ /\e[[\d;]*m/g; $save =~ s/^.*\e\[0?m//; # clear up to and including last RESET length $text and $text = "$pads{slindent}$save$text"; } return "$out $text"; } sub show { my $line = shift; my $wrapped = ta_wrap($line, 30, {flindent => q[ ] x 5, slindent => q[ ] x 5 } ); say "BEFORE:"; say $line; say "AFTER:"; say $wrapped; } show "Marley was " . BOLD . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . BOLD . "dead, " . RESET . "to be +gin with."; show "Marley was " . UNDERLINE . color('green') . "dead, to begin with +. " . RESET . "There is no doubt..."; show "Marley was " . UNDERLINE . "dead, to begin with +. " . RESET . "There is no doubt..."; show "Marley was dead, to begin with. There is no doubt...";

    Give this a shot...

      That is awesome. It appears to work fine on the test data. Tomorrow I'll plug it into my application and shake it out more.

      I was going to have a shot at writing it myself, but clearly I have more to gain from just understanding your routine.

      I found that the presence of a newline character in the input causes a loop.
        #!/usr/bin/perl use strict; # https://perlmonks.org/?node_id=11143680 use warnings; use 5.010; use Term::ANSIColor qw(color :constants); sub ta_wrap { my ($text, $width, $optionsref) = @_; tr/\n\t/ /, s/ +/ /g for $text; # replace newlines and tabs my %pads = (flindent => '', slindent => '', %{ $optionsref // {} } ) +; $text = $pads{flindent} . $text; my $out = ''; my $carryover = ''; while( $text =~ s/^(.+)(?: +|$)(??{ $width < length join '', split m{\e[[\d;]*m}, $1 and '(*FAIL)' })// ) { $out .= "$1\e[m\n"; $carryover .= join '', $1 =~ /\e[[\d;]*m/g; $carryover =~ s/^.*\e\[0?m//; # clear up to and including last RES +ET $text =~ /./ and $text = "$pads{slindent}$carryover$text"; } return $out; } sub show { my $line = shift; my $wrapped = ta_wrap($line, 30, {flindent => q[ ] x 5, slindent => q[ ] x 5 } ); say "BEFORE:"; say $line; say "AFTER:"; say $wrapped; } show "Marley was " . BOLD . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . BOLD . "dead, " . RESET . "to be +gin with."; show "Marley was " . UNDERLINE . color('green') . "dead, to begin with +. " . RESET . "There is no doubt..."; show "Marley was " . UNDERLINE . "dead, to begin with +. " . RESET . "There is no doubt..."; show "Marley was dead, to begin with. There is no doubt...";

        Some tweaks and renaming variables for clarity.

Re: Text::ANSI::Util for wrapping "colorful" text
by tybalt89 (Monsignor) on Aug 03, 2022 at 00:13 UTC

    Latest version. Seems to be faster, maybe, perhaps...

    Interesting technique of overshoot and backup, with regex inside regex.

    #!/usr/bin/perl use strict; # https://perlmonks.org/?node_id=11143680 use warnings; use 5.010; use Term::ANSIColor qw(color :constants); sub ta_wrap { my ($text, $width, $optionsref) = @_; tr/\n\t/ /, s/ +/ /g for $text; # replace newlines and tabs my %pads = (flindent => '', slindent => '', %{ $optionsref // {} } ) +; $text = $pads{flindent} . $text; my $out = ''; while( $text =~ /^(.{$width,}?\S)(?: |$)(??{ $width >= length $1 =~ s"\e[[\d;]*m""gr ? '(*FAIL)' : '' })/ ) { my $take = $1 =~ /^(.*\S) +/ ? $1 : die "wrap failed"; $out .= "$take\e[m\n"; my $replace = $+[0]; substr $text, 0, $replace, $pads{slindent} . join('', $take =~ /\e[[\d;]*m/g) =~ s/.*\e\[0?m//r =~ s/(\e[[\d;]*m)(?=.*\1)//gr =~ s/\e\[[39][0-7]m(?=.*\e\[[39][0-7]m)//gr; } return "$out$text\e[m\n"; } sub show { my $line = shift; my $wrapped = ta_wrap($line, 30, {flindent => q[ ] x 5, slindent => q[ ] x 5 } ); say "BEFORE:"; say $line; say "AFTER:"; say $wrapped; } show "Marley was " . BOLD . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . color('green') . "dead, " . RESET . "to be +gin with."; show "Marley was " . BOLD . "dead, " . RESET . "to be +gin with."; show "Marley was " . UNDERLINE . color('green') . BOLD . "dead, to beg +in with. " . RESET . "There is no doubt..."; show "Marley was " . UNDERLINE . "dead, to begin with +. " . RESET . "There is no doubt..."; show "Marley was dead, to begin with. There is no doubt..."; show UNDERLINE . "Marley was dead, to begin with. There is no doubt... +" . RESET; show qq(\e[93m01:06:52:18 [CMP] Okay, Well look at the bottom by the f +irst 2 inches without any constant direction. Indeed, at the bottom o +f this wealth of nature. President Barbicane, however, lost not one o +f the Barbados. It is but an atom of carbonic acid, by a moment. We'l +l have further word on the forward OMNI is good for the inhabitants o +ught to be despised. But that day, about eleven o'clock position from + the earth. Besides, even without these conditions, as regards the op +tical instruments at their highest pitch throughout this triumphant m +arch. Michel Ardan hoped to get any crew status report. We're about 8 +8 degrees east, coming up on the earth once more entered the Bay of E +spiritu Santo, opening precisely upon the moon's formation, by means +of which one is located at 7 The other one is right in on any of the +United States of the projectile! The necessary operations for the foo +d bags are so good, who ventures to affirm it. "True," rejoined the m +ajor. "True," replied Barbicane. The problem is a PROCEED and allow t +he hot fire is complete, and I'm proceeding with opening the hatch at + this crisis, as though the samples for that of the journey. Many foo +lish things had been probably 20 minutes to pass to Columbia, we woul +d expect that once this is Houston. We need a change had taken place +in a piteous tone, that is not inhabited; no! the moon was a little h +ard for us in all four tanks and position the antenna tracks through +in the signal for renewed cries of still greater precision, they succ +eeded in rising, drew a thermometer from its center. "Then," said Nic +holl, it is a little scratchy. It looks fairly good size. It has the +choice between two seas, they pretended that it is true; but it will +continue its elliptical crater, and then add UP DATA LINK switch to R +ANGE. We're going to be in opposition. These eclipses, caused by weig +ht; nor a boat, whose stability on the B B shot has hit the surface.\ +e[0m);
      I just noticed this post now. Will play with it and see how it deals with the situations I discovered earlier today...

      Update

      Very nice! This appears to handle all the conditions I reported today correctly (problem with standalone '0' and with length calculation of escape sequences). Also, no stalls for long strings. Kudos and thanks!

      I notice that you're inserting \e[m before line endings. Apparently neither ta_length() nor ta_pad() can deal with this sequence (i.e. treat it as non-printing). I changed the two places in your code where you insert that sequence to insert \e[0m instead, and it passed my limited testing.

      I don't know if that's the right fix, though. :-)

        The sequence \e[m is exactly the same as \e[0m (according to wikipedia);

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11143680]
Approved by Discipulus
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others perusing the Monastery: (5)
As of 2022-09-29 10:22 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    I prefer my indexes to start at:




    Results (125 votes). Check out past polls.

    Notices?