Completed
Push — master ( d14c1f...3b84d0 )
by Vitaly
03:05
created

JSMin   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 67
c 1
b 0
f 1
lcom 1
cbo 3
dl 0
loc 293
rs 5.7097

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A minify() 0 12 2
C min() 0 49 17
C action() 0 56 16
B get() 0 20 6
A next() 0 15 4
A peek() 0 5 1
A singleLineComment() 0 15 4
C multipleLineComment() 0 27 7
C isRegexpLiteral() 0 24 7
A isAlphaNum() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like JSMin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JSMin, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace samsonphp\minify;
3
4
use Exception;
5
6
/**
7
 * jsmin.php - extended PHP implementation of Douglas Crockford's JSMin.
8
 *
9
 * <code>
10
 * $minifiedJs = JSMin::minify($js);
11
 * </code>
12
 *
13
 * This is a direct port of jsmin.c to PHP with a few PHP performance tweaks and
14
 * modifications to preserve some comments (see below). Also, rather than using
15
 * stdin/stdout, JSMin::minify() accepts a string as input and returns another
16
 * string as output.
17
 * 
18
 * Comments containing IE conditional compilation are preserved, as are multi-line
19
 * comments that begin with "/*!" (for documentation purposes). In the latter case
20
 * newlines are inserted around the comment to enhance readability.
21
 *
22
 * PHP 5 or higher is required.
23
 *
24
 * Permission is hereby granted to use this version of the library under the
25
 * same terms as jsmin.c, which has the following license:
26
 *
27
 * --
28
 * Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
29
 *
30
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
31
 * this software and associated documentation files (the "Software"), to deal in
32
 * the Software without restriction, including without limitation the rights to
33
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
34
 * of the Software, and to permit persons to whom the Software is furnished to do
35
 * so, subject to the following conditions:
36
 *
37
 * The above copyright notice and this permission notice shall be included in all
38
 * copies or substantial portions of the Software.
39
 *
40
 * The Software shall be used for Good, not Evil.
41
 *
42
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
48
 * SOFTWARE.
49
 * --
50
 *
51
 * @package JSMin
52
 * @author Ryan Grove <[email protected]> (PHP port)
53
 * @author Steve Clay <[email protected]> (modifications + cleanup)
54
 * @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp)
55
 * @copyright 2002 Douglas Crockford <[email protected]> (jsmin.c)
56
 * @copyright 2008 Ryan Grove <[email protected]> (PHP port)
57
 * @license http://opensource.org/licenses/mit-license.php MIT License
58
 * @link http://code.google.com/p/jsmin-php/
59
 */
