PluginProbe ʕ •ᴥ•ʔ
Ocean Extra / 2.5.7
Ocean Extra v2.5.7
2.5.7 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.4.1 1.1.4.2 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.0.1 1.2.1 1.2.1.1 1.2.1.2 1.2.10 1.2.2 1.2.2.1 1.2.2.2 1.2.2.3 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.10 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 1.4.14 1.4.15 1.4.16 1.4.17 1.4.18 1.4.19 1.4.2 1.4.20 1.4.21 1.4.22 1.4.23 1.4.24 1.4.25 1.4.26 1.4.27 1.4.28 1.4.29 1.4.3 1.4.30 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.12 1.5.13 1.5.14 1.5.15 1.5.16 1.5.17 1.5.18 1.5.19 1.5.2 1.5.20 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.3.1 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6
ocean-extra / includes / jshrink.php
ocean-extra / includes Last commit date
admin-bar 1 year ago client-migration 7 months ago compatibility 1 year ago customizer 1 day ago freemius 9 months ago menu-icons 1 year ago metabox 1 year ago onboarding 1 month ago panel 10 months ago post-settings 3 months ago preloader 1 year ago shortcodes 1 year ago themepanel 1 year ago widgets 1 day ago wizard 3 years ago adobe-font.php 1 year ago custom-code.php 9 months ago dashboard.php 1 year ago image-resizer.php 1 year ago jshrink.php 3 years ago mautic.php 3 months ago ocean-extra-strings.php 3 years ago plugins-tab.php 1 year ago update-message.php 1 year ago utils.php 1 year ago walker.php 4 years ago
jshrink.php
721 lines
1 <?php
2 /*
3 * This file is part of the JShrink package.
4 *
5 * (c) Robert Hafner <tedivm@tedivm.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11 /**
12 * JShrink
13 *
14 *
15 * @package JShrink
16 * @author Robert Hafner <tedivm@tedivm.com>
17 */
18
19 namespace OceanWP;
20
21 // Exit if accessed directly.
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 /**
27 * Minifier
28 *
29 * Usage - Minifier::minify($js);
30 * Usage - Minifier::minify($js, $options);
31 * Usage - Minifier::minify($js, array('flaggedComments' => false));
32 *
33 * @package JShrink
34 * @author Robert Hafner <tedivm@tedivm.com>
35 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
36 */
37 class Minifier
38 {
39 /**
40 * The input javascript to be minified.
41 *
42 * @var string
43 */
44 protected $input;
45
46 /**
47 * Length of input javascript.
48 *
49 * @var int
50 */
51 protected $len = 0;
52
53 /**
54 * The location of the character (in the input string) that is next to be
55 * processed.
56 *
57 * @var int
58 */
59 protected $index = 0;
60
61 /**
62 * The first of the characters currently being looked at.
63 *
64 * @var string
65 */
66 protected $a = '';
67
68 /**
69 * The next character being looked at (after a);
70 *
71 * @var string
72 */
73 protected $b = '';
74
75 /**
76 * This character is only active when certain look ahead actions take place.
77 *
78 * @var string
79 */
80 protected $c;
81
82 /**
83 * This character is only active when certain look ahead actions take place.
84 *
85 * @var string
86 */
87 protected $last_char;
88
89 /**
90 * This character is only active when certain look ahead actions take place.
91 *
92 * @var string
93 */
94 protected $output;
95
96 /**
97 * Contains the options for the current minification process.
98 *
99 * @var array
100 */
101 protected $options;
102
103 /**
104 * These characters are used to define strings.
105 */
106 protected $stringDelimiters = ['\'' => true, '"' => true, '`' => true];
107
108 /**
109 * Contains the default options for minification. This array is merged with
110 * the one passed in by the user to create the request specific set of
111 * options (stored in the $options attribute).
112 *
113 * @var array
114 */
115 protected static $defaultOptions = ['flaggedComments' => true];
116
117
118 protected static $keywords = ["delete", "do", "for", "in", "instanceof", "return", "typeof", "yield"];
119
120 /**
121 * Contains lock ids which are used to replace certain code patterns and
122 * prevent them from being minified
123 *
124 * @var array
125 */
126 protected $locks = [];
127
128 /**
129 * Takes a string containing javascript and removes unneeded characters in
130 * order to shrink the code without altering it's functionality.
131 *
132 * @param string $js The raw javascript to be minified
133 * @param array $options Various runtime options in an associative array
134 * @throws \Exception
135 * @return bool|string
136 */
137 public static function minify($js, $options = [])
138 {
139 try {
140 $jshrink = new Minifier();
141 $js = $jshrink->lock($js);
142 $js = ltrim($jshrink->minifyToString($js, $options));
143 $js = $jshrink->unlock($js);
144 unset($jshrink);
145 return $js;
146 } catch (\Exception $e) {
147 if (isset($jshrink)) {
148 // Since the breakdownScript function probably wasn't finished
149 // we clean it out before discarding it.
150 $jshrink->clean();
151 unset($jshrink);
152 }
153 throw $e;
154 }
155 }
156
157 /**
158 * Processes a javascript string and outputs only the required characters,
159 * stripping out all unneeded characters.
160 *
161 * @param string $js The raw javascript to be minified
162 * @param array $options Various runtime options in an associative array
163 */
164 protected function minifyToString($js, $options)
165 {
166 $this->initialize($js, $options);
167 $this->loop();
168 $this->clean();
169 return $this->output;
170 }
171
172 /**
173 * Initializes internal variables, normalizes new lines,
174 *
175 * @param string $js The raw javascript to be minified
176 * @param array $options Various runtime options in an associative array
177 */
178 protected function initialize($js, $options)
179 {
180 $this->options = array_merge(static::$defaultOptions, $options);
181 $this->input = $js;
182
183 // We add a newline to the end of the script to make it easier to deal
184 // with comments at the bottom of the script- this prevents the unclosed
185 // comment error that can otherwise occur.
186 $this->input .= PHP_EOL;
187
188 // save input length to skip calculation every time
189 $this->len = strlen($this->input);
190
191 // Populate "a" with a new line, "b" with the first character, before
192 // entering the loop
193 $this->a = "\n";
194 $this->b = "\n";
195 $this->last_char = "\n";
196 $this->output = "";
197 }
198
199 /**
200 * Characters that can't stand alone preserve the newline.
201 *
202 * @var array
203 */
204 protected $noNewLineCharacters = [
205 '(' => true,
206 '-' => true,
207 '+' => true,
208 '[' => true,
209 '@' => true];
210
211
212 protected function echo($char) {
213 $this->output .= $char;
214 $this->last_char = $char[-1];
215 }
216
217
218 /**
219 * The primary action occurs here. This function loops through the input string,
220 * outputting anything that's relevant and discarding anything that is not.
221 */
222 protected function loop()
223 {
224 while ($this->a !== false && !is_null($this->a) && $this->a !== '') {
225 switch ($this->a) {
226 // new lines
227 case "\r":
228 case "\n":
229 // if the next line is something that can't stand alone preserve the newline
230 if ($this->b !== false && isset($this->noNewLineCharacters[$this->b])) {
231 $this->echo($this->a);
232 $this->saveString();
233 break;
234 }
235
236 // if B is a space we skip the rest of the switch block and go down to the
237 // string/regex check below, resetting $this->b with getReal
238 if ($this->b === ' ') {
239 break;
240 }
241
242 // otherwise we treat the newline like a space
243
244 // no break
245 case ' ':
246 if (static::isAlphaNumeric($this->b)) {
247 $this->echo($this->a);
248 }
249
250 $this->saveString();
251 break;
252
253 default:
254 switch ($this->b) {
255 case "\r":
256 case "\n":
257 if (strpos('}])+-"\'', $this->a) !== false) {
258 $this->echo($this->a);
259 $this->saveString();
260 break;
261 } else {
262 if (static::isAlphaNumeric($this->a)) {
263 $this->echo($this->a);
264 $this->saveString();
265 }
266 }
267 break;
268
269 case ' ':
270 if (!static::isAlphaNumeric($this->a)) {
271 break;
272 }
273
274 // no break
275 default:
276 // check for some regex that breaks stuff
277 if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
278 $this->saveRegex();
279 continue 3;
280 }
281
282 $this->echo($this->a);
283 $this->saveString();
284 break;
285 }
286 }
287
288 // do reg check of doom
289 $this->b = $this->getReal();
290
291 if ($this->b == '/') {
292 $valid_tokens = "(,=:[!&|?\n";
293
294 # Find last "real" token, excluding spaces.
295 $last_token = $this->a;
296 if ($last_token == " ") {
297 $last_token = $this->last_char;
298 }
299
300 if (strpos($valid_tokens, $last_token) !== false) {
301 // Regex can appear unquoted after these symbols
302 $this->saveRegex();
303 } else if ($this->endsInKeyword()) {
304 // This block checks for the "return" token before the slash.
305 $this->saveRegex();
306 }
307 }
308
309 // if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) {
310 // $this->saveRegex();
311 // }
312 }
313 }
314
315 /**
316 * Resets attributes that do not need to be stored between requests so that
317 * the next request is ready to go. Another reason for this is to make sure
318 * the variables are cleared and are not taking up memory.
319 */
320 protected function clean()
321 {
322 unset($this->input);
323 $this->len = 0;
324 $this->index = 0;
325 $this->a = $this->b = '';
326 unset($this->c);
327 unset($this->options);
328 }
329
330 /**
331 * Returns the next string for processing based off of the current index.
332 *
333 * @return string
334 */
335 protected function getChar()
336 {
337 // Check to see if we had anything in the look ahead buffer and use that.
338 if (isset($this->c)) {
339 $char = $this->c;
340 unset($this->c);
341 } else {
342 // Otherwise we start pulling from the input.
343 $char = $this->index < $this->len ? $this->input[$this->index] : false;
344
345 // If the next character doesn't exist return false.
346 if (isset($char) && $char === false) {
347 return false;
348 }
349
350 // Otherwise increment the pointer and use this char.
351 $this->index++;
352 }
353
354 # Convert all line endings to unix standard.
355 # `\r\n` converts to `\n\n` and is minified.
356 if ($char == "\r") {
357 $char = "\n";
358 }
359
360 // Normalize all whitespace except for the newline character into a
361 // standard space.
362 if ($char !== "\n" && $char < "\x20") {
363 return ' ';
364 }
365
366 return $char;
367 }
368
369 /**
370 * This function returns the next character without moving the index forward.
371 *
372 *
373 * @return string The next character
374 * @throws \RuntimeException
375 */
376 protected function peek()
377 {
378 if ($this->index >= $this->len) {
379 return false;
380 }
381
382 $char = $this->input[$this->index];
383 # Convert all line endings to unix standard.
384 # `\r\n` converts to `\n\n` and is minified.
385 if ($char == "\r") {
386 $char = "\n";
387 }
388
389 // Normalize all whitespace except for the newline character into a
390 // standard space.
391 if ($char !== "\n" && $char < "\x20") {
392 return ' ';
393 }
394
395 # Return the next character but don't push the index.
396 return $char;
397 }
398
399 /**
400 * This function gets the next "real" character. It is essentially a wrapper
401 * around the getChar function that skips comments. This has significant
402 * performance benefits as the skipping is done using native functions (ie,
403 * c code) rather than in script php.
404 *
405 *
406 * @return string Next 'real' character to be processed.
407 * @throws \RuntimeException
408 */
409 protected function getReal()
410 {
411 $startIndex = $this->index;
412 $char = $this->getChar();
413
414 // Check to see if we're potentially in a comment
415 if ($char !== '/') {
416 return $char;
417 }
418
419 $this->c = $this->getChar();
420
421 if ($this->c === '/') {
422 $this->processOneLineComments($startIndex);
423
424 return $this->getReal();
425 } elseif ($this->c === '*') {
426 $this->processMultiLineComments($startIndex);
427
428 return $this->getReal();
429 }
430
431 return $char;
432 }
433
434 /**
435 * Removed one line comments, with the exception of some very specific types of
436 * conditional comments.
437 *
438 * @param int $startIndex The index point where "getReal" function started
439 * @return void
440 */
441 protected function processOneLineComments($startIndex)
442 {
443 $thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : false;
444
445 // kill rest of line
446 $this->getNext("\n");
447
448 unset($this->c);
449
450 if ($thirdCommentString == '@') {
451 $endPoint = $this->index - $startIndex;
452 $this->c = "\n" . substr($this->input, $startIndex, $endPoint);
453 }
454 }
455
456 /**
457 * Skips multiline comments where appropriate, and includes them where needed.
458 * Conditional comments and "license" style blocks are preserved.
459 *
460 * @param int $startIndex The index point where "getReal" function started
461 * @return void
462 * @throws \RuntimeException Unclosed comments will throw an error
463 */
464 protected function processMultiLineComments($startIndex)
465 {
466 $this->getChar(); // current C
467 $thirdCommentString = $this->getChar();
468
469 // Detect a completely empty comment, ie `/**/`
470 if ($thirdCommentString == "*") {
471 $peekChar = $this->peek();
472 if ($peekChar == "/") {
473 $this->index++;
474 return;
475 }
476 }
477
478 // kill everything up to the next */ if it's there
479 if ($this->getNext('*/')) {
480 $this->getChar(); // get *
481 $this->getChar(); // get /
482 $char = $this->getChar(); // get next real character
483
484 // Now we reinsert conditional comments and YUI-style licensing comments
485 if (($this->options['flaggedComments'] && $thirdCommentString === '!')
486 || ($thirdCommentString === '@')) {
487
488 // If conditional comments or flagged comments are not the first thing in the script
489 // we need to echo a and fill it with a space before moving on.
490 if ($startIndex > 0) {
491 $this->echo($this->a);
492 $this->a = " ";
493
494 // If the comment started on a new line we let it stay on the new line
495 if ($this->input[($startIndex - 1)] === "\n") {
496 $this->echo("\n");
497 }
498 }
499
500 $endPoint = ($this->index - 1) - $startIndex;
501 $this->echo(substr($this->input, $startIndex, $endPoint));
502
503 $this->c = $char;
504
505 return;
506 }
507 } else {
508 $char = false;
509 }
510
511 if ($char === false) {
512 throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2));
513 }
514
515 // if we're here c is part of the comment and therefore tossed
516 $this->c = $char;
517 }
518
519 /**
520 * Pushes the index ahead to the next instance of the supplied string. If it
521 * is found the first character of the string is returned and the index is set
522 * to it's position.
523 *
524 * @param string $string
525 * @return string|false Returns the first character of the string or false.
526 */
527 protected function getNext($string)
528 {
529 // Find the next occurrence of "string" after the current position.
530 $pos = strpos($this->input, $string, $this->index);
531
532 // If it's not there return false.
533 if ($pos === false) {
534 return false;
535 }
536
537 // Adjust position of index to jump ahead to the asked for string
538 $this->index = $pos;
539
540 // Return the first character of that string.
541 return $this->index < $this->len ? $this->input[$this->index] : false;
542 }
543
544 /**
545 * When a javascript string is detected this function crawls for the end of
546 * it and saves the whole string.
547 *
548 * @throws \RuntimeException Unclosed strings will throw an error
549 */
550 protected function saveString()
551 {
552 $startpos = $this->index;
553
554 // saveString is always called after a gets cleared, so we push b into
555 // that spot.
556 $this->a = $this->b;
557
558 // If this isn't a string we don't need to do anything.
559 if (!isset($this->stringDelimiters[$this->a])) {
560 return;
561 }
562
563 // String type is the quote used, " or '
564 $stringType = $this->a;
565
566 // Echo out that starting quote
567 $this->echo($this->a);
568
569 // Loop until the string is done
570 // Grab the very next character and load it into a
571 while (($this->a = $this->getChar()) !== false) {
572 switch ($this->a) {
573
574 // If the string opener (single or double quote) is used
575 // output it and break out of the while loop-
576 // The string is finished!
577 case $stringType:
578 break 2;
579
580 // New lines in strings without line delimiters are bad- actual
581 // new lines will be represented by the string \n and not the actual
582 // character, so those will be treated just fine using the switch
583 // block below.
584 case "\n":
585 if ($stringType === '`') {
586 $this->echo($this->a);
587 } else {
588 throw new \RuntimeException('Unclosed string at position: ' . $startpos);
589 }
590 break;
591
592 // Escaped characters get picked up here. If it's an escaped new line it's not really needed
593 case '\\':
594
595 // a is a slash. We want to keep it, and the next character,
596 // unless it's a new line. New lines as actual strings will be
597 // preserved, but escaped new lines should be reduced.
598 $this->b = $this->getChar();
599
600 // If b is a new line we discard a and b and restart the loop.
601 if ($this->b === "\n") {
602 break;
603 }
604
605 // echo out the escaped character and restart the loop.
606 $this->echo($this->a . $this->b);
607 break;
608
609
610 // Since we're not dealing with any special cases we simply
611 // output the character and continue our loop.
612 default:
613 $this->echo($this->a);
614 }
615 }
616 }
617
618 /**
619 * When a regular expression is detected this function crawls for the end of
620 * it and saves the whole regex.
621 *
622 * @throws \RuntimeException Unclosed regex will throw an error
623 */
624 protected function saveRegex()
625 {
626 if ($this->a != " ") {
627 $this->echo($this->a);
628 }
629
630 $this->echo($this->b);
631
632 while (($this->a = $this->getChar()) !== false) {
633 if ($this->a === '/') {
634 break;
635 }
636
637 if ($this->a === '\\') {
638 $this->echo($this->a);
639 $this->a = $this->getChar();
640 }
641
642 if ($this->a === "\n") {
643 throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index);
644 }
645
646 $this->echo($this->a);
647 }
648 $this->b = $this->getReal();
649 }
650
651 /**
652 * Checks to see if a character is alphanumeric.
653 *
654 * @param string $char Just one character
655 * @return bool
656 */
657 protected static function isAlphaNumeric($char)
658 {
659 return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
660 }
661
662 protected function endsInKeyword() {
663
664 # When this function is called A is not yet assigned to output.
665 $testOutput = $this->output . $this->a;
666
667 foreach(static::$keywords as $keyword) {
668 if (preg_match('/[^\w]'.$keyword.'[ ]?$/i', $testOutput) === 1) {
669 return true;
670 }
671 }
672 return false;
673 }
674
675
676
677 /**
678 * Replace patterns in the given string and store the replacement
679 *
680 * @param string $js The string to lock
681 * @return bool
682 */
683 protected function lock($js)
684 {
685 /* lock things like <code>"asd" + ++x;</code> */
686 $lock = '"LOCK---' . crc32(time()) . '"';
687
688 $matches = [];
689 preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
690 if (empty($matches)) {
691 return $js;
692 }
693
694 $this->locks[$lock] = $matches[2];
695
696 $js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
697 /* -- */
698
699 return $js;
700 }
701
702 /**
703 * Replace "locks" with the original characters
704 *
705 * @param string $js The string to unlock
706 * @return bool
707 */
708 protected function unlock($js)
709 {
710 if (empty($this->locks)) {
711 return $js;
712 }
713
714 foreach ($this->locks as $lock => $replacement) {
715 $js = str_replace($lock, $replacement, $js);
716 }
717
718 return $js;
719 }
720 }
721