Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

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