PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/Plugin/CoreCompiler/lib/JSMin.php

https://gitlab.com/x33n/ImpressPages
PHP | 437 lines | 270 code | 25 blank | 142 comment | 57 complexity | da47a12a17e5e62a3995367a13197861 MD5 | raw file
  1. <?php
  2. /**
  3. * JSMin.php - modified PHP implementation of Douglas Crockford's JSMin.
  4. *
  5. * <code>
  6. * $minifiedJs = JSMin::minify($js);
  7. * </code>
  8. *
  9. * This is a modified port of jsmin.c. Improvements:
  10. *
  11. * Does not choke on some regexp literals containing quote characters. E.g. /'/
  12. *
  13. * Spaces are preserved after some add/sub operators, so they are not mistakenly
  14. * converted to post-inc/dec. E.g. a + ++b -> a+ ++b
  15. *
  16. * Preserves multi-line comments that begin with /*!
  17. *
  18. * PHP 5 or higher is required.
  19. *
  20. * Permission is hereby granted to use this version of the library under the
  21. * same terms as jsmin.c, which has the following license:
  22. *
  23. * --
  24. * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
  25. *
  26. * Permission is hereby granted, free of charge, to any person obtaining a copy of
  27. * this software and associated documentation files (the "Software"), to deal in
  28. * the Software without restriction, including without limitation the rights to
  29. * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  30. * of the Software, and to permit persons to whom the Software is furnished to do
  31. * so, subject to the following conditions:
  32. *
  33. * The above copyright notice and this permission notice shall be included in all
  34. * copies or substantial portions of the Software.
  35. *
  36. * The Software shall be used for Good, not Evil.
  37. *
  38. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  39. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  40. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  41. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  42. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  43. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  44. * SOFTWARE.
  45. * --
  46. *
  47. * @package JSMin
  48. * @author Ryan Grove <ryan@wonko.com> (PHP port)
  49. * @author Steve Clay <steve@mrclay.org> (modifications + cleanup)
  50. * @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp)
  51. * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
  52. * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
  53. * @license http://opensource.org/licenses/mit-license.php MIT License
  54. * @link http://code.google.com/p/jsmin-php/
  55. */
  56. class JSMin {
  57. const ORD_LF = 10;
  58. const ORD_SPACE = 32;
  59. const ACTION_KEEP_A = 1;
  60. const ACTION_DELETE_A = 2;
  61. const ACTION_DELETE_A_B = 3;
  62. protected $a = "\n";
  63. protected $b = '';
  64. protected $input = '';
  65. protected $inputIndex = 0;
  66. protected $inputLength = 0;
  67. protected $lookAhead = null;
  68. protected $output = '';
  69. protected $lastByteOut = '';
  70. protected $keptComment = '';
  71. /**
  72. * Minify Javascript.
  73. *
  74. * @param string $js Javascript to be minified
  75. *
  76. * @return string
  77. */
  78. public static function minify($js)
  79. {
  80. $jsmin = new JSMin($js);
  81. return $jsmin->min();
  82. }
  83. /**
  84. * @param string $input
  85. */
  86. public function __construct($input)
  87. {
  88. $this->input = $input;
  89. }
  90. /**
  91. * Perform minification, return result
  92. *
  93. * @return string
  94. */
  95. public function min()
  96. {
  97. if ($this->output !== '') { // min already run
  98. return $this->output;
  99. }
  100. $mbIntEnc = null;
  101. if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) {
  102. $mbIntEnc = mb_internal_encoding();
  103. mb_internal_encoding('8bit');
  104. }
  105. $this->input = str_replace("\r\n", "\n", $this->input);
  106. $this->inputLength = strlen($this->input);
  107. $this->action(self::ACTION_DELETE_A_B);
  108. while ($this->a !== null) {
  109. // determine next command
  110. $command = self::ACTION_KEEP_A; // default
  111. if ($this->a === ' ') {
  112. if (($this->lastByteOut === '+' || $this->lastByteOut === '-')
  113. && ($this->b === $this->lastByteOut)) {
  114. // Don't delete this space. If we do, the addition/subtraction
  115. // could be parsed as a post-increment
  116. } elseif (! $this->isAlphaNum($this->b)) {
  117. $command = self::ACTION_DELETE_A;
  118. }
  119. } elseif ($this->a === "\n") {
  120. if ($this->b === ' ') {
  121. $command = self::ACTION_DELETE_A_B;
  122. // in case of mbstring.func_overload & 2, must check for null b,
  123. // otherwise mb_strpos will give WARNING
  124. } elseif ($this->b === null
  125. || (false === strpos('{[(+-!~', $this->b)
  126. && ! $this->isAlphaNum($this->b))) {
  127. $command = self::ACTION_DELETE_A;
  128. }
  129. } elseif (! $this->isAlphaNum($this->a)) {
  130. if ($this->b === ' '
  131. || ($this->b === "\n"
  132. && (false === strpos('}])+-"\'', $this->a)))) {
  133. $command = self::ACTION_DELETE_A_B;
  134. }
  135. }
  136. $this->action($command);
  137. }
  138. $this->output = trim($this->output);
  139. if ($mbIntEnc !== null) {
  140. mb_internal_encoding($mbIntEnc);
  141. }
  142. return $this->output;
  143. }
  144. /**
  145. * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
  146. * ACTION_DELETE_A = Copy B to A. Get the next B.
  147. * ACTION_DELETE_A_B = Get the next B.
  148. *
  149. * @param int $command
  150. * @throws JSMin_UnterminatedRegExpException|JSMin_UnterminatedStringException
  151. */
  152. protected function action($command)
  153. {
  154. // make sure we don't compress "a + ++b" to "a+++b", etc.
  155. if ($command === self::ACTION_DELETE_A_B
  156. && $this->b === ' '
  157. && ($this->a === '+' || $this->a === '-')) {
  158. // Note: we're at an addition/substraction operator; the inputIndex
  159. // will certainly be a valid index
  160. if ($this->input[$this->inputIndex] === $this->a) {
  161. // This is "+ +" or "- -". Don't delete the space.
  162. $command = self::ACTION_KEEP_A;
  163. }
  164. }
  165. switch ($command) {
  166. case self::ACTION_KEEP_A: // 1
  167. $this->output .= $this->a;
  168. if ($this->keptComment) {
  169. $this->output = rtrim($this->output, "\n");
  170. $this->output .= $this->keptComment;
  171. $this->keptComment = '';
  172. }
  173. $this->lastByteOut = $this->a;
  174. // fallthrough intentional
  175. case self::ACTION_DELETE_A: // 2
  176. $this->a = $this->b;
  177. if ($this->a === "'" || $this->a === '"') { // string literal
  178. $str = $this->a; // in case needed for exception
  179. for(;;) {
  180. $this->output .= $this->a;
  181. $this->lastByteOut = $this->a;
  182. $this->a = $this->get();
  183. if ($this->a === $this->b) { // end quote
  184. break;
  185. }
  186. if ($this->isEOF($this->a)) {
  187. throw new JSMin_UnterminatedStringException(
  188. "JSMin: Unterminated String at byte {$this->inputIndex}: {$str}");
  189. }
  190. $str .= $this->a;
  191. if ($this->a === '\\') {
  192. $this->output .= $this->a;
  193. $this->lastByteOut = $this->a;
  194. $this->a = $this->get();
  195. $str .= $this->a;
  196. }
  197. }
  198. }
  199. // fallthrough intentional
  200. case self::ACTION_DELETE_A_B: // 3
  201. $this->b = $this->next();
  202. if ($this->b === '/' && $this->isRegexpLiteral()) {
  203. $this->output .= $this->a . $this->b;
  204. $pattern = '/'; // keep entire pattern in case we need to report it in the exception
  205. for(;;) {
  206. $this->a = $this->get();
  207. $pattern .= $this->a;
  208. if ($this->a === '[') {
  209. for(;;) {
  210. $this->output .= $this->a;
  211. $this->a = $this->get();
  212. $pattern .= $this->a;
  213. if ($this->a === ']') {
  214. break;
  215. }
  216. if ($this->a === '\\') {
  217. $this->output .= $this->a;
  218. $this->a = $this->get();
  219. $pattern .= $this->a;
  220. }
  221. if ($this->isEOF($this->a)) {
  222. throw new JSMin_UnterminatedRegExpException(
  223. "JSMin: Unterminated set in RegExp at byte "
  224. . $this->inputIndex .": {$pattern}");
  225. }
  226. }
  227. }
  228. if ($this->a === '/') { // end pattern
  229. break; // while (true)
  230. } elseif ($this->a === '\\') {
  231. $this->output .= $this->a;
  232. $this->a = $this->get();
  233. $pattern .= $this->a;
  234. } elseif ($this->isEOF($this->a)) {
  235. throw new JSMin_UnterminatedRegExpException(
  236. "JSMin: Unterminated RegExp at byte {$this->inputIndex}: {$pattern}");
  237. }
  238. $this->output .= $this->a;
  239. $this->lastByteOut = $this->a;
  240. }
  241. $this->b = $this->next();
  242. }
  243. // end case ACTION_DELETE_A_B
  244. }
  245. }
  246. /**
  247. * @return bool
  248. */
  249. protected function isRegexpLiteral()
  250. {
  251. if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) {
  252. // we obviously aren't dividing
  253. return true;
  254. }
  255. if ($this->a === ' ' || $this->a === "\n") {
  256. $length = strlen($this->output);
  257. if ($length < 2) { // weird edge case
  258. return true;
  259. }
  260. // you can't divide a keyword
  261. if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) {
  262. if ($this->output === $m[0]) { // odd but could happen
  263. return true;
  264. }
  265. // make sure it's a keyword, not end of an identifier
  266. $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
  267. if (! $this->isAlphaNum($charBeforeKeyword)) {
  268. return true;
  269. }
  270. }
  271. }
  272. return false;
  273. }
  274. /**
  275. * Return the next character from stdin. Watch out for lookahead. If the character is a control character,
  276. * translate it to a space or linefeed.
  277. *
  278. * @return string
  279. */
  280. protected function get()
  281. {
  282. $c = $this->lookAhead;
  283. $this->lookAhead = null;
  284. if ($c === null) {
  285. // getc(stdin)
  286. if ($this->inputIndex < $this->inputLength) {
  287. $c = $this->input[$this->inputIndex];
  288. $this->inputIndex += 1;
  289. } else {
  290. $c = null;
  291. }
  292. }
  293. if (ord($c) >= self::ORD_SPACE || $c === "\n" || $c === null) {
  294. return $c;
  295. }
  296. if ($c === "\r") {
  297. return "\n";
  298. }
  299. return ' ';
  300. }
  301. /**
  302. * Does $a indicate end of input?
  303. *
  304. * @param string $a
  305. * @return bool
  306. */
  307. protected function isEOF($a)
  308. {
  309. return ord($a) <= self::ORD_LF;
  310. }
  311. /**
  312. * Get next char (without getting it). If is ctrl character, translate to a space or newline.
  313. *
  314. * @return string
  315. */
  316. protected function peek()
  317. {
  318. $this->lookAhead = $this->get();
  319. return $this->lookAhead;
  320. }
  321. /**
  322. * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character.
  323. *
  324. * @param string $c
  325. *
  326. * @return bool
  327. */
  328. protected function isAlphaNum($c)
  329. {
  330. return (preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126);
  331. }
  332. /**
  333. * Consume a single line comment from input (possibly retaining it)
  334. */
  335. protected function consumeSingleLineComment()
  336. {
  337. $comment = '';
  338. while (true) {
  339. $get = $this->get();
  340. $comment .= $get;
  341. if (ord($get) <= self::ORD_LF) { // end of line reached
  342. // if IE conditional comment
  343. if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
  344. $this->keptComment .= "/{$comment}";
  345. }
  346. return;
  347. }
  348. }
  349. }
  350. /**
  351. * Consume a multiple line comment from input (possibly retaining it)
  352. *
  353. * @throws JSMin_UnterminatedCommentException
  354. */
  355. protected function consumeMultipleLineComment()
  356. {
  357. $this->get();
  358. $comment = '';
  359. for(;;) {
  360. $get = $this->get();
  361. if ($get === '*') {
  362. if ($this->peek() === '/') { // end of comment reached
  363. $this->get();
  364. if (0 === strpos($comment, '!')) {
  365. // preserved by YUI Compressor
  366. if (!$this->keptComment) {
  367. // don't prepend a newline if two comments right after one another
  368. $this->keptComment = "\n";
  369. }
  370. $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n";
  371. } else if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
  372. // IE conditional
  373. $this->keptComment .= "/*{$comment}*/";
  374. }
  375. return;
  376. }
  377. } elseif ($get === null) {
  378. throw new JSMin_UnterminatedCommentException(
  379. "JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}");
  380. }
  381. $comment .= $get;
  382. }
  383. }
  384. /**
  385. * Get the next character, skipping over comments. Some comments may be preserved.
  386. *
  387. * @return string
  388. */
  389. protected function next()
  390. {
  391. $get = $this->get();
  392. if ($get === '/') {
  393. switch ($this->peek()) {
  394. case '/':
  395. $this->consumeSingleLineComment();
  396. $get = "\n";
  397. break;
  398. case '*':
  399. $this->consumeMultipleLineComment();
  400. $get = ' ';
  401. break;
  402. }
  403. }
  404. return $get;
  405. }
  406. }
  407. class JSMin_UnterminatedStringException extends Exception {}
  408. class JSMin_UnterminatedCommentException extends Exception {}
  409. class JSMin_UnterminatedRegExpException extends Exception {}