Merge branch 'develop' into feature/minify
diff --git a/system/core/Output.php b/system/core/Output.php
index 719c432..05bc48e 100644
--- a/system/core/Output.php
+++ b/system/core/Output.php
@@ -740,13 +740,13 @@
 				preg_match_all('{<style.+</style>}msU', $output, $style_clean);
 				foreach ($style_clean[0] as $s)
 				{
-					$output = str_replace($s, $this->_minify_script_style($s, TRUE), $output);
+					$output = str_replace($s, $this->_minify_js_css($s, 'css', TRUE), $output);
 				}
 
 				// Minify the javascript in <script> tags.
 				foreach ($javascript_clean[0] as $s)
 				{
-					$javascript_mini[] = $this->_minify_script_style($s, TRUE);
+					$javascript_mini[] = $this->_minify_js_css($s, 'js', TRUE);
 				}
 
 				// Replace multiple spaces with a single space.
@@ -792,13 +792,14 @@
 			break;
 
 			case 'text/css':
+
+				return $this->_minify_js_css($output, 'css');
+
 			case 'text/javascript':
 			case 'application/javascript':
 			case 'application/x-javascript':
 
-				$output = $this->_minify_script_style($output);
-
-			break;
+				return $this->_minify_js_css($output, 'js');
 
 			default: break;
 		}
@@ -809,163 +810,101 @@
 	// --------------------------------------------------------------------
 
 	/**
-	 * Minify Style and Script
+	 * Minify JavaScript and CSS code
 	 *
-	 * Reduce excessive size of CSS/JavaScript content.  To remove spaces this
-	 * script walks the string as an array and determines if the pointer is inside
-	 * a string created by single quotes or double quotes.  spaces inside those
-	 * strings are not stripped.  Opening and closing tags are severed from
-	 * the string initially and saved without stripping whitespace to preserve
-	 * the tags and any associated properties if tags are present
+	 * Strips comments and excessive whitespace characters
 	 *
-	 * Minification logic/workflow is similar to methods used by Douglas Crockford
-	 * in JSMIN. http://www.crockford.com/javascript/jsmin.html
-	 *
-	 * KNOWN ISSUE: ending a line with a closing parenthesis ')' and no semicolon
-	 * where there should be one will break the Javascript. New lines after a
-	 * closing parenthesis are not recognized by the script. For best results
-	 * be sure to terminate lines with a semicolon when appropriate.
-	 *
-	 * @param	string	$output		Output to minify
-	 * @param	bool	$has_tags	Specify if the output has style or script tags
-	 * @return	string	Minified output
+	 * @param	string	$output
+	 * @param	string	$type	'js' or 'css'
+	 * @param	bool	$tags	Whether $output contains the 'script' or 'style' tag
+	 * @return	string
 	 */
