diff options
Diffstat (limited to 'include/jshrink.class.php')
-rw-r--r-- | include/jshrink.class.php | 469 |
1 files changed, 469 insertions, 0 deletions
diff --git a/include/jshrink.class.php b/include/jshrink.class.php new file mode 100644 index 000000000..b32807c4a --- /dev/null +++ b/include/jshrink.class.php @@ -0,0 +1,469 @@ +<?php +/** + * 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 + * @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 + */ + + +/** + * JShrink_Minifier + * + * Usage - JShrink_Minifier::minify($js); + * Usage - JShrink_Minifier::minify($js, $options); + * Usage - JShrink_Minifier::minify($js, array('flaggedComments' => false)); + * + * @package JShrink + * @author Robert Hafner <tedivm@tedivm.com> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ +class JShrink_Minifier +{ + /** + * The input javascript to be minified. + * + * @var string + */ + protected $input; + + /** + * The location of the character (in the input string) that is next to be + * processed. + * + * @var int + */ + protected $index = 0; + + /** + * The first of the characters currently being looked at. + * + * @var string + */ + protected $a = ''; + + + /** + * The next character being looked at (after a); + * + * @var string + */ + protected $b = ''; + + /** + * This character is only active when certain look ahead actions take place. + * + * @var string + */ + protected $c; + + /** + * Contains the options for the current minification process. + * + * @var array + */ + protected $options; + + /** + * 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 + * options (stored in the $options attribute). + * + * @var array + */ + static protected $defaultOptions = array('flaggedComments' => true); + + /** + * Contains a copy of the JShrink object used to run minification. This is + * only used internally, and is only stored for performance reasons. There + * is no internal data shared between minification requests. + */ + static protected $jshrink; + + /** + * Minifier::minify takes a string containing javascript and removes + * unneeded characters in order to shrink the code without altering it's + * functionality. + */ + static public function minify($js, $options = array()) + { + try{ + ob_start(); + $currentOptions = array_merge(self::$defaultOptions, $options); + + if(!isset(self::$jshrink)) + self::$jshrink = new JShrink_Minifier(); + + self::$jshrink->breakdownScript($js, $currentOptions); + return ob_get_clean(); + + }catch(Exception $e){ + if(isset(self::$jshrink)) + self::$jshrink->clean(); + + ob_end_clean(); + throw $e; + } + } + + /** + * Processes a javascript string and outputs only the required characters, + * stripping out all unneeded characters. + * + * @param string $js The raw javascript to be minified + * @param array $currentOptions Various runtime options in an associative array + */ + protected function breakdownScript($js, $currentOptions) + { + // reset work attributes in case this isn't the first run. + $this->clean(); + + $this->options = $currentOptions; + + $js = str_replace("\r\n", "\n", $js); + $this->input = str_replace("\r", "\n", $js); + + + $this->a = $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 + // $a is on the very first run + while(strlen($this->a) > 1) + { + echo $this->a; + $this->a = $this->getReal(); + } + + $this->b = $this->getReal(); + + while($this->a !== false && !is_null($this->a) && $this->a !== '') + { + + // now we give $b the same check for conditional comments we gave $a + // 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) + { + // new lines + case "\n": + // if the next line is something that can't stand alone + // preserve the newline + if(strpos('(-+{[@', $this->b) !== false) + { + echo $this->a; + $this->saveString(); + break; + } + + // if its a space we move down to the string test below + if($this->b === ' ') + break; + + // otherwise we treat the newline like a space + + case ' ': + if(self::isAlphaNumeric($this->b)) + echo $this->a; + + $this->saveString(); + break; + + default: + switch($this->b) + { + 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 ' ': + if(!self::isAlphaNumeric($this->a)) + break; + + default: + // check for some regex that breaks stuff + if($this->a == '/' && ($this->b == '\'' || $this->b == '"')) + { + $this->saveRegex(); + continue; + } + + echo $this->a; + $this->saveString(); + break; + } + } + + // do reg check of doom + $this->b = $this->getReal(); + + if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) + $this->saveRegex(); + } + $this->clean(); + } + + /** + * Returns the next string for processing based off of the current 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) + return ' '; + + return $char; + } + + /** + * This function gets the next "real" character. It is essentially a wrapper + * around the getChar function that skips comments. This has significant + * performance benefits as the skipping is done using native functions (ie, + * c code) rather than in script php. + * + * @return string Next 'real' character to be processed. + */ + protected function getReal() + { + $startIndex = $this->index; + $char = $this->getChar(); + + if($char == '/') + { + $this->c = $this->getChar(); + + if($this->c == '/') + { + $thirdCommentString = substr($this->input, $this->index, 1); + + // kill rest of line + $char = $this->getNext("\n"); + + if($thirdCommentString == '@') + { + $endPoint = ($this->index) - $startIndex; + unset($this->c); + $char = "\n" . substr($this->input, $startIndex, $endPoint); + }else{ + $char = $this->getChar(); + $char = $this->getChar(); + } + + }elseif($this->c == '*'){ + + $this->getChar(); // current C + $thirdCommentString = $this->getChar(); + + if($thirdCommentString == '@') + { + // conditional comment + + // we're gonna back up a bit and and send the comment back, + // where the first char will be echoed and the rest will be + // treated like a string + $this->index = $this->index-2; + return '/'; + + }elseif($this->getNext('*/')){ + // kill everything up to the next */ + + $this->getChar(); // get * + $this->getChar(); // get / + + $char = $this->getChar(); // get next real character + + // if YUI-style comments are enabled we reinsert it into the stream + if($this->options['flaggedComments'] && $thirdCommentString == '!') + { + $endPoint = ($this->index - 1) - $startIndex; + echo "\n" . substr($this->input, $startIndex, $endPoint) . "\n"; + } + + }else{ + $char = false; + } + + if($char === false) + throw new RuntimeException('Stray comment. ' . $this->index); + + // if we're here c is part of the comment and therefore tossed + if(isset($this->c)) + unset($this->c); + } + } + return $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. + * + * @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) + return false; + + $this->index = $pos; + return substr($this->input, $this->index, 1); + } + + /** + * When a javascript string is detected this function crawls for the end of + * it and saves the whole string. + * + */ + protected function saveString() + { + $this->a = $this->b; + if($this->a == "'" || $this->a == '"') // is the character a quote + { + // save literal string + $stringType = $this->a; + + while(1) + { + echo $this->a; + $this->a = $this->getChar(); + + switch($this->a) + { + case $stringType: + break 2; + + case "\n": + throw new RuntimeException('Unclosed string. ' . $this->index); + break; + + case '\\': + echo $this->a; + $this->a = $this->getChar(); + } + } + } + } + + /** + * When a regular expression is detected this funcion crawls for the end of + * it and saves the whole regex. + */ + protected function saveRegex() + { + echo $this->a . $this->b; + + while(($this->a = $this->getChar()) !== false) + { + if($this->a == '/') + break; + + if($this->a == '\\') + { + echo $this->a; + $this->a = $this->getChar(); + } + + if($this->a == "\n") + throw new RuntimeException('Stray regex pattern. ' . $this->index); + + echo $this->a; + } + $this->b = $this->getReal(); + } + + /** + * Resets attributes that do not need to be stored between requests so that + * the next request is ready to go. + */ + protected function clean() + { + unset($this->input); + $this->index = 0; + $this->a = $this->b = ''; + unset($this->c); + unset($this->options); + } + + /** + * Checks to see if a character is alphanumeric. + * + * @return bool + */ + static protected function isAlphaNumeric($char) + { + return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/'; + } + +}
\ No newline at end of file |