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