1919 * The ASCII renderer renders tables with ASCII borders.
2020 */
2121class Ascii extends Renderer {
22+ /**
23+ * Valid wrapping modes.
24+ */
25+ private const VALID_WRAPPING_MODES = array ( 'wrap ' , 'word-wrap ' , 'truncate ' );
26+
27+ /**
28+ * Ellipsis character(s) used for truncation.
29+ */
30+ private const ELLIPSIS = '... ' ;
31+
32+ /**
33+ * Width of the ellipsis in characters.
34+ */
35+ private const ELLIPSIS_WIDTH = 3 ;
36+
2237 protected $ _characters = array (
2338 'corner ' => '+ ' ,
2439 'line ' => '- ' ,
@@ -28,6 +43,7 @@ class Ascii extends Renderer {
2843 protected $ _border = null ;
2944 protected $ _constraintWidth = null ;
3045 protected $ _pre_colorized = false ;
46+ protected $ _wrapping_mode = 'wrap ' ; // 'wrap', 'word-wrap', or 'truncate'
3147
3248 /**
3349 * Set the widths of each column in the table.
@@ -96,6 +112,19 @@ public function setConstraintWidth( $constraintWidth ) {
96112 $ this ->_constraintWidth = $ constraintWidth ;
97113 }
98114
115+ /**
116+ * Set the wrapping mode for table cells.
117+ *
118+ * @param string $mode One of: 'wrap' (default - wrap at character boundaries),
119+ * 'word-wrap' (wrap at word boundaries), or 'truncate' (truncate with ellipsis).
120+ */
121+ public function setWrappingMode ( $ mode ) {
122+ if ( ! in_array ( $ mode , self ::VALID_WRAPPING_MODES , true ) ) {
123+ throw new \InvalidArgumentException ( "Invalid wrapping mode ' $ mode'. Must be one of: " . implode ( ', ' , self ::VALID_WRAPPING_MODES ) );
124+ }
125+ $ this ->_wrapping_mode = $ mode ;
126+ }
127+
99128 /**
100129 * Set the characters used for rendering the Ascii table.
101130 *
@@ -148,21 +177,8 @@ public function row( array $row ) {
148177
149178 $ wrapped_lines = [];
150179 foreach ( $ split_lines as $ line ) {
151- // Use the new color-aware wrapping for pre-colorized content
152- if ( self ::isPreColorized ( $ col ) && Colors::width ( $ line , true , $ encoding ) > $ col_width ) {
153- $ line_wrapped = Colors::wrapPreColorized ( $ line , $ col_width , $ encoding );
154- $ wrapped_lines = array_merge ( $ wrapped_lines , $ line_wrapped );
155- } else {
156- // For non-colorized content, use the original logic
157- do {
158- $ wrapped_value = \cli \safe_substr ( $ line , 0 , $ col_width , true /*is_width*/ , $ encoding );
159- $ val_width = Colors::width ( $ wrapped_value , self ::isPreColorized ( $ col ), $ encoding );
160- if ( $ val_width ) {
161- $ wrapped_lines [] = $ wrapped_value ;
162- $ line = \cli \safe_substr ( $ line , \cli \safe_strlen ( $ wrapped_value , $ encoding ), null /*length*/ , false /*is_width*/ , $ encoding );
163- }
164- } while ( $ line );
165- }
180+ $ line_wrapped = $ this ->wrapText ( $ line , $ col_width , $ encoding , self ::isPreColorized ( $ col ) );
181+ $ wrapped_lines = array_merge ( $ wrapped_lines , $ line_wrapped );
166182 }
167183
168184 $ row [ $ col ] = array_shift ( $ wrapped_lines );
@@ -235,6 +251,126 @@ public function setPreColorized( $pre_colorized ) {
235251 $ this ->_pre_colorized = $ pre_colorized ;
236252 }
237253
254+ /**
255+ * Wrap text based on the configured wrapping mode.
256+ *
257+ * @param string $text The text to wrap.
258+ * @param int $width The maximum width.
259+ * @param string|bool $encoding The text encoding.
260+ * @param bool $is_precolorized Whether the text is pre-colorized.
261+ * @return array Array of wrapped lines.
262+ */
263+ protected function wrapText ( $ text , $ width , $ encoding , $ is_precolorized ) {
264+ if ( ! $ width ) {
265+ return array ( $ text );
266+ }
267+
268+ $ text_width = Colors::width ( $ text , $ is_precolorized , $ encoding );
269+
270+ // If text fits, no wrapping needed
271+ if ( $ text_width <= $ width ) {
272+ return array ( $ text );
273+ }
274+
275+ // Handle truncate mode
276+ if ( 'truncate ' === $ this ->_wrapping_mode ) {
277+ if ( $ width <= self ::ELLIPSIS_WIDTH ) {
278+ // Not enough space for ellipsis, just truncate
279+ return array ( \cli \safe_substr ( $ text , 0 , $ width , true /*is_width*/ , $ encoding ) );
280+ }
281+
282+ // Truncate and add ellipsis
283+ $ truncated = \cli \safe_substr ( $ text , 0 , $ width - self ::ELLIPSIS_WIDTH , true /*is_width*/ , $ encoding );
284+ return array ( $ truncated . self ::ELLIPSIS );
285+ }
286+
287+ // Handle word-wrap mode
288+ if ( 'word-wrap ' === $ this ->_wrapping_mode ) {
289+ return $ this ->wordWrap ( $ text , $ width , $ encoding , $ is_precolorized );
290+ }
291+
292+ // Default: character-boundary wrapping
293+ $ wrapped_lines = array ();
294+ $ line = $ text ;
295+
296+ // Use the new color-aware wrapping for pre-colorized content
297+ if ( $ is_precolorized ) {
298+ $ wrapped_lines = Colors::wrapPreColorized ( $ line , $ width , $ encoding );
299+ } else {
300+ // For non-colorized content, use character-boundary wrapping
301+ do {
302+ $ wrapped_value = \cli \safe_substr ( $ line , 0 , $ width , true /*is_width*/ , $ encoding );
303+ $ val_width = Colors::width ( $ wrapped_value , $ is_precolorized , $ encoding );
304+ if ( $ val_width ) {
305+ $ wrapped_lines [] = $ wrapped_value ;
306+ $ line = \cli \safe_substr ( $ line , \cli \safe_strlen ( $ wrapped_value , $ encoding ), null /*length*/ , false /*is_width*/ , $ encoding );
307+ }
308+ } while ( $ line );
309+ }
310+
311+ return $ wrapped_lines ;
312+ }
313+
314+ /**
315+ * Wrap text at word boundaries.
316+ *
317+ * @param string $text The text to wrap.
318+ * @param int $width The maximum width.
319+ * @param string|bool $encoding The text encoding.
320+ * @param bool $is_precolorized Whether the text is pre-colorized.
321+ * @return array Array of wrapped lines.
322+ */
323+ protected function wordWrap ( $ text , $ width , $ encoding , $ is_precolorized ) {
324+ $ wrapped_lines = array ();
325+ $ current_line = '' ;
326+ $ current_line_width = 0 ;
327+
328+ // Split by spaces and hyphens while keeping the delimiters
329+ $ words = preg_split ( '/(\s+|-)/u ' , $ text , -1 , PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
330+
331+ foreach ( $ words as $ word ) {
332+ $ word_width = Colors::width ( $ word , $ is_precolorized , $ encoding );
333+
334+ // If this word alone exceeds the width, we need to split it
335+ if ( $ word_width > $ width ) {
336+ // Flush current line if not empty
337+ if ( $ current_line !== '' ) {
338+ $ wrapped_lines [] = $ current_line ;
339+ $ current_line = '' ;
340+ $ current_line_width = 0 ;
341+ }
342+
343+ // Split the long word at character boundaries
344+ $ remaining_word = $ word ;
345+ while ( $ remaining_word ) {
346+ $ chunk = \cli \safe_substr ( $ remaining_word , 0 , $ width , true /*is_width*/ , $ encoding );
347+ $ wrapped_lines [] = $ chunk ;
348+ $ remaining_word = \cli \safe_substr ( $ remaining_word , \cli \safe_strlen ( $ chunk , $ encoding ), null /*length*/ , false /*is_width*/ , $ encoding );
349+ }
350+ continue ;
351+ }
352+
353+ // Check if adding this word would exceed the width
354+ if ( $ current_line !== '' && $ current_line_width + $ word_width > $ width ) {
355+ // Start a new line
356+ $ wrapped_lines [] = $ current_line ;
357+ $ current_line = $ word ;
358+ $ current_line_width = $ word_width ;
359+ } else {
360+ // Add to current line
361+ $ current_line .= $ word ;
362+ $ current_line_width += $ word_width ;
363+ }
364+ }
365+
366+ // Add any remaining content
367+ if ( $ current_line !== '' ) {
368+ $ wrapped_lines [] = $ current_line ;
369+ }
370+
371+ return $ wrapped_lines ?: array ( '' );
372+ }
373+
238374 /**
239375 * Is a column pre-colorized?
240376 *
0 commit comments