]>
git.wh0rd.org - tt-rss.git/blob - vendor/JShrink/Minifier.php
3 * This file is part of the JShrink package.
5 * (c) Robert Hafner <tedivm@tedivm.com>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
16 * @author Robert Hafner <tedivm@tedivm.com>
24 * Usage - Minifier::minify($js);
25 * Usage - Minifier::minify($js, $options);
26 * Usage - Minifier::minify($js, array('flaggedComments' => false));
29 * @author Robert Hafner <tedivm@tedivm.com>
30 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
35 * The input javascript to be minified.
42 * The location of the character (in the input string) that is next to be
50 * The first of the characters currently being looked at.
57 * The next character being looked at (after a);
64 * This character is only active when certain look ahead actions take place.
71 * Contains the options for the current minification process.
78 * Contains the default options for minification. This array is merged with
79 * the one passed in by the user to create the request specific set of
80 * options (stored in the $options attribute).
84 protected static $defaultOptions = array('flaggedComments' => true);
87 * Contains lock ids which are used to replace certain code patterns and
88 * prevent them from being minified
92 protected $locks = array();
95 * Takes a string containing javascript and removes unneeded characters in
96 * order to shrink the code without altering it's functionality.
98 * @param string $js The raw javascript to be minified
99 * @param array $options Various runtime options in an associative array
101 * @return bool|string
103 public static function minify($js, $options = array())
108 $jshrink = new Minifier();
109 $js = $jshrink->lock($js);
110 $jshrink->minifyDirectToOutput($js, $options);
112 // Sometimes there's a leading new line, so we trim that out here.
113 $js = ltrim(ob_get_clean());
114 $js = $jshrink->unlock($js);
119 } catch (\Exception
$e) {
121 if (isset($jshrink)) {
122 // Since the breakdownScript function probably wasn't finished
123 // we clean it out before discarding it.
128 // without this call things get weird, with partially outputted js.
135 * Processes a javascript string and outputs only the required characters,
136 * stripping out all unneeded characters.
138 * @param string $js The raw javascript to be minified
139 * @param array $options Various runtime options in an associative array
141 protected function minifyDirectToOutput($js, $options)
143 $this->initialize($js, $options);
149 * Initializes internal variables, normalizes new lines,
151 * @param string $js The raw javascript to be minified
152 * @param array $options Various runtime options in an associative array
154 protected function initialize($js, $options)
156 $this->options
= array_merge(static::$defaultOptions, $options);
157 $js = str_replace("\r\n", "\n", $js);
158 $js = str_replace('/**/', '', $js);
159 $this->input
= str_replace("\r", "\n", $js);
161 // We add a newline to the end of the script to make it easier to deal
162 // with comments at the bottom of the script- this prevents the unclosed
163 // comment error that can otherwise occur.
164 $this->input
.= PHP_EOL
;
166 // Populate "a" with a new line, "b" with the first character, before
169 $this->b
= $this->getReal();
173 * The primary action occurs here. This function loops through the input string,
174 * outputting anything that's relevant and discarding anything that is not.
176 protected function loop()
178 while ($this->a
!== false && !is_null($this->a
) && $this->a
!== '') {
183 // if the next line is something that can't stand alone preserve the newline
184 if (strpos('(-+{[@', $this->b
) !== false) {
190 // if B is a space we skip the rest of the switch block and go down to the
191 // string/regex check below, resetting $this->b with getReal
195 // otherwise we treat the newline like a space
198 if(static::isAlphaNumeric($this->b
))
207 if (strpos('}])+-"\'', $this->a
) !== false) {
212 if (static::isAlphaNumeric($this->a
)) {
220 if(!static::isAlphaNumeric($this->a
))
224 // check for some regex that breaks stuff
225 if ($this->a
=== '/' && ($this->b
=== '\'' ||
$this->b
=== '"')) {
236 // do reg check of doom
237 $this->b
= $this->getReal();
239 if(($this->b
== '/' && strpos('(,=:[!&|?', $this->a
) !== false))
245 * Resets attributes that do not need to be stored between requests so that
246 * the next request is ready to go. Another reason for this is to make sure
247 * the variables are cleared and are not taking up memory.
249 protected function clean()
253 $this->a
= $this->b
= '';
255 unset($this->options
);
259 * Returns the next string for processing based off of the current index.
263 protected function getChar()
265 // Check to see if we had anything in the look ahead buffer and use that.
266 if (isset($this->c
)) {
270 // Otherwise we start pulling from the input.
272 $char = substr($this->input
, $this->index
, 1);
274 // If the next character doesn't exist return false.
275 if (isset($char) && $char === false) {
279 // Otherwise increment the pointer and use this char.
283 // Normalize all whitespace except for the newline character into a
285 if($char !== "\n" && ord($char) < 32)
293 * This function gets the next "real" character. It is essentially a wrapper
294 * around the getChar function that skips comments. This has significant
295 * performance benefits as the skipping is done using native functions (ie,
296 * c code) rather than in script php.
299 * @return string Next 'real' character to be processed.
300 * @throws \RuntimeException
302 protected function getReal()
304 $startIndex = $this->index
;
305 $char = $this->getChar();
307 // Check to see if we're potentially in a comment
312 $this->c
= $this->getChar();
314 if ($this->c
=== '/') {
315 return $this->processOneLineComments($startIndex);
317 } elseif ($this->c
=== '*') {
318 return $this->processMultiLineComments($startIndex);
325 * Removed one line comments, with the exception of some very specific types of
326 * conditional comments.
328 * @param int $startIndex The index point where "getReal" function started
331 protected function processOneLineComments($startIndex)
333 $thirdCommentString = substr($this->input
, $this->index
, 1);
336 $this->getNext("\n");
338 if ($thirdCommentString == '@') {
339 $endPoint = $this->index
- $startIndex;
341 $char = "\n" . substr($this->input
, $startIndex, $endPoint);
343 // first one is contents of $this->c
345 $char = $this->getChar();
352 * Skips multiline comments where appropriate, and includes them where needed.
353 * Conditional comments and "license" style blocks are preserved.
355 * @param int $startIndex The index point where "getReal" function started
356 * @return bool|string False if there's no character
357 * @throws \RuntimeException Unclosed comments will throw an error
359 protected function processMultiLineComments($startIndex)
361 $this->getChar(); // current C
362 $thirdCommentString = $this->getChar();
364 // kill everything up to the next */ if it's there
365 if ($this->getNext('*/')) {
367 $this->getChar(); // get *
368 $this->getChar(); // get /
369 $char = $this->getChar(); // get next real character
371 // Now we reinsert conditional comments and YUI-style licensing comments
372 if (($this->options
['flaggedComments'] && $thirdCommentString === '!')
373 ||
($thirdCommentString === '@') ) {
375 // If conditional comments or flagged comments are not the first thing in the script
376 // we need to echo a and fill it with a space before moving on.
377 if ($startIndex > 0) {
381 // If the comment started on a new line we let it stay on the new line
382 if ($this->input
[($startIndex - 1)] === "\n") {
387 $endPoint = ($this->index
- 1) - $startIndex;
388 echo substr($this->input
, $startIndex, $endPoint);
398 throw new \
RuntimeException('Unclosed multiline comment at position: ' . ($this->index
- 2));
400 // if we're here c is part of the comment and therefore tossed
408 * Pushes the index ahead to the next instance of the supplied string. If it
409 * is found the first character of the string is returned and the index is set
412 * @param string $string
413 * @return string|false Returns the first character of the string or false.
415 protected function getNext($string)
417 // Find the next occurrence of "string" after the current position.
418 $pos = strpos($this->input
, $string, $this->index
);
420 // If it's not there return false.
425 // Adjust position of index to jump ahead to the asked for string
428 // Return the first character of that string.
429 return substr($this->input
, $this->index
, 1);
433 * When a javascript string is detected this function crawls for the end of
434 * it and saves the whole string.
436 * @throws \RuntimeException Unclosed strings will throw an error
438 protected function saveString()
440 $startpos = $this->index
;
442 // saveString is always called after a gets cleared, so we push b into
446 // If this isn't a string we don't need to do anything.
447 if ($this->a
!== "'" && $this->a
!== '"') {
451 // String type is the quote used, " or '
452 $stringType = $this->a
;
454 // Echo out that starting quote
457 // Loop until the string is done
460 // Grab the very next character and load it into a
461 $this->a
= $this->getChar();
465 // If the string opener (single or double quote) is used
466 // output it and break out of the while loop-
467 // The string is finished!
471 // New lines in strings without line delimiters are bad- actual
472 // new lines will be represented by the string \n and not the actual
473 // character, so those will be treated just fine using the switch
476 throw new \
RuntimeException('Unclosed string at position: ' . $startpos );
479 // Escaped characters get picked up here. If it's an escaped new line it's not really needed
482 // a is a slash. We want to keep it, and the next character,
483 // unless it's a new line. New lines as actual strings will be
484 // preserved, but escaped new lines should be reduced.
485 $this->b
= $this->getChar();
487 // If b is a new line we discard a and b and restart the loop.
488 if ($this->b
=== "\n") {
492 // echo out the escaped character and restart the loop.
493 echo $this->a
. $this->b
;
497 // Since we're not dealing with any special cases we simply
498 // output the character and continue our loop.
506 * When a regular expression is detected this function crawls for the end of
507 * it and saves the whole regex.
509 * @throws \RuntimeException Unclosed regex will throw an error
511 protected function saveRegex()
513 echo $this->a
. $this->b
;
515 while (($this->a
= $this->getChar()) !== false) {
519 if ($this->a
=== '\\') {
521 $this->a
= $this->getChar();
524 if($this->a
=== "\n")
525 throw new \
RuntimeException('Unclosed regex pattern at position: ' . $this->index
);
529 $this->b
= $this->getReal();
533 * Checks to see if a character is alphanumeric.
535 * @param string $char Just one character
538 protected static function isAlphaNumeric($char)
540 return preg_match('/^[\w\$\pL]$/', $char) === 1 ||
$char == '/';
544 * Replace patterns in the given string and store the replacement
546 * @param string $js The string to lock
549 protected function lock($js)
551 /* lock things like <code>"asd" + ++x;</code> */
552 $lock = '"LOCK---' . crc32(time()) . '"';
555 preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
556 if (empty($matches)) {
560 $this->locks
[$lock] = $matches[2];
562 $js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
569 * Replace "locks" with the original characters
571 * @param string $js The string to unlock
574 protected function unlock($js)
576 if (empty($this->locks
)) {
580 foreach ($this->locks
as $lock => $replacement) {
581 $js = str_replace($lock, $replacement, $js);