2 namespace Consolidation\OutputFormatters\Transformations;
4 use Symfony\Component\Console\Helper\TableStyle;
9 protected $minimumWidths = [];
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
14 protected $extraPaddingAtBeginningOfLine = 0;
15 protected $extraPaddingAtEndOfLine = 0;
16 protected $paddingInEachCell = 3;
18 public function __construct($width)
20 $this->width = $width;
24 * Calculate our padding widths from the specified table style.
25 * @param TableStyle $style
27 public function setPaddingFromStyle(TableStyle $style)
29 $verticalBorderLen = strlen(sprintf($style->getBorderFormat(), $style->getVerticalBorderChar()));
30 $paddingLen = strlen($style->getPaddingChar());
32 $this->extraPaddingAtBeginningOfLine = 0;
33 $this->extraPaddingAtEndOfLine = $verticalBorderLen;
34 $this->paddingInEachCell = $verticalBorderLen + $paddingLen + 1;
38 * If columns have minimum widths, then set them here.
39 * @param array $minimumWidths
41 public function setMinimumWidths($minimumWidths)
43 $this->minimumWidths = $minimumWidths;
47 * Wrap the cells in each part of the provided data table
51 public function wrap($rows, $widths = [])
53 // If the width was not set, then disable wordwrap.
58 // Calculate the column widths to use based on the content.
59 $auto_widths = $this->columnAutowidth($rows, $widths);
61 // Do wordwrap on all cells.
63 foreach ($rows as $rowkey => $row) {
64 foreach ($row as $colkey => $cell) {
65 $newrows[$rowkey][$colkey] = $this->wrapCell($cell, $auto_widths[$colkey]);
73 * Wrap one cell. Guard against modifying non-strings and
74 * then call through to wordwrap().
77 * @param string $cellWidth
80 protected function wrapCell($cell, $cellWidth)
82 if (!is_string($cell)) {
85 return wordwrap($cell, $cellWidth, "\n", true);
89 * Determine the best fit for column widths. Ported from Drush.
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.
95 protected function columnAutowidth($rows, $widths)
97 $auto_widths = $widths;
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.
104 // We will also calculate the longest word in each column
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;
112 if (empty($widths[$col_id])) {
113 $length = strlen($cell);
115 $col_dist[$col_id][0] = 0;
117 while ($length > 0) {
118 if (!isset($col_dist[$col_id][$length])) {
119 $col_dist[$col_id][$length] = 0;
121 $col_dist[$col_id][$length]++;
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]));
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);
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;
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);
154 if (!$column || $width <= 1) {
155 // If we have reached a width of 1 then give up, so wordwrap can still progress.
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]);
169 protected function selectColumnToReduce($col_dist, $auto_widths, $max_word_lens)
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)) {
180 $count = current($counts);
181 $width = key($counts);
185 if ($column !== false) {
186 return [$column, $width];
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)) {
192 $count = current($counts);
193 $width = key($counts);
197 return [$column, $width];
200 protected function shouldSelectThisColumn($count, $counts, $width)
203 // If we are just starting out, select the first column.
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);
215 * Return the length of the longest word in the string.
219 protected static function longestWordLength($str)
221 $words = preg_split('/[ -]/', $str);
222 $lengths = array_map(function ($s) {
225 return max($lengths);