1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Helper;
13
14use Symfony\Component\Console\Exception\InvalidArgumentException;
15use Symfony\Component\Console\Exception\RuntimeException;
16use Symfony\Component\Console\Formatter\OutputFormatter;
17use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface;
18use Symfony\Component\Console\Output\ConsoleSectionOutput;
19use Symfony\Component\Console\Output\OutputInterface;
20
21/**
22 * Provides helpers to display a table.
23 *
24 * @author Fabien Potencier <fabien@symfony.com>
25 * @author Саша Стаменковић <umpirsky@gmail.com>
26 * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
27 * @author Max Grigorian <maxakawizard@gmail.com>
28 * @author Dany Maillard <danymaillard93b@gmail.com>
29 */
30class Table
31{
32    private const SEPARATOR_TOP = 0;
33    private const SEPARATOR_TOP_BOTTOM = 1;
34    private const SEPARATOR_MID = 2;
35    private const SEPARATOR_BOTTOM = 3;
36    private const BORDER_OUTSIDE = 0;
37    private const BORDER_INSIDE = 1;
38
39    private $headerTitle;
40    private $footerTitle;
41
42    /**
43     * Table headers.
44     */
45    private $headers = [];
46
47    /**
48     * Table rows.
49     */
50    private $rows = [];
51    private $horizontal = false;
52
53    /**
54     * Column widths cache.
55     */
56    private $effectiveColumnWidths = [];
57
58    /**
59     * Number of columns cache.
60     *
61     * @var int
62     */
63    private $numberOfColumns;
64
65    /**
66     * @var OutputInterface
67     */
68    private $output;
69
70    /**
71     * @var TableStyle
72     */
73    private $style;
74
75    /**
76     * @var array
77     */
78    private $columnStyles = [];
79
80    /**
81     * User set column widths.
82     *
83     * @var array
84     */
85    private $columnWidths = [];
86    private $columnMaxWidths = [];
87
88    private static $styles;
89
90    private $rendered = false;
91
92    public function __construct(OutputInterface $output)
93    {
94        $this->output = $output;
95
96        if (!self::$styles) {
97            self::$styles = self::initStyles();
98        }
99
100        $this->setStyle('default');
101    }
102
103    /**
104     * Sets a style definition.
105     */
106    public static function setStyleDefinition(string $name, TableStyle $style)
107    {
108        if (!self::$styles) {
109            self::$styles = self::initStyles();
110        }
111
112        self::$styles[$name] = $style;
113    }
114
115    /**
116     * Gets a style definition by name.
117     *
118     * @return TableStyle
119     */
120    public static function getStyleDefinition(string $name)
121    {
122        if (!self::$styles) {
123            self::$styles = self::initStyles();
124        }
125
126        if (isset(self::$styles[$name])) {
127            return self::$styles[$name];
128        }
129
130        throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
131    }
132
133    /**
134     * Sets table style.
135     *
136     * @param TableStyle|string $name The style name or a TableStyle instance
137     *
138     * @return $this
139     */
140    public function setStyle($name)
141    {
142        $this->style = $this->resolveStyle($name);
143
144        return $this;
145    }
146
147    /**
148     * Gets the current table style.
149     *
150     * @return TableStyle
151     */
152    public function getStyle()
153    {
154        return $this->style;
155    }
156
157    /**
158     * Sets table column style.
159     *
160     * @param TableStyle|string $name The style name or a TableStyle instance
161     *
162     * @return $this
163     */
164    public function setColumnStyle(int $columnIndex, $name)
165    {
166        $this->columnStyles[$columnIndex] = $this->resolveStyle($name);
167
168        return $this;
169    }
170
171    /**
172     * Gets the current style for a column.
173     *
174     * If style was not set, it returns the global table style.
175     *
176     * @return TableStyle
177     */
178    public function getColumnStyle(int $columnIndex)
179    {
180        return $this->columnStyles[$columnIndex] ?? $this->getStyle();
181    }
182
183    /**
184     * Sets the minimum width of a column.
185     *
186     * @return $this
187     */
188    public function setColumnWidth(int $columnIndex, int $width)
189    {
190        $this->columnWidths[$columnIndex] = $width;
191
192        return $this;
193    }
194
195    /**
196     * Sets the minimum width of all columns.
197     *
198     * @return $this
199     */
200    public function setColumnWidths(array $widths)
201    {
202        $this->columnWidths = [];
203        foreach ($widths as $index => $width) {
204            $this->setColumnWidth($index, $width);
205        }
206
207        return $this;
208    }
209
210    /**
211     * Sets the maximum width of a column.
212     *
213     * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while
214     * formatted strings are preserved.
215     *
216     * @return $this
217     */
218    public function setColumnMaxWidth(int $columnIndex, int $width): self
219    {
220        if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
221            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())));
222        }
223
224        $this->columnMaxWidths[$columnIndex] = $width;
225
226        return $this;
227    }
228
229    public function setHeaders(array $headers)
230    {
231        $headers = array_values($headers);
232        if (!empty($headers) && !\is_array($headers[0])) {
233            $headers = [$headers];
234        }
235
236        $this->headers = $headers;
237
238        return $this;
239    }
240
241    public function setRows(array $rows)
242    {
243        $this->rows = [];
244
245        return $this->addRows($rows);
246    }
247
248    public function addRows(array $rows)
249    {
250        foreach ($rows as $row) {
251            $this->addRow($row);
252        }
253
254        return $this;
255    }
256
257    public function addRow($row)
258    {
259        if ($row instanceof TableSeparator) {
260            $this->rows[] = $row;
261
262            return $this;
263        }
264
265        if (!\is_array($row)) {
266            throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.');
267        }
268
269        $this->rows[] = array_values($row);
270
271        return $this;
272    }
273
274    /**
275     * Adds a row to the table, and re-renders the table.
276     */
277    public function appendRow($row): self
278    {
279        if (!$this->output instanceof ConsoleSectionOutput) {
280            throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
281        }
282
283        if ($this->rendered) {
284            $this->output->clear($this->calculateRowCount());
285        }
286
287        $this->addRow($row);
288        $this->render();
289
290        return $this;
291    }
292
293    public function setRow($column, array $row)
294    {
295        $this->rows[$column] = $row;
296
297        return $this;
298    }
299
300    public function setHeaderTitle(?string $title): self
301    {
302        $this->headerTitle = $title;
303
304        return $this;
305    }
306
307    public function setFooterTitle(?string $title): self
308    {
309        $this->footerTitle = $title;
310
311        return $this;
312    }
313
314    public function setHorizontal(bool $horizontal = true): self
315    {
316        $this->horizontal = $horizontal;
317
318        return $this;
319    }
320
321    /**
322     * Renders table to output.
323     *
324     * Example:
325     *
326     *     +---------------+-----------------------+------------------+
327     *     | ISBN          | Title                 | Author           |
328     *     +---------------+-----------------------+------------------+
329     *     | 99921-58-10-7 | Divine Comedy         | Dante Alighieri  |
330     *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
331     *     | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
332     *     +---------------+-----------------------+------------------+
333     */
334    public function render()
335    {
336        $divider = new TableSeparator();
337        if ($this->horizontal) {
338            $rows = [];
339            foreach ($this->headers[0] ?? [] as $i => $header) {
340                $rows[$i] = [$header];
341                foreach ($this->rows as $row) {
342                    if ($row instanceof TableSeparator) {
343                        continue;
344                    }
345                    if (isset($row[$i])) {
346                        $rows[$i][] = $row[$i];
347                    } elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) {
348                        // Noop, there is a "title"
349                    } else {
350                        $rows[$i][] = null;
351                    }
352                }
353            }
354        } else {
355            $rows = array_merge($this->headers, [$divider], $this->rows);
356        }
357
358        $this->calculateNumberOfColumns($rows);
359
360        $rows = $this->buildTableRows($rows);
361        $this->calculateColumnsWidth($rows);
362
363        $isHeader = !$this->horizontal;
364        $isFirstRow = $this->horizontal;
365        foreach ($rows as $row) {
366            if ($divider === $row) {
367                $isHeader = false;
368                $isFirstRow = true;
369
370                continue;
371            }
372            if ($row instanceof TableSeparator) {
373                $this->renderRowSeparator();
374
375                continue;
376            }
377            if (!$row) {
378                continue;
379            }
380
381            if ($isHeader || $isFirstRow) {
382                if ($isFirstRow) {
383                    $this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM);
384                    $isFirstRow = false;
385                } else {
386                    $this->renderRowSeparator(self::SEPARATOR_TOP, $this->headerTitle, $this->style->getHeaderTitleFormat());
387                }
388            }
389            if ($this->horizontal) {
390                $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
391            } else {
392                $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
393            }
394        }
395        $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
396
397        $this->cleanup();
398        $this->rendered = true;
399    }
400
401    /**
402     * Renders horizontal header separator.
403     *
404     * Example:
405     *
406     *     +-----+-----------+-------+
407     */
408    private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $title = null, string $titleFormat = null)
409    {
410        if (0 === $count = $this->numberOfColumns) {
411            return;
412        }
413
414        $borders = $this->style->getBorderChars();
415        if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) {
416            return;
417        }
418
419        $crossings = $this->style->getCrossingChars();
420        if (self::SEPARATOR_MID === $type) {
421            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
422        } elseif (self::SEPARATOR_TOP === $type) {
423            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
424        } elseif (self::SEPARATOR_TOP_BOTTOM === $type) {
425            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
426        } else {
427            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
428        }
429
430        $markup = $leftChar;
431        for ($column = 0; $column < $count; ++$column) {
432            $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
433            $markup .= $column === $count - 1 ? $rightChar : $midChar;
434        }
435
436        if (null !== $title) {
437            $titleLength = Helper::strlenWithoutDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title));
438            $markupLength = Helper::strlen($markup);
439            if ($titleLength > $limit = $markupLength - 4) {
440                $titleLength = $limit;
441                $formatLength = Helper::strlenWithoutDecoration($formatter, sprintf($titleFormat, ''));
442                $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
443            }
444
445            $titleStart = ($markupLength - $titleLength) / 2;
446            if (false === mb_detect_encoding($markup, null, true)) {
447                $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
448            } else {
449                $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength);
450            }
451        }
452
453        $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
454    }
455
456    /**
457     * Renders vertical column separator.
458     */
459    private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
460    {
461        $borders = $this->style->getBorderChars();
462
463        return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
464    }
465
466    /**
467     * Renders table row.
468     *
469     * Example:
470     *
471     *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
472     */
473    private function renderRow(array $row, string $cellFormat, string $firstCellFormat = null)
474    {
475        $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
476        $columns = $this->getRowColumns($row);
477        $last = \count($columns) - 1;
478        foreach ($columns as $i => $column) {
479            if ($firstCellFormat && 0 === $i) {
480                $rowContent .= $this->renderCell($row, $column, $firstCellFormat);
481            } else {
482                $rowContent .= $this->renderCell($row, $column, $cellFormat);
483            }
484            $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE);
485        }
486        $this->output->writeln($rowContent);
487    }
488
489    /**
490     * Renders table cell with padding.
491     */
492    private function renderCell(array $row, int $column, string $cellFormat): string
493    {
494        $cell = isset($row[$column]) ? $row[$column] : '';
495        $width = $this->effectiveColumnWidths[$column];
496        if ($cell instanceof TableCell && $cell->getColspan() > 1) {
497            // add the width of the following columns(numbers of colspan).
498            foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
499                $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn];
500            }
501        }
502
503        // str_pad won't work properly with multi-byte strings, we need to fix the padding
504        if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
505            $width += \strlen($cell) - mb_strwidth($cell, $encoding);
506        }
507
508        $style = $this->getColumnStyle($column);
509
510        if ($cell instanceof TableSeparator) {
511            return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
512        }
513
514        $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
515        $content = sprintf($style->getCellRowContentFormat(), $cell);
516
517        $padType = $style->getPadType();
518        if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
519            $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
520            if ($isNotStyledByTag) {
521                $cellFormat = $cell->getStyle()->getCellFormat();
522                if (!\is_string($cellFormat)) {
523                    $tag = http_build_query($cell->getStyle()->getTagOptions(), null, ';');
524                    $cellFormat = '<'.$tag.'>%s</>';
525                }
526
527                if (strstr($content, '</>')) {
528                    $content = str_replace('</>', '', $content);
529                    $width -= 3;
530                }
531                if (strstr($content, '<fg=default;bg=default>')) {
532                    $content = str_replace('<fg=default;bg=default>', '', $content);
533                    $width -= \strlen('<fg=default;bg=default>');
534                }
535            }
536
537            $padType = $cell->getStyle()->getPadByAlign();
538        }
539
540        return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
541    }
542
543    /**
544     * Calculate number of columns for this table.
545     */
546    private function calculateNumberOfColumns(array $rows)
547    {
548        $columns = [0];
549        foreach ($rows as $row) {
550            if ($row instanceof TableSeparator) {
551                continue;
552            }
553
554            $columns[] = $this->getNumberOfColumns($row);
555        }
556
557        $this->numberOfColumns = max($columns);
558    }
559
560    private function buildTableRows(array $rows): TableRows
561    {
562        /** @var WrappableOutputFormatterInterface $formatter */
563        $formatter = $this->output->getFormatter();
564        $unmergedRows = [];
565        for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
566            $rows = $this->fillNextRows($rows, $rowKey);
567
568            // Remove any new line breaks and replace it with a new line
569            foreach ($rows[$rowKey] as $column => $cell) {
570                $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
571
572                if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) {
573                    $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
574                }
575                if (!strstr($cell, "\n")) {
576                    continue;
577                }
578                $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell)));
579                $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
580                $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
581                foreach ($lines as $lineKey => $line) {
582                    if ($colspan > 1) {
583                        $line = new TableCell($line, ['colspan' => $colspan]);
584                    }
585                    if (0 === $lineKey) {
586                        $rows[$rowKey][$column] = $line;
587                    } else {
588                        if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) {
589                            $unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey);
590                        }
591                        $unmergedRows[$rowKey][$lineKey][$column] = $line;
592                    }
593                }
594            }
595        }
596
597        return new TableRows(function () use ($rows, $unmergedRows): \Traversable {
598            foreach ($rows as $rowKey => $row) {
599                yield $this->fillCells($row);
600
601                if (isset($unmergedRows[$rowKey])) {
602                    foreach ($unmergedRows[$rowKey] as $unmergedRow) {
603                        yield $this->fillCells($unmergedRow);
604                    }
605                }
606            }
607        });
608    }
609
610    private function calculateRowCount(): int
611    {
612        $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows))));
613
614        if ($this->headers) {
615            ++$numberOfRows; // Add row for header separator
616        }
617
618        if (\count($this->rows) > 0) {
619            ++$numberOfRows; // Add row for footer separator
620        }
621
622        return $numberOfRows;
623    }
624
625    /**
626     * fill rows that contains rowspan > 1.
627     *
628     * @throws InvalidArgumentException
629     */
630    private function fillNextRows(array $rows, int $line): array
631    {
632        $unmergedRows = [];
633        foreach ($rows[$line] as $column => $cell) {
634            if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
635                throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
636            }
637            if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
638                $nbLines = $cell->getRowspan() - 1;
639                $lines = [$cell];
640                if (strstr($cell, "\n")) {
641                    $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
642                    $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
643
644                    $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
645                    unset($lines[0]);
646                }
647
648                // create a two dimensional array (rowspan x colspan)
649                $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
650                foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
651                    $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : '';
652                    $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
653                    if ($nbLines === $unmergedRowKey - $line) {
654                        break;
655                    }
656                }
657            }
658        }
659
660        foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
661            // we need to know if $unmergedRow will be merged or inserted into $rows
662            if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
663                foreach ($unmergedRow as $cellKey => $cell) {
664                    // insert cell into row at cellKey position
665                    array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]);
666                }
667            } else {
668                $row = $this->copyRow($rows, $unmergedRowKey - 1);
669                foreach ($unmergedRow as $column => $cell) {
670                    if (!empty($cell)) {
671                        $row[$column] = $unmergedRow[$column];
672                    }
673                }
674                array_splice($rows, $unmergedRowKey, 0, [$row]);
675            }
676        }
677
678        return $rows;
679    }
680
681    /**
682     * fill cells for a row that contains colspan > 1.
683     */
684    private function fillCells($row)
685    {
686        $newRow = [];
687
688        foreach ($row as $column => $cell) {
689            $newRow[] = $cell;
690            if ($cell instanceof TableCell && $cell->getColspan() > 1) {
691                foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
692                    // insert empty value at column position
693                    $newRow[] = '';
694                }
695            }
696        }
697
698        return $newRow ?: $row;
699    }
700
701    private function copyRow(array $rows, int $line): array
702    {
703        $row = $rows[$line];
704        foreach ($row as $cellKey => $cellValue) {
705            $row[$cellKey] = '';
706            if ($cellValue instanceof TableCell) {
707                $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]);
708            }
709        }
710
711        return $row;
712    }
713
714    /**
715     * Gets number of columns by row.
716     */
717    private function getNumberOfColumns(array $row): int
718    {
719        $columns = \count($row);
720        foreach ($row as $column) {
721            $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0;
722        }
723
724        return $columns;
725    }
726
727    /**
728     * Gets list of columns for the given row.
729     */
730    private function getRowColumns(array $row): array
731    {
732        $columns = range(0, $this->numberOfColumns - 1);
733        foreach ($row as $cellKey => $cell) {
734            if ($cell instanceof TableCell && $cell->getColspan() > 1) {
735                // exclude grouped columns.
736                $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1));
737            }
738        }
739
740        return $columns;
741    }
742
743    /**
744     * Calculates columns widths.
745     */
746    private function calculateColumnsWidth(iterable $rows)
747    {
748        for ($column = 0; $column < $this->numberOfColumns; ++$column) {
749            $lengths = [];
750            foreach ($rows as $row) {
751                if ($row instanceof TableSeparator) {
752                    continue;
753                }
754
755                foreach ($row as $i => $cell) {
756                    if ($cell instanceof TableCell) {
757                        $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
758                        $textLength = Helper::strlen($textContent);
759                        if ($textLength > 0) {
760                            $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
761                            foreach ($contentColumns as $position => $content) {
762                                $row[$i + $position] = $content;
763                            }
764                        }
765                    }
766                }
767
768                $lengths[] = $this->getCellWidth($row, $column);
769            }
770
771            $this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2;
772        }
773    }
774
775    private function getColumnSeparatorWidth(): int
776    {
777        return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
778    }
779
780    private function getCellWidth(array $row, int $column): int
781    {
782        $cellWidth = 0;
783
784        if (isset($row[$column])) {
785            $cell = $row[$column];
786            $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
787        }
788
789        $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
790        $cellWidth = max($cellWidth, $columnWidth);
791
792        return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
793    }
794
795    /**
796     * Called after rendering to cleanup cache data.
797     */
798    private function cleanup()
799    {
800        $this->effectiveColumnWidths = [];
801        $this->numberOfColumns = null;
802    }
803
804    private static function initStyles(): array
805    {
806        $borderless = new TableStyle();
807        $borderless
808            ->setHorizontalBorderChars('=')
809            ->setVerticalBorderChars(' ')
810            ->setDefaultCrossingChar(' ')
811        ;
812
813        $compact = new TableStyle();
814        $compact
815            ->setHorizontalBorderChars('')
816            ->setVerticalBorderChars(' ')
817            ->setDefaultCrossingChar('')
818            ->setCellRowContentFormat('%s')
819        ;
820
821        $styleGuide = new TableStyle();
822        $styleGuide
823            ->setHorizontalBorderChars('-')
824            ->setVerticalBorderChars(' ')
825            ->setDefaultCrossingChar(' ')
826            ->setCellHeaderFormat('%s')
827        ;
828
829        $box = (new TableStyle())
830            ->setHorizontalBorderChars('─')
831            ->setVerticalBorderChars('│')
832            ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
833        ;
834
835        $boxDouble = (new TableStyle())
836            ->setHorizontalBorderChars('═', '─')
837            ->setVerticalBorderChars('║', '│')
838            ->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣')
839        ;
840
841        return [
842            'default' => new TableStyle(),
843            'borderless' => $borderless,
844            'compact' => $compact,
845            'symfony-style-guide' => $styleGuide,
846            'box' => $box,
847            'box-double' => $boxDouble,
848        ];
849    }
850
851    private function resolveStyle($name): TableStyle
852    {
853        if ($name instanceof TableStyle) {
854            return $name;
855        }
856
857        if (isset(self::$styles[$name])) {
858            return self::$styles[$name];
859        }
860
861        throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
862    }
863}
864