Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions lib/cli/Colors.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,90 @@ static public function getStringCache() {
static public function clearStringCache() {
self::$_string_cache = array();
}

/**
* Get the ANSI reset code.
*
* @return string The ANSI reset code.
*/
static public function getResetCode() {
return "\x1b[0m";
}

/**
* Wrap a pre-colorized string at a specific width, preserving color codes.
*
* This function wraps text that contains ANSI color codes, ensuring that:
* 1. Color codes are never split in the middle
* 2. Active colors are properly terminated and restored across line breaks
* 3. The wrapped segments maintain the correct display width
*
* @param string $string The string to wrap (with ANSI codes).
* @param int $width The maximum display width per line.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return array Array of wrapped string segments.
*/
static public function wrapPreColorized( $string, $width, $encoding = false ) {
$wrapped = array();
$current_line = '';
$current_width = 0;
$active_color = '';

// Pattern to match ANSI escape sequences
$ansi_pattern = '/(\x1b\[[0-9;]*m)/';

// Split the string into parts: ANSI codes and text
$parts = preg_split( $ansi_pattern, $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );

foreach ( $parts as $part ) {
// Check if this part is an ANSI code
if ( preg_match( $ansi_pattern, $part ) ) {
// It's an ANSI code, add it to current line without counting width
$current_line .= $part;

// Track the active color
if ( preg_match( '/\x1b\[0m/', $part ) ) {
// Reset code
$active_color = '';
} elseif ( preg_match( '/\x1b\[([0-9;]+)m/', $part, $matches ) && $matches[1] !== '0' ) {
Comment thread
swissspidy marked this conversation as resolved.
Outdated
// Non-reset color code
$active_color = $part;
}
} else {
// It's text content, process it character by character
$text_length = \cli\safe_strlen( $part, $encoding );
$offset = 0;

while ( $offset < $text_length ) {
$char = \cli\safe_substr( $part, $offset, 1, false, $encoding );
$char_width = \cli\strwidth( $char, $encoding );

// Check if adding this character would exceed the width
if ( $current_width + $char_width > $width && $current_width > 0 ) {
// Need to wrap - finish current line
if ( $active_color ) {
$current_line .= self::getResetCode();
}
$wrapped[] = $current_line;

// Start new line
$current_line = $active_color ? $active_color : '';
$current_width = 0;
}

// Add the character
$current_line .= $char;
$current_width += $char_width;
$offset++;
}
}
}

// Add the last line if there's any content
if ( $current_line !== '' && $current_line !== $active_color ) {
Comment thread
swissspidy marked this conversation as resolved.
Outdated
$wrapped[] = $current_line;
}

return $wrapped;
}
}
23 changes: 15 additions & 8 deletions lib/cli/table/Ascii.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,21 @@ public function row( array $row ) {

$wrapped_lines = [];
foreach ( $split_lines as $line ) {
do {
$wrapped_value = \cli\safe_substr( $line, 0, $col_width, true /*is_width*/, $encoding );
$val_width = Colors::width( $wrapped_value, self::isPreColorized( $col ), $encoding );
if ( $val_width ) {
$wrapped_lines[] = $wrapped_value;
$line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding );
}
} while ( $line );
// Use the new color-aware wrapping for pre-colorized content
if ( self::isPreColorized( $col ) && Colors::width( $line, true, $encoding ) > $col_width ) {
$line_wrapped = Colors::wrapPreColorized( $line, $col_width, $encoding );
$wrapped_lines = array_merge( $wrapped_lines, $line_wrapped );
} else {
// For non-colorized content, use the original logic
do {
$wrapped_value = \cli\safe_substr( $line, 0, $col_width, true /*is_width*/, $encoding );
$val_width = Colors::width( $wrapped_value, self::isPreColorized( $col ), $encoding );
if ( $val_width ) {
$wrapped_lines[] = $wrapped_value;
$line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding );
}
} while ( $line );
}
}

$row[ $col ] = array_shift( $wrapped_lines );
Expand Down
39 changes: 39 additions & 0 deletions tests/Test_Table_Ascii.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,45 @@ public function testDrawOneColumnColorDisabledTable() {
$this->assertInOutEquals(array($headers, $rows), $output);
}

/**
* Test that colorized text wraps correctly while maintaining color codes.
*/
public function testWrappedColorizedText() {
Colors::enable( true );
$headers = array('Column 1', 'Column 2');
$green_code = "\x1b\x5b\x33\x32\x3b\x31\x6d"; // Green + bright
$reset_code = "\x1b\x5b\x30\x6d"; // Reset

// Create a long colorized string that will wrap
$long_text = Colors::colorize('%GThis is a long green text%n', true);

$rows = array(
array('Short', $long_text),
);

// Expected output with wrapped text maintaining colors
// The color codes are preserved across wrapped lines
$output = <<<OUT
+------------+--------------+
| Column 1 | Column 2 |
+------------+--------------+
| Short | {$green_code}This is a lo{$reset_code} |
| | {$green_code}ng green tex{$reset_code} |
| | {$green_code}t{$reset_code} |
+------------+--------------+

OUT;

$this->_instance->setHeaders($headers);
$this->_instance->setRows($rows);
$renderer = new Ascii([10, 12]);
$renderer->setConstraintWidth(30);
$this->_instance->setRenderer($renderer);
$this->_instance->setAsciiPreColorized(true);
$this->_instance->display();
$this->assertOutFileEqualsWith($output);
}

/**
* Checks that spacing and borders are handled correctly in table
*/
Expand Down