Completed
Push — master ( 9a01d9...c990d6 )
by frank
08:32
created

classes/external/php/minify-2.3.1-jsmin.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 <[email protected]> (PHP port)
49
 * @author Steve Clay <[email protected]> (modifications + cleanup)
50
 * @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp)
51
 * @copyright 2002 Douglas Crockford <[email protected]> (jsmin.c)
52
 * @copyright 2008 Ryan Grove <[email protected]> (PHP port)
53
 * @license http://opensource.org/licenses/mit-license.php MIT License
54
 * @link http://code.google.com/p/jsmin-php/
55
 */
56
57
class JSMin {
58
    const ORD_LF            = 10;
59
    const ORD_SPACE         = 32;
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 View Code Duplication
        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 View Code Duplication
        while ($this->a !== null) {
122
            // determine next command
123
            $command = self::ACTION_KEEP_A; // default
124
            if ($this->a === ' ') {
125
                if (($this->lastByteOut === '+' || $this->lastByteOut === '-')
0 ignored issues
show
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
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->a === "\n") {
133
                if ($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->b === ' '
145
                    || ($this->b === "\n"
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|JSMin_UnterminatedStringException
167
     */
168
    protected function action($command)
169
    {
170
        // make sure we don't compress "a + ++b" to "a+++b", etc.
171 View Code Duplication
        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 View Code Duplication
            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
                        $this->a = $this->get();
204
                        if ($this->a === $this->b) { // end quote
205
                            break;
206
                        }
207
                        if ($delimiter === '`' && $this->a === "\n") {
0 ignored issues
show
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
208
                            // leave the newline
209
                        } elseif ($this->isEOF($this->a)) {
210
                            $byte = $this->inputIndex - 1;
211
                            throw new JSMin_UnterminatedStringException(
212
                                "JSMin: Unterminated String at byte {$byte}: {$str}");
213
                        }
214
                        $str .= $this->a;
215 View Code Duplication
                        if ($this->a === '\\') {
216
                            $this->output .= $this->a;
217
                            $this->lastByteOut = $this->a;
218
                            $this->a = $this->get();
219
                            $str .= $this->a;
220
                        }
221
                    }
222
                }
223
224
                // fallthrough intentional
225 View Code Duplication
            case self::ACTION_DELETE_A_B: // 3
226
                $this->b = $this->next();
227
                if ($this->b === '/' && $this->isRegexpLiteral()) {
228
                    $this->output .= $this->a . $this->b;
229
                    $pattern = '/'; // keep entire pattern in case we need to report it in the exception
230
                    for(;;) {
231
                        $this->a = $this->get();
232
                        $pattern .= $this->a;
233
                        if ($this->a === '[') {
234
                            for(;;) {
235
                                $this->output .= $this->a;
236
                                $this->a = $this->get();
237
                                $pattern .= $this->a;
238
                                if ($this->a === ']') {
239
                                    break;
240
                                }
241
                                if ($this->a === '\\') {
242
                                    $this->output .= $this->a;
243
                                    $this->a = $this->get();
244
                                    $pattern .= $this->a;
245
                                }
246
                                if ($this->isEOF($this->a)) {
247
                                    throw new JSMin_UnterminatedRegExpException(
248
                                        "JSMin: Unterminated set in RegExp at byte "
249
                                            . $this->inputIndex .": {$pattern}");
250
                                }
251
                            }
252
                        }
253
254
                        if ($this->a === '/') { // end pattern
255
                            break; // while (true)
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
256
                        } elseif ($this->a === '\\') {
257
                            $this->output .= $this->a;
258
                            $this->a = $this->get();
259
                            $pattern .= $this->a;
260
                        } elseif ($this->isEOF($this->a)) {
261
                            $byte = $this->inputIndex - 1;
262
                            throw new JSMin_UnterminatedRegExpException(
263
                                "JSMin: Unterminated RegExp at byte {$byte}: {$pattern}");
264
                        }
265
                        $this->output .= $this->a;
266
                        $this->lastByteOut = $this->a;
267
                    }
268
                    $this->b = $this->next();
269
                }
270
            // end case ACTION_DELETE_A_B
271
        }
272
    }
273
274
    /**
275
     * @return bool
276
     */
277
    protected function isRegexpLiteral()
278
    {
279
        if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) {
280
            // we can't divide after these tokens
281
            return true;
282
        }
283
284
        // check if first non-ws token is "/" (see starts-regex.js)
285
        $length = strlen($this->output);
286
        if ($this->a === ' ' || $this->a === "\n") {
287
            if ($length < 2) { // weird edge case
288
                return true;
289
            }
290
        }
291
292
        // if the "/" follows a keyword, it must be a regexp, otherwise it's best to assume division
293
294
        $subject = $this->output . trim($this->a);
295
        if (!preg_match('/(?:case|else|in|return|typeof)$/', $subject, $m)) {
296
            // not a keyword
297
            return false;
298
        }
299
300
        // can't be sure it's a keyword yet (see not-regexp.js)
301
        $charBeforeKeyword = substr($subject, 0 - strlen($m[0]) - 1, 1);
302
        if ($this->isAlphaNum($charBeforeKeyword)) {
303
            // this is really an identifier ending in a keyword, e.g. "xreturn"
304
            return false;
305
        }
306
307
        // it's a regexp. Remove unneeded whitespace after keyword
308
        if ($this->a === ' ' || $this->a === "\n") {
309
            $this->a = '';
310
        }
311
312
        return true;
313
    }
314
315
    /**
316
     * Return the next character from stdin. Watch out for lookahead. If the character is a control character,
317
     * translate it to a space or linefeed.
318
     *
319
     * @return string
320
     */
321 View Code Duplication
    protected function get()
322
    {
323
        $c = $this->lookAhead;
324
        $this->lookAhead = null;
325
        if ($c === null) {
326
            // getc(stdin)
327
            if ($this->inputIndex < $this->inputLength) {
328
                $c = $this->input[$this->inputIndex];
329
                $this->inputIndex += 1;
330
            } else {
331
                $c = null;
332
            }
333
        }
334
        if (ord($c) >= self::ORD_SPACE || $c === "\n" || $c === null) {
335
            return $c;
336
        }
337
        if ($c === "\r") {
338
            return "\n";
339
        }
340
        return ' ';
341
    }
342
343
    /**
344
     * Does $a indicate end of input?
345
     *
346
     * @param string $a
347
     * @return bool
348
     */
349
    protected function isEOF($a)
350
    {
351
        return ord($a) <= self::ORD_LF;
352
    }
353
354
    /**
355
     * Get next char (without getting it). If is ctrl character, translate to a space or newline.
356
     *
357
     * @return string
358
     */
359
    protected function peek()
360
    {
361
        $this->lookAhead = $this->get();
362
        return $this->lookAhead;
363
    }
364
365
    /**
366
     * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character.
367
     *
368
     * @param string $c
369
     *
370
     * @return bool
371
     */
372
    protected function isAlphaNum($c)
373
    {
374
        return (preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126);
375
    }
376
377
    /**
378
     * Consume a single line comment from input (possibly retaining it)
379
     */
380 View Code Duplication
    protected function consumeSingleLineComment()
381
    {
382
        $comment = '';
383
        while (true) {
384
            $get = $this->get();
385
            $comment .= $get;
386
            if (ord($get) <= self::ORD_LF) { // end of line reached
387
                // if IE conditional comment
388
                if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
389
                    $this->keptComment .= "/{$comment}";
390
                }
391
                return;
392
            }
393
        }
394
    }
395
396
    /**
397
     * Consume a multiple line comment from input (possibly retaining it)
398
     *
399
     * @throws JSMin_UnterminatedCommentException
400
     */
401 View Code Duplication
    protected function consumeMultipleLineComment()
402
    {
403
        $this->get();
404
        $comment = '';
405
        for(;;) {
406
            $get = $this->get();
407
            if ($get === '*') {
408
                if ($this->peek() === '/') { // end of comment reached
409
                    $this->get();
410
                    if (0 === strpos($comment, '!')) {
411
                        // preserved by YUI Compressor
412
                        if (!$this->keptComment) {
413
                            // don't prepend a newline if two comments right after one another
414
                            $this->keptComment = "\n";
415
                        }
416
                        $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n";
417
                    } else if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
418
                        // IE conditional
419
                        $this->keptComment .= "/*{$comment}*/";
420
                    }
421
                    return;
422
                }
423
            } elseif ($get === null) {
424
                throw new JSMin_UnterminatedCommentException(
425
                    "JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}");
426
            }
427
            $comment .= $get;
428
        }
429
    }
430
431
    /**
432
     * Get the next character, skipping over comments. Some comments may be preserved.
433
     *
434
     * @return string
435
     */
436 View Code Duplication
    protected function next()
437
    {
438
        $get = $this->get();
439
        if ($get === '/') {
440
            switch ($this->peek()) {
441
                case '/':
442
                    $this->consumeSingleLineComment();
443
                    $get = "\n";
444
                    break;
445
                case '*':
446
                    $this->consumeMultipleLineComment();
447
                    $get = ' ';
448
                    break;
449
            }
450
        }
451
        return $get;
452
    }
453
}
454
455
class JSMin_UnterminatedStringException extends Exception {}
456
class JSMin_UnterminatedCommentException extends Exception {}
457
class JSMin_UnterminatedRegExpException extends Exception {}
458