diff --git a/include/thirdparty/JShrink/Minifier.php b/include/thirdparty/JShrink/Minifier.php index fad43d2..088d264 100644 --- a/include/thirdparty/JShrink/Minifier.php +++ b/include/thirdparty/JShrink/Minifier.php @@ -74,6 +74,20 @@ class Minifier */ protected $c; + /** + * This character is only active when certain look ahead actions take place. + * + * @var string + */ + protected $last_char; + + /** + * This character is only active when certain look ahead actions take place. + * + * @var string + */ + protected $output; + /** * Contains the options for the current minification process. * @@ -95,6 +109,9 @@ class Minifier */ protected static $defaultOptions = ['flaggedComments' => true]; + + protected static $keywords = ["delete", "do", "for", "in", "instanceof", "return", "typeof", "yield"]; + /** * Contains lock ids which are used to replace certain code patterns and * prevent them from being minified @@ -115,17 +132,11 @@ class Minifier public static function minify($js, $options = []) { try { - ob_start(); - $jshrink = new Minifier(); $js = $jshrink->lock($js); - $jshrink->minifyDirectToOutput($js, $options); - - // Sometimes there's a leading new line, so we trim that out here. - $js = ltrim(ob_get_clean()); + $js = ltrim($jshrink->minifyToString($js, $options)); $js = $jshrink->unlock($js); unset($jshrink); - return $js; } catch (\Exception $e) { if (isset($jshrink)) { @@ -134,9 +145,6 @@ class Minifier $jshrink->clean(); unset($jshrink); } - - // without this call things get weird, with partially outputted js. - ob_end_clean(); throw $e; } } @@ -148,11 +156,12 @@ class Minifier * @param string $js The raw javascript to be minified * @param array $options Various runtime options in an associative array */ - protected function minifyDirectToOutput($js, $options) + protected function minifyToString($js, $options) { $this->initialize($js, $options); $this->loop(); $this->clean(); + return $this->output; } /** @@ -164,7 +173,7 @@ class Minifier protected function initialize($js, $options) { $this->options = array_merge(static::$defaultOptions, $options); - $this->input = str_replace(["\r\n", '/**/', "\r"], ["\n", "", "\n"], $js); + $this->input = $js; // We add a newline to the end of the script to make it easier to deal // with comments at the bottom of the script- this prevents the unclosed @@ -177,7 +186,9 @@ class Minifier // Populate "a" with a new line, "b" with the first character, before // entering the loop $this->a = "\n"; - $this->b = $this->getReal(); + $this->b = "\n"; + $this->last_char = "\n"; + $this->output = ""; } /** @@ -192,6 +203,13 @@ class Minifier '[' => true, '@' => true]; + + protected function echo($char) { + $this->output .= $char; + $this->last_char = $char[-1]; + } + + /** * The primary action occurs here. This function loops through the input string, * outputting anything that's relevant and discarding anything that is not. @@ -201,10 +219,11 @@ class Minifier while ($this->a !== false && !is_null($this->a) && $this->a !== '') { switch ($this->a) { // new lines + case "\r": case "\n": // if the next line is something that can't stand alone preserve the newline if ($this->b !== false && isset($this->noNewLineCharacters[$this->b])) { - echo $this->a; + $this->echo($this->a); $this->saveString(); break; } @@ -220,7 +239,7 @@ class Minifier // no break case ' ': if (static::isAlphaNumeric($this->b)) { - echo $this->a; + $this->echo($this->a); } $this->saveString(); @@ -228,14 +247,15 @@ class Minifier default: switch ($this->b) { + case "\r": case "\n": if (strpos('}])+-"\'', $this->a) !== false) { - echo $this->a; + $this->echo($this->a); $this->saveString(); break; } else { if (static::isAlphaNumeric($this->a)) { - echo $this->a; + $this->echo($this->a); $this->saveString(); } } @@ -254,7 +274,7 @@ class Minifier continue 3; } - echo $this->a; + $this->echo($this->a); $this->saveString(); break; } @@ -263,9 +283,27 @@ class Minifier // do reg check of doom $this->b = $this->getReal(); - if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) { - $this->saveRegex(); + if ($this->b == '/') { + $valid_tokens = "(,=:[!&|?\n"; + + # Find last "real" token, excluding spaces. + $last_token = $this->a; + if ($last_token == " ") { + $last_token = $this->last_char; + } + + if (strpos($valid_tokens, $last_token) !== false) { + // Regex can appear unquoted after these symbols + $this->saveRegex(); + } else if ($this->endsInKeyword()) { + // This block checks for the "return" token before the slash. + $this->saveRegex(); + } } + + // if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) { + // $this->saveRegex(); + // } } } @@ -308,6 +346,12 @@ class Minifier $this->index++; } + # Convert all line endings to unix standard. + # `\r\n` converts to `\n\n` and is minified. + if ($char == "\r") { + $char = "\n"; + } + // Normalize all whitespace except for the newline character into a // standard space. if ($char !== "\n" && $char < "\x20") { @@ -317,6 +361,36 @@ class Minifier return $char; } + /** + * This function returns the next character without moving the index forward. + * + * + * @return string The next character + * @throws \RuntimeException + */ + protected function peek() + { + if ($this->index >= $this->len) { + return false; + } + + $char = $this->input[$this->index]; + # Convert all line endings to unix standard. + # `\r\n` converts to `\n\n` and is minified. + if ($char == "\r") { + $char = "\n"; + } + + // Normalize all whitespace except for the newline character into a + // standard space. + if ($char !== "\n" && $char < "\x20") { + return ' '; + } + + # Return the next character but don't push the index. + return $char; + } + /** * This function gets the next "real" character. It is essentially a wrapper * around the getChar function that skips comments. This has significant @@ -387,6 +461,15 @@ class Minifier $this->getChar(); // current C $thirdCommentString = $this->getChar(); + // Detect a completely empty comment, ie `/**/` + if ($thirdCommentString == "*") { + $peekChar = $this->peek(); + if ($peekChar == "/") { + $this->index++; + return; + } + } + // kill everything up to the next */ if it's there if ($this->getNext('*/')) { $this->getChar(); // get * @@ -400,17 +483,17 @@ class Minifier // If conditional comments or flagged comments are not the first thing in the script // we need to echo a and fill it with a space before moving on. if ($startIndex > 0) { - echo $this->a; + $this->echo($this->a); $this->a = " "; // If the comment started on a new line we let it stay on the new line if ($this->input[($startIndex - 1)] === "\n") { - echo "\n"; + $this->echo("\n"); } } $endPoint = ($this->index - 1) - $startIndex; - echo substr($this->input, $startIndex, $endPoint); + $this->echo(substr($this->input, $startIndex, $endPoint)); $this->c = $char; @@ -476,7 +559,7 @@ class Minifier $stringType = $this->a; // Echo out that starting quote - echo $this->a; + $this->echo($this->a); // Loop until the string is done // Grab the very next character and load it into a @@ -495,7 +578,7 @@ class Minifier // block below. case "\n": if ($stringType === '`') { - echo $this->a; + $this->echo($this->a); } else { throw new \RuntimeException('Unclosed string at position: ' . $startpos); } @@ -515,14 +598,14 @@ class Minifier } // echo out the escaped character and restart the loop. - echo $this->a . $this->b; + $this->echo($this->a . $this->b); break; // Since we're not dealing with any special cases we simply // output the character and continue our loop. default: - echo $this->a; + $this->echo($this->a); } } } @@ -535,7 +618,11 @@ class Minifier */ protected function saveRegex() { - echo $this->a . $this->b; + if ($this->a != " ") { + $this->echo($this->a); + } + + $this->echo($this->b); while (($this->a = $this->getChar()) !== false) { if ($this->a === '/') { @@ -543,7 +630,7 @@ class Minifier } if ($this->a === '\\') { - echo $this->a; + $this->echo($this->a); $this->a = $this->getChar(); } @@ -551,7 +638,7 @@ class Minifier throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index); } - echo $this->a; + $this->echo($this->a); } $this->b = $this->getReal(); } @@ -567,6 +654,21 @@ class Minifier return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/'; } + protected function endsInKeyword() { + + # When this function is called A is not yet assigned to output. + $testOutput = $this->output . $this->a; + + foreach(static::$keywords as $keyword) { + if (preg_match('/[^\w]'.$keyword.'[ ]?$/i', $testOutput) === 1) { + return true; + } + } + return false; + } + + + /** * Replace patterns in the given string and store the replacement * diff --git a/include/thirdparty/JShrink/Minifier1.2.php b/include/thirdparty/JShrink/Minifier1.4.php similarity index 100% rename from include/thirdparty/JShrink/Minifier1.2.php rename to include/thirdparty/JShrink/Minifier1.4.php