1: <?php declare(strict_types = 1);
2:
3: namespace ApiGen\Renderer;
4:
5: use Nette\Utils\FileSystem;
6: use PhpToken;
7: use Symfony\Component\Console\Style\OutputStyle;
8:
9: use function count;
10: use function explode;
11: use function htmlspecialchars;
12: use function sprintf;
13: use function strlen;
14: use function strtolower;
15: use function strval;
16: use function substr_count;
17:
18: use const TOKEN_PARSE;
19: use const T_ABSTRACT;
20: use const T_ARRAY;
21: use const T_AS;
22: use const T_BREAK;
23: use const T_CASE;
24: use const T_CATCH;
25: use const T_CLASS;
26: use const T_CLONE;
27: use const T_CLOSE_TAG;
28: use const T_COMMENT;
29: use const T_CONST;
30: use const T_CONSTANT_ENCAPSED_STRING;
31: use const T_CONTINUE;
32: use const T_DECLARE;
33: use const T_DEFAULT;
34: use const T_DNUMBER;
35: use const T_DO;
36: use const T_DOC_COMMENT;
37: use const T_ECHO;
38: use const T_ELSE;
39: use const T_ELSEIF;
40: use const T_EMPTY;
41: use const T_ENCAPSED_AND_WHITESPACE;
42: use const T_ENDDECLARE;
43: use const T_ENDFOR;
44: use const T_ENDFOREACH;
45: use const T_ENDIF;
46: use const T_ENDSWITCH;
47: use const T_ENDWHILE;
48: use const T_ENUM;
49: use const T_EVAL;
50: use const T_EXIT;
51: use const T_EXTENDS;
52: use const T_FINAL;
53: use const T_FINALLY;
54: use const T_FN;
55: use const T_FOR;
56: use const T_FOREACH;
57: use const T_FUNCTION;
58: use const T_GLOBAL;
59: use const T_GOTO;
60: use const T_HALT_COMPILER;
61: use const T_IF;
62: use const T_IMPLEMENTS;
63: use const T_INCLUDE;
64: use const T_INCLUDE_ONCE;
65: use const T_INLINE_HTML;
66: use const T_INSTANCEOF;
67: use const T_INSTEADOF;
68: use const T_INTERFACE;
69: use const T_ISSET;
70: use const T_LIST;
71: use const T_LNUMBER;
72: use const T_LOGICAL_AND;
73: use const T_LOGICAL_OR;
74: use const T_LOGICAL_XOR;
75: use const T_MATCH;
76: use const T_NAMESPACE;
77: use const T_NEW;
78: use const T_OPEN_TAG;
79: use const T_OPEN_TAG_WITH_ECHO;
80: use const T_PRINT;
81: use const T_PRIVATE;
82: use const T_PROTECTED;
83: use const T_PUBLIC;
84: use const T_READONLY;
85: use const T_REQUIRE;
86: use const T_REQUIRE_ONCE;
87: use const T_RETURN;
88: use const T_STATIC;
89: use const T_STRING;
90: use const T_SWITCH;
91: use const T_THROW;
92: use const T_TRAIT;
93: use const T_TRY;
94: use const T_UNSET;
95: use const T_USE;
96: use const T_VAR;
97: use const T_VARIABLE;
98: use const T_WHILE;
99: use const T_WHITESPACE;
100: use const T_YIELD;
101: use const T_YIELD_FROM;
102:
103:
104: class SourceHighlighter
105: {
106: public const PHP_TAG = 'php-tag';
107: public const PHP_KEYWORD = 'php-kw';
108: public const PHP_NUMBER = 'php-num';
109: public const PHP_STRING = 'php-str';
110: public const PHP_VARIABLE = 'php-var';
111: public const PHP_COMMENT = 'php-comment';
112:
113: /** @var string[] indexed by [tokenId] */
114: public array $tokenClass = [
115: T_OPEN_TAG => self::PHP_TAG,
116: T_OPEN_TAG_WITH_ECHO => self::PHP_TAG,
117: T_CLOSE_TAG => self::PHP_TAG,
118: T_INCLUDE => self::PHP_KEYWORD,
119: T_INCLUDE_ONCE => self::PHP_KEYWORD,
120: T_REQUIRE => self::PHP_KEYWORD,
121: T_REQUIRE_ONCE => self::PHP_KEYWORD,
122: T_LOGICAL_OR => self::PHP_KEYWORD,
123: T_LOGICAL_XOR => self::PHP_KEYWORD,
124: T_LOGICAL_AND => self::PHP_KEYWORD,
125: T_PRINT => self::PHP_KEYWORD,
126: T_YIELD => self::PHP_KEYWORD,
127: T_YIELD_FROM => self::PHP_KEYWORD,
128: T_INSTANCEOF => self::PHP_KEYWORD,
129: T_NEW => self::PHP_KEYWORD,
130: T_CLONE => self::PHP_KEYWORD,
131: T_ELSEIF => self::PHP_KEYWORD,
132: T_ELSE => self::PHP_KEYWORD,
133: T_EVAL => self::PHP_KEYWORD,
134: T_EXIT => self::PHP_KEYWORD,
135: T_IF => self::PHP_KEYWORD,
136: T_ENDIF => self::PHP_KEYWORD,
137: T_ECHO => self::PHP_KEYWORD,
138: T_DO => self::PHP_KEYWORD,
139: T_WHILE => self::PHP_KEYWORD,
140: T_ENDWHILE => self::PHP_KEYWORD,
141: T_FOR => self::PHP_KEYWORD,
142: T_ENDFOR => self::PHP_KEYWORD,
143: T_FOREACH => self::PHP_KEYWORD,
144: T_ENDFOREACH => self::PHP_KEYWORD,
145: T_DECLARE => self::PHP_KEYWORD,
146: T_ENDDECLARE => self::PHP_KEYWORD,
147: T_AS => self::PHP_KEYWORD,
148: T_SWITCH => self::PHP_KEYWORD,
149: T_ENDSWITCH => self::PHP_KEYWORD,
150: T_CASE => self::PHP_KEYWORD,
151: T_DEFAULT => self::PHP_KEYWORD,
152: T_BREAK => self::PHP_KEYWORD,
153: T_CONTINUE => self::PHP_KEYWORD,
154: T_GOTO => self::PHP_KEYWORD,
155: T_FUNCTION => self::PHP_KEYWORD,
156: T_FN => self::PHP_KEYWORD,
157: T_CONST => self::PHP_KEYWORD,
158: T_RETURN => self::PHP_KEYWORD,
159: T_CATCH => self::PHP_KEYWORD,
160: T_TRY => self::PHP_KEYWORD,
161: T_FINALLY => self::PHP_KEYWORD,
162: T_THROW => self::PHP_KEYWORD,
163: T_USE => self::PHP_KEYWORD,
164: T_INSTEADOF => self::PHP_KEYWORD,
165: T_GLOBAL => self::PHP_KEYWORD,
166: T_STATIC => self::PHP_KEYWORD,
167: T_ABSTRACT => self::PHP_KEYWORD,
168: T_FINAL => self::PHP_KEYWORD,
169: T_PRIVATE => self::PHP_KEYWORD,
170: T_PROTECTED => self::PHP_KEYWORD,
171: T_PUBLIC => self::PHP_KEYWORD,
172: T_VAR => self::PHP_KEYWORD,
173: T_UNSET => self::PHP_KEYWORD,
174: T_ISSET => self::PHP_KEYWORD,
175: T_EMPTY => self::PHP_KEYWORD,
176: T_HALT_COMPILER => self::PHP_KEYWORD,
177: T_CLASS => self::PHP_KEYWORD,
178: T_TRAIT => self::PHP_KEYWORD,
179: T_INTERFACE => self::PHP_KEYWORD,
180: T_EXTENDS => self::PHP_KEYWORD,
181: T_IMPLEMENTS => self::PHP_KEYWORD,
182: T_LIST => self::PHP_KEYWORD,
183: T_ARRAY => self::PHP_KEYWORD,
184: T_NAMESPACE => self::PHP_KEYWORD,
185: T_ENUM => self::PHP_KEYWORD,
186: T_READONLY => self::PHP_KEYWORD,
187: T_MATCH => self::PHP_KEYWORD,
188: T_LNUMBER => self::PHP_NUMBER,
189: T_DNUMBER => self::PHP_NUMBER,
190: T_CONSTANT_ENCAPSED_STRING => self::PHP_STRING,
191: T_ENCAPSED_AND_WHITESPACE => self::PHP_STRING,
192: T_VARIABLE => self::PHP_VARIABLE,
193: T_COMMENT => self::PHP_COMMENT,
194: T_DOC_COMMENT => self::PHP_COMMENT,
195: ];
196:
197: /** @var string[] indexed by [identifierName] */
198: public array $identifierClass = [
199: 'true' => self::PHP_KEYWORD,
200: 'false' => self::PHP_KEYWORD,
201: 'null' => self::PHP_KEYWORD,
202: ];
203:
204:
205: public function __construct(
206: protected OutputStyle $output,
207: ) {
208: }
209:
210:
211: public function highlight(string $path): string
212: {
213: $source = FileSystem::read($path);
214: $align = strlen(strval(1 + substr_count($source, "\n")));
215: $lineStart = "<tr id=\"%1\$d\" class=\"source-line\"><td><a class=\"source-lineNum\" href=\"#%1\$d\">%1\${$align}d: </a></td><td>";
216: $lineEnd = '</td></tr>';
217:
218: $line = 1;
219: $out = sprintf($lineStart, $line);
220:
221: foreach ($this->tokenize($path, $source) as $id => $text) {
222: if ($text === "\n") {
223: $out .= $lineEnd . sprintf($lineStart, ++$line);
224:
225: } else {
226: $html = htmlspecialchars($text);
227: $class = $this->tokenClass[$id] ?? ($id === T_STRING ? $this->identifierClass[strtolower($text)] ?? null : null);
228: $out .= $class ? "<span class=\"{$class}\">{$html}</span>" : $html;
229: }
230: }
231:
232: return $out . $lineEnd;
233: }
234:
235:
236: /**
237: * @return iterable<int, string>
238: */
239: protected function tokenize(string $path, string $source): iterable
240: {
241: try {
242: $tokens = PhpToken::tokenize($source, TOKEN_PARSE);
243:
244: } catch (\ParseError $e) {
245: $this->output->newLine();
246: $this->output->warning(sprintf("Parse error in %s:%d\n%s", $path, $e->getLine(), $e->getMessage()));
247: $tokens = [new PhpToken(T_INLINE_HTML, $source)];
248: }
249:
250: foreach ($tokens as $token) {
251: $lines = explode("\n", $token->text);
252: $lastLine = count($lines) - 1;
253:
254: foreach ($lines as $i => $line) {
255: yield $token->id => $line;
256:
257: if ($i !== $lastLine) {
258: yield T_WHITESPACE => "\n";
259: }
260: }
261: }
262: }
263: }
264: