Highlighter.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <?php
  2. namespace JakubOnderka\PhpConsoleHighlighter;
  3. use JakubOnderka\PhpConsoleColor\ConsoleColor;
  4. class Highlighter
  5. {
  6. const TOKEN_DEFAULT = 'token_default',
  7. TOKEN_COMMENT = 'token_comment',
  8. TOKEN_STRING = 'token_string',
  9. TOKEN_HTML = 'token_html',
  10. TOKEN_KEYWORD = 'token_keyword';
  11. const ACTUAL_LINE_MARK = 'actual_line_mark',
  12. LINE_NUMBER = 'line_number';
  13. /** @var ConsoleColor */
  14. private $color;
  15. /** @var array */
  16. private $defaultTheme = array(
  17. self::TOKEN_STRING => 'red',
  18. self::TOKEN_COMMENT => 'yellow',
  19. self::TOKEN_KEYWORD => 'green',
  20. self::TOKEN_DEFAULT => 'default',
  21. self::TOKEN_HTML => 'cyan',
  22. self::ACTUAL_LINE_MARK => 'red',
  23. self::LINE_NUMBER => 'dark_gray',
  24. );
  25. /**
  26. * @param ConsoleColor $color
  27. * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
  28. */
  29. public function __construct(ConsoleColor $color)
  30. {
  31. $this->color = $color;
  32. foreach ($this->defaultTheme as $name => $styles) {
  33. if (!$this->color->hasTheme($name)) {
  34. $this->color->addTheme($name, $styles);
  35. }
  36. }
  37. }
  38. /**
  39. * @param string $source
  40. * @param int $lineNumber
  41. * @param int $linesBefore
  42. * @param int $linesAfter
  43. * @return string
  44. * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
  45. * @throws \InvalidArgumentException
  46. */
  47. public function getCodeSnippet($source, $lineNumber, $linesBefore = 2, $linesAfter = 2)
  48. {
  49. $tokenLines = $this->getHighlightedLines($source);
  50. $offset = $lineNumber - $linesBefore - 1;
  51. $offset = max($offset, 0);
  52. $length = $linesAfter + $linesBefore + 1;
  53. $tokenLines = array_slice($tokenLines, $offset, $length, $preserveKeys = true);
  54. $lines = $this->colorLines($tokenLines);
  55. return $this->lineNumbers($lines, $lineNumber);
  56. }
  57. /**
  58. * @param string $source
  59. * @return string
  60. * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
  61. * @throws \InvalidArgumentException
  62. */
  63. public function getWholeFile($source)
  64. {
  65. $tokenLines = $this->getHighlightedLines($source);
  66. $lines = $this->colorLines($tokenLines);
  67. return implode(PHP_EOL, $lines);
  68. }
  69. /**
  70. * @param string $source
  71. * @return string
  72. * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
  73. * @throws \InvalidArgumentException
  74. */
  75. public function getWholeFileWithLineNumbers($source)
  76. {
  77. $tokenLines = $this->getHighlightedLines($source);
  78. $lines = $this->colorLines($tokenLines);
  79. return $this->lineNumbers($lines);
  80. }
  81. /**
  82. * @param string $source
  83. * @return array
  84. */
  85. private function getHighlightedLines($source)
  86. {
  87. $source = str_replace(array("\r\n", "\r"), "\n", $source);
  88. $tokens = $this->tokenize($source);
  89. return $this->splitToLines($tokens);
  90. }
  91. /**
  92. * @param string $source
  93. * @return array
  94. */
  95. private function tokenize($source)
  96. {
  97. $tokens = token_get_all($source);
  98. $output = array();
  99. $currentType = null;
  100. $buffer = '';
  101. foreach ($tokens as $token) {
  102. if (is_array($token)) {
  103. switch ($token[0]) {
  104. case T_WHITESPACE:
  105. break;
  106. case T_OPEN_TAG:
  107. case T_OPEN_TAG_WITH_ECHO:
  108. case T_CLOSE_TAG:
  109. case T_STRING:
  110. case T_VARIABLE:
  111. // Constants
  112. case T_DIR:
  113. case T_FILE:
  114. case T_METHOD_C:
  115. case T_DNUMBER:
  116. case T_LNUMBER:
  117. case T_NS_C:
  118. case T_LINE:
  119. case T_CLASS_C:
  120. case T_FUNC_C:
  121. case T_TRAIT_C:
  122. $newType = self::TOKEN_DEFAULT;
  123. break;
  124. case T_COMMENT:
  125. case T_DOC_COMMENT:
  126. $newType = self::TOKEN_COMMENT;
  127. break;
  128. case T_ENCAPSED_AND_WHITESPACE:
  129. case T_CONSTANT_ENCAPSED_STRING:
  130. $newType = self::TOKEN_STRING;
  131. break;
  132. case T_INLINE_HTML:
  133. $newType = self::TOKEN_HTML;
  134. break;
  135. default:
  136. $newType = self::TOKEN_KEYWORD;
  137. }
  138. } else {
  139. $newType = $token === '"' ? self::TOKEN_STRING : self::TOKEN_KEYWORD;
  140. }
  141. if ($currentType === null) {
  142. $currentType = $newType;
  143. }
  144. if ($currentType !== $newType) {
  145. $output[] = array($currentType, $buffer);
  146. $buffer = '';
  147. $currentType = $newType;
  148. }
  149. $buffer .= is_array($token) ? $token[1] : $token;
  150. }
  151. if (isset($newType)) {
  152. $output[] = array($newType, $buffer);
  153. }
  154. return $output;
  155. }
  156. /**
  157. * @param array $tokens
  158. * @return array
  159. */
  160. private function splitToLines(array $tokens)
  161. {
  162. $lines = array();
  163. $line = array();
  164. foreach ($tokens as $token) {
  165. foreach (explode("\n", $token[1]) as $count => $tokenLine) {
  166. if ($count > 0) {
  167. $lines[] = $line;
  168. $line = array();
  169. }
  170. if ($tokenLine === '') {
  171. continue;
  172. }
  173. $line[] = array($token[0], $tokenLine);
  174. }
  175. }
  176. $lines[] = $line;
  177. return $lines;
  178. }
  179. /**
  180. * @param array $tokenLines
  181. * @return array
  182. * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
  183. * @throws \InvalidArgumentException
  184. */
  185. private function colorLines(array $tokenLines)
  186. {
  187. $lines = array();
  188. foreach ($tokenLines as $lineCount => $tokenLine) {
  189. $line = '';
  190. foreach ($tokenLine as $token) {
  191. list($tokenType, $tokenValue) = $token;
  192. if ($this->color->hasTheme($tokenType)) {
  193. $line .= $this->color->apply($tokenType, $tokenValue);
  194. } else {
  195. $line .= $tokenValue;
  196. }
  197. }
  198. $lines[$lineCount] = $line;
  199. }
  200. return $lines;
  201. }
  202. /**
  203. * @param array $lines
  204. * @param null|int $markLine
  205. * @return string
  206. * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
  207. */
  208. private function lineNumbers(array $lines, $markLine = null)
  209. {
  210. end($lines);
  211. $lineStrlen = strlen(key($lines) + 1);
  212. $snippet = '';
  213. foreach ($lines as $i => $line) {
  214. if ($markLine !== null) {
  215. $snippet .= ($markLine === $i + 1 ? $this->color->apply(self::ACTUAL_LINE_MARK, ' > ') : ' ');
  216. }
  217. $snippet .= $this->color->apply(self::LINE_NUMBER, str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| ');
  218. $snippet .= $line . PHP_EOL;
  219. }
  220. return $snippet;
  221. }
  222. }