jshrink.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <?php
  2. /*
  3. JShrink
  4. Copyright (c) 2009, Robert Hafner
  5. All rights reserved.
  6. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
  7. * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
  8. disclaimer.
  9. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
  10. following disclaimer in the documentation and/or other materials provided with the distribution.
  11. * Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote
  12. products derived from this software without specific prior written permission.
  13. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  14. INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  15. DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  16. SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  17. SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  18. WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  19. THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  20. */
  21. /**
  22. * JShrink
  23. *
  24. * Usage - JShrink::minify($js);
  25. * Usage - JShrink::minify($js, $options);
  26. * Usage - JShrink::minify($js, array('flaggedComments' => false));
  27. *
  28. * @version 0.2
  29. * @package JShrink
  30. * @author Robert Hafner <tedivm@tedivm.com>
  31. * @license http://www.opensource.org/licenses/bsd-license.php
  32. */
  33. class JShrink
  34. {
  35. protected $input;
  36. protected $index = 0;
  37. protected $a = '';
  38. protected $b = '';
  39. protected $c;
  40. protected $options;
  41. static protected $defaultOptions = array('flaggedComments' => true);
  42. static public function minify($js, $options = array())
  43. {
  44. try{
  45. $currentOptions = array_merge(self::$defaultOptions, $options);
  46. ob_start();
  47. $currentOptions = array_merge(self::$defaultOptions, $options);
  48. $me = new JShrink();
  49. $me->breakdownScript($js, $currentOptions);
  50. $output = ob_get_clean();
  51. return $output;
  52. }catch(Exception $e){
  53. ob_end_clean();
  54. throw $e;
  55. }
  56. }
  57. protected function breakdownScript($js, $currentOptions)
  58. {
  59. $this->options = $currentOptions;
  60. $js = str_replace("\r\n", "\n", $js);
  61. $this->input = str_replace("\r", "\n", $js);
  62. $this->a = $this->getReal();
  63. // the only time the length can be higher than 1 is if a conditional comment needs to be displayed
  64. // and the only time that can happen for $a is on the very first run
  65. while(strlen($this->a) > 1)
  66. {
  67. echo $this->a;
  68. $this->a = $this->getReal();
  69. }
  70. $this->b = $this->getReal();
  71. while($this->a !== false && !is_null($this->a) && $this->a !== '')
  72. {
  73. // now we give $b the same check for conditional comments we gave $a before we began looping
  74. if(strlen($this->b) > 1)
  75. {
  76. echo $this->a . $this->b;
  77. $this->a = $this->getReal();
  78. $this->b = $this->getReal();
  79. continue;
  80. }
  81. switch($this->a)
  82. {
  83. // new lines
  84. case "\n":
  85. // if the next line is something that can't stand alone preserver the newline
  86. if(strpos('(-+{[@', $this->b) !== false)
  87. {
  88. echo $this->a;
  89. $this->saveString();
  90. break;
  91. }
  92. // if its a space we move down to the string test below
  93. if($this->b === ' ')
  94. break;
  95. // otherwise we treat the newline like a space
  96. case ' ':
  97. if(self::isAlphaNumeric($this->b))
  98. echo $this->a;
  99. $this->saveString();
  100. break;
  101. default:
  102. switch($this->b)
  103. {
  104. case "\n":
  105. if(strpos('}])+-"\'', $this->a) !== false)
  106. {
  107. echo $this->a;
  108. $this->saveString();
  109. break;
  110. }else{
  111. if(self::isAlphaNumeric($this->a))
  112. {
  113. echo $this->a;
  114. $this->saveString();
  115. }
  116. }
  117. break;
  118. case ' ':
  119. if(!self::isAlphaNumeric($this->a))
  120. break;
  121. default:
  122. // check for some regex that breaks stuff
  123. if($this->a == '/' && ($this->b == '\'' || $this->b == '"'))
  124. {
  125. $this->saveRegex();
  126. continue;
  127. }
  128. echo $this->a;
  129. $this->saveString();
  130. break;
  131. }
  132. }
  133. // do reg check of doom
  134. $this->b = $this->getReal();
  135. if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
  136. $this->saveRegex();
  137. }
  138. }
  139. protected function getChar()
  140. {
  141. if(isset($this->c))
  142. {
  143. $char = $this->c;
  144. unset($this->c);
  145. }else{
  146. if(isset($this->input[$this->index]))
  147. {
  148. $char = $this->input[$this->index];
  149. $this->index++;
  150. }else{
  151. return false;
  152. }
  153. }
  154. if($char === "\n" || ord($char) >= 32)
  155. return $char;
  156. return ' ';
  157. }
  158. protected function getReal()
  159. {
  160. $startIndex = $this->index;
  161. $char = $this->getChar();
  162. if($char == '/')
  163. {
  164. $this->c = $this->getChar();
  165. if($this->c == '/')
  166. {
  167. $thirdCommentString = $this->input[$this->index];
  168. // kill rest of line
  169. $char = $this->getNext("\n");
  170. if($thirdCommentString == '@')
  171. {
  172. $endPoint = ($this->index) - $startIndex;
  173. unset($this->c);
  174. $char = "\n" . substr($this->input, $startIndex, $endPoint);// . "\n";
  175. }else{
  176. $char = $this->getChar();
  177. $char = $this->getChar();
  178. }
  179. }elseif($this->c == '*'){
  180. $this->getChar(); // current C
  181. $thirdCommentString = $this->getChar();
  182. if($thirdCommentString == '@')
  183. {
  184. // we're gonna back up a bit and and send the comment back, where the first
  185. // char will be echoed and the rest will be treated like a string
  186. $this->index = $this->index-2;
  187. return '/';
  188. }elseif($this->getNext('*/')){
  189. // kill everything up to the next */
  190. $this->getChar(); // get *
  191. $this->getChar(); // get /
  192. $char = $this->getChar(); // get next real charactor
  193. // if YUI-style comments are enabled we reinsert it into the stream
  194. if($this->options['flaggedComments'] && $thirdCommentString == '!')
  195. {
  196. $endPoint = ($this->index - 1) - $startIndex;
  197. echo "\n" . substr($this->input, $startIndex, $endPoint) . "\n";
  198. }
  199. }else{
  200. $char = false;
  201. }
  202. if($char === false)
  203. throw new JShrinkException('Stray comment. ' . $this->index);
  204. // if we're here c is part of the comment and therefore tossed
  205. if(isset($this->c))
  206. unset($this->c);
  207. }
  208. }
  209. return $char;
  210. }
  211. protected function getNext($string)
  212. {
  213. $pos = strpos($this->input, $string, $this->index);
  214. if($pos === false)
  215. return false;
  216. $this->index = $pos ;
  217. return $this->input[$this->index];
  218. }
  219. protected function saveString()
  220. {
  221. $this->a = $this->b;
  222. if($this->a == '\'' || $this->a == '"')
  223. {
  224. // save literal string
  225. $stringType = $this->a;
  226. while(1)
  227. {
  228. echo $this->a;
  229. $this->a = $this->getChar();
  230. switch($this->a)
  231. {
  232. case $stringType:
  233. break 2;
  234. case "\n":
  235. throw new JShrinkException('Unclosed string. ' . $this->index);
  236. break;
  237. case '\\':
  238. echo $this->a;
  239. $this->a = $this->getChar();
  240. }
  241. }
  242. }
  243. }
  244. protected function saveRegex()
  245. {
  246. echo $this->a . $this->b;
  247. while(($this->a = $this->getChar()) !== false)
  248. {
  249. if($this->a == '/')
  250. break;
  251. if($this->a == '\\')
  252. {
  253. echo $this->a;
  254. $this->a = $this->getChar();
  255. }
  256. if($this->a == "\n")
  257. throw new JShrinkException('Stray regex pattern. ' . $this->index);
  258. echo $this->a;
  259. }
  260. $this->b = $this->getReal();
  261. }
  262. static protected function isAlphaNumeric($char)
  263. {
  264. return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/';
  265. }
  266. }
  267. // Adding a custom exception handler for your own projects just means changing this line
  268. class JShrinkException extends Exception {}
  269. ?>