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