File Coverage

blib/lib/HTML/Packer.pm
Criterion Covered Total %
statement 134 145 92.4
branch 75 94 79.7
condition 18 30 60.0
subroutine 26 26 100.0
pod 2 6 33.3
total 255 301 84.7


line stmt bran cond sub pod time code
1             package HTML::Packer;
2              
3 9     9   284026 use 5.008009;
  9         109  
4 9     9   73 use strict;
  9         32  
  9         147  
5 9     9   65 use warnings;
  9         54  
  9         203  
6 9     9   72 use Carp;
  9         27  
  9         390  
7 9     9   2351 use Regexp::RegGrp;
  9         20374  
  9         3525  
8              
9             # -----------------------------------------------------------------------------
10              
11             our $VERSION = '2.08';
12              
13             our @BOOLEAN_ACCESSORS = (
14             'remove_comments',
15             'remove_comments_aggressive',
16             'remove_newlines',
17             'no_compress_comment',
18             'html5',
19             );
20              
21             our @JAVASCRIPT_OPTS = ( 'clean', 'obfuscate', 'shrink', 'best' );
22             our @CSS_OPTS = ( 'minify', 'pretty' );
23              
24             our $REQUIRED_JAVASCRIPT_PACKER = '1.002001';
25             our $REQUIRED_CSS_PACKER = '1.000001';
26              
27             our @SAVE_SPACE_ELEMENTS = (
28             'a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'button', 'cite',
29             'del', 'dfn', 'em', 'font', 'i', 'input', 'ins', 'kbd', 'label', 'q',
30             's', 'samp', 'select', 'small', 'strike', 'strong', 'sub', 'sup', 'u', 'var'
31             );
32              
33             our @VOID_ELEMENTS = (
34             'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input',
35             'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
36             );
37              
38             # Some regular expressions are from HTML::Clean
39              
40             our $COMMENT = '((?>\s*))()((?>\s*))';
41             our $COMMENT_SAFE = '((?>\s*))()((?>\s*))';
42              
43             our $PACKER_COMMENT = '';
44              
45             our $DOCTYPE = '<\!DOCTYPE[^>]*>';
46              
47             our $DONT_CLEAN = '(<\s*(pre|code|textarea|script|style)[^>]*>)(.*?)(<\s*\/\2[^>]*>)';
48              
49             our $WHITESPACES = [
50             {
51             regexp => qr/^\s*/s,
52             replacement => ''
53             },
54             {
55             regexp => qr/\s*$/s,
56             replacement => ''
57             },
58             {
59             regexp => '^\s*',
60             replacement => '',
61             modifier => 'm'
62             },
63             {
64             regexp => '[^\S\n]*$',
65             replacement => '',
66             modifier => 'm'
67             },
68             {
69             regexp => qr/(?<=>)[^<>]*(?=<)/sm,
70             replacement => sub {
71             my $match = $_[0]->{match};
72              
73             $match =~ s/[^\S\n]{2,}/ /sg;
74             $match =~ s/\s*\n+\s*/\n/sg;
75              
76             return $match;
77             }
78             },
79             {
80             regexp => '<\s*(\/)?\s*',
81             replacement => sub {
82             return sprintf( '<%s', $_[0]->{submatches}->[0] );
83             },
84             modifier => 's'
85             },
86             {
87             regexp => '\s*(\/)?\s*>',
88             replacement => sub {
89             return sprintf( '%s>', $_[0]->{submatches}->[0] );
90             },
91             modifier => 's'
92             }
93             ];
94              
95             our $NEWLINES_TAGS = [
96             {
97             regexp => '(\s*)(<\s*\/?\s*(?:' . join( '|', @SAVE_SPACE_ELEMENTS ) . ')\b[^>]*>)(\s*)',
98             replacement => sub {
99             return sprintf( '%s%s%s', $_[0]->{submatches}->[0] ? ' ' : '', $_[0]->{submatches}->[1], $_[0]->{submatches}->[2] ? ' ' : '' );
100             },
101             modifier => 'is'
102             }
103             ];
104              
105             our $NEWLINES = [
106             {
107             regexp => '(.)\n(.)',
108             replacement => sub {
109             my ( $pre, $post ) = @{$_[0]->{submatches}};
110              
111             my $ret;
112              
113             if ( $pre eq '>' and $post eq '<' ) {
114             $ret = $pre . $post;
115             }
116             elsif ( $pre eq '-' and $post =~ /[\w]/ ) {
117             $ret = $pre . $post;
118             }
119             else {
120             $ret = $pre . ' ' . $post;
121             }
122              
123             return $ret;
124             }
125             }
126             ];
127              
128             our @REGGRPS = ( 'newlines', 'newlines_tags', 'whitespaces', 'void_elements' );
129              
130             our $GLOBAL_REGGRP = 'global';
131              
132             ##########################################################################################
133              
134             {
135 9     9   93 no strict 'refs';
  9         27  
  9         10710  
136              
137             foreach my $field ( @BOOLEAN_ACCESSORS ) {
138             next if defined *{ __PACKAGE__ . '::' . $field }{CODE};
139              
140             *{ __PACKAGE__ . '::' . $field} = sub {
141 335     335   518 my ( $self, $value ) = @_;
142              
143 335 100       638 $self->{'_' . $field} = $value ? 1 : undef if ( defined( $value ) );
    100          
144              
145 335         883 return $self->{'_' . $field};
146             };
147             }
148              
149             foreach my $reggrp ( @REGGRPS, $GLOBAL_REGGRP ) {
150             next if defined *{ __PACKAGE__ . '::reggrp_' . $reggrp }{CODE};
151              
152             *{ __PACKAGE__ . '::reggrp_' . $reggrp } = sub {
153 197     197   311 my ( $self ) = shift;
154              
155 197         843 return $self->{ '_reggrp_' . $reggrp };
156             };
157             }
158             }
159              
160             sub do_javascript {
161 75     75 1 167 my ( $self, $value ) = @_;
162              
163 75 100       168 if ( defined( $value ) ) {
164 33 100       135 if ( grep( $value eq $_, @JAVASCRIPT_OPTS ) ) {
    100          
165 30         108 $self->{_do_javascript} = $value;
166             }
167             elsif ( ! $value ) {
168 4         46 $self->{_do_javascript} = undef;
169             }
170             }
171              
172 75         154 return $self->{_do_javascript};
173             }
174              
175             sub do_stylesheet {
176 52     52 1 124 my ( $self, $value ) = @_;
177              
178 52 100       143 if ( defined( $value ) ) {
179 11 100       61 if ( grep( $value eq $_, @CSS_OPTS ) ) {
    100          
180 8         64 $self->{_do_stylesheet} = $value;
181             }
182             elsif ( ! $value ) {
183 4         46 $self->{_do_stylesheet} = undef;
184             }
185             }
186              
187 52         119 return $self->{_do_stylesheet};
188             }
189              
190             # these variables are used in the closures defined in the init function
191             # below - we have to use globals as using $self within the closures leads
192             # to a reference cycle and thus memory leak, and we can't scope them to
193             # the init method as they may change. they are set by the minify sub
194             our $remove_comments;
195             our $remove_comments_aggressive;
196             our $remove_newlines;
197             our $html5;
198             our $do_javascript;
199             our $do_stylesheet;
200             our $js_packer;
201             our $css_packer;
202             our $reggrp_ws;
203              
204             sub init {
205 33     33 0 33093 my $class = shift;
206 33         104 my $self = {};
207              
208 31         62 bless( $self, $class );
209              
210 31         108 $self->{whitespaces}->{reggrp_data} = $WHITESPACES;
211 31         71 $self->{newlines}->{reggrp_data} = $NEWLINES;
212 31         69 $self->{newlines_tags}->{reggrp_data} = $NEWLINES_TAGS;
213             $self->{global}->{reggrp_data} = [
214             {
215             regexp => $DOCTYPE,
216             replacement => sub {
217 1     3   13 return '';
218             },
219             store => sub {
220 1     1   6 my $doctype = $_[0]->{match};
221              
222 1         12 $doctype =~ s/\s+/ /gsm;
223              
224 1         14 return $doctype;
225             }
226             },
227             {
228             # this is using a variable that won't be initialized until after we have
229             # called ->minify so we endup calling ->init again (see FIXME)
230             regexp => $remove_comments_aggressive ? $COMMENT : $COMMENT_SAFE,
231             replacement => sub {
232             return $remove_comments ? (
233             $remove_newlines ? ' ' : (
234             ( $_[0]->{submatches}->[0] =~ /\n/s or $_[0]->{submatches}->[2] =~ /\n/s ) ? "\n" : ''
235             )
236 37 50 33 37   11227 ) : '';
    100          
    100          
237             },
238             store => sub {
239             my $ret = $remove_comments ? '' : (
240             ( ( not $remove_newlines and $_[0]->{submatches}->[0] =~ /\n/s ) ? "\n" : '' ) .
241             $_[0]->{submatches}->[1] .
242 37 100 66 37   523 ( ( not $remove_newlines and $_[0]->{submatches}->[2] =~ /\n/s ) ? "\n" : '' )
    100 100        
    100          
243             );
244              
245 37         78 return $ret;
246             }
247             },
248             {
249             regexp => $DONT_CLEAN,
250             replacement => sub {
251 26     26   5015 return '';
252             },
253             store => sub {
254 26     26   346 my ( $opening, undef, $content, $closing ) = @{$_[0]->{submatches}};
  26         87  
255              
256 26 50       59 if ( $content ) {
257 26 100       93 my $opening_script_re = '<\s*script' . ( $html5 ? '[^>]*>' : '[^>]*(?:java|ecma)script[^>]*>' );
258 26 100       76 my $opening_style_re = '<\s*style' . ( $html5 ? '[^>]*>' : '[^>]*text\/css[^>]*>' );
259 26         38 my $js_type_re = q{type=['"]((application|text)/){0,1}(x-){0,1}(java|ecma)script['"]};
260              
261 26 100 33     509 if (
    100 66        
262             $opening =~ /$opening_script_re/i
263             && ( $opening =~ /$js_type_re/i || $opening !~ /type/i )
264             ) {
265 18 100       63 $opening =~ s/ type="(text\/)?(java|ecma)script"//i if ( $html5 );
266              
267 18 50 33     87 if ( $js_packer and $do_javascript ) {
268 18         121 $js_packer->minify( \$content, { compress => $do_javascript } );
269              
270 18 100       19205 unless ( $html5 ) {
271 16         63 $content = '/**/';
272             }
273             }
274             }
275             elsif ( $opening =~ /$opening_style_re/i ) {
276 3 100       21 $opening =~ s/ type="text\/css"//i if ( $html5 );
277              
278 3 50 33     24 if ( $css_packer and $do_stylesheet ) {
279 3         20 $css_packer->minify( \$content, { compress => $do_stylesheet } );
280 3 50       1579 $content = "\n" . $content if ( $do_stylesheet eq 'pretty' );
281             }
282             }
283             }
284             else {
285 1         16 $content = '';
286             }
287              
288 26         86 $reggrp_ws->exec( \$opening );
289 26         1611 $reggrp_ws->exec( \$closing );
290              
291 26         1510 return $opening . $content . $closing;
292             },
293 31 100       518 modifier => 'ism'
294             }
295             ];
296              
297             $self->{void_elements}->{reggrp_data} = [
298             {
299             regexp => '<\s*((?:' . join( '|', @VOID_ELEMENTS ) . ')\b[^>]*)\s*\/>',
300             replacement => sub {
301 3     3   226 return '<' . $_[0]->{submatches}->[0] . '>';
302             },
303 31         273 modifier => 'ism'
304             }
305             ];
306              
307 31         92 foreach ( @HTML::Packer::REGGRPS ) {
308 121         23203 $self->{ '_reggrp_' . $_ } = Regexp::RegGrp->new( { reggrp => $self->{$_}->{reggrp_data} } );
309             }
310              
311             $self->{ '_reggrp_' . $GLOBAL_REGGRP } = Regexp::RegGrp->new(
312             {
313             reggrp => $self->{$GLOBAL_REGGRP}->{reggrp_data},
314 31         3530 restore_pattern => qr//
315             }
316             );
317              
318 31         8110 return $self;
319             }
320              
321             sub minify {
322 35     35 0 9112 my ( $self, $input, $opts );
323              
324 35 50 33     287 unless (
325             ref( $_[0] ) and
326             $_[0]->isa( __PACKAGE__ )
327             ) {
328 0         0 $self = __PACKAGE__->init();
329              
330 0 0       0 shift( @_ ) unless ( ref( $_[0] ) );
331              
332 0         0 ( $input, $opts ) = @_;
333             }
334             else {
335 35         87 ( $self, $input, $opts ) = @_;
336             }
337              
338 34 50       90 if ( ref( $input ) ne 'SCALAR' ) {
339 0         0 carp( 'First argument must be a scalarref!' );
340 0         0 return undef;
341             }
342              
343 34         49 my $html;
344 34         48 my $cont = 'void';
345              
346 34 50       70 if ( defined( wantarray ) ) {
347 0 0       0 my $tmp_input = ref( $input ) ? ${$input} : $input;
  0         0  
348              
349 0         0 $html = \$tmp_input;
350 0         0 $cont = 'scalar';
351             }
352             else {
353 34 50       78 $html = ref( $input ) ? $input : \$input;
354             }
355              
356 34 50       101 if ( ref( $opts ) eq 'HASH' ) {
357 34         77 foreach my $field ( @BOOLEAN_ACCESSORS ) {
358 170 100       396 $self->$field( $opts->{$field} ) if ( defined( $opts->{$field} ) );
359             }
360              
361 34 100       119 $self->do_javascript( $opts->{do_javascript} ) if ( defined( $opts->{do_javascript} ) );
362 34 100       89 $self->do_stylesheet( $opts->{do_stylesheet} ) if ( defined( $opts->{do_stylesheet} ) );
363             }
364              
365 34 100 100     86 if ( not $self->no_compress_comment() and ${$html} =~ /$PACKER_COMMENT/s ) {
  32         241  
366 1         3 my $compress = $1;
367 1 50       4 if ( $compress eq '_no_compress_' ) {
368 1 50       6 return ( $cont eq 'scalar' ) ? ${$html} : undef;
  0         0  
369             }
370             }
371              
372             # (re)initialize variables used in the closures
373 33   100     99 $remove_comments = $self->remove_comments || $self->remove_comments_aggressive;
374 33         76 $remove_comments_aggressive = $self->remove_comments_aggressive;
375 33         66 $remove_newlines = $self->remove_newlines;
376 33         70 $html5 = $self->html5;
377 33         61 $do_javascript = $self->do_javascript;
378 33         64 $do_stylesheet = $self->do_stylesheet;
379 33         74 $js_packer = $self->javascript_packer;
380 33         99 $css_packer = $self->css_packer;
381 33         89 $reggrp_ws = $self->reggrp_whitespaces;
382              
383             # FIXME: hacky way to get around ->init being called before ->minify
384 33 100       83 $self = ref( $self )->init if $remove_comments_aggressive;
385              
386 33         77 $self->reggrp_global()->exec( $html );
387 33         1163 $self->reggrp_whitespaces()->exec( $html );
388 33 100       3899 if ( $self->remove_newlines() ) {
389 29         76 $self->reggrp_newlines_tags()->exec( $html );
390 29         504 $self->reggrp_newlines()->exec( $html );
391             }
392 33 100       452 if ( $self->html5() ) {
393 4         12 $self->reggrp_void_elements()->exec( $html );
394             }
395              
396 33         277 $self->reggrp_global()->restore_stored( $html );
397              
398 33 50       1499 return ${$html} if ( $cont eq 'scalar' );
  0         0  
399             }
400              
401             sub javascript_packer {
402 34     35 0 6168 my $self = shift;
403              
404 34 100       79 unless ( $self->{_checked_javascript_packer} ) {
405 28     4   1786 eval "use JavaScript::Packer $REQUIRED_JAVASCRIPT_PACKER;";
  3     1   523  
  3     1   5167  
  3         43  
406              
407 28 50       91 unless ( $@ ) {
408 28         39 $self->{_javascript_packer} = eval {
409 28         107 JavaScript::Packer->init();
410             };
411             }
412              
413 28         89809 $self->{_checked_javascript_packer} = 1;
414             }
415              
416 34         1655 return $self->{_javascript_packer};
417             }
418              
419             sub css_packer {
420 34     35 0 3578 my $self = shift;
421              
422 34 100       136 unless ( $self->{_checked_css_packer} ) {
423 28     4   1831 eval "use CSS::Packer $REQUIRED_CSS_PACKER;";
  3     1   920  
  3         5086  
  3         41  
424              
425 28 50       107 unless ( $@ ) {
426 28         46 $self->{_css_packer} = eval {
427 28         101 CSS::Packer->init();
428             };
429             }
430              
431 28         48393 $self->{_checked_css_packer} = 1;
432             }
433              
434 34         1416 return $self->{_css_packer};
435             }
436              
437             1;
438              
439             __END__