1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace PhpMyAdmin\SqlParser\Utils; |
||||
6 | |||||
7 | use PhpMyAdmin\SqlParser\Components\JoinKeyword; |
||||
8 | use PhpMyAdmin\SqlParser\Lexer; |
||||
9 | use PhpMyAdmin\SqlParser\Parser; |
||||
10 | use PhpMyAdmin\SqlParser\Token; |
||||
11 | use PhpMyAdmin\SqlParser\TokensList; |
||||
12 | use PhpMyAdmin\SqlParser\TokenType; |
||||
13 | |||||
14 | use function array_merge; |
||||
15 | use function array_pop; |
||||
16 | use function end; |
||||
17 | use function htmlspecialchars; |
||||
18 | use function in_array; |
||||
19 | use function mb_strlen; |
||||
20 | use function str_contains; |
||||
21 | use function str_repeat; |
||||
22 | use function str_replace; |
||||
23 | use function strtoupper; |
||||
24 | |||||
25 | use const ENT_NOQUOTES; |
||||
26 | use const PHP_SAPI; |
||||
27 | |||||
28 | /** |
||||
29 | * Utilities that are used for formatting queries. |
||||
30 | */ |
||||
31 | class Formatter |
||||
32 | { |
||||
33 | /** |
||||
34 | * The formatting options. |
||||
35 | * |
||||
36 | * @var array<string, bool|string|array<int, array<string, int|string>>> |
||||
37 | */ |
||||
38 | public array $options; |
||||
39 | |||||
40 | /** |
||||
41 | * Clauses that are usually short. |
||||
42 | * |
||||
43 | * These clauses share the line with the next clause. |
||||
44 | * |
||||
45 | * E.g. if INSERT was not here, the formatter would produce: |
||||
46 | * |
||||
47 | * INSERT |
||||
48 | * INTO foo |
||||
49 | * VALUES(0, 0, 0),(1, 1, 1); |
||||
50 | * |
||||
51 | * Instead of: |
||||
52 | * |
||||
53 | * INSERT INTO foo |
||||
54 | * VALUES(0, 0, 0),(1, 1, 1) |
||||
55 | * |
||||
56 | * @var array<string, bool> |
||||
57 | */ |
||||
58 | public static array $shortClauses = [ |
||||
59 | 'CREATE' => true, |
||||
60 | 'INSERT' => true, |
||||
61 | ]; |
||||
62 | |||||
63 | /** |
||||
64 | * Clauses that must be inlined. |
||||
65 | * |
||||
66 | * These clauses usually are short and it's nicer to have them inline. |
||||
67 | * |
||||
68 | * @var array<string, bool> |
||||
69 | */ |
||||
70 | public static array $inlineClauses = [ |
||||
71 | 'CREATE' => true, |
||||
72 | 'INTO' => true, |
||||
73 | 'LIMIT' => true, |
||||
74 | 'PARTITION BY' => true, |
||||
75 | 'PARTITION' => true, |
||||
76 | 'PROCEDURE' => true, |
||||
77 | 'SUBPARTITION BY' => true, |
||||
78 | 'VALUES' => true, |
||||
79 | ]; |
||||
80 | |||||
81 | private const FORMATTERS = [ |
||||
82 | 'PARTITION BY', |
||||
83 | 'SUBPARTITION BY', |
||||
84 | ]; |
||||
85 | |||||
86 | /** @param array<string, bool|string|array<int, array<string, int|string>>> $options the formatting options */ |
||||
87 | 48 | public function __construct(array $options = []) |
|||
88 | { |
||||
89 | 48 | $this->options = $this->getMergedOptions($options); |
|||
90 | } |
||||
91 | |||||
92 | /** |
||||
93 | * The specified formatting options are merged with the default values. |
||||
94 | * |
||||
95 | * @param array<string, bool|string|array<int, array<string, int|string>>> $options |
||||
96 | * |
||||
97 | * @return array<string, bool|string|array<int, array<string, int|string>>> |
||||
98 | */ |
||||
99 | 56 | protected function getMergedOptions(array $options): array |
|||
100 | { |
||||
101 | 56 | $options = array_merge( |
|||
102 | 56 | $this->getDefaultOptions(), |
|||
103 | 56 | $options, |
|||
104 | 56 | ); |
|||
105 | |||||
106 | 56 | if (isset($options['formats'])) { |
|||
107 | 8 | $options['formats'] = self::mergeFormats($this->getDefaultFormats(), $options['formats']); |
|||
108 | } else { |
||||
109 | 48 | $options['formats'] = $this->getDefaultFormats(); |
|||
110 | } |
||||
111 | |||||
112 | 56 | if ($options['line_ending'] === null) { |
|||
113 | 48 | $options['line_ending'] = $options['type'] === 'html' ? '<br/>' : "\n"; |
|||
114 | } |
||||
115 | |||||
116 | 56 | if ($options['indentation'] === null) { |
|||
117 | 56 | $options['indentation'] = $options['type'] === 'html' ? ' ' : ' '; |
|||
118 | } |
||||
119 | |||||
120 | // `parts_newline` requires `clause_newline` |
||||
121 | 56 | $options['parts_newline'] &= $options['clause_newline']; |
|||
122 | |||||
123 | 56 | return $options; |
|||
124 | } |
||||
125 | |||||
126 | /** |
||||
127 | * The default formatting options. |
||||
128 | * |
||||
129 | * @return array<string, bool|string|null> |
||||
130 | * @psalm-return array{ |
||||
131 | * type: ('cli'|'text'), |
||||
132 | * line_ending: null, |
||||
133 | * indentation: null, |
||||
134 | * remove_comments: false, |
||||
135 | * clause_newline: true, |
||||
136 | * parts_newline: true, |
||||
137 | * indent_parts: true |
||||
138 | * } |
||||
139 | */ |
||||
140 | 48 | protected function getDefaultOptions(): array |
|||
141 | { |
||||
142 | 48 | return [ |
|||
143 | /* |
||||
144 | * The format of the result. |
||||
145 | * |
||||
146 | * @var string The type ('text', 'cli' or 'html') |
||||
147 | */ |
||||
148 | 48 | 'type' => PHP_SAPI === 'cli' ? 'cli' : 'text', |
|||
149 | |||||
150 | /* |
||||
151 | * The line ending used. |
||||
152 | * By default, for text this is "\n" and for HTML this is "<br/>". |
||||
153 | * |
||||
154 | * @var string |
||||
155 | */ |
||||
156 | 48 | 'line_ending' => null, |
|||
157 | |||||
158 | /* |
||||
159 | * The string used for indentation. |
||||
160 | * |
||||
161 | * @var string |
||||
162 | */ |
||||
163 | 48 | 'indentation' => null, |
|||
164 | |||||
165 | /* |
||||
166 | * Whether comments should be removed or not. |
||||
167 | * |
||||
168 | * @var bool |
||||
169 | */ |
||||
170 | 48 | 'remove_comments' => false, |
|||
171 | |||||
172 | /* |
||||
173 | * Whether each clause should be on a new line. |
||||
174 | * |
||||
175 | * @var bool |
||||
176 | */ |
||||
177 | 48 | 'clause_newline' => true, |
|||
178 | |||||
179 | /* |
||||
180 | * Whether each part should be on a new line. |
||||
181 | * Parts are delimited by brackets and commas. |
||||
182 | * |
||||
183 | * @var bool |
||||
184 | */ |
||||
185 | 48 | 'parts_newline' => true, |
|||
186 | |||||
187 | /* |
||||
188 | * Whether each part of each clause should be indented. |
||||
189 | * |
||||
190 | * @var bool |
||||
191 | */ |
||||
192 | 48 | 'indent_parts' => true, |
|||
193 | 48 | ]; |
|||
194 | } |
||||
195 | |||||
196 | /** |
||||
197 | * The styles used for HTML formatting. |
||||
198 | * [$type, $flags, $span, $callback]. |
||||
199 | * |
||||
200 | * @return array<int, array<string, int|string>> |
||||
201 | * @psalm-return list<array{type: int, flags: int, html: string, cli: string, function: string}> |
||||
202 | */ |
||||
203 | 48 | protected function getDefaultFormats(): array |
|||
204 | { |
||||
205 | 48 | return [ |
|||
206 | 48 | [ |
|||
207 | 48 | 'type' => TokenType::Keyword->value, |
|||
208 | 48 | 'flags' => Token::FLAG_KEYWORD_RESERVED, |
|||
209 | 48 | 'html' => 'class="sql-reserved"', |
|||
210 | 48 | 'cli' => "\x1b[35m", |
|||
211 | 48 | 'function' => 'strtoupper', |
|||
212 | 48 | ], |
|||
213 | 48 | [ |
|||
214 | 48 | 'type' => TokenType::Keyword->value, |
|||
215 | 48 | 'flags' => 0, |
|||
216 | 48 | 'html' => 'class="sql-keyword"', |
|||
217 | 48 | 'cli' => "\x1b[95m", |
|||
218 | 48 | 'function' => 'strtoupper', |
|||
219 | 48 | ], |
|||
220 | 48 | [ |
|||
221 | 48 | 'type' => TokenType::Comment->value, |
|||
222 | 48 | 'flags' => 0, |
|||
223 | 48 | 'html' => 'class="sql-comment"', |
|||
224 | 48 | 'cli' => "\x1b[37m", |
|||
225 | 48 | 'function' => '', |
|||
226 | 48 | ], |
|||
227 | 48 | [ |
|||
228 | 48 | 'type' => TokenType::Bool->value, |
|||
229 | 48 | 'flags' => 0, |
|||
230 | 48 | 'html' => 'class="sql-atom"', |
|||
231 | 48 | 'cli' => "\x1b[36m", |
|||
232 | 48 | 'function' => 'strtoupper', |
|||
233 | 48 | ], |
|||
234 | 48 | [ |
|||
235 | 48 | 'type' => TokenType::Number->value, |
|||
236 | 48 | 'flags' => 0, |
|||
237 | 48 | 'html' => 'class="sql-number"', |
|||
238 | 48 | 'cli' => "\x1b[92m", |
|||
239 | 48 | 'function' => 'strtolower', |
|||
240 | 48 | ], |
|||
241 | 48 | [ |
|||
242 | 48 | 'type' => TokenType::String->value, |
|||
243 | 48 | 'flags' => 0, |
|||
244 | 48 | 'html' => 'class="sql-string"', |
|||
245 | 48 | 'cli' => "\x1b[91m", |
|||
246 | 48 | 'function' => '', |
|||
247 | 48 | ], |
|||
248 | 48 | [ |
|||
249 | 48 | 'type' => TokenType::Symbol->value, |
|||
250 | 48 | 'flags' => Token::FLAG_SYMBOL_PARAMETER, |
|||
251 | 48 | 'html' => 'class="sql-parameter"', |
|||
252 | 48 | 'cli' => "\x1b[31m", |
|||
253 | 48 | 'function' => '', |
|||
254 | 48 | ], |
|||
255 | 48 | [ |
|||
256 | 48 | 'type' => TokenType::Symbol->value, |
|||
257 | 48 | 'flags' => 0, |
|||
258 | 48 | 'html' => 'class="sql-variable"', |
|||
259 | 48 | 'cli' => "\x1b[36m", |
|||
260 | 48 | 'function' => '', |
|||
261 | 48 | ], |
|||
262 | 48 | ]; |
|||
263 | } |
||||
264 | |||||
265 | /** |
||||
266 | * @param array<int, array<string, int|string>> $formats |
||||
267 | * @param array<int, array<string, int|string>> $newFormats |
||||
268 | * |
||||
269 | * @return array<int, array<string, int|string>> |
||||
270 | */ |
||||
271 | 8 | private static function mergeFormats(array $formats, array $newFormats): array |
|||
272 | { |
||||
273 | 8 | $added = []; |
|||
274 | 8 | $integers = [ |
|||
275 | 8 | 'flags', |
|||
276 | 8 | 'type', |
|||
277 | 8 | ]; |
|||
278 | 8 | $strings = [ |
|||
279 | 8 | 'html', |
|||
280 | 8 | 'cli', |
|||
281 | 8 | 'function', |
|||
282 | 8 | ]; |
|||
283 | |||||
284 | /* Sanitize the array so that we do not have to care later */ |
||||
285 | 8 | foreach ($newFormats as $j => $new) { |
|||
286 | 8 | foreach ($integers as $name) { |
|||
287 | 8 | if (isset($new[$name])) { |
|||
288 | 6 | continue; |
|||
289 | } |
||||
290 | |||||
291 | 6 | $newFormats[$j][$name] = 0; |
|||
292 | } |
||||
293 | |||||
294 | 8 | foreach ($strings as $name) { |
|||
295 | 8 | if (isset($new[$name])) { |
|||
296 | 6 | continue; |
|||
297 | } |
||||
298 | |||||
299 | 8 | $newFormats[$j][$name] = ''; |
|||
300 | } |
||||
301 | } |
||||
302 | |||||
303 | /* Process changes to existing formats */ |
||||
304 | 8 | foreach ($formats as $i => $original) { |
|||
305 | 8 | foreach ($newFormats as $j => $new) { |
|||
306 | 8 | if ($new['type'] !== $original['type'] || $original['flags'] !== $new['flags']) { |
|||
307 | 6 | continue; |
|||
308 | } |
||||
309 | |||||
310 | 6 | $formats[$i] = $new; |
|||
311 | 6 | $added[] = $j; |
|||
312 | } |
||||
313 | } |
||||
314 | |||||
315 | /* Add not already handled formats */ |
||||
316 | 8 | foreach ($newFormats as $j => $new) { |
|||
317 | 8 | if (in_array($j, $added)) { |
|||
318 | 6 | continue; |
|||
319 | } |
||||
320 | |||||
321 | 2 | $formats[] = $new; |
|||
322 | } |
||||
323 | |||||
324 | 8 | return $formats; |
|||
325 | } |
||||
326 | |||||
327 | /** |
||||
328 | * Formats the given list of tokens. |
||||
329 | * |
||||
330 | * @param TokensList $list the list of tokens |
||||
331 | */ |
||||
332 | 48 | public function formatList(TokensList $list): string |
|||
333 | { |
||||
334 | /** |
||||
335 | * The query to be returned. |
||||
336 | */ |
||||
337 | 48 | $ret = ''; |
|||
338 | |||||
339 | /** |
||||
340 | * The indentation level. |
||||
341 | */ |
||||
342 | 48 | $indent = 0; |
|||
343 | |||||
344 | /** |
||||
345 | * Whether the line ended. |
||||
346 | */ |
||||
347 | 48 | $lineEnded = false; |
|||
348 | |||||
349 | /** |
||||
350 | * Whether current group is short (no linebreaks). |
||||
351 | */ |
||||
352 | 48 | $shortGroup = false; |
|||
353 | |||||
354 | /** |
||||
355 | * The name of the last clause. |
||||
356 | */ |
||||
357 | 48 | $lastClause = ''; |
|||
358 | |||||
359 | /** |
||||
360 | * A stack that keeps track of the indentation level every time a new |
||||
361 | * block is found. |
||||
362 | */ |
||||
363 | 48 | $blocksIndentation = []; |
|||
364 | |||||
365 | /** |
||||
366 | * A stack that keeps track of the line endings every time a new block |
||||
367 | * is found. |
||||
368 | */ |
||||
369 | 48 | $blocksLineEndings = []; |
|||
370 | |||||
371 | /** |
||||
372 | * Whether clause's options were formatted. |
||||
373 | */ |
||||
374 | 48 | $formattedOptions = false; |
|||
375 | |||||
376 | /** |
||||
377 | * Previously parsed token. |
||||
378 | */ |
||||
379 | 48 | $prev = null; |
|||
380 | |||||
381 | // In order to be able to format the queries correctly, the next token |
||||
382 | // must be taken into consideration. The loop below uses two pointers, |
||||
383 | // `$prev` and `$curr` which store two consecutive tokens. |
||||
384 | // Actually, at every iteration the previous token is being used. |
||||
385 | 48 | for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) { |
|||
386 | /** |
||||
387 | * Token parsed at this moment. |
||||
388 | */ |
||||
389 | 48 | $curr = $list->tokens[$list->idx]; |
|||
390 | 48 | if ($list->idx + 1 < $list->count) { |
|||
391 | 46 | $next = $list->tokens[$list->idx + 1]; |
|||
392 | } else { |
||||
393 | 48 | $next = null; |
|||
394 | } |
||||
395 | |||||
396 | 48 | if ($curr->type === TokenType::Whitespace) { |
|||
397 | // Keep linebreaks before and after comments |
||||
398 | if ( |
||||
399 | 46 | str_contains($curr->token, "\n") && ( |
|||
400 | 46 | ($prev !== null && $prev->type === TokenType::Comment) || |
|||
401 | 46 | ($next !== null && $next->type === TokenType::Comment) |
|||
402 | ) |
||||
403 | ) { |
||||
404 | 2 | $lineEnded = true; |
|||
405 | } |
||||
406 | |||||
407 | // Whitespaces are skipped because the formatter adds its own. |
||||
408 | 46 | continue; |
|||
409 | } |
||||
410 | |||||
411 | 48 | if ($curr->type === TokenType::Comment && $this->options['remove_comments']) { |
|||
412 | // Skip Comments if option `remove_comments` is enabled |
||||
413 | 2 | continue; |
|||
414 | } |
||||
415 | |||||
416 | // Checking if pointers were initialized. |
||||
417 | 48 | if ($prev !== null) { |
|||
418 | // Checking if a new clause started. |
||||
419 | 46 | if (static::isClause($prev) !== false) { |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
420 | 46 | $lastClause = $prev->value; |
|||
421 | 46 | $formattedOptions = false; |
|||
422 | } |
||||
423 | |||||
424 | // The options of a clause should stay on the same line and everything that follows. |
||||
425 | if ( |
||||
426 | 46 | $this->options['parts_newline'] |
|||
427 | 46 | && ! $formattedOptions |
|||
428 | 46 | && empty(self::$inlineClauses[$lastClause]) |
|||
429 | && ( |
||||
430 | 46 | $curr->type !== TokenType::Keyword |
|||
431 | 46 | || ($curr->flags & Token::FLAG_KEYWORD_FUNCTION) |
|||
432 | ) |
||||
433 | ) { |
||||
434 | 42 | $formattedOptions = true; |
|||
435 | 42 | $lineEnded = true; |
|||
436 | 42 | ++$indent; |
|||
437 | } |
||||
438 | |||||
439 | // Checking if this clause ended. |
||||
440 | 46 | $isClause = static::isClause($curr); |
|||
441 | |||||
442 | 46 | if ($isClause !== false) { |
|||
443 | if ( |
||||
444 | 20 | ($isClause === 2 || $this->options['clause_newline']) |
|||
445 | 20 | && empty(self::$shortClauses[$lastClause]) |
|||
446 | ) { |
||||
447 | 20 | $lineEnded = true; |
|||
448 | 20 | if ($this->options['parts_newline'] && $indent > 0) { |
|||
449 | 18 | --$indent; |
|||
450 | } |
||||
451 | } |
||||
452 | } |
||||
453 | |||||
454 | // Inline JOINs |
||||
455 | if ( |
||||
456 | 46 | ($prev->type === TokenType::Keyword && isset(JoinKeyword::JOINS[$prev->value])) |
|||
457 | 46 | || (in_array($curr->value, ['ON', 'USING'], true) |
|||
458 | 46 | && isset(JoinKeyword::JOINS[$list->tokens[$list->idx - 2]->value])) |
|||
459 | 46 | || isset($list->tokens[$list->idx - 4], JoinKeyword::JOINS[$list->tokens[$list->idx - 4]->value]) |
|||
460 | 46 | || isset($list->tokens[$list->idx - 6], JoinKeyword::JOINS[$list->tokens[$list->idx - 6]->value]) |
|||
461 | ) { |
||||
462 | 2 | $lineEnded = false; |
|||
463 | } |
||||
464 | |||||
465 | // Indenting BEGIN ... END blocks. |
||||
466 | 46 | if ($prev->type === TokenType::Keyword && $prev->keyword === 'BEGIN') { |
|||
467 | 2 | $lineEnded = true; |
|||
468 | 2 | $blocksIndentation[] = $indent; |
|||
469 | 2 | ++$indent; |
|||
470 | 46 | } elseif ($curr->type === TokenType::Keyword && $curr->keyword === 'END') { |
|||
471 | 2 | $lineEnded = true; |
|||
472 | 2 | $indent = array_pop($blocksIndentation); |
|||
473 | } |
||||
474 | |||||
475 | // Formatting fragments delimited by comma. |
||||
476 | 46 | if ($prev->type === TokenType::Operator && $prev->value === ',') { |
|||
477 | // Fragments delimited by a comma are broken into multiple |
||||
478 | // pieces only if the clause is not inlined or this fragment |
||||
479 | // is between brackets that are on new line. |
||||
480 | if ( |
||||
481 | 8 | end($blocksLineEndings) === true |
|||
482 | || ( |
||||
483 | 8 | empty(self::$inlineClauses[$lastClause]) |
|||
484 | 8 | && ! $shortGroup |
|||
485 | 8 | && $this->options['parts_newline'] |
|||
486 | ) |
||||
487 | ) { |
||||
488 | 6 | $lineEnded = true; |
|||
489 | } |
||||
490 | } |
||||
491 | |||||
492 | // Handling brackets. |
||||
493 | // Brackets are indented only if the length of the fragment between |
||||
494 | // them is longer than 30 characters. |
||||
495 | 46 | if ($prev->type === TokenType::Operator && $prev->value === '(') { |
|||
496 | 12 | $blocksIndentation[] = $indent; |
|||
497 | 12 | $shortGroup = true; |
|||
498 | 12 | if (static::getGroupLength($list) > 30) { |
|||
499 | 2 | ++$indent; |
|||
500 | 2 | $lineEnded = true; |
|||
501 | 2 | $shortGroup = false; |
|||
502 | } |
||||
503 | |||||
504 | 12 | $blocksLineEndings[] = $lineEnded; |
|||
505 | 46 | } elseif ($curr->type === TokenType::Operator && $curr->value === ')') { |
|||
506 | 10 | $indent = array_pop($blocksIndentation); |
|||
507 | 10 | $lineEnded |= array_pop($blocksLineEndings); |
|||
508 | 10 | $shortGroup = false; |
|||
509 | } |
||||
510 | |||||
511 | // Adding the token. |
||||
512 | 46 | $ret .= $this->toString($prev); |
|||
0 ignored issues
–
show
$prev of type null is incompatible with the type PhpMyAdmin\SqlParser\Token expected by parameter $token of PhpMyAdmin\SqlParser\Utils\Formatter::toString() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
513 | |||||
514 | // Finishing the line. |
||||
515 | 46 | if ($lineEnded) { |
|||
516 | 44 | $ret .= $this->options['line_ending'] . str_repeat($this->options['indentation'], (int) $indent); |
|||
517 | 44 | $lineEnded = false; |
|||
518 | } elseif ( |
||||
519 | 46 | $prev->keyword === 'DELIMITER' |
|||
520 | 46 | || ! ( |
|||
521 | 46 | ($prev->type === TokenType::Operator && ($prev->value === '.' || $prev->value === '(')) |
|||
522 | 46 | // No space after . ( |
|||
523 | 46 | || ($curr->type === TokenType::Operator |
|||
524 | 46 | && ($curr->value === '.' || $curr->value === ',' |
|||
525 | 46 | || $curr->value === '(' || $curr->value === ')')) |
|||
526 | 46 | // No space before . , ( ) |
|||
527 | 46 | || $curr->type === TokenType::Delimiter && mb_strlen((string) $curr->value, 'UTF-8') < 2 |
|||
528 | 46 | ) |
|||
529 | ) { |
||||
530 | // If the line ended, there is no point in adding whitespaces. |
||||
531 | // Also, some tokens do not have spaces before or after them. |
||||
532 | // A space after delimiters that are longer than 2 characters. |
||||
533 | 26 | $ret .= ' '; |
|||
534 | } |
||||
535 | } |
||||
536 | |||||
537 | // Iteration finished, consider current token as previous. |
||||
538 | 48 | $prev = $curr; |
|||
539 | } |
||||
540 | |||||
541 | 48 | if ($this->options['type'] === 'cli') { |
|||
542 | 40 | return $ret . "\x1b[0m"; |
|||
543 | } |
||||
544 | |||||
545 | 42 | return $ret; |
|||
546 | } |
||||
547 | |||||
548 | 38 | public function escapeConsole(string $string): string |
|||
549 | { |
||||
550 | 38 | return str_replace( |
|||
551 | 38 | [ |
|||
552 | 38 | "\x00", |
|||
553 | 38 | "\x01", |
|||
554 | 38 | "\x02", |
|||
555 | 38 | "\x03", |
|||
556 | 38 | "\x04", |
|||
557 | 38 | "\x05", |
|||
558 | 38 | "\x06", |
|||
559 | 38 | "\x07", |
|||
560 | 38 | "\x08", |
|||
561 | 38 | "\x09", |
|||
562 | 38 | "\x0A", |
|||
563 | 38 | "\x0B", |
|||
564 | 38 | "\x0C", |
|||
565 | 38 | "\x0D", |
|||
566 | 38 | "\x0E", |
|||
567 | 38 | "\x0F", |
|||
568 | 38 | "\x10", |
|||
569 | 38 | "\x11", |
|||
570 | 38 | "\x12", |
|||
571 | 38 | "\x13", |
|||
572 | 38 | "\x14", |
|||
573 | 38 | "\x15", |
|||
574 | 38 | "\x16", |
|||
575 | 38 | "\x17", |
|||
576 | 38 | "\x18", |
|||
577 | 38 | "\x19", |
|||
578 | 38 | "\x1A", |
|||
579 | 38 | "\x1B", |
|||
580 | 38 | "\x1C", |
|||
581 | 38 | "\x1D", |
|||
582 | 38 | "\x1E", |
|||
583 | 38 | "\x1F", |
|||
584 | 38 | ], |
|||
585 | 38 | [ |
|||
586 | 38 | '\x00', |
|||
587 | 38 | '\x01', |
|||
588 | 38 | '\x02', |
|||
589 | 38 | '\x03', |
|||
590 | 38 | '\x04', |
|||
591 | 38 | '\x05', |
|||
592 | 38 | '\x06', |
|||
593 | 38 | '\x07', |
|||
594 | 38 | '\x08', |
|||
595 | 38 | '\x09', |
|||
596 | 38 | '\x0A', |
|||
597 | 38 | '\x0B', |
|||
598 | 38 | '\x0C', |
|||
599 | 38 | '\x0D', |
|||
600 | 38 | '\x0E', |
|||
601 | 38 | '\x0F', |
|||
602 | 38 | '\x10', |
|||
603 | 38 | '\x11', |
|||
604 | 38 | '\x12', |
|||
605 | 38 | '\x13', |
|||
606 | 38 | '\x14', |
|||
607 | 38 | '\x15', |
|||
608 | 38 | '\x16', |
|||
609 | 38 | '\x17', |
|||
610 | 38 | '\x18', |
|||
611 | 38 | '\x19', |
|||
612 | 38 | '\x1A', |
|||
613 | 38 | '\x1B', |
|||
614 | 38 | '\x1C', |
|||
615 | 38 | '\x1D', |
|||
616 | 38 | '\x1E', |
|||
617 | 38 | '\x1F', |
|||
618 | 38 | ], |
|||
619 | 38 | $string, |
|||
620 | 38 | ); |
|||
621 | } |
||||
622 | |||||
623 | /** |
||||
624 | * Tries to print the query and returns the result. |
||||
625 | * |
||||
626 | * @param Token $token the token to be printed |
||||
627 | */ |
||||
628 | 46 | public function toString(Token $token): string |
|||
629 | { |
||||
630 | 46 | $text = $token->token; |
|||
631 | 46 | static $prev; |
|||
632 | |||||
633 | 46 | foreach ($this->options['formats'] as $format) { |
|||
634 | if ( |
||||
635 | 46 | $token->type->value !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags']) |
|||
636 | ) { |
||||
637 | 46 | continue; |
|||
638 | } |
||||
639 | |||||
640 | // Running transformation function. |
||||
641 | 46 | if (! empty($format['function'])) { |
|||
642 | 46 | $func = $format['function']; |
|||
643 | 46 | $text = $func($text); |
|||
644 | } |
||||
645 | |||||
646 | // Formatting HTML. |
||||
647 | 46 | if ($this->options['type'] === 'html') { |
|||
648 | 36 | return '<span ' . $format['html'] . '>' . htmlspecialchars($text, ENT_NOQUOTES) . '</span>'; |
|||
649 | } |
||||
650 | |||||
651 | 42 | if ($this->options['type'] === 'cli') { |
|||
652 | 38 | if ($prev !== $format['cli']) { |
|||
653 | 38 | $prev = $format['cli']; |
|||
654 | |||||
655 | 38 | return $format['cli'] . $this->escapeConsole($text); |
|||
656 | } |
||||
657 | |||||
658 | 10 | return $this->escapeConsole($text); |
|||
659 | } |
||||
660 | |||||
661 | 36 | break; |
|||
662 | } |
||||
663 | |||||
664 | 36 | if ($this->options['type'] === 'cli') { |
|||
665 | 28 | if ($prev !== "\x1b[39m") { |
|||
666 | 28 | $prev = "\x1b[39m"; |
|||
667 | |||||
668 | 28 | return "\x1b[39m" . $this->escapeConsole($text); |
|||
669 | } |
||||
670 | |||||
671 | 16 | return $this->escapeConsole($text); |
|||
672 | } |
||||
673 | |||||
674 | 36 | if ($this->options['type'] === 'html') { |
|||
675 | 28 | return htmlspecialchars($text, ENT_NOQUOTES); |
|||
676 | } |
||||
677 | |||||
678 | 36 | return $text; |
|||
679 | } |
||||
680 | |||||
681 | /** |
||||
682 | * Formats a query. |
||||
683 | * |
||||
684 | * @param string $query The query to be formatted |
||||
685 | * @param array<string, bool|string|array<int, array<string, int|string>>> $options the formatting options |
||||
686 | * |
||||
687 | * @return string the formatted string |
||||
688 | */ |
||||
689 | 48 | public static function format(string $query, array $options = []): string |
|||
690 | { |
||||
691 | 48 | $lexer = new Lexer($query); |
|||
692 | 48 | $formatter = new self($options); |
|||
693 | |||||
694 | 48 | return $formatter->formatList($lexer->list); |
|||
695 | } |
||||
696 | |||||
697 | /** |
||||
698 | * Computes the length of a group. |
||||
699 | * |
||||
700 | * A group is delimited by a pair of brackets. |
||||
701 | * |
||||
702 | * @param TokensList $list the list of tokens |
||||
703 | */ |
||||
704 | 12 | public static function getGroupLength(TokensList $list): int |
|||
705 | { |
||||
706 | /** |
||||
707 | * The number of opening brackets found. |
||||
708 | * This counter starts at one because by the time this function called, |
||||
709 | * the list already advanced one position and the opening bracket was |
||||
710 | * already parsed. |
||||
711 | */ |
||||
712 | 12 | $count = 1; |
|||
713 | |||||
714 | /** |
||||
715 | * The length of this group. |
||||
716 | */ |
||||
717 | 12 | $length = 0; |
|||
718 | |||||
719 | 12 | for ($idx = $list->idx; $idx < $list->count; ++$idx) { |
|||
720 | // Counting the brackets. |
||||
721 | 12 | if ($list->tokens[$idx]->type === TokenType::Operator) { |
|||
722 | 12 | if ($list->tokens[$idx]->value === '(') { |
|||
723 | 2 | ++$count; |
|||
724 | 12 | } elseif ($list->tokens[$idx]->value === ')') { |
|||
725 | 12 | --$count; |
|||
726 | 12 | if ($count === 0) { |
|||
727 | 12 | break; |
|||
728 | } |
||||
729 | } |
||||
730 | } |
||||
731 | |||||
732 | // Keeping track of this group's length. |
||||
733 | 10 | $length += mb_strlen((string) $list->tokens[$idx]->value, 'UTF-8'); |
|||
734 | } |
||||
735 | |||||
736 | 12 | return $length; |
|||
737 | } |
||||
738 | |||||
739 | /** |
||||
740 | * Checks if a token is a statement or a clause inside a statement. |
||||
741 | * |
||||
742 | * @param Token $token the token to be checked |
||||
743 | * |
||||
744 | * @psalm-return 1|2|false |
||||
745 | */ |
||||
746 | 46 | public static function isClause(Token $token): int|false |
|||
747 | { |
||||
748 | if ( |
||||
749 | 46 | ($token->type === TokenType::Keyword && isset(Parser::STATEMENT_PARSERS[$token->keyword])) |
|||
750 | 46 | || ($token->type === TokenType::None && strtoupper($token->token) === 'DELIMITER') |
|||
751 | ) { |
||||
752 | 44 | return 2; |
|||
753 | } |
||||
754 | |||||
755 | if ( |
||||
756 | 46 | $token->type === TokenType::Keyword && ( |
|||
757 | 46 | in_array($token->keyword, self::FORMATTERS, true) || isset(Parser::KEYWORD_PARSERS[$token->keyword]) |
|||
758 | ) |
||||
759 | ) { |
||||
760 | 20 | return 1; |
|||
761 | } |
||||
762 | |||||
763 | 46 | return false; |
|||
764 | } |
||||
765 | } |
||||
766 |