PluginProbe
Autoptimize / 3.1.16
Autoptimize v3.1.16
3.1.16 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 All 108 releases
autoptimize / classes / external / php / jsmin.php

jsmin.php in Autoptimize 3.1.16, at classes/external/php/jsmin.php

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