File Coverage

blib/lib/HTML/Packer.pm
Criterion Covered Total %
statement 172 179 96.0
branch 95 108 87.9
condition 24 33 72.7
subroutine 29 29 100.0
pod 4 8 50.0
total 324 357 90.7


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