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
22 * This file is part of the JShrink package.
24 * (c) Robert Hafner <tedivm@tedivm.com>
28 Copyright (c) 2009, Robert Hafner
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.
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.
58 * @author Robert Hafner <tedivm@tedivm.com>
66 * Usage - Minifier::minify($js);
67 * Usage - Minifier::minify($js, $options);
68 * Usage - Minifier::minify($js, array('flaggedComments' => false));
71 * @author Robert Hafner <tedivm@tedivm.com>
72 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
77 * The input javascript to be minified.
84 * The location of the character (in the input string) that is next to be
92 * The first of the characters currently being looked at.
99 * The next character being looked at (after a);
106 * This character is only active when certain look ahead actions take place.
113 * Contains the options for the current minification process.
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).
126 protected static $defaultOptions = array('flaggedComments' => true);
129 * Contains lock ids which are used to replace certain code patterns and
130 * prevent them from being minified
134 protected $locks = array();
137 * Takes a string containing javascript and removes unneeded characters in
138 * order to shrink the code without altering it's functionality.
140 * @param string $js The raw javascript to be minified
141 * @param array $options Various runtime options in an associative array
143 * @return bool|string
145 public static function minify($js, $options = array())
150 $jshrink = new Minifier();
151 $js = $jshrink->lock($js);
152 $jshrink->minifyDirectToOutput($js, $options);
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);
161 } catch (\Exception $e) {
163 if (isset($jshrink)) {
164 // Since the breakdownScript function probably wasn't finished
165 // we clean it out before discarding it.
170 // without this call things get weird, with partially outputted js.
177 * Processes a javascript string and outputs only the required characters,
178 * stripping out all unneeded characters.
180 * @param string $js The raw javascript to be minified
181 * @param array $options Various runtime options in an associative array
183 protected function minifyDirectToOutput($js, $options)
185 $this->initialize($js, $options);
191 * Initializes internal variables, normalizes new lines,
193 * @param string $js The raw javascript to be minified
194 * @param array $options Various runtime options in an associative array
196 protected function initialize($js, $options)
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);
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;
208 // Populate "a" with a new line, "b" with the first character, before
211 $this->b = $this->getReal();
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.
218 protected function loop()
220 while ($this->a !== false && !is_null($this->a) && $this->a !== '') {
225 // if the next line is something that can't stand alone preserve the newline
226 if (strpos('(-+{[@', $this->b) !== false) {
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
237 // otherwise we treat the newline like a space
240 if(static::isAlphaNumeric($this->b))
249 if (strpos('}])+-"\'', $this->a) !== false) {
254 if (static::isAlphaNumeric($this->a)) {
262 if(!static::isAlphaNumeric($this->a))
266 // check for some regex that breaks stuff
267 if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
278 // do reg check of doom
279 $this->b = $this->getReal();
281 if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
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.
291 protected function clean()
295 $this->a = $this->b = '';
297 unset($this->options);
301 * Returns the next string for processing based off of the current index.
305 protected function getChar()
307 // Check to see if we had anything in the look ahead buffer and use that.
308 if (isset($this->c)) {
312 // Otherwise we start pulling from the input.
314 $char = substr($this->input, $this->index, 1);
316 // If the next character doesn't exist return false.
317 if (isset($char) && $char === false) {
321 // Otherwise increment the pointer and use this char.
325 // Normalize all whitespace except for the newline character into a
327 if($char !== "\n" && ord($char) < 32)
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.
341 * @return string Next 'real' character to be processed.
342 * @throws \RuntimeException
344 protected function getReal()
346 $startIndex = $this->index;
347 $char = $this->getChar();
349 // Check to see if we're potentially in a comment
354 $this->c = $this->getChar();
356 if ($this->c === '/') {
357 return $this->processOneLineComments($startIndex);
359 } elseif ($this->c === '*') {
360 return $this->processMultiLineComments($startIndex);
367 * Removed one line comments, with the exception of some very specific types of
368 * conditional comments.
370 * @param int $startIndex The index point where "getReal" function started
373 protected function processOneLineComments($startIndex)
375 $thirdCommentString = substr($this->input, $this->index, 1);
378 $this->getNext("\n");
380 if ($thirdCommentString == '@') {
381 $endPoint = $this->index - $startIndex;
383 $char = "\n" . substr($this->input, $startIndex, $endPoint);
385 // first one is contents of $this->c
387 $char = $this->getChar();
394 * Skips multiline comments where appropriate, and includes them where needed.
395 * Conditional comments and "license" style blocks are preserved.
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
401 protected function processMultiLineComments($startIndex)
403 $this->getChar(); // current C
404 $thirdCommentString = $this->getChar();
406 // kill everything up to the next */ if it's there
407 if ($this->getNext('*/')) {
409 $this->getChar(); // get *
410 $this->getChar(); // get /
411 $char = $this->getChar(); // get next real character
413 // Now we reinsert conditional comments and YUI-style licensing comments
414 if (($this->options['flaggedComments'] && $thirdCommentString === '!')
415 || ($thirdCommentString === '@') ) {
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) {
423 // If the comment started on a new line we let it stay on the new line
424 if ($this->input[($startIndex - 1)] === "\n") {
429 $endPoint = ($this->index - 1) - $startIndex;
430 echo substr($this->input, $startIndex, $endPoint);
440 throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2));
442 // if we're here c is part of the comment and therefore tossed
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
454 * @param string $string
455 * @return string|false Returns the first character of the string or false.
457 protected function getNext($string)
459 // Find the next occurrence of "string" after the current position.
460 $pos = strpos($this->input, $string, $this->index);
462 // If it's not there return false.
467 // Adjust position of index to jump ahead to the asked for string
470 // Return the first character of that string.
471 return substr($this->input, $this->index, 1);
475 * When a javascript string is detected this function crawls for the end of
476 * it and saves the whole string.
478 * @throws \RuntimeException Unclosed strings will throw an error
480 protected function saveString()
482 $startpos = $this->index;
484 // saveString is always called after a gets cleared, so we push b into
488 // If this isn't a string we don't need to do anything.
489 if ($this->a !== "'" && $this->a !== '"') {
493 // String type is the quote used, " or '
494 $stringType = $this->a;
496 // Echo out that starting quote
499 // Loop until the string is done
502 // Grab the very next character and load it into a
503 $this->a = $this->getChar();
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!
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
518 throw new \RuntimeException('Unclosed string at position: ' . $startpos );
521 // Escaped characters get picked up here. If it's an escaped new line it's not really needed
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();
529 // If b is a new line we discard a and b and restart the loop.
530 if ($this->b === "\n") {
534 // echo out the escaped character and restart the loop.
535 echo $this->a . $this->b;
539 // Since we're not dealing with any special cases we simply
540 // output the character and continue our loop.
548 * When a regular expression is detected this function crawls for the end of
549 * it and saves the whole regex.
551 * @throws \RuntimeException Unclosed regex will throw an error
553 protected function saveRegex()
555 echo $this->a . $this->b;
557 while (($this->a = $this->getChar()) !== false) {
561 if ($this->a === '\\') {
563 $this->a = $this->getChar();
566 if($this->a === "\n")
567 throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index);
571 $this->b = $this->getReal();
575 * Checks to see if a character is alphanumeric.
577 * @param string $char Just one character
580 protected static function isAlphaNumeric($char)
582 return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
586 * Replace patterns in the given string and store the replacement
588 * @param string $js The string to lock
591 protected function lock($js)
593 /* lock things like <code>"asd" + ++x;</code> */
594 $lock = '"LOCK---' . crc32(time()) . '"';
597 preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
598 if (empty($matches)) {
602 $this->locks[$lock] = $matches[2];
604 $js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
611 * Replace "locks" with the original characters
613 * @param string $js The string to unlock
616 protected function unlock($js)
618 if (empty($this->locks)) {
622 foreach ($this->locks as $lock => $replacement) {
623 $js = str_replace($lock, $replacement, $js);