CWIS Developer Documentation
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 }
unlock($js)
Replace "locks" with the original characters.
Definition: Minifier.php:574
saveString()
When a javascript string is detected this function crawls for the end of it and saves the whole strin...
Definition: Minifier.php:438
JShrink.
getReal()
This function gets the next "real" character.
Definition: Minifier.php:302
static isAlphaNumeric($char)
Checks to see if a character is alphanumeric.
Definition: Minifier.php:538
processMultiLineComments($startIndex)
Skips multiline comments where appropriate, and includes them where needed.
Definition: Minifier.php:359
processOneLineComments($startIndex)
Removed one line comments, with the exception of some very specific types of conditional comments...
Definition: Minifier.php:331
getNext($string)
Pushes the index ahead to the next instance of the supplied string.
Definition: Minifier.php:415
saveRegex()
When a regular expression is detected this function crawls for the end of it and saves the whole rege...
Definition: Minifier.php:511
static minify($js, $options=array())
Takes a string containing javascript and removes unneeded characters in order to shrink the code with...
Definition: Minifier.php:103
clean()
Resets attributes that do not need to be stored between requests so that the next request is ready to...
Definition: Minifier.php:249
loop()
The primary action occurs here.
Definition: Minifier.php:176
lock($js)
Replace patterns in the given string and store the replacement.
Definition: Minifier.php:549
getChar()
Returns the next string for processing based off of the current index.
Definition: Minifier.php:263
minifyDirectToOutput($js, $options)
Processes a javascript string and outputs only the required characters, stripping out all unneeded ch...
Definition: Minifier.php:141
initialize($js, $options)
Initializes internal variables, normalizes new lines,.
Definition: Minifier.php:154