source: trunk/include/jshrink.class.php @ 25005

Last change on this file since 25005 was 19576, checked in by mistic100, 12 years ago

bug:2663 replace JSmin by JShrink

File size: 11.2 KB
Line 
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.5.1
43 */
44
45
46/**
47 * JShrink_Minifier
48 *
49 * Usage - JShrink_Minifier::minify($js);
50 * Usage - JShrink_Minifier::minify($js, $options);
51 * Usage - JShrink_Minifier::minify($js, array('flaggedComments' => false));
52 *
53 * @package     JShrink
54 * @author      Robert Hafner <tedivm@tedivm.com>
55 * @license     http://www.opensource.org/licenses/bsd-license.php  BSD License
56 */
57class JShrink_Minifier
58{
59        /**
60         * The input javascript to be minified.
61         *
62         * @var string
63         */
64        protected $input;
65
66        /**
67         * The location of the character (in the input string) that is next to be
68     * processed.
69         *
70         * @var int
71         */
72        protected $index = 0;
73
74        /**
75         * The first of the characters currently being looked at.
76         *
77         * @var string
78         */
79        protected $a = '';
80
81
82        /**
83         * The next character being looked at (after a);
84         *
85         * @var string
86         */
87        protected $b = '';
88
89        /**
90         * This character is only active when certain look ahead actions take place.
91         *
92         *  @var string
93         */
94        protected $c;
95
96        /**
97         * Contains the options for the current minification process.
98         *
99         * @var array
100         */
101        protected $options;
102
103        /**
104         * Contains the default options for minification. This array is merged with
105     * the one passed in by the user to create the request specific set of
106     * 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
114     * only used internally, and is only stored for performance reasons. There
115     * is no internal data shared between minification requests.
116         */
117        static protected $jshrink;
118
119        /**
120         * Minifier::minify takes a string containing javascript and removes
121     * unneeded characters in order to shrink the code without altering it's
122     * functionality.
123         */
124        static public function minify($js, $options = array())
125        {
126                try{
127                        ob_start();
128                        $currentOptions = array_merge(self::$defaultOptions, $options);
129
130                        if(!isset(self::$jshrink))
131                                self::$jshrink = new JShrink_Minifier();
132
133                        self::$jshrink->breakdownScript($js, $currentOptions);
134                        return ob_get_clean();
135
136                }catch(Exception $e){
137                        if(isset(self::$jshrink))
138                                self::$jshrink->clean();
139
140                        ob_end_clean();
141                        throw $e;
142                }
143        }
144
145        /**
146         * Processes a javascript string and outputs only the required characters,
147     * stripping out all unneeded characters.
148         *
149         * @param string $js The raw javascript to be minified
150         * @param array $currentOptions Various runtime options in an associative array
151         */
152        protected function breakdownScript($js, $currentOptions)
153        {
154                // reset work attributes in case this isn't the first run.
155                $this->clean();
156
157                $this->options = $currentOptions;
158
159                $js = str_replace("\r\n", "\n", $js);
160                $this->input = str_replace("\r", "\n", $js);
161
162
163                $this->a = $this->getReal();
164
165                // the only time the length can be higher than 1 is if a conditional
166        // comment needs to be displayed and the only time that can happen for
167        // $a is on the very first run
168                while(strlen($this->a) > 1)
169                {
170                        echo $this->a;
171                        $this->a = $this->getReal();
172                }
173
174                $this->b = $this->getReal();
175
176                while($this->a !== false && !is_null($this->a) && $this->a !== '')
177                {
178
179                        // now we give $b the same check for conditional comments we gave $a
180            // before we began looping
181                        if(strlen($this->b) > 1)
182                        {
183                                echo $this->a . $this->b;
184                                $this->a = $this->getReal();
185                                $this->b = $this->getReal();
186                                continue;
187                        }
188
189                        switch($this->a)
190                        {
191                                // new lines
192                                case "\n":
193                                        // if the next line is something that can't stand alone
194                    // preserve the newline
195                                        if(strpos('(-+{[@', $this->b) !== false)
196                                        {
197                                                echo $this->a;
198                                                $this->saveString();
199                                                break;
200                                        }
201
202                                        // if its a space we move down to the string test below
203                                        if($this->b === ' ')
204                                                break;
205
206                                        // otherwise we treat the newline like a space
207
208                                case ' ':
209                                        if(self::isAlphaNumeric($this->b))
210                                                echo $this->a;
211
212                                        $this->saveString();
213                                        break;
214
215                                default:
216                                        switch($this->b)
217                                        {
218                                                case "\n":
219                                                        if(strpos('}])+-"\'', $this->a) !== false)
220                                                        {
221                                                                echo $this->a;
222                                                                $this->saveString();
223                                                                break;
224                                                        }else{
225                                                                if(self::isAlphaNumeric($this->a))
226                                                                {
227                                                                        echo $this->a;
228                                                                        $this->saveString();
229                                                                }
230                                                        }
231                                                        break;
232
233                                                case ' ':
234                                                        if(!self::isAlphaNumeric($this->a))
235                                                                break;
236
237                                                default:
238                                                        // check for some regex that breaks stuff
239                                                        if($this->a == '/' && ($this->b == '\'' || $this->b == '"'))
240                                                        {
241                                                                $this->saveRegex();
242                                                                continue;
243                                                        }
244
245                                                        echo $this->a;
246                                                        $this->saveString();
247                                                        break;
248                                        }
249                        }
250
251                        // do reg check of doom
252                        $this->b = $this->getReal();
253
254                        if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
255                                $this->saveRegex();
256                }
257                $this->clean();
258        }
259
260        /**
261         * Returns the next string for processing based off of the current index.
262         *
263         * @return string
264         */
265        protected function getChar()
266        {
267                if(isset($this->c))
268                {
269                        $char = $this->c;
270                        unset($this->c);
271                }else{
272                        $tchar = substr($this->input, $this->index, 1);
273                        if(isset($tchar) && $tchar !== false)
274                        {
275                                $char = $tchar;
276                                $this->index++;
277                        }else{
278                                return false;
279                        }
280                }
281
282                if($char !== "\n" && ord($char) < 32)
283                        return ' ';
284
285                return $char;
286        }
287
288        /**
289         * This function gets the next "real" character. It is essentially a wrapper
290     * around the getChar function that skips comments. This has significant
291     * performance benefits as the skipping is done using native functions (ie,
292     * c code) rather than in script php.
293         *
294         * @return string Next 'real' character to be processed.
295         */
296        protected function getReal()
297        {
298                $startIndex = $this->index;
299                $char = $this->getChar();
300
301                if($char == '/')
302                {
303                        $this->c = $this->getChar();
304
305                        if($this->c == '/')
306                        {
307                                $thirdCommentString = substr($this->input, $this->index, 1);
308
309                                // kill rest of line
310                                $char = $this->getNext("\n");
311
312                                if($thirdCommentString == '@')
313                                {
314                                        $endPoint = ($this->index) - $startIndex;
315                                        unset($this->c);
316                                        $char = "\n" . substr($this->input, $startIndex, $endPoint);
317                                }else{
318                                        $char = $this->getChar();
319                                        $char = $this->getChar();
320                                }
321
322                        }elseif($this->c == '*'){
323
324                                $this->getChar(); // current C
325                                $thirdCommentString = $this->getChar();
326
327                                if($thirdCommentString == '@')
328                                {
329                                        // conditional comment
330
331                                        // we're gonna back up a bit and and send the comment back,
332                    // where the first char will be echoed and the rest will be
333                    // treated like a string
334                                        $this->index = $this->index-2;
335                                        return '/';
336
337                                }elseif($this->getNext('*/')){
338                                // kill everything up to the next */
339
340                                        $this->getChar(); // get *
341                                        $this->getChar(); // get /
342
343                                        $char = $this->getChar(); // get next real character
344
345                                        // if YUI-style comments are enabled we reinsert it into the stream
346                                        if($this->options['flaggedComments'] && $thirdCommentString == '!')
347                                        {
348                                                $endPoint = ($this->index - 1) - $startIndex;
349                                                echo "\n" . substr($this->input, $startIndex, $endPoint) . "\n";
350                                        }
351
352                                }else{
353                                        $char = false;
354                                }
355
356                                if($char === false)
357                                        throw new RuntimeException('Stray comment. ' . $this->index);
358
359                                // if we're here c is part of the comment and therefore tossed
360                                if(isset($this->c))
361                                        unset($this->c);
362                        }
363                }
364                return $char;
365        }
366
367        /**
368         * Pushes the index ahead to the next instance of the supplied string. If it
369     * is found the first character of the string is returned.
370         *
371         * @return string|false Returns the first character of the string or false.
372         */
373        protected function getNext($string)
374        {
375                $pos = strpos($this->input, $string, $this->index);
376
377                if($pos === false)
378                        return false;
379
380                $this->index = $pos;
381                return substr($this->input, $this->index, 1);
382        }
383
384        /**
385         * When a javascript string is detected this function crawls for the end of
386     * it and saves the whole string.
387         *
388         */
389        protected function saveString()
390        {
391                $this->a = $this->b;
392                if($this->a == "'" || $this->a == '"') // is the character a quote
393                {
394                        // save literal string
395                        $stringType = $this->a;
396
397                        while(1)
398                        {
399                                echo $this->a;
400                                $this->a = $this->getChar();
401
402                                switch($this->a)
403                                {
404                                        case $stringType:
405                                                break 2;
406
407                                        case "\n":
408                                                throw new RuntimeException('Unclosed string. ' . $this->index);
409                                                break;
410
411                                        case '\\':
412                                                echo $this->a;
413                                                $this->a = $this->getChar();
414                                }
415                        }
416                }
417        }
418
419        /**
420         * When a regular expression is detected this funcion crawls for the end of
421     * it and saves the whole regex.
422         */
423        protected function saveRegex()
424        {
425                echo $this->a . $this->b;
426
427                while(($this->a = $this->getChar()) !== false)
428                {
429                        if($this->a == '/')
430                                break;
431
432                        if($this->a == '\\')
433                        {
434                                echo $this->a;
435                                $this->a = $this->getChar();
436                        }
437
438                        if($this->a == "\n")
439                                throw new RuntimeException('Stray regex pattern. ' . $this->index);
440
441                        echo $this->a;
442                }
443                $this->b = $this->getReal();
444        }
445
446        /**
447         * Resets attributes that do not need to be stored between requests so that
448     * the next request is ready to go.
449         */
450        protected function clean()
451        {
452                unset($this->input);
453                $this->index = 0;
454                $this->a = $this->b = '';
455                unset($this->c);
456                unset($this->options);
457        }
458
459        /**
460         * Checks to see if a character is alphanumeric.
461         *
462         * @return bool
463         */
464        static protected function isAlphaNumeric($char)
465        {
466                return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/';
467        }
468
469}
Note: See TracBrowser for help on using the repository browser.