PageRenderTime 42ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/common/libraries/plugin/minify/JSMin.php

https://bitbucket.org/chamilo/chamilo-dev/
PHP | 369 lines | 267 code | 14 blank | 88 comment | 36 complexity | caa833dd1a26292ace9fe35f20145eb6 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, LGPL-2.1, LGPL-3.0, GPL-3.0, MIT
  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. {
  54. const ORD_LF = 10;
  55. const ORD_SPACE = 32;
  56. const ACTION_KEEP_A = 1;
  57. const ACTION_DELETE_A = 2;
  58. const ACTION_DELETE_A_B = 3;
  59. protected $a = "\n";
  60. protected $b = '';
  61. protected $input = '';
  62. protected $inputIndex = 0;
  63. protected $inputLength = 0;
  64. protected $lookAhead = null;
  65. protected $output = '';
  66. /**
  67. * Minify Javascript
  68. *
  69. * @param string $js Javascript to be minified
  70. * @return string
  71. */
  72. public static function minify($js)
  73. {
  74. $jsmin = new JSMin($js);
  75. return $jsmin->min();
  76. }
  77. /**
  78. * Setup process
  79. */
  80. public function __construct($input)
  81. {
  82. $this->input = str_replace("\r\n", "\n", $input);
  83. $this->inputLength = strlen($this->input);
  84. }
  85. /**
  86. * Perform minification, return result
  87. */
  88. public function min()
  89. {
  90. if ($this->output !== '')
  91. { // min already run
  92. return $this->output;
  93. }
  94. $this->action(self :: ACTION_DELETE_A_B);
  95. while ($this->a !== null)
  96. {
  97. // determine next command
  98. $command = self :: ACTION_KEEP_A; // default
  99. if ($this->a === ' ')
  100. {
  101. if (! $this->isAlphaNum($this->b))
  102. {
  103. $command = self :: ACTION_DELETE_A;
  104. }
  105. }
  106. elseif ($this->a === "\n")
  107. {
  108. if ($this->b === ' ')
  109. {
  110. $command = self :: ACTION_DELETE_A_B;
  111. }
  112. elseif (false === strpos('{[(+-', $this->b) && ! $this->isAlphaNum($this->b))
  113. {
  114. $command = self :: ACTION_DELETE_A;
  115. }
  116. }
  117. elseif (! $this->isAlphaNum($this->a))
  118. {
  119. if ($this->b === ' ' || ($this->b === "\n" && (false === strpos('}])+-"\'', $this->a))))
  120. {
  121. $command = self :: ACTION_DELETE_A_B;
  122. }
  123. }
  124. $this->action($command);
  125. }
  126. $this->output = trim($this->output);
  127. return $this->output;
  128. }
  129. /**
  130. * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
  131. * ACTION_DELETE_A = Copy B to A. Get the next B.
  132. * ACTION_DELETE_A_B = Get the next B.
  133. */
  134. protected function action($command)
  135. {
  136. switch ($command)
  137. {
  138. case self :: ACTION_KEEP_A :
  139. $this->output .= $this->a;
  140. // fallthrough
  141. case self :: ACTION_DELETE_A :
  142. $this->a = $this->b;
  143. if ($this->a === "'" || $this->a === '"')
  144. { // string literal
  145. $str = $this->a; // in case needed for exception
  146. while (true)
  147. {
  148. $this->output .= $this->a;
  149. $this->a = $this->get();
  150. if ($this->a === $this->b)
  151. { // end quote
  152. break;
  153. }
  154. if (ord($this->a) <= self :: ORD_LF)
  155. {
  156. throw new JSMin_UnterminatedStringException('Unterminated String: ' . var_export($str, true));
  157. }
  158. $str .= $this->a;
  159. if ($this->a === '\\')
  160. {
  161. $this->output .= $this->a;
  162. $this->a = $this->get();
  163. $str .= $this->a;
  164. }
  165. }
  166. }
  167. // fallthrough
  168. case self :: ACTION_DELETE_A_B :
  169. $this->b = $this->next();
  170. if ($this->b === '/' && $this->isRegexpLiteral())
  171. { // RegExp literal
  172. $this->output .= $this->a . $this->b;
  173. $pattern = '/'; // in case needed for exception
  174. while (true)
  175. {
  176. $this->a = $this->get();
  177. $pattern .= $this->a;
  178. if ($this->a === '/')
  179. { // end pattern
  180. break; // while (true)
  181. }
  182. elseif ($this->a === '\\')
  183. {
  184. $this->output .= $this->a;
  185. $this->a = $this->get();
  186. $pattern .= $this->a;
  187. }
  188. elseif (ord($this->a) <= self :: ORD_LF)
  189. {
  190. throw new JSMin_UnterminatedRegExpException('Unterminated RegExp: ' . var_export($pattern, true));
  191. }
  192. $this->output .= $this->a;
  193. }
  194. $this->b = $this->next();
  195. }
  196. // end case ACTION_DELETE_A_B
  197. }
  198. }
  199. protected function isRegexpLiteral()
  200. {
  201. if (false !== strpos("\n{;(,=:[!&|?", $this->a))
  202. { // we aren't dividing
  203. return true;
  204. }
  205. if (' ' === $this->a)
  206. {
  207. $length = strlen($this->output);
  208. if ($length < 2)
  209. { // weird edge case
  210. return true;
  211. }
  212. // you can't divide a keyword
  213. if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m))
  214. {
  215. if ($this->output === $m[0])
  216. { // odd but could happen
  217. return true;
  218. }
  219. // make sure it's a keyword, not end of an identifier
  220. $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
  221. if (! $this->isAlphaNum($charBeforeKeyword))
  222. {
  223. return true;
  224. }
  225. }
  226. }
  227. return false;
  228. }
  229. /**
  230. * Get next char. Convert ctrl char to space.
  231. */
  232. protected function get()
  233. {
  234. $c = $this->lookAhead;
  235. $this->lookAhead = null;
  236. if ($c === null)
  237. {
  238. if ($this->inputIndex < $this->inputLength)
  239. {
  240. $c = $this->input[$this->inputIndex];
  241. $this->inputIndex += 1;
  242. }
  243. else
  244. {
  245. return null;
  246. }
  247. }
  248. if ($c === "\r" || $c === "\n")
  249. {
  250. return "\n";
  251. }
  252. if (ord($c) < self :: ORD_SPACE)
  253. { // control char
  254. return ' ';
  255. }
  256. return $c;
  257. }
  258. /**
  259. * Get next char. If is ctrl character, translate to a space or newline.
  260. */
  261. protected function peek()
  262. {
  263. $this->lookAhead = $this->get();
  264. return $this->lookAhead;
  265. }
  266. /**
  267. * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII?
  268. */
  269. protected function isAlphaNum($c)
  270. {
  271. return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126);
  272. }
  273. protected function singleLineComment()
  274. {
  275. $comment = '';
  276. while (true)
  277. {
  278. $get = $this->get();
  279. $comment .= $get;
  280. if (ord($get) <= self :: ORD_LF)
  281. { // EOL reached
  282. // if IE conditional comment
  283. if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment))
  284. {
  285. return "/{$comment}";
  286. }
  287. return $get;
  288. }
  289. }
  290. }
  291. protected function multipleLineComment()
  292. {
  293. $this->get();
  294. $comment = '';
  295. while (true)
  296. {
  297. $get = $this->get();
  298. if ($get === '*')
  299. {
  300. if ($this->peek() === '/')
  301. { // end of comment reached
  302. $this->get();
  303. // if comment preserved by YUI Compressor
  304. if (0 === strpos($comment, '!'))
  305. {
  306. return "\n/*" . substr($comment, 1) . "*/\n";
  307. }
  308. // if IE conditional comment
  309. if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment))
  310. {
  311. return "/*{$comment}*/";
  312. }
  313. return ' ';
  314. }
  315. }
  316. elseif ($get === null)
  317. {
  318. throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true));
  319. }
  320. $comment .= $get;
  321. }
  322. }
  323. /**
  324. * Get the next character, skipping over comments.
  325. * Some comments may be preserved.
  326. */
  327. protected function next()
  328. {
  329. $get = $this->get();
  330. if ($get !== '/')
  331. {
  332. return $get;
  333. }
  334. switch ($this->peek())
  335. {
  336. case '/' :
  337. return $this->singleLineComment();
  338. case '*' :
  339. return $this->multipleLineComment();
  340. default :
  341. return $get;
  342. }
  343. }
  344. }
  345. class JSMin_UnterminatedStringException extends Exception
  346. {
  347. }
  348. class JSMin_UnterminatedCommentException extends Exception
  349. {
  350. }
  351. class JSMin_UnterminatedRegExpException extends Exception
  352. {
  353. }