18519d077eaa5006f0331c71b396bbd13b055d16
[yaffs-website] / vendor / consolidation / output-formatters / src / Transformations / WordWrapper.php
1 <?php
2 namespace Consolidation\OutputFormatters\Transformations;
3
4 use Symfony\Component\Console\Helper\TableStyle;
5
6 class WordWrapper
7 {
8     protected $width;
9     protected $minimumWidths = [];
10
11     // For now, hardcode these to match what the Symfony Table helper does.
12     // Note that these might actually need to be adjusted depending on the
13     // table style.
14     protected $extraPaddingAtBeginningOfLine = 0;
15     protected $extraPaddingAtEndOfLine = 0;
16     protected $paddingInEachCell = 3;
17
18     public function __construct($width)
19     {
20         $this->width = $width;
21     }
22
23     /**
24      * Calculate our padding widths from the specified table style.
25      * @param TableStyle $style
26      */
27     public function setPaddingFromStyle(TableStyle $style)
28     {
29         $verticalBorderLen = strlen(sprintf($style->getBorderFormat(), $style->getVerticalBorderChar()));
30         $paddingLen = strlen($style->getPaddingChar());
31
32         $this->extraPaddingAtBeginningOfLine = 0;
33         $this->extraPaddingAtEndOfLine = $verticalBorderLen;
34         $this->paddingInEachCell = $verticalBorderLen + $paddingLen + 1;
35     }
36
37     /**
38      * If columns have minimum widths, then set them here.
39      * @param array $minimumWidths
40      */
41     public function setMinimumWidths($minimumWidths)
42     {
43         $this->minimumWidths = $minimumWidths;
44     }
45
46     /**
47      * Wrap the cells in each part of the provided data table
48      * @param array $rows
49      * @return array
50      */
51     public function wrap($rows, $widths = [])
52     {
53         // If the width was not set, then disable wordwrap.
54         if (!$this->width) {
55             return $rows;
56         }
57
58         // Calculate the column widths to use based on the content.
59         $auto_widths = $this->columnAutowidth($rows, $widths);
60
61         // Do wordwrap on all cells.
62         $newrows = array();
63         foreach ($rows as $rowkey => $row) {
64             foreach ($row as $colkey => $cell) {
65                 $newrows[$rowkey][$colkey] = $this->wrapCell($cell, $auto_widths[$colkey]);
66             }
67         }
68
69         return $newrows;
70     }
71
72     /**
73      * Wrap one cell.  Guard against modifying non-strings and
74      * then call through to wordwrap().
75      *
76      * @param mixed $cell
77      * @param string $cellWidth
78      * @return mixed
79      */
80     protected function wrapCell($cell, $cellWidth)
81     {
82         if (!is_string($cell)) {
83             return $cell;
84         }
85         return wordwrap($cell, $cellWidth, "\n", true);
86     }
87
88     /**
89      * Determine the best fit for column widths. Ported from Drush.
90      *
91      * @param array $rows The rows to use for calculations.
92      * @param array $widths Manually specified widths of each column
93      *   (in characters) - these will be left as is.
94      */
95     protected function columnAutowidth($rows, $widths)
96     {
97         $auto_widths = $widths;
98
99         // First we determine the distribution of row lengths in each column.
100         // This is an array of descending character length keys (i.e. starting at
101         // the rightmost character column), with the value indicating the number
102         // of rows where that character column is present.
103         $col_dist = [];
104         // We will also calculate the longest word in each column
105         $max_word_lens = [];
106         foreach ($rows as $rowkey => $row) {
107             foreach ($row as $col_id => $cell) {
108                 $longest_word_len = static::longestWordLength($cell);
109                 if ((!isset($max_word_lens[$col_id]) || ($max_word_lens[$col_id] < $longest_word_len))) {
110                     $max_word_lens[$col_id] = $longest_word_len;
111                 }
112                 if (empty($widths[$col_id])) {
113                     $length = strlen($cell);
114                     if ($length == 0) {
115                         $col_dist[$col_id][0] = 0;
116                     }
117                     while ($length > 0) {
118                         if (!isset($col_dist[$col_id][$length])) {
119                             $col_dist[$col_id][$length] = 0;
120                         }
121                         $col_dist[$col_id][$length]++;
122                         $length--;
123                     }
124                 }
125             }
126         }
127
128         foreach ($col_dist as $col_id => $count) {
129             // Sort the distribution in decending key order.
130             krsort($col_dist[$col_id]);
131             // Initially we set all columns to their "ideal" longest width
132             // - i.e. the width of their longest column.
133             $auto_widths[$col_id] = max(array_keys($col_dist[$col_id]));
134         }
135
136         // We determine what width we have available to use, and what width the
137         // above "ideal" columns take up.
138         $available_width = $this->width - ($this->extraPaddingAtBeginningOfLine + $this->extraPaddingAtEndOfLine + (count($auto_widths) * $this->paddingInEachCell));
139         $auto_width_current = array_sum($auto_widths);
140
141         // If we cannot fit into the minimum width anyway, then just return
142         // the max word length of each column as the 'ideal'
143         $minimumIdealLength = array_sum($this->minimumWidths);
144         if ($minimumIdealLength && ($available_width < $minimumIdealLength)) {
145             return $max_word_lens;
146         }
147
148         // If we need to reduce a column so that we can fit the space we use this
149         // loop to figure out which column will cause the "least wrapping",
150         // (relative to the other columns) and reduce the width of that column.
151         while ($auto_width_current > $available_width) {
152             list($column, $width) = $this->selectColumnToReduce($col_dist, $auto_widths, $max_word_lens);
153
154             if (!$column || $width <= 1) {
155                 // If we have reached a width of 1 then give up, so wordwrap can still progress.
156                 break;
157             }
158             // Reduce the width of the selected column.
159             $auto_widths[$column]--;
160             // Reduce our overall table width counter.
161             $auto_width_current--;
162             // Remove the corresponding data from the disctribution, so next time
163             // around we use the data for the row to the left.
164             unset($col_dist[$column][$width]);
165         }
166         return $auto_widths;
167     }
168
169     protected function selectColumnToReduce($col_dist, $auto_widths, $max_word_lens)
170     {
171         $column = false;
172         $count = 0;
173         $width = 0;
174         foreach ($col_dist as $col_id => $counts) {
175             // Of the columns whose length is still > than the the lenght
176             // of their maximum word length
177             if ($auto_widths[$col_id] > $max_word_lens[$col_id]) {
178                 if ($this->shouldSelectThisColumn($count, $counts, $width)) {
179                     $column = $col_id;
180                     $count = current($counts);
181                     $width = key($counts);
182                 }
183             }
184         }
185         if ($column !== false) {
186             return [$column, $width];
187         }
188         foreach ($col_dist as $col_id => $counts) {
189             if (empty($this->minimumWidths) || ($auto_widths[$col_id] > $this->minimumWidths[$col_id])) {
190                 if ($this->shouldSelectThisColumn($count, $counts, $width)) {
191                     $column = $col_id;
192                     $count = current($counts);
193                     $width = key($counts);
194                 }
195             }
196         }
197         return [$column, $width];
198     }
199
200     protected function shouldSelectThisColumn($count, $counts, $width)
201     {
202         return
203             // If we are just starting out, select the first column.
204             ($count == 0) ||
205             // OR: if this column would cause less wrapping than the currently
206             // selected column, then select it.
207             (current($counts) < $count) ||
208             // OR: if this column would cause the same amount of wrapping, but is
209             // longer, then we choose to wrap the longer column (proportionally
210             // less wrapping, and helps avoid triple line wraps).
211             (current($counts) == $count && key($counts) > $width);
212     }
213
214     /**
215      * Return the length of the longest word in the string.
216      * @param string $str
217      * @return int
218      */
219     protected static function longestWordLength($str)
220     {
221         $words = preg_split('/[ -]/', $str);
222         $lengths = array_map(function ($s) {
223             return strlen($s);
224         }, $words);
225         return max($lengths);
226     }
227 }