|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00053 class JSMin { 00054 const ORD_LF = 10; 00055 const ORD_SPACE = 32; 00056 const ACTION_KEEP_A = 1; 00057 const ACTION_DELETE_A = 2; 00058 const ACTION_DELETE_A_B = 3; 00059 00060 protected $a = "\n"; 00061 protected $b = ''; 00062 protected $input = ''; 00063 protected $inputIndex = 0; 00064 protected $inputLength = 0; 00065 protected $lookAhead = null; 00066 protected $output = ''; 00067 00074 public static function minify($js) 00075 { 00076 $jsmin = new JSMin($js); 00077 return $jsmin->min(); 00078 } 00079 00083 public function __construct($input) 00084 { 00085 $this->input = str_replace("\r\n", "\n", $input); 00086 $this->inputLength = strlen($this->input); 00087 } 00088 00092 public function min() 00093 { 00094 if ($this->output !== '') { // min already run 00095 return $this->output; 00096 } 00097 $this->action(self::ACTION_DELETE_A_B); 00098 00099 while ($this->a !== null) { 00100 // determine next command 00101 $command = self::ACTION_KEEP_A; // default 00102 if ($this->a === ' ') { 00103 if (! $this->isAlphaNum($this->b)) { 00104 $command = self::ACTION_DELETE_A; 00105 } 00106 } elseif ($this->a === "\n") { 00107 if ($this->b === ' ') { 00108 $command = self::ACTION_DELETE_A_B; 00109 } elseif (false === strpos('{[(+-', $this->b) 00110 && ! $this->isAlphaNum($this->b)) { 00111 $command = self::ACTION_DELETE_A; 00112 } 00113 } elseif (! $this->isAlphaNum($this->a)) { 00114 if ($this->b === ' ' 00115 || ($this->b === "\n" 00116 && (false === strpos('}])+-"\'', $this->a)))) { 00117 $command = self::ACTION_DELETE_A_B; 00118 } 00119 } 00120 $this->action($command); 00121 } 00122 $this->output = trim($this->output); 00123 return $this->output; 00124 } 00125 00131 protected function action($command) 00132 { 00133 switch ($command) { 00134 case self::ACTION_KEEP_A: 00135 $this->output .= $this->a; 00136 // fallthrough 00137 case self::ACTION_DELETE_A: 00138 $this->a = $this->b; 00139 if ($this->a === "'" || $this->a === '"') { // string literal 00140 $str = $this->a; // in case needed for exception 00141 while (true) { 00142 $this->output .= $this->a; 00143 $this->a = $this->get(); 00144 if ($this->a === $this->b) { // end quote 00145 break; 00146 } 00147 if (ord($this->a) <= self::ORD_LF) { 00148 throw new JSMin_UnterminatedStringException( 00149 'Unterminated String: ' . var_export($str, true)); 00150 } 00151 $str .= $this->a; 00152 if ($this->a === '\\') { 00153 $this->output .= $this->a; 00154 $this->a = $this->get(); 00155 $str .= $this->a; 00156 } 00157 } 00158 } 00159 // fallthrough 00160 case self::ACTION_DELETE_A_B: 00161 $this->b = $this->next(); 00162 if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal 00163 $this->output .= $this->a . $this->b; 00164 $pattern = '/'; // in case needed for exception 00165 while (true) { 00166 $this->a = $this->get(); 00167 $pattern .= $this->a; 00168 if ($this->a === '/') { // end pattern 00169 break; // while (true) 00170 } elseif ($this->a === '\\') { 00171 $this->output .= $this->a; 00172 $this->a = $this->get(); 00173 $pattern .= $this->a; 00174 } elseif (ord($this->a) <= self::ORD_LF) { 00175 throw new JSMin_UnterminatedRegExpException( 00176 'Unterminated RegExp: '. var_export($pattern, true)); 00177 } 00178 $this->output .= $this->a; 00179 } 00180 $this->b = $this->next(); 00181 } 00182 // end case ACTION_DELETE_A_B 00183 } 00184 } 00185 00186 protected function isRegexpLiteral() 00187 { 00188 if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing 00189 return true; 00190 } 00191 if (' ' === $this->a) { 00192 $length = strlen($this->output); 00193 if ($length < 2) { // weird edge case 00194 return true; 00195 } 00196 // you can't divide a keyword 00197 if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) { 00198 if ($this->output === $m[0]) { // odd but could happen 00199 return true; 00200 } 00201 // make sure it's a keyword, not end of an identifier 00202 $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1); 00203 if (! $this->isAlphaNum($charBeforeKeyword)) { 00204 return true; 00205 } 00206 } 00207 } 00208 return false; 00209 } 00210 00214 protected function get() 00215 { 00216 $c = $this->lookAhead; 00217 $this->lookAhead = null; 00218 if ($c === null) { 00219 if ($this->inputIndex < $this->inputLength) { 00220 $c = $this->input[$this->inputIndex]; 00221 $this->inputIndex += 1; 00222 } else { 00223 return null; 00224 } 00225 } 00226 if ($c === "\r" || $c === "\n") { 00227 return "\n"; 00228 } 00229 if (ord($c) < self::ORD_SPACE) { // control char 00230 return ' '; 00231 } 00232 return $c; 00233 } 00234 00238 protected function peek() 00239 { 00240 $this->lookAhead = $this->get(); 00241 return $this->lookAhead; 00242 } 00243 00247 protected function isAlphaNum($c) 00248 { 00249 return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126); 00250 } 00251 00252 protected function singleLineComment() 00253 { 00254 $comment = ''; 00255 while (true) { 00256 $get = $this->get(); 00257 $comment .= $get; 00258 if (ord($get) <= self::ORD_LF) { // EOL reached 00259 // if IE conditional comment 00260 if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { 00261 return "/{$comment}"; 00262 } 00263 return $get; 00264 } 00265 } 00266 } 00267 00268 protected function multipleLineComment() 00269 { 00270 $this->get(); 00271 $comment = ''; 00272 while (true) { 00273 $get = $this->get(); 00274 if ($get === '*') { 00275 if ($this->peek() === '/') { // end of comment reached 00276 $this->get(); 00277 // if comment preserved by YUI Compressor 00278 if (0 === strpos($comment, '!')) { 00279 return "\n/*" . substr($comment, 1) . "*/\n"; 00280 } 00281 // if IE conditional comment 00282 if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { 00283 return "/*{$comment}*/"; 00284 } 00285 return ' '; 00286 } 00287 } elseif ($get === null) { 00288 throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true)); 00289 } 00290 $comment .= $get; 00291 } 00292 } 00293 00298 protected function next() 00299 { 00300 $get = $this->get(); 00301 if ($get !== '/') { 00302 return $get; 00303 } 00304 switch ($this->peek()) { 00305 case '/': return $this->singleLineComment(); 00306 case '*': return $this->multipleLineComment(); 00307 default: return $get; 00308 } 00309 } 00310 } 00311 00312 class JSMin_UnterminatedStringException extends Exception {} 00313 class JSMin_UnterminatedCommentException extends Exception {} 00314 class JSMin_UnterminatedRegExpException extends Exception {}