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