60
61
class JSMin {
62
    const ORD_LF            = 10;
63
    const ORD_SPACE         = 32;
64
    const ACTION_KEEP_A     = 1;
65
    const ACTION_DELETE_A   = 2;
66
    const ACTION_DELETE_A_B = 3;
67
    
68
    protected $a           = "\n";
69
    protected $b           = '';
70
    protected $input       = '';
71
    protected $inputIndex  = 0;
72
    protected $inputLength = 0;
73
    protected $lookAhead   = null;
74
    protected $output      = '';
75
76
    public function __construct($input)
77
    {
78
        $this->input = $input;
79
    }
80
81
    /*
82
     * Don't create a JSMin instance, instead use the static function minify,
83
     * which checks for mb_string function overloading and avoids errors
84
     * trying to re-minify the output of Closure Compiler
85
     *
86
     * @private
87
     */
88
89
    /**
90
     * Minify Javascript
91
     *
92
     * @param string $js Javascript to be minified
93
     *
94
     * @return string
95
     */
96
    public static function minify($js)
97
    {
98
        // look out for syntax like "++ +" and "- ++"
99
        $p = '\\+';
100
        $m = '\\-';
101
        if (preg_match("/([$p$m])(?:\\1 [$p$m]| (?:$p$p|$m$m))/", $js)) {
102
            // likely pre-minified and would be broken by JSMin
103
            return $js;
104
        }
105
        $jsmin = new JSMin($js);
106
        return $jsmin->min();
107
    }
108
    
109
    /**
110
     * Perform minification, return result
111
     */
112
    public function min()
113
    {
114
        if ($this->output !== '') { // min already run
115
            return $this->output;
116
        }
117
118
        $mbIntEnc = null;
119
        if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) {
120
            $mbIntEnc = mb_internal_encoding();
121
            mb_internal_encoding('8bit');
122
        }
123
        $this->input = str_replace("\r\n", "\n", $this->input);
124
        $this->inputLength = strlen($this->input);
125
126
        $this->action(self::ACTION_DELETE_A_B);
127
        
128
        while ($this->a !== null) {
129
            // determine next command
130
            $command = self::ACTION_KEEP_A; // default
131
            if ($this->a === ' ') {
132
                if (! $this->isAlphaNum($this->b)) {
133
                    $command = self::ACTION_DELETE_A;
134
                }
135
            } elseif ($this->a === "\n") {
136
                if ($this->b === ' ') {
137
                    $command = self::ACTION_DELETE_A_B;
138
                // in case of mbstring.func_overload & 2, must check for null b,
139
                // otherwise mb_strpos will give WARNING
140
                } elseif ($this->b === null
141
                          || (FALSE === strpos('{[(+-', $this->b)
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected false, but found FALSE.
Loading history...
142
                              && ! $this->isAlphaNum($this->b))) {
143
                    $command = self::ACTION_DELETE_A;
144
                }
145
            } elseif (! $this->isAlphaNum($this->a)) {
146
                if ($this->b === ' '
147
                    || ($this->b === "\n" 
148
                        && (FALSE === strpos('}])+-"\'', $this->a)))) {
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected false, but found FALSE.
Loading history...
149
                    $command = self::ACTION_DELETE_A_B;
150
                }
151
            }
152
            $this->action($command);
153
        }
154
        $this->output = trim($this->output);
155
156
        if ($mbIntEnc !== null) {
157
            mb_internal_encoding($mbIntEnc);
158
        }
159
        return $this->output;
160
    }
161
    
162
    /**
163
     * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
164
     * ACTION_DELETE_A = Copy B to A. Get the next B.
165
     * ACTION_DELETE_A_B = Get the next B.
166
     */
167
    protected function action($command)
168
    {
169
        switch ($command) {
170
            case self::ACTION_KEEP_A:
171
                $this->output .= $this->a;
172
                // fallthrough
173
            case self::ACTION_DELETE_A:
174
                $this->a = $this->b;
175
                if ($this->a === "'" || $this->a === '"') { // string literal
176
                    $str = $this->a; // in case needed for exception
177
                    while (TRUE) {
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
178
                        $this->output .= $this->a;
179
                        $this->a       = $this->get();
180
                        if ($this->a === $this->b) { // end quote
181
                            break;
182
                        }
183
                        if (ord($this->a) <= self::ORD_LF) {
184
                            throw new JSMin_UnterminatedStringException(
185
                                "JSMin: Unterminated String at byte "
186
                                . $this->inputIndex . ": {$str}");
187
                        }
188
                        $str .= $this->a;
189
                        if ($this->a === '\\') {
190
                            $this->output .= $this->a;
191
                            $this->a       = $this->get();
192
                            $str .= $this->a;
193
                        }
194
                    }
195
                }
196
                // fallthrough
197
            case self::ACTION_DELETE_A_B:
198
                $this->b = $this->next();
199
                if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal
200
                    $this->output .= $this->a . $this->b;
201
                    $pattern = '/'; // in case needed for exception
202
                    while (TRUE) {
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
203
                        $this->a = $this->get();
204
                        $pattern .= $this->a;
205
                        if ($this->a === '/') { // end pattern
206
                            break; // while (TRUE)
207
                        } elseif ($this->a === '\\') {
208
                            $this->output .= $this->a;
209
                            $this->a       = $this->get();
210
                            $pattern      .= $this->a;
211
                        } elseif (ord($this->a) <= self::ORD_LF) {
212
                            throw new JSMin_UnterminatedRegExpException(
213
                                "JSMin: Unterminated RegExp at byte "
214
                                . $this->inputIndex .": {$pattern}");
215
                        }
216
                        $this->output .= $this->a;
217
                    }
218
                    $this->b = $this->next();
219
                }
220
            // end case ACTION_DELETE_A_B
221
        }
222
    }
223
224
    /**
225
     * Get next char. Convert ctrl char to space.
226
     */
227
    protected function get()
