Your IP : 10.10.0.253


Current Path : /var/www/libraries/compojoom/minifier/
Upload File :
Current File : /var/www/libraries/compojoom/minifier/minifier.php

<?php
/**
 * @package    Lib_Compojoom
 * @author     DanielDimitrov <daniel@compojoom.com>
 * @date       20.08.14
 *
 * @copyright  Copyright (C) 2008 - 2013 compojoom.com . All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

defined('_JEXEC') or die('Restricted access');

/**
 * This file is part of the JShrink package.
 *
 * @link     https://github.com/tedious/JShrink
 * @license  http://www.opensource.org/licenses/bsd-license.php  BSD License
 *
 * (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.
 *
 * Copyright (c) 2009, Robert Hafner
 * 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 the Stash Project nor the
 *           names of its 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 Robert Hafner 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.
 */

/**
 * JShrink
 *
 *
 * @package    JShrink
 * @author     Robert Hafner <tedivm@tedivm.com>
 */

/**
 * This is the original Minifier class. I've renamed it to CompojoomMinifier in order for it
 * to be autoloaded with our library. Other than that all functions from jshrink are the same
 *
 * Usage - CompojoomMinifier::minify($js);
 * Usage - CompojoomMinifier::minify($js, $options);
 * Usage - CompojoomMinifier::minify($js, array('flaggedComments' => false));
 *
 * @since  4.0
 */
class CompojoomMinifier
{
	/**
	 * The input javascript to be minified.
	 *
	 * @var string
	 */
	protected $input;

	/**
	 * Length of input javascript.
	 *
	 * @var int
	 */
	protected $len = 0;

	/**
	 * 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;

	/**
	 * These characters are used to define strings.
	 */
	protected $stringDelimiters = array('\'' => true, '"' => true, '`' => true);

	/**
	 * 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
	 */
	protected static $defaultOptions = array('flaggedComments' => true);

	/**
	 * Contains lock ids which are used to replace certain code patterns and
	 * prevent them from being minified
	 *
	 * @var array
	 */
	protected $locks = [];

	/**
	 * Takes a string containing javascript and removes unneeded characters in
	 * order to shrink the code without altering it's functionality.
	 *
	 * @param  string      $js      The raw javascript to be minified
	 * @param  array       $options Various runtime options in an associative array
	 * @throws \Exception
	 * @return bool|string
	 */
	public static function minify($js, $options = array())
	{
		try {
			ob_start();

			$jshrink = new CompojoomMinifier();
			$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 = $jshrink->unlock($js);
			unset($jshrink);

			return $js;
		} catch (\Exception $e) {
			if (isset($jshrink)) {
				// Since the breakdownScript function probably wasn't finished
				// we clean it out before discarding it.
				$jshrink->clean();
				unset($jshrink);
			}

			// without this call things get weird, with partially outputted js.
			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  $options Various runtime options in an associative array
	 */
	protected function minifyDirectToOutput($js, $options)
	{
		$this->initialize($js, $options);
		$this->loop();
		$this->clean();
	}

	/**
	 *  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(array("\r\n", '/**/', "\r"), array("\n", "", "\n"), $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
		// comment error that can otherwise occur.
		$this->input .= PHP_EOL;

		// save input length to skip calculation every time
		$this->len = strlen($this->input);

		// Populate "a" with a new line, "b" with the first character, before
		// entering the loop
		$this->a = "\n";
		$this->b = $this->getReal();
	}

	/**
	 * Characters that can't stand alone preserve the newline.
	 *
	 * @var array
	 */
	protected $noNewLineCharacters = array(
		'(' => true,
		'-' => true,
		'+' => true,
		'[' => true,
		'@' => true);

	/**
	 * 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;
					}

					// 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;
					}

				// otherwise we treat the newline like a space

				// no break
				case ' ':
					if (static::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 (static::isAlphaNumeric($this->a)) {
									echo $this->a;
									$this->saveString();
								}
							}
							break;

						case ' ':
							if (!static::isAlphaNumeric($this->a)) {
								break;
							}

						// no break
						default:
							// check for some regex that breaks stuff
							if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
								$this->saveRegex();
								continue 3;
							}

							echo $this->a;
							$this->saveString();
							break;
					}
			}

			// do reg check of doom
			$this->b = $this->getReal();

			if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) {
				$this->saveRegex();
			}
		}
	}

	/**
	 * Resets attributes that do not need to be stored between requests so that
	 * 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);
	}

	/**
	 * 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 the next character doesn't exist return false.
			if (isset($char) && $char === false) {
				return false;
			}

			// Otherwise increment the pointer and use this char.
			$this->index++;
		}

		// Normalize all whitespace except for the newline character into a
		// standard space.
		if ($char !== "\n" && $char < "\x20") {
			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.
	 * @throws \RuntimeException
	 */
	protected function getReal()
	{
		$startIndex = $this->index;
		$char = $this->getChar();

		// Check to see if we're potentially in a comment
		if ($char !== '/') {
			return $char;
		}

		$this->c = $this->getChar();

		if ($this->c === '/') {
			$this->processOneLineComments($startIndex);

			return $this->getReal();
		} elseif ($this->c === '*') {
			$this->processMultiLineComments($startIndex);

			return $this->getReal();
		}

		return $char;
	}

	/**
	 * Removed one line comments, with the exception of some very specific types of
	 * 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;

		// kill rest of line
		$this->getNext("\n");

		unset($this->c);

		if ($thirdCommentString == '@') {
			$endPoint = $this->index - $startIndex;
			$this->c = "\n" . substr($this->input, $startIndex, $endPoint);
		}
	}

	/**
	 * 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();

		// kill everything up to the next */ if it's there
		if ($this->getNext('*/')) {
			$this->getChar(); // get *
			$this->getChar(); // get /
			$char = $this->getChar(); // get next real character

			// Now we reinsert conditional comments and YUI-style licensing comments
			if (($this->options['flaggedComments'] && $thirdCommentString === '!')
				|| ($thirdCommentString === '@')) {

				// 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->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";
					}
				}

				$endPoint = ($this->index - 1) - $startIndex;
				echo substr($this->input, $startIndex, $endPoint);

				$this->c = $char;

				return;
			}
		} else {
			$char = false;
		}

		if ($char === false) {
			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.
	 *
	 * @throws \RuntimeException Unclosed strings will throw an error
	 */
	protected function saveString()
	{
		$startpos = $this->index;

		// saveString is always called after a gets cleared, so we push b into
		// that spot.
		$this->a = $this->b;

		// If this isn't a string we don't need to do anything.
		if (!isset($this->stringDelimiters[$this->a])) {
			return;
		}

		// String type is the quote used, " or '
		$stringType = $this->a;

		// Echo out that starting quote
		echo $this->a;

		// Loop until the string is done
		// 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.
	 *
	 * @throws \RuntimeException Unclosed regex will throw an error
	 */
	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('Unclosed regex pattern at position: ' . $this->index);
			}

			echo $this->a;
		}
		$this->b = $this->getReal();
	}

	/**
	 * Checks to see if a character is alphanumeric.
	 *
	 * @param  string $char Just one character
	 * @return bool
	 */
	protected static function isAlphaNumeric($char)
	{
		return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
	}

	/**
	 * Replace patterns in the given string and store the replacement
	 *
	 * @param  string $js The string to lock
	 * @return bool
	 */
	protected function lock($js)
	{
		/* lock things like <code>"asd" + ++x;</code> */
		$lock = '"LOCK---' . crc32(time()) . '"';

		$matches = array();
		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;
	}
}