├── README.md ├── jsmin.php └── test ├── .gitignore ├── setup.php ├── test.php └── utf8-with-bom.js /README.md: -------------------------------------------------------------------------------- 1 | jsmin-php 2 | ========= 3 | 4 | This project is unmaintained. I stopped using it years ago. You shouldn't use 5 | it. You shouldn't use any version of JSMin. There are much better tools 6 | available now. 7 | 8 | Here are some of them: 9 | 10 | - [Uglify](https://github.com/mishoo/UglifyJS2) 11 | - [Google Closure Compiler](https://developers.google.com/closure/compiler/) 12 | - [JShrink](https://github.com/tedivm/JShrink) -------------------------------------------------------------------------------- /jsmin.php: -------------------------------------------------------------------------------- 1 | 41 | * @copyright 2002 Douglas Crockford (jsmin.c) 42 | * @copyright 2008 Ryan Grove (PHP port) 43 | * @copyright 2012 Adam Goforth (Updates) 44 | * @license http://opensource.org/licenses/mit-license.php MIT License 45 | * @version 1.1.2 (2012-05-01) 46 | * @link https://github.com/rgrove/jsmin-php 47 | */ 48 | 49 | class JSMin { 50 | const ORD_LF = 10; 51 | const ORD_SPACE = 32; 52 | const ACTION_KEEP_A = 1; 53 | const ACTION_DELETE_A = 2; 54 | const ACTION_DELETE_A_B = 3; 55 | 56 | protected $a = ''; 57 | protected $b = ''; 58 | protected $input = ''; 59 | protected $inputIndex = 0; 60 | protected $inputLength = 0; 61 | protected $lookAhead = null; 62 | protected $output = ''; 63 | 64 | // -- Public Static Methods -------------------------------------------------- 65 | 66 | /** 67 | * Minify Javascript 68 | * 69 | * @uses __construct() 70 | * @uses min() 71 | * @param string $js Javascript to be minified 72 | * @return string 73 | */ 74 | public static function minify($js) { 75 | $jsmin = new JSMin($js); 76 | return $jsmin->min(); 77 | } 78 | 79 | // -- Public Instance Methods ------------------------------------------------ 80 | 81 | /** 82 | * Constructor 83 | * 84 | * @param string $input Javascript to be minified 85 | */ 86 | public function __construct($input) { 87 | $this->input = str_replace("\r\n", "\n", $input); 88 | $this->inputLength = strlen($this->input); 89 | } 90 | 91 | // -- Protected Instance Methods --------------------------------------------- 92 | 93 | /** 94 | * Action -- do something! What to do is determined by the $command argument. 95 | * 96 | * action treats a string as a single character. Wow! 97 | * action recognizes a regular expression if it is preceded by ( or , or =. 98 | * 99 | * @uses next() 100 | * @uses get() 101 | * @throws JSMinException If parser errors are found: 102 | * - Unterminated string literal 103 | * - Unterminated regular expression set in regex literal 104 | * - Unterminated regular expression literal 105 | * @param int $command One of class constants: 106 | * ACTION_KEEP_A Output A. Copy B to A. Get the next B. 107 | * ACTION_DELETE_A Copy B to A. Get the next B. (Delete A). 108 | * ACTION_DELETE_A_B Get the next B. (Delete B). 109 | */ 110 | protected function action($command) { 111 | switch($command) { 112 | case self::ACTION_KEEP_A: 113 | $this->output .= $this->a; 114 | 115 | case self::ACTION_DELETE_A: 116 | $this->a = $this->b; 117 | 118 | if ($this->a === "'" || $this->a === '"') { 119 | for (;;) { 120 | $this->output .= $this->a; 121 | $this->a = $this->get(); 122 | 123 | if ($this->a === $this->b) { 124 | break; 125 | } 126 | 127 | if (ord($this->a) <= self::ORD_LF) { 128 | throw new JSMinException('Unterminated string literal.'); 129 | } 130 | 131 | if ($this->a === '\\') { 132 | $this->output .= $this->a; 133 | $this->a = $this->get(); 134 | } 135 | } 136 | } 137 | 138 | case self::ACTION_DELETE_A_B: 139 | $this->b = $this->next(); 140 | 141 | if ($this->b === '/' && ( 142 | $this->a === '(' || $this->a === ',' || $this->a === '=' || 143 | $this->a === ':' || $this->a === '[' || $this->a === '!' || 144 | $this->a === '&' || $this->a === '|' || $this->a === '?' || 145 | $this->a === '{' || $this->a === '}' || $this->a === ';' || 146 | $this->a === "\n" )) { 147 | 148 | $this->output .= $this->a . $this->b; 149 | 150 | for (;;) { 151 | $this->a = $this->get(); 152 | 153 | if ($this->a === '[') { 154 | /* 155 | inside a regex [...] set, which MAY contain a '/' itself. Example: mootools Form.Validator near line 460: 156 | return Form.Validator.getValidator('IsEmpty').test(element) || (/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]\.?){0,63}[a-z0-9!#$%&'*+/=?^_`{|}~-]@(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])$/i).test(element.get('value')); 157 | */ 158 | for (;;) { 159 | $this->output .= $this->a; 160 | $this->a = $this->get(); 161 | 162 | if ($this->a === ']') { 163 | break; 164 | } elseif ($this->a === '\\') { 165 | $this->output .= $this->a; 166 | $this->a = $this->get(); 167 | } elseif (ord($this->a) <= self::ORD_LF) { 168 | throw new JSMinException('Unterminated regular expression set in regex literal.'); 169 | } 170 | } 171 | } elseif ($this->a === '/') { 172 | break; 173 | } elseif ($this->a === '\\') { 174 | $this->output .= $this->a; 175 | $this->a = $this->get(); 176 | } elseif (ord($this->a) <= self::ORD_LF) { 177 | throw new JSMinException('Unterminated regular expression literal.'); 178 | } 179 | 180 | $this->output .= $this->a; 181 | } 182 | 183 | $this->b = $this->next(); 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Get next char. Convert ctrl char to space. 190 | * 191 | * @return string|null 192 | */ 193 | protected function get() { 194 | $c = $this->lookAhead; 195 | $this->lookAhead = null; 196 | 197 | if ($c === null) { 198 | if ($this->inputIndex < $this->inputLength) { 199 | $c = substr($this->input, $this->inputIndex, 1); 200 | $this->inputIndex += 1; 201 | } else { 202 | $c = null; 203 | } 204 | } 205 | 206 | if ($c === "\r") { 207 | return "\n"; 208 | } 209 | 210 | if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) { 211 | return $c; 212 | } 213 | 214 | return ' '; 215 | } 216 | 217 | /** 218 | * Is $c a letter, digit, underscore, dollar sign, or non-ASCII character. 219 | * 220 | * @return bool 221 | */ 222 | protected function isAlphaNum($c) { 223 | return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; 224 | } 225 | 226 | /** 227 | * Perform minification, return result 228 | * 229 | * @uses action() 230 | * @uses isAlphaNum() 231 | * @uses get() 232 | * @uses peek() 233 | * @return string 234 | */ 235 | protected function min() { 236 | if (0 == strncmp($this->peek(), "\xef", 1)) { 237 | $this->get(); 238 | $this->get(); 239 | $this->get(); 240 | } 241 | 242 | $this->a = "\n"; 243 | $this->action(self::ACTION_DELETE_A_B); 244 | 245 | while ($this->a !== null) { 246 | switch ($this->a) { 247 | case ' ': 248 | if ($this->isAlphaNum($this->b)) { 249 | $this->action(self::ACTION_KEEP_A); 250 | } else { 251 | $this->action(self::ACTION_DELETE_A); 252 | } 253 | break; 254 | 255 | case "\n": 256 | switch ($this->b) { 257 | case '{': 258 | case '[': 259 | case '(': 260 | case '+': 261 | case '-': 262 | case '!': 263 | case '~': 264 | $this->action(self::ACTION_KEEP_A); 265 | break; 266 | 267 | case ' ': 268 | $this->action(self::ACTION_DELETE_A_B); 269 | break; 270 | 271 | default: 272 | if ($this->isAlphaNum($this->b)) { 273 | $this->action(self::ACTION_KEEP_A); 274 | } 275 | else { 276 | $this->action(self::ACTION_DELETE_A); 277 | } 278 | } 279 | break; 280 | 281 | default: 282 | switch ($this->b) { 283 | case ' ': 284 | if ($this->isAlphaNum($this->a)) { 285 | $this->action(self::ACTION_KEEP_A); 286 | break; 287 | } 288 | 289 | $this->action(self::ACTION_DELETE_A_B); 290 | break; 291 | 292 | case "\n": 293 | switch ($this->a) { 294 | case '}': 295 | case ']': 296 | case ')': 297 | case '+': 298 | case '-': 299 | case '"': 300 | case "'": 301 | $this->action(self::ACTION_KEEP_A); 302 | break; 303 | 304 | default: 305 | if ($this->isAlphaNum($this->a)) { 306 | $this->action(self::ACTION_KEEP_A); 307 | } 308 | else { 309 | $this->action(self::ACTION_DELETE_A_B); 310 | } 311 | } 312 | break; 313 | 314 | default: 315 | $this->action(self::ACTION_KEEP_A); 316 | break; 317 | } 318 | } 319 | } 320 | 321 | return $this->output; 322 | } 323 | 324 | /** 325 | * Get the next character, skipping over comments. peek() is used to see 326 | * if a '/' is followed by a '/' or '*'. 327 | * 328 | * @uses get() 329 | * @uses peek() 330 | * @throws JSMinException On unterminated comment. 331 | * @return string 332 | */ 333 | protected function next() { 334 | $c = $this->get(); 335 | 336 | if ($c === '/') { 337 | switch($this->peek()) { 338 | case '/': 339 | for (;;) { 340 | $c = $this->get(); 341 | 342 | if (ord($c) <= self::ORD_LF) { 343 | return $c; 344 | } 345 | } 346 | 347 | case '*': 348 | $this->get(); 349 | 350 | for (;;) { 351 | switch($this->get()) { 352 | case '*': 353 | if ($this->peek() === '/') { 354 | $this->get(); 355 | return ' '; 356 | } 357 | break; 358 | 359 | case null: 360 | throw new JSMinException('Unterminated comment.'); 361 | } 362 | } 363 | 364 | default: 365 | return $c; 366 | } 367 | } 368 | 369 | return $c; 370 | } 371 | 372 | /** 373 | * Get next char. If is ctrl character, translate to a space or newline. 374 | * 375 | * @uses get() 376 | * @return string|null 377 | */ 378 | protected function peek() { 379 | $this->lookAhead = $this->get(); 380 | return $this->lookAhead; 381 | } 382 | } 383 | 384 | // -- Exceptions --------------------------------------------------------------- 385 | class JSMinException extends Exception {} 386 | ?> 387 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | libs/ 2 | jsmin 3 | jsmin.c 4 | -------------------------------------------------------------------------------- /test/setup.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js.uncompressed.js', 7 | 'ext' => 'https://ajax.googleapis.com/ajax/libs/ext-core/3.1.0/ext-core-debug.js', 8 | 'jquery' => 'https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js', 9 | 'mootools' => 'https://ajax.googleapis.com/ajax/libs/mootools/1.3.0/mootools.js', 10 | 'yui' => 'http://yui.yahooapis.com/3.3.0/build/yui/yui.js' 11 | ); 12 | 13 | // Download latest JSMin and compile it. 14 | echo "Fetching $url_jsmin...\n"; 15 | file_put_contents(__DIR__ . '/jsmin.c', file_get_contents($url_jsmin)); 16 | 17 | echo "Compiling jsmin.c...\n"; 18 | if (system('cc jsmin.c -o jsmin') === false) { 19 | die(); 20 | } 21 | 22 | // Download libs. 23 | @mkdir(__DIR__ . '/libs', 0755); 24 | 25 | foreach($libs as $name => $url) { 26 | echo "Fetching $url...\n"; 27 | file_put_contents(__DIR__ . "/libs/$name.js", file_get_contents($url)); 28 | } 29 | 30 | // Copy utf-8 file to the libs directory 31 | echo "Copying UTF-8 file with BOM...\n"; 32 | copy(__DIR__ . '/utf8-with-bom.js', __DIR__ . '/libs/utf8-with-bom.js'); 33 | 34 | echo "Done\n"; 35 | -------------------------------------------------------------------------------- /test/test.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | Output differs between jsmin.c and jsmin.php.\n"; 26 | } 27 | } 28 | 29 | echo "Done.\n"; 30 | -------------------------------------------------------------------------------- /test/utf8-with-bom.js: -------------------------------------------------------------------------------- 1 | var foo = function() { 2 | var bar = 2 3 | !bar 4 | ~bar 5 | return bar 6 | }; --------------------------------------------------------------------------------