Skip to content

Commit 59deb21

Browse files
Copilotswissspidy
andcommitted
Changes before error encountered
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
1 parent 94ae440 commit 59deb21

File tree

3 files changed

+202
-8
lines changed

3 files changed

+202
-8
lines changed

lib/cli/Colors.php

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,153 @@ static public function getStringCache() {
280280
static public function clearStringCache() {
281281
self::$_string_cache = array();
282282
}
283+
284+
/**
285+
* Get the active color code at the end of a colorized string.
286+
*
287+
* This function extracts the last active color state from a string that contains
288+
* color codes. It's used to maintain color continuity when wrapping text.
289+
*
290+
* @param string $string The string to analyze.
291+
* @param bool $pre_colorized Whether the string contains ANSI codes (true) or color tokens (false).
292+
* @return string The active color token/code or empty string if no color is active.
293+
*/
294+
static public function getActiveColor( $string, $pre_colorized = false ) {
295+
$string = (string) $string;
296+
297+
if ( $pre_colorized ) {
298+
// For pre-colorized strings, we need to track ANSI escape codes
299+
// Pattern: \x1b[...m where ... can be numbers separated by semicolons
300+
$pattern = '/\x1b\[([0-9;]+)m/';
301+
$matches = array();
302+
preg_match_all( $pattern, $string, $matches, PREG_OFFSET_CAPTURE );
303+
304+
if ( empty( $matches[0] ) ) {
305+
return '';
306+
}
307+
308+
// Get the last ANSI code
309+
$last_code = end( $matches[0] )[0];
310+
$last_params = end( $matches[1] )[0];
311+
312+
// If it's a reset code (0 or 0m), no color is active
313+
if ( $last_params === '0' ) {
314+
return '';
315+
}
316+
317+
// Return the full ANSI code
318+
return $last_code;
319+
} else {
320+
// Track the last seen color token
321+
$last_color = '';
322+
323+
// Get all color tokens
324+
$colors = self::getColors();
325+
326+
// Find all color tokens in the string
327+
foreach ( $colors as $token => $value ) {
328+
$pos = 0;
329+
while ( ( $pos = strpos( $string, $token, $pos ) ) !== false ) {
330+
// Make sure this isn't an escaped %%
331+
if ( $pos === 0 || $string[ $pos - 1 ] !== '%' ) {
332+
$last_color = $token;
333+
}
334+
$pos += strlen( $token );
335+
}
336+
}
337+
338+
// If the last color was a reset (%n or %N), return empty
339+
if ( $last_color === '%n' || $last_color === '%N' ) {
340+
return '';
341+
}
342+
343+
return $last_color;
344+
}
345+
}
346+
347+
/**
348+
* Get the ANSI reset code.
349+
*
350+
* @return string The ANSI reset code.
351+
*/
352+
static public function getResetCode() {
353+
return "\x1b[0m";
354+
}
355+
356+
/**
357+
* Wrap a pre-colorized string at a specific width, preserving color codes.
358+
*
359+
* This function wraps text that contains ANSI color codes, ensuring that:
360+
* 1. Color codes are never split in the middle
361+
* 2. Active colors are properly terminated and restored across line breaks
362+
* 3. The wrapped segments maintain the correct display width
363+
*
364+
* @param string $string The string to wrap (with ANSI codes).
365+
* @param int $width The maximum display width per line.
366+
* @param string|bool $encoding Optional. The encoding of the string. Default false.
367+
* @return array Array of wrapped string segments.
368+
*/
369+
static public function wrapPreColorized( $string, $width, $encoding = false ) {
370+
$wrapped = array();
371+
$current_line = '';
372+
$current_width = 0;
373+
$active_color = '';
374+
375+
// Pattern to match ANSI escape sequences
376+
$ansi_pattern = '/(\x1b\[[0-9;]*m)/';
377+
378+
// Split the string into parts: ANSI codes and text
379+
$parts = preg_split( $ansi_pattern, $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
380+
381+
foreach ( $parts as $part ) {
382+
// Check if this part is an ANSI code
383+
if ( preg_match( $ansi_pattern, $part ) ) {
384+
// It's an ANSI code, add it to current line without counting width
385+
$current_line .= $part;
386+
387+
// Track the active color
388+
if ( preg_match( '/\x1b\[0m/', $part ) ) {
389+
// Reset code
390+
$active_color = '';
391+
} elseif ( preg_match( '/\x1b\[([0-9;]+)m/', $part, $matches ) && $matches[1] !== '0' ) {
392+
// Non-reset color code
393+
$active_color = $part;
394+
}
395+
} else {
396+
// It's text content, process it character by character
397+
$text_length = \cli\safe_strlen( $part, $encoding );
398+
$offset = 0;
399+
400+
while ( $offset < $text_length ) {
401+
$char = \cli\safe_substr( $part, $offset, 1, false, $encoding );
402+
$char_width = \cli\strwidth( $char, $encoding );
403+
404+
// Check if adding this character would exceed the width
405+
if ( $current_width + $char_width > $width && $current_width > 0 ) {
406+
// Need to wrap - finish current line
407+
if ( $active_color ) {
408+
$current_line .= self::getResetCode();
409+
}
410+
$wrapped[] = $current_line;
411+
412+
// Start new line
413+
$current_line = $active_color ? $active_color : '';
414+
$current_width = 0;
415+
}
416+
417+
// Add the character
418+
$current_line .= $char;
419+
$current_width += $char_width;
420+
$offset++;
421+
}
422+
}
423+
}
424+
425+
// Add the last line if there's any content
426+
if ( $current_line !== '' && $current_line !== $active_color ) {
427+
$wrapped[] = $current_line;
428+
}
429+
430+
return $wrapped;
431+
}
283432
}

lib/cli/table/Ascii.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,21 @@ public function row( array $row ) {
146146

147147
$wrapped_lines = [];
148148
foreach ( $split_lines as $line ) {
149-
do {
150-
$wrapped_value = \cli\safe_substr( $line, 0, $col_width, true /*is_width*/, $encoding );
151-
$val_width = Colors::width( $wrapped_value, self::isPreColorized( $col ), $encoding );
152-
if ( $val_width ) {
153-
$wrapped_lines[] = $wrapped_value;
154-
$line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding );
155-
}
156-
} while ( $line );
149+
// Use the new color-aware wrapping for pre-colorized content
150+
if ( self::isPreColorized( $col ) && Colors::width( $line, true, $encoding ) > $col_width ) {
151+
$line_wrapped = Colors::wrapPreColorized( $line, $col_width, $encoding );
152+
$wrapped_lines = array_merge( $wrapped_lines, $line_wrapped );
153+
} else {
154+
// For non-colorized content, use the original logic
155+
do {
156+
$wrapped_value = \cli\safe_substr( $line, 0, $col_width, true /*is_width*/, $encoding );
157+
$val_width = Colors::width( $wrapped_value, self::isPreColorized( $col ), $encoding );
158+
if ( $val_width ) {
159+
$wrapped_lines[] = $wrapped_value;
160+
$line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding );
161+
}
162+
} while ( $line );
163+
}
157164
}
158165

