Patched to Drupal 8.4.8 level. See https://www.drupal.org/sa-core-2018-004 and patch...
[yaffs-website] / vendor / pear / console_table / Table.php
1 <?php
2 /**
3  * Utility for printing tables from commandline scripts.
4  *
5  * PHP versions 5 and 7
6  *
7  * All rights reserved.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions are met:
11  *
12  * o Redistributions of source code must retain the above copyright notice,
13  *   this list of conditions and the following disclaimer.
14  * o Redistributions in binary form must reproduce the above copyright notice,
15  *   this list of conditions and the following disclaimer in the documentation
16  *   and/or other materials provided with the distribution.
17  * o The names of the authors may not be used to endorse or promote products
18  *   derived from this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  *
32  * @category  Console
33  * @package   Console_Table
34  * @author    Richard Heyes <richard@phpguru.org>
35  * @author    Jan Schneider <jan@horde.org>
36  * @copyright 2002-2005 Richard Heyes
37  * @copyright 2006-2008 Jan Schneider
38  * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
39  * @version   CVS: $Id$
40  * @link      http://pear.php.net/package/Console_Table
41  */
42
43 define('CONSOLE_TABLE_HORIZONTAL_RULE', 1);
44 define('CONSOLE_TABLE_ALIGN_LEFT', -1);
45 define('CONSOLE_TABLE_ALIGN_CENTER', 0);
46 define('CONSOLE_TABLE_ALIGN_RIGHT', 1);
47 define('CONSOLE_TABLE_BORDER_ASCII', -1);
48
49 /**
50  * The main class.
51  *
52  * @category Console
53  * @package  Console_Table
54  * @author   Jan Schneider <jan@horde.org>
55  * @license  http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
56  * @link     http://pear.php.net/package/Console_Table
57  */
58 class Console_Table
59 {
60     /**
61      * The table headers.
62      *
63      * @var array
64      */
65     var $_headers = array();
66
67     /**
68      * The data of the table.
69      *
70      * @var array
71      */
72     var $_data = array();
73
74     /**
75      * The maximum number of columns in a row.
76      *
77      * @var integer
78      */
79     var $_max_cols = 0;
80
81     /**
82      * The maximum number of rows in the table.
83      *
84      * @var integer
85      */
86     var $_max_rows = 0;
87
88     /**
89      * Lengths of the columns, calculated when rows are added to the table.
90      *
91      * @var array
92      */
93     var $_cell_lengths = array();
94
95     /**
96      * Heights of the rows.
97      *
98      * @var array
99      */
100     var $_row_heights = array();
101
102     /**
103      * How many spaces to use to pad the table.
104      *
105      * @var integer
106      */
107     var $_padding = 1;
108
109     /**
110      * Column filters.
111      *
112      * @var array
113      */
114     var $_filters = array();
115
116     /**
117      * Columns to calculate totals for.
118      *
119      * @var array
120      */
121     var $_calculateTotals;
122
123     /**
124      * Alignment of the columns.
125      *
126      * @var array
127      */
128     var $_col_align = array();
129
130     /**
131      * Default alignment of columns.
132      *
133      * @var integer
134      */
135     var $_defaultAlign;
136
137     /**
138      * Character set of the data.
139      *
140      * @var string
141      */
142     var $_charset = 'utf-8';
143
144     /**
145      * Border characters.
146      * Allowed keys:
147      * - intersection - intersection ("+")
148      * - horizontal - horizontal rule character ("-")
149      * - vertical - vertical rule character ("|")
150      *
151      * @var array
152      */
153     var $_border = array(
154         'intersection' => '+',
155         'horizontal' => '-',
156         'vertical' => '|',
157     );
158
159     /**
160      * If borders are shown or not
161      * Allowed keys: top, right, bottom, left, inner: true and false
162      *
163      * @var array
164      */
165     var $_borderVisibility = array(
166         'top'    => true,
167         'right'  => true,
168         'bottom' => true,
169         'left'   => true,
170         'inner'  => true
171     );
172
173     /**
174      * Whether the data has ANSI colors.
175      *
176      * @var Console_Color2
177      */
178     var $_ansiColor = false;
179
180     /**
181      * Constructor.
182      *
183      * @param integer $align   Default alignment. One of
184      *                         CONSOLE_TABLE_ALIGN_LEFT,
185      *                         CONSOLE_TABLE_ALIGN_CENTER or
186      *                         CONSOLE_TABLE_ALIGN_RIGHT.
187      * @param string  $border  The character used for table borders or
188      *                         CONSOLE_TABLE_BORDER_ASCII.
189      * @param integer $padding How many spaces to use to pad the table.
190      * @param string  $charset A charset supported by the mbstring PHP
191      *                         extension.
192      * @param boolean $color   Whether the data contains ansi color codes.
193      */
194     function __construct($align = CONSOLE_TABLE_ALIGN_LEFT,
195                          $border = CONSOLE_TABLE_BORDER_ASCII, $padding = 1,
196                          $charset = null, $color = false)
197     {
198         $this->_defaultAlign = $align;
199         $this->setBorder($border);
200         $this->_padding      = $padding;
201         if ($color) {
202             if (!class_exists('Console_Color2')) {
203                 include_once 'Console/Color2.php';
204             }
205             $this->_ansiColor = new Console_Color2();
206         }
207         if (!empty($charset)) {
208             $this->setCharset($charset);
209         }
210     }
211
212     /**
213      * Converts an array to a table.
214      *
215      * @param array   $headers      Headers for the table.
216      * @param array   $data         A two dimensional array with the table
217      *                              data.
218      * @param boolean $returnObject Whether to return the Console_Table object
219      *                              instead of the rendered table.
220      *
221      * @static
222      *
223      * @return Console_Table|string  A Console_Table object or the generated
224      *                               table.
225      */
226     function fromArray($headers, $data, $returnObject = false)
227     {
228         if (!is_array($headers) || !is_array($data)) {
229             return false;
230         }
231
232         $table = new Console_Table();
233         $table->setHeaders($headers);
234
235         foreach ($data as $row) {
236             $table->addRow($row);
237         }
238
239         return $returnObject ? $table : $table->getTable();
240     }
241
242     /**
243      * Adds a filter to a column.
244      *
245      * Filters are standard PHP callbacks which are run on the data before
246      * table generation is performed. Filters are applied in the order they
247      * are added. The callback function must accept a single argument, which
248      * is a single table cell.
249      *
250      * @param integer $col       Column to apply filter to.
251      * @param mixed   &$callback PHP callback to apply.
252      *
253      * @return void
254      */
255     function addFilter($col, &$callback)
256     {
257         $this->_filters[] = array($col, &$callback);
258     }
259
260     /**
261      * Sets the charset of the provided table data.
262      *
263      * @param string $charset A charset supported by the mbstring PHP
264      *                        extension.
265      *
266      * @return void
267      */
268     function setCharset($charset)
269     {
270         $locale = setlocale(LC_CTYPE, 0);
271         setlocale(LC_CTYPE, 'en_US');
272         $this->_charset = strtolower($charset);
273         setlocale(LC_CTYPE, $locale);
274     }
275
276     /**
277      * Set the table border settings
278      *
279      * Border definition modes:
280      * - CONSOLE_TABLE_BORDER_ASCII: Default border with +, - and |
281      * - array with keys "intersection", "horizontal" and "vertical"
282      * - single character string that sets all three of the array keys
283      *
284      * @param mixed $border Border definition
285      *
286      * @return void
287      * @see $_border
288      */
289     function setBorder($border)
290     {
291         if ($border === CONSOLE_TABLE_BORDER_ASCII) {
292             $intersection = '+';
293             $horizontal = '-';
294             $vertical = '|';
295         } else if (is_string($border)) {
296             $intersection = $horizontal = $vertical = $border;
297         } else if ($border == '') {
298             $intersection = $horizontal = $vertical = '';
299         } else {
300             extract($border);
301         }
302
303         $this->_border = array(
304             'intersection' => $intersection,
305             'horizontal' => $horizontal,
306             'vertical' => $vertical,
307         );
308     }
309
310     /**
311      * Set which borders shall be shown.
312      *
313      * @param array $visibility Visibility settings.
314      *                          Allowed keys: left, right, top, bottom, inner
315      *
316      * @return void
317      * @see    $_borderVisibility
318      */
319     function setBorderVisibility($visibility)
320     {
321         $this->_borderVisibility = array_merge(
322             $this->_borderVisibility,
323             array_intersect_key(
324                 $visibility,
325                 $this->_borderVisibility
326             )
327         );
328     }
329
330     /**
331      * Sets the alignment for the columns.
332      *
333      * @param integer $col_id The column number.
334      * @param integer $align  Alignment to set for this column. One of
335      *                        CONSOLE_TABLE_ALIGN_LEFT
336      *                        CONSOLE_TABLE_ALIGN_CENTER
337      *                        CONSOLE_TABLE_ALIGN_RIGHT.
338      *
339      * @return void
340      */
341     function setAlign($col_id, $align = CONSOLE_TABLE_ALIGN_LEFT)
342     {
343         switch ($align) {
344         case CONSOLE_TABLE_ALIGN_CENTER:
345             $pad = STR_PAD_BOTH;
346             break;
347         case CONSOLE_TABLE_ALIGN_RIGHT:
348             $pad = STR_PAD_LEFT;
349             break;
350         default:
351             $pad = STR_PAD_RIGHT;
352             break;
353         }
354         $this->_col_align[$col_id] = $pad;
355     }
356
357     /**
358      * Specifies which columns are to have totals calculated for them and
359      * added as a new row at the bottom.
360      *
361      * @param array $cols Array of column numbers (starting with 0).
362      *
363      * @return void
364      */
365     function calculateTotalsFor($cols)
366     {
367         $this->_calculateTotals = $cols;
368     }
369
370     /**
371      * Sets the headers for the columns.
372      *
373      * @param array $headers The column headers.
374      *
375      * @return void
376      */
377     function setHeaders($headers)
378     {
379         $this->_headers = array(array_values($headers));
380         $this->_updateRowsCols($headers);
381     }
382
383     /**
384      * Adds a row to the table.
385      *
386      * @param array   $row    The row data to add.
387      * @param boolean $append Whether to append or prepend the row.
388      *
389      * @return void
390      */
391     function addRow($row, $append = true)
392     {
393         if ($append) {
394             $this->_data[] = array_values($row);
395         } else {
396             array_unshift($this->_data, array_values($row));
397         }
398
399         $this->_updateRowsCols($row);
400     }
401
402     /**
403      * Inserts a row after a given row number in the table.
404      *
405      * If $row_id is not given it will prepend the row.
406      *
407      * @param array   $row    The data to insert.
408      * @param integer $row_id Row number to insert before.
409      *
410      * @return void
411      */
412     function insertRow($row, $row_id = 0)
413     {
414         array_splice($this->_data, $row_id, 0, array($row));
415
416         $this->_updateRowsCols($row);
417     }
418
419     /**
420      * Adds a column to the table.
421      *
422      * @param array   $col_data The data of the column.
423      * @param integer $col_id   The column index to populate.
424      * @param integer $row_id   If starting row is not zero, specify it here.
425      *
426      * @return void
427      */
428     function addCol($col_data, $col_id = 0, $row_id = 0)
429     {
430         foreach ($col_data as $col_cell) {
431             $this->_data[$row_id++][$col_id] = $col_cell;
432         }
433
434         $this->_updateRowsCols();
435         $this->_max_cols = max($this->_max_cols, $col_id + 1);
436     }
437
438     /**
439      * Adds data to the table.
440      *
441      * @param array   $data   A two dimensional array with the table data.
442      * @param integer $col_id Starting column number.
443      * @param integer $row_id Starting row number.
444      *
445      * @return void
446      */
447     function addData($data, $col_id = 0, $row_id = 0)
448     {
449         foreach ($data as $row) {
450             if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) {
451                 $this->_data[$row_id] = CONSOLE_TABLE_HORIZONTAL_RULE;
452                 $row_id++;
453                 continue;
454             }
455             $starting_col = $col_id;
456             foreach ($row as $cell) {
457                 $this->_data[$row_id][$starting_col++] = $cell;
458             }
459             $this->_updateRowsCols();
460             $this->_max_cols = max($this->_max_cols, $starting_col);
461             $row_id++;
462         }
463     }
464
465     /**
466      * Adds a horizontal seperator to the table.
467      *
468      * @return void
469      */
470     function addSeparator()
471     {
472         $this->_data[] = CONSOLE_TABLE_HORIZONTAL_RULE;
473     }
474
475     /**
476      * Returns the generated table.
477      *
478      * @return string  The generated table.
479      */
480     function getTable()
481     {
482         $this->_applyFilters();
483         $this->_calculateTotals();
484         $this->_validateTable();
485
486         return $this->_buildTable();
487     }
488
489     /**
490      * Calculates totals for columns.
491      *
492      * @return void
493      */
494     function _calculateTotals()
495     {
496         if (empty($this->_calculateTotals)) {
497             return;
498         }
499
500         $this->addSeparator();
501
502         $totals = array();
503         foreach ($this->_data as $row) {
504             if (is_array($row)) {
505                 foreach ($this->_calculateTotals as $columnID) {
506                     $totals[$columnID] += $row[$columnID];
507                 }
508             }
509         }
510
511         $this->_data[] = $totals;
512         $this->_updateRowsCols();
513     }
514
515     /**
516      * Applies any column filters to the data.
517      *
518      * @return void
519      */
520     function _applyFilters()
521     {
522         if (empty($this->_filters)) {
523             return;
524         }
525
526         foreach ($this->_filters as $filter) {
527             $column   = $filter[0];
528             $callback = $filter[1];
529
530             foreach ($this->_data as $row_id => $row_data) {
531                 if ($row_data !== CONSOLE_TABLE_HORIZONTAL_RULE) {
532                     $this->_data[$row_id][$column] =
533                         call_user_func($callback, $row_data[$column]);
534                 }
535             }
536         }
537     }
538
539     /**
540      * Ensures that column and row counts are correct.
541      *
542      * @return void
543      */
544     function _validateTable()
545     {
546         if (!empty($this->_headers)) {
547             $this->_calculateRowHeight(-1, $this->_headers[0]);
548         }
549
550         for ($i = 0; $i < $this->_max_rows; $i++) {
551             for ($j = 0; $j < $this->_max_cols; $j++) {
552                 if (!isset($this->_data[$i][$j]) &&
553                     (!isset($this->_data[$i]) ||
554                      $this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE)) {
555                     $this->_data[$i][$j] = '';
556                 }
557
558             }
559             $this->_calculateRowHeight($i, $this->_data[$i]);
560
561             if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) {
562                  ksort($this->_data[$i]);
563             }
564
565         }
566
567         $this->_splitMultilineRows();
568
569         // Update cell lengths.
570         for ($i = 0; $i < count($this->_headers); $i++) {
571             $this->_calculateCellLengths($this->_headers[$i]);
572         }
573         for ($i = 0; $i < $this->_max_rows; $i++) {
574             $this->_calculateCellLengths($this->_data[$i]);
575         }
576
577         ksort($this->_data);
578     }
579
580     /**
581      * Splits multiline rows into many smaller one-line rows.
582      *
583      * @return void
584      */
585     function _splitMultilineRows()
586     {
587         ksort($this->_data);
588         $sections          = array(&$this->_headers, &$this->_data);
589         $max_rows          = array(count($this->_headers), $this->_max_rows);
590         $row_height_offset = array(-1, 0);
591
592         for ($s = 0; $s <= 1; $s++) {
593             $inserted = 0;
594             $new_data = $sections[$s];
595
596             for ($i = 0; $i < $max_rows[$s]; $i++) {
597                 // Process only rows that have many lines.
598                 $height = $this->_row_heights[$i + $row_height_offset[$s]];
599                 if ($height > 1) {
600                     // Split column data into one-liners.
601                     $split = array();
602                     for ($j = 0; $j < $this->_max_cols; $j++) {
603                         $split[$j] = preg_split('/\r?\n|\r/',
604                                                 $sections[$s][$i][$j]);
605                     }
606
607                     $new_rows = array();
608                     // Construct new 'virtual' rows - insert empty strings for
609                     // columns that have less lines that the highest one.
610                     for ($i2 = 0; $i2 < $height; $i2++) {
611                         for ($j = 0; $j < $this->_max_cols; $j++) {
612                             $new_rows[$i2][$j] = !isset($split[$j][$i2])
613                                 ? ''
614                                 : $split[$j][$i2];
615                         }
616                     }
617
618                     // Replace current row with smaller rows.  $inserted is
619                     // used to take account of bigger array because of already
620                     // inserted rows.
621                     array_splice($new_data, $i + $inserted, 1, $new_rows);
622                     $inserted += count($new_rows) - 1;
623                 }
624             }
625
626             // Has the data been modified?
627             if ($inserted > 0) {
628                 $sections[$s] = $new_data;
629                 $this->_updateRowsCols();
630             }
631         }
632     }
633
634     /**
635      * Builds the table.
636      *
637      * @return string  The generated table string.
638      */
639     function _buildTable()
640     {
641         if (!count($this->_data)) {
642             return '';
643         }
644
645         $vertical = $this->_border['vertical'];
646         $separator = $this->_getSeparator();
647
648         $return = array();
649         for ($i = 0; $i < count($this->_data); $i++) {
650             if (is_array($this->_data[$i])) {
651                 for ($j = 0; $j < count($this->_data[$i]); $j++) {
652                     if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE &&
653                         $this->_strlen($this->_data[$i][$j]) <
654                         $this->_cell_lengths[$j]) {
655                         $this->_data[$i][$j] = $this->_strpad($this->_data[$i][$j],
656                                                               $this->_cell_lengths[$j],
657                                                               ' ',
658                                                               $this->_col_align[$j]);
659                     }
660                 }
661             }
662
663             if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) {
664                 $row_begin = $this->_borderVisibility['left']
665                     ? $vertical . str_repeat(' ', $this->_padding)
666                     : '';
667                 $row_end = $this->_borderVisibility['right']
668                     ? str_repeat(' ', $this->_padding) . $vertical
669                     : '';
670                 $implode_char = str_repeat(' ', $this->_padding) . $vertical
671                     . str_repeat(' ', $this->_padding);
672                 $return[]     = $row_begin
673                     . implode($implode_char, $this->_data[$i]) . $row_end;
674             } elseif (!empty($separator)) {
675                 $return[] = $separator;
676             }
677
678         }
679
680         $return = implode(PHP_EOL, $return);
681         if (!empty($separator)) {
682             if ($this->_borderVisibility['inner']) {
683                 $return = $separator . PHP_EOL . $return;
684             }
685             if ($this->_borderVisibility['bottom']) {
686                 $return .= PHP_EOL . $separator;
687             }
688         }
689         $return .= PHP_EOL;
690
691         if (!empty($this->_headers)) {
692             $return = $this->_getHeaderLine() .  PHP_EOL . $return;
693         }
694
695         return $return;
696     }
697
698     /**
699      * Creates a horizontal separator for header separation and table
700      * start/end etc.
701      *
702      * @return string  The horizontal separator.
703      */
704     function _getSeparator()
705     {
706         if (!$this->_border) {
707             return;
708         }
709
710         $horizontal = $this->_border['horizontal'];
711         $intersection = $this->_border['intersection'];
712
713         $return = array();
714         foreach ($this->_cell_lengths as $cl) {
715             $return[] = str_repeat($horizontal, $cl);
716         }
717
718         $row_begin = $this->_borderVisibility['left']
719             ? $intersection . str_repeat($horizontal, $this->_padding)
720             : '';
721         $row_end = $this->_borderVisibility['right']
722             ? str_repeat($horizontal, $this->_padding) . $intersection
723             : '';
724         $implode_char = str_repeat($horizontal, $this->_padding) . $intersection
725             . str_repeat($horizontal, $this->_padding);
726
727         return $row_begin . implode($implode_char, $return) . $row_end;
728     }
729
730     /**
731      * Returns the header line for the table.
732      *
733      * @return string  The header line of the table.
734      */
735     function _getHeaderLine()
736     {
737         // Make sure column count is correct
738         for ($j = 0; $j < count($this->_headers); $j++) {
739             for ($i = 0; $i < $this->_max_cols; $i++) {
740                 if (!isset($this->_headers[$j][$i])) {
741                     $this->_headers[$j][$i] = '';
742                 }
743             }
744         }
745
746         for ($j = 0; $j < count($this->_headers); $j++) {
747             for ($i = 0; $i < count($this->_headers[$j]); $i++) {
748                 if ($this->_strlen($this->_headers[$j][$i]) <
749                     $this->_cell_lengths[$i]) {
750                     $this->_headers[$j][$i] =
751                         $this->_strpad($this->_headers[$j][$i],
752                                        $this->_cell_lengths[$i],
753                                        ' ',
754                                        $this->_col_align[$i]);
755                 }
756             }
757         }
758
759         $vertical = $this->_border['vertical'];
760         $row_begin = $this->_borderVisibility['left']
761             ? $vertical . str_repeat(' ', $this->_padding)
762             : '';
763         $row_end = $this->_borderVisibility['right']
764             ? str_repeat(' ', $this->_padding) . $vertical
765             : '';
766         $implode_char = str_repeat(' ', $this->_padding) . $vertical
767             . str_repeat(' ', $this->_padding);
768
769         $separator = $this->_getSeparator();
770         if (!empty($separator) && $this->_borderVisibility['top']) {
771             $return[] = $separator;
772         }
773         for ($j = 0; $j < count($this->_headers); $j++) {
774             $return[] = $row_begin
775                 . implode($implode_char, $this->_headers[$j]) . $row_end;
776         }
777
778         return implode(PHP_EOL, $return);
779     }
780
781     /**
782      * Updates values for maximum columns and rows.
783      *
784      * @param array $rowdata Data array of a single row.
785      *
786      * @return void
787      */
788     function _updateRowsCols($rowdata = null)
789     {
790         // Update maximum columns.
791         $this->_max_cols = max($this->_max_cols, is_array($rowdata) ? count($rowdata) : 0);
792
793         // Update maximum rows.
794         ksort($this->_data);
795         $keys            = array_keys($this->_data);
796         $this->_max_rows = end($keys) + 1;
797
798         switch ($this->_defaultAlign) {
799         case CONSOLE_TABLE_ALIGN_CENTER:
800             $pad = STR_PAD_BOTH;
801             break;
802         case CONSOLE_TABLE_ALIGN_RIGHT:
803             $pad = STR_PAD_LEFT;
804             break;
805         default:
806             $pad = STR_PAD_RIGHT;
807             break;
808         }
809
810         // Set default column alignments
811         for ($i = 0; $i < $this->_max_cols; $i++) {
812             if (!isset($this->_col_align[$i])) {
813                 $this->_col_align[$i] = $pad;
814             }
815         }
816     }
817
818     /**
819      * Calculates the maximum length for each column of a row.
820      *
821      * @param array $row The row data.
822      *
823      * @return void
824      */
825     function _calculateCellLengths($row)
826     {
827         if (is_array($row)) {
828             for ($i = 0; $i < count($row); $i++) {
829                if (!isset($this->_cell_lengths[$i])) {
830                     $this->_cell_lengths[$i] = 0;
831                 }
832                 $this->_cell_lengths[$i] = max($this->_cell_lengths[$i],
833                                                $this->_strlen($row[$i]));
834             }
835         }
836     }
837
838     /**
839      * Calculates the maximum height for all columns of a row.
840      *
841      * @param integer $row_number The row number.
842      * @param array   $row        The row data.
843      *
844      * @return void
845      */
846     function _calculateRowHeight($row_number, $row)
847     {
848         if (!isset($this->_row_heights[$row_number])) {
849             $this->_row_heights[$row_number] = 1;
850         }
851
852         // Do not process horizontal rule rows.
853         if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) {
854             return;
855         }
856
857         for ($i = 0, $c = count($row); $i < $c; ++$i) {
858             $lines                           = preg_split('/\r?\n|\r/', $row[$i]);
859             $this->_row_heights[$row_number] = max($this->_row_heights[$row_number],
860                                                    count($lines));
861         }
862     }
863
864     /**
865      * Returns the character length of a string.
866      *
867      * @param string $str A multibyte or singlebyte string.
868      *
869      * @return integer  The string length.
870      */
871     function _strlen($str)
872     {
873         static $mbstring;
874
875         // Strip ANSI color codes if requested.
876         if ($this->_ansiColor) {
877             $str = $this->_ansiColor->strip($str);
878         }
879
880         // Cache expensive function_exists() calls.
881         if (!isset($mbstring)) {
882             $mbstring = function_exists('mb_strwidth');
883         }
884
885         if ($mbstring) {
886             return mb_strwidth($str, $this->_charset);
887         }
888
889         return strlen($str);
890     }
891
892     /**
893      * Returns part of a string.
894      *
895      * @param string  $string The string to be converted.
896      * @param integer $start  The part's start position, zero based.
897      * @param integer $length The part's length.
898      *
899      * @return string  The string's part.
900      */
901     function _substr($string, $start, $length = null)
902     {
903         static $mbstring;
904
905         // Cache expensive function_exists() calls.
906         if (!isset($mbstring)) {
907             $mbstring = function_exists('mb_substr');
908         }
909
910         if (is_null($length)) {
911             $length = $this->_strlen($string);
912         }
913         if ($mbstring) {
914             $ret = @mb_substr($string, $start, $length, $this->_charset);
915             if (!empty($ret)) {
916                 return $ret;
917             }
918         }
919         return substr($string, $start, $length);
920     }
921
922     /**
923      * Returns a string padded to a certain length with another string.
924      *
925      * This method behaves exactly like str_pad but is multibyte safe.
926      *
927      * @param string  $input  The string to be padded.
928      * @param integer $length The length of the resulting string.
929      * @param string  $pad    The string to pad the input string with. Must
930      *                        be in the same charset like the input string.
931      * @param const   $type   The padding type. One of STR_PAD_LEFT,
932      *                        STR_PAD_RIGHT, or STR_PAD_BOTH.
933      *
934      * @return string  The padded string.
935      */
936     function _strpad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT)
937     {
938         $mb_length  = $this->_strlen($input);
939         $sb_length  = strlen($input);
940         $pad_length = $this->_strlen($pad);
941
942         /* Return if we already have the length. */
943         if ($mb_length >= $length) {
944             return $input;
945         }
946
947         /* Shortcut for single byte strings. */
948         if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
949             return str_pad($input, $length, $pad, $type);
950         }
951
952         switch ($type) {
953         case STR_PAD_LEFT:
954             $left   = $length - $mb_length;
955             $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
956                                      0, $left, $this->_charset) . $input;
957             break;
958         case STR_PAD_BOTH:
959             $left   = floor(($length - $mb_length) / 2);
960             $right  = ceil(($length - $mb_length) / 2);
961             $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
962                                      0, $left, $this->_charset) .
963                 $input .
964                 $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
965                                0, $right, $this->_charset);
966             break;
967         case STR_PAD_RIGHT:
968             $right  = $length - $mb_length;
969             $output = $input .
970                 $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
971                                0, $right, $this->_charset);
972             break;
973         }
974
975         return $output;
976     }
977
978 }