fixes #951 update JShrink to version 1.4.0

compatibility with PHP 8.0 but no longer to <5.6 (even if it still work on a 5.4 installation)
This commit is contained in:
plegall 2021-08-02 19:15:53 +02:00
parent a516a5e945
commit c64efe6ecb
2 changed files with 522 additions and 378 deletions

View file

@ -1,469 +1,613 @@
<?php <?php
/*
* This file is part of the JShrink package.
*
* (c) Robert Hafner <tedivm@tedivm.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/** /**
* JShrink * JShrink
* *
* Copyright (c) 2009-2012, Robert Hafner <tedivm@tedivm.com>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Robert Hafner nor the names of his
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* *
* @package JShrink * @package JShrink
* @author Robert Hafner <tedivm@tedivm.com> * @author Robert Hafner <tedivm@tedivm.com>
* @copyright 2009-2012 Robert Hafner <tedivm@tedivm.com>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @link https://github.com/tedivm/JShrink
* @version Release: 0.5.1
*/ */
namespace JShrink;
/** /**
* JShrink_Minifier * Minifier
* *
* Usage - JShrink_Minifier::minify($js); * Usage - Minifier::minify($js);
* Usage - JShrink_Minifier::minify($js, $options); * Usage - Minifier::minify($js, $options);
* Usage - JShrink_Minifier::minify($js, array('flaggedComments' => false)); * Usage - Minifier::minify($js, array('flaggedComments' => false));
* *
* @package JShrink * @package JShrink
* @author Robert Hafner <tedivm@tedivm.com> * @author Robert Hafner <tedivm@tedivm.com>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License * @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/ */
class JShrink_Minifier class Minifier
{ {
/** /**
* The input javascript to be minified. * The input javascript to be minified.
* *
* @var string * @var string
*/ */
protected $input; protected $input;
/** /**
* The location of the character (in the input string) that is next to be * Length of input javascript.
*
* @var int
*/
protected $len = 0;
/**
* The location of the character (in the input string) that is next to be
* processed. * processed.
* *
* @var int * @var int
*/ */
protected $index = 0; protected $index = 0;
/** /**
* The first of the characters currently being looked at. * The first of the characters currently being looked at.
* *
* @var string * @var string
*/ */
protected $a = ''; protected $a = '';
/**
* The next character being looked at (after a);
*
* @var string
*/
protected $b = '';
/** /**
* The next character being looked at (after a); * This character is only active when certain look ahead actions take place.
* *
* @var string * @var string
*/ */
protected $b = ''; protected $c;
/** /**
* This character is only active when certain look ahead actions take place. * Contains the options for the current minification process.
* *
* @var string * @var array
*/ */
protected $c; protected $options;
/** /**
* Contains the options for the current minification process. * These characters are used to define strings.
* */
* @var array protected $stringDelimiters = ['\'' => true, '"' => true, '`' => true];
*/
protected $options;
/** /**
* Contains the default options for minification. This array is merged with * Contains the default options for minification. This array is merged with
* the one passed in by the user to create the request specific set of * the one passed in by the user to create the request specific set of
* options (stored in the $options attribute). * options (stored in the $options attribute).
* *
* @var array * @var array
*/ */
static protected $defaultOptions = array('flaggedComments' => true); protected static $defaultOptions = ['flaggedComments' => true];
/** /**
* Contains a copy of the JShrink object used to run minification. This is * Contains lock ids which are used to replace certain code patterns and
* only used internally, and is only stored for performance reasons. There * prevent them from being minified
* is no internal data shared between minification requests. *
*/ * @var array
static protected $jshrink; */
protected $locks = [];
/** /**
* Minifier::minify takes a string containing javascript and removes * Takes a string containing javascript and removes unneeded characters in
* unneeded characters in order to shrink the code without altering it's * order to shrink the code without altering it's functionality.
* functionality. *
*/ * @param string $js The raw javascript to be minified
static public function minify($js, $options = array()) * @param array $options Various runtime options in an associative array
{ * @throws \Exception
try{ * @return bool|string
ob_start(); */
$currentOptions = array_merge(self::$defaultOptions, $options); public static function minify($js, $options = [])
{
try {
ob_start();
if(!isset(self::$jshrink)) $jshrink = new Minifier();
self::$jshrink = new JShrink_Minifier(); $js = $jshrink->lock($js);
$jshrink->minifyDirectToOutput($js, $options);
self::$jshrink->breakdownScript($js, $currentOptions); // Sometimes there's a leading new line, so we trim that out here.
return ob_get_clean(); $js = ltrim(ob_get_clean());
$js = $jshrink->unlock($js);
unset($jshrink);
}catch(Exception $e){ return $js;
if(isset(self::$jshrink)) } catch (\Exception $e) {
self::$jshrink->clean(); if (isset($jshrink)) {
// Since the breakdownScript function probably wasn't finished
// we clean it out before discarding it.
$jshrink->clean();
unset($jshrink);
}
ob_end_clean(); // without this call things get weird, with partially outputted js.
throw $e; ob_end_clean();
} throw $e;
} }
}
/** /**
* Processes a javascript string and outputs only the required characters, * Processes a javascript string and outputs only the required characters,
* stripping out all unneeded characters. * stripping out all unneeded characters.
* *
* @param string $js The raw javascript to be minified * @param string $js The raw javascript to be minified
* @param array $currentOptions Various runtime options in an associative array * @param array $options Various runtime options in an associative array
*/ */
protected function breakdownScript($js, $currentOptions) protected function minifyDirectToOutput($js, $options)
{ {
// reset work attributes in case this isn't the first run. $this->initialize($js, $options);
$this->clean(); $this->loop();
$this->clean();
}
$this->options = $currentOptions; /**
* Initializes internal variables, normalizes new lines,
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
*/
protected function initialize($js, $options)
{
$this->options = array_merge(static::$defaultOptions, $options);
$this->input = str_replace(["\r\n", '/**/', "\r"], ["\n", "", "\n"], $js);
$js = str_replace("\r\n", "\n", $js); // We add a newline to the end of the script to make it easier to deal
$this->input = str_replace("\r", "\n", $js); // with comments at the bottom of the script- this prevents the unclosed
// comment error that can otherwise occur.
$this->input .= PHP_EOL;
// save input length to skip calculation every time
$this->len = strlen($this->input);
$this->a = $this->getReal(); // Populate "a" with a new line, "b" with the first character, before
// entering the loop
$this->a = "\n";
$this->b = $this->getReal();
}
// the only time the length can be higher than 1 is if a conditional /**
// comment needs to be displayed and the only time that can happen for * Characters that can't stand alone preserve the newline.
// $a is on the very first run *
while(strlen($this->a) > 1) * @var array
{ */
echo $this->a; protected $noNewLineCharacters = [
$this->a = $this->getReal(); '(' => true,
} '-' => true,
'+' => true,
'[' => true,
'@' => true];
$this->b = $this->getReal(); /**
* The primary action occurs here. This function loops through the input string,
* outputting anything that's relevant and discarding anything that is not.
*/
protected function loop()
{
while ($this->a !== false && !is_null($this->a) && $this->a !== '') {
switch ($this->a) {
// new lines
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->saveString();
break;
}
while($this->a !== false && !is_null($this->a) && $this->a !== '') // if B is a space we skip the rest of the switch block and go down to the
{ // string/regex check below, resetting $this->b with getReal
if ($this->b === ' ') {
break;
}
// now we give $b the same check for conditional comments we gave $a // otherwise we treat the newline like a space
// before we began looping
if(strlen($this->b) > 1)
{
echo $this->a . $this->b;
$this->a = $this->getReal();
$this->b = $this->getReal();
continue;
}
switch($this->a) // no break
{ case ' ':
// new lines if (static::isAlphaNumeric($this->b)) {
case "\n": echo $this->a;
// if the next line is something that can't stand alone }
// preserve the newline
if($this->b !== false && strpos('(-+{[@', $this->b) !== false)
{
echo $this->a;
$this->saveString();
break;
}
// if its a space we move down to the string test below $this->saveString();
if($this->b === ' ') break;
break;
// otherwise we treat the newline like a space default:
switch ($this->b) {
case "\n":
if (strpos('}])+-"\'', $this->a) !== false) {
echo $this->a;
$this->saveString();
break;
} else {
if (static::isAlphaNumeric($this->a)) {
echo $this->a;
$this->saveString();
}
}
break;
case ' ': case ' ':
if(self::isAlphaNumeric($this->b)) if (!static::isAlphaNumeric($this->a)) {
echo $this->a; break;
}
$this->saveString(); // no break
break; default:
// check for some regex that breaks stuff
if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
$this->saveRegex();
continue 3;
}
default: echo $this->a;
switch($this->b) $this->saveString();
{ break;
case "\n": }
if(strpos('}])+-"\'', $this->a) !== false) }
{
echo $this->a;
$this->saveString();
break;
}else{
if(self::isAlphaNumeric($this->a))
{
echo $this->a;
$this->saveString();
}
}
break;
case ' ': // do reg check of doom
if(!self::isAlphaNumeric($this->a)) $this->b = $this->getReal();
break;
default: if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) {
// check for some regex that breaks stuff $this->saveRegex();
if($this->a == '/' && ($this->b == '\'' || $this->b == '"')) }
{ }
$this->saveRegex(); }
continue 3;
}
echo $this->a; /**
$this->saveString(); * Resets attributes that do not need to be stored between requests so that
break; * the next request is ready to go. Another reason for this is to make sure
} * the variables are cleared and are not taking up memory.
} */
protected function clean()
{
unset($this->input);
$this->len = 0;
$this->index = 0;
$this->a = $this->b = '';
unset($this->c);
unset($this->options);
}
// do reg check of doom /**
$this->b = $this->getReal(); * Returns the next string for processing based off of the current index.
*
* @return string
*/
protected function getChar()
{
// Check to see if we had anything in the look ahead buffer and use that.
if (isset($this->c)) {
$char = $this->c;
unset($this->c);
} else {
// Otherwise we start pulling from the input.
$char = $this->index < $this->len ? $this->input[$this->index] : false;
if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) // If the next character doesn't exist return false.
$this->saveRegex(); if (isset($char) && $char === false) {
} return false;
$this->clean(); }
}
/** // Otherwise increment the pointer and use this char.
* Returns the next string for processing based off of the current index. $this->index++;
* }
* @return string
*/
protected function getChar()
{
if(isset($this->c))
{
$char = $this->c;
unset($this->c);
}else{
$tchar = substr($this->input, $this->index, 1);
if(isset($tchar) && $tchar !== false)
{
$char = $tchar;
$this->index++;
}else{
return false;
}
}
if($char !== "\n" && ord($char) < 32) // Normalize all whitespace except for the newline character into a
return ' '; // standard space.
if ($char !== "\n" && $char < "\x20") {
return ' ';
}
return $char; return $char;
} }
/** /**
* This function gets the next "real" character. It is essentially a wrapper * This function gets the next "real" character. It is essentially a wrapper
* around the getChar function that skips comments. This has significant * around the getChar function that skips comments. This has significant
* performance benefits as the skipping is done using native functions (ie, * performance benefits as the skipping is done using native functions (ie,
* c code) rather than in script php. * c code) rather than in script php.
* *
* @return string Next 'real' character to be processed. *
*/ * @return string Next 'real' character to be processed.
protected function getReal() * @throws \RuntimeException
{ */
$startIndex = $this->index; protected function getReal()
$char = $this->getChar(); {
$startIndex = $this->index;
$char = $this->getChar();
if($char == '/') // Check to see if we're potentially in a comment
{ if ($char !== '/') {
$this->c = $this->getChar(); return $char;
}
if($this->c == '/') $this->c = $this->getChar();
{
$thirdCommentString = substr($this->input, $this->index, 1);
// kill rest of line if ($this->c === '/') {
$char = $this->getNext("\n"); $this->processOneLineComments($startIndex);
if($thirdCommentString == '@') return $this->getReal();
{ } elseif ($this->c === '*') {
$endPoint = ($this->index) - $startIndex; $this->processMultiLineComments($startIndex);
unset($this->c);
$char = "\n" . substr($this->input, $startIndex, $endPoint);
}else{
$char = $this->getChar();
$char = $this->getChar();
}
}elseif($this->c == '*'){ return $this->getReal();
}
$this->getChar(); // current C return $char;
$thirdCommentString = $this->getChar(); }
if($thirdCommentString == '@') /**
{ * Removed one line comments, with the exception of some very specific types of
// conditional comment * conditional comments.
*
* @param int $startIndex The index point where "getReal" function started
* @return void
*/
protected function processOneLineComments($startIndex)
{
$thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : false;
// we're gonna back up a bit and and send the comment back, // kill rest of line
// where the first char will be echoed and the rest will be $this->getNext("\n");
// treated like a string
$this->index = $this->index-2;
return '/';
}elseif($this->getNext('*/')){ unset($this->c);
// kill everything up to the next */
$this->getChar(); // get * if ($thirdCommentString == '@') {
$this->getChar(); // get / $endPoint = $this->index - $startIndex;
$this->c = "\n" . substr($this->input, $startIndex, $endPoint);
}
}
$char = $this->getChar(); // get next real character /**
* Skips multiline comments where appropriate, and includes them where needed.
* Conditional comments and "license" style blocks are preserved.
*
* @param int $startIndex The index point where "getReal" function started
* @return void
* @throws \RuntimeException Unclosed comments will throw an error
*/
protected function processMultiLineComments($startIndex)
{
$this->getChar(); // current C
$thirdCommentString = $this->getChar();
// if YUI-style comments are enabled we reinsert it into the stream // kill everything up to the next */ if it's there
if($this->options['flaggedComments'] && $thirdCommentString == '!') if ($this->getNext('*/')) {
{ $this->getChar(); // get *
$endPoint = ($this->index - 1) - $startIndex; $this->getChar(); // get /
echo "\n" . substr($this->input, $startIndex, $endPoint) . "\n"; $char = $this->getChar(); // get next real character
}
}else{ // Now we reinsert conditional comments and YUI-style licensing comments
$char = false; if (($this->options['flaggedComments'] && $thirdCommentString === '!')
} || ($thirdCommentString === '@')) {
if($char === false) // If conditional comments or flagged comments are not the first thing in the script
throw new RuntimeException('Stray comment. ' . $this->index); // we need to echo a and fill it with a space before moving on.
if ($startIndex > 0) {
echo $this->a;
$this->a = " ";
// if we're here c is part of the comment and therefore tossed // If the comment started on a new line we let it stay on the new line
if(isset($this->c)) if ($this->input[($startIndex - 1)] === "\n") {
unset($this->c); echo "\n";
} }
} }
return $char;
}
/** $endPoint = ($this->index - 1) - $startIndex;
* Pushes the index ahead to the next instance of the supplied string. If it echo substr($this->input, $startIndex, $endPoint);
* is found the first character of the string is returned.
*
* @return string|false Returns the first character of the string or false.
*/
protected function getNext($string)
{
$pos = strpos($this->input, $string, $this->index);
if($pos === false) $this->c = $char;
return false;
$this->index = $pos; return;
return substr($this->input, $this->index, 1); }
} } else {
$char = false;
}
/** if ($char === false) {
* When a javascript string is detected this function crawls for the end of throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2));
}
// if we're here c is part of the comment and therefore tossed
$this->c = $char;
}
/**
* Pushes the index ahead to the next instance of the supplied string. If it
* is found the first character of the string is returned and the index is set
* to it's position.
*
* @param string $string
* @return string|false Returns the first character of the string or false.
*/
protected function getNext($string)
{
// Find the next occurrence of "string" after the current position.
$pos = strpos($this->input, $string, $this->index);
// If it's not there return false.
if ($pos === false) {
return false;
}
// Adjust position of index to jump ahead to the asked for string
$this->index = $pos;
// Return the first character of that string.
return $this->index < $this->len ? $this->input[$this->index] : false;
}
/**
* When a javascript string is detected this function crawls for the end of
* it and saves the whole string. * it and saves the whole string.
* *
*/ * @throws \RuntimeException Unclosed strings will throw an error
protected function saveString() */
{ protected function saveString()
$this->a = $this->b; {
if($this->a == "'" || $this->a == '"') // is the character a quote $startpos = $this->index;
{
// save literal string
$stringType = $this->a;
while(1) // saveString is always called after a gets cleared, so we push b into
{ // that spot.
echo $this->a; $this->a = $this->b;
$this->a = $this->getChar();
switch($this->a) // If this isn't a string we don't need to do anything.
{ if (!isset($this->stringDelimiters[$this->a])) {
case $stringType: return;
break 2; }
case "\n": // String type is the quote used, " or '
throw new RuntimeException('Unclosed string. ' . $this->index); $stringType = $this->a;
break;
case '\\': // Echo out that starting quote
echo $this->a; echo $this->a;
$this->a = $this->getChar();
}
}
}
}
/** // Loop until the string is done
* When a regular expression is detected this funcion crawls for the end of // Grab the very next character and load it into a
while (($this->a = $this->getChar()) !== false) {
switch ($this->a) {
// If the string opener (single or double quote) is used
// output it and break out of the while loop-
// The string is finished!
case $stringType:
break 2;
// New lines in strings without line delimiters are bad- actual
// new lines will be represented by the string \n and not the actual
// character, so those will be treated just fine using the switch
// block below.
case "\n":
if ($stringType === '`') {
echo $this->a;
} else {
throw new \RuntimeException('Unclosed string at position: ' . $startpos);
}
break;
// Escaped characters get picked up here. If it's an escaped new line it's not really needed
case '\\':
// a is a slash. We want to keep it, and the next character,
// unless it's a new line. New lines as actual strings will be
// preserved, but escaped new lines should be reduced.
$this->b = $this->getChar();
// If b is a new line we discard a and b and restart the loop.
if ($this->b === "\n") {
break;
}
// echo out the escaped character and restart the loop.
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;
}
}
}
/**
* When a regular expression is detected this function crawls for the end of
* it and saves the whole regex. * it and saves the whole regex.
*/ *
protected function saveRegex() * @throws \RuntimeException Unclosed regex will throw an error
{ */
echo $this->a . $this->b; protected function saveRegex()
{
echo $this->a . $this->b;
while(($this->a = $this->getChar()) !== false) while (($this->a = $this->getChar()) !== false) {
{ if ($this->a === '/') {
if($this->a == '/') break;
break; }
if($this->a == '\\') if ($this->a === '\\') {
{ echo $this->a;
echo $this->a; $this->a = $this->getChar();
$this->a = $this->getChar(); }
}
if($this->a == "\n") if ($this->a === "\n") {
throw new RuntimeException('Stray regex pattern. ' . $this->index); throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index);
}
echo $this->a; echo $this->a;
} }
$this->b = $this->getReal(); $this->b = $this->getReal();
} }
/** /**
* Resets attributes that do not need to be stored between requests so that * Checks to see if a character is alphanumeric.
* the next request is ready to go. *
*/ * @param string $char Just one character
protected function clean() * @return bool
{ */
unset($this->input); protected static function isAlphaNumeric($char)
$this->index = 0; {
$this->a = $this->b = ''; return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
unset($this->c); }
unset($this->options);
}
/** /**
* Checks to see if a character is alphanumeric. * Replace patterns in the given string and store the replacement
* *
* @return bool * @param string $js The string to lock
*/ * @return bool
static protected function isAlphaNumeric($char) */
{ protected function lock($js)
return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/'; {
} /* lock things like <code>"asd" + ++x;</code> */
$lock = '"LOCK---' . crc32(time()) . '"';
} $matches = [];
preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
if (empty($matches)) {
return $js;
}
$this->locks[$lock] = $matches[2];
$js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
/* -- */
return $js;
}
/**
* Replace "locks" with the original characters
*
* @param string $js The string to unlock
* @return bool
*/
protected function unlock($js)
{
if (empty($this->locks)) {
return $js;
}
foreach ($this->locks as $lock => $replacement) {
$js = str_replace($lock, $replacement, $js);
}
return $js;
}
}

View file

@ -1993,7 +1993,7 @@ final class FileCombiner
if (strpos($file, '.min')===false and strpos($file, '.packed')===false ) if (strpos($file, '.min')===false and strpos($file, '.packed')===false )
{ {
require_once(PHPWG_ROOT_PATH.'include/jshrink.class.php'); require_once(PHPWG_ROOT_PATH.'include/jshrink.class.php');
try { $js = JShrink_Minifier::minify($js); } catch(Exception $e) {} try { $js = JShrink\Minifier::minify($js); } catch(Exception $e) {}
} }
return trim($js, " \t\r\n;").";\n"; return trim($js, " \t\r\n;").";\n";
} }