jsmin.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <?php
  2. /**
  3. * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
  4. *
  5. * This is pretty much a direct port of jsmin.c to PHP with just a few
  6. * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
  7. * outputs to stdout, this library accepts a string as input and returns another
  8. * string as output.
  9. *
  10. * PHP 5 or higher is required.
  11. *
  12. * Permission is hereby granted to use this version of the library under the
  13. * same terms as jsmin.c, which has the following license:
  14. *
  15. * --
  16. * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
  17. *
  18. * Permission is hereby granted, free of charge, to any person obtaining a copy of
  19. * this software and associated documentation files (the "Software"), to deal in
  20. * the Software without restriction, including without limitation the rights to
  21. * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  22. * of the Software, and to permit persons to whom the Software is furnished to do
  23. * so, subject to the following conditions:
  24. *
  25. * The above copyright notice and this permission notice shall be included in all
  26. * copies or substantial portions of the Software.
  27. *
  28. * The Software shall be used for Good, not Evil.
  29. *
  30. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  31. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  32. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  33. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  34. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  35. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  36. * SOFTWARE.
  37. * --
  38. *
  39. * @package JSMin
  40. * @author Ryan Grove <ryan@wonko.com>
  41. * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
  42. * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
  43. * @license http://opensource.org/licenses/mit-license.php MIT License
  44. * @version 1.1.1 (2008-03-02)
  45. * @link https://github.com/rgrove/jsmin-php/
  46. */
  47. class JSMin {
  48. const ORD_LF = 10;
  49. const ORD_SPACE = 32;
  50. const ACTION_KEEP_A = 1;
  51. const ACTION_DELETE_A = 2;
  52. const ACTION_DELETE_A_B = 3;
  53. protected $a = '';
  54. protected $b = '';
  55. protected $input = '';
  56. protected $inputIndex = 0;
  57. protected $inputLength = 0;
  58. protected $lookAhead = null;
  59. protected $output = '';
  60. // -- Public Static Methods --------------------------------------------------
  61. /**
  62. * Minify Javascript
  63. *
  64. * @uses __construct()
  65. * @uses min()
  66. * @param string $js Javascript to be minified
  67. * @return string
  68. */
  69. public static function minify($js) {
  70. $jsmin = new JSMin($js);
  71. return $jsmin->min();
  72. }
  73. // -- Public Instance Methods ------------------------------------------------
  74. /**
  75. * Constructor
  76. *
  77. * @param string $input Javascript to be minified
  78. */
  79. public function __construct($input) {
  80. $this->input = str_replace("\r\n", "\n", $input);
  81. $this->inputLength = strlen($this->input);
  82. }
  83. // -- Protected Instance Methods ---------------------------------------------
  84. /**
  85. * Action -- do something! What to do is determined by the $command argument.
  86. *
  87. * action treats a string as a single character. Wow!
  88. * action recognizes a regular expression if it is preceded by ( or , or =.
  89. *
  90. * @uses next()
  91. * @uses get()
  92. * @throws JSMinException If parser errors are found:
  93. * - Unterminated string literal
  94. * - Unterminated regular expression set in regex literal
  95. * - Unterminated regular expression literal
  96. * @param int $command One of class constants:
  97. * ACTION_KEEP_A Output A. Copy B to A. Get the next B.
  98. * ACTION_DELETE_A Copy B to A. Get the next B. (Delete A).
  99. * ACTION_DELETE_A_B Get the next B. (Delete B).
  100. */
  101. protected function action($command) {
  102. switch($command) {
  103. case self::ACTION_KEEP_A:
  104. $this->output .= $this->a;
  105. case self::ACTION_DELETE_A:
  106. $this->a = $this->b;
  107. if ($this->a === "'" || $this->a === '"') {
  108. for (;;) {
  109. $this->output .= $this->a;
  110. $this->a = $this->get();
  111. if ($this->a === $this->b) {
  112. break;
  113. }
  114. if (ord($this->a) <= self::ORD_LF) {
  115. throw new JSMinException('Unterminated string literal.');
  116. }
  117. if ($this->a === '\\') {
  118. $this->output .= $this->a;
  119. $this->a = $this->get();
  120. }
  121. }
  122. }
  123. case self::ACTION_DELETE_A_B:
  124. $this->b = $this->next();
  125. if ($this->b === '/' && (
  126. $this->a === '(' || $this->a === ',' || $this->a === '=' ||
  127. $this->a === ':' || $this->a === '[' || $this->a === '!' ||
  128. $this->a === '&' || $this->a === '|' || $this->a === '?' ||
  129. $this->a === '{' || $this->a === '}' || $this->a === ';' ||
  130. $this->a === "\n" )) {
  131. $this->output .= $this->a . $this->b;
  132. for (;;) {
  133. $this->a = $this->get();
  134. if ($this->a === '[') {
  135. /*
  136. inside a regex [...] set, which MAY contain a '/' itself. Example: mootools Form.Validator near line 460:
  137. return Form.Validator.getValidator('IsEmpty').test(element) || (/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]\.?){0,63}[a-z0-9!#$%&'*+/=?^_`{|}~-]@(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])$/i).test(element.get('value'));
  138. */
  139. for (;;) {
  140. $this->output .= $this->a;
  141. $this->a = $this->get();
  142. if ($this->a === ']') {
  143. break;
  144. } elseif ($this->a === '\\') {
  145. $this->output .= $this->a;
  146. $this->a = $this->get();
  147. } elseif (ord($this->a) <= self::ORD_LF) {
  148. throw new JSMinException('Unterminated regular expression set in regex literal.');
  149. }
  150. }
  151. } elseif ($this->a === '/') {
  152. break;
  153. } elseif ($this->a === '\\') {
  154. $this->output .= $this->a;
  155. $this->a = $this->get();
  156. } elseif (ord($this->a) <= self::ORD_LF) {
  157. throw new JSMinException('Unterminated regular expression literal.');
  158. }
  159. $this->output .= $this->a;
  160. }
  161. $this->b = $this->next();
  162. }
  163. }
  164. }
  165. /**
  166. * Get next char. Convert ctrl char to space.
  167. *
  168. * @return string|null
  169. */
  170. protected function get() {
  171. $c = $this->lookAhead;
  172. $this->lookAhead = null;
  173. if ($c === null) {
  174. if ($this->inputIndex < $this->inputLength) {
  175. $c = substr($this->input, $this->inputIndex, 1);
  176. $this->inputIndex += 1;
  177. } else {
  178. $c = null;
  179. }
  180. }
  181. if ($c === "\r") {
  182. return "\n";
  183. }
  184. if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) {
  185. return $c;
  186. }
  187. return ' ';
  188. }
  189. /**
  190. * Is $c a letter, digit, underscore, dollar sign, or non-ASCII character.
  191. *
  192. * @return bool
  193. */
  194. protected function isAlphaNum($c) {
  195. return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
  196. }
  197. /**
  198. * Perform minification, return result
  199. *
  200. * @uses action()
  201. * @uses isAlphaNum()
  202. * @return string
  203. */
  204. protected function min() {
  205. $this->a = "\n";
  206. $this->action(self::ACTION_DELETE_A_B);
  207. while ($this->a !== null) {
  208. switch ($this->a) {
  209. case ' ':
  210. if ($this->isAlphaNum($this->b)) {
  211. $this->action(self::ACTION_KEEP_A);
  212. } else {
  213. $this->action(self::ACTION_DELETE_A);
  214. }
  215. break;
  216. case "\n":
  217. switch ($this->b) {
  218. case '{':
  219. case '[':
  220. case '(':
  221. case '+':
  222. case '-':
  223. $this->action(self::ACTION_KEEP_A);
  224. break;
  225. case ' ':
  226. $this->action(self::ACTION_DELETE_A_B);
  227. break;
  228. default:
  229. if ($this->isAlphaNum($this->b)) {
  230. $this->action(self::ACTION_KEEP_A);
  231. }
  232. else {
  233. $this->action(self::ACTION_DELETE_A);
  234. }
  235. }
  236. break;
  237. default:
  238. switch ($this->b) {
  239. case ' ':
  240. if ($this->isAlphaNum($this->a)) {
  241. $this->action(self::ACTION_KEEP_A);
  242. break;
  243. }
  244. $this->action(self::ACTION_DELETE_A_B);
  245. break;
  246. case "\n":
  247. switch ($this->a) {
  248. case '}':
  249. case ']':
  250. case ')':
  251. case '+':
  252. case '-':
  253. case '"':
  254. case "'":
  255. $this->action(self::ACTION_KEEP_A);
  256. break;
  257. default:
  258. if ($this->isAlphaNum($this->a)) {
  259. $this->action(self::ACTION_KEEP_A);
  260. }
  261. else {
  262. $this->action(self::ACTION_DELETE_A_B);
  263. }
  264. }
  265. break;
  266. default:
  267. $this->action(self::ACTION_KEEP_A);
  268. break;
  269. }
  270. }
  271. }
  272. return $this->output;
  273. }
  274. /**
  275. * Get the next character, skipping over comments. peek() is used to see
  276. * if a '/' is followed by a '/' or '*'.
  277. *
  278. * @uses get()
  279. * @uses peek()
  280. * @throws JSMinException On unterminated comment.
  281. * @return string
  282. */
  283. protected function next() {
  284. $c = $this->get();
  285. if ($c === '/') {
  286. switch($this->peek()) {
  287. case '/':
  288. for (;;) {
  289. $c = $this->get();
  290. if (ord($c) <= self::ORD_LF) {
  291. return $c;
  292. }
  293. }
  294. case '*':
  295. $this->get();
  296. for (;;) {
  297. switch($this->get()) {
  298. case '*':
  299. if ($this->peek() === '/') {
  300. $this->get();
  301. return ' ';
  302. }
  303. break;
  304. case null:
  305. throw new JSMinException('Unterminated comment.');
  306. }
  307. }
  308. default:
  309. return $c;
  310. }
  311. }
  312. return $c;
  313. }
  314. /**
  315. * Get next char. If is ctrl character, translate to a space or newline.
  316. *
  317. * @uses get()
  318. * @return string|null
  319. */
  320. protected function peek() {
  321. $this->lookAhead = $this->get();
  322. return $this->lookAhead;
  323. }
  324. }
  325. // -- Exceptions ---------------------------------------------------------------
  326. class JSMinException extends Exception {}
  327. ?>