2021-05-10 00:23:30 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
2024-04-01 16:51:34 +00:00
namespace RectorPrefix202404\Symfony\Component\Console\Helper ;
2021-05-10 00:23:30 +00:00
2024-04-01 16:51:34 +00:00
use RectorPrefix202404\Symfony\Component\Console\Exception\InvalidArgumentException ;
use RectorPrefix202404\Symfony\Component\Console\Exception\RuntimeException ;
use RectorPrefix202404\Symfony\Component\Console\Formatter\OutputFormatter ;
use RectorPrefix202404\Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface ;
use RectorPrefix202404\Symfony\Component\Console\Output\ConsoleSectionOutput ;
use RectorPrefix202404\Symfony\Component\Console\Output\OutputInterface ;
2021-05-10 00:23:30 +00:00
/**
* Provides helpers to display a table .
*
* @ author Fabien Potencier < fabien @ symfony . com >
* @ author Саша Стаменковић < umpirsky @ gmail . com >
* @ author Abdellatif Ait boudad < a . aitboudad @ gmail . com >
* @ author Max Grigorian < maxakawizard @ gmail . com >
* @ author Dany Maillard < danymaillard93b @ gmail . com >
*/
class Table
{
private const SEPARATOR_TOP = 0 ;
private const SEPARATOR_TOP_BOTTOM = 1 ;
private const SEPARATOR_MID = 2 ;
private const SEPARATOR_BOTTOM = 3 ;
private const BORDER_OUTSIDE = 0 ;
private const BORDER_INSIDE = 1 ;
2022-06-04 10:45:13 +00:00
private const DISPLAY_ORIENTATION_DEFAULT = 'default' ;
private const DISPLAY_ORIENTATION_HORIZONTAL = 'horizontal' ;
private const DISPLAY_ORIENTATION_VERTICAL = 'vertical' ;
2021-12-09 13:05:29 +00:00
/**
* @ var string | null
*/
2021-05-10 00:23:30 +00:00
private $headerTitle ;
2021-12-09 13:05:29 +00:00
/**
* @ var string | null
*/
2021-05-10 00:23:30 +00:00
private $footerTitle ;
/**
2021-12-09 13:05:29 +00:00
* @ var mixed []
2021-05-10 00:23:30 +00:00
*/
private $headers = [];
/**
2021-12-09 13:05:29 +00:00
* @ var mixed []
2021-05-10 00:23:30 +00:00
*/
private $rows = [];
/**
2021-12-09 13:05:29 +00:00
* @ var mixed []
2021-05-10 00:23:30 +00:00
*/
private $effectiveColumnWidths = [];
/**
* @ var int
*/
private $numberOfColumns ;
2022-06-04 10:45:13 +00:00
/**
* @ var \Symfony\Component\Console\Output\OutputInterface
*/
2021-05-10 00:23:30 +00:00
private $output ;
2022-06-04 10:45:13 +00:00
/**
* @ var \Symfony\Component\Console\Helper\TableStyle
*/
2021-05-10 00:23:30 +00:00
private $style ;
/**
2021-12-09 13:05:29 +00:00
* @ var mixed []
2021-05-10 00:23:30 +00:00
*/
private $columnStyles = [];
/**
2021-12-09 13:05:29 +00:00
* @ var mixed []
2021-05-10 00:23:30 +00:00
*/
private $columnWidths = [];
2021-12-09 13:05:29 +00:00
/**
* @ var mixed []
*/
2021-05-10 00:23:30 +00:00
private $columnMaxWidths = [];
2021-11-30 07:57:42 +00:00
/**
2021-12-09 13:05:29 +00:00
* @ var bool
2021-11-30 07:57:42 +00:00
*/
2021-05-10 00:23:30 +00:00
private $rendered = \false ;
2022-06-04 10:45:13 +00:00
/**
* @ var string
*/
private $displayOrientation = self :: DISPLAY_ORIENTATION_DEFAULT ;
2021-12-09 13:05:29 +00:00
/**
* @ var mixed []
*/
private static $styles ;
2022-06-07 08:22:29 +00:00
public function __construct ( OutputInterface $output )
2021-05-10 00:23:30 +00:00
{
$this -> output = $output ;
2021-12-09 13:05:29 +00:00
self :: $styles = self :: $styles ? ? self :: initStyles ();
2021-05-10 00:23:30 +00:00
$this -> setStyle ( 'default' );
}
/**
* Sets a style definition .
2023-05-30 20:19:22 +00:00
*
* @ return void
2021-05-10 00:23:30 +00:00
*/
2022-06-07 08:22:29 +00:00
public static function setStyleDefinition ( string $name , TableStyle $style )
2021-05-10 00:23:30 +00:00
{
2021-12-09 13:05:29 +00:00
self :: $styles = self :: $styles ? ? self :: initStyles ();
2021-05-10 00:23:30 +00:00
self :: $styles [ $name ] = $style ;
}
/**
* Gets a style definition by name .
*/
2022-06-07 08:22:29 +00:00
public static function getStyleDefinition ( string $name ) : TableStyle
2021-05-10 00:23:30 +00:00
{
2021-12-09 13:05:29 +00:00
self :: $styles = self :: $styles ? ? self :: initStyles ();
if ( ! isset ( self :: $styles [ $name ])) {
2022-06-07 08:22:29 +00:00
throw new InvalidArgumentException ( \sprintf ( 'Style "%s" is not defined.' , $name ));
2021-05-10 00:23:30 +00:00
}
2021-12-09 13:05:29 +00:00
return self :: $styles [ $name ];
2021-05-10 00:23:30 +00:00
}
/**
* Sets table style .
*
* @ return $this
2022-04-26 08:13:18 +00:00
* @ param \Symfony\Component\Console\Helper\TableStyle | string $name
2021-05-10 00:23:30 +00:00
*/
public function setStyle ( $name )
{
$this -> style = $this -> resolveStyle ( $name );
return $this ;
}
/**
* Gets the current table style .
*/
2022-06-07 08:22:29 +00:00
public function getStyle () : TableStyle
2021-05-10 00:23:30 +00:00
{
return $this -> style ;
}
/**
* Sets table column style .
*
* @ param TableStyle | string $name The style name or a TableStyle instance
*
* @ return $this
*/
2021-12-10 10:22:23 +00:00
public function setColumnStyle ( int $columnIndex , $name )
2021-05-10 00:23:30 +00:00
{
$this -> columnStyles [ $columnIndex ] = $this -> resolveStyle ( $name );
return $this ;
}
/**
* Gets the current style for a column .
*
* If style was not set , it returns the global table style .
*/
2022-06-07 08:22:29 +00:00
public function getColumnStyle ( int $columnIndex ) : TableStyle
2021-05-10 00:23:30 +00:00
{
return $this -> columnStyles [ $columnIndex ] ? ? $this -> getStyle ();
}
/**
* Sets the minimum width of a column .
*
* @ return $this
*/
2021-12-10 10:22:23 +00:00
public function setColumnWidth ( int $columnIndex , int $width )
2021-05-10 00:23:30 +00:00
{
$this -> columnWidths [ $columnIndex ] = $width ;
return $this ;
}
/**
* Sets the minimum width of all columns .
*
* @ return $this
*/
2021-12-10 10:22:23 +00:00
public function setColumnWidths ( array $widths )
2021-05-10 00:23:30 +00:00
{
$this -> columnWidths = [];
foreach ( $widths as $index => $width ) {
$this -> setColumnWidth ( $index , $width );
}
return $this ;
}
/**
* Sets the maximum width of a column .
*
* Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines , while
* formatted strings are preserved .
*
* @ return $this
*/
2021-12-10 10:22:23 +00:00
public function setColumnMaxWidth ( int $columnIndex , int $width )
2021-05-10 00:23:30 +00:00
{
2022-06-07 08:22:29 +00:00
if ( ! $this -> output -> getFormatter () instanceof WrappableOutputFormatterInterface ) {
throw new \LogicException ( \sprintf ( 'Setting a maximum column width is only supported when using a "%s" formatter, got "%s".' , WrappableOutputFormatterInterface :: class , \get_debug_type ( $this -> output -> getFormatter ())));
2021-05-10 00:23:30 +00:00
}
$this -> columnMaxWidths [ $columnIndex ] = $width ;
return $this ;
}
2021-07-05 22:50:18 +00:00
/**
2021-11-30 07:57:42 +00:00
* @ return $this
2021-07-05 22:50:18 +00:00
*/
2021-12-10 10:22:23 +00:00
public function setHeaders ( array $headers )
2021-05-10 00:23:30 +00:00
{
$headers = \array_values ( $headers );
2022-12-01 16:57:08 +00:00
if ( $headers && ! \is_array ( $headers [ 0 ])) {
2021-05-10 00:23:30 +00:00
$headers = [ $headers ];
}
$this -> headers = $headers ;
return $this ;
}
2022-12-01 16:57:08 +00:00
/**
* @ return $this
*/
2021-12-10 10:22:23 +00:00
public function setRows ( array $rows )
2021-05-10 00:23:30 +00:00
{
$this -> rows = [];
return $this -> addRows ( $rows );
}
2021-07-05 22:50:18 +00:00
/**
2021-11-30 07:57:42 +00:00
* @ return $this
2021-07-05 22:50:18 +00:00
*/
2021-12-10 10:22:23 +00:00
public function addRows ( array $rows )
2021-05-10 00:23:30 +00:00
{
foreach ( $rows as $row ) {
$this -> addRow ( $row );
}
return $this ;
}
2021-11-30 07:57:42 +00:00
/**
* @ return $this
2022-04-26 08:13:18 +00:00
* @ param \Symfony\Component\Console\Helper\TableSeparator | mixed [] $row
2021-11-30 07:57:42 +00:00
*/
2021-05-10 00:23:30 +00:00
public function addRow ( $row )
{
2022-06-07 08:22:29 +00:00
if ( $row instanceof TableSeparator ) {
2021-05-10 00:23:30 +00:00
$this -> rows [] = $row ;
return $this ;
}
$this -> rows [] = \array_values ( $row );
return $this ;
}
/**
* Adds a row to the table , and re - renders the table .
2021-11-30 07:57:42 +00:00
*
* @ return $this
2022-04-26 08:13:18 +00:00
* @ param \Symfony\Component\Console\Helper\TableSeparator | mixed [] $row
2021-05-10 00:23:30 +00:00
*/
2021-12-09 13:05:29 +00:00
public function appendRow ( $row )
2021-05-10 00:23:30 +00:00
{
2022-06-07 08:22:29 +00:00
if ( ! $this -> output instanceof ConsoleSectionOutput ) {
throw new RuntimeException ( \sprintf ( 'Output should be an instance of "%s" when calling "%s".' , ConsoleSectionOutput :: class , __METHOD__ ));
2021-05-10 00:23:30 +00:00
}
if ( $this -> rendered ) {
$this -> output -> clear ( $this -> calculateRowCount ());
}
$this -> addRow ( $row );
$this -> render ();
return $this ;
}
2021-07-05 22:50:18 +00:00
/**
2021-11-30 07:57:42 +00:00
* @ return $this
2021-12-09 13:05:29 +00:00
* @ param int | string $column
2021-07-05 22:50:18 +00:00
*/
2021-12-10 10:22:23 +00:00
public function setRow ( $column , array $row )
2021-05-10 00:23:30 +00:00
{
$this -> rows [ $column ] = $row ;
return $this ;
}
/**
2021-11-30 07:57:42 +00:00
* @ return $this
2021-05-10 00:23:30 +00:00
*/
2021-12-10 10:22:23 +00:00
public function setHeaderTitle ( ? string $title )
2021-05-10 00:23:30 +00:00
{
$this -> headerTitle = $title ;
return $this ;
}
/**
2021-11-30 07:57:42 +00:00
* @ return $this
2021-05-10 00:23:30 +00:00
*/
2021-12-10 10:22:23 +00:00
public function setFooterTitle ( ? string $title )
2021-05-10 00:23:30 +00:00
{
$this -> footerTitle = $title ;
return $this ;
}
/**
2021-11-30 07:57:42 +00:00
* @ return $this
2021-05-10 00:23:30 +00:00
*/
2021-12-10 10:22:23 +00:00
public function setHorizontal ( bool $horizontal = \true )
2021-05-10 00:23:30 +00:00
{
2022-06-04 10:45:13 +00:00
$this -> displayOrientation = $horizontal ? self :: DISPLAY_ORIENTATION_HORIZONTAL : self :: DISPLAY_ORIENTATION_DEFAULT ;
return $this ;
}
/**
* @ return $this
*/
public function setVertical ( bool $vertical = \true )
{
$this -> displayOrientation = $vertical ? self :: DISPLAY_ORIENTATION_VERTICAL : self :: DISPLAY_ORIENTATION_DEFAULT ;
2021-05-10 00:23:30 +00:00
return $this ;
}
/**
* Renders table to output .
*
* Example :
*
* +---------------+-----------------------+------------------+
* | ISBN | Title | Author |
* +---------------+-----------------------+------------------+
* | 99921 - 58 - 10 - 7 | Divine Comedy | Dante Alighieri |
* | 9971 - 5 - 0210 - 0 | A Tale of Two Cities | Charles Dickens |
* | 960 - 425 - 05 9 - 0 | The Lord of the Rings | J . R . R . Tolkien |
* +---------------+-----------------------+------------------+
2023-05-30 20:19:22 +00:00
*
* @ return void
2021-05-10 00:23:30 +00:00
*/
public function render ()
{
2022-06-07 08:22:29 +00:00
$divider = new TableSeparator ();
2022-06-04 10:45:13 +00:00
$isCellWithColspan = static function ( $cell ) {
2022-06-07 08:22:29 +00:00
return $cell instanceof TableCell && $cell -> getColspan () >= 2 ;
2022-06-04 10:45:13 +00:00
};
$horizontal = self :: DISPLAY_ORIENTATION_HORIZONTAL === $this -> displayOrientation ;
$vertical = self :: DISPLAY_ORIENTATION_VERTICAL === $this -> displayOrientation ;
$rows = [];
if ( $horizontal ) {
2021-05-10 00:23:30 +00:00
foreach ( $this -> headers [ 0 ] ? ? [] as $i => $header ) {
$rows [ $i ] = [ $header ];
foreach ( $this -> rows as $row ) {
2022-06-07 08:22:29 +00:00
if ( $row instanceof TableSeparator ) {
2021-05-10 00:23:30 +00:00
continue ;
}
if ( isset ( $row [ $i ])) {
$rows [ $i ][] = $row [ $i ];
2022-06-04 10:45:13 +00:00
} elseif ( $isCellWithColspan ( $rows [ $i ][ 0 ])) {
2021-05-10 00:23:30 +00:00
// Noop, there is a "title"
} else {
$rows [ $i ][] = null ;
}
}
}
2022-06-04 10:45:13 +00:00
} elseif ( $vertical ) {
$formatter = $this -> output -> getFormatter ();
$maxHeaderLength = \array_reduce ( $this -> headers [ 0 ] ? ? [], static function ( $max , $header ) use ( $formatter ) {
2022-06-07 08:22:29 +00:00
return \max ( $max , Helper :: width ( Helper :: removeDecoration ( $formatter , $header )));
2022-06-04 10:45:13 +00:00
}, 0 );
foreach ( $this -> rows as $row ) {
2022-06-07 08:22:29 +00:00
if ( $row instanceof TableSeparator ) {
2022-06-04 10:45:13 +00:00
continue ;
}
if ( $rows ) {
$rows [] = [ $divider ];
}
$containsColspan = \false ;
foreach ( $row as $cell ) {
if ( $containsColspan = $isCellWithColspan ( $cell )) {
break ;
}
}
$headers = $this -> headers [ 0 ] ? ? [];
$maxRows = \max ( \count ( $headers ), \count ( $row ));
for ( $i = 0 ; $i < $maxRows ; ++ $i ) {
$cell = ( string ) ( $row [ $i ] ? ? '' );
2024-02-27 11:28:02 +00:00
$eol = \strpos ( $cell , " \r \n " ) !== \false ? " \r \n " : " \n " ;
$parts = \explode ( $eol , $cell );
2023-11-30 18:02:55 +00:00
foreach ( $parts as $idx => $part ) {
if ( $headers && ! $containsColspan ) {
if ( 0 === $idx ) {
2024-04-03 06:21:54 +00:00
$rows [] = [ \sprintf ( '<comment>%s%s</>: %s' , \str_repeat ( ' ' , $maxHeaderLength - Helper :: width ( Helper :: removeDecoration ( $formatter , $headers [ $i ] ? ? '' ))), $headers [ $i ] ? ? '' , $part )];
2023-11-30 18:02:55 +00:00
} else {
$rows [] = [ \sprintf ( '%s %s' , \str_pad ( '' , $maxHeaderLength , ' ' , \STR_PAD_LEFT ), $part )];
}
} elseif ( '' !== $cell ) {
$rows [] = [ $part ];
}
2022-06-04 10:45:13 +00:00
}
}
}
2021-05-10 00:23:30 +00:00
} else {
$rows = \array_merge ( $this -> headers , [ $divider ], $this -> rows );
}
$this -> calculateNumberOfColumns ( $rows );
2022-04-27 22:40:38 +00:00
$rowGroups = $this -> buildTableRows ( $rows );
$this -> calculateColumnsWidth ( $rowGroups );
2022-06-04 10:45:13 +00:00
$isHeader = ! $horizontal ;
$isFirstRow = $horizontal ;
2021-07-21 13:46:30 +00:00
$hasTitle = ( bool ) $this -> headerTitle ;
2022-04-27 22:40:38 +00:00
foreach ( $rowGroups as $rowGroup ) {
$isHeaderSeparatorRendered = \false ;
foreach ( $rowGroup as $row ) {
if ( $divider === $row ) {
$isHeader = \false ;
$isFirstRow = \true ;
continue ;
}
2022-06-07 08:22:29 +00:00
if ( $row instanceof TableSeparator ) {
2022-04-27 22:40:38 +00:00
$this -> renderRowSeparator ();
continue ;
}
if ( ! $row ) {
continue ;
}
if ( $isHeader && ! $isHeaderSeparatorRendered ) {
2023-11-10 19:13:04 +00:00
$this -> renderRowSeparator ( self :: SEPARATOR_TOP , $hasTitle ? $this -> headerTitle : null , $hasTitle ? $this -> style -> getHeaderTitleFormat () : null );
2022-04-27 22:40:38 +00:00
$hasTitle = \false ;
$isHeaderSeparatorRendered = \true ;
}
if ( $isFirstRow ) {
2023-11-10 19:13:04 +00:00
$this -> renderRowSeparator ( $horizontal ? self :: SEPARATOR_TOP : self :: SEPARATOR_TOP_BOTTOM , $hasTitle ? $this -> headerTitle : null , $hasTitle ? $this -> style -> getHeaderTitleFormat () : null );
2022-04-27 22:40:38 +00:00
$isFirstRow = \false ;
$hasTitle = \false ;
}
2022-06-04 10:45:13 +00:00
if ( $vertical ) {
$isHeader = \false ;
$isFirstRow = \false ;
}
if ( $horizontal ) {
2022-04-27 22:40:38 +00:00
$this -> renderRow ( $row , $this -> style -> getCellRowFormat (), $this -> style -> getCellHeaderFormat ());
} else {
$this -> renderRow ( $row , $isHeader ? $this -> style -> getCellHeaderFormat () : $this -> style -> getCellRowFormat ());
}
2021-05-10 00:23:30 +00:00
}
}
$this -> renderRowSeparator ( self :: SEPARATOR_BOTTOM , $this -> footerTitle , $this -> style -> getFooterTitleFormat ());
$this -> cleanup ();
$this -> rendered = \true ;
}
/**
* Renders horizontal header separator .
*
* Example :
*
* +-----+-----------+-------+
*/
2024-01-31 07:59:42 +00:00
private function renderRowSeparator ( int $type = self :: SEPARATOR_MID , ? string $title = null , ? string $titleFormat = null ) : void
2021-05-10 00:23:30 +00:00
{
2022-06-04 10:45:13 +00:00
if ( ! ( $count = $this -> numberOfColumns )) {
2021-05-10 00:23:30 +00:00
return ;
}
$borders = $this -> style -> getBorderChars ();
if ( ! $borders [ 0 ] && ! $borders [ 2 ] && ! $this -> style -> getCrossingChar ()) {
return ;
}
$crossings = $this -> style -> getCrossingChars ();
if ( self :: SEPARATOR_MID === $type ) {
[ $horizontal , $leftChar , $midChar , $rightChar ] = [ $borders [ 2 ], $crossings [ 8 ], $crossings [ 0 ], $crossings [ 4 ]];
} elseif ( self :: SEPARATOR_TOP === $type ) {
[ $horizontal , $leftChar , $midChar , $rightChar ] = [ $borders [ 0 ], $crossings [ 1 ], $crossings [ 2 ], $crossings [ 3 ]];
} elseif ( self :: SEPARATOR_TOP_BOTTOM === $type ) {
[ $horizontal , $leftChar , $midChar , $rightChar ] = [ $borders [ 0 ], $crossings [ 9 ], $crossings [ 10 ], $crossings [ 11 ]];
} else {
[ $horizontal , $leftChar , $midChar , $rightChar ] = [ $borders [ 0 ], $crossings [ 7 ], $crossings [ 6 ], $crossings [ 5 ]];
}
$markup = $leftChar ;
for ( $column = 0 ; $column < $count ; ++ $column ) {
$markup .= \str_repeat ( $horizontal , $this -> effectiveColumnWidths [ $column ]);
$markup .= $column === $count - 1 ? $rightChar : $midChar ;
}
if ( null !== $title ) {
2022-06-07 08:22:29 +00:00
$titleLength = Helper :: width ( Helper :: removeDecoration ( $formatter = $this -> output -> getFormatter (), $formattedTitle = \sprintf ( $titleFormat , $title )));
$markupLength = Helper :: width ( $markup );
2021-05-10 00:23:30 +00:00
if ( $titleLength > ( $limit = $markupLength - 4 )) {
$titleLength = $limit ;
2022-06-07 08:22:29 +00:00
$formatLength = Helper :: width ( Helper :: removeDecoration ( $formatter , \sprintf ( $titleFormat , '' )));
$formattedTitle = \sprintf ( $titleFormat , Helper :: substr ( $title , 0 , $limit - $formatLength - 3 ) . '...' );
2021-05-10 00:23:30 +00:00
}
2021-06-17 14:59:13 +00:00
$titleStart = \intdiv ( $markupLength - $titleLength , 2 );
2021-05-10 00:23:30 +00:00
if ( \false === \mb_detect_encoding ( $markup , null , \true )) {
$markup = \substr_replace ( $markup , $formattedTitle , $titleStart , $titleLength );
} else {
$markup = \mb_substr ( $markup , 0 , $titleStart ) . $formattedTitle . \mb_substr ( $markup , $titleStart + $titleLength );
}
}
$this -> output -> writeln ( \sprintf ( $this -> style -> getBorderFormat (), $markup ));
}
/**
* Renders vertical column separator .
*/
2021-07-05 23:06:30 +00:00
private function renderColumnSeparator ( int $type = self :: BORDER_OUTSIDE ) : string
2021-05-10 00:23:30 +00:00
{
$borders = $this -> style -> getBorderChars ();
return \sprintf ( $this -> style -> getBorderFormat (), self :: BORDER_OUTSIDE === $type ? $borders [ 1 ] : $borders [ 3 ]);
}
/**
* Renders table row .
*
* Example :
*
* | 9971 - 5 - 0210 - 0 | A Tale of Two Cities | Charles Dickens |
*/
2024-01-31 07:59:42 +00:00
private function renderRow ( array $row , string $cellFormat , ? string $firstCellFormat = null ) : void
2021-05-10 00:23:30 +00:00
{
$rowContent = $this -> renderColumnSeparator ( self :: BORDER_OUTSIDE );
$columns = $this -> getRowColumns ( $row );
$last = \count ( $columns ) - 1 ;
foreach ( $columns as $i => $column ) {
if ( $firstCellFormat && 0 === $i ) {
$rowContent .= $this -> renderCell ( $row , $column , $firstCellFormat );
} else {
$rowContent .= $this -> renderCell ( $row , $column , $cellFormat );
}
$rowContent .= $this -> renderColumnSeparator ( $last === $i ? self :: BORDER_OUTSIDE : self :: BORDER_INSIDE );
}
$this -> output -> writeln ( $rowContent );
}
/**
* Renders table cell with padding .
*/
2021-07-05 23:06:30 +00:00
private function renderCell ( array $row , int $column , string $cellFormat ) : string
2021-05-10 00:23:30 +00:00
{
$cell = $row [ $column ] ? ? '' ;
$width = $this -> effectiveColumnWidths [ $column ];
2022-06-07 08:22:29 +00:00
if ( $cell instanceof TableCell && $cell -> getColspan () > 1 ) {
2021-05-10 00:23:30 +00:00
// add the width of the following columns(numbers of colspan).
foreach ( \range ( $column + 1 , $column + $cell -> getColspan () - 1 ) as $nextColumn ) {
$width += $this -> getColumnSeparatorWidth () + $this -> effectiveColumnWidths [ $nextColumn ];
}
}
// str_pad won't work properly with multi-byte strings, we need to fix the padding
if ( \false !== ( $encoding = \mb_detect_encoding ( $cell , null , \true ))) {
$width += \strlen ( $cell ) - \mb_strwidth ( $cell , $encoding );
}
$style = $this -> getColumnStyle ( $column );
2022-06-07 08:22:29 +00:00
if ( $cell instanceof TableSeparator ) {
2021-05-10 00:23:30 +00:00
return \sprintf ( $style -> getBorderFormat (), \str_repeat ( $style -> getBorderChars ()[ 2 ], $width ));
}
2022-06-07 08:22:29 +00:00
$width += Helper :: length ( $cell ) - Helper :: length ( Helper :: removeDecoration ( $this -> output -> getFormatter (), $cell ));
2021-05-10 00:23:30 +00:00
$content = \sprintf ( $style -> getCellRowContentFormat (), $cell );
$padType = $style -> getPadType ();
2022-06-07 08:22:29 +00:00
if ( $cell instanceof TableCell && $cell -> getStyle () instanceof TableCellStyle ) {
2021-05-10 00:23:30 +00:00
$isNotStyledByTag = ! \preg_match ( '/^<(\\w+|(\\w+=[\\w,]+;?)*)>.+<\\/(\\w+|(\\w+=\\w+;?)*)?>$/' , $cell );
if ( $isNotStyledByTag ) {
$cellFormat = $cell -> getStyle () -> getCellFormat ();
if ( ! \is_string ( $cellFormat )) {
2021-06-01 12:39:02 +00:00
$tag = \http_build_query ( $cell -> getStyle () -> getTagOptions (), '' , ';' );
2021-05-10 00:23:30 +00:00
$cellFormat = '<' . $tag . '>%s</>' ;
}
2022-06-04 10:45:13 +00:00
if ( \strpos ( $content , '</>' ) !== \false ) {
2021-05-10 00:23:30 +00:00
$content = \str_replace ( '</>' , '' , $content );
$width -= 3 ;
}
2022-06-04 10:45:13 +00:00
if ( \strpos ( $content , '<fg=default;bg=default>' ) !== \false ) {
2021-05-10 00:23:30 +00:00
$content = \str_replace ( '<fg=default;bg=default>' , '' , $content );
$width -= \strlen ( '<fg=default;bg=default>' );
}
}
$padType = $cell -> getStyle () -> getPadByAlign ();
}
return \sprintf ( $cellFormat , \str_pad ( $content , $width , $style -> getPaddingChar (), $padType ));
}
/**
* Calculate number of columns for this table .
*/
2023-05-30 20:19:22 +00:00
private function calculateNumberOfColumns ( array $rows ) : void
2021-05-10 00:23:30 +00:00
{
$columns = [ 0 ];
foreach ( $rows as $row ) {
2022-06-07 08:22:29 +00:00
if ( $row instanceof TableSeparator ) {
2021-05-10 00:23:30 +00:00
continue ;
}
$columns [] = $this -> getNumberOfColumns ( $row );
}
$this -> numberOfColumns = \max ( $columns );
}
2022-06-07 08:22:29 +00:00
private function buildTableRows ( array $rows ) : TableRows
2021-05-10 00:23:30 +00:00
{
/** @var WrappableOutputFormatterInterface $formatter */
$formatter = $this -> output -> getFormatter ();
$unmergedRows = [];
for ( $rowKey = 0 ; $rowKey < \count ( $rows ); ++ $rowKey ) {
$rows = $this -> fillNextRows ( $rows , $rowKey );
// Remove any new line breaks and replace it with a new line
foreach ( $rows [ $rowKey ] as $column => $cell ) {
2022-06-07 08:22:29 +00:00
$colspan = $cell instanceof TableCell ? $cell -> getColspan () : 1 ;
if ( isset ( $this -> columnMaxWidths [ $column ]) && Helper :: width ( Helper :: removeDecoration ( $formatter , $cell )) > $this -> columnMaxWidths [ $column ]) {
2021-05-10 00:23:30 +00:00
$cell = $formatter -> formatAndWrap ( $cell , $this -> columnMaxWidths [ $column ] * $colspan );
}
2022-06-04 10:45:13 +00:00
if ( \strpos ( $cell ? ? '' , " \n " ) === \false ) {
2021-05-10 00:23:30 +00:00
continue ;
}
2024-02-27 11:28:02 +00:00
$eol = \strpos ( $cell ? ? '' , " \r \n " ) !== \false ? " \r \n " : " \n " ;
$escaped = \implode ( $eol , \array_map ( \Closure :: fromCallable ([ OutputFormatter :: class , 'escapeTrailingBackslash' ]), \explode ( $eol , $cell )));
2022-06-07 08:22:29 +00:00
$cell = $cell instanceof TableCell ? new TableCell ( $escaped , [ 'colspan' => $cell -> getColspan ()]) : $escaped ;
2024-02-27 11:28:02 +00:00
$lines = \explode ( $eol , \str_replace ( $eol , '<fg=default;bg=default></>' . $eol , $cell ));
2021-05-10 00:23:30 +00:00
foreach ( $lines as $lineKey => $line ) {
if ( $colspan > 1 ) {
2022-06-07 08:22:29 +00:00
$line = new TableCell ( $line , [ 'colspan' => $colspan ]);
2021-05-10 00:23:30 +00:00
}
if ( 0 === $lineKey ) {
$rows [ $rowKey ][ $column ] = $line ;
} else {
if ( ! \array_key_exists ( $rowKey , $unmergedRows ) || ! \array_key_exists ( $lineKey , $unmergedRows [ $rowKey ])) {
$unmergedRows [ $rowKey ][ $lineKey ] = $this -> copyRow ( $rows , $rowKey );
}
$unmergedRows [ $rowKey ][ $lineKey ][ $column ] = $line ;
}
}
}
}
2022-06-07 08:22:29 +00:00
return new TableRows ( function () use ( $rows , $unmergedRows ) : \Traversable {
2021-05-10 00:23:30 +00:00
foreach ( $rows as $rowKey => $row ) {
2022-06-07 08:22:29 +00:00
$rowGroup = [ $row instanceof TableSeparator ? $row : $this -> fillCells ( $row )];
2021-05-10 00:23:30 +00:00
if ( isset ( $unmergedRows [ $rowKey ])) {
2021-07-04 22:54:28 +00:00
foreach ( $unmergedRows [ $rowKey ] as $row ) {
2022-06-07 08:22:29 +00:00
$rowGroup [] = $row instanceof TableSeparator ? $row : $this -> fillCells ( $row );
2021-05-10 00:23:30 +00:00
}
}
2022-04-27 22:40:38 +00:00
( yield $rowGroup );
2021-05-10 00:23:30 +00:00
}
});
}
private function calculateRowCount () : int
{
2022-06-07 08:22:29 +00:00
$numberOfRows = \count ( \iterator_to_array ( $this -> buildTableRows ( \array_merge ( $this -> headers , [ new TableSeparator ()], $this -> rows ))));
2021-05-10 00:23:30 +00:00
if ( $this -> headers ) {
++ $numberOfRows ;
// Add row for header separator
}
2022-06-04 10:45:13 +00:00
if ( $this -> rows ) {
2021-05-10 00:23:30 +00:00
++ $numberOfRows ;
// Add row for footer separator
}
return $numberOfRows ;
}
/**
* fill rows that contains rowspan > 1.
*
* @ throws InvalidArgumentException
*/
2021-07-05 23:06:30 +00:00
private function fillNextRows ( array $rows , int $line ) : array
2021-05-10 00:23:30 +00:00
{
$unmergedRows = [];
foreach ( $rows [ $line ] as $column => $cell ) {
2022-06-07 08:22:29 +00:00
if ( null !== $cell && ! $cell instanceof TableCell && ! \is_scalar ( $cell ) && ! $cell instanceof \Stringable ) {
throw new InvalidArgumentException ( \sprintf ( 'A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.' , \get_debug_type ( $cell )));
2021-05-10 00:23:30 +00:00
}
2022-06-07 08:22:29 +00:00
if ( $cell instanceof TableCell && $cell -> getRowspan () > 1 ) {
2021-05-10 00:23:30 +00:00
$nbLines = $cell -> getRowspan () - 1 ;
$lines = [ $cell ];
2022-06-04 10:45:13 +00:00
if ( \strpos ( $cell , " \n " ) !== \false ) {
2024-02-27 11:28:02 +00:00
$eol = \strpos ( $cell , " \r \n " ) !== \false ? " \r \n " : " \n " ;
$lines = \explode ( $eol , \str_replace ( $eol , '<fg=default;bg=default>' . $eol . '</>' , $cell ));
$nbLines = \count ( $lines ) > $nbLines ? \substr_count ( $cell , $eol ) : $nbLines ;
2022-06-07 08:22:29 +00:00
$rows [ $line ][ $column ] = new TableCell ( $lines [ 0 ], [ 'colspan' => $cell -> getColspan (), 'style' => $cell -> getStyle ()]);
2021-05-10 00:23:30 +00:00
unset ( $lines [ 0 ]);
}
// create a two dimensional array (rowspan x colspan)
$unmergedRows = \array_replace_recursive ( \array_fill ( $line + 1 , $nbLines , []), $unmergedRows );
foreach ( $unmergedRows as $unmergedRowKey => $unmergedRow ) {
$value = $lines [ $unmergedRowKey - $line ] ? ? '' ;
2022-06-07 08:22:29 +00:00
$unmergedRows [ $unmergedRowKey ][ $column ] = new TableCell ( $value , [ 'colspan' => $cell -> getColspan (), 'style' => $cell -> getStyle ()]);
2021-05-10 00:23:30 +00:00
if ( $nbLines === $unmergedRowKey - $line ) {
break ;
}
}
}
}
foreach ( $unmergedRows as $unmergedRowKey => $unmergedRow ) {
// we need to know if $unmergedRow will be merged or inserted into $rows
if ( isset ( $rows [ $unmergedRowKey ]) && \is_array ( $rows [ $unmergedRowKey ]) && $this -> getNumberOfColumns ( $rows [ $unmergedRowKey ]) + $this -> getNumberOfColumns ( $unmergedRows [ $unmergedRowKey ]) <= $this -> numberOfColumns ) {
foreach ( $unmergedRow as $cellKey => $cell ) {
// insert cell into row at cellKey position
\array_splice ( $rows [ $unmergedRowKey ], $cellKey , 0 , [ $cell ]);
}
} else {
$row = $this -> copyRow ( $rows , $unmergedRowKey - 1 );
foreach ( $unmergedRow as $column => $cell ) {
if ( ! empty ( $cell )) {
$row [ $column ] = $unmergedRow [ $column ];
}
}
\array_splice ( $rows , $unmergedRowKey , 0 , [ $row ]);
}
}
return $rows ;
}
/**
* fill cells for a row that contains colspan > 1.
*/
2023-05-30 20:19:22 +00:00
private function fillCells ( iterable $row ) : iterable
2021-05-10 00:23:30 +00:00
{
$newRow = [];
foreach ( $row as $column => $cell ) {
$newRow [] = $cell ;
2022-06-07 08:22:29 +00:00
if ( $cell instanceof TableCell && $cell -> getColspan () > 1 ) {
2021-05-10 00:23:30 +00:00
foreach ( \range ( $column + 1 , $column + $cell -> getColspan () - 1 ) as $position ) {
// insert empty value at column position
$newRow [] = '' ;
}
}
}
return $newRow ? : $row ;
}
2021-07-05 23:06:30 +00:00
private function copyRow ( array $rows , int $line ) : array
2021-05-10 00:23:30 +00:00
{
$row = $rows [ $line ];
foreach ( $row as $cellKey => $cellValue ) {
$row [ $cellKey ] = '' ;
2022-06-07 08:22:29 +00:00
if ( $cellValue instanceof TableCell ) {
$row [ $cellKey ] = new TableCell ( '' , [ 'colspan' => $cellValue -> getColspan ()]);
2021-05-10 00:23:30 +00:00
}
}
return $row ;
}
/**
* Gets number of columns by row .
*/
2021-07-05 23:06:30 +00:00
private function getNumberOfColumns ( array $row ) : int
2021-05-10 00:23:30 +00:00
{
$columns = \count ( $row );
foreach ( $row as $column ) {
2022-06-07 08:22:29 +00:00
$columns += $column instanceof TableCell ? $column -> getColspan () - 1 : 0 ;
2021-05-10 00:23:30 +00:00
}
return $columns ;
}
/**
* Gets list of columns for the given row .
*/
2021-07-05 23:06:30 +00:00
private function getRowColumns ( array $row ) : array
2021-05-10 00:23:30 +00:00
{
$columns = \range ( 0 , $this -> numberOfColumns - 1 );
foreach ( $row as $cellKey => $cell ) {
2022-06-07 08:22:29 +00:00
if ( $cell instanceof TableCell && $cell -> getColspan () > 1 ) {
2021-05-10 00:23:30 +00:00
// exclude grouped columns.
$columns = \array_diff ( $columns , \range ( $cellKey + 1 , $cellKey + $cell -> getColspan () - 1 ));
}
}
return $columns ;
}
/**
* Calculates columns widths .
*/
2023-05-30 20:19:22 +00:00
private function calculateColumnsWidth ( iterable $groups ) : void
2021-05-10 00:23:30 +00:00
{
for ( $column = 0 ; $column < $this -> numberOfColumns ; ++ $column ) {
$lengths = [];
2022-04-27 22:40:38 +00:00
foreach ( $groups as $group ) {
foreach ( $group as $row ) {
2022-06-07 08:22:29 +00:00
if ( $row instanceof TableSeparator ) {
2022-04-27 22:40:38 +00:00
continue ;
}
foreach ( $row as $i => $cell ) {
2022-06-07 08:22:29 +00:00
if ( $cell instanceof TableCell ) {
$textContent = Helper :: removeDecoration ( $this -> output -> getFormatter (), $cell );
$textLength = Helper :: width ( $textContent );
2022-04-27 22:40:38 +00:00
if ( $textLength > 0 ) {
2023-04-29 04:13:11 +00:00
$contentColumns = \mb_str_split ( $textContent , \ceil ( $textLength / $cell -> getColspan ()));
2022-04-27 22:40:38 +00:00
foreach ( $contentColumns as $position => $content ) {
$row [ $i + $position ] = $content ;
}
2021-05-10 00:23:30 +00:00
}
}
}
2022-04-27 22:40:38 +00:00
$lengths [] = $this -> getCellWidth ( $row , $column );
2021-05-10 00:23:30 +00:00
}
}
2022-06-07 08:22:29 +00:00
$this -> effectiveColumnWidths [ $column ] = \max ( $lengths ) + Helper :: width ( $this -> style -> getCellRowContentFormat ()) - 2 ;
2021-05-10 00:23:30 +00:00
}
}
private function getColumnSeparatorWidth () : int
{
2022-06-07 08:22:29 +00:00
return Helper :: width ( \sprintf ( $this -> style -> getBorderFormat (), $this -> style -> getBorderChars ()[ 3 ]));
2021-05-10 00:23:30 +00:00
}
2021-07-05 23:06:30 +00:00
private function getCellWidth ( array $row , int $column ) : int
2021-05-10 00:23:30 +00:00
{
$cellWidth = 0 ;
if ( isset ( $row [ $column ])) {
$cell = $row [ $column ];
2022-06-07 08:22:29 +00:00
$cellWidth = Helper :: width ( Helper :: removeDecoration ( $this -> output -> getFormatter (), $cell ));
2021-05-10 00:23:30 +00:00
}
$columnWidth = $this -> columnWidths [ $column ] ? ? 0 ;
$cellWidth = \max ( $cellWidth , $columnWidth );
return isset ( $this -> columnMaxWidths [ $column ]) ? \min ( $this -> columnMaxWidths [ $column ], $cellWidth ) : $cellWidth ;
}
/**
* Called after rendering to cleanup cache data .
*/
2023-05-30 20:19:22 +00:00
private function cleanup () : void
2021-05-10 00:23:30 +00:00
{
$this -> effectiveColumnWidths = [];
2021-12-09 13:05:29 +00:00
unset ( $this -> numberOfColumns );
2021-05-10 00:23:30 +00:00
}
2021-11-30 07:57:42 +00:00
/**
* @ return array < string , TableStyle >
*/
2021-05-10 00:23:30 +00:00
private static function initStyles () : array
{
2022-06-07 08:22:29 +00:00
$borderless = new TableStyle ();
2021-05-10 00:23:30 +00:00
$borderless -> setHorizontalBorderChars ( '=' ) -> setVerticalBorderChars ( ' ' ) -> setDefaultCrossingChar ( ' ' );
2022-06-07 08:22:29 +00:00
$compact = new TableStyle ();
2022-04-02 08:38:27 +00:00
$compact -> setHorizontalBorderChars ( '' ) -> setVerticalBorderChars ( '' ) -> setDefaultCrossingChar ( '' ) -> setCellRowContentFormat ( '%s ' );
2022-06-07 08:22:29 +00:00
$styleGuide = new TableStyle ();
2021-05-10 00:23:30 +00:00
$styleGuide -> setHorizontalBorderChars ( '-' ) -> setVerticalBorderChars ( ' ' ) -> setDefaultCrossingChar ( ' ' ) -> setCellHeaderFormat ( '%s' );
2022-06-07 08:22:29 +00:00
$box = ( new TableStyle ()) -> setHorizontalBorderChars ( '─' ) -> setVerticalBorderChars ( '│' ) -> setCrossingChars ( '┼' , '┌' , '┬' , '┐' , '┤' , '┘' , '┴' , '└' , '├' );
$boxDouble = ( new TableStyle ()) -> setHorizontalBorderChars ( '═' , '─' ) -> setVerticalBorderChars ( '║' , '│' ) -> setCrossingChars ( '┼' , '╔' , '╤' , '╗' , '╢' , '╝' , '╧' , '╚' , '╟' , '╠' , '╪' , '╣' );
return [ 'default' => new TableStyle (), 'borderless' => $borderless , 'compact' => $compact , 'symfony-style-guide' => $styleGuide , 'box' => $box , 'box-double' => $boxDouble ];
2021-05-10 00:23:30 +00:00
}
2021-12-09 13:05:29 +00:00
/**
2022-04-26 08:13:18 +00:00
* @ param \Symfony\Component\Console\Helper\TableStyle | string $name
2021-12-09 13:05:29 +00:00
*/
2022-06-07 08:22:29 +00:00
private function resolveStyle ( $name ) : TableStyle
2021-05-10 00:23:30 +00:00
{
2022-06-07 08:22:29 +00:00
if ( $name instanceof TableStyle ) {
2021-05-10 00:23:30 +00:00
return $name ;
}
2021-12-09 13:05:29 +00:00
if ( ! isset ( self :: $styles [ $name ])) {
2022-06-07 08:22:29 +00:00
throw new InvalidArgumentException ( \sprintf ( 'Style "%s" is not defined.' , $name ));
2021-05-10 00:23:30 +00:00
}
2021-12-09 13:05:29 +00:00
return self :: $styles [ $name ];
2021-05-10 00:23:30 +00:00
}
}