PageRenderTime 56ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/jstools/jsmin.php

https://github.com/oat-sa/tao-core
PHP | 845 lines | 476 code | 157 blank | 212 comment | 49 complexity | 7be17f85f3131b1197942c6611483cad MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0, MIT, LGPL-2.1
  1. <?php
  2. /**
  3. * JSMin.php (for PHP 5)
  4. *
  5. * PHP adaptation of JSMin, published by Douglas Crockford as jsmin.c, also based
  6. * on its Java translation by John Reilly.
  7. *
  8. * Permission is hereby granted to use the PHP version under the same conditions
  9. * as jsmin.c, which has the following notice :
  10. *
  11. * ----------------------------------------------------------------------------
  12. *
  13. * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
  14. *
  15. * Permission is hereby granted, free of charge, to any person obtaining a copy of
  16. * this software and associated documentation files (the "Software"), to deal in
  17. * the Software without restriction, including without limitation the rights to
  18. * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  19. * of the Software, and to permit persons to whom the Software is furnished to do
  20. * so, subject to the following conditions:
  21. *
  22. * The above copyright notice and this permission notice shall be included in all
  23. * copies or substantial portions of the Software.
  24. *
  25. * The Software shall be used for Good, not Evil.
  26. *
  27. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  28. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  29. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  30. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  31. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  32. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  33. * SOFTWARE.
  34. *
  35. * ----------------------------------------------------------------------------
  36. *
  37. * @copyright No new copyright ; please keep above and following information.
  38. * @author David Holmes <dholmes@cfdsoftware.net> of CFD Labs, France
  39. * @version 0.1 (PHP translation) 2006-04-11
  40. *
  41. * Please note, this is a brutal and simple conversion : it could undoubtedly be
  42. * improved, as a PHP implementation, by applying more PHP-specific programming
  43. * features.
  44. *
  45. * PHP 5 is required because OO style of programming is used, as well as classes
  46. * from the Standard PHP Library (SPL).
  47. *
  48. * Note : whereas jsmin.c works specifically with the standard input and output
  49. * streams, this implementation only falls back on them if file pathnames are
  50. * not provided to the JSMin() constructor.
  51. *
  52. * Examples comparing with the application compiled from jsmin.c :
  53. *
  54. * jsmin < orig.js > mini.js JSMin.php orig.js mini.js
  55. * JSMin.php orig.js > mini.js
  56. * JSMin.php - mini.js < orig.js
  57. * jsmin < orig.js JSMin.php orig.js
  58. * JSMin.php orig.js -
  59. * jsmin > mini.js JSMin.php - mini.js
  60. * JSMin.php > mini.js
  61. * jsmin comm1 comm2 < a.js > b.js JSMin.php a.js b.js comm1 comm2
  62. * JSMin.php a.js b.js -c comm1 comm2
  63. * JSMin.php a.js --comm comm1 comm2 > b.js
  64. * JSMin.php -c comm1 comm2 < a.js > b.js
  65. * (etc...)
  66. *
  67. * See JSMin.php -h (or --help) for command-line documentation.
  68. */
  69. /**
  70. * Version of this PHP translation.
  71. */
  72. define('VERSION', '0.2');
  73. /**
  74. * How fgetc() reports an End Of File.
  75. * N.B. : use === and not == to test the result of fgetc() ! (see manual)
  76. */
  77. define('EOF', FALSE);
  78. /**
  79. * Some ASCII character ordinals.
  80. * N.B. : PHP identifiers are case-insensitive !
  81. */
  82. define('ORD_NL', ord("\n"));
  83. define('ORD_space', ord(' '));
  84. define('ORD_cA', ord('A'));
  85. define('ORD_cZ', ord('Z'));
  86. define('ORD_a', ord('a'));
  87. define('ORD_z', ord('z'));
  88. define('ORD_0', ord('0'));
  89. define('ORD_9', ord('9'));
  90. /**
  91. * Generic exception class related to JSMin.
  92. */
  93. class JSMinException extends Exception {
  94. }
  95. /**
  96. * A JSMin exception indicating that a file provided for input or output could not be properly opened.
  97. */
  98. class FileOpenFailedJSMinException extends JSMinException {
  99. }
  100. /**
  101. * A JSMin exception indicating that an unterminated comment was encountered in input.
  102. */
  103. class UnterminatedCommentJSMinException extends JSMinException {
  104. }
  105. /**
  106. * A JSMin exception indicatig that an unterminated string literal was encountered in input.
  107. */
  108. class UnterminatedStringLiteralJSMinException extends JSMinException {
  109. }
  110. /**
  111. * A JSMin exception indicatig that an unterminated regular expression lieteral was encountered in input.
  112. */
  113. class UnterminatedRegExpLiteralJSMinException extends JSMinException {
  114. }
  115. /**
  116. * Main JSMin application class.
  117. *
  118. * Example of use :
  119. *
  120. * $jsMin = new JSMin(...input..., ...output...);
  121. * $jsMin -> minify();
  122. *
  123. * Do not specify input and/or output (or default to '-') to use stdin and/or stdout.
  124. */
  125. class JSMin {
  126. /**
  127. * Constant describing an {@link action()} : Output A. Copy B to A. Get the next B.
  128. */
  129. const ACT_FULL = 1;
  130. /**
  131. * Constant describing an {@link action()} : Copy B to A. Get the next B. (Delete A).
  132. */
  133. const ACT_BUF = 2;
  134. /**
  135. * Constant describing an {@link action()} : Get the next B. (Delete B).
  136. */
  137. const ACT_IMM = 3;
  138. /**
  139. * The input stream, from which to read a JS file to minimize. Obtained by fopen().
  140. * @var SplFileObject
  141. */
  142. private $in;
  143. /**
  144. * The output stream, in which to write the minimized JS file. Obtained by fopen().
  145. * @var SplFileObject
  146. */
  147. private $out;
  148. /**
  149. * Temporary I/O character (A).
  150. * @var string
  151. */
  152. private $theA;
  153. /**
  154. * Temporary I/O character (B).
  155. * @var string
  156. */
  157. private $theB;
  158. /**
  159. * Indicates whether a character is alphanumeric or _, $, \ or non-ASCII.
  160. *
  161. * @param string $c The single character to test.
  162. * @return boolean Whether the char is a letter, digit, underscore, dollar, backslash, or non-ASCII.
  163. */
  164. private static function isAlphaNum($c) {
  165. // Get ASCII value of character for C-like comparisons
  166. $a = ord($c);
  167. // Compare using defined character ordinals, or between PHP strings
  168. // Note : === is micro-faster than == when types are known to be the same
  169. return
  170. ($a >= ORD_a && $a <= ORD_z) ||
  171. ($a >= ORD_0 && $a <= ORD_9) ||
  172. ($a >= ORD_cA && $a <= ORD_cZ) ||
  173. $c === '_' || $c === '$' || $c === '\\' || $a > 126
  174. ;
  175. }
  176. /**
  177. * Get the next character from the input stream.
  178. *
  179. * If said character is a control character, translate it to a space or linefeed.
  180. *
  181. * @return string The next character from the specified input stream.
  182. * @see $in
  183. * @see peek()
  184. */
  185. private function get() {
  186. // Get next input character and advance position in file
  187. $c = $this -> in -> fgetc();
  188. // Test for non-problematic characters
  189. if ($c === "\n" || $c === EOF || ord($c) >= ORD_space) {
  190. return $c;
  191. }
  192. // else
  193. // Make linefeeds into newlines
  194. if ($c === "\r") {
  195. return "\n";
  196. }
  197. // else
  198. // Consider space
  199. return ' ';
  200. }
  201. /**
  202. * Get the next character from the input stream, without gettng it.
  203. *
  204. * @return string The next character from the specified input stream, without advancing the position
  205. * in the underlying file.
  206. * @see $in
  207. * @see get()
  208. */
  209. private function peek() {
  210. // Get next input character
  211. $c = $this -> in -> fgetc();
  212. // Regress position in file
  213. $this -> in -> fseek(-1, SEEK_CUR);
  214. // Return character obtained
  215. return $c;
  216. }
  217. /**
  218. * Get the next character from the input stream, excluding comments.
  219. *
  220. * {@link peek()} is used to see if a '/' is followed by a '*' or '/'.
  221. * Multiline comments are actually returned as a single space.
  222. *
  223. * @return string The next character from the specified input stream, skipping comments.
  224. * @see $in
  225. */
  226. private function next() {
  227. // Get next char from input, translated if necessary
  228. $c = $this -> get();
  229. // Check comment possibility
  230. if ($c == '/') {
  231. // Look ahead : a comment is two slashes or slashes followed by asterisk (to be closed)
  232. switch ($this -> peek()) {
  233. case '/' :
  234. // Comment is up to the end of the line
  235. // TOTEST : simple $this -> in -> fgets()
  236. while (true) {
  237. $c = $this -> get();
  238. if (ord($c) <= ORD_NL) {
  239. return $c;
  240. }
  241. }
  242. case '*' :
  243. // Comment is up to comment close.
  244. // Might not be terminated, if we hit the end of file.
  245. while (true) {
  246. // N.B. not using switch() because of having to test EOF with ===
  247. $c = $this -> get();
  248. if ($c == '*') {
  249. // Comment termination if the char ahead is a slash
  250. if ($this -> peek() == '/') {
  251. // Advance again and make into a single space
  252. $this -> get();
  253. return ' ';
  254. }
  255. }
  256. else if ($c === EOF) {
  257. // Whoopsie
  258. throw new UnterminatedCommentJSMinException();
  259. }
  260. }
  261. default :
  262. // Not a comment after all
  263. return $c;
  264. }
  265. }
  266. // No risk of a comment
  267. return $c;
  268. }
  269. /**
  270. * Do something !
  271. *
  272. * The action to perform is determined by the argument :
  273. *
  274. * JSMin :: ACT_FULL : Output A. Copy B to A. Get the next B.
  275. * JSMin :: ACT_BUF : Copy B to A. Get the next B. (Delete A).
  276. * JSMin :: ACT_IMM : Get the next B. (Delete B).
  277. *
  278. * A string is treated as a single character. Also, regular expressions are recognized if preceded
  279. * by '(', ',' or '='.
  280. *
  281. * @param int $action The action to perform : one of the JSMin :: ACT_* constants.
  282. */
  283. private function action($action) {
  284. // Choice of possible actions
  285. // Note the frequent fallthroughs : the actions are decrementally "long"
  286. switch ($action) {
  287. case self :: ACT_FULL :
  288. // Write A to output, then fall through
  289. $this -> out -> fwrite($this -> theA);
  290. case self :: ACT_BUF : // N.B. possible fallthrough from above
  291. // Copy B to A
  292. $tmpA = $this -> theA = $this -> theB;
  293. // Treating a string as a single char : outputting it whole
  294. // Note that the string-opening char (" or ') is memorized in B
  295. if ($tmpA == '\'' || $tmpA == '"') {
  296. while (true) {
  297. // Output string contents
  298. $this -> out -> fwrite($tmpA);
  299. // Get next character, watching out for termination of the current string,
  300. // new line & co (then the string is not terminated !), or a backslash
  301. // (upon which the following char is directly output to serve the escape mechanism)
  302. $tmpA = $this -> theA = $this -> get();
  303. if ($tmpA == $this -> theB) {
  304. // String terminated
  305. break; // from while(true)
  306. }
  307. // else
  308. if (ord($tmpA) <= ORD_NL) {
  309. // Whoopsie
  310. throw new UnterminatedStringLiteralJSMinException();
  311. }
  312. // else
  313. if ($tmpA == '\\') {
  314. // Escape next char immediately
  315. $this -> out -> fwrite($tmpA);
  316. $tmpA = $this -> theA = $this -> get();
  317. }
  318. }
  319. }
  320. case self :: ACT_IMM : // N.B. possible fallthrough from above
  321. // Get the next B
  322. $this -> theB = $this -> next();
  323. // Special case of recognising regular expressions (beginning with /) that are
  324. // preceded by '(', ',' or '='
  325. $tmpA = $this -> theA;
  326. if ($this -> theB == '/' &&
  327. ($tmpA == '(' || $tmpA == ',' || $tmpA == '=' ||
  328. $tmpA == '[' || $tmpA == '!' || $tmpA == ':' ||
  329. $tmpA == '&' || $tmpA == '|' || $tmpA == '?' ||
  330. $tmpA == '{' || $tmpA == '}' || $tmpA == ';' ||
  331. $tmpA == '\n')) {
  332. // Output the two successive chars
  333. $this -> out -> fwrite($tmpA);
  334. $this -> out -> fwrite($this -> theB);
  335. // Look for the end of the RE literal, watching out for escaped chars or a control /
  336. // end of line char (the RE literal then being unterminated !)
  337. while (true) {
  338. $tmpA = $this -> theA = $this -> get();
  339. if ($tmpA == '/') {
  340. // RE literal terminated
  341. break; // from while(true)
  342. }
  343. // else
  344. if ($tmpA == '\\') {
  345. // Escape next char immediately
  346. $this -> out -> fwrite($tmpA);
  347. $tmpA = $this -> theA = $this -> get();
  348. }
  349. else if (ord($tmpA) <= ORD_NL) {
  350. // Whoopsie
  351. throw new UnterminatedRegExpLiteralJSMinException();
  352. }
  353. // Output RE characters
  354. $this -> out -> fwrite($tmpA);
  355. }
  356. // Move forward after the RE literal
  357. $this -> theB = $this -> next();
  358. }
  359. break;
  360. default : throw new JSMinException('Expected a JSMin :: ACT_* constant in action().');
  361. }
  362. }
  363. /**
  364. * Run the JSMin application : minify some JS code.
  365. *
  366. * The code is read from the input stream, and its minified version is written to the output one.
  367. * That is : characters which are insignificant to JavaScript are removed, as well as comments ;
  368. * tabs are replaced with spaces ; carriage returns are replaced with linefeeds, and finally most
  369. * spaces and linefeeds are deleted.
  370. *
  371. * Note : name was changed from jsmin() because PHP identifiers are case-insensitive, and it is already
  372. * the name of this class.
  373. *
  374. * @see __construct()
  375. */
  376. public function minify() {
  377. // Initialize A and run the first (minimal) action
  378. $this -> theA = "\n";
  379. $this -> action(self :: ACT_IMM);
  380. // Proceed all the way to the end of the input file
  381. while ($this -> theA !== EOF) {
  382. switch ($this -> theA) {
  383. case ' ' :
  384. if (self :: isAlphaNum($this -> theB)) {
  385. $this -> action(self :: ACT_FULL);
  386. }
  387. else {
  388. $this -> action(self :: ACT_BUF);
  389. }
  390. break;
  391. case "\n" :
  392. switch ($this -> theB) {
  393. case '{' : case '[' : case '(' :
  394. case '+' : case '-' :
  395. $this -> action(self :: ACT_FULL);
  396. break;
  397. case ' ' :
  398. $this -> action(self :: ACT_IMM);
  399. break;
  400. default :
  401. if (self :: isAlphaNum($this -> theB)) {
  402. $this -> action(self :: ACT_FULL);
  403. }
  404. else {
  405. $this -> action(self :: ACT_BUF);
  406. }
  407. break;
  408. }
  409. break;
  410. default :
  411. switch ($this -> theB) {
  412. case ' ' :
  413. if (self :: isAlphaNum($this -> theA)) {
  414. $this -> action(self :: ACT_FULL);
  415. break;
  416. }
  417. // else
  418. $this -> action(self :: ACT_IMM);
  419. break;
  420. case "\n" :
  421. switch ($this -> theA) {
  422. case '}' : case ']' : case ')' : case '+' :
  423. case '-' : case '"' : case '\'' :
  424. $this -> action(self :: ACT_FULL);
  425. break;
  426. default :
  427. if (self :: isAlphaNum($this -> theA)) {
  428. $this -> action(self :: ACT_FULL);
  429. }
  430. else {
  431. $this -> action(self :: ACT_IMM);
  432. }
  433. break;
  434. }
  435. break;
  436. default :
  437. $this -> action(self :: ACT_FULL);
  438. break;
  439. }
  440. break;
  441. }
  442. }
  443. }
  444. /**
  445. * Prepare a new JSMin application.
  446. *
  447. * The next step is to {@link minify()} the input into the output.
  448. *
  449. * @param string $inFileName The pathname of the input (unminified JS) file. STDIN if '-' or absent.
  450. * @param string $outFileName The pathname of the output (minified JS) file. STDOUT if '-' or absent.
  451. * @param array $comments Optional lines to present as comments at the beginning of the output.
  452. * @throws FileOpenFailedJSMinException If the input and/or output file pathname is not provided, and
  453. * respectively STDIN and/or STDOUT are not available (ie, script is not being used in CLI).
  454. */
  455. public function __construct($inFileName = '-', $outFileName = '-', $comments = NULL, $truncateFile=true) {
  456. // Recuperate input and output streams.
  457. // Use STDIN and STDOUT by default, if they are defined (CLI mode) and no file names are provided
  458. if ($inFileName == '-') $inFileName = 'php://stdin';
  459. if ($outFileName == '-') $outFileName = 'php://stdout';
  460. try {
  461. $this -> in = new SplFileObject($inFileName, 'rb', TRUE);
  462. }
  463. catch (Exception $e) {
  464. throw new FileOpenFailedJSMinException(
  465. 'Failed to open "'.$inFileName.'" for reading only.'
  466. );
  467. }
  468. try {
  469. $openMode = $truncateFile ? "wb" : "ab";
  470. $this -> out = new SplFileObject($outFileName, $openMode, TRUE);
  471. }
  472. catch (Exception $e) {
  473. throw new FileOpenFailedJSMinException(
  474. 'Failed to open "'.$outFileName.'" for writing only.'
  475. );
  476. }
  477. // Present possible initial comments
  478. if ($comments !== NULL) {
  479. foreach ($comments as $comm) {
  480. $this -> out -> fwrite('// '.$comm."\n");
  481. }
  482. }
  483. }
  484. }
  485. //
  486. // OTHER FUNCTIONS
  487. //
  488. /**
  489. * Displays inline help for the application.
  490. */
  491. function printHelp() {
  492. // All the inline help
  493. echo "\n";
  494. echo "Usage : JSMin.php [inputFile] [outputFile] [[-c] comm1 comm2 ...]\n";
  495. echo " JSMin.php [-v|-h]\n";
  496. echo "\n";
  497. echo "Minify JavaScript code using JSMin, the JavaScript Minifier.\n";
  498. echo "\n";
  499. echo "JSMin is a filter which removes comments and unnecessary whitespace\n";
  500. echo "from a script read in the inputFile (stdin by default), as well as\n";
  501. echo "omitting or modifying some characters, before writing the results to\n";
  502. echo "the outputFile (stdout by default).\n";
  503. echo "It does not change the behaviour of the program that is minifies.\n";
  504. echo "The result may be harder to debug. It will definitely be harder to\n";
  505. echo "read. It typically reduces filesize by half, resulting in faster\n";
  506. echo "downloads. It also encourages a more expressive programming style\n";
  507. echo "because it eliminates the download cost of clean, literate self-\n";
  508. echo "documentation.\n";
  509. echo "\n";
  510. echo "The '-' character can be used to explicitely specify a standard\n";
  511. echo "stream for input or output.\n";
  512. echo "\n";
  513. echo "With the optional -c (--comm) option, all following parameters will\n";
  514. echo "be listed at the beginning of the output as comments. This is a\n";
  515. echo "convenient way of replacing copyright messages and other info. The\n";
  516. echo "option is unnecessary if an input and output file are specified :\n";
  517. echo "following parameters will then automatically be treated thus.\n";
  518. echo "\n";
  519. echo "Options :\n";
  520. echo "\n";
  521. echo " -c, --comm Present following parameters as initial comments.\n";
  522. echo " -h, --help Display this information.\n";
  523. echo " -v, --version Display short version information.\n";
  524. echo "\n";
  525. echo "The JavaScript Minifier is copyright (c) 2002 by Douglas Crockford\n";
  526. echo "and available online at http://javascript.crockford.com/jsmin.html.\n";
  527. echo "This PHP translation is by David Holmes of CFD Labs, France, 2006.\n";
  528. echo "\n";
  529. }
  530. /**
  531. * Displays version information for the application.
  532. */
  533. function printVersion() {
  534. // Minimum info
  535. echo "JSMin, the JavaScript Minifier, copyright (c) 2002 by Douglas Crockford.\n";
  536. echo "PHP translation (for PHP 5) version ".VERSION." by David Holmes, CFD Labs.\n";
  537. }
  538. //
  539. // ENTRY POINT
  540. //
  541. define('EXIT_SUCCESS', 0);
  542. define('EXIT_FAILURE', 1);
  543. // Examine command-line parameters
  544. // First shift off the first parameter, the executable's name
  545. // hack to ensure that the script is running from command line.
  546. if (!empty($argc) && strstr($argv[0], basename(__FILE__)))
  547. {
  548. echo $argc;
  549. echo $argv;
  550. array_shift($argv);
  551. $inFileName = NULL;
  552. $outFileName = NULL;
  553. $haveCommentParams = FALSE;
  554. $comments = array();
  555. foreach ($argv as $arg) {
  556. // Bypass the rest if we are now considering initial comments
  557. if ($haveCommentParams) {
  558. $comments[] = $arg;
  559. continue;
  560. }
  561. // else
  562. // Look for an option (length > 1 because of '-' for indicating stdin or
  563. // stdout)
  564. if ($arg[0] == '-' && strlen($arg) > 1) {
  565. switch ($arg) {
  566. case '-c' : case '--comm' :
  567. // Following parameters will be initial comments
  568. $haveCommentParams = TRUE;
  569. break;
  570. case '-h' : case '--help' :
  571. // Display inline help and exit normally
  572. printHelp();
  573. exit(EXIT_SUCCESS);
  574. case '-v' : case '--version' :
  575. // Display short version information and exit normally
  576. printVersion();
  577. exit(EXIT_SUCCESS);
  578. default :
  579. // Reject any other (unknown) option
  580. echo "\n";
  581. echo "ERROR : unknown option \"$arg\", sorry.\n";
  582. printHelp();
  583. exit(EXIT_FAILURE);
  584. }
  585. continue;
  586. }
  587. // else
  588. // At this point, parameter is neither to be considered as an initial
  589. // comment, nor is it an option. It is an input or output file.
  590. if ($inFileName === NULL) {
  591. // No input file yet, this is it
  592. $inFileName = $arg;
  593. }
  594. else if ($outFileName === NULL) {
  595. // An input file but no output file yet, this is it
  596. $outFileName = $arg;
  597. }
  598. else {
  599. // Already have input and output file, this is a first initial comment
  600. $haveCommentParams = TRUE;
  601. $comments[] = $arg;
  602. }
  603. }
  604. if ($inFileName === NULL) $inFileName = '-';
  605. if ($outFileName === NULL) $outFileName = '-';
  606. // Prepare and run the JSMin application
  607. // If pathnames are not provided or '-', standard input/output streams are used
  608. $jsMin = new JSMin($inFileName, $outFileName, $comments);
  609. $jsMin -> minify();
  610. }
  611. ?>