├── .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
--------------------------------------------------------------------------------