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