PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / tedivm / jshrink / src / JShrink / Minifier.php
matomo / app / vendor / tedivm / jshrink / src / JShrink Last commit date
Minifier.php 8 months ago
Minifier.php
628 lines
1 <?php
2
3 /*
4 * This file is part of the JShrink package.
5 *
6 * (c) Robert Hafner <tedivm@tedivm.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11 /**
12 * JShrink
13 *
14 *
15 * @package JShrink
16 * @author Robert Hafner <tedivm@tedivm.com>
17 */
18 namespace JShrink;
19
20 /**
21 * Minifier
22 *
23 * Usage - Minifier::minify($js);
24 * Usage - Minifier::minify($js, $options);
25 * Usage - Minifier::minify($js, array('flaggedComments' => false));
26 *
27 * @package JShrink
28 * @author Robert Hafner <tedivm@tedivm.com>
29 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
30 */
31 class Minifier
32 {
33 /**
34 * The input javascript to be minified.
35 *
36 * @var string
37 */
38 protected $input;
39 /**
40 * Length of input javascript.
41 *
42 * @var int
43 */
44 protected $len = 0;
45 /**
46 * The location of the character (in the input string) that is next to be
47 * processed.
48 *
49 * @var int
50 */
51 protected $index = 0;
52 /**
53 * The first of the characters currently being looked at.
54 *
55 * @var string
56 */
57 protected $a = '';
58 /**
59 * The next character being looked at (after a);
60 *
61 * @var string
62 */
63 protected $b = '';
64 /**
65 * This character is only active when certain look ahead actions take place.
66 *
67 * @var string
68 */
69 protected $c;
70 /**
71 * This character is only active when certain look ahead actions take place.
72 *
73 * @var string
74 */
75 protected $last_char;
76 /**
77 * This character is only active when certain look ahead actions take place.
78 *
79 * @var string
80 */
81 protected $output;
82 /**
83 * Contains the options for the current minification process.
84 *
85 * @var array
86 */
87 protected $options;
88 /**
89 * These characters are used to define strings.
90 */
91 protected $stringDelimiters = ['\'' => \true, '"' => \true, '`' => \true];
92 /**
93 * Contains the default options for minification. This array is merged with
94 * the one passed in by the user to create the request specific set of
95 * options (stored in the $options attribute).
96 *
97 * @var array
98 */
99 protected static $defaultOptions = ['flaggedComments' => \true];
100 protected static $keywords = ["delete", "do", "for", "in", "instanceof", "return", "typeof", "yield"];
101 protected $max_keyword_len;
102 /**
103 * Contains lock ids which are used to replace certain code patterns and
104 * prevent them from being minified
105 *
106 * @var array
107 */
108 protected $locks = [];
109 /**
110 * Takes a string containing javascript and removes unneeded characters in
111 * order to shrink the code without altering it's functionality.
112 *
113 * @param string $js The raw javascript to be minified
114 * @param array $options Various runtime options in an associative array
115 * @throws \Exception
116 * @return bool|string
117 */
118 public static function minify($js, $options = [])
119 {
120 try {
121 $jshrink = new \JShrink\Minifier();
122 $js = $jshrink->lock($js);
123 $js = ltrim($jshrink->minifyToString($js, $options));
124 $js = $jshrink->unlock($js);
125 unset($jshrink);
126 return $js;
127 } catch (\Exception $e) {
128 if (isset($jshrink)) {
129 // Since the breakdownScript function probably wasn't finished
130 // we clean it out before discarding it.
131 $jshrink->clean();
132 unset($jshrink);
133 }
134 throw $e;
135 }
136 }
137 /**
138 * Processes a javascript string and outputs only the required characters,
139 * stripping out all unneeded characters.
140 *
141 * @param string $js The raw javascript to be minified
142 * @param array $options Various runtime options in an associative array
143 */
144 protected function minifyToString($js, $options)
145 {
146 $this->initialize($js, $options);
147 $this->loop();
148 $this->clean();
149 return $this->output;
150 }
151 /**
152 * Initializes internal variables, normalizes new lines,
153 *
154 * @param string $js The raw javascript to be minified
155 * @param array $options Various runtime options in an associative array
156 */
157 protected function initialize($js, $options)
158 {
159 $this->options = array_merge(static::$defaultOptions, $options);
160 $this->input = $js;
161 // We add a newline to the end of the script to make it easier to deal
162 // with comments at the bottom of the script- this prevents the unclosed
163 // comment error that can otherwise occur.
164 $this->input .= \PHP_EOL;
165 // save input length to skip calculation every time
166 $this->len = strlen($this->input);
167 // Populate "a" with a new line, "b" with the first character, before
168 // entering the loop
169 $this->a = "\n";
170 $this->b = "\n";
171 $this->last_char = "\n";
172 $this->output = "";
173 $this->max_keyword_len = max(array_map('strlen', static::$keywords));
174 }
175 /**
176 * Characters that can't stand alone preserve the newline.
177 *
178 * @var array
179 */
180 protected $noNewLineCharacters = ['(' => \true, '-' => \true, '+' => \true, '[' => \true, '#' => \true, '@' => \true];
181 protected function echo($char)
182 {
183 $this->output .= $char;
184 $this->last_char = $char[-1];
185 }
186 /**
187 * The primary action occurs here. This function loops through the input string,
188 * outputting anything that's relevant and discarding anything that is not.
189 */
190 protected function loop()
191 {
192 while ($this->a !== \false && !is_null($this->a) && $this->a !== '') {
193 switch ($this->a) {
194 // new lines
195 case "\r":
196 case "\n":
197 // if the next line is something that can't stand alone preserve the newline
198 if ($this->b !== \false && isset($this->noNewLineCharacters[$this->b])) {
199 $this->echo($this->a);
200 $this->saveString();
201 break;
202 }
203 // if B is a space we skip the rest of the switch block and go down to the
204 // string/regex check below, resetting $this->b with getReal
205 if ($this->b === ' ') {
206 break;
207 }
208 // otherwise we treat the newline like a space
209 // no break
210 case ' ':
211 if (static::isAlphaNumeric($this->b)) {
212 $this->echo($this->a);
213 }
214 $this->saveString();
215 break;
216 default:
217 switch ($this->b) {
218 case "\r":
219 case "\n":
220 if (strpos('}])+-"\'', $this->a) !== \false) {
221 $this->echo($this->a);
222 $this->saveString();
223 break;
224 } else {
225 if (static::isAlphaNumeric($this->a)) {
226 $this->echo($this->a);
227 $this->saveString();
228 }
229 }
230 break;
231 case ' ':
232 if (!static::isAlphaNumeric($this->a)) {
233 break;
234 }
235 // no break
236 default:
237 // check for some regex that breaks stuff
238 if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
239 $this->saveRegex();
240 continue 3;
241 }
242 $this->echo($this->a);
243 $this->saveString();
244 break;
245 }
246 }
247 // do reg check of doom
248 $this->b = $this->getReal();
249 if ($this->b == '/') {
250 $valid_tokens = "(,=:[!&|?\n";
251 # Find last "real" token, excluding spaces.
252 $last_token = $this->a;
253 if ($last_token == " ") {
254 $last_token = $this->last_char;
255 }
256 if (strpos($valid_tokens, $last_token) !== \false) {
257 // Regex can appear unquoted after these symbols
258 $this->saveRegex();
259 } else {
260 if ($this->endsInKeyword()) {
261 // This block checks for the "return" token before the slash.
262 $this->saveRegex();
263 }
264 }
265 }
266 // if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) {
267 // $this->saveRegex();
268 // }
269 }
270 }
271 /**
272 * Resets attributes that do not need to be stored between requests so that
273 * the next request is ready to go. Another reason for this is to make sure
274 * the variables are cleared and are not taking up memory.
275 */
276 protected function clean()
277 {
278 unset($this->input);
279 $this->len = 0;
280 $this->index = 0;
281 $this->a = $this->b = '';
282 unset($this->c);
283 unset($this->options);
284 }
285 /**
286 * Returns the next string for processing based off of the current index.
287 *
288 * @return string
289 */
290 protected function getChar()
291 {
292 // Check to see if we had anything in the look ahead buffer and use that.
293 if (isset($this->c)) {
294 $char = $this->c;
295 unset($this->c);
296 } else {
297 // Otherwise we start pulling from the input.
298 $char = $this->index < $this->len ? $this->input[$this->index] : \false;
299 // If the next character doesn't exist return false.
300 if (isset($char) && $char === \false) {
301 return \false;
302 }
303 // Otherwise increment the pointer and use this char.
304 $this->index++;
305 }
306 # Convert all line endings to unix standard.
307 # `\r\n` converts to `\n\n` and is minified.
308 if ($char == "\r") {
309 $char = "\n";
310 }
311 // Normalize all whitespace except for the newline character into a
312 // standard space.
313 if ($char !== "\n" && $char < " ") {
314 return ' ';
315 }
316 return $char;
317 }
318 /**
319 * This function returns the next character without moving the index forward.
320 *
321 *
322 * @return string The next character
323 * @throws \RuntimeException
324 */
325 protected function peek()
326 {
327 if ($this->index >= $this->len) {
328 return \false;
329 }
330 $char = $this->input[$this->index];
331 # Convert all line endings to unix standard.
332 # `\r\n` converts to `\n\n` and is minified.
333 if ($char == "\r") {
334 $char = "\n";
335 }
336 // Normalize all whitespace except for the newline character into a
337 // standard space.
338 if ($char !== "\n" && $char < " ") {
339 return ' ';
340 }
341 # Return the next character but don't push the index.
342 return $char;
343 }
344 /**
345 * This function gets the next "real" character. It is essentially a wrapper
346 * around the getChar function that skips comments. This has significant
347 * performance benefits as the skipping is done using native functions (ie,
348 * c code) rather than in script php.
349 *
350 *
351 * @return string Next 'real' character to be processed.
352 * @throws \RuntimeException
353 */
354 protected function getReal()
355 {
356 $startIndex = $this->index;
357 $char = $this->getChar();
358 // Check to see if we're potentially in a comment
359 if ($char !== '/') {
360 return $char;
361 }
362 $this->c = $this->getChar();
363 if ($this->c === '/') {
364 $this->processOneLineComments($startIndex);
365 return $this->getReal();
366 } elseif ($this->c === '*') {
367 $this->processMultiLineComments($startIndex);
368 return $this->getReal();
369 }
370 return $char;
371 }
372 /**
373 * Removed one line comments, with the exception of some very specific types of
374 * conditional comments.
375 *
376 * @param int $startIndex The index point where "getReal" function started
377 * @return void
378 */
379 protected function processOneLineComments($startIndex)
380 {
381 $thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : \false;
382 // kill rest of line
383 $this->getNext("\n");
384 unset($this->c);
385 if ($thirdCommentString == '@') {
386 $endPoint = $this->index - $startIndex;
387 $this->c = "\n" . substr($this->input, $startIndex, $endPoint);
388 }
389 }
390 /**
391 * Skips multiline comments where appropriate, and includes them where needed.
392 * Conditional comments and "license" style blocks are preserved.
393 *
394 * @param int $startIndex The index point where "getReal" function started
395 * @return void
396 * @throws \RuntimeException Unclosed comments will throw an error
397 */
398 protected function processMultiLineComments($startIndex)
399 {
400 $this->getChar();
401 // current C
402 $thirdCommentString = $this->getChar();
403 // Detect a completely empty comment, ie `/**/`
404 if ($thirdCommentString == "*") {
405 $peekChar = $this->peek();
406 if ($peekChar == "/") {
407 $this->index++;
408 return;
409 }
410 }
411 // kill everything up to the next */ if it's there
412 if ($this->getNext('*/')) {
413 $this->getChar();
414 // get *
415 $this->getChar();
416 // get /
417 $char = $this->getChar();
418 // get next real character
419 // Now we reinsert conditional comments and YUI-style licensing comments
420 if ($this->options['flaggedComments'] && $thirdCommentString === '!' || $thirdCommentString === '@') {
421 // If conditional comments or flagged comments are not the first thing in the script
422 // we need to echo a and fill it with a space before moving on.
423 if ($startIndex > 0) {
424 $this->echo($this->a);
425 $this->a = " ";
426 // If the comment started on a new line we let it stay on the new line
427 if ($this->input[$startIndex - 1] === "\n") {
428 $this->echo("\n");
429 }
430 }
431 $endPoint = $this->index - 1 - $startIndex;
432 $this->echo(substr($this->input, $startIndex, $endPoint));
433 $this->c = $char;
434 return;
435 }
436 } else {
437 $char = \false;
438 }
439 if ($char === \false) {
440 throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2));
441 }
442 // if we're here c is part of the comment and therefore tossed
443 $this->c = $char;
444 }
445 /**
446 * Pushes the index ahead to the next instance of the supplied string. If it
447 * is found the first character of the string is returned and the index is set
448 * to it's position.
449 *
450 * @param string $string
451 * @return string|false Returns the first character of the string or false.
452 */
453 protected function getNext($string)
454 {
455 // Find the next occurrence of "string" after the current position.
456 $pos = strpos($this->input, $string, $this->index);
457 // If it's not there return false.
458 if ($pos === \false) {
459 return \false;
460 }
461 // Adjust position of index to jump ahead to the asked for string
462 $this->index = $pos;
463 // Return the first character of that string.
464 return $this->index < $this->len ? $this->input[$this->index] : \false;
465 }
466 /**
467 * When a javascript string is detected this function crawls for the end of
468 * it and saves the whole string.
469 *
470 * @throws \RuntimeException Unclosed strings will throw an error
471 */
472 protected function saveString()
473 {
474 $startpos = $this->index;
475 // saveString is always called after a gets cleared, so we push b into
476 // that spot.
477 $this->a = $this->b;
478 // If this isn't a string we don't need to do anything.
479 if (!isset($this->stringDelimiters[$this->a])) {
480 return;
481 }
482 // String type is the quote used, " or '
483 $stringType = $this->a;
484 // Echo out that starting quote
485 $this->echo($this->a);
486 // Loop until the string is done
487 // Grab the very next character and load it into a
488 while (($this->a = $this->getChar()) !== \false) {
489 switch ($this->a) {
490 // If the string opener (single or double quote) is used
491 // output it and break out of the while loop-
492 // The string is finished!
493 case $stringType:
494 break 2;
495 // New lines in strings without line delimiters are bad- actual
496 // new lines will be represented by the string \n and not the actual
497 // character, so those will be treated just fine using the switch
498 // block below.
499 case "\n":
500 if ($stringType === '`') {
501 $this->echo($this->a);
502 } else {
503 throw new \RuntimeException('Unclosed string at position: ' . $startpos);
504 }
505 break;
506 // Escaped characters get picked up here. If it's an escaped new line it's not really needed
507 case '\\':
508 // a is a slash. We want to keep it, and the next character,
509 // unless it's a new line. New lines as actual strings will be
510 // preserved, but escaped new lines should be reduced.
511 $this->b = $this->getChar();
512 // If b is a new line we discard a and b and restart the loop.
513 if ($this->b === "\n") {
514 break;
515 }
516 // echo out the escaped character and restart the loop.
517 $this->echo($this->a . $this->b);
518 break;
519 // Since we're not dealing with any special cases we simply
520 // output the character and continue our loop.
521 default:
522 $this->echo($this->a);
523 }
524 }
525 }
526 /**
527 * When a regular expression is detected this function crawls for the end of
528 * it and saves the whole regex.
529 *
530 * @throws \RuntimeException Unclosed regex will throw an error
531 */
532 protected function saveRegex()
533 {
534 if ($this->a != " ") {
535 $this->echo($this->a);
536 }
537 $this->echo($this->b);
538 // Flag to make sure that we don't end the regex too early because of
539 // unescaped forward slashes inside a character class. e.g /[/]/
540 // In non-v-mode, The only characters that cannot appear literally are \, ], and -
541 // In v-mode more characters are reserved and forbidden from appearing literally
542 // including but not limited to [ ] \ /
543 $character_class = \false;
544 $character_class_index = null;
545 while (($this->a = $this->getChar()) !== \false) {
546 if ($this->a === '/' && !$character_class) {
547 break;
548 }
549 if ($this->a === '[') {
550 $character_class = \true;
551 $character_class_index = $this->index;
552 } elseif ($this->a === ']') {
553 $character_class = \false;
554 }
555 if ($this->a === '\\') {
556 $this->echo($this->a);
557 $this->a = $this->getChar();
558 }
559 if ($this->a === "\n") {
560 if ($character_class) {
561 throw new \RuntimeException('Unclosed character class at position: ' . $character_class_index);
562 }
563 throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index);
564 }
565 $this->echo($this->a);
566 }
567 $this->b = $this->getReal();
568 }
569 /**
570 * Checks to see if a character is alphanumeric.
571 *
572 * @param string $char Just one character
573 * @return bool
574 */
575 protected static function isAlphaNumeric($char)
576 {
577 return preg_match('/^[\\w\\$\\pL]$/', $char) === 1 || $char == '/';
578 }
579 protected function endsInKeyword()
580 {
581 # When this function is called A is not yet assigned to output.
582 # Regular expression only needs to check final part of output for keyword.
583 $testOutput = substr($this->output . $this->a, -1 * ($this->max_keyword_len + 10));
584 foreach (static::$keywords as $keyword) {
585 if (preg_match('/[^\\w]' . $keyword . '[ ]?$/i', $testOutput) === 1) {
586 return \true;
587 }
588 }
589 return \false;
590 }
591 /**
592 * Replace patterns in the given string and store the replacement
593 *
594 * @param string $js The string to lock
595 * @return bool
596 */
597 protected function lock($js)
598 {
599 /* lock things like <code>"asd" + ++x;</code> */
600 $lock = '"LOCK---' . crc32(time()) . '"';
601 $matches = [];
602 preg_match('/([+-])(\\s+)([+-])/S', $js, $matches);
603 if (empty($matches)) {
604 return $js;
605 }
606 $this->locks[$lock] = $matches[2];
607 $js = preg_replace('/([+-])\\s+([+-])/S', "\$1{$lock}\$2", $js);
608 /* -- */
609 return $js;
610 }
611 /**
612 * Replace "locks" with the original characters
613 *
614 * @param string $js The string to unlock
615 * @return bool
616 */
617 protected function unlock($js)
618 {
619 if (empty($this->locks)) {
620 return $js;
621 }
622 foreach ($this->locks as $lock => $replacement) {
623 $js = str_replace($lock, $replacement, $js);
624 }
625 return $js;
626 }
627 }
628