<?php
/*
 * == Aimy Extensions Comment Start ==
 *
 * JShrink is licensed under the Modified (3-clause) BSD License, which is
 * compatible with the GNU GPL.
 *
 * See http://www.gnu.org/licenses/license-list.html#ModifiedBSD for details.
 *
 * == Aimy Extensions Comment End ==
 *
 *
 * This file is part of the JShrink package.
 *
 * (c) Robert Hafner <tedivm@tedivm.com>
 *
 *
 * 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.
 */
 namespace JShrink; defined( '_JEXEC' ) or die(); class Minifier { protected $input; protected $len = 0; protected $index = 0; protected $a = ''; protected $b = ''; protected $c; protected $last_char; protected $output; protected $options; protected $stringDelimiters = ['\'' => true, '"' => true, '`' => true]; protected static $defaultOptions = ['flaggedComments' => true]; protected static $keywords = ["delete", "do", "for", "in", "instanceof", "return", "typeof", "yield"]; protected $max_keyword_len; protected $locks = []; public static function minify($js, $options = []) { try { $jshrink = new Minifier(); $js = $jshrink->lock($js); $js = ltrim($jshrink->minifyToString($js, $options)); $js = $jshrink->unlock($js); unset($jshrink); return $js; } catch (\Exception $e) { if (isset($jshrink)) { $jshrink->clean(); unset($jshrink); } throw $e; } } protected function minifyToString($js, $options) { $this->initialize($js, $options); $this->loop(); $this->clean(); return $this->output; } protected function initialize($js, $options) { $this->options = array_merge(static::$defaultOptions, $options); $this->input = $js; $this->input .= PHP_EOL; $this->len = strlen($this->input); $this->a = "\n"; $this->b = "\n"; $this->last_char = "\n"; $this->output = ""; $this->max_keyword_len = max(array_map('strlen', static::$keywords)); } protected $noNewLineCharacters = [ '(' => true, '-' => true, '+' => true, '[' => true, '#' => true, '@' => true]; protected function echo($char) { $this->output .= $char; $this->last_char = $char[-1]; } protected function loop() { while ($this->a !== false && !is_null($this->a) && $this->a !== '') { switch ($this->a) { case "\r": case "\n": if ($this->b !== false && isset($this->noNewLineCharacters[$this->b])) { $this->echo($this->a); $this->saveString(); break; } if ($this->b === ' ') { break; } case ' ': if (static::isAlphaNumeric($this->b)) { $this->echo($this->a); } $this->saveString(); break; default: switch ($this->b) { case "\r": case "\n": if (strpos('}])+-"\'', $this->a) !== false) { $this->echo($this->a); $this->saveString(); break; } else { if (static::isAlphaNumeric($this->a)) { $this->echo($this->a); $this->saveString(); } } break; case ' ': if (!static::isAlphaNumeric($this->a)) { break; } default: if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) { $this->saveRegex(); continue 3; } $this->echo($this->a); $this->saveString(); break; } } $this->b = $this->getReal(); if ($this->b == '/') { $valid_tokens = "(,=:[!&|?\n"; $last_token = $this->a; if ($last_token == " ") { $last_token = $this->last_char; } if (strpos($valid_tokens, $last_token) !== false) { $this->saveRegex(); } else if ($this->endsInKeyword()) { $this->saveRegex(); } } } } protected function clean() { unset($this->input); $this->len = 0; $this->index = 0; $this->a = $this->b = ''; unset($this->c); unset($this->options); } protected function getChar() { if (isset($this->c)) { $char = $this->c; unset($this->c); } else { $char = $this->index < $this->len ? $this->input[$this->index] : false; if (isset($char) && $char === false) { return false; } $this->index++; } if ($char == "\r") { $char = "\n"; } if ($char !== "\n" && $char < "\x20") { return ' '; } return $char; } protected function peek() { if ($this->index >= $this->len) { return false; } $char = $this->input[$this->index]; if ($char == "\r") { $char = "\n"; } if ($char !== "\n" && $char < "\x20") { return ' '; } return $char; } protected function getReal() { $startIndex = $this->index; $char = $this->getChar(); 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; } protected function processOneLineComments($startIndex) { $thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : false; $this->getNext("\n"); unset($this->c); if ($thirdCommentString == '@') { $endPoint = $this->index - $startIndex; $this->c = "\n" . substr($this->input, $startIndex, $endPoint); } } protected function processMultiLineComments($startIndex) { $this->getChar(); $thirdCommentString = $this->getChar(); if ($thirdCommentString == "*") { $peekChar = $this->peek(); if ($peekChar == "/") { $this->index++; return; } } if ($this->getNext('*/')) { $this->getChar(); $this->getChar(); $char = $this->getChar(); if (($this->options['flaggedComments'] && $thirdCommentString === '!') || ($thirdCommentString === '@')) { if ($startIndex > 0) { $this->echo($this->a); $this->a = " "; if ($this->input[($startIndex - 1)] === "\n") { $this->echo("\n"); } } $endPoint = ($this->index - 1) - $startIndex; $this->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)); } $this->c = $char; } protected function getNext($string) { $pos = strpos($this->input, $string, $this->index); if ($pos === false) { return false; } $this->index = $pos; return $this->index < $this->len ? $this->input[$this->index] : false; } protected function saveString() { $startpos = $this->index; $this->a = $this->b; if (!isset($this->stringDelimiters[$this->a])) { return; } $stringType = $this->a; $this->echo($this->a); while (($this->a = $this->getChar()) !== false) { switch ($this->a) { case $stringType: break 2; case "\n": if ($stringType === '`') { $this->echo($this->a); } else { throw new \RuntimeException('Unclosed string at position: ' . $startpos); } break; case '\\': $this->b = $this->getChar(); if ($this->b === "\n") { break; } $this->echo($this->a . $this->b); break; default: $this->echo($this->a); } } } protected function saveRegex() { if ($this->a != " ") { $this->echo($this->a); } $this->echo($this->b); while (($this->a = $this->getChar()) !== false) { if ($this->a === '/') { break; } if ($this->a === '\\') { $this->echo($this->a); $this->a = $this->getChar(); } if ($this->a === "\n") { throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index); } $this->echo($this->a); } $this->b = $this->getReal(); } protected static function isAlphaNumeric($char) { return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/'; } protected function endsInKeyword() { $testOutput = substr($this->output . $this->a, -1 * ($this->max_keyword_len + 10)); foreach(static::$keywords as $keyword) { if (preg_match('/[^\w]'.$keyword.'[ ]?$/i', $testOutput) === 1) { return true; } } return false; } protected function lock($js) { $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; } protected function unlock($js) { if (empty($this->locks)) { return $js; } foreach ($this->locks as $lock => $replacement) { $js = str_replace($lock, $replacement, $js); } return $js; } } 