228
    {
229
        $c = $this->lookAhead;
230
        $this->lookAhead = null;
231
        if ($c === null) {
232
            if ($this->inputIndex < $this->inputLength) {
233
                $c = $this->input[$this->inputIndex];
234
                $this->inputIndex += 1;
235
            } else {
236
                return null;
237
            }
238
        }
239
        if ($c === "\r" || $c === "\n") {
240
            return "\n";
241
        }
242
        if (ord($c) < self::ORD_SPACE) { // control char
243
            return ' ';
244
        }
245
        return $c;
246
    }
247
248
    /**
249
     * Get the next character, skipping over comments.
250
     * Some comments may be preserved.
251
     */
252
    protected function next()
253
    {
254
        $get = $this->get();
255
        if ($get !== '/') {
256
            return $get;
257
        }
258
        switch ($this->peek()) {
259
            case '/':
260
                return $this->singleLineComment();
261
            case '*':
262
                return $this->multipleLineComment();
263
            default:
264
                return $get;
265
        }
266
    }
267
    
268
    /**
269
     * Get next char. If is ctrl character, translate to a space or newline.
270
     */
271
    protected function peek()
272
    {
273
        $this->lookAhead = $this->get();
274
        return $this->lookAhead;
275
    }
276
277
    protected function singleLineComment()
278
    {
279
        $comment = '';
280
        while (TRUE) {
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
281
            $get = $this->get();
282
            $comment .= $get;
283
            if (ord($get) <= self::ORD_LF) { // EOL reached
284
                // if IE conditional comment
285
                if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
286
                    return "/{$comment}";
287
                }
288
                return $get;
289
            }
290
        }
291
    }
292
293
    protected function multipleLineComment()
294
    {
295
        $this->get();
296
        $comment = '';
297
        while (TRUE) {
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
298
            $get = $this->get();
299
            if ($get === '*') {
300
                if ($this->peek() === '/') { // end of comment reached
301
                    $this->get();
302
                    // if comment preserved by YUI Compressor
303
                    if (0 === strpos($comment, '!')) {
304
                        return "\n/*" . substr($comment, 1) . "*/\n";
305
                    }
306
                    // if IE conditional comment
307
                    if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
308
                        return "/*{$comment}*/";
309
                    }
310
                    return ' ';
311
                }
312
            } elseif ($get === null) {
313
                throw new JSMin_UnterminatedCommentException(
314
                    "JSMin: Unterminated comment at byte "
315
                    . $this->inputIndex . ": /*{$comment}");
316
            }
317
            $comment .= $get;
318
        }
319
    }
320
321
    protected function isRegexpLiteral()
322
    {
323
        if (FALSE !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected false, but found FALSE.
Loading history...
324
            return TRUE;
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
325
        }
326
        if (' ' === $this->a) {
327
            $length = strlen($this->output);
328
            if ($length < 2) { // weird edge case
329
                return TRUE;
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
330
            }
331
            // you can't divide a keyword
332
            if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) {
333
                if ($this->output === $m[0]) { // odd but could happen
334
                    return TRUE;
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
335
                }
336
                // make sure it's a keyword, not end of an identifier
337
                $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
338
                if (!$this->isAlphaNum($charBeforeKeyword)) {
339
                    return TRUE;
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected true, but found TRUE.
Loading history...
340
                }
341
            }
342
        }
343
        return FALSE;
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected false, but found FALSE.
Loading history...
344
    }
345
    
346
    /**
347
     * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII?
348
     */
349
    protected function isAlphaNum($c)
350
    {
351
        return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126);
352
    }
353
}
354
355
class JSMin_UnterminatedStringException extends Exception {}
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style introduced by
This class is not in CamelCase format.

Classes in PHP are usually named in CamelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. The whole name starts with a capital letter as well.

Thus the name database provider becomes DatabaseProvider.

Loading history...
356
class JSMin_UnterminatedCommentException extends Exception {}
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style introduced by
This class is not in CamelCase format.

Classes in PHP are usually named in CamelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. The whole name starts with a capital letter as well.

Thus the name database provider becomes DatabaseProvider.

Loading history...
357
class JSMin_UnterminatedRegExpException extends Exception {}
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style introduced by
This class is not in CamelCase format.

Classes in PHP are usually named in CamelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. The whole name starts with a capital letter as well.

Thus the name database provider becomes DatabaseProvider.

Loading history...
358