File Coverage

blib/lib/HTML/Packer.pm
Criterion Covered Total %
statement 164 176 93.1
branch 91 108 84.2
condition 24 33 72.7
subroutine 28 28 100.0
pod 4 8 50.0
total 311 353 88.1


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