PluginProbe
Depicter — Popup & Slider Builder / trunk
Depicter — Popup & Slider Builder vtrunk
4.8.1 trunk 1.0.0 1.1.0 1.1.2 1.1.4 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.5 1.3.8 1.5.0 1.5.1 1.5.2 1.5.5 1.6.0 1.6.1 1.6.2 1.7.0 All 76 releases
depicter / vendor / symfony / console / Helper / Table.php

Table.php in Depicter — Popup & Slider Builder trunk, at vendor/symfony/console/Helper/Table.php

918 lines 29.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
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 $rowGroups = $this->buildTableRows($rows);
387 $this->calculateColumnsWidth($rowGroups);
388
389 $isHeader = !$this->horizontal;
390 $isFirstRow = $this->horizontal;
391 $hasTitle = (bool) $this->headerTitle;
392
393 foreach ($rowGroups as $rowGroup) {
394 $isHeaderSeparatorRendered = false;
395
396 foreach ($rowGroup as $row) {
397 if ($divider === $row) {
398 $isHeader = false;
399 $isFirstRow = true;
400
401 continue;
402 }
403
404 if ($row instanceof TableSeparator) {
405 $this->renderRowSeparator();
406
407 continue;
408 }
409
410 if (!$row) {
411 continue;
412 }
413
414 if ($isHeader && !$isHeaderSeparatorRendered) {
415 $this->renderRowSeparator(
416 $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
417 $hasTitle ? $this->headerTitle : null,
418 $hasTitle ? $this->style->getHeaderTitleFormat() : null
419 );
420 $hasTitle = false;
421 $isHeaderSeparatorRendered = true;
422 }
423
424 if ($isFirstRow) {
425 $this->renderRowSeparator(
426 $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
427 $hasTitle ? $this->headerTitle : null,
428 $hasTitle ? $this->style->getHeaderTitleFormat() : null
429 );
430 $isFirstRow = false;
431 $hasTitle = false;
432 }
433
434 if ($this->horizontal) {
435 $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
436 } else {
437 $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
438 }
439 }
440 }
441 $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
442
443 $this->cleanup();
444 $this->rendered = true;
445 }
446
447 /**
448 * Renders horizontal header separator.
449 *
450 * Example:
451 *
452 * +-----+-----------+-------+
453 */
454 private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null)
455 {
456 if (0 === $count = $this->numberOfColumns) {
457 return;
458 }
459
460 $borders = $this->style->getBorderChars();
461 if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) {
462 return;
463 }
464
465 $crossings = $this->style->getCrossingChars();
466 if (self::SEPARATOR_MID === $type) {
467 [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
468 } elseif (self::SEPARATOR_TOP === $type) {
469 [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
470 } elseif (self::SEPARATOR_TOP_BOTTOM === $type) {
471 [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
472 } else {
473 [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
474 }
475
476 $markup = $leftChar;
477 for ($column = 0; $column < $count; ++$column) {
478 $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
479 $markup .= $column === $count - 1 ? $rightChar : $midChar;
480 }
481
482 if (null !== $title) {
483 $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title)));
484 $markupLength = Helper::width($markup);
485 if ($titleLength > $limit = $markupLength - 4) {
486 $titleLength = $limit;
487 $formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, '')));
488 $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
489 }
490
491 $titleStart = intdiv($markupLength - $titleLength, 2);
492 if (false === mb_detect_encoding($markup, null, true)) {
493 $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
494 } else {
495 $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength);
496 }
497 }
498
499 $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
500 }
501
502 /**
503 * Renders vertical column separator.
504 */
505 private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
506 {
507 $borders = $this->style->getBorderChars();
508
509 return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
510 }
511
512 /**
513 * Renders table row.
514 *
515 * Example:
516 *
517 * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
518 */
519 private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null)
520 {
521 $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
522 $columns = $this->getRowColumns($row);
523 $last = \count($columns) - 1;
524 foreach ($columns as $i => $column) {
525 if ($firstCellFormat && 0 === $i) {
526 $rowContent .= $this->renderCell($row, $column, $firstCellFormat);
527 } else {
528 $rowContent .= $this->renderCell($row, $column, $cellFormat);
529 }
530 $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE);
531 }
532 $this->output->writeln($rowContent);
533 }
534
535 /**
536 * Renders table cell with padding.
537 */
538 private function renderCell(array $row, int $column, string $cellFormat): string
539 {
540 $cell = $row[$column] ?? '';
541 $width = $this->effectiveColumnWidths[$column];
542 if ($cell instanceof TableCell && $cell->getColspan() > 1) {
543 // add the width of the following columns(numbers of colspan).
544 foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
545 $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn];
546 }
547 }
548
549 // str_pad won't work properly with multi-byte strings, we need to fix the padding
550 if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
551 $width += \strlen($cell) - mb_strwidth($cell, $encoding);
552 }
553
554 $style = $this->getColumnStyle($column);
555
556 if ($cell instanceof TableSeparator) {
557 return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
558 }
559
560 $width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell));
561 $content = sprintf($style->getCellRowContentFormat(), $cell);
562
563 $padType = $style->getPadType();
564 if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
565 $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
566 if ($isNotStyledByTag) {
567 $cellFormat = $cell->getStyle()->getCellFormat();
568 if (!\is_string($cellFormat)) {
569 $tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';');
570 $cellFormat = '<'.$tag.'>%s</>';
571 }
572
573 if (strstr($content, '</>')) {
574 $content = str_replace('</>', '', $content);
575 $width -= 3;
576 }
577 if (strstr($content, '<fg=default;bg=default>')) {
578 $content = str_replace('<fg=default;bg=default>', '', $content);
579 $width -= \strlen('<fg=default;bg=default>');
580 }
581 }
582
583 $padType = $cell->getStyle()->getPadByAlign();
584 }
585
586 return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
587 }
588
589 /**
590 * Calculate number of columns for this table.
591 */
592 private function calculateNumberOfColumns(array $rows)
593 {
594 $columns = [0];
595 foreach ($rows as $row) {
596 if ($row instanceof TableSeparator) {
597 continue;
598 }
599
600 $columns[] = $this->getNumberOfColumns($row);
601 }
602
603 $this->numberOfColumns = max($columns);
604 }
605
606 private function buildTableRows(array $rows): TableRows
607 {
608 /** @var WrappableOutputFormatterInterface $formatter */
609 $formatter = $this->output->getFormatter();
610 $unmergedRows = [];
611 for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
612 $rows = $this->fillNextRows($rows, $rowKey);
613
614 // Remove any new line breaks and replace it with a new line
615 foreach ($rows[$rowKey] as $column => $cell) {
616 $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
617
618 if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
619 $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
620 }
621 if (!strstr($cell ?? '', "\n")) {
622 continue;
623 }
624 $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n";
625 $escaped = implode($eol, array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode($eol, $cell)));
626 $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
627 $lines = explode($eol, str_replace($eol, '<fg=default;bg=default></>'.$eol, $cell));
628 foreach ($lines as $lineKey => $line) {
629 if ($colspan > 1) {
630 $line = new TableCell($line, ['colspan' => $colspan]);
631 }
632 if (0 === $lineKey) {
633 $rows[$rowKey][$column] = $line;
634 } else {
635 if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) {
636 $unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey);
637 }
638 $unmergedRows[$rowKey][$lineKey][$column] = $line;
639 }
640 }
641 }
642 }
643
644 return new TableRows(function () use ($rows, $unmergedRows): \Traversable {
645 foreach ($rows as $rowKey => $row) {
646 $rowGroup = [$row instanceof TableSeparator ? $row : $this->fillCells($row)];
647
648 if (isset($unmergedRows[$rowKey])) {
649 foreach ($unmergedRows[$rowKey] as $row) {
650 $rowGroup[] = $row instanceof TableSeparator ? $row : $this->fillCells($row);
651 }
652 }
653 yield $rowGroup;
654 }
655 });
656 }
657
658 private function calculateRowCount(): int
659 {
660 $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows))));
661
662 if ($this->headers) {
663 ++$numberOfRows; // Add row for header separator
664 }
665
666 if (\count($this->rows) > 0) {
667 ++$numberOfRows; // Add row for footer separator
668 }
669
670 return $numberOfRows;
671 }
672
673 /**
674 * fill rows that contains rowspan > 1.
675 *
676 * @throws InvalidArgumentException
677 */
678 private function fillNextRows(array $rows, int $line): array
679 {
680 $unmergedRows = [];
681 foreach ($rows[$line] as $column => $cell) {
682 if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
683 throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
684 }
685 if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
686 $nbLines = $cell->getRowspan() - 1;
687 $lines = [$cell];
688 if (strstr($cell, "\n")) {
689 $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
690 $lines = explode($eol, str_replace($eol, '<fg=default;bg=default>'.$eol.'</>', $cell));
691 $nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines;
692
693 $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
694 unset($lines[0]);
695 }
696
697 // create a two dimensional array (rowspan x colspan)
698 $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
699 foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
700 $value = $lines[$unmergedRowKey - $line] ?? '';
701 $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
702 if ($nbLines === $unmergedRowKey - $line) {
703 break;
704 }
705 }
706 }
707 }
708
709 foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
710 // we need to know if $unmergedRow will be merged or inserted into $rows
711 if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
712 foreach ($unmergedRow as $cellKey => $cell) {
713 // insert cell into row at cellKey position
714 array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]);
715 }
716 } else {
717 $row = $this->copyRow($rows, $unmergedRowKey - 1);
718 foreach ($unmergedRow as $column => $cell) {
719 if (!empty($cell)) {
720 $row[$column] = $unmergedRow[$column];
721 }
722 }
723 array_splice($rows, $unmergedRowKey, 0, [$row]);
724 }
725 }
726
727 return $rows;
728 }
729
730 /**
731 * fill cells for a row that contains colspan > 1.
732 */
733 private function fillCells(iterable $row)
734 {
735 $newRow = [];
736
737 foreach ($row as $column => $cell) {
738 $newRow[] = $cell;
739 if ($cell instanceof TableCell && $cell->getColspan() > 1) {
740 foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
741 // insert empty value at column position
742 $newRow[] = '';
743 }
744 }
745 }
746
747 return $newRow ?: $row;
748 }
749
750 private function copyRow(array $rows, int $line): array
751 {
752 $row = $rows[$line];
753 foreach ($row as $cellKey => $cellValue) {
754 $row[$cellKey] = '';
755 if ($cellValue instanceof TableCell) {
756 $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]);
757 }
758 }
759
760 return $row;
761 }
762
763 /**
764 * Gets number of columns by row.
765 */
766 private function getNumberOfColumns(array $row): int
767 {
768 $columns = \count($row);
769 foreach ($row as $column) {
770 $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0;
771 }
772
773 return $columns;
774 }
775
776 /**
777 * Gets list of columns for the given row.
778 */
779 private function getRowColumns(array $row): array
780 {
781 $columns = range(0, $this->numberOfColumns - 1);
782 foreach ($row as $cellKey => $cell) {
783 if ($cell instanceof TableCell && $cell->getColspan() > 1) {
784 // exclude grouped columns.
785 $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1));
786 }
787 }
788
789 return $columns;
790 }
791
792 /**
793 * Calculates columns widths.
794 */
795 private function calculateColumnsWidth(iterable $groups)
796 {
797 for ($column = 0; $column < $this->numberOfColumns; ++$column) {
798 $lengths = [];
799 foreach ($groups as $group) {
800 foreach ($group as $row) {
801 if ($row instanceof TableSeparator) {
802 continue;
803 }
804
805 foreach ($row as $i => $cell) {
806 if ($cell instanceof TableCell) {
807 $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
808 $textLength = Helper::width($textContent);
809 if ($textLength > 0) {
810 $contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan()));
811 foreach ($contentColumns as $position => $content) {
812 $row[$i + $position] = $content;
813 }
814 }
815 }
816 }
817
818 $lengths[] = $this->getCellWidth($row, $column);
819 }
820 }
821
822 $this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2;
823 }
824 }
825
826 private function getColumnSeparatorWidth(): int
827 {
828 return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
829 }
830
831 private function getCellWidth(array $row, int $column): int
832 {
833 $cellWidth = 0;
834
835 if (isset($row[$column])) {
836 $cell = $row[$column];
837 $cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell));
838 }
839
840 $columnWidth = $this->columnWidths[$column] ?? 0;
841 $cellWidth = max($cellWidth, $columnWidth);
842
843 return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
844 }
845
846 /**
847 * Called after rendering to cleanup cache data.
848 */
849 private function cleanup()
850 {
851 $this->effectiveColumnWidths = [];
852 $this->numberOfColumns = null;
853 }
854
855 /**
856 * @return array<string, TableStyle>
857 */
858 private static function initStyles(): array
859 {
860 $borderless = new TableStyle();
861 $borderless
862 ->setHorizontalBorderChars('=')
863 ->setVerticalBorderChars(' ')
864 ->setDefaultCrossingChar(' ')
865 ;
866
867 $compact = new TableStyle();
868 $compact
869 ->setHorizontalBorderChars('')
870 ->setVerticalBorderChars('')
871 ->setDefaultCrossingChar('')
872 ->setCellRowContentFormat('%s ')
873 ;
874
875 $styleGuide = new TableStyle();
876 $styleGuide
877 ->setHorizontalBorderChars('-')
878 ->setVerticalBorderChars(' ')
879 ->setDefaultCrossingChar(' ')
880 ->setCellHeaderFormat('%s')
881 ;
882
883 $box = (new TableStyle())
884 ->setHorizontalBorderChars('')
885 ->setVerticalBorderChars('')
886 ->setCrossingChars('', '', '', '', '', '', '', '', '')
887 ;
888
889 $boxDouble = (new TableStyle())
890 ->setHorizontalBorderChars('', '')
891 ->setVerticalBorderChars('', '')
892 ->setCrossingChars('', '', '', '', '', '', '', '', '', '', '', '')
893 ;
894
895 return [
896 'default' => new TableStyle(),
897 'borderless' => $borderless,
898 'compact' => $compact,
899 'symfony-style-guide' => $styleGuide,
900 'box' => $box,
901 'box-double' => $boxDouble,
902 ];
903 }
904
905 private function resolveStyle($name): TableStyle
906 {
907 if ($name instanceof TableStyle) {
908 return $name;
909 }
910
911 if (isset(self::$styles[$name])) {
912 return self::$styles[$name];
913 }
914
915 throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
916 }
917 }
918