├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── JShrink │ └── Minifier.php └── tests ├── JShrink └── Test │ └── JShrinkTest.php ├── Resources ├── development │ ├── input │ │ └── .gitkeep │ └── output │ │ └── .gitkeep ├── jshrink │ ├── input │ │ ├── empty_comment.js │ │ ├── ending_comment.js │ │ ├── prefix_increment.js │ │ ├── preserve-regex.js │ │ ├── preserve-strings.js │ │ ├── preserve_license.js │ │ ├── remove_multiline_comments.js │ │ ├── remove_oneline_comments.js │ │ ├── strictmode.js │ │ └── utf_chars.js │ └── output │ │ ├── empty_comment.js │ │ ├── ending_comment.js │ │ ├── prefix_increment.js │ │ ├── preserve-regex.js │ │ ├── preserve-strings.js │ │ ├── preserve_license.js │ │ ├── remove_multiline_comments.js │ │ ├── remove_oneline_comments.js │ │ ├── strictmode.js │ │ └── utf_chars.js ├── minify │ ├── input │ │ ├── 144.js │ │ ├── condcomm.js │ │ └── issue132.js │ └── output │ │ ├── 144.js │ │ ├── condcomm.js │ │ └── issue132.js ├── requests │ ├── input │ │ ├── .gitkeep │ │ ├── ifreturn.js │ │ └── whitespace.js │ └── output │ │ ├── .gitkeep │ │ ├── ifreturn.js │ │ └── whitespace.js └── uglify │ ├── README │ ├── input │ ├── array1.js │ ├── array2.js │ ├── array3.js │ ├── array4.js │ ├── assignment.js │ ├── concatstring.js │ ├── empty-blocks.js │ ├── forstatement.js │ ├── if.js │ ├── ifreturn2.js │ ├── null_string.js │ ├── strict-equals.js │ ├── var.js │ └── with.js │ └── output │ ├── array1.js │ ├── array2.js │ ├── array3.js │ ├── array4.js │ ├── assignment.js │ ├── concatstring.js │ ├── empty-blocks.js │ ├── forstatement.js │ ├── if.js │ ├── ifreturn2.js │ ├── null_string.js │ ├── strict-equals.js │ ├── var.js │ └── with.js ├── bootstrap.php └── runTests.sh /.coveralls.yml: -------------------------------------------------------------------------------- 1 | src_dir: src 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.settings 3 | /.buildpath 4 | /.project 5 | /composer.lock 6 | /vendor 7 | /report 8 | /build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - hhvm 8 | 9 | before_script: 10 | - composer self-update && composer install --dev 11 | 12 | script: ./tests/runTests.sh 13 | 14 | after_script: 15 | - php vendor/bin/coveralls -v -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributions Welcome! 2 | 3 | Pull Requests and Community Contributions are the bread and butter of open source software. Every contribution- from bug 4 | reports to feature requests, typos to full new features- are greatly appreciated. 5 | 6 | 7 | ## Important Guidelines 8 | 9 | * One Item Per Pull Request or Issue. This makes it much easier to review code and merge it back in, and prevents issues 10 | with one request from blocking another. 11 | 12 | * Code Coverage is extremely important, and pull requests are much more likely to be accepted if testing is also improved. 13 | New code should be properly tested, and all tests must pass. 14 | 15 | * Read the LICENSE document and make sure you understand it, because your code is going to be released under it. 16 | 17 | * Be prepared to make revisions. Don't be discouraged if you're asked to make changes, as that is just another step 18 | towards refining the code and getting it merged back in. 19 | 20 | * Remember to add the relevant documentation, particular the docblock comments. 21 | 22 | 23 | ## Code Styling 24 | 25 | This project follows the PSR standards set forth by the [PHP Framework Interop Group](http://www.php-fig.org/). 26 | 27 | * [PSR-0: Class and file naming conventions](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) 28 | * [PSR-1: Basic coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md) 29 | * [PSR-2: Coding style guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) 30 | 31 | All code most follow these standards to be accepted. The easiest way to accomplish this is to run php-cs-fixer once the 32 | new changes are finished. The php-cs-fixer package is installed as a development dependency of this project. 33 | 34 | composer install --dev 35 | vendor/bin/php-cs-fixer fix ./ --level="all" -vv 36 | 37 | 38 | ## Running the test suite 39 | 40 | First install dependencies using Composer. It's important to include the dev packages: 41 | 42 | composer install --dev 43 | 44 | The "runTests.sh" script runs the full test suite- phpunit, php-cs-fixer, as well as any environmental setup: 45 | 46 | tests/runTests.sh 47 | 48 | To call phpunit directly: 49 | 50 | vendor/bin/phpunit 51 | 52 | To call php-cs-fixer directly: 53 | 54 | vendor/bin/php-cs-fixer fix ./ --level="all" -vv --dry-run 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Robert Hafner 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Stash Project nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL Robert Hafner BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DO NOT FORK THIS REPO- this is a development fork. Use the link below to go to the main version of JShrink. 2 | 3 | [JShrink Main Repository](https://github.com/tedious/JShrink) 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tedivm/jshrink", 3 | "description": "Javascript Minifier built in PHP", 4 | "keywords": ["minifier","javascript"], 5 | "homepage": "http://github.com/tedious/JShrink", 6 | "type": "library", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Robert Hafner", 11 | "email": "tedivm@tedivm.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "4.0.*", 19 | "fabpot/php-cs-fixer": "0.4.0", 20 | "satooshi/php-coveralls": "dev-master" 21 | }, 22 | "autoload": { 23 | "psr-0": {"JShrink": "src/"} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/JShrink/ 7 | 8 | 9 | 10 | 11 | requests 12 | development 13 | 14 | 15 | 16 | 17 | ./src/JShrink/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/JShrink/Minifier.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | /** 12 | * JShrink 13 | * 14 | * 15 | * @package JShrink 16 | * @author Robert Hafner 17 | */ 18 | 19 | namespace JShrink; 20 | 21 | /** 22 | * Minifier 23 | * 24 | * Usage - Minifier::minify($js); 25 | * Usage - Minifier::minify($js, $options); 26 | * Usage - Minifier::minify($js, array('flaggedComments' => false)); 27 | * 28 | * @package JShrink 29 | * @author Robert Hafner 30 | * @license http://www.opensource.org/licenses/bsd-license.php BSD License 31 | */ 32 | class Minifier 33 | { 34 | /** 35 | * The input javascript to be minified. 36 | * 37 | * @var string 38 | */ 39 | protected $input; 40 | 41 | /** 42 | * The location of the character (in the input string) that is next to be 43 | * processed. 44 | * 45 | * @var int 46 | */ 47 | protected $index = 0; 48 | 49 | /** 50 | * The first of the characters currently being looked at. 51 | * 52 | * @var string 53 | */ 54 | protected $a = ''; 55 | 56 | /** 57 | * The next character being looked at (after a); 58 | * 59 | * @var string 60 | */ 61 | protected $b = ''; 62 | 63 | /** 64 | * This character is only active when certain look ahead actions take place. 65 | * 66 | * @var string 67 | */ 68 | protected $c; 69 | 70 | /** 71 | * Contains the options for the current minification process. 72 | * 73 | * @var array 74 | */ 75 | protected $options; 76 | 77 | /** 78 | * Contains the default options for minification. This array is merged with 79 | * the one passed in by the user to create the request specific set of 80 | * options (stored in the $options attribute). 81 | * 82 | * @var array 83 | */ 84 | protected static $defaultOptions = array('flaggedComments' => true); 85 | 86 | /** 87 | * Contains lock ids which are used to replace certain code patterns and 88 | * prevent them from being minified 89 | * 90 | * @var array 91 | */ 92 | protected $locks = array(); 93 | 94 | /** 95 | * Takes a string containing javascript and removes unneeded characters in 96 | * order to shrink the code without altering it's functionality. 97 | * 98 | * @param string $js The raw javascript to be minified 99 | * @param array $options Various runtime options in an associative array 100 | * @throws \Exception 101 | * @return bool|string 102 | */ 103 | public static function minify($js, $options = array()) 104 | { 105 | try { 106 | ob_start(); 107 | 108 | $jshrink = new Minifier(); 109 | $js = $jshrink->lock($js); 110 | $jshrink->minifyDirectToOutput($js, $options); 111 | 112 | // Sometimes there's a leading new line, so we trim that out here. 113 | $js = ltrim(ob_get_clean()); 114 | $js = $jshrink->unlock($js); 115 | unset($jshrink); 116 | 117 | return $js; 118 | 119 | } catch (\Exception $e) { 120 | 121 | if (isset($jshrink)) { 122 | // Since the breakdownScript function probably wasn't finished 123 | // we clean it out before discarding it. 124 | $jshrink->clean(); 125 | unset($jshrink); 126 | } 127 | 128 | // without this call things get weird, with partially outputted js. 129 | ob_end_clean(); 130 | throw $e; 131 | } 132 | } 133 | 134 | /** 135 | * Processes a javascript string and outputs only the required characters, 136 | * stripping out all unneeded characters. 137 | * 138 | * @param string $js The raw javascript to be minified 139 | * @param array $options Various runtime options in an associative array 140 | */ 141 | protected function minifyDirectToOutput($js, $options) 142 | { 143 | $this->initialize($js, $options); 144 | $this->loop(); 145 | $this->clean(); 146 | } 147 | 148 | /** 149 | * Initializes internal variables, normalizes new lines, 150 | * 151 | * @param string $js The raw javascript to be minified 152 | * @param array $options Various runtime options in an associative array 153 | */ 154 | protected function initialize($js, $options) 155 | { 156 | $this->options = array_merge(static::$defaultOptions, $options); 157 | $js = str_replace("\r\n", "\n", $js); 158 | $js = str_replace('/**/', '', $js); 159 | $this->input = str_replace("\r", "\n", $js); 160 | 161 | // We add a newline to the end of the script to make it easier to deal 162 | // with comments at the bottom of the script- this prevents the unclosed 163 | // comment error that can otherwise occur. 164 | $this->input .= PHP_EOL; 165 | 166 | // Populate "a" with a new line, "b" with the first character, before 167 | // entering the loop 168 | $this->a = "\n"; 169 | $this->b = $this->getReal(); 170 | } 171 | 172 | /** 173 | * The primary action occurs here. This function loops through the input string, 174 | * outputting anything that's relevant and discarding anything that is not. 175 | */ 176 | protected function loop() 177 | { 178 | while ($this->a !== false && !is_null($this->a) && $this->a !== '') { 179 | 180 | switch ($this->a) { 181 | // new lines 182 | case "\n": 183 | // if the next line is something that can't stand alone preserve the newline 184 | if (strpos('(-+{[@', $this->b) !== false) { 185 | echo $this->a; 186 | $this->saveString(); 187 | break; 188 | } 189 | 190 | // if B is a space we skip the rest of the switch block and go down to the 191 | // string/regex check below, resetting $this->b with getReal 192 | if($this->b === ' ') 193 | break; 194 | 195 | // otherwise we treat the newline like a space 196 | 197 | case ' ': 198 | if(static::isAlphaNumeric($this->b)) 199 | echo $this->a; 200 | 201 | $this->saveString(); 202 | break; 203 | 204 | default: 205 | switch ($this->b) { 206 | case "\n": 207 | if (strpos('}])+-"\'', $this->a) !== false) { 208 | echo $this->a; 209 | $this->saveString(); 210 | break; 211 | } else { 212 | if (static::isAlphaNumeric($this->a)) { 213 | echo $this->a; 214 | $this->saveString(); 215 | } 216 | } 217 | break; 218 | 219 | case ' ': 220 | if(!static::isAlphaNumeric($this->a)) 221 | break; 222 | 223 | default: 224 | // check for some regex that breaks stuff 225 | if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) { 226 | $this->saveRegex(); 227 | continue; 228 | } 229 | 230 | echo $this->a; 231 | $this->saveString(); 232 | break; 233 | } 234 | } 235 | 236 | // do reg check of doom 237 | $this->b = $this->getReal(); 238 | 239 | if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) 240 | $this->saveRegex(); 241 | } 242 | } 243 | 244 | /** 245 | * Resets attributes that do not need to be stored between requests so that 246 | * the next request is ready to go. Another reason for this is to make sure 247 | * the variables are cleared and are not taking up memory. 248 | */ 249 | protected function clean() 250 | { 251 | unset($this->input); 252 | $this->index = 0; 253 | $this->a = $this->b = ''; 254 | unset($this->c); 255 | unset($this->options); 256 | } 257 | 258 | /** 259 | * Returns the next string for processing based off of the current index. 260 | * 261 | * @return string 262 | */ 263 | protected function getChar() 264 | { 265 | // Check to see if we had anything in the look ahead buffer and use that. 266 | if (isset($this->c)) { 267 | $char = $this->c; 268 | unset($this->c); 269 | 270 | // Otherwise we start pulling from the input. 271 | } else { 272 | $char = substr($this->input, $this->index, 1); 273 | 274 | // If the next character doesn't exist return false. 275 | if (isset($char) && $char === false) { 276 | return false; 277 | } 278 | 279 | // Otherwise increment the pointer and use this char. 280 | $this->index++; 281 | } 282 | 283 | // Normalize all whitespace except for the newline character into a 284 | // standard space. 285 | if($char !== "\n" && ord($char) < 32) 286 | 287 | return ' '; 288 | 289 | return $char; 290 | } 291 | 292 | /** 293 | * This function gets the next "real" character. It is essentially a wrapper 294 | * around the getChar function that skips comments. This has significant 295 | * performance benefits as the skipping is done using native functions (ie, 296 | * c code) rather than in script php. 297 | * 298 | * 299 | * @return string Next 'real' character to be processed. 300 | * @throws \RuntimeException 301 | */ 302 | protected function getReal() 303 | { 304 | $startIndex = $this->index; 305 | $char = $this->getChar(); 306 | 307 | // Check to see if we're potentially in a comment 308 | if ($char !== '/') { 309 | return $char; 310 | } 311 | 312 | $this->c = $this->getChar(); 313 | 314 | if ($this->c === '/') { 315 | return $this->processOneLineComments($startIndex); 316 | 317 | } elseif ($this->c === '*') { 318 | return $this->processMultiLineComments($startIndex); 319 | } 320 | 321 | return $char; 322 | } 323 | 324 | /** 325 | * Removed one line comments, with the exception of some very specific types of 326 | * conditional comments. 327 | * 328 | * @param int $startIndex The index point where "getReal" function started 329 | * @return string 330 | */ 331 | protected function processOneLineComments($startIndex) 332 | { 333 | $thirdCommentString = substr($this->input, $this->index, 1); 334 | 335 | // kill rest of line 336 | $this->getNext("\n"); 337 | 338 | if ($thirdCommentString == '@') { 339 | $endPoint = $this->index - $startIndex; 340 | unset($this->c); 341 | $char = "\n" . substr($this->input, $startIndex, $endPoint); 342 | } else { 343 | // first one is contents of $this->c 344 | $this->getChar(); 345 | $char = $this->getChar(); 346 | } 347 | 348 | return $char; 349 | } 350 | 351 | /** 352 | * Skips multiline comments where appropriate, and includes them where needed. 353 | * Conditional comments and "license" style blocks are preserved. 354 | * 355 | * @param int $startIndex The index point where "getReal" function started 356 | * @return bool|string False if there's no character 357 | * @throws \RuntimeException Unclosed comments will throw an error 358 | */ 359 | protected function processMultiLineComments($startIndex) 360 | { 361 | $this->getChar(); // current C 362 | $thirdCommentString = $this->getChar(); 363 | 364 | // kill everything up to the next */ if it's there 365 | if ($this->getNext('*/')) { 366 | 367 | $this->getChar(); // get * 368 | $this->getChar(); // get / 369 | $char = $this->getChar(); // get next real character 370 | 371 | // Now we reinsert conditional comments and YUI-style licensing comments 372 | if (($this->options['flaggedComments'] && $thirdCommentString === '!') 373 | || ($thirdCommentString === '@') ) { 374 | 375 | // If conditional comments or flagged comments are not the first thing in the script 376 | // we need to echo a and fill it with a space before moving on. 377 | if ($startIndex > 0) { 378 | echo $this->a; 379 | $this->a = " "; 380 | 381 | // If the comment started on a new line we let it stay on the new line 382 | if ($this->input[($startIndex - 1)] === "\n") { 383 | echo "\n"; 384 | } 385 | } 386 | 387 | $endPoint = ($this->index - 1) - $startIndex; 388 | echo substr($this->input, $startIndex, $endPoint); 389 | 390 | return $char; 391 | } 392 | 393 | } else { 394 | $char = false; 395 | } 396 | 397 | if($char === false) 398 | throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2)); 399 | 400 | // if we're here c is part of the comment and therefore tossed 401 | if(isset($this->c)) 402 | unset($this->c); 403 | 404 | return $char; 405 | } 406 | 407 | /** 408 | * Pushes the index ahead to the next instance of the supplied string. If it 409 | * is found the first character of the string is returned and the index is set 410 | * to it's position. 411 | * 412 | * @param string $string 413 | * @return string|false Returns the first character of the string or false. 414 | */ 415 | protected function getNext($string) 416 | { 417 | // Find the next occurrence of "string" after the current position. 418 | $pos = strpos($this->input, $string, $this->index); 419 | 420 | // If it's not there return false. 421 | if($pos === false) 422 | 423 | return false; 424 | 425 | // Adjust position of index to jump ahead to the asked for string 426 | $this->index = $pos; 427 | 428 | // Return the first character of that string. 429 | return substr($this->input, $this->index, 1); 430 | } 431 | 432 | /** 433 | * When a javascript string is detected this function crawls for the end of 434 | * it and saves the whole string. 435 | * 436 | * @throws \RuntimeException Unclosed strings will throw an error 437 | */ 438 | protected function saveString() 439 | { 440 | $startpos = $this->index; 441 | 442 | // saveString is always called after a gets cleared, so we push b into 443 | // that spot. 444 | $this->a = $this->b; 445 | 446 | // If this isn't a string we don't need to do anything. 447 | if ($this->a !== "'" && $this->a !== '"') { 448 | return; 449 | } 450 | 451 | // String type is the quote used, " or ' 452 | $stringType = $this->a; 453 | 454 | // Echo out that starting quote 455 | echo $this->a; 456 | 457 | // Loop until the string is done 458 | while (true) { 459 | 460 | // Grab the very next character and load it into a 461 | $this->a = $this->getChar(); 462 | 463 | switch ($this->a) { 464 | 465 | // If the string opener (single or double quote) is used 466 | // output it and break out of the while loop- 467 | // The string is finished! 468 | case $stringType: 469 | break 2; 470 | 471 | // New lines in strings without line delimiters are bad- actual 472 | // new lines will be represented by the string \n and not the actual 473 | // character, so those will be treated just fine using the switch 474 | // block below. 475 | case "\n": 476 | throw new \RuntimeException('Unclosed string at position: ' . $startpos ); 477 | break; 478 | 479 | // Escaped characters get picked up here. If it's an escaped new line it's not really needed 480 | case '\\': 481 | 482 | // a is a slash. We want to keep it, and the next character, 483 | // unless it's a new line. New lines as actual strings will be 484 | // preserved, but escaped new lines should be reduced. 485 | $this->b = $this->getChar(); 486 | 487 | // If b is a new line we discard a and b and restart the loop. 488 | if ($this->b === "\n") { 489 | break; 490 | } 491 | 492 | // echo out the escaped character and restart the loop. 493 | echo $this->a . $this->b; 494 | break; 495 | 496 | 497 | // Since we're not dealing with any special cases we simply 498 | // output the character and continue our loop. 499 | default: 500 | echo $this->a; 501 | } 502 | } 503 | } 504 | 505 | /** 506 | * When a regular expression is detected this function crawls for the end of 507 | * it and saves the whole regex. 508 | * 509 | * @throws \RuntimeException Unclosed regex will throw an error 510 | */ 511 | protected function saveRegex() 512 | { 513 | echo $this->a . $this->b; 514 | 515 | while (($this->a = $this->getChar()) !== false) { 516 | if($this->a === '/') 517 | break; 518 | 519 | if ($this->a === '\\') { 520 | echo $this->a; 521 | $this->a = $this->getChar(); 522 | } 523 | 524 | if($this->a === "\n") 525 | throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index); 526 | 527 | echo $this->a; 528 | } 529 | $this->b = $this->getReal(); 530 | } 531 | 532 | /** 533 | * Checks to see if a character is alphanumeric. 534 | * 535 | * @param string $char Just one character 536 | * @return bool 537 | */ 538 | protected static function isAlphaNumeric($char) 539 | { 540 | return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/'; 541 | } 542 | 543 | /** 544 | * Replace patterns in the given string and store the replacement 545 | * 546 | * @param string $js The string to lock 547 | * @return bool 548 | */ 549 | protected function lock($js) 550 | { 551 | /* lock things like "asd" + ++x; */ 552 | $lock = '"LOCK---' . crc32(time()) . '"'; 553 | 554 | $matches = array(); 555 | preg_match('/([+-])(\s+)([+-])/S', $js, $matches); 556 | if (empty($matches)) { 557 | return $js; 558 | } 559 | 560 | $this->locks[$lock] = $matches[2]; 561 | 562 | $js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js); 563 | /* -- */ 564 | 565 | return $js; 566 | } 567 | 568 | /** 569 | * Replace "locks" with the original characters 570 | * 571 | * @param string $js The string to unlock 572 | * @return bool 573 | */ 574 | protected function unlock($js) 575 | { 576 | if (empty($this->locks)) { 577 | return $js; 578 | } 579 | 580 | foreach ($this->locks as $lock => $replacement) { 581 | $js = str_replace($lock, $replacement, $js); 582 | } 583 | 584 | return $js; 585 | } 586 | 587 | } 588 | -------------------------------------------------------------------------------- /tests/JShrink/Test/JShrinkTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JShrink\Test; 13 | 14 | use JShrink\Minifier; 15 | 16 | class JShrinkTest extends \PHPUnit_Framework_TestCase 17 | { 18 | /** 19 | * @expectedException RuntimeException 20 | * @expectedExceptionMessage Unclosed multiline comment at position: 1 21 | */ 22 | public function testUnclosedCommentException() 23 | { 24 | \JShrink\Minifier::minify('/* This comment is hanging out.'); 25 | } 26 | 27 | /** 28 | * @expectedException RuntimeException 29 | * @expectedExceptionMessage Unclosed string at position: 14 30 | */ 31 | public function testUnclosedStringException() 32 | { 33 | \JShrink\Minifier::minify('var string = "This string is hanging out.'); 34 | } 35 | 36 | /** 37 | * @expectedException RuntimeException 38 | * @expectedExceptionMessage Unclosed regex pattern at position: 23 39 | */ 40 | public function testUnclosedRegexException() 41 | { 42 | \JShrink\Minifier::minify('var re = /[^A-Za-z0-9_ 43 | var string = "Another Filler"'); 44 | } 45 | 46 | /** 47 | * @jshrink 48 | * @dataProvider JShrinkProvider 49 | */ 50 | public function testJShrink($testName, $input, $output) 51 | { 52 | $this->assertEquals($output, \JShrink\Minifier::minify($input), 'Running JShrink Test: ' . $testName); 53 | } 54 | 55 | /** 56 | * @uglify 57 | * @dataProvider uglifyProvider 58 | */ 59 | public function testUglify($testName, $input, $output) 60 | { 61 | $this->assertEquals($output, \JShrink\Minifier::minify($input), 'Running Uglify Test: ' . $testName); 62 | } 63 | 64 | /** 65 | * @group requests 66 | * @dataProvider requestProvider 67 | */ 68 | public function testRequests($testName, $input, $output) 69 | { 70 | $this->assertEquals($output, \JShrink\Minifier::minify($input), 'Running User Requested Test: ' . $testName); 71 | } 72 | 73 | /** 74 | * @group development 75 | * @dataProvider developmentProvider 76 | */ 77 | public function testDevelopment($testName, $input, $output) 78 | { 79 | $this->assertEquals($output, \JShrink\Minifier::minify($input), 'Running Development Test: ' . $testName); 80 | } 81 | 82 | /** 83 | * This function loads all of the test cases from the specified group. 84 | * Groups are created simply by populating the appropriate directories: 85 | * 86 | * /tests/Resources/GROUPNAME/input/ 87 | * /tests/Resources/GROUPNAME/output/ 88 | * 89 | * Each test case should have two identically named files, with the raw 90 | * javascript going in the test folder and the expected results to be in 91 | * the output folder. 92 | * 93 | * @param $group string 94 | * @return array 95 | */ 96 | public function getTestFiles($group) 97 | { 98 | $baseDir = __DIR__ . '/../../Resources/' . $group . '/'; 99 | $testDir = $baseDir . 'input/'; 100 | $expectDir = $baseDir . 'output/'; 101 | 102 | $returnData = array(); 103 | 104 | $testFiles = scandir($testDir); 105 | foreach ($testFiles as $testFile) { 106 | if(substr($testFile, -3) !== '.js' || !file_exists(($expectDir . $testFile))) 107 | continue; 108 | 109 | $testInput = file_get_contents($testDir . $testFile); 110 | $testOutput = file_get_contents($expectDir . $testFile); 111 | 112 | $returnData[] = array($testFile, $testInput, $testOutput); 113 | } 114 | 115 | return $returnData; 116 | } 117 | 118 | public function uglifyProvider() 119 | { 120 | return $this->getTestFiles('uglify'); 121 | } 122 | 123 | public function JShrinkProvider() 124 | { 125 | return $this->getTestFiles('jshrink'); 126 | } 127 | 128 | public function requestProvider() 129 | { 130 | return $this->getTestFiles('requests'); 131 | } 132 | 133 | public function developmentProvider() 134 | { 135 | return $this->getTestFiles('development'); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Resources/development/input/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedivm/JShrink/f8765d3abbd4758951bb5217b5dbe82f3de23a45/tests/Resources/development/input/.gitkeep -------------------------------------------------------------------------------- /tests/Resources/development/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedivm/JShrink/f8765d3abbd4758951bb5217b5dbe82f3de23a45/tests/Resources/development/output/.gitkeep -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/empty_comment.js: -------------------------------------------------------------------------------- 1 | /**/ 2 | var test; 3 | /**/ 4 | -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/ending_comment.js: -------------------------------------------------------------------------------- 1 | var sth = "sth"; //comment -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/prefix_increment.js: -------------------------------------------------------------------------------- 1 | do{div.innerHTML=""} while(1) -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/preserve-regex.js: -------------------------------------------------------------------------------- 1 | var re = /[^A-Za-z0-9_\\]/ 2 | var string = "Just a filler string"; -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/preserve-strings.js: -------------------------------------------------------------------------------- 1 | var test = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 2 | 3 | var test = "abcdefg\"hijklmnopqrst\"uvwxyzABCDEFGHIJKLMNO\"PQRSTUVWXYZ0123456789"; 4 | 5 | 6 | 7 | var test = "abcdefghij\ 8 | klmnopqrstuvwxyzABCD \ 9 | EFGHIJKLMNOPQRSTUVWXYZ0123456789"; -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/preserve_license.js: -------------------------------------------------------------------------------- 1 | /*! 2 | This comment should be preserved. 3 | */ 4 | 5 | var test; 6 | var test; 7 | /*! 8 | This comment should be preserved. 9 | */ 10 | 11 | var test; -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/remove_multiline_comments.js: -------------------------------------------------------------------------------- 1 | /* This line should not be there later. */ 2 | 3 | var test; 4 | var test; 5 | var test; 6 | 7 | /* 8 | Neither should this one. 9 | */ 10 | 11 | var test; 12 | var test; 13 | 14 | /* 15 | Or this one. 16 | */ 17 | 18 | var test; 19 | 20 | 21 | /** 22 | * 23 | * Even if this one is special! 24 | * 25 | */ -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/remove_oneline_comments.js: -------------------------------------------------------------------------------- 1 | // This line should not be there later. 2 | 3 | var test; 4 | var test; 5 | var test; 6 | 7 | // Neither should this one. 8 | var test; 9 | var test; 10 | 11 | // Or this one. 12 | 13 | var test; -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/strictmode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var foo=22; -------------------------------------------------------------------------------- /tests/Resources/jshrink/input/utf_chars.js: -------------------------------------------------------------------------------- 1 | var π = Math.PI, 2 | ε = 1e-6, 3 | radians = π / 180, 4 | degrees = 180 / π; 5 | 6 | function sgn(x) { 7 | 8 | 9 | var π = Math.PI; 10 | return x > 0 ? 1 : x < 0 ? -1 : 0; 11 | } -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/empty_comment.js: -------------------------------------------------------------------------------- 1 | var test; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/ending_comment.js: -------------------------------------------------------------------------------- 1 | var sth="sth"; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/prefix_increment.js: -------------------------------------------------------------------------------- 1 | do{div.innerHTML=""}while(1) -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/preserve-regex.js: -------------------------------------------------------------------------------- 1 | var re=/[^A-Za-z0-9_\\]/ 2 | var string="Just a filler string"; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/preserve-strings.js: -------------------------------------------------------------------------------- 1 | var test="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";var test="abcdefg\"hijklmnopqrst\"uvwxyzABCDEFGHIJKLMNO\"PQRSTUVWXYZ0123456789";var test="abcdefghijklmnopqrstuvwxyzABCD EFGHIJKLMNOPQRSTUVWXYZ0123456789"; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/preserve_license.js: -------------------------------------------------------------------------------- 1 | /*! 2 | This comment should be preserved. 3 | */ 4 | var test;var test; 5 | /*! 6 | This comment should be preserved. 7 | */ 8 | var test; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/remove_multiline_comments.js: -------------------------------------------------------------------------------- 1 | var test;var test;var test;var test;var test;var test; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/remove_oneline_comments.js: -------------------------------------------------------------------------------- 1 | var test;var test;var test;var test;var test;var test; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/strictmode.js: -------------------------------------------------------------------------------- 1 | "use strict";var foo=22; -------------------------------------------------------------------------------- /tests/Resources/jshrink/output/utf_chars.js: -------------------------------------------------------------------------------- 1 | var π=Math.PI,ε=1e-6,radians=π/ 180,degrees=180 / π;function sgn(x){var π=Math.PI;return x>0?1:x<0?-1:0;} -------------------------------------------------------------------------------- /tests/Resources/minify/input/144.js: -------------------------------------------------------------------------------- 1 | a / ++b; 2 | a * --b; 3 | a++ - b; 4 | a + --b; 5 | a - ++b; 6 | a + -b; 7 | a + ++b; 8 | a + --b; 9 | a - --b; -------------------------------------------------------------------------------- /tests/Resources/minify/input/condcomm.js: -------------------------------------------------------------------------------- 1 | var isWin; 2 | /*@cc_on 3 | @if (@_win32) 4 | isWin = true; 5 | @else @*/ isWin = false; 6 | /*@end 7 | @*/ 8 | 9 | isWin = /*@cc_on!*/!1; 10 | 11 | var recognizesCondComm = true; 12 | //@cc_on/* 13 | recognizesCondComm = false; 14 | //@cc_on*/ 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/Resources/minify/input/issue132.js: -------------------------------------------------------------------------------- 1 | // from jQuery tablesorter 2 | ts.addParser({ 3 | id: "currency", 4 | is: function(s) { 5 | return /^[£$€?.]/.test(s); 6 | }, 7 | }); -------------------------------------------------------------------------------- /tests/Resources/minify/output/144.js: -------------------------------------------------------------------------------- 1 | a/++b;a*--b;a++-b;a+--b;a-++b;a+-b;a+ ++b;a+--b;a- --b; -------------------------------------------------------------------------------- /tests/Resources/minify/output/condcomm.js: -------------------------------------------------------------------------------- 1 | var isWin; 2 | /*@cc_on 3 | @if (@_win32) 4 | isWin = true; 5 | @else @*/ isWin=false; 6 | /*@end 7 | @*/ 8 | isWin=/*@cc_on!*/!1;var recognizesCondComm=true; 9 | //@cc_on/* 10 | recognizesCondComm=false; 11 | //@cc_on*/ -------------------------------------------------------------------------------- /tests/Resources/minify/output/issue132.js: -------------------------------------------------------------------------------- 1 | ts.addParser({id:"currency",is:function(s){return /^[£$€?.]/.test(s);},}); -------------------------------------------------------------------------------- /tests/Resources/requests/input/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedivm/JShrink/f8765d3abbd4758951bb5217b5dbe82f3de23a45/tests/Resources/requests/input/.gitkeep -------------------------------------------------------------------------------- /tests/Resources/requests/input/ifreturn.js: -------------------------------------------------------------------------------- 1 | function a(b) { 2 | if (b == 1) { 3 | return 2; 4 | } else { 5 | return 17; 6 | } 7 | 8 | return 3; 9 | } -------------------------------------------------------------------------------- /tests/Resources/requests/input/whitespace.js: -------------------------------------------------------------------------------- 1 | function id(a) { 2 | // Form-Feed 3 | // Vertical Tab 4 | // No-Break Space 5 | ᠎// Mongolian Vowel Separator 6 |  // En quad 7 |  // Em quad 8 |  // En space 9 |  // Em space 10 |  // Three-Per-Em Space 11 |  // Four-Per-Em Space 12 |  // Six-Per-Em Space 13 |  // Figure Space 14 |  // Punctuation Space 15 |  // Thin Space 16 |  // Hair Space 17 |  // Narrow No-Break Space 18 |  // Medium Mathematical Space 19 |  // Ideographic Space 20 | return a; 21 | } 22 | -------------------------------------------------------------------------------- /tests/Resources/requests/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tedivm/JShrink/f8765d3abbd4758951bb5217b5dbe82f3de23a45/tests/Resources/requests/output/.gitkeep -------------------------------------------------------------------------------- /tests/Resources/requests/output/ifreturn.js: -------------------------------------------------------------------------------- 1 | function a(b){if(b==1){return 2;}else{return 17;} 2 | return 3;} -------------------------------------------------------------------------------- /tests/Resources/requests/output/whitespace.js: -------------------------------------------------------------------------------- 1 | function id(a){return a;} -------------------------------------------------------------------------------- /tests/Resources/uglify/README: -------------------------------------------------------------------------------- 1 | The files contained in this subdirectory are test cases that have been copied from the uglify.js test suite, and then subsequently unmangled by Akshay Joshi. 2 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/array1.js: -------------------------------------------------------------------------------- 1 | new Array(); 2 | new Array(1); 3 | new Array(1, 2, 3); 4 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/array2.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var Array = function(){}; 3 | return new Array(1, 2, 3, 4); 4 | })(); 5 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/array3.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | return new Array(1, 2, 3, 4); 3 | function Array() {}; 4 | })(); 5 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/array4.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | (function(){ 3 | return new Array(1, 2, 3); 4 | })(); 5 | function Array(){}; 6 | })(); 7 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/assignment.js: -------------------------------------------------------------------------------- 1 | a=1; 2 | b=a; 3 | c=1; 4 | d=b; 5 | e=d; 6 | longname=2; 7 | if (longname+1) { 8 | x=3; 9 | if (x) var z = 7; 10 | } 11 | z=1,y=1,x=1 12 | 13 | g+=1; 14 | h=g; 15 | 16 | ++i; 17 | j=i; 18 | 19 | i++; 20 | j=i+17; -------------------------------------------------------------------------------- /tests/Resources/uglify/input/concatstring.js: -------------------------------------------------------------------------------- 1 | var a = a + "a" + "b" + 1 + c; 2 | var b = a + "c" + "ds" + 123 + c; 3 | var c = a + "c" + 123 + d + "ds" + c; -------------------------------------------------------------------------------- /tests/Resources/uglify/input/empty-blocks.js: -------------------------------------------------------------------------------- 1 | var x = 5; 2 | function bar() { return --x; } 3 | function foo() { while (bar()); } 4 | function mak() { for(;;); } 5 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/forstatement.js: -------------------------------------------------------------------------------- 1 | a=func(); 2 | b=z; 3 | for (a++; i < 10; i++) { alert(i); } 4 | 5 | var z=1; 6 | g=2; 7 | for (; i < 10; i++) { alert(i); } 8 | 9 | var a = 2; 10 | for (var i = 1; i < 10; i++) { alert(i); } 11 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/if.js: -------------------------------------------------------------------------------- 1 | var a = 1; 2 | if (a == 1) { 3 | a = 2; 4 | } else { 5 | a = 17; 6 | } 7 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/ifreturn2.js: -------------------------------------------------------------------------------- 1 | function x(a) { 2 | if (typeof a === 'object') 3 | return a; 4 | 5 | if (a === 42) 6 | return 0; 7 | 8 | return a * 2; 9 | } 10 | 11 | function y(a) { 12 | if (typeof a === 'object') 13 | return a; 14 | 15 | return null; 16 | }; 17 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/null_string.js: -------------------------------------------------------------------------------- 1 | var nullString = "\0" -------------------------------------------------------------------------------- /tests/Resources/uglify/input/strict-equals.js: -------------------------------------------------------------------------------- 1 | typeof a === 'string' 2 | b + "" !== c + "" 3 | d < e === f < g 4 | -------------------------------------------------------------------------------- /tests/Resources/uglify/input/var.js: -------------------------------------------------------------------------------- 1 | var a = 1; 2 | var b = 2; -------------------------------------------------------------------------------- /tests/Resources/uglify/input/with.js: -------------------------------------------------------------------------------- 1 | with({}) { 2 | }; 3 | -------------------------------------------------------------------------------- /tests/Resources/uglify/output/array1.js: -------------------------------------------------------------------------------- 1 | new Array();new Array(1);new Array(1,2,3); -------------------------------------------------------------------------------- /tests/Resources/uglify/output/array2.js: -------------------------------------------------------------------------------- 1 | (function(){var Array=function(){};return new Array(1,2,3,4);})(); -------------------------------------------------------------------------------- /tests/Resources/uglify/output/array3.js: -------------------------------------------------------------------------------- 1 | (function(){return new Array(1,2,3,4);function Array(){};})(); -------------------------------------------------------------------------------- /tests/Resources/uglify/output/array4.js: -------------------------------------------------------------------------------- 1 | (function(){(function(){return new Array(1,2,3);})();function Array(){};})(); -------------------------------------------------------------------------------- /tests/Resources/uglify/output/assignment.js: -------------------------------------------------------------------------------- 1 | a=1;b=a;c=1;d=b;e=d;longname=2;if(longname+1){x=3;if(x)var z=7;} 2 | z=1,y=1,x=1 3 | g+=1;h=g;++i;j=i;i++;j=i+17; -------------------------------------------------------------------------------- /tests/Resources/uglify/output/concatstring.js: -------------------------------------------------------------------------------- 1 | var a=a+"a"+"b"+1+c;var b=a+"c"+"ds"+123+c;var c=a+"c"+123+d+"ds"+c; -------------------------------------------------------------------------------- /tests/Resources/uglify/output/empty-blocks.js: -------------------------------------------------------------------------------- 1 | var x=5;function bar(){return--x;} 2 | function foo(){while(bar());} 3 | function mak(){for(;;);} -------------------------------------------------------------------------------- /tests/Resources/uglify/output/forstatement.js: -------------------------------------------------------------------------------- 1 | a=func();b=z;for(a++;i<10;i++){alert(i);} 2 | var z=1;g=2;for(;i<10;i++){alert(i);} 3 | var a=2;for(var i=1;i<10;i++){alert(i);} -------------------------------------------------------------------------------- /tests/Resources/uglify/output/if.js: -------------------------------------------------------------------------------- 1 | var a=1;if(a==1){a=2;}else{a=17;} -------------------------------------------------------------------------------- /tests/Resources/uglify/output/ifreturn2.js: -------------------------------------------------------------------------------- 1 | function x(a){if(typeof a==='object') 2 | return a;if(a===42) 3 | return 0;return a*2;} 4 | function y(a){if(typeof a==='object') 5 | return a;return null;}; -------------------------------------------------------------------------------- /tests/Resources/uglify/output/null_string.js: -------------------------------------------------------------------------------- 1 | var nullString="\0" -------------------------------------------------------------------------------- /tests/Resources/uglify/output/strict-equals.js: -------------------------------------------------------------------------------- 1 | typeof a==='string' 2 | b+""!==c+"" 3 | d 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | define('TESTING', true);// 13 | error_reporting(-1); 14 | 15 | date_default_timezone_set('UTC'); 16 | 17 | $filename = __DIR__ .'/../vendor/autoload.php'; 18 | 19 | if (!file_exists($filename)) { 20 | echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" . PHP_EOL; 21 | echo " You need to execute `composer install` before running the tests. " . PHP_EOL; 22 | echo " Vendors are required for complete test execution. " . PHP_EOL; 23 | echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" . PHP_EOL . PHP_EOL; 24 | $filename = __DIR__ .'/../autoload.php'; 25 | } 26 | 27 | $loader = require $filename; 28 | $loader->add('JShrink\\Test', __DIR__); 29 | -------------------------------------------------------------------------------- /tests/runTests.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env/sh 2 | set -e 3 | 4 | echo 'Running unit tests.' 5 | ./vendor/bin/phpunit --verbose --coverage-clover build/logs/clover.xml 6 | 7 | echo '' 8 | echo '' 9 | echo '' 10 | echo 'Testing for Coding Styling Compliance.' 11 | echo 'All code should follow PSR standards.' 12 | ./vendor/bin/php-cs-fixer fix ./ --level="all" -vv --dry-run --------------------------------------------------------------------------------