CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

JShrink-Minifier.php
Go to the documentation of this file.
1 <?php
2 /*
3 * This file is part of the JShrink package.
4 *
5 * (c) Robert Hafner <tedivm@tedivm.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
19 namespace JShrink;
20
32 class Minifier
33 {
39 protected $input;
40
47 protected $index = 0;
48
54 protected $a = '';
55
61 protected $b = '';
62
68 protected $c;
69
75 protected $options;
76
84 protected static $defaultOptions = array('flaggedComments' => true);
85
92 protected $locks = array();
93
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
141 protected function minifyDirectToOutput($js, $options)
142 {
143 $this->initialize($js, $options);
144 $this->loop();
145 $this->clean();
146 }
147
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
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
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
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
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
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
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
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
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
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
538 protected static function isAlphaNumeric($char)
539 {
540 return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/';
541 }
542
549 protected function lock($js)
550 {
551 /* lock things like <code>"asd" + ++x;</code> */
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
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 }
JShrink\Minifier\unlock
unlock($js)
Replace "locks" with the original characters.
Definition: JShrink-Minifier.php:574
JShrink\Minifier\saveString
saveString()
When a javascript string is detected this function crawls for the end of it and saves the whole strin...
Definition: JShrink-Minifier.php:438
JShrink
JShrink.
JShrink\Minifier\getReal
getReal()
This function gets the next "real" character.
Definition: JShrink-Minifier.php:302
JShrink\Minifier\isAlphaNumeric
static isAlphaNumeric($char)
Checks to see if a character is alphanumeric.
Definition: JShrink-Minifier.php:538
JShrink\Minifier\processMultiLineComments
processMultiLineComments($startIndex)
Skips multiline comments where appropriate, and includes them where needed.
Definition: JShrink-Minifier.php:359
JShrink\Minifier\processOneLineComments
processOneLineComments($startIndex)
Removed one line comments, with the exception of some very specific types of conditional comments...
Definition: JShrink-Minifier.php:331
JShrink\Minifier\getNext
getNext($string)
Pushes the index ahead to the next instance of the supplied string.
Definition: JShrink-Minifier.php:415
JShrink\Minifier\saveRegex
saveRegex()
When a regular expression is detected this function crawls for the end of it and saves the whole rege...
Definition: JShrink-Minifier.php:511
JShrink\Minifier\minify
static minify($js, $options=array())
Takes a string containing javascript and removes unneeded characters in order to shrink the code with...
Definition: JShrink-Minifier.php:103
JShrink\Minifier\clean
clean()
Resets attributes that do not need to be stored between requests so that the next request is ready to...
Definition: JShrink-Minifier.php:249
JShrink\Minifier\$options
$options
Definition: JShrink-Minifier.php:75
JShrink\Minifier\loop
loop()
The primary action occurs here.
Definition: JShrink-Minifier.php:176
JShrink\Minifier\lock
lock($js)
Replace patterns in the given string and store the replacement.
Definition: JShrink-Minifier.php:549
JShrink\Minifier\getChar
getChar()
Returns the next string for processing based off of the current index.
Definition: JShrink-Minifier.php:263
JShrink\Minifier
Definition: JShrink-Minifier.php:32
JShrink\Minifier\$input
$input
Definition: JShrink-Minifier.php:39
JShrink\Minifier\minifyDirectToOutput
minifyDirectToOutput($js, $options)
Processes a javascript string and outputs only the required characters, stripping out all unneeded ch...
Definition: JShrink-Minifier.php:141
JShrink\Minifier\$c
$c
Definition: JShrink-Minifier.php:68
JShrink\Minifier\initialize
initialize($js, $options)
Initializes internal variables, normalizes new lines,.
Definition: JShrink-Minifier.php:154