File Coverage

blib/lib/CSS/Minifier.pm
Criterion Covered Total %
statement 101 108 93.5
branch 33 42 78.5
condition 61 96 63.5
subroutine 17 17 100.0
pod 0 13 0.0
total 212 276 76.8


line stmt bran cond sub pod time code
1             package CSS::Minifier;
2              
3 1     1   41138 use strict;
  1         1  
  1         47  
4 1     1   5 use warnings;
  1         1  
  1         1537  
5              
6             require Exporter;
7             our @ISA = qw(Exporter);
8             our @EXPORT_OK = qw(minify);
9              
10             our $VERSION = '0.01';
11              
12             # -----------------------------------------------------------------------------
13              
14             sub isSpace {
15 598     598 0 572 my $x = shift;
16 598   66     3229 return ($x eq ' ' || $x eq "\t");
17             }
18              
19             sub isEndspace {
20 515     515 0 528 my $x = shift;
21 515   66     4698 return ($x eq "\n" || $x eq "\r" || $x eq "\f");
22             }
23              
24             sub isWhitespace {
25 598     598 0 665 my $x = shift;
26 598   100     1295 return (isSpace($x) || isEndspace($x));
27             }
28              
29             # whitespace characters before or after these characters can be removed.
30             sub isInfix {
31 240     240 0 243 my $x = shift;
32 240   100     1817 return ($x eq '{' || $x eq '}' || $x eq ';' || $x eq ':');
33             }
34              
35             # whitespace characters after these characters can be removed.
36             sub isPrefix {
37 229     229 0 253 my $x = shift;
38 229         328 return (isInfix($x));
39             }
40              
41             # whitespace characters before these characters can removed.
42             sub isPostfix {
43 11     11 0 14 my $x = shift;
44 11         16 return (isInfix($x));
45             }
46              
47             # -----------------------------------------------------------------------------
48              
49             sub _get {
50 552     552   565 my $s = shift;
51 552 100       1057 if ($s->{inputType} eq 'file') {
    50          
52 536         1880 return getc($s->{input});
53             }
54             elsif ($s->{inputType} eq 'string') {
55 16 100       33 if ($s->{'inputPos'} < length($s->{input})) {
56 13         59 return substr($s->{input}, $s->{inputPos}++, 1);
57             }
58             else { # Simulate getc() when off the end of the input string.
59 3         11 return undef;
60             }
61             }
62             else {
63 0         0 die "no input";
64             }
65             }
66              
67             sub _put {
68 256     256   247 my $s = shift;
69 256         233 my $x = shift;
70 256         409 my $outfile = ($s->{outfile});
71 256 100       430 if (defined($s->{outfile})) {
72 247         538 print $outfile $x;
73             }
74             else {
75 9         33 $s->{output} .= $x;
76             }
77             }
78              
79             # -----------------------------------------------------------------------------
80             # print a
81             # new b
82             #
83             # i.e. print a and advance
84             sub action1 {
85 254     254 0 279 my $s = shift;
86 254         330 $s->{last} = $s->{a};
87 254         393 _put($s, $s->{a});
88 254         418 action2($s);
89             }
90              
91             # move b to a
92             # new b
93             #
94             # i.e. delete a and advance
95             sub action2 {
96 542     542 0 557 my $s = shift;
97 542         708 $s->{a} = $s->{b};
98 542         831 $s->{b} = $s->{c};
99 542         764 $s->{c} = _get($s);
100             }
101              
102             # -----------------------------------------------------------------------------
103              
104             # put string literals
105             # when this sub is called, $s->{a} is on the opening delimiter character
106             sub putLiteral {
107 2     2 0 3 my $s = shift;
108 2         4 my $delimiter = $s->{a}; # ' or "
109              
110 2         4 action1($s);
111 2   66     4 do {
112 15   66     70 while (defined($s->{a}) && $s->{a} eq '\\') { # escape character escapes only the next one character
113 2         5 action1($s);
114 2         3 action1($s);
115             }
116 15         26 action1($s);
117             } until ($s->{last} eq $delimiter || !defined($s->{a}));
118 2 50       6 if ($s->{last} ne $delimiter) { # ran off end of file before printing the closing delimiter
119 0 0       0 die 'unterminated ' . ($delimiter eq '\'' ? 'single quoted string' : 'double quoted string') . ' literal, stopped';
120             }
121             }
122              
123             # -----------------------------------------------------------------------------
124              
125             # If $s->{a} is a whitespace then collapse all following whitespace.
126             # If any of the whitespace is a new line then ensure $s->{a} is a new line
127             # when this function ends.
128             sub collapseWhitespace {
129 11     11 0 10 my $s = shift;
130 11   33     38 while (defined($s->{a}) && isWhitespace($s->{a}) &&
      33        
      66        
131             defined($s->{b}) && isWhitespace($s->{b})) {
132 1 50 33     5 if (isEndspace($s->{a}) || isEndspace($s->{b})) {
133 0         0 $s->{a} = "\n";
134             }
135 1         2 action2($s); # delete b
136             }
137             }
138              
139             # Advance $s->{a} to non-whitespace or end of file.
140             # Doesn't print any of this whitespace.
141             sub skipWhitespace {
142 56     56 0 68 my $s = shift;
143 56   100     159 while (defined($s->{a}) && isWhitespace($s->{a})) {
144 90         187 action2($s);
145             }
146             }
147              
148             # #s->{a} should be on whitespace when this function is called
149             sub preserveWhitespace {
150 11     11 0 13 my $s = shift;
151 11         16 collapseWhitespace($s);
152 11 100 33     66 if (defined($s->{a}) && defined($s->{b}) && !isPostfix($s->{b})) {
      66        
153 4         8 action1($s); # print the whitespace character
154             }
155 11         18 skipWhitespace($s);
156             }
157              
158             # -----------------------------------------------------------------------------
159              
160             sub minify {
161 3     3 0 6011 my %h = @_;
162             # Immediately turn hash into a hash reference so that notation is the same in this function
163             # as others. Easier refactoring.
164 3         6 my $s = \%h; # hash reference for "state". This module is functional programming and the state is passed between functions.
165            
166             # determine if the the input is a string or a file handle.
167 3         5 my $ref = \$s->{input};
168 3 100 66     23 if (defined($ref) && ref($ref) eq 'SCALAR'){
169 1         3 $s->{inputPos} = 0;
170 1         3 $s->{inputType} = 'string';
171             }
172             else {
173 2         5 $s->{inputType} = 'file';
174             }
175            
176             # Determine if the output is to a string or a file.
177 3 100       8 if (!defined($s->{outfile})) {
178 1         2 $s->{output} = '';
179             }
180            
181             # Print the copyright notice first
182 3 50       7 if ($s->{copyright}) {
183 0         0 _put($s, '/* ' . $s->{copyright} . ' */');
184             }
185            
186             # Initialize the buffer.
187 3   66     5 do {
188 4         8 $s->{a} = _get($s);
189             } while (defined($s->{a}) && isWhitespace($s->{a}));
190 3         15 $s->{b} = _get($s);
191 3         6 $s->{c} = _get($s);
192 3         167 $s->{last} = undef;
193              
194             # local variables
195 3         5 my $macIeCommentHackFlag = 0; # marks if a have recently seen a comment with an escaped close like this /* foo \*/
196             # and have not yet seen a regular comment to close this like /* bar */
197              
198 3         8 while (defined($s->{a})) { # on this line $s->{a} should always be a non-whitespace character or undef (i.e. end of file)
199            
200 236 50       409 if (isWhitespace($s->{a})) { # check that this program is running correctly
201 0         0 die 'minifier bug: minify while loop starting with whitespace, stopped';
202             }
203            
204             # Each branch handles trailing whitespace and ensures $s->{a} is on non-whitespace or undef when branch finishes
205 236 100 66     1495 if ($s->{a} eq '/' && defined($s->{b}) && $s->{b} eq '*') { # a comment
    100 66        
    100 66        
206 5   100     7 do {
      33        
207 192         447 action2($s); # advance buffer by one
208             # if a is \, b is *, c is /, hack flag false
209             # Mac/IE hack start
210             # set hack flag true
211             # print /*\*/
212 192 100 66     480 if ($s->{a} eq '\\' &&
      66        
      33        
      33        
      66        
213             defined($s->{b}) && $s->{b} eq '*' &&
214             defined($s->{c}) && $s->{c} eq '/' &&
215             !$macIeCommentHackFlag) {
216 1         2 $macIeCommentHackFlag = 1;
217 1         3 _put($s, '/*\\*/');
218 1         2 $s->{last} = '/';
219             }
220             # if a is not \, b is *, c is /, hack flag true
221             # Mac/IE hack end
222             # set hack flag false
223             # print /**/
224 192 100 66     1785 if ($s->{a} ne '\\' &&
      100        
      66        
      66        
      66        
225             defined($s->{b}) && $s->{b} eq '*' &&
226             defined($s->{c}) && $s->{c} eq '/' &&
227             $macIeCommentHackFlag) {
228 1         4 $macIeCommentHackFlag = 0;
229 1         4 _put($s, '/**/');
230 1         6 $s->{last} = '/';
231             }
232            
233             } until (!defined($s->{b}) || ($s->{a} eq '*' && $s->{b} eq '/'));
234 5 50       12 if (defined($s->{b})) { # $s->{a} is asterisk and $s->{b} is forward slash
235 5         10 action2($s); # the *
236 5         9 $s->{a} = ' '; # the /
237 5         8 skipWhitespace($s);
238             }
239             else {
240 0         0 die 'unterminated comment, stopped';
241             }
242             }
243             elsif ($s->{a} eq '\'' || $s->{a} eq '"') {
244 2         6 putLiteral($s);
245 2 50 33     8 if (defined($s->{a}) && isWhitespace($s->{a})) {
246 0         0 preserveWhitespace($s); # can this be skipWhitespace?
247             }
248             }
249             elsif (isPrefix($s->{a})) {
250 40         52 action1($s);
251 40         96 skipWhitespace($s);
252             }
253             else { # anything else just prints
254 189         317 action1($s);
255 189 100 66     522 if (defined($s->{a}) && isWhitespace($s->{a})) {
256 11         23 preserveWhitespace($s);
257             }
258             }
259             }
260            
261 3 100       21 if (!defined($s->{outfile})) {
262 1         8 return $s->{output};
263             }
264            
265             }
266              
267             # -----------------------------------------------------------------------------
268              
269             1;
270             __END__