Added missing modules, including some as submodules.
[yaffs-website] / web / modules / contrib / advagg / advagg_js_minify / jshrink.inc
1 <?php
2 // @codingStandardsIgnoreFile
3 // @ignore comment_docblock_file:file
4 // @ignore style_curly_braces:file
5 // @ignore style_string_spacing:file
6 // @ignore style_else_spacing:file
7 // @ignore comment_comment_docblock_missing:file
8 // @ignore comment_comment_eg:file
9 // @ignore production_code:file
10 // @ignore druplart_unary:file
11 // @ignore style_uppercase_constants:file
12 // @ignore comment_comment_space:file
13 // @ignore druplart_conditional_assignment:file
14 // @ignore style_paren_spacing:file
15 // @ignore style_no_tabs:file
16 // @ignore comment_docblock_comment:file
17 // @ignore style_control_spacing:file
18 // @ignore comment_comment_indent:file
19 // @ignore :file
20
21 /*
22  * This file is part of the JShrink package.
23  *
24  * (c) Robert Hafner <tedivm@tedivm.com>
25  */
26
27 /*
28 Copyright (c) 2009, Robert Hafner
29 All rights reserved.
30 Redistribution and use in source and binary forms, with or without
31 modification, are permitted provided that the following conditions are met:
32   * Redistributions of source code must retain the above copyright
33     notice, this list of conditions and the following disclaimer.
34   * Redistributions in binary form must reproduce the above copyright
35     notice, this list of conditions and the following disclaimer in the
36     documentation and/or other materials provided with the distribution.
37   * Neither the name of the Stash Project nor the
38     names of its contributors may be used to endorse or promote products
39     derived from this software without specific prior written permission.
40
41 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
42 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
43 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
44 DISCLAIMED. IN NO EVENT SHALL Robert Hafner BE LIABLE FOR ANY
45 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
46 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
47 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
48 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
49 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
50 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51 */
52
53 /**
54  * JShrink
55  *
56  *
57  * @package    JShrink
58  * @author     Robert Hafner <tedivm@tedivm.com>
59  */
60
61 namespace JShrink;
62
63 /**
64  * Minifier
65  *
66  * Usage - Minifier::minify($js);
67  * Usage - Minifier::minify($js, $options);
68  * Usage - Minifier::minify($js, array('flaggedComments' => false));
69  *
70  * @package JShrink
71  * @author Robert Hafner <tedivm@tedivm.com>
72  * @license http://www.opensource.org/licenses/bsd-license.php  BSD License
73  */
74 class Minifier
75 {
76     /**
77      * The input javascript to be minified.
78      *
79      * @var string
80      */
81     protected $input;
82
83     /**
84      * The location of the character (in the input string) that is next to be
85      * processed.
86      *
87      * @var int
88      */
89     protected $index = 0;
90
91     /**
92      * The first of the characters currently being looked at.
93      *
94      * @var string
95      */
96     protected $a = '';
97
98     /**
99      * The next character being looked at (after a);
100      *
101      * @var string
102      */
103     protected $b = '';
104
105     /**
106      * This character is only active when certain look ahead actions take place.
107      *
108      *  @var string
109      */
110     protected $c;
111
112     /**
113      * Contains the options for the current minification process.
114      *
115      * @var array
116      */
117     protected $options;
118
119     /**
120      * Contains the default options for minification. This array is merged with
121      * the one passed in by the user to create the request specific set of
122      * options (stored in the $options attribute).
123      *
124      * @var array
125      */
126     protected static $defaultOptions = array('flaggedComments' => true);
127
128     /**
129      * Contains lock ids which are used to replace certain code patterns and
130      * prevent them from being minified
131      *
132      * @var array
133      */
134     protected $locks = array();
135
136     /**
137      * Takes a string containing javascript and removes unneeded characters in
138      * order to shrink the code without altering it's functionality.
139      *
140      * @param  string      $js      The raw javascript to be minified
141      * @param  array       $options Various runtime options in an associative array
142      * @throws \Exception
143      * @return bool|string
144      */
145     public static function minify($js, $options = array())
146     {
147         try {
148             ob_start();
149
150             $jshrink = new Minifier();
151             $js = $jshrink->lock($js);
152             $jshrink->minifyDirectToOutput($js, $options);
153
154             // Sometimes there's a leading new line, so we trim that out here.
155             $js = ltrim(ob_get_clean());
156             $js = $jshrink->unlock($js);
157             unset($jshrink);
158
159             return $js;
160
161         } catch (\Exception $e) {
162
163             if (isset($jshrink)) {
164                 // Since the breakdownScript function probably wasn't finished
165                 // we clean it out before discarding it.
166                 $jshrink->clean();
167                 unset($jshrink);
168             }
169
170             // without this call things get weird, with partially outputted js.
171             ob_end_clean();
172             throw $e;
173         }
174     }
175
176     /**
177      * Processes a javascript string and outputs only the required characters,
178      * stripping out all unneeded characters.
179      *
180      * @param string $js      The raw javascript to be minified
181      * @param array  $options Various runtime options in an associative array
182      */
183     protected function minifyDirectToOutput($js, $options)
184     {
185         $this->initialize($js, $options);
186         $this->loop();
187         $this->clean();
188     }
189
190     /**
191      *  Initializes internal variables, normalizes new lines,
192      *
193      * @param string $js      The raw javascript to be minified
194      * @param array  $options Various runtime options in an associative array
195      */
196     protected function initialize($js, $options)
197     {
198         $this->options = array_merge(static::$defaultOptions, $options);
199         $js = str_replace("\r\n", "\n", $js);
200         $js = str_replace('/**/', '', $js);
201         $this->input = str_replace("\r", "\n", $js);
202
203         // We add a newline to the end of the script to make it easier to deal
204         // with comments at the bottom of the script- this prevents the unclosed
205         // comment error that can otherwise occur.
206         $this->input .= PHP_EOL;
207
208         // Populate "a" with a new line, "b" with the first character, before
209         // entering the loop
210         $this->a = "\n";
211         $this->b = $this->getReal();
212     }
213
214     /**
215      * The primary action occurs here. This function loops through the input string,
216      * outputting anything that's relevant and discarding anything that is not.
217      */
218     protected function loop()
219     {
220         while ($this->a !== false && !is_null($this->a) && $this->a !== '') {
221
222             switch ($this->a) {
223                 // new lines
224                 case "\n":
225                     // if the next line is something that can't stand alone preserve the newline
226                     if (strpos('(-+{[@', $this->b) !== false) {
227                         echo $this->a;
228                         $this->saveString();
229                         break;
230                     }
231
232                     // if B is a space we skip the rest of the switch block and go down to the
233                     // string/regex check below, resetting $this->b with getReal
234                     if($this->b === ' ')
235                         break;
236
237                 // otherwise we treat the newline like a space
238
239                 case ' ':
240                     if(static::isAlphaNumeric($this->b))
241                         echo $this->a;
242
243                     $this->saveString();
244                     break;
245
246                 default:
247                     switch ($this->b) {
248                         case "\n":
249                             if (strpos('}])+-"\'', $this->a) !== false) {
250                                 echo $this->a;
251                                 $this->saveString();
252                                 break;
253                             } else {
254                                 if (static::isAlphaNumeric($this->a)) {
255                                     echo $this->a;
256                                     $this->saveString();
257                                 }
258                             }
259                             break;
260
261                         case ' ':
262                             if(!static::isAlphaNumeric($this->a))
263                                 break;
264
265                         default:
266                             // check for some regex that breaks stuff
267                             if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
268                                 $this->saveRegex();
269                                 continue;
270                             }
271
272                             echo $this->a;
273                             $this->saveString();
274                             break;
275                     }
276             }
277
278             // do reg check of doom
279             $this->b = $this->getReal();
280
281             if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
282                 $this->saveRegex();
283         }
284     }
285
286     /**
287      * Resets attributes that do not need to be stored between requests so that
288      * the next request is ready to go. Another reason for this is to make sure
289      * the variables are cleared and are not taking up memory.
290      */
291     protected function clean()
292     {
293         unset($this->input);
294         $this->index = 0;
295         $this->a = $this->b = '';
296         unset($this->c);
297         unset($this->options);
298     }
299
300     /**
301      * Returns the next string for processing based off of the current index.
302      *
303      * @return string
304      */
305     protected function getChar()
306     {
307         // Check to see if we had anything in the look ahead buffer and use that.
308         if (isset($this->c)) {
309             $char = $this->c;
310             unset($this->c);
311
312         // Otherwise we start pulling from the input.
313         } else {
314             $char = substr($this->input, $this->index, 1);
315
316             // If the next character doesn't exist return false.
317             if (isset($char) && $char === false) {
318                 return false;
319             }
320
321             // Otherwise increment the pointer and use this char.
322             $this->index++;
323         }
324
325         // Normalize all whitespace except for the newline character into a
326         // standard space.
327         if($char !== "\n" && ord($char) < 32)
328
329             return ' ';
330
331         return $char;
332     }
333
334     /**
335      * This function gets the next "real" character. It is essentially a wrapper
336      * around the getChar function that skips comments. This has significant
337      * performance benefits as the skipping is done using native functions (ie,
338      * c code) rather than in script php.
339      *
340      *
341      * @return string            Next 'real' character to be processed.
342      * @throws \RuntimeException
343      */
344     protected function getReal()
345     {
346         $startIndex = $this->index;
347         $char = $this->getChar();
348
349         // Check to see if we're potentially in a comment
350         if ($char !== '/') {
351             return $char;
352         }
353
354         $this->c = $this->getChar();
355
356         if ($this->c === '/') {
357             return $this->processOneLineComments($startIndex);
358
359         } elseif ($this->c === '*') {
360             return $this->processMultiLineComments($startIndex);
361         }
362
363         return $char;
364     }
365
366     /**
367      * Removed one line comments, with the exception of some very specific types of
368      * conditional comments.
369      *
370      * @param  int    $startIndex The index point where "getReal" function started
371      * @return string
372      */
373     protected function processOneLineComments($startIndex)
374     {
375         $thirdCommentString = substr($this->input, $this->index, 1);
376
377         // kill rest of line
378         $this->getNext("\n");
379
380         if ($thirdCommentString == '@') {
381             $endPoint = $this->index - $startIndex;
382             unset($this->c);
383             $char = "\n" . substr($this->input, $startIndex, $endPoint);
384         } else {
385             // first one is contents of $this->c
386             $this->getChar();
387             $char = $this->getChar();
388         }
389
390         return $char;
391     }
392
393     /**
394      * Skips multiline comments where appropriate, and includes them where needed.
395      * Conditional comments and "license" style blocks are preserved.
396      *
397      * @param  int               $startIndex The index point where "getReal" function started
398      * @return bool|string       False if there's no character
399      * @throws \RuntimeException Unclosed comments will throw an error
400      */
401     protected function processMultiLineComments($startIndex)
402     {
403         $this->getChar(); // current C
404         $thirdCommentString = $this->getChar();
405
406         // kill everything up to the next */ if it's there
407         if ($this->getNext('*/')) {
408
409             $this->getChar(); // get *
410             $this->getChar(); // get /
411             $char = $this->getChar(); // get next real character
412
413             // Now we reinsert conditional comments and YUI-style licensing comments
414             if (($this->options['flaggedComments'] && $thirdCommentString === '!')
415                 || ($thirdCommentString === '@') ) {
416
417                 // If conditional comments or flagged comments are not the first thing in the script
418                 // we need to echo a and fill it with a space before moving on.
419                 if ($startIndex > 0) {
420                     echo $this->a;
421                     $this->a = " ";
422
423                     // If the comment started on a new line we let it stay on the new line
424                     if ($this->input[($startIndex - 1)] === "\n") {
425                         echo "\n";
426                     }
427                 }
428
429                 $endPoint = ($this->index - 1) - $startIndex;
430                 echo substr($this->input, $startIndex, $endPoint);
431
432                 return $char;
433             }
434
435         } else {
436             $char = false;
437         }
438
439         if($char === false)
440             throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2));
441
442         // if we're here c is part of the comment and therefore tossed
443         if(isset($this->c))
444             unset($this->c);
445
446         return $char;
447     }
448
449     /**
450      * Pushes the index ahead to the next instance of the supplied string. If it
451      * is found the first character of the string is returned and the index is set
452      * to it's position.
453      *
454      * @param  string       $string
455      * @return string|false Returns the first character of the string or false.
456      */
457     protected function getNext($string)
458     {
459         // Find the next occurrence of "string" after the current position.
460         $pos = strpos($this->input, $string, $this->index);
461
462         // If it's not there return false.
463         if($pos === false)
464
465             return false;
466
467         // Adjust position of index to jump ahead to the asked for string
468         $this->index = $pos;
469
470         // Return the first character of that string.
471         return substr($this->input, $this->index, 1);
472     }
473
474     /**
475      * When a javascript string is detected this function crawls for the end of
476      * it and saves the whole string.
477      *
478      * @throws \RuntimeException Unclosed strings will throw an error
479      */
480     protected function saveString()
481     {
482         $startpos = $this->index;
483
484         // saveString is always called after a gets cleared, so we push b into
485         // that spot.
486         $this->a = $this->b;
487
488         // If this isn't a string we don't need to do anything.
489         if ($this->a !== "'" && $this->a !== '"') {
490             return;
491         }
492
493         // String type is the quote used, " or '
494         $stringType = $this->a;
495
496         // Echo out that starting quote
497         echo $this->a;
498
499         // Loop until the string is done
500         while (true) {
501
502             // Grab the very next character and load it into a
503             $this->a = $this->getChar();
504
505             switch ($this->a) {
506
507                 // If the string opener (single or double quote) is used
508                 // output it and break out of the while loop-
509                 // The string is finished!
510                 case $stringType:
511                     break 2;
512
513                 // New lines in strings without line delimiters are bad- actual
514                 // new lines will be represented by the string \n and not the actual
515                 // character, so those will be treated just fine using the switch
516                 // block below.
517                 case "\n":
518                     throw new \RuntimeException('Unclosed string at position: ' . $startpos );
519                     break;
520
521                 // Escaped characters get picked up here. If it's an escaped new line it's not really needed
522                 case '\\':
523
524                     // a is a slash. We want to keep it, and the next character,
525                     // unless it's a new line. New lines as actual strings will be
526                     // preserved, but escaped new lines should be reduced.
527                     $this->b = $this->getChar();
528
529                     // If b is a new line we discard a and b and restart the loop.
530                     if ($this->b === "\n") {
531                         break;
532                     }
533
534                     // echo out the escaped character and restart the loop.
535                     echo $this->a . $this->b;
536                     break;
537
538
539                 // Since we're not dealing with any special cases we simply
540                 // output the character and continue our loop.
541                 default:
542                     echo $this->a;
543             }
544         }
545     }
546
547     /**
548      * When a regular expression is detected this function crawls for the end of
549      * it and saves the whole regex.
550      *
551      * @throws \RuntimeException Unclosed regex will throw an error
552      */
553     protected function saveRegex()
554     {
555         echo $this->a . $this->b;
556
557         while (($this->a = $this->getChar()) !== false) {
558             if($this->a === '/')
559                 break;
560
561             if ($this->a === '\\') {
562                 echo $this->a;
563                 $this->a = $this->getChar();
564             }
565
566             if($this->a === "\n")
567                 throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index);
568
569             echo $this->a;
570         }
571         $this->b = $this->getReal();
572     }
573
574     /**
575      * Checks to see if a character is alphanumeric.
576      *
577      * @param  string $char Just one character
578      * @return bool
579      */
580     protected static function isAlphaNumeric($char)
581     {
582         return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
583     }
584
585     /**
586      * Replace patterns in the given string and store the replacement
587      *
588      * @param  string $js The string to lock
589      * @return bool
590      */
591     protected function lock($js)
592     {
593         /* lock things like <code>"asd" + ++x;</code> */
594         $lock = '"LOCK---' . crc32(time()) . '"';
595
596         $matches = array();
597         preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
598         if (empty($matches)) {
599             return $js;
600         }
601
602         $this->locks[$lock] = $matches[2];
603
604         $js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
605         /* -- */
606
607         return $js;
608     }
609
610     /**
611      * Replace "locks" with the original characters
612      *
613      * @param  string $js The string to unlock
614      * @return bool
615      */
616     protected function unlock($js)
617     {
618         if (empty($this->locks)) {
619             return $js;
620         }
621
622         foreach ($this->locks as $lock => $replacement) {
623             $js = str_replace($lock, $replacement, $js);
624         }
625
626         return $js;
627     }
628
629 }