159166
$row[ $col ] = array_shift( $wrapped_lines );

tests/Test_Table_Ascii.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,44 @@ public function testDrawOneColumnColorDisabledTable() {
114114
$this->assertInOutEquals(array($headers, $rows), $output);
115115
}
116116

117+
/**
118+
* Test that colorized text wraps correctly while maintaining color codes.
119+
*/
120+
public function testWrappedColorizedText() {
121+
Colors::enable( true );
122+
$headers = array('Column 1', 'Column 2');
123+
$green_code = "\x1b\x5b\x33\x32\x3b\x31\x6d"; // Green + bright
124+
$reset_code = "\x1b\x5b\x30\x6d"; // Reset
125+
126+
// Create a long colorized string that will wrap
127+
$long_text = Colors::colorize('%GThis is a long green text%n', true);
128+
129+
$rows = array(
130+
array('Short', $long_text),
131+
);
132+
133+
// Expected output with wrapped text maintaining colors
134+
$output = <<<OUT
135+
+----------+------------+
136+
| Column 1 | Column 2 |
137+
+----------+------------+
138+
| Short | {$green_code}This is a {$reset_code} |
139+
| | {$green_code}long green{$reset_code} |
140+
| | {$green_code} text{$reset_code} |
141+
+----------+------------+
142+
143+
OUT;
144+
145+
$this->_instance->setHeaders($headers);
146+
$this->_instance->setRows($rows);
147+
$renderer = new Ascii([10, 12]);
148+
$renderer->setConstraintWidth(30);
149+
$this->_instance->setRenderer($renderer);
150+
$this->_instance->setAsciiPreColorized(true);
151+
$this->_instance->display();
152+
$this->assertOutFileEqualsWith($output);
153+
}
154+
117155
/**
118156
* Checks that spacing and borders are handled correctly in table
119157
*/

0 commit comments

Comments
 (0)