]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - jssource/Minifier.php
Release 6.5.0
[Github/sugarcrm.git] / jssource / Minifier.php
1 <?php
2 /**
3  * JShrink
4  *
5  * Copyright (c) 2009-2012, Robert Hafner <tedivm@tedivm.com>.
6  * All rights reserved.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  *
12  *   * Redistributions of source code must retain the above copyright
13  *     notice, this list of conditions and the following disclaimer.
14  *
15  *   * Redistributions in binary form must reproduce the above copyright
16  *     notice, this list of conditions and the following disclaimer in
17  *     the documentation and/or other materials provided with the
18  *     distribution.
19  *
20  *   * Neither the name of Robert Hafner nor the names of his
21  *     contributors may be used to endorse or promote products derived
22  *     from this software without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
34  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35  * POSSIBILITY OF SUCH DAMAGE.
36  *
37  * @package    JShrink
38  * @author     Robert Hafner <tedivm@tedivm.com>
39  * @copyright  2009-2012 Robert Hafner <tedivm@tedivm.com>
40  * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
41  * @link       https://github.com/tedivm/JShrink
42  * @version    Release: 0.4
43  */
44
45 // Some changes done by Akshay Joshi to preserve compatibility with PHP 5.2.
46
47 /**
48  * Minifier
49  *
50  * Usage - Minifier::minify($js);
51  * Usage - Minifier::minify($js, $options);
52  * Usage - Minifier::minify($js, array('flaggedComments' => false));
53  *
54  * @version 0.4
55  * @package JShrink
56  * @author  Robert Hafner <tedivm@tedivm.com>
57  * @license http://www.opensource.org/licenses/bsd-license.php  BSD License
58  */
59 class Minifier
60 {
61     /**
62      * The input javascript to be minified.
63      *
64      * @var string
65      */
66     protected $input;
67
68     /**
69      * The location of the character (in the input string) that is next to be processed.
70      *
71      * @var int
72      */
73     protected $index = 0;
74
75     /**
76      * The first of the characters currently being looked at.
77      *
78      * @var string
79      */
80     protected $a = '';
81
82
83     /**
84      * The next character being looked at (after a);
85      *
86      * @var string
87      */
88     protected $b = '';
89
90     /**
91      * This character is only active when certain look ahead actions take place.
92      *
93      *  @var string
94      */
95     protected $c;
96
97     /**
98      * Contains the options for the current minification process.
99      *
100      * @var array
101      */
102     protected $options;
103
104     /**
105      * Contains the default options for minification. This array is merged with the one passed in by the user to create
106      * the request specific set of options (stored in the $options attribute).
107      *
108      * @var array
109      */
110     static protected $defaultOptions = array('flaggedComments' => true);
111
112     /**
113      * Contains a copy of the JShrink object used to run minification. This is only used internally, and is only stored
114      * for performance reasons. There is no internal data shared between minification requests.
115      */
116     static protected $jshrink;
117
118     /**
119      * Minifier::minify takes a string containing javascript and removes unneeded characters in order to shrink the code
120      * without altering it's functionality.
121      */
122     static public function minify($js, $options = array())
123     {
124         try{
125             ob_start();
126             $currentOptions = array_merge(self::$defaultOptions, $options);
127
128             if(!isset(self::$jshrink))
129                 self::$jshrink = new Minifier();
130
131             self::$jshrink->breakdownScript($js, $currentOptions);
132             return ob_get_clean();
133
134         }catch(Exception $e){
135             if(isset(self::$jshrink))
136                 self::$jshrink->clean();
137
138             ob_end_clean();
139             throw $e;
140         }
141     }
142
143     /**
144      * Processes a javascript string and outputs only the required characters, stripping out all unneeded characters.
145      *
146      * @param string $js The raw javascript to be minified
147      * @param array $currentOptions Various runtime options in an associative array
148      */
149     protected function breakdownScript($js, $currentOptions)
150     {
151         // reset work attributes in case this isn't the first run.
152         $this->clean();
153
154         $this->options = $currentOptions;
155
156         $js = str_replace("\r\n", "\n", $js);
157         $this->input = str_replace("\r", "\n", $js);
158         $this->input = preg_replace('/\h/u', ' ', $this->input);
159
160
161         $this->a = $this->getReal();
162
163         // the only time the length can be higher than 1 is if a conditional comment needs to be displayed
164         // and the only time that can happen for $a is on the very first run
165         while(strlen($this->a) > 1)
166         {
167             echo $this->a;
168             $this->a = $this->getReal();
169         }
170
171         $this->b = $this->getReal();
172
173         while($this->a !== false && !is_null($this->a) && $this->a !== '')
174         {
175
176             // now we give $b the same check for conditional comments we gave $a before we began looping
177             if(strlen($this->b) > 1)
178             {
179                 echo $this->a . $this->b;
180                 $this->a = $this->getReal();
181                 $this->b = $this->getReal();
182                 continue;
183             }
184
185             switch($this->a)
186             {
187                 // new lines
188                 case "\n":
189                     // if the next line is something that can't stand alone preserve the newline
190                     if($this->b != '' && strpos('(-+{[@', $this->b) !== false)
191                     {
192                         echo $this->a;
193                         $this->saveString();
194                         break;
195                     }
196
197                     // if its a space we move down to the string test below
198                     if($this->b === ' ')
199                         break;
200
201                     // otherwise we treat the newline like a space
202
203                 case ' ':
204                     if(self::isAlphaNumeric($this->b))
205                         echo $this->a;
206
207                     $this->saveString();
208                     break;
209
210                 default:
211                     switch($this->b)
212                     {
213                         case "\n":
214                             if(strpos('}])+-"\'', $this->a) !== false)
215                             {
216                                 echo $this->a;
217                                 $this->saveString();
218                                 break;
219                             }else{
220                                 if(self::isAlphaNumeric($this->a))
221                                 {
222                                     echo $this->a;
223                                     $this->saveString();
224                                 }
225                             }
226                             break;
227
228                         case ' ':
229                             if(!self::isAlphaNumeric($this->a))
230                                 break;
231
232                         default:
233                             // check for some regex that breaks stuff
234                             if($this->a == '/' && ($this->b == '\'' || $this->b == '"'))
235                             {
236                                 $this->saveRegex();
237                                 continue;
238                             }
239
240                             echo $this->a;
241                             $this->saveString();
242                             break;
243                     }
244             }
245
246             // do reg check of doom
247             $this->b = $this->getReal();
248
249             if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
250                 $this->saveRegex();
251         }
252         $this->clean();
253     }
254
255     /**
256      * Returns the next string for processing based off of the current index.
257      *
258      * @return string
259      */
260     protected function getChar()
261     {
262         if(isset($this->c))
263         {
264             $char = $this->c;
265             unset($this->c);
266         }else{
267             $tchar = substr($this->input, $this->index, 1);
268             if(isset($tchar) && $tchar !== false)
269             {
270                 $char = $tchar;
271                 $this->index++;
272             }else{
273                 return false;
274             }
275         }
276
277         if($char !== "\n" && ord($char) < 32)
278             return ' ';
279
280         return $char;
281     }
282
283     /**
284      * This function gets the next "real" character. It is essentially a wrapper around the getChar function that skips
285      * comments. This has signifigant peformance benefits as the skipping is done using native functions (ie, c code)
286      * rather than in script php.
287      *
288      * @return string Next 'real' character to be processed.
289      */
290     protected function getReal()
291     {
292         $startIndex = $this->index;
293         $char = $this->getChar();
294
295         if($char == '/')
296         {
297             $this->c = $this->getChar();
298
299             if($this->c == '/')
300             {
301                 $thirdCommentString = substr($this->input, $this->index, 1);
302
303                 // kill rest of line
304                 $char = $this->getNext("\n");
305
306                 if($thirdCommentString == '@')
307                 {
308                     $endPoint = ($this->index) - $startIndex;
309                     unset($this->c);
310                     $char = "\n" . substr($this->input, $startIndex, $endPoint);// . "\n";
311                 }else{
312                     $char = $this->getChar();
313                     $char = $this->getChar();
314                 }
315
316             }elseif($this->c == '*'){
317
318                 $this->getChar(); // current C
319                 $thirdCommentString = $this->getChar();
320
321                 if($thirdCommentString == '@')
322                 {
323                     // conditional comment
324
325                     // we're gonna back up a bit and and send the comment back, where the first
326                     // char will be echoed and the rest will be treated like a string
327                     $this->index = $this->index-2;
328                     return '/';
329
330                 }elseif($this->getNext('*/')){
331                 // kill everything up to the next */
332
333                     $this->getChar(); // get *
334                     $this->getChar(); // get /
335
336                     $char = $this->getChar(); // get next real character
337
338                     // if YUI-style comments are enabled we reinsert it into the stream
339                     if($this->options['flaggedComments'] && $thirdCommentString == '!')
340                     {
341                         $endPoint = ($this->index - 1) - $startIndex;
342                         echo "\n" . substr($this->input, $startIndex, $endPoint) . "\n";
343                     }
344
345                 }else{
346                     $char = false;
347                 }
348
349                 if($char === false)
350                     throw new RuntimeException('Stray comment. ' . $this->index);
351
352                 // if we're here c is part of the comment and therefore tossed
353                 if(isset($this->c))
354                     unset($this->c);
355             }
356         }
357         return $char;
358     }
359
360     /**
361      * Pushes the index ahead to the next instance of the supplied string. If it is found the first character of the
362      * string is returned.
363      *
364      * @return string|false Returns the first character of the string if found, false otherwise.
365      */
366     protected function getNext($string)
367     {
368         $pos = strpos($this->input, $string, $this->index);
369
370         if($pos === false)
371             return false;
372
373         $this->index = $pos;
374         return substr($this->input, $this->index, 1);
375     }
376
377     /**
378      * When a javascript string is detected this function crawls for the end of it and saves the whole string.
379      *
380      */
381     protected function saveString()
382     {
383         $this->a = $this->b;
384         if($this->a == "'" || $this->a == '"') // is the character a quote
385         {
386             // save literal string
387             $stringType = $this->a;
388
389             while(1)
390             {
391                 echo $this->a;
392                 $this->a = $this->getChar();
393
394                 switch($this->a)
395                 {
396                     case $stringType:
397                         break 2;
398
399                     case "\n":
400                         throw new RuntimeException('Unclosed string. ' . $this->index);
401                         break;
402
403                     case '\\':
404                         echo $this->a;
405                         $this->a = $this->getChar();
406                 }
407             }
408         }
409     }
410
411     /**
412      * When a regular expression is detected this funcion crawls for the end of it and saves the whole regex.
413      */
414     protected function saveRegex()
415     {
416         echo $this->a . $this->b;
417
418         while(($this->a = $this->getChar()) !== false)
419         {
420             if($this->a == '/')
421                 break;
422
423             if($this->a == '\\')
424             {
425                 echo $this->a;
426                 $this->a = $this->getChar();
427             }
428
429             if($this->a == "\n")
430                 throw new RuntimeException('Stray regex pattern. ' . $this->index);
431
432             echo $this->a;
433         }
434         $this->b = $this->getReal();
435     }
436
437     /**
438      * Resets attributes that do not need to be stored between requests so that the next request is ready to go.
439      */
440     protected function clean()
441     {
442         unset($this->input);
443         $this->index = 0;
444         $this->a = $this->b = '';
445         unset($this->c);
446         unset($this->options);
447     }
448
449     /**
450      * Checks to see if a character is alphanumeric.
451      *
452      * @return bool
453      */
454     static protected function isAlphaNumeric($char)
455     {
456         return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/';
457     }
458
459 }