File Coverage

blib/lib/JavaScript/Minifier.pm
Criterion Covered Total %
statement 139 143 97.2
branch 73 82 89.0
condition 113 141 80.1
subroutine 20 20 100.0
pod 0 16 0.0
total 345 402 85.8


line stmt bran cond sub pod time code
1             package JavaScript::Minifier;
2              
3 3     3   36465 use strict;
  3         3  
  3         90  
4 3     3   10 use warnings;
  3         3  
  3         4021  
5              
6             our $VERSION = '1.14'; # VERSION
7              
8             require Exporter;
9             our @ISA = qw(Exporter);
10             our @EXPORT = qw(minify);
11              
12             #return true if the character is allowed in identifier.
13             sub isAlphanum {
14 432   66 432 0 2382 return ($_[0] =~ /[\w\$\\]/ || ord($_[0]) > 126);
15             }
16              
17             sub isEndspace {
18 427   66 427 0 2281 return ($_[0] eq "\n" || $_[0] eq "\r" || $_[0] eq "\f");
19             }
20              
21             sub isWhitespace {
22 2537   66 2537 0 20382 return ($_[0] eq ' ' || $_[0] eq "\t" || $_[0] eq "\n"
23             || $_[0] eq "\r" || $_[0] eq "\f");
24             }
25              
26             # New line characters before or after these characters can be removed.
27             # Not + - / in this list because they require special care.
28             sub isInfix {
29 58     58 0 243 $_[0] =~ /[,;:=&%*<>\?\|\n]/;
30             }
31              
32             # New line characters after these characters can be removed.
33             sub isPrefix {
34 31   66 31 0 88 return ($_[0] =~ /[\{\(\[!]/ || isInfix($_[0]));
35             }
36              
37             # New line characters before these characters can removed.
38             sub isPostfix {
39 41   66 41 0 140 return ($_[0] =~ /[\}\)\]]/ || isInfix($_[0]));
40             }
41              
42             # -----------------------------------------------------------------------------
43              
44             sub _get {
45 1427     1427   814 my $s = shift;
46              
47 1427 100       1690 if ($s->{inputType} eq 'file') {
    50          
48 1289         1826 my $char = getc($s->{input});
49 1289 100       2129 $s->{last_read_char} = $char
50             if defined $char;
51              
52 1289         4858 return $char;
53             }
54             elsif ($s->{inputType} eq 'string') {
55 138 100       157 if ($s->{'inputPos'} < length($s->{input})) {
56 118         356 return $s->{last_read_char}
57             = substr($s->{input}, $s->{inputPos}++, 1);
58             }
59             else { # Simulate getc() when off the end of the input string.
60 20         47 return undef;
61             }
62             }
63             else {
64 0         0 die "no input";
65             }
66             }
67              
68             sub _put {
69 743     743   448 my $s = shift;
70 743         650 my $x = shift;
71 743         749 my $outfile = ($s->{outfile});
72 743 100       837 if (defined($s->{outfile})) {
73 657         1191 print $outfile $x;
74             }
75             else {
76 86         98 $s->{output} .= $x;
77             }
78             }
79              
80             # -----------------------------------------------------------------------------
81              
82             # print a
83             # move b to a
84             # move c to b
85             # move d to c
86             # new d
87             #
88             # i.e. print a and advance
89             sub action1 {
90 575     575 0 446 my $s = shift;
91 575 100       613 if (!isWhitespace($s->{a})) {
92 495         575 $s->{lastnws} = $s->{a};
93             }
94 575         780 $s->{last} = $s->{a};
95 575         574 action2($s);
96             }
97              
98             # sneeky output $s->{a} for comments
99             sub action2 {
100 740     740 0 495 my $s = shift;
101 740         721 _put($s, $s->{a});
102 740         704 action3($s);
103             }
104              
105             # move b to a
106             # move c to b
107             # move d to c
108             # new d
109             #
110             # i.e. delete a
111             sub action3 {
112 1243     1243 0 802 my $s = shift;
113 1243         1222 $s->{a} = $s->{b};
114 1243         1150 action4($s);
115             }
116              
117             # move c to b
118             # move d to c
119             # new d
120             #
121             # i.e. delete b
122             sub action4 {
123 1342     1342 0 927 my $s = shift;
124 1342         1188 $s->{b} = $s->{c};
125 1342         1369 $s->{c} = $s->{d};
126 1342         1221 $s->{d} = _get($s);
127             }
128              
129             # -----------------------------------------------------------------------------
130              
131             # put string and regexp literals
132             # when this sub is called, $s->{a} is on the opening delimiter character
133             sub putLiteral {
134 15     15 0 15 my $s = shift;
135 15         22 my $delimiter = $s->{a}; # ', " or /
136 15         21 action1($s);
137 15   66     16 do {
138 93   66     340 while (defined($s->{a}) && $s->{a} eq '\\') { # escape character only escapes only the next one character
139 11         12 action1($s);
140 11         11 action1($s);
141             }
142 93         91 action1($s);
143             } until ($s->{last} eq $delimiter || !defined($s->{a}));
144 15 50       36 if ($s->{last} ne $delimiter) { # ran off end of file before printing the closing delimiter
145 0 0       0 die 'unterminated ' . ($delimiter eq '\'' ? 'single quoted string' : $delimiter eq '"' ? 'double quoted string' : 'regular expression') . ' literal, stopped';
    0          
146             }
147             }
148              
149             # -----------------------------------------------------------------------------
150              
151             # If $s->{a} is a whitespace then collapse all following whitespace.
152             # If any of the whitespace is a new line then ensure $s->{a} is a new line
153             # when this function ends.
154             sub collapseWhitespace {
155 420     420 0 269 my $s = shift;
156 420   100     843 while (defined($s->{a}) && isWhitespace($s->{a}) &&
      100        
      100        
157             defined($s->{b}) && isWhitespace($s->{b})) {
158 99 100 100     120 if (isEndspace($s->{a}) || isEndspace($s->{b})) {
159 82         92 $s->{a} = "\n";
160             }
161 99         99 action4($s); # delete b
162             }
163             }
164              
165             # Advance $s->{a} to non-whitespace or end of file.
166             # Doesn't print any of this whitespace.
167             sub skipWhitespace {
168 241     241 0 202 my $s = shift;
169 241   100     525 while (defined($s->{a}) && isWhitespace($s->{a})) {
170 193         199 action3($s);
171             }
172             }
173              
174             # Advance $s->{a} to non-whitespace or end of file
175             # If any of the whitespace is a new line then print one new line.
176             sub preserveEndspace {
177 125     125 0 104 my $s = shift;
178 125         124 collapseWhitespace($s);
179 125 100 100     288 if (defined($s->{a}) && isEndspace($s->{a}) && defined($s->{b}) && !isPostfix($s->{b}) ) {
      100        
      100        
180 27         29 action1($s);
181             }
182 125         139 skipWhitespace($s);
183             }
184              
185             sub onWhitespaceConditionalComment {
186 28     28 0 20 my $s = shift;
187 28   66     64 return (defined($s->{a}) && isWhitespace($s->{a}) &&
188             defined($s->{b}) && $s->{b} eq '/' &&
189             defined($s->{c}) && ($s->{c} eq '/' || $s->{c} eq '*') &&
190             defined($s->{d}) && $s->{d} eq '@');
191             }
192              
193             # -----------------------------------------------------------------------------
194              
195             sub minify {
196 20     20 0 11751 my %h = @_;
197             # Immediately turn hash into a hash reference so that notation is the same in this function
198             # as others. Easier refactoring.
199 20         37 my $s = \%h; # hash reference for "state". This module is functional programming and the state is passed between functions.
200              
201             # determine if the the input is a string or a file handle.
202 20         36 my $ref = \$s->{input};
203 20 100 66     118 if (defined($ref) && ref($ref) eq 'SCALAR'){
204 5         8 $s->{inputPos} = 0;
205 5         10 $s->{inputType} = 'string';
206             }
207             else {
208 15         21 $s->{inputType} = 'file';
209             }
210              
211             # Determine if the output is to a string or a file.
212 20 100       40 if (!defined($s->{outfile})) {
213 5         9 $s->{output} = '';
214             }
215              
216             # Print the copyright notice first
217 20 100       44 if ($s->{copyright}) {
218 1         3 _put($s, '/* ' . $s->{copyright} . ' */');
219             }
220              
221             # Initialize the buffer.
222 20   66     19 do {
223 25         39 $s->{a} = _get($s);
224             } while (defined($s->{a}) && isWhitespace($s->{a}));
225 20         34 $s->{b} = _get($s);
226 20         29 $s->{c} = _get($s);
227 20         29 $s->{d} = _get($s);
228 20         40 $s->{last} = undef; # assign for safety
229 20         21 $s->{lastnws} = undef; # assign for safety
230              
231             # local variables
232 20         17 my $ccFlag; # marks if a comment is an Internet Explorer conditional comment and should be printed to output
233              
234 20         34 while (defined($s->{a})) { # on this line $s->{a} should always be a non-whitespace character or undef (i.e. end of file)
235              
236 460 50       491 if (isWhitespace($s->{a})) { # check that this program is running correctly
237 0         0 die 'minifier bug: minify while loop starting with whitespace, stopped';
238             }
239              
240             # Each branch handles trailing whitespace and ensures $s->{a} is on non-whitespace or undef when branch finishes
241 460 100 100     2442 if ($s->{a} eq '/') { # a division, comment, or regexp literal
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
      100        
      66        
      66        
242 95 100 66     662 if (defined($s->{b}) && $s->{b} eq '/') { # slash-slash comment
    100 66        
    100 100        
      33        
243 29   66     87 $ccFlag = defined($s->{c}) && $s->{c} eq '@'; # tests in IE7 show no space allowed between slashes and at symbol
244 29   66     19 do {
245 156 100       214 $ccFlag ? action2($s) : action3($s);
246             } until (!defined($s->{a}) || isEndspace($s->{a}));
247 29 50       54 if (defined($s->{a})) { # $s->{a} is a new line
248 29 100 100     72 if ($ccFlag) {
    100 100        
249 14         16 action1($s); # cannot use preserveEndspace($s) here because it might not print the new line
250 14         20 skipWhitespace($s);
251             }
252             elsif (defined($s->{last}) && !isEndspace($s->{last}) && !isPrefix($s->{last})) {
253 8         9 preserveEndspace($s);
254             }
255             else {
256 7         8 skipWhitespace($s);
257             }
258             }
259             }
260             elsif (defined($s->{b}) && $s->{b} eq '*') { # slash-star comment
261 38   66     116 $ccFlag = defined($s->{c}) && $s->{c} eq '@'; # test in IE7 shows no space allowed between star and at symbol
262 38   100     26 do {
      33        
263 265 100       358 $ccFlag ? action2($s) : action3($s);
264             } until (!defined($s->{b}) || ($s->{a} eq '*' && $s->{b} eq '/'));
265 38 50       55 if (defined($s->{b})) { # $s->{a} is asterisk and $s->{b} is foreslash
266 38 100       45 if ($ccFlag) {
267 15         17 action2($s); # the *
268 15         17 action2($s); # the /
269             # inside the conditional comment there may be a missing terminal semi-colon
270 15         18 preserveEndspace($s);
271             }
272             else { # the comment is being removed
273 23         24 action3($s); # the *
274 23         30 $s->{a} = ' '; # the /
275 23         25 collapseWhitespace($s);
276 23 100 66     122 if (defined($s->{last}) && defined($s->{b}) &&
    100 66        
      66        
      100        
277             ((isAlphanum($s->{last}) && (isAlphanum($s->{b})||$s->{b} eq '.')) ||
278             ($s->{last} eq '+' && $s->{b} eq '+') || ($s->{last} eq '-' && $s->{b} eq '-'))) { # for a situation like 5-/**/-2 or a/**/a
279             # When entering this block $s->{a} is whitespace.
280             # The comment represented whitespace that cannot be removed. Therefore replace the now gone comment with a whitespace.
281 4         7 action1($s);
282             }
283             elsif (defined($s->{last}) && !isPrefix($s->{last})) {
284 7         10 preserveEndspace($s);
285             }
286             else {
287 12         13 skipWhitespace($s);
288             }
289             }
290             }
291             else {
292 0         0 die 'unterminated comment, stopped';
293             }
294             }
295             elsif (defined($s->{lastnws}) && ($s->{lastnws} eq ')' || $s->{lastnws} eq ']' ||
296             $s->{lastnws} eq '.' || isAlphanum($s->{lastnws}))) { # division
297 19         29 action1($s);
298 19         25 collapseWhitespace($s);
299             # don't want a division to become a slash-slash comment with following conditional comment
300 19 100       32 onWhitespaceConditionalComment($s) ? action1($s) : preserveEndspace($s);
301             }
302             else { # regexp literal
303 9         15 putLiteral($s);
304 9         16 collapseWhitespace($s);
305             # don't want closing delimiter to become a slash-slash comment with following conditional comment
306 9 100       18 onWhitespaceConditionalComment($s) ? action1($s) : preserveEndspace($s);
307             }
308             }
309             elsif ($s->{a} eq '\'' || $s->{a} eq '"' ) { # string literal
310 6         9 putLiteral($s);
311 6         7 preserveEndspace($s);
312             }
313             elsif ($s->{a} eq '+' || $s->{a} eq '-') { # careful with + + and - -
314 31         34 action1($s);
315 31         39 collapseWhitespace($s);
316 31 100 100     75 if (defined($s->{a}) && isWhitespace($s->{a})) {
317 4 100 66     20 (defined($s->{b}) && $s->{b} eq $s->{last}) ? action1($s) : preserveEndspace($s);
318             }
319             }
320             elsif (isAlphanum($s->{a})) { # keyword, identifiers, numbers
321 213         223 action1($s);
322 213         277 collapseWhitespace($s);
323 213 100 100     474 if (defined($s->{a}) && isWhitespace($s->{a})) {
324             # if $s->{b} is '.' could be (12 .toString()) which is property invocation. If space removed becomes decimal point and error.
325 49 100 33     121 (defined($s->{b}) && (isAlphanum($s->{b}) || $s->{b} eq '.')) ? action1($s) : preserveEndspace($s);
326             }
327             }
328             elsif ($s->{a} eq ']' || $s->{a} eq '}' || $s->{a} eq ')') { # no need to be followed by space but maybe needs following new line
329 31         32 action1($s);
330 31         41 preserveEndspace($s);
331             }
332             elsif ($s->{stripDebug} && $s->{a} eq ';' &&
333             defined($s->{b}) && $s->{b} eq ';' &&
334             defined($s->{c}) && $s->{c} eq ';') {
335 1         1 action3($s); # delete one of the semi-colons
336 1         1 $s->{a} = '/'; # replace the other two semi-colons
337 1         3 $s->{b} = '/'; # so the remainder of line is removed
338             }
339             else { # anything else just prints and trailing whitespace discarded
340 83         97 action1($s);
341 83         90 skipWhitespace($s);
342             }
343             }
344              
345 20 100 66     128 if ( defined $s->{last_read_char} and $s->{last_read_char} =~ /\n/ ) {
346 2         6 _put($s, "\n");
347             }
348              
349 20 100       111 if (!defined($s->{outfile})) {
350 5         41 return $s->{output};
351             }
352              
353             } # minify()
354              
355             1;
356             __END__