Minor dependency updates
[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             for ($j = 0; $j < count($this->_data[$i]); $j++) {
651                 if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE &&
652                     $this->_strlen($this->_data[$i][$j]) <
653                     $this->_cell_lengths[$j]) {
654                     $this->_data[$i][$j] = $this->_strpad($this->_data[$i][$j],
655                                                           $this->_cell_lengths[$j],
656                                                           ' ',
657                                                           $this->_col_align[$j]);
658                 }
659             }
660
661             if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) {
662                 $row_begin = $this->_borderVisibility['left']
663                     ? $vertical . str_repeat(' ', $this->_padding)
664                     : '';
665                 $row_end = $this->_borderVisibility['right']
666                     ? str_repeat(' ', $this->_padding) . $vertical
667                     : '';
668                 $implode_char = str_repeat(' ', $this->_padding) . $vertical
669                     . str_repeat(' ', $this->_padding);
670                 $return[]     = $row_begin
671                     . implode($implode_char, $this->_data[$i]) . $row_end;
672             } elseif (!empty($separator)) {
673                 $return[] = $separator;
674             }
675
676         }
677
678         $return = implode(PHP_EOL, $return);
679         if (!empty($separator)) {
680             if ($this->_borderVisibility['inner']) {
681                 $return = $separator . PHP_EOL . $return;
682             }
683             if ($this->_borderVisibility['bottom']) {
684                 $return .= PHP_EOL . $separator;
685             }
686         }
687         $return .= PHP_EOL;
688
689         if (!empty($this->_headers)) {
690             $return = $this->_getHeaderLine() .  PHP_EOL . $return;
691         }
692
693         return $return;
694     }
695
696     /**
697      * Creates a horizontal separator for header separation and table
698      * start/end etc.
699      *
700      * @return string  The horizontal separator.
701      */
702     function _getSeparator()
703     {
704         if (!$this->_border) {
705             return;
706         }
707
708         $horizontal = $this->_border['horizontal'];
709         $intersection = $this->_border['intersection'];
710
711         $return = array();
712         foreach ($this->_cell_lengths as $cl) {
713             $return[] = str_repeat($horizontal, $cl);
714         }
715
716         $row_begin = $this->_borderVisibility['left']
717             ? $intersection . str_repeat($horizontal, $this->_padding)
718             : '';
719         $row_end = $this->_borderVisibility['right']
720             ? str_repeat($horizontal, $this->_padding) . $intersection
721             : '';
722         $implode_char = str_repeat($horizontal, $this->_padding) . $intersection
723             . str_repeat($horizontal, $this->_padding);
724
725         return $row_begin . implode($implode_char, $return) . $row_end;
726     }
727
728     /**
729      * Returns the header line for the table.
730      *
731      * @return string  The header line of the table.
732      */
733     function _getHeaderLine()
734     {
735         // Make sure column count is correct
736         for ($j = 0; $j < count($this->_headers); $j++) {
737             for ($i = 0; $i < $this->_max_cols; $i++) {
738                 if (!isset($this->_headers[$j][$i])) {
739                     $this->_headers[$j][$i] = '';
740                 }
741             }
742         }
743
744         for ($j = 0; $j < count($this->_headers); $j++) {
745             for ($i = 0; $i < count($this->_headers[$j]); $i++) {
746                 if ($this->_strlen($this->_headers[$j][$i]) <
747                     $this->_cell_lengths[$i]) {
748                     $this->_headers[$j][$i] =
749                         $this->_strpad($this->_headers[$j][$i],
750                                        $this->_cell_lengths[$i],
751                                        ' ',
752                                        $this->_col_align[$i]);
753                 }
754             }
755         }
756
757         $vertical = $this->_border['vertical'];
758         $row_begin = $this->_borderVisibility['left']
759             ? $vertical . str_repeat(' ', $this->_padding)
760             : '';
761         $row_end = $this->_borderVisibility['right']
762             ? str_repeat(' ', $this->_padding) . $vertical
763             : '';
764         $implode_char = str_repeat(' ', $this->_padding) . $vertical
765             . str_repeat(' ', $this->_padding);
766
767         $separator = $this->_getSeparator();
768         if (!empty($separator) && $this->_borderVisibility['top']) {
769             $return[] = $separator;
770         }
771         for ($j = 0; $j < count($this->_headers); $j++) {
772             $return[] = $row_begin
773                 . implode($implode_char, $this->_headers[$j]) . $row_end;
774         }
775
776         return implode(PHP_EOL, $return);
777     }
778
779     /**
780      * Updates values for maximum columns and rows.
781      *
782      * @param array $rowdata Data array of a single row.
783      *
784      * @return void
785      */
786     function _updateRowsCols($rowdata = null)
787     {
788         // Update maximum columns.
789         $this->_max_cols = max($this->_max_cols, count($rowdata));
790
791         // Update maximum rows.
792         ksort($this->_data);
793         $keys            = array_keys($this->_data);
794         $this->_max_rows = end($keys) + 1;
795
796         switch ($this->_defaultAlign) {
797         case CONSOLE_TABLE_ALIGN_CENTER:
798             $pad = STR_PAD_BOTH;
799             break;
800         case CONSOLE_TABLE_ALIGN_RIGHT:
801             $pad = STR_PAD_LEFT;
802             break;
803         default:
804             $pad = STR_PAD_RIGHT;
805             break;
806         }
807
808         // Set default column alignments
809         for ($i = 0; $i < $this->_max_cols; $i++) {
810             if (!isset($this->_col_align[$i])) {
811                 $this->_col_align[$i] = $pad;
812             }
813         }
814     }
815
816     /**
817      * Calculates the maximum length for each column of a row.
818      *
819      * @param array $row The row data.
820      *
821      * @return void
822      */
823     function _calculateCellLengths($row)
824     {
825         for ($i = 0; $i < count($row); $i++) {
826             if (!isset($this->_cell_lengths[$i])) {
827                 $this->_cell_lengths[$i] = 0;
828             }
829             $this->_cell_lengths[$i] = max($this->_cell_lengths[$i],
830                                            $this->_strlen($row[$i]));
831         }
832     }
833
834     /**
835      * Calculates the maximum height for all columns of a row.
836      *
837      * @param integer $row_number The row number.
838      * @param array   $row        The row data.
839      *
840      * @return void
841      */
842     function _calculateRowHeight($row_number, $row)
843     {
844         if (!isset($this->_row_heights[$row_number])) {
845             $this->_row_heights[$row_number] = 1;
846         }
847
848         // Do not process horizontal rule rows.
849         if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) {
850             return;
851         }
852
853         for ($i = 0, $c = count($row); $i < $c; ++$i) {
854             $lines                           = preg_split('/\r?\n|\r/', $row[$i]);
855             $this->_row_heights[$row_number] = max($this->_row_heights[$row_number],
856                                                    count($lines));
857         }
858     }
859
860     /**
861      * Returns the character length of a string.
862      *
863      * @param string $str A multibyte or singlebyte string.
864      *
865      * @return integer  The string length.
866      */
867     function _strlen($str)
868     {
869         static $mbstring;
870
871         // Strip ANSI color codes if requested.
872         if ($this->_ansiColor) {
873             $str = $this->_ansiColor->strip($str);
874         }
875
876         // Cache expensive function_exists() calls.
877         if (!isset($mbstring)) {
878             $mbstring = function_exists('mb_strwidth');
879         }
880
881         if ($mbstring) {
882             return mb_strwidth($str, $this->_charset);
883         }
884
885         return strlen($str);
886     }
887
888     /**
889      * Returns part of a string.
890      *
891      * @param string  $string The string to be converted.
892      * @param integer $start  The part's start position, zero based.
893      * @param integer $length The part's length.
894      *
895      * @return string  The string's part.
896      */
897     function _substr($string, $start, $length = null)
898     {
899         static $mbstring;
900
901         // Cache expensive function_exists() calls.
902         if (!isset($mbstring)) {
903             $mbstring = function_exists('mb_substr');
904         }
905
906         if (is_null($length)) {
907             $length = $this->_strlen($string);
908         }
909         if ($mbstring) {
910             $ret = @mb_substr($string, $start, $length, $this->_charset);
911             if (!empty($ret)) {
912                 return $ret;
913             }
914         }
915         return substr($string, $start, $length);
916     }
917
918     /**
919      * Returns a string padded to a certain length with another string.
920      *
921      * This method behaves exactly like str_pad but is multibyte safe.
922      *
923      * @param string  $input  The string to be padded.
924      * @param integer $length The length of the resulting string.
925      * @param string  $pad    The string to pad the input string with. Must
926      *                        be in the same charset like the input string.
927      * @param const   $type   The padding type. One of STR_PAD_LEFT,
928      *                        STR_PAD_RIGHT, or STR_PAD_BOTH.
929      *
930      * @return string  The padded string.
931      */
932     function _strpad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT)
933     {
934         $mb_length  = $this->_strlen($input);
935         $sb_length  = strlen($input);
936         $pad_length = $this->_strlen($pad);
937
938         /* Return if we already have the length. */
939         if ($mb_length >= $length) {
940             return $input;
941         }
942
943         /* Shortcut for single byte strings. */
944         if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
945             return str_pad($input, $length, $pad, $type);
946         }
947
948         switch ($type) {
949         case STR_PAD_LEFT:
950             $left   = $length - $mb_length;
951             $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
952                                      0, $left, $this->_charset) . $input;
953             break;
954         case STR_PAD_BOTH:
955             $left   = floor(($length - $mb_length) / 2);
956             $right  = ceil(($length - $mb_length) / 2);
957             $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
958                                      0, $left, $this->_charset) .
959                 $input .
960                 $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
961                                0, $right, $this->_charset);
962             break;
963         case STR_PAD_RIGHT:
964             $right  = $length - $mb_length;
965             $output = $input .
966                 $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
967                                0, $right, $this->_charset);
968             break;
969         }
970
971         return $output;
972     }
973
974 }