| Total Complexity | 88 |
| Total Lines | 389 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 0 |
Complex classes like JSMin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use JSMin, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 58 | class JSMin { |
||
| 59 | const ORD_LF = 10; |
||
| 60 | const ORD_SPACE = 32; |
||
| 61 | const ACTION_KEEP_A = 1; |
||
| 62 | const ACTION_DELETE_A = 2; |
||
| 63 | const ACTION_DELETE_A_B = 3; |
||
| 64 | |||
| 65 | protected $a = "\n"; |
||
| 66 | protected $b = ''; |
||
| 67 | protected $input = ''; |
||
| 68 | protected $inputIndex = 0; |
||
| 69 | protected $inputLength = 0; |
||
| 70 | protected $lookAhead = null; |
||
| 71 | protected $output = ''; |
||
| 72 | protected $lastByteOut = ''; |
||
| 73 | protected $keptComment = ''; |
||
| 74 | |||
| 75 | /** |
||
| 76 | * Minify Javascript. |
||
| 77 | * |
||
| 78 | * @param string $js Javascript to be minified |
||
| 79 | * |
||
| 80 | * @return string |
||
| 81 | */ |
||
| 82 | public static function minify($js) |
||
| 83 | { |
||
| 84 | $jsmin = new JSMin($js); |
||
| 85 | |||
| 86 | |||
| 87 | return $jsmin->min(); |
||
| 88 | } |
||
| 89 | |||
| 90 | /** |
||
| 91 | * @param string $input |
||
| 92 | */ |
||
| 93 | public function __construct($input) |
||
| 94 | { |
||
| 95 | $this->input = $input; |
||
| 96 | } |
||
| 97 | |||
| 98 | /** |
||
| 99 | * Perform minification, return result |
||
| 100 | * |
||
| 101 | * @return string |
||
| 102 | */ |
||
| 103 | public function min() |
||
| 104 | { |
||
| 105 | if ($this->output !== '') { // min already run |
||
| 106 | return $this->output; |
||
| 107 | } |
||
| 108 | |||
| 109 | $mbIntEnc = null; |
||
| 110 | if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { |
||
| 111 | $mbIntEnc = mb_internal_encoding(); |
||
| 112 | mb_internal_encoding('8bit'); |
||
| 113 | } |
||
| 114 | $this->input = str_replace("\r\n", "\n", $this->input); |
||
| 115 | $this->inputLength = strlen($this->input); |
||
| 116 | |||
| 117 | $this->action(self::ACTION_DELETE_A_B); |
||
| 118 | |||
| 119 | while ($this->a !== null) { |
||
| 120 | // determine next command |
||
| 121 | $command = self::ACTION_KEEP_A; // default |
||
| 122 | if ($this->a === ' ') { |
||
| 123 | if (($this->lastByteOut === '+' || $this->lastByteOut === '-') |
||
| 124 | && ($this->b === $this->lastByteOut)) { |
||
| 125 | // Don't delete this space. If we do, the addition/subtraction |
||
| 126 | // could be parsed as a post-increment |
||
| 127 | } elseif (! $this->isAlphaNum($this->b)) { |
||
| 128 | $command = self::ACTION_DELETE_A; |
||
| 129 | } |
||
| 130 | } elseif ($this->a === "\n") { |
||
| 131 | if ($this->b === ' ') { |
||
| 132 | $command = self::ACTION_DELETE_A_B; |
||
| 133 | |||
| 134 | // in case of mbstring.func_overload & 2, must check for null b, |
||
| 135 | // otherwise mb_strpos will give WARNING |
||
| 136 | } elseif ($this->b === null |
||
| 137 | || (false === strpos('{[(+-!~', $this->b) |
||
| 138 | && ! $this->isAlphaNum($this->b))) { |
||
| 139 | $command = self::ACTION_DELETE_A; |
||
| 140 | } |
||
| 141 | } elseif (! $this->isAlphaNum($this->a)) { |
||
| 142 | if ($this->b === ' ' |
||
| 143 | || ($this->b === "\n" |
||
| 144 | && (false === strpos('}])+-\'', $this->a)))) { |
||
| 145 | $command = self::ACTION_DELETE_A_B; |
||
| 146 | } |
||
| 147 | } |
||
| 148 | $this->action($command); |
||
| 149 | } |
||
| 150 | $this->output = trim($this->output); |
||
| 151 | |||
| 152 | if ($mbIntEnc !== null) { |
||
| 153 | mb_internal_encoding($mbIntEnc); |
||
|
|
|||
| 154 | } |
||
| 155 | return $this->output; |
||
| 156 | } |
||
| 157 | |||
| 158 | /** |
||
| 159 | * ACTION_KEEP_A = Output A. Copy B to A. Get the next B. |
||
| 160 | * ACTION_DELETE_A = Copy B to A. Get the next B. |
||
| 161 | * ACTION_DELETE_A_B = Get the next B. |
||
| 162 | * |
||
| 163 | * @param int $command |
||
| 164 | * @throws JSMin_UnterminatedRegExpException|JSMin_UnterminatedStringException |
||
| 165 | */ |
||
| 166 | protected function action($command) |
||
| 167 | { |
||
| 168 | // make sure we don't compress "a + ++b" to "a+++b", etc. |
||
| 169 | if ($command === self::ACTION_DELETE_A_B |
||
| 170 | && $this->b === ' ' |
||
| 171 | && ($this->a === '+' || $this->a === '-')) { |
||
| 172 | // Note: we're at an addition/substraction operator; the inputIndex |
||
| 173 | // will certainly be a valid index |
||
| 174 | if ($this->input[$this->inputIndex] === $this->a) { |
||
| 175 | // This is "+ +" or "- -". Don't delete the space. |
||
| 176 | $command = self::ACTION_KEEP_A; |
||
| 177 | } |
||
| 178 | } |
||
| 179 | |||
| 180 | switch ($command) { |
||
| 181 | case self::ACTION_KEEP_A: // 1 |
||
| 182 | $this->output .= $this->a; |
||
| 183 | |||
| 184 | if ($this->keptComment) { |
||
| 185 | $this->output = rtrim($this->output, "\n"); |
||
| 186 | $this->output .= $this->keptComment; |
||
| 187 | $this->keptComment = ''; |
||
| 188 | } |
||
| 189 | |||
| 190 | $this->lastByteOut = $this->a; |
||
| 191 | |||
| 192 | // fallthrough intentional |
||
| 193 | case self::ACTION_DELETE_A: // 2 |
||
| 194 | $this->a = $this->b; |
||
| 195 | if ($this->a === "'" || $this->a === '"') { // string literal |
||
| 196 | $str = $this->a; // in case needed for exception |
||
| 197 | for(;;) { |
||
| 198 | $this->output .= $this->a; |
||
| 199 | $this->lastByteOut = $this->a; |
||
| 200 | |||
| 201 | $this->a = $this->get(); |
||
| 202 | if ($this->a === $this->b) { // end quote |
||
| 203 | break; |
||
| 204 | } |
||
| 205 | if ($this->isEOF($this->a)) { |
||
| 206 | $byte = $this->inputIndex - 1; |
||
| 207 | throw new JSMin_UnterminatedStringException( |
||
| 208 | "JSMin: Unterminated String at byte {$byte}: {$str}"); |
||
| 209 | } |
||
| 210 | $str .= $this->a; |
||
| 211 | if ($this->a === '\\') { |
||
| 212 | $this->output .= $this->a; |
||
| 213 | $this->lastByteOut = $this->a; |
||
| 214 | |||
| 215 | $this->a = $this->get(); |
||
| 216 | $str .= $this->a; |
||
| 217 | } |
||
| 218 | } |
||
| 219 | } |
||
| 220 | |||
| 221 | // fallthrough intentional |
||
| 222 | case self::ACTION_DELETE_A_B: // 3 |
||
| 223 | $this->b = $this->next(); |
||
| 224 | if ($this->b === '/' && $this->isRegexpLiteral()) { |
||
| 225 | $this->output .= $this->a . $this->b; |
||
| 226 | $pattern = '/'; // keep entire pattern in case we need to report it in the exception |
||
| 227 | for(;;) { |
||
| 228 | $this->a = $this->get(); |
||
| 229 | $pattern .= $this->a; |
||
| 230 | if ($this->a === '[') { |
||
| 231 | for(;;) { |
||
| 232 | $this->output .= $this->a; |
||
| 233 | $this->a = $this->get(); |
||
| 234 | $pattern .= $this->a; |
||
| 235 | if ($this->a === ']') { |
||
| 236 | break; |
||
| 237 | } |
||
| 238 | if ($this->a === '\\') { |
||
| 239 | $this->output .= $this->a; |
||
| 240 | $this->a = $this->get(); |
||
| 241 | $pattern .= $this->a; |
||
| 242 | } |
||
| 243 | if ($this->isEOF($this->a)) { |
||
| 244 | throw new JSMin_UnterminatedRegExpException( |
||
| 245 | "JSMin: Unterminated set in RegExp at byte " |
||
| 246 | . $this->inputIndex .": {$pattern}"); |
||
| 247 | } |
||
| 248 | } |
||
| 249 | } |
||
| 250 | |||
| 251 | if ($this->a === '/') { // end pattern |
||
| 252 | break; // while (true) |
||
| 253 | } elseif ($this->a === '\\') { |
||
| 254 | $this->output .= $this->a; |
||
| 255 | $this->a = $this->get(); |
||
| 256 | $pattern .= $this->a; |
||
| 257 | } elseif ($this->isEOF($this->a)) { |
||
| 258 | $byte = $this->inputIndex - 1; |
||
| 259 | throw new JSMin_UnterminatedRegExpException( |
||
| 260 | "JSMin: Unterminated RegExp at byte {$byte}: {$pattern}"); |
||
| 261 | } |
||
| 262 | $this->output .= $this->a; |
||
| 263 | $this->lastByteOut = $this->a; |
||
| 264 | } |
||
| 265 | $this->b = $this->next(); |
||
| 266 | } |
||
| 267 | // end case ACTION_DELETE_A_B |
||
| 268 | } |
||
| 269 | } |
||
| 270 | |||
| 271 | /** |
||
| 272 | * @return bool |
||
| 273 | */ |
||
| 274 | protected function isRegexpLiteral() |
||
| 275 | { |
||
| 276 | if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) { |
||
| 277 | // we obviously aren't dividing |
||
| 278 | return true; |
||
| 279 | } |
||
| 280 | |||
| 281 | // we have to check for a preceding keyword, and we don't need to pattern |
||
| 282 | // match over the whole output. |
||
| 283 | $recentOutput = substr($this->output, -10); |
||
| 284 | |||
| 285 | // check if return/typeof directly precede a pattern without a space |
||
| 286 | foreach (array('return', 'typeof') as $keyword) { |
||
| 287 | if ($this->a !== substr($keyword, -1)) { |
||
| 288 | // certainly wasn't keyword |
||
| 289 | continue; |
||
| 290 | } |
||
| 291 | if (preg_match("~(^|[\\s\\S])" . substr($keyword, 0, -1) . "$~", $recentOutput, $m)) { |
||
| 292 | if ($m[1] === '' || !$this->isAlphaNum($m[1])) { |
||
| 293 | return true; |
||
| 294 | } |
||
| 295 | } |
||
| 296 | } |
||
| 297 | |||
| 298 | // check all keywords |
||
| 299 | if ($this->a === ' ' || $this->a === "\n") { |
||
| 300 | if (preg_match('~(^|[\\s\\S])(?:case|else|in|return|typeof)$~', $recentOutput, $m)) { |
||
| 301 | if ($m[1] === '' || !$this->isAlphaNum($m[1])) { |
||
| 302 | return true; |
||
| 303 | } |
||
| 304 | } |
||
| 305 | } |
||
| 306 | |||
| 307 | return false; |
||
| 308 | } |
||
| 309 | |||
| 310 | /** |
||
| 311 | * Return the next character from stdin. Watch out for lookahead. If the character is a control character, |
||
| 312 | * translate it to a space or linefeed. |
||
| 313 | * |
||
| 314 | * @return string |
||
| 315 | */ |
||
| 316 | protected function get() |
||
| 317 | { |
||
| 318 | $c = $this->lookAhead; |
||
| 319 | $this->lookAhead = null; |
||
| 320 | if ($c === null) { |
||
| 321 | // getc(stdin) |
||
| 322 | if ($this->inputIndex < $this->inputLength) { |
||
| 323 | $c = $this->input[$this->inputIndex]; |
||
| 324 | $this->inputIndex += 1; |
||
| 325 | } else { |
||
| 326 | $c = null; |
||
| 327 | } |
||
| 328 | } |
||
| 329 | if (ord($c) >= self::ORD_SPACE || $c === "\n" || $c === null) { |
||
| 330 | return $c; |
||
| 331 | } |
||
| 332 | if ($c === "\r") { |
||
| 333 | return "\n"; |
||
| 334 | } |
||
| 335 | return ' '; |
||
| 336 | } |
||
| 337 | |||
| 338 | /** |
||
| 339 | * Does $a indicate end of input? |
||
| 340 | * |
||
| 341 | * @param string $a |
||
| 342 | * @return bool |
||
| 343 | */ |
||
| 344 | protected function isEOF($a) |
||
| 345 | { |
||
| 346 | return ord($a) <= self::ORD_LF; |
||
| 347 | } |
||
| 348 | |||
| 349 | /** |
||
| 350 | * Get next char (without getting it). If is ctrl character, translate to a space or newline. |
||
| 351 | * |
||
| 352 | * @return string |
||
| 353 | */ |
||
| 354 | protected function peek() |
||
| 355 | { |
||
| 356 | $this->lookAhead = $this->get(); |
||
| 357 | return $this->lookAhead; |
||
| 358 | } |
||
| 359 | |||
| 360 | /** |
||
| 361 | * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character. |
||
| 362 | * |
||
| 363 | * @param string $c |
||
| 364 | * |
||
| 365 | * @return bool |
||
| 366 | */ |
||
| 367 | protected function isAlphaNum($c) |
||
| 368 | { |
||
| 369 | return (preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126); |
||
| 370 | } |
||
| 371 | |||
| 372 | /** |
||
| 373 | * Consume a single line comment from input (possibly retaining it) |
||
| 374 | */ |
||
| 375 | protected function consumeSingleLineComment() |
||
| 376 | { |
||
| 377 | $comment = ''; |
||
| 378 | while (true) { |
||
| 379 | $get = $this->get(); |
||
| 380 | $comment .= $get; |
||
| 381 | if (ord($get) <= self::ORD_LF) { // end of line reached |
||
| 382 | // if IE conditional comment |
||
| 383 | if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { |
||
| 384 | $this->keptComment .= "/{$comment}"; |
||
| 385 | } |
||
| 386 | return; |
||
| 387 | } |
||
| 388 | } |
||
| 389 | } |
||
| 390 | |||
| 391 | /** |
||
| 392 | * Consume a multiple line comment from input (possibly retaining it) |
||
| 393 | * |
||
| 394 | * @throws JSMin_UnterminatedCommentException |
||
| 395 | */ |
||
| 396 | protected function consumeMultipleLineComment() |
||
| 397 | { |
||
| 398 | $this->get(); |
||
| 399 | $comment = ''; |
||
| 400 | for(;;) { |
||
| 401 | $get = $this->get(); |
||
| 402 | if ($get === '*') { |
||
| 403 | if ($this->peek() === '/') { // end of comment reached |
||
| 404 | $this->get(); |
||
| 405 | if (0 === strpos($comment, '!')) { |
||
| 406 | // preserved by YUI Compressor |
||
| 407 | if (!$this->keptComment) { |
||
| 408 | // don't prepend a newline if two comments right after one another |
||
| 409 | $this->keptComment = "\n"; |
||
| 410 | } |
||
| 411 | $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n"; |
||
| 412 | } else if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { |
||
| 413 | // IE conditional |
||
| 414 | $this->keptComment .= "/*{$comment}*/"; |
||
| 415 | } |
||
| 416 | return; |
||
| 417 | } |
||
| 418 | } elseif ($get === null) { |
||
| 419 | throw new JSMin_UnterminatedCommentException( |
||
| 420 | "JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}"); |
||
| 421 | } |
||
| 422 | $comment .= $get; |
||
| 423 | } |
||
| 424 | } |
||
| 425 | |||
| 426 | /** |
||
| 427 | * Get the next character, skipping over comments. Some comments may be preserved. |
||
| 428 | * |
||
| 429 | * @return string |
||
| 430 | */ |
||
| 431 | protected function next() |
||
| 447 | } |
||
| 448 | } |
||
| 449 | |||
| 450 | class JSMin_UnterminatedStringException extends \Exception {} |
||
| 451 | class JSMin_UnterminatedCommentException extends \Exception {} |
||
| 452 | class JSMin_UnterminatedRegExpException extends \Exception {} |