JSMin.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. <?php
  2. /**
  3. * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
  4. *
  5. * This is a direct port of jsmin.c to PHP with a few PHP performance tweaks and
  6. * modifications to preserve some comments (see below). Also, rather than using
  7. * stdin/stdout, JSMin::minify() accepts a string as input and returns another
  8. * string as output.
  9. *
  10. * Comments containing IE conditional compilation are preserved, as are multi-line
  11. * comments that begin with "/*!" (for documentation purposes). In the latter case
  12. * newlines are inserted around the comment to enhance readability.
  13. *
  14. * PHP 5 or higher is required.
  15. *
  16. * Permission is hereby granted to use this version of the library under the
  17. * same terms as jsmin.c, which has the following license:
  18. *
  19. * --
  20. * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
  21. *
  22. * Permission is hereby granted, free of charge, to any person obtaining a copy of
  23. * this software and associated documentation files (the "Software"), to deal in
  24. * the Software without restriction, including without limitation the rights to
  25. * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  26. * of the Software, and to permit persons to whom the Software is furnished to do
  27. * so, subject to the following conditions:
  28. *
  29. * The above copyright notice and this permission notice shall be included in all
  30. * copies or substantial portions of the Software.
  31. *
  32. * The Software shall be used for Good, not Evil.
  33. *
  34. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  35. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  36. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  37. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  38. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  39. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  40. * SOFTWARE.
  41. * --
  42. *
  43. * @package JSMin
  44. * @author Ryan Grove <ryan@wonko.com> (PHP port)
  45. * @author Steve Clay <steve@mrclay.org> (modifications + cleanup)
  46. * @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp)
  47. * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
  48. * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
  49. * @license http://opensource.org/licenses/mit-license.php MIT License
  50. * @link http://code.google.com/p/jsmin-php/
  51. */
  52. class JSMin {
  53. const ORD_LF = 10;
  54. const ORD_SPACE = 32;
  55. const ACTION_KEEP_A = 1;
  56. const ACTION_DELETE_A = 2;
  57. const ACTION_DELETE_A_B = 3;
  58. protected $a = "\n";
  59. protected $b = '';
  60. protected $input = '';
  61. protected $inputIndex = 0;
  62. protected $inputLength = 0;
  63. protected $lookAhead = null;
  64. protected $output = '';
  65. /**
  66. * Minify Javascript
  67. *
  68. * @param string $js Javascript to be minified
  69. * @return string
  70. */
  71. public static function minify($js)
  72. {
  73. $jsmin = new JSMin($js);
  74. return $jsmin->min();
  75. }
  76. /**
  77. * Setup process
  78. */
  79. public function __construct($input)
  80. {
  81. $this->input = str_replace("\r\n", "\n", $input);
  82. $this->inputLength = strlen($this->input);
  83. }
  84. /**
  85. * Perform minification, return result
  86. */
  87. public function min()
  88. {
  89. if ($this->output !== '') { // min already run
  90. return $this->output;
  91. }
  92. $this->action(self::ACTION_DELETE_A_B);
  93. while ($this->a !== null) {
  94. // determine next command
  95. $command = self::ACTION_KEEP_A; // default
  96. if ($this->a === ' ') {
  97. if (! $this->isAlphaNum($this->b)) {
  98. $command = self::ACTION_DELETE_A;
  99. }
  100. } elseif ($this->a === "\n") {
  101. if ($this->b === ' ') {
  102. $command = self::ACTION_DELETE_A_B;
  103. } elseif (false === strpos('{[(+-', $this->b)
  104. && ! $this->isAlphaNum($this->b)) {
  105. $command = self::ACTION_DELETE_A;
  106. }
  107. } elseif (! $this->isAlphaNum($this->a)) {
  108. if ($this->b === ' '
  109. || ($this->b === "\n"
  110. && (false === strpos('}])+-"\'', $this->a)))) {
  111. $command = self::ACTION_DELETE_A_B;
  112. }
  113. }
  114. $this->action($command);
  115. }
  116. $this->output = trim($this->output);
  117. return $this->output;
  118. }
  119. /**
  120. * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
  121. * ACTION_DELETE_A = Copy B to A. Get the next B.
  122. * ACTION_DELETE_A_B = Get the next B.
  123. */
  124. protected function action($command)
  125. {
  126. switch ($command) {
  127. case self::ACTION_KEEP_A:
  128. $this->output .= $this->a;
  129. // fallthrough
  130. case self::ACTION_DELETE_A:
  131. $this->a = $this->b;
  132. if ($this->a === "'" || $this->a === '"') { // string literal
  133. $str = $this->a; // in case needed for exception
  134. while (true) {
  135. $this->output .= $this->a;
  136. $this->a = $this->get();
  137. if ($this->a === $this->b) { // end quote
  138. break;
  139. }
  140. if (ord($this->a) <= self::ORD_LF) {
  141. throw new JSMin_UnterminatedStringException(
  142. 'Unterminated String: ' . var_export($str, true));
  143. }
  144. $str .= $this->a;
  145. if ($this->a === '\\') {
  146. $this->output .= $this->a;
  147. $this->a = $this->get();
  148. $str .= $this->a;
  149. }
  150. }
  151. }
  152. // fallthrough
  153. case self::ACTION_DELETE_A_B:
  154. $this->b = $this->next();
  155. if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal
  156. $this->output .= $this->a . $this->b;
  157. $pattern = '/'; // in case needed for exception
  158. while (true) {
  159. $this->a = $this->get();
  160. $pattern .= $this->a;
  161. if ($this->a === '/') { // end pattern
  162. break; // while (true)
  163. } elseif ($this->a === '\\') {
  164. $this->output .= $this->a;
  165. $this->a = $this->get();
  166. $pattern .= $this->a;
  167. } elseif (ord($this->a) <= self::ORD_LF) {
  168. throw new JSMin_UnterminatedRegExpException(
  169. 'Unterminated RegExp: '. var_export($pattern, true));
  170. }
  171. $this->output .= $this->a;
  172. }
  173. $this->b = $this->next();
  174. }
  175. // end case ACTION_DELETE_A_B
  176. }
  177. }
  178. protected function isRegexpLiteral()
  179. {
  180. if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing
  181. return true;
  182. }
  183. if (' ' === $this->a) {
  184. $length = strlen($this->output);
  185. if ($length < 2) { // weird edge case
  186. return true;
  187. }
  188. // you can't divide a keyword
  189. if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) {
  190. if ($this->output === $m[0]) { // odd but could happen
  191. return true;
  192. }
  193. // make sure it's a keyword, not end of an identifier
  194. $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
  195. if (! $this->isAlphaNum($charBeforeKeyword)) {
  196. return true;
  197. }
  198. }
  199. }
  200. return false;
  201. }
  202. /**
  203. * Get next char. Convert ctrl char to space.
  204. */
  205. protected function get()
  206. {
  207. $c = $this->lookAhead;
  208. $this->lookAhead = null;
  209. if ($c === null) {
  210. if ($this->inputIndex < $this->inputLength) {
  211. $c = $this->input[$this->inputIndex];
  212. $this->inputIndex += 1;
  213. } else {
  214. return null;
  215. }
  216. }
  217. if ($c === "\r" || $c === "\n") {
  218. return "\n";
  219. }
  220. if (ord($c) < self::ORD_SPACE) { // control char
  221. return ' ';
  222. }
  223. return $c;
  224. }
  225. /**
  226. * Get next char. If is ctrl character, translate to a space or newline.
  227. */
  228. protected function peek()
  229. {
  230. $this->lookAhead = $this->get();
  231. return $this->lookAhead;
  232. }
  233. /**
  234. * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII?
  235. */
  236. protected function isAlphaNum($c)
  237. {
  238. return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126);
  239. }
  240. protected function singleLineComment()
  241. {
  242. $comment = '';
  243. while (true) {
  244. $get = $this->get();
  245. $comment .= $get;
  246. if (ord($get) <= self::ORD_LF) { // EOL reached
  247. // if IE conditional comment
  248. if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
  249. return "/{$comment}";
  250. }
  251. return $get;
  252. }
  253. }
  254. }
  255. protected function multipleLineComment()
  256. {
  257. $this->get();
  258. $comment = '';
  259. while (true) {
  260. $get = $this->get();
  261. if ($get === '*') {
  262. if ($this->peek() === '/') { // end of comment reached
  263. $this->get();
  264. // if comment preserved by YUI Compressor
  265. if (0 === strpos($comment, '!')) {
  266. return "\n/*" . substr($comment, 1) . "*/\n";
  267. }
  268. // if IE conditional comment
  269. if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
  270. return "/*{$comment}*/";
  271. }
  272. return ' ';
  273. }
  274. } elseif ($get === null) {
  275. throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true));
  276. }
  277. $comment .= $get;
  278. }
  279. }
  280. /**
  281. * Get the next character, skipping over comments.
  282. * Some comments may be preserved.
  283. */
  284. protected function next()
  285. {
  286. $get = $this->get();
  287. if ($get !== '/') {
  288. return $get;
  289. }
  290. switch ($this->peek()) {
  291. case '/': return $this->singleLineComment();
  292. case '*': return $this->multipleLineComment();
  293. default: return $get;
  294. }
  295. }
  296. }
  297. class JSMin_UnterminatedStringException extends Exception {}
  298. class JSMin_UnterminatedCommentException extends Exception {}
  299. class JSMin_UnterminatedRegExpException extends Exception {}