Highlighter.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <?php
  2. declare(strict_types=1);
  3. namespace NunoMaduro\Collision;
  4. use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
  5. /**
  6. * @internal
  7. */
  8. final class Highlighter implements HighlighterContract
  9. {
  10. public const TOKEN_DEFAULT = 'token_default';
  11. public const TOKEN_COMMENT = 'token_comment';
  12. public const TOKEN_STRING = 'token_string';
  13. public const TOKEN_HTML = 'token_html';
  14. public const TOKEN_KEYWORD = 'token_keyword';
  15. public const ACTUAL_LINE_MARK = 'actual_line_mark';
  16. public const LINE_NUMBER = 'line_number';
  17. private const ARROW_SYMBOL = '>';
  18. private const DELIMITER = '|';
  19. private const ARROW_SYMBOL_UTF8 = '➜';
  20. private const DELIMITER_UTF8 = '▕'; // '▶';
  21. private const LINE_NUMBER_DIVIDER = 'line_divider';
  22. private const MARKED_LINE_NUMBER = 'marked_line';
  23. private const WIDTH = 3;
  24. /**
  25. * Holds the theme.
  26. *
  27. * @var array
  28. */
  29. private const THEME = [
  30. self::TOKEN_STRING => ['light_gray'],
  31. self::TOKEN_COMMENT => ['dark_gray', 'italic'],
  32. self::TOKEN_KEYWORD => ['magenta', 'bold'],
  33. self::TOKEN_DEFAULT => ['default', 'bold'],
  34. self::TOKEN_HTML => ['blue', 'bold'],
  35. self::ACTUAL_LINE_MARK => ['red', 'bold'],
  36. self::LINE_NUMBER => ['dark_gray'],
  37. self::MARKED_LINE_NUMBER => ['italic', 'bold'],
  38. self::LINE_NUMBER_DIVIDER => ['dark_gray'],
  39. ];
  40. /** @var ConsoleColor */
  41. private $color;
  42. /** @var array */
  43. private const DEFAULT_THEME = [
  44. self::TOKEN_STRING => 'red',
  45. self::TOKEN_COMMENT => 'yellow',
  46. self::TOKEN_KEYWORD => 'green',
  47. self::TOKEN_DEFAULT => 'default',
  48. self::TOKEN_HTML => 'cyan',
  49. self::ACTUAL_LINE_MARK => 'dark_gray',
  50. self::LINE_NUMBER => 'dark_gray',
  51. self::MARKED_LINE_NUMBER => 'dark_gray',
  52. self::LINE_NUMBER_DIVIDER => 'dark_gray',
  53. ];
  54. /** @var string */
  55. private $delimiter = self::DELIMITER_UTF8;
  56. /** @var string */
  57. private $arrow = self::ARROW_SYMBOL_UTF8;
  58. /**
  59. * @var string
  60. */
  61. private const NO_MARK = ' ';
  62. /**
  63. * Creates an instance of the Highlighter.
  64. */
  65. public function __construct(ConsoleColor $color = null, bool $UTF8 = true)
  66. {
  67. $this->color = $color ?: new ConsoleColor();
  68. foreach (self::DEFAULT_THEME as $name => $styles) {
  69. if (!$this->color->hasTheme($name)) {
  70. $this->color->addTheme($name, $styles);
  71. }
  72. }
  73. foreach (self::THEME as $name => $styles) {
  74. $this->color->addTheme($name, $styles);
  75. }
  76. if (!$UTF8) {
  77. $this->delimiter = self::DELIMITER;
  78. $this->arrow = self::ARROW_SYMBOL;
  79. }
  80. $this->delimiter .= ' ';
  81. }
  82. /**
  83. * {@inheritdoc}
  84. */
  85. public function highlight(string $content, int $line): string
  86. {
  87. return rtrim($this->getCodeSnippet($content, $line, 4, 4));
  88. }
  89. /**
  90. * @param string $source
  91. * @param int $lineNumber
  92. * @param int $linesBefore
  93. * @param int $linesAfter
  94. */
  95. public function getCodeSnippet($source, $lineNumber, $linesBefore = 2, $linesAfter = 2): string
  96. {
  97. $tokenLines = $this->getHighlightedLines($source);
  98. $offset = $lineNumber - $linesBefore - 1;
  99. $offset = max($offset, 0);
  100. $length = $linesAfter + $linesBefore + 1;
  101. $tokenLines = array_slice($tokenLines, $offset, $length, $preserveKeys = true);
  102. $lines = $this->colorLines($tokenLines);
  103. return $this->lineNumbers($lines, $lineNumber);
  104. }
  105. /**
  106. * @param string $source
  107. */
  108. private function getHighlightedLines($source): array
  109. {
  110. $source = str_replace(["\r\n", "\r"], "\n", $source);
  111. $tokens = $this->tokenize($source);
  112. return $this->splitToLines($tokens);
  113. }
  114. /**
  115. * @param string $source
  116. */
  117. private function tokenize($source): array
  118. {
  119. $tokens = token_get_all($source);
  120. $output = [];
  121. $currentType = null;
  122. $buffer = '';
  123. foreach ($tokens as $token) {
  124. if (is_array($token)) {
  125. switch ($token[0]) {
  126. case T_WHITESPACE:
  127. break;
  128. case T_OPEN_TAG:
  129. case T_OPEN_TAG_WITH_ECHO:
  130. case T_CLOSE_TAG:
  131. case T_STRING:
  132. case T_VARIABLE:
  133. // Constants
  134. case T_DIR:
  135. case T_FILE:
  136. case T_METHOD_C:
  137. case T_DNUMBER:
  138. case T_LNUMBER:
  139. case T_NS_C:
  140. case T_LINE:
  141. case T_CLASS_C:
  142. case T_FUNC_C:
  143. case T_TRAIT_C:
  144. $newType = self::TOKEN_DEFAULT;
  145. break;
  146. case T_COMMENT:
  147. case T_DOC_COMMENT:
  148. $newType = self::TOKEN_COMMENT;
  149. break;
  150. case T_ENCAPSED_AND_WHITESPACE:
  151. case T_CONSTANT_ENCAPSED_STRING:
  152. $newType = self::TOKEN_STRING;
  153. break;
  154. case T_INLINE_HTML:
  155. $newType = self::TOKEN_HTML;
  156. break;
  157. default:
  158. $newType = self::TOKEN_KEYWORD;
  159. }
  160. } else {
  161. $newType = $token === '"' ? self::TOKEN_STRING : self::TOKEN_KEYWORD;
  162. }
  163. if ($currentType === null) {
  164. $currentType = $newType;
  165. }
  166. if ($currentType !== $newType) {
  167. $output[] = [$currentType, $buffer];
  168. $buffer = '';
  169. $currentType = $newType;
  170. }
  171. $buffer .= is_array($token) ? $token[1] : $token;
  172. }
  173. if (isset($newType)) {
  174. $output[] = [$newType, $buffer];
  175. }
  176. return $output;
  177. }
  178. private function splitToLines(array $tokens): array
  179. {
  180. $lines = [];
  181. $line = [];
  182. foreach ($tokens as $token) {
  183. foreach (explode("\n", $token[1]) as $count => $tokenLine) {
  184. if ($count > 0) {
  185. $lines[] = $line;
  186. $line = [];
  187. }
  188. if ($tokenLine === '') {
  189. continue;
  190. }
  191. $line[] = [$token[0], $tokenLine];
  192. }
  193. }
  194. $lines[] = $line;
  195. return $lines;
  196. }
  197. private function colorLines(array $tokenLines): array
  198. {
  199. $lines = [];
  200. foreach ($tokenLines as $lineCount => $tokenLine) {
  201. $line = '';
  202. foreach ($tokenLine as $token) {
  203. [$tokenType, $tokenValue] = $token;
  204. if ($this->color->hasTheme($tokenType)) {
  205. $line .= $this->color->apply($tokenType, $tokenValue);
  206. } else {
  207. $line .= $tokenValue;
  208. }
  209. }
  210. $lines[$lineCount] = $line;
  211. }
  212. return $lines;
  213. }
  214. /**
  215. * @param int|null $markLine
  216. */
  217. private function lineNumbers(array $lines, $markLine = null): string
  218. {
  219. $lineStrlen = strlen((string) (array_key_last($lines) + 1));
  220. $lineStrlen = $lineStrlen < self::WIDTH ? self::WIDTH : $lineStrlen;
  221. $snippet = '';
  222. $mark = ' ' . $this->arrow . ' ';
  223. foreach ($lines as $i => $line) {
  224. $coloredLineNumber = $this->coloredLineNumber(self::LINE_NUMBER, $i, $lineStrlen);
  225. if (null !== $markLine) {
  226. $snippet .=
  227. ($markLine === $i + 1
  228. ? $this->color->apply(self::ACTUAL_LINE_MARK, $mark)
  229. : self::NO_MARK
  230. );
  231. $coloredLineNumber =
  232. ($markLine === $i + 1 ?
  233. $this->coloredLineNumber(self::MARKED_LINE_NUMBER, $i, $lineStrlen) :
  234. $coloredLineNumber
  235. );
  236. }
  237. $snippet .= $coloredLineNumber;
  238. $snippet .=
  239. $this->color->apply(self::LINE_NUMBER_DIVIDER, $this->delimiter);
  240. $snippet .= $line . PHP_EOL;
  241. }
  242. return $snippet;
  243. }
  244. /**
  245. * @param string $style
  246. * @param int $i
  247. * @param int $lineStrlen
  248. */
  249. private function coloredLineNumber($style, $i, $lineStrlen): string
  250. {
  251. return $this->color->apply($style, str_pad((string) ($i + 1), $lineStrlen, ' ', STR_PAD_LEFT));
  252. }
  253. }