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