Text.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <?php
  2. /**
  3. * Hoa
  4. *
  5. *
  6. * @license
  7. *
  8. * New BSD License
  9. *
  10. * Copyright © 2007-2017, Hoa community. All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions are met:
  14. * * Redistributions of source code must retain the above copyright
  15. * notice, this list of conditions and the following disclaimer.
  16. * * Redistributions in binary form must reproduce the above copyright
  17. * notice, this list of conditions and the following disclaimer in the
  18. * documentation and/or other materials provided with the distribution.
  19. * * Neither the name of the Hoa nor the names of its contributors may be
  20. * used to endorse or promote products derived from this software without
  21. * specific prior written permission.
  22. *
  23. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  24. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  25. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  26. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
  27. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  28. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  29. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  30. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  31. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  32. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33. * POSSIBILITY OF SUCH DAMAGE.
  34. */
  35. namespace Hoa\Console\Chrome;
  36. use Hoa\Console;
  37. /**
  38. * Class \Hoa\Console\Chrome\Text.
  39. *
  40. * This class builts the text layout.
  41. *
  42. * @copyright Copyright © 2007-2017 Hoa community
  43. * @license New BSD License
  44. */
  45. class Text
  46. {
  47. /**
  48. * Align the text to left.
  49. *
  50. * @const int
  51. */
  52. const ALIGN_LEFT = 0;
  53. /**
  54. * Align the text to right.
  55. *
  56. * @const int
  57. */
  58. const ALIGN_RIGHT = 1;
  59. /**
  60. * Align the text to center.
  61. *
  62. * @const int
  63. */
  64. const ALIGN_CENTER = 2;
  65. /**
  66. * Colorize a portion of a text.
  67. * It is kind of a shortcut of \Hoa\Console\Color.
  68. *
  69. * @param string $text Text.
  70. * @param string $attributesBefore Style to apply.
  71. * @param string $attributesAfter Reset style.
  72. * @return string
  73. */
  74. public static function colorize(
  75. $text,
  76. $attributesBefore,
  77. $attributesAfter = 'normal'
  78. ) {
  79. ob_start();
  80. Console\Cursor::colorize($attributesBefore);
  81. Console::getOutput()->writeAll($text);
  82. Console\Cursor::colorize($attributesAfter);
  83. $out = ob_get_contents();
  84. ob_end_clean();
  85. return $out;
  86. }
  87. /**
  88. * Built column from an array.
  89. * The array has this structure :
  90. * [
  91. * ['Firstname', 'Lastname', 'Love', 'Made' ],
  92. * ['Ivan', 'Enderlin', 'Hoa' ],
  93. * ['Rasmus', 'Lerdorf' ],
  94. * [null, 'Berners-Lee', null, 'The Web']
  95. * ]
  96. * The cell can have a new-line character (\n).
  97. * The column can have a global alignement, a horizontal and a vertical
  98. * padding (this horizontal padding is actually the right padding), and a
  99. * separator.
  100. * Separator has this form : 'first-column|second-column|third-column|…'.
  101. * For example : '|: ', will set a ': ' between the first and second column,
  102. * and nothing for the other.
  103. *
  104. * @param Array $line The table represented by an array
  105. * (see the documentation).
  106. * @param int $alignement The global alignement of the text
  107. * in cell.
  108. * @param int $horizontalPadding The horizontal padding (right
  109. * padding).
  110. * @param int $verticalPadding The vertical padding.
  111. * @param string $separator String where each character is a
  112. * column separator.
  113. * @return string
  114. */
  115. public static function columnize(
  116. array $line,
  117. $alignement = self::ALIGN_LEFT,
  118. $horizontalPadding = 2,
  119. $verticalPadding = 0,
  120. $separator = null
  121. ) {
  122. if (empty($line)) {
  123. return '';
  124. }
  125. $separator = explode('|', $separator);
  126. $nbColumn = 0;
  127. $nbLine = count($line);
  128. $xtraWidth = 2 * ($verticalPadding + 2); // + separator
  129. // Get the number of column.
  130. foreach ($line as $key => &$column) {
  131. if (!is_array($column)) {
  132. $column = [$column];
  133. }
  134. $handle = count($column);
  135. $handle > $nbColumn and $nbColumn = $handle;
  136. }
  137. $xtraWidth += $horizontalPadding * $nbColumn;
  138. // Get the column width.
  139. $columnWidth = array_fill(0, $nbColumn, 0);
  140. for ($e = 0; $e < $nbColumn; $e++) {
  141. for ($i = 0; $i < $nbLine; $i++) {
  142. if (!isset($line[$i][$e])) {
  143. continue;
  144. }
  145. $handle = self::getMaxLineWidth($line[$i][$e]);
  146. $handle > $columnWidth[$e] and $columnWidth[$e] = $handle;
  147. }
  148. }
  149. // If the sum of each column is greater than the window width, we reduce
  150. // all greaters columns.
  151. $window = Console\Window::getSize();
  152. $envWindow = $window['x'];
  153. while ($envWindow <= ($cWidthSum = $xtraWidth + array_sum($columnWidth))) {
  154. $diff = $cWidthSum - $envWindow;
  155. $max = max($columnWidth) - $xtraWidth;
  156. $newWidth = $max - $diff;
  157. $i = array_search(max($columnWidth), $columnWidth);
  158. $columnWidth[$i] = $newWidth;
  159. foreach ($line as $key => &$c) {
  160. if (isset($c[$i])) {
  161. $c[$i] = self::wordwrap($c[$i], $newWidth);
  162. }
  163. }
  164. }
  165. // Manage the horizontal right padding.
  166. $columnWidth = array_map(
  167. function ($x) use ($horizontalPadding) {
  168. return $x + 2 * $horizontalPadding;
  169. },
  170. $columnWidth
  171. );
  172. // Prepare the new table, i.e. a new line (\n) must be a new line in the
  173. // array (structurally meaning).
  174. $newLine = [];
  175. foreach ($line as $key => $plpl) {
  176. $i = self::getMaxLineNumber($plpl);
  177. while ($i-- >= 0) {
  178. $newLine[] = array_fill(0, $nbColumn, null);
  179. }
  180. }
  181. $yek = 0;
  182. foreach ($line as $key => $col) {
  183. foreach ($col as $kkey => $value) {
  184. if (false === strpos($value, "\n")) {
  185. $newLine[$yek][$kkey] = $value;
  186. continue;
  187. }
  188. foreach (explode("\n", $value) as $foo => $oof) {
  189. $newLine[$yek + $foo][$kkey] = $oof;
  190. }
  191. }
  192. $i = self::getMaxLineNumber($col);
  193. $i > 0 and $yek += $i;
  194. $yek++;
  195. }
  196. // Place the column separator.
  197. foreach ($newLine as $key => $col) {
  198. foreach ($col as $kkey => $value) {
  199. if (isset($separator[$kkey])) {
  200. $newLine[$key][$kkey] =
  201. $separator[$kkey] .
  202. str_replace(
  203. "\n",
  204. "\n" . $separator[$kkey],
  205. $value
  206. );
  207. }
  208. }
  209. }
  210. $line = $newLine;
  211. unset($newLine);
  212. // Complete the table with empty cells.
  213. foreach ($line as $key => &$column) {
  214. $handle = count($column);
  215. if ($nbColumn - $handle > 0) {
  216. $column += array_fill($handle, $nbColumn - $handle, null);
  217. }
  218. }
  219. // Built!
  220. $out = null;
  221. $dash = $alignement === self::ALIGN_LEFT ? '-' : '';
  222. foreach ($line as $key => $handle) {
  223. $format = null;
  224. foreach ($handle as $i => $hand) {
  225. if (preg_match_all('#(\\e\[[0-9]+m)#', $hand, $match)) {
  226. $a = $columnWidth[$i];
  227. foreach ($match as $m) {
  228. $a += strlen($m[1]);
  229. }
  230. $format .= '%' . $dash . ($a + floor(count($match) / 2)) . 's';
  231. } else {
  232. $format .= '%' . $dash . $columnWidth[$i] . 's';
  233. }
  234. }
  235. $format .= str_repeat("\n", $verticalPadding + 1);
  236. array_unshift($handle, $format);
  237. $out .= call_user_func_array('sprintf', $handle);
  238. }
  239. return $out;
  240. }
  241. /**
  242. * Align a text according a “layer”. The layer width is given in arguments.
  243. *
  244. * @param string $text The text.
  245. * @param int $alignement The text alignement.
  246. * @param int $width The layer width.
  247. * @return string
  248. */
  249. public static function align(
  250. $text,
  251. $alignement = self::ALIGN_LEFT,
  252. $width = null
  253. ) {
  254. if (null === $width) {
  255. $window = Console\Window::getSize();
  256. $width = $window['x'];
  257. }
  258. $out = null;
  259. switch ($alignement) {
  260. case self::ALIGN_LEFT:
  261. $out .= sprintf('%-' . $width . 's', self::wordwrap($text, $width));
  262. break;
  263. case self::ALIGN_CENTER:
  264. foreach (explode("\n", self::wordwrap($text, $width)) as $key => $value) {
  265. $out .= str_repeat(' ', ceil(($width - strlen($value)) / 2)) .
  266. $value . "\n";
  267. }
  268. break;
  269. case self::ALIGN_RIGHT:
  270. default:
  271. foreach (explode("\n", self::wordwrap($text, $width)) as $key => $value) {
  272. $out .= sprintf('%' . $width . 's' . "\n", $value);
  273. }
  274. break;
  275. }
  276. return $out;
  277. }
  278. /**
  279. * Get the maximum line width.
  280. *
  281. * @param mixed $lines The line (or group of lines).
  282. * @return int
  283. */
  284. protected static function getMaxLineWidth($lines)
  285. {
  286. if (!is_array($lines)) {
  287. $lines = [$lines];
  288. }
  289. $width = 0;
  290. foreach ($lines as $foo => $line) {
  291. foreach (explode("\n", $line) as $fooo => $lin) {
  292. $lin = preg_replace('#\\e\[[0-9]+m#', '', $lin);
  293. strlen($lin) > $width and $width = strlen($lin);
  294. }
  295. }
  296. return $width;
  297. }
  298. /**
  299. * Get the maximum line number (count the new-line character).
  300. *
  301. * @param mixed $lines The line (or group of lines).
  302. * @return int
  303. */
  304. protected static function getMaxLineNumber($lines)
  305. {
  306. if (!is_array($lines)) {
  307. $lines = [$lines];
  308. }
  309. $number = 0;
  310. foreach ($lines as $foo => $line) {
  311. substr_count($line, "\n") > $number and
  312. $number = substr_count($line, "\n");
  313. }
  314. return $number;
  315. }
  316. /**
  317. * My own wordwrap (just force the wordwrap() $cut parameter)..
  318. *
  319. * @param string $text Text to wrap.
  320. * @param int $width Line width.
  321. * @param string $break String to make the break.
  322. * @return string
  323. */
  324. public static function wordwrap($text, $width = null, $break = "\n")
  325. {
  326. if (null === $width) {
  327. $window = Console\Window::getSize();
  328. $width = $window['x'];
  329. }
  330. return wordwrap($text, $width, $break, true);
  331. }
  332. /**
  333. * Underline with a special string.
  334. *
  335. * @param string $text The text to underline.
  336. * @param string $pattern The string used to underline.
  337. * @return string
  338. */
  339. public static function underline($text, $pattern = '*')
  340. {
  341. $text = explode("\n", $text);
  342. $card = strlen($pattern);
  343. foreach ($text as $key => &$value) {
  344. $i = -1;
  345. $max = strlen($value);
  346. while ($value{++$i} == ' ' && $i < $max);
  347. $underline =
  348. str_repeat(' ', $i) .
  349. str_repeat($pattern, strlen(trim($value)) / $card) .
  350. str_repeat(' ', strlen($value) - $i - strlen(trim($value)));
  351. $value .= "\n" . $underline;
  352. }
  353. return implode("\n", $text);
  354. }
  355. }