-	protected function _minify_script_style($output, $has_tags = FALSE)
+	protected function _minify_js_css($output, $type, $tags = FALSE)
 	{
-		// We only need this if there are tags in the file
-		if ($has_tags === TRUE)
+		if ($tags === TRUE)
 		{
-			// Remove opening tag and save for later
-			$pos = strpos($output, '>') + 1;
-			$open_tag = substr($output, 0, $pos);
-			$output = substr_replace($output, '', 0, $pos);
+			$tags = array('close' => strrchr($output, '<'));
 
-			// Remove closing tag and save it for later
-			$pos = strrpos($output, '</');
-			$closing_tag = substr($output, $pos);
-			$output = substr_replace($output, '', $pos);
+			$open_length = strpos($output, '>') + 1;
+			$tags['open'] = substr($output, 0, $open_length);
+
+			$output = substr($output, $open_length, -strlen($tags['close']));
+
+			// Strip spaces from the tags
+			$tags = preg_replace('#\s{2,}#', ' ', $tags);
 		}
 
-		// Remove CSS comments
-		$output = preg_replace('@/\*([^/][^*]*\*)*/(?!.+?["\'])@i', '', $output);
+		$output = trim($output);
 
-		// Remove Javascript inline comments
-		if ($has_tags === TRUE && strpos(strtolower($open_tag), 'script') !== FALSE)
+		if ($type === 'js')
 		{
-			$lines = preg_split('/\r?\n|\n?\r/', $output);
-			foreach ($lines as &$line)
+			// Catch all string literals and comment blocks
+			if (preg_match_all('#((?:((?<!\\\)\'|")|/\*).*(?(2)(?<!\\\)\2|\*/))#msuUS', $output, $match, PREG_OFFSET_CAPTURE))
 			{
-				$in_string = $in_dstring = FALSE;
-				for ($i = 0, $len = strlen($line); $i < $len; $i++)
+				$js_literals = $js_code = array();
+				for ($match = $match[0], $c = count($match), $i = $pos = $offset = 0; $i < $c; $i++)
 				{
-					if ( ! $in_string && ! $in_dstring && substr($line, $i, 2) === '//')
-					{
-						$line = substr($line, 0, $i);
-						break;
-					}
+					$js_code[$pos++] = trim(substr($output, $offset, $match[$i][1] - $offset));
+					$offset = $match[$i][1] + strlen($match[$i][0]);
 
-					if ($line[$i] === "'" && ! $in_dstring)
+					// Save only if we haven't matched a comment block
+					if ($match[$i][0][0] !== '/')
 					{
-						$in_string = ! $in_string;
-					}
-					elseif ($line[$i] === '"' && ! $in_string)
-					{
-						$in_dstring = ! $in_dstring;
+						$js_literals[$pos++] = array_shift($match[$i]);
 					}
 				}
+				$js_code[$pos] = substr($output, $offset);
+
+				// $match might be quite large, so free it up together with other vars that we no longer need
+				unset($match, $offset, $pos);
+			}
+			else
+			{
+				$js_code = array($output);
+				$js_literals = array();
 			}
 
-			$output = implode("\n", $lines);
+			$varname = 'js_code';
+		}
+		else
+		{
+			$varname = 'output';
 		}
 
-		// Remove spaces around curly brackets, colons,
-		// semi-colons, parenthesis, commas
-		$chunks = preg_split('/([\'|"]).+(?![^\\\]\\1)\\1/iU', $output, -1, PREG_SPLIT_OFFSET_CAPTURE);
-		for ($i = count($chunks) - 1; $i >= 0; $i--)
+		// Standartize new lines
+		$$varname = str_replace(array("\r\n", "\r"), "\n", $$varname);
+
+		if ($type === 'js')
 		{
-			$output = substr_replace(
-				$output,
-				preg_replace('/\s*(:|;|,|}|{|\(|\))\s*/i', '$1', $chunks[$i][0]),
-				$chunks[$i][1],
-				strlen($chunks[$i][0])
+			$patterns = array(
+				'#\n?//[^\n]*#'					=> '',		// Remove // line comments
+				'#\s*([!\#%&()*+,\-./:;<=>?@\[\]^`{|}~])\s*#'	=> '$1',	// Remove spaces following and preceeding JS-wise non-special & non-word characters
+				'#\s{2,}#'					=> ' '		// Reduce the remaining multiple whitespace characters to a single space
+			);
+		}
+		else
+		{
+			$patterns = array(
+				'#/\*.*(?=\*/)\*/#s'	=> '',		// Remove /* block comments */
+				'#\n?//[^\n]*#'		=> '',		// Remove // line comments
+				'#\s*([^\w.\#%])\s*#U'	=> '$1',	// Remove spaces following and preceeding non-word characters, excluding dots, hashes and the percent sign
+				'#\s{2,}#'		=> ' '		// Reduce the remaining multiple space characters to a single space
 			);
 		}
 
-		// Replace tabs with spaces
-		// Replace carriage returns & multiple new lines with single new line
-		// and trim any leading or trailing whitespace
-		$output = trim(preg_replace(array('/\t+/', '/\r/', '/\n+/'), array(' ', "\n", "\n"), $output));
+		$$varname = preg_replace(array_keys($patterns), array_values($patterns), $$varname);
 
-		// Remove spaces when safe to do so.
-		$in_string = $in_dstring = $prev = FALSE;
-		$array_output = str_split($output);
-		foreach ($array_output as $key => $value)
+		// Glue back JS quoted strings
+		if ($type === 'js')
 		{
-			if ($in_string === FALSE && $in_dstring === FALSE)
-			{
-				if ($value === ' ')
-				{
-					// Get the next element in the array for comparisons
-					$next = $array_output[$key + 1];
-
-					// Strip spaces preceded/followed by a non-ASCII character
-					// that are not preceded/followed by an alphanumeric character,
-					// '\', '$', '_', '.' and '#'
-					if ((preg_match('/^[\x20-\x7f]*$/D', $next) OR preg_match('/^[\x20-\x7f]*$/D', $prev))
-						&& ( ! ctype_alnum($next) OR ! ctype_alnum($prev))
-						&& ! in_array($next, array('\\', '_', '$', '.', '#'), TRUE)
-						&& ! in_array($prev, array('\\', '_', '$', '.', '#'), TRUE)
-					)
-					{
-						unset($array_output[$key]);
-					}
-				}
-				else
-				{
-					// Save this value as previous for the next iteration
-					// if it is not a blank space
-					$prev = $value;
-				}
-			}
-
-			if ($value === "'" && ! $in_dstring)
-			{
-				$in_string = ! $in_string;
-			}
-			elseif ($value === '"' && ! $in_string)
-			{
-				$in_dstring = ! $in_dstring;
-			}
+			$js_code += $js_literals;
+			ksort($js_code);
+			$output = implode($js_code);
+			unset($js_code, $js_literals, $varname, $patterns);
 		}
 
-		// Put the string back together after spaces have been stripped
-		$output = implode($array_output);
-
-		// Remove new line characters unless previous or next character is
-		// printable or Non-ASCII
-		preg_match_all('/[\n]/', $output, $lf, PREG_OFFSET_CAPTURE);
-		$removed_lf = 0;
-		foreach ($lf as $feed_position)
-		{
-			foreach ($feed_position as $position)
-			{
-				$position = $position[1] - $removed_lf;
-				$next = $output[$position + 1];
-				$prev = $output[$position - 1];
-				if ( ! ctype_print($next) && ! ctype_print($prev)
-					&& ! preg_match('/^[\x20-\x7f]*$/D', $next)
-					&& ! preg_match('/^[\x20-\x7f]*$/D', $prev)
-				)
-				{
-					$output = substr_replace($output, '', $position, 1);
-					$removed_lf++;
-				}
-			}
-		}
-
-		// Put the opening and closing tags back if applicable
-		return isset($open_tag)
-			? $open_tag.$output.$closing_tag
+		return is_array($tags)
+			? $tags['open'].$output.$tags['close']
 			: $output;
 	}