File Coverage

blib/lib/JavaScript/Minifier.pm
Criterion Covered Total %
statement 149 155 96.1
branch 79 90 87.7
condition 131 162 80.8
subroutine 20 20 100.0
pod 0 16 0.0
total 379 443 85.5


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