File Coverage

blib/lib/CSS/Prepare.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package CSS::Prepare;
2              
3 57     57   5008655 use Modern::Perl;
  57         13526  
  57         478  
4              
5 57     57   58806 use CSS::Prepare::CSSGrammar;
  57         601  
  57         8304  
6 57     57   38832 use CSS::Prepare::Property::Background;
  0            
  0            
7             use CSS::Prepare::Property::Border;
8             use CSS::Prepare::Property::BorderRadius;
9             use CSS::Prepare::Property::Color;
10             use CSS::Prepare::Property::Effects;
11             use CSS::Prepare::Property::Font;
12             use CSS::Prepare::Property::Formatting;
13             use CSS::Prepare::Property::Generated;
14             use CSS::Prepare::Property::Hacks;
15             use CSS::Prepare::Property::Margin;
16             use CSS::Prepare::Property::Padding;
17             use CSS::Prepare::Property::Tables;
18             use CSS::Prepare::Property::Text;
19             use CSS::Prepare::Property::UI;
20             use CSS::Prepare::Property::Values;
21             use CSS::Prepare::Property::Vendor;
22             use Digest::SHA1 qw( sha1_hex );
23             use FileHandle;
24             use File::Basename;
25             use File::Path;
26             use List::Util qw( first );
27             use Storable qw( dclone );
28              
29             use version;
30             our $VERSION = qv( 0.9.2.4 );
31              
32             use constant MAX_REDIRECT => 3;
33             use constant RE_IS_URL => qr{^ http s? : // }x;
34             use constant RE_MATCH_HOSTNAME => qr{^ ( http s? : // [^/]+ ) /? .* $}x;
35             use constant RE_MATCH_DIRECTORY => qr{^ (.*?) (?: / [^/]* )? $}x;
36             use constant NOT_VENDOR_PREFIX => qw(
37             background border empty font letter
38             list margin padding page table
39             text unicode white z
40             );
41              
42             my @MODULES = qw(
43             Background
44             Border
45             BorderRadius
46             Color
47             Effects
48             Font
49             Formatting
50             Hacks
51             Generated
52             Margin
53             Padding
54             Tables
55             Text
56             UI
57             Vendor
58             );
59              
60              
61              
62             sub new {
63             my $class = shift;
64             my %args = @_;
65            
66             my $self = {
67             hacks => 1,
68             extended => 0,
69             suboptimal_threshold => 10,
70             http_timeout => 30,
71             pretty => 0,
72             assets_output => undef,
73             assets_base => undef,
74             location => undef,
75             status => \&status_to_stderr,
76             %args
77             };
78             bless $self, $class;
79            
80             my %http_providers = (
81             lite => 'HTTP::Lite',
82             lwp => 'LWP::UserAgent',
83             );
84            
85             # sort to prefer HTTP::Lite over LWP::UserAgent
86             HTTP:
87             foreach my $provider ( sort keys %http_providers ) {
88             my $module = $http_providers{ $provider };
89            
90             eval "require $module";
91             unless ($@) {
92             $self->{'http_provider'} = $provider;
93             last HTTP;
94             }
95             }
96            
97             # check for ability to use plugins
98             if ( $self->support_extended_syntax() ) {
99             eval "use Module::Pluggable require => 1;";
100             unless ($@) {
101             foreach my $plugin ( $self->plugins() ) {
102             $self->{'has_plugins'} = 1;
103             }
104             }
105             }
106            
107             return $self;
108             }
109             sub get_hacks {
110             my $self = shift;
111             return $self->{'hacks'};
112             }
113             sub set_hacks {
114             my $self = shift;
115             my $hacks = shift // 0;
116            
117             $self->{'hacks'} = $hacks;
118             }
119             sub support_hacks {
120             my $self = shift;
121             return $self->{'hacks'};
122             }
123             sub get_extended {
124             my $self = shift;
125             return $self->{'extended'};
126             }
127             sub set_extended {
128             my $self = shift;
129             my $extended = shift // 0;
130            
131             $self->{'extended'} = $extended;
132             }
133             sub support_extended_syntax {
134             my $self = shift;
135             return $self->{'extended'};
136             }
137             sub get_suboptimal_threshold {
138             my $self = shift;
139             return $self->{'suboptimal_threshold'};
140             }
141             sub set_suboptimal_threshold {
142             my $self = shift;
143             my $suboptimal_threshold = shift // 0;
144            
145             $self->{'suboptimal_threshold'} = $suboptimal_threshold;
146             }
147             sub suboptimal_threshold {
148             my $self = shift;
149             return $self->{'suboptimal_threshold'};
150             }
151             sub get_pretty {
152             my $self = shift;
153             return $self->{'pretty'};
154             }
155             sub set_pretty {
156             my $self = shift;
157             my $value = shift;
158            
159             $self->{'pretty'} = $value;
160             }
161             sub pretty_output {
162             my $self = shift;
163             return $self->{'pretty'};
164             }
165             sub assets_output {
166             my $self = shift;
167             return defined $self->{'assets_output'};
168             }
169             sub location {
170             my $self = shift;
171             return $self->{'location'};
172             }
173             sub set_base_directory {
174             my $self = shift;
175             my $base = shift;
176            
177             # ensure trailing slash
178             $base =~ s{/?$}{/};
179            
180             $self->{'base_directory'} = $base;
181             }
182             sub get_base_directory {
183             my $self = shift;
184            
185             return $self->{'base_directory'};
186             }
187             sub set_base_url {
188             my $self = shift;
189            
190             $self->{'base_url'} = shift;
191             }
192             sub get_base_url {
193             my $self = shift;
194            
195             return $self->{'base_url'};
196             }
197             sub get_http_timeout {
198             my $self = shift;
199             return $self->{'http_timeout'};
200             }
201             sub set_http_timeout {
202             my $self = shift;
203            
204             $self->{'http_timeout'} = shift;
205             }
206             sub has_http {
207             my $self = shift;
208            
209             return defined $self->{'http_provider'};
210             }
211             sub get_http_provider {
212             my $self = shift;
213            
214             return $self->{'http_provider'};
215             }
216             sub has_plugins {
217             my $self = shift;
218             return $self->{'has_plugins'};
219             }
220              
221             my $elements_first = sub {
222             my $a_element = ( $a =~ m{^[a-z]}i );
223             my $b_element = ( $b =~ m{^[a-z]}i );
224             my $element_count = $a_element + $b_element;
225              
226             return ( $a_element ? -1 : 1 )
227             if 1 == $element_count;
228             return $a cmp $b;
229             };
230              
231             sub parse_file {
232             my $self = shift;
233             my $file = shift;
234             my $location = shift;
235            
236             my $string = $self->read_file( $file );
237             return $self->parse( $string, $file, $location )
238             if defined $string;
239            
240             return;
241             }
242             sub parse_file_structure {
243             my $self = shift;
244             my $file = shift;
245            
246             my $base = $self->get_base_directory();
247             return undef
248             unless defined $base && -d $base;
249            
250             my $stylesheet = basename( $file );
251             my $directory = dirname( $file );
252             my @blocks;
253             my $path;
254            
255             foreach my $section ( split m{/}, $directory ) {
256             $path .= "${section}/";
257             my $target = "${base}${path}${stylesheet}";
258            
259             my @file_blocks = $self->parse_file( $target );
260             push @blocks, @file_blocks
261             if @file_blocks; # non-existent file is not an error
262             }
263            
264             return @blocks;
265             }
266             sub parse_url {
267             my $self = shift;
268             my $url = shift;
269             my $location = shift;
270            
271             my $string = $self->read_url( $url );
272             return $self->parse( $string, $url, $location )
273             if defined $string;
274            
275             return;
276             }
277             sub parse_url_structure {
278             my $self = shift;
279             my $file = shift;
280            
281             my $base = $self->get_base_url();
282             return undef
283             unless defined $base && $base =~ m{https?://};
284            
285             my $stylesheet = basename( $file );
286             my $directory = dirname( $file );
287             my @blocks;
288             my $path;
289            
290             foreach my $section ( split m{/}, $directory ) {
291             $path .= "${section}/";
292             my $target = "${base}${path}${stylesheet}";
293            
294             my @file_blocks = $self->parse_url( $target );
295             push @blocks, @file_blocks
296             if @file_blocks; # non-existent url is not an error
297             }
298            
299             return @blocks;
300             }
301             sub parse_string {
302             my $self = shift;
303             my $string = shift;
304             my $location = shift;
305            
306             return $self->parse( $string )
307             }
308             sub parse_stylesheet {
309             my $self = shift;
310             my $stylesheet = shift;
311             my $location = shift;
312            
313             my $target = $self->canonicalise_location( $stylesheet, $location );
314            
315             return $self->parse_url( $target, $location )
316             if $target =~ RE_IS_URL;
317             return $self->parse_file( $target, $location );
318             }
319             sub canonicalise_location {
320             my $self = shift;
321             my $file = shift;
322             my $location = shift;
323            
324             my $target;
325            
326             # don't interfere with an absolute URL
327             if ( $file =~ RE_IS_URL ) {
328             $target = $file;
329             }
330             else {
331             if ( $file =~ m{^/} ) {
332             my $base = $self->get_base_directory();
333            
334             if ( defined $base ) {
335             $target = "$base/$file";
336             }
337             else {
338             $target = $file;
339             }
340            
341             if ( defined $location ) {
342             if ( $location =~ RE_MATCH_HOSTNAME ) {
343             $target = "${1}${file}";
344             }
345             }
346             }
347             else {
348             if ( defined $location ) {
349             $location =~ RE_MATCH_DIRECTORY;
350             $target = "${1}/${file}";
351             }
352             else {
353             my $base = $self->get_base_directory() || '';
354             $target = "${base}${file}";
355             }
356             }
357             }
358            
359             return $target;
360             }
361             sub output_as_string {
362             my $self = shift;
363             my @data = @_;
364            
365             my $output = '';
366             foreach my $block ( @data ) {
367             my $type = $block->{'type'} // '';
368            
369             if ( 'at-media' eq $type || 'import' eq $type ) {
370             my $query = $block->{'query'};
371             my $string = $self->output_as_string( @{$block->{'blocks'}} );
372            
373             if ( defined $query && $query ) {
374             $string =~ s{^}{ }gm;
375             $output .= "\@media ${query}{\n${string}}\n";
376             }
377             else {
378             $output .= $string;
379             }
380             }
381             elsif ( 'verbatim' eq $type ) {
382             $output .= $block->{'string'};
383             }
384             elsif ( 'boundary' eq $type ) {
385             # just skip the block
386             }
387             else {
388             $output .= $self->output_block_as_string( $block );
389             }
390             }
391            
392             return $output;
393             }
394             sub output_block_as_string {
395             my $self = shift;
396             my $block = shift;
397            
398             my $shorthands_first_hacks_last = sub {
399             # sort hacks after normal properties
400             my $a_hack = ( $a =~ m{^ \s* [_\*] }x );
401             my $b_hack = ( $b =~ m{^ \s* [_\*] }x );
402             my $hack_count = $a_hack + $b_hack;
403             return $a_hack ? 1 : -1
404             if 1 == $hack_count;
405            
406             # sort properties with vendor prefixes before
407             # their matching properties
408             $a =~ m{^ \s* ( [-] \w+ - )? ( .* ) $}sx;
409             my $a_vendor = defined $1
410             && ! defined first
411             { $_ eq $1 } NOT_VENDOR_PREFIX;
412             $b =~ m{^ \s* ( [-] \w+ - )? ( .* ) $}sx;
413             my $b_vendor = defined $1
414             && ! defined first
415             { $_ eq $1 } NOT_VENDOR_PREFIX;
416             my $vendors = ( $a_vendor ) + ( $b_vendor );
417             return $a_vendor ? -1 : 1
418             if 1 == $vendors;
419              
420             # sort more-specific properties after less-specific properties
421             $a =~ m{^ \s* ( [^:]+ ) : }x;
422             my $a_declaration = $1;
423             $b =~ m{^ \s* ( [^:]+ ) : }x;
424             my $b_declaration = $1;
425             my $a_specifics = ( $a_declaration =~ tr{-}{-} );
426             my $b_specifics = ( $b_declaration =~ tr{-}{-} );
427             return $a_specifics <=> $b_specifics
428             if $a_specifics != $b_specifics;
429            
430             # just sort alphabetically
431             return $a cmp $b;
432             };
433            
434             my %properties = $self->output_properties( $block->{'block'} );
435             return '' unless %properties;
436            
437             # unique selectors only
438             my %seen;
439             my @selectors = grep { !$seen{$_}++ } @{$block->{'selectors'}};
440            
441             my $output;
442             my $separator = $self->pretty_output ? ",\n" : ',';
443            
444             my $selector = join $separator, sort $elements_first @selectors;
445             my $properties = join '', sort $shorthands_first_hacks_last
446             keys %properties;
447            
448             return $self->pretty_output
449             ? "${selector} \{\n${properties}\}\n"
450             : "${selector}\{${properties}\}\n";
451             }
452             sub output_properties {
453             my $self = shift;
454             my $block = shift;
455            
456             # separate out the important rules from the normal, so that they are
457             # not accidentally shorthanded, despite being different values
458             my %normal;
459             my %important;
460             foreach my $key ( keys %{$block} ) {
461             if ( $key =~ m{^important-(.*)$} ) {
462             $important{ $1 } = $block->{ $key };
463             }
464             else {
465             $normal{ $key } = $block->{ $key };
466             }
467             }
468            
469             my %properties;
470             my @outputters;
471            
472             if ( $self->has_plugins() ) {
473             map { push @outputters, "${_}::output" }
474             $self->plugins();
475             }
476             map { push @outputters, "CSS::Prepare::Property::${_}::output" }
477             @MODULES;
478            
479             foreach my $outputter ( @outputters ) {
480             my( @normal, @important );
481            
482             eval {
483             no strict 'refs';
484             @normal = &$outputter( $self, \%normal );
485             @important = &$outputter( $self, \%important );
486             };
487             say STDERR $@ if $@;
488            
489             foreach my $property ( @normal ) {
490             $properties{ $property } = 1
491             if defined $property;
492             }
493             foreach my $property ( @important ) {
494             if ( defined $property ) {
495             my $prefix = $self->output_separator;
496             $property =~ s{;$}{${prefix}!important;};
497             $properties{ $property } = 1;
498             }
499             }
500             }
501            
502             return %properties;
503             }
504             sub output_format {
505             my $self = shift;
506            
507             return $self->pretty_output
508             ? $pretty_format
509             : $concise_format;
510             }
511             sub output_separator {
512             my $self = shift;
513            
514             return $self->pretty_output
515             ? $pretty_separator
516             : $concise_separator;
517             }
518              
519             sub fetch_file {
520             my $self = shift;
521             my $file = shift;
522             my $location = shift;
523            
524             my $target = $self->canonicalise_location( $file, $location );
525            
526             return $self->read_url( $target )
527             if $target =~ RE_IS_URL;
528             return $self->read_file( $target );
529             }
530             sub read_file {
531             my $self = shift;
532             my $file = shift;
533            
534             my $handle = FileHandle->new( $file );
535             if ( defined $handle ) {
536             local $/;
537            
538             return <$handle>;
539             }
540            
541             return;
542             }
543             sub read_url {
544             my $self = shift;
545             my $url = shift;
546            
547             my $provider = $self->get_http_provider();
548             given ( $provider ) {
549             when ( 'lite' ) { return $self->get_url_lite( $url ); }
550             when ( 'lwp' ) { return $self->get_url_lwp( $url ); }
551             }
552            
553             return;
554             }
555             sub get_url_lite {
556             my $self = shift;
557             my $url = shift;
558             my $depth = shift // 1;
559            
560             # don't follow infinite redirections
561             return unless $depth <= MAX_REDIRECT;
562            
563             my $http = HTTP::Lite->new();
564             $http->{'timeout'} = $self->get_http_timeout;
565            
566             my $code = $http->request( $url );
567            
568             given ( $code ) {
569             when ( 200 ) { return $http->body(); }
570             when ( 301 || 302 || 303 || 307 ) {
571             my $location = $http->get_header( 'Location' );
572             return $self->get_url_lite( $location, $depth+1 );
573             }
574             default { return; }
575             }
576             }
577             sub get_url_lwp {
578             my $self = shift;
579             my $url = shift;
580            
581             my $http = LWP::UserAgent->new( max_redirect => MAX_REDIRECT );
582             $http->timeout( $self->get_http_timeout );
583            
584             my $resp = $http->get( $url );
585             my $code = $resp->code();
586            
587             given ( $code ) {
588             when ( 200 ) { return $resp->decoded_content(); }
589             default { return; }
590             }
591             }
592             sub copy_file_to_staging {
593             my $self = shift;
594             my $file = shift;
595             my $location = $self->location()
596             // shift;
597            
598             return unless $self->assets_output;
599             my $content = $self->fetch_file( $file, $location );
600             return unless $content;
601            
602             my $hex = sha1_hex $content;
603             my $filename = basename $file;
604             my $assets_file = sprintf "%s/%s/%s-%s",
605             $self->{'assets_base'},
606             substr( $hex, 0, 3 ),
607             substr( $hex, 4 ),
608             $filename;
609             my $output_file = sprintf "%s/%s/%s-%s",
610             $self->{'assets_output'},
611             substr( $hex, 0, 3 ),
612             substr( $hex, 4 ),
613             $filename;
614             my $output_dir = dirname $output_file;
615            
616             mkpath $output_dir;
617             my $handle = FileHandle->new( $output_file, 'w' );
618             print {$handle} $content;
619            
620             return $assets_file;
621             }
622              
623             sub parse {
624             my $self = shift;
625             my $string = shift;
626             my $location = shift;
627            
628             return unless defined $string;
629            
630             my( $charset, $stripped ) = strip_charset( $string );
631             return { errors => [{ fatal => "Unsupported charset '${charset}'" }] }
632             unless 'UTF-8' eq $charset;
633            
634             $stripped = $self->strip_comments( $stripped );
635             $string = escape_braces_in_strings( $stripped );
636            
637             my @split = $self->split_into_statements( $string, $location );
638            
639             my @statements;
640             my @appended;
641             foreach my $statement ( @split ) {
642             my $type = $statement->{'type'};
643            
644             if ( 'appended' eq $type ) {
645             push @statements, @{$statement->{'content'}};
646             }
647             elsif ( 'import' eq $type ) {
648             push @statements, $statement;
649             }
650             elsif ( 'rulesets' eq $type ) {
651             my ( $rule_sets, $appended ) = $self->parse_rule_sets(
652             $statement->{'content'}, $location
653             );
654            
655             push @statements, @$rule_sets;
656             push @split, {
657             type => 'appended',
658             content => $appended,
659             }
660             if defined $appended;
661             }
662             elsif ( 'at-media' eq $type ) {
663             my ( $rule_sets, $appended ) = $self->parse_rule_sets(
664             $statement->{'content'}, $location
665             );
666            
667             push @{$statement->{'blocks'}}, @$rule_sets;
668             delete $statement->{'content'};
669             push @statements, $statement;
670             push @split, {
671             type => 'appended',
672             content => $appended,
673             }
674             if defined $appended;
675             }
676             else {
677             die "unknown type '${type}'";
678             }
679             }
680            
681             return @statements;
682             }
683             sub parse_rule_sets {
684             my $self = shift;
685             my $styles = shift;
686             my $location = shift;
687            
688             return []
689             unless defined $styles;
690            
691             my @declaration_blocks
692             = split_into_declaration_blocks( $styles );
693            
694             my @rule_sets;
695             my @appended;
696             foreach my $block ( @declaration_blocks ) {
697             my $type = $block->{'type'} // '';
698             my $preserve_as_is = defined $block->{'errors'}
699             || 'verbatim' eq $type
700             || 'boundary' eq $type;
701            
702             if ( $preserve_as_is ) {
703             push @rule_sets, $block;
704             }
705             else {
706             # extract from the string a data structure of selectors
707             my( $selectors, $selectors_errors )
708             = parse_selectors( $block->{'selector'} );
709            
710             my $declarations = {};
711             my $declaration_errors = [];
712             my $append_blocks;
713            
714             # CSS2.1 4.1.6: "the whole statement should be ignored if
715             # there is an error anywhere in the selector"
716             if ( ! @$selectors_errors ) {
717             # extract from the string a data structure of
718             # declarations and their properties
719             ( $declarations, $declaration_errors, $append_blocks )
720             = $self->parse_declaration_block(
721             $block->{'block'}, $location, $selectors
722             );
723             }
724            
725             push @appended, @$append_blocks
726             if defined @$append_blocks;
727            
728             my $is_empty = !@$selectors_errors
729             && !@$declaration_errors
730             && !%{$declarations};
731            
732             push @rule_sets, {
733             original => unescape_braces( $block->{'block'} ),
734             selectors => $selectors,
735             errors => [
736             @$selectors_errors,
737             @$declaration_errors
738             ],
739             block => $declarations,
740             }
741             unless $is_empty;
742             }
743             }
744            
745             return \@rule_sets, \@appended;
746             }
747             sub strip_charset {
748             my $string = shift;
749            
750             # "User agents must support at least the UTF-8 encoding."
751             my $charset = "UTF-8";
752            
753             # "Authors using an @charset rule must place the rule at the very beginning
754             # of the style sheet, preceded by no characters"
755             if ( $string =~ s{^ \@charset \s " ([^"]+) "; }{}sx ) {
756             $charset = $1;
757             }
758            
759             return ( $charset, $string );
760             }
761             sub strip_comments {
762             my $self = shift;
763             my $string = shift;
764            
765             # remove CDO/CDC markers
766             $string =~ s{ }{}gsx;
768            
769             if ( $self->support_extended_syntax ) {
770             # remove line-level comments
771             $string =~ s{ (?: ^ | \s ) // [^\n]* $ }{}gmx;
772             }
773            
774             # disguise verbatim comments
775             $string =~ s{
776             \/ \* \! ( .*? ) \* \/
777             }{%-COMMENT-%$1%-ENDCOMMENT-%}gsx;
778            
779             # disguise boundary markers
780             $string =~ s{
781             \/ \* ( \s+ \-\-+ \s+ ) \* \/
782             }{%-COMMENT-%$1%-ENDCOMMENT-%}gsx;
783            
784             # remove CSS comments
785             $string =~ s{ \/ \* .*? \* \/ }{}gsx;
786            
787             # reveal verbatim comments and boundary markers
788             $string =~ s{%-COMMENT-%}{/*}gsx;
789             $string =~ s{%-ENDCOMMENT-%}{*/}gsx;
790            
791             return $string;
792             }
793             sub escape_braces_in_strings {
794             my $string = shift;
795            
796             my $strip_next_string = qr{
797             ^
798             ( .*? ) # $1: everything before the string
799             ( ['"] ) # $2: the string delimiter
800             ( .*? ) # $3: the content of the string
801             (?
802             }sx;
803            
804             # find all strings, and tokenise the braces within
805             my $return;
806             while ( $string =~ s{$strip_next_string}{}sx ) {
807             my $before = $1;
808             my $delim = $2;
809             my $content = $3;
810            
811             $content =~ s{ \{ }{\%-LEFTBRACE-\%}gsx;
812             $content =~ s{ \} }{\%-RIGHTBRACE-\%}gsx;
813             $return .= "${before}${delim}${content}${delim}";
814             }
815             $return .= $string;
816            
817             return $return;
818             }
819             sub unescape_braces {
820             my $string = shift;
821            
822             $string =~ s{\%-LEFTBRACE-\%}{\{}gs;
823             $string =~ s{\%-RIGHTBRACE-\%}{\}}gs;
824            
825             return $string;
826             }
827             sub split_into_statements {
828             my $self = shift;
829             my $string = shift;
830             my $location = shift;
831            
832             # "In CSS 2.1, any @import rules must precede all other rules (except the
833             # @charset rule, if present)." (CSS 2.1 #6.3)
834             my ( $remainder, @statements )
835             = $self->do_import_rules( $string, $location );
836            
837             my $splitter = qr{
838             ^
839             ( .*? ) # $1: everything before the media block
840             \@media \s+ ( # $2: the media query
841             $grammar_media_query_list
842             ) \s*
843             ( # $3: (used in the nested expression)
844             \{ (?: # the content of the media block,
845             (?> [^\{\}]+ ) # which is a nested recursive match...
846             | #
847             (?3) # ...triggered here ("(?3)" means use $3
848             )* \} # matching again)
849             )
850             }sx;
851            
852             while ( $remainder =~ s{$splitter}{}sx ) {
853             my $before = $1;
854             my $query = $2;
855             my $block = $3;
856            
857             # strip the outer braces from the media block
858             $block =~ s{^ \{ (.*) \} $}{$1}sx;
859            
860             push @statements, {
861             type => 'rulesets',
862             content => $before,
863             };
864             push @statements, {
865             type => 'at-media',
866             query => $query,
867             content => $block,
868             };
869             }
870            
871             push @statements, {
872             type => 'rulesets',
873             content => $remainder,
874             };
875            
876             return @statements;
877             }
878             sub do_import_rules {
879             my $self = shift;
880             my $string = shift;
881             my $directory = shift;
882            
883             # "In CSS 2.1, any @import rules must precede all other rules (except the
884             # @charset rule, if present)." (CSS 2.1 #6.3)
885             my $splitter = qr{
886             ^
887             \s* \@import \s*
888             (
889             $string_value
890             |
891             $url_value
892             )
893             (?:
894             \s+
895             ( $media_types_value )
896             )?
897             \s* \;
898             }x;
899            
900             my @blocks;
901             while ( $string =~ s{$splitter}{}sx ) {
902             my $import = $1;
903             my $media = $2;
904            
905             $import =~ s{^ url\( \s* (.*?) \) $}{$1}x; # strip url()
906             $import =~ s{^ ( ['"] ) (.*?) \1 $}{$2}x; # strip quotes
907            
908             my @styles = $self->parse_stylesheet( $import, $directory );
909            
910             if ( @styles ) {
911             push @blocks, {
912             type => 'import',
913             query => $media,
914             blocks => [ @styles ],
915             };
916             }
917             }
918            
919             return $string, @blocks;
920             }
921             sub split_into_declaration_blocks {
922             my $string = shift;
923             my @blocks;
924            
925             my $get_import_rule = qr{
926             ^
927             \@import \s+
928             (?: $string_value | $url_value )
929             (?: \s+ ( $media_types_value ) )?
930             \s* \; \s*
931             }x;
932             my $get_charset_rule = qr{
933             ^
934             \@charset \s \" [^"]+ \";
935             \s*
936             }x;
937             my $get_block = qr{
938             ^
939             (? .*? ) \s*
940             \{ (? [^\}]* ) \} \s*
941             }sx;
942             my $get_comment = qr{
943             ^
944             ( \/ \* (.*?) \* \/ ) \s*
945             }sx;
946             my $get_verbatim = qr{
947             ^
948             \/ \* \s+ verbatim \s+ \*\/
949             \s* ( .*? ) \s*
950             \/ \* \s+ \-\-+ \s+ \*\/
951             }sx;
952             my $get_chunk_boundary = qr{
953             ^
954             \/ \* \s+ \-\-+ \s+ \* \/ \s*
955             }sx;
956            
957             while ( $string ) {
958             $string =~ s{^\s*}{}sx;
959            
960             # check for a rogue @import rule
961             if ( $string =~ s{$get_import_rule}{}sx ) {
962             push @blocks, {
963             errors => [
964             {
965             error => '@import rule after statement(s) -- '
966             . 'ignored (CSS 2.1 #4.1.5)',
967             },
968             ],
969             };
970             }
971            
972             # check for a rogue @charset rule
973             elsif ( $string =~ s{$get_charset_rule}{}sx ) {
974             push @blocks, {
975             errors => [
976             {
977             error => '@charset rule inside stylsheet -- '
978             . 'ignored (CSS 2.1 #4.4)',
979             },
980             ],
981             };
982             }
983            
984             # check for chunk boundaries
985             elsif ( $string =~ s{$get_chunk_boundary}{}sx ) {
986             push @blocks, {
987             type => 'boundary',
988             };
989             }
990            
991             # check for verbatim blocks
992             elsif ( $string =~ s{$get_verbatim}{}sx ) {
993             push @blocks, {
994             type => 'verbatim',
995             string => "$1\n",
996             };
997             }
998            
999             # check for verbatim comments
1000             elsif ( $string =~ s{$get_comment}{}sx ) {
1001             push @blocks, {
1002             type => 'verbatim',
1003             string => "$1\n",
1004             };
1005             }
1006            
1007             # try and find the next declaration
1008             elsif ( $string =~ s{$get_block}{}sx ) {
1009             my %match = %+;
1010             push @blocks, \%match;
1011             }
1012            
1013             # give up
1014             elsif ( $string ) {
1015             push @blocks, {
1016             errors => [{
1017             error => "Unknown content:\n${string}\n",
1018             }],
1019             };
1020             $string = undef;
1021             }
1022             }
1023            
1024             return @blocks;
1025             }
1026             sub parse_selectors {
1027             my $string = shift;
1028             my @selectors;
1029            
1030             my $splitter = qr{
1031             ^
1032             \s*
1033             ( [^,]+ )
1034             \s*
1035             \,?
1036             }sx;
1037            
1038             while ( $string =~ s{$splitter}{}sx ) {
1039             my $selector = $1;
1040             $selector =~ s{\s+}{ }sg;
1041            
1042             # CSS2.1 4.1.6: "the whole statement should be ignored if
1043             # there is an error anywhere in the selector"
1044             if ( ! is_valid_selector( $selector ) ) {
1045             return [], [
1046             {
1047             error => 'ignored block - unknown selector'
1048             . " '${selector}' (CSS 2.1 #4.1.7)",
1049             }
1050             ];
1051             }
1052             else {
1053             push @selectors, $selector;
1054             }
1055             }
1056            
1057             return \@selectors, [];
1058             }
1059             sub parse_declaration_block {
1060             my $self = shift;
1061             my $block = shift;
1062             my $location = shift;
1063             my $selectors = shift;
1064            
1065             # make '{' and '}' back into actual brackets again
1066             $block = unescape_braces( $block );
1067            
1068             my( $remainder, @declarations ) = get_declarations_from_block( $block );
1069             my( $declarations, $append_blocks )
1070             = $self->expand_declarations( \@declarations, $selectors );
1071            
1072             my %canonical;
1073             my @errors;
1074            
1075             DECLARATION:
1076             foreach my $declaration ( @$declarations ) {
1077             my %match = %{$declaration};
1078            
1079             my $star_hack = 0;
1080             my $underscore_hack = 0;
1081             my $important = 0;
1082             my $has_hack = 0;
1083            
1084             if ( $self->support_hacks ) {
1085             $star_hack = 1
1086             if $match{'property'} =~ s{^\*}{};
1087             $underscore_hack = 1
1088             if $match{'property'} =~ s{^_}{};
1089             $has_hack = $star_hack || $underscore_hack;
1090             }
1091            
1092             $important = 1
1093             if $match{'value'} =~ s{ \! \s* important $}{}x;
1094            
1095             # strip possible extraneous whitespace
1096             $match{'value'} =~ s{ \s+ $}{}x;
1097            
1098             my( $parsed_as, $errors )
1099             = $self->parse_declaration( $has_hack, $location, %match );
1100            
1101             if ( defined $parsed_as or $errors ) {
1102             push @errors, @$errors
1103             if @$errors;
1104            
1105             my %parsed;
1106             foreach my $property ( keys %$parsed_as ) {
1107             my $value = $parsed_as->{ $property };
1108             $property = "_$property"
1109             if $underscore_hack;
1110             $property = "*$property"
1111             if $star_hack;
1112             $property = "important-$property"
1113             if $important;
1114            
1115             $parsed{ $property } = $value;
1116             }
1117            
1118             if ( %parsed ) {
1119             %canonical = (
1120             %canonical,
1121             %parsed,
1122             );
1123             }
1124             }
1125             else {
1126             push @errors, {
1127             error => "invalid property: '$match{'property'}'"
1128             };
1129             }
1130             }
1131             if ( $remainder !~ m{^ \s* $}sx ) {
1132             $remainder =~ s{^ \s* (.*?) \s* $}{$1}sx;
1133            
1134             push @errors, {
1135             error => "invalid property: '$remainder'",
1136             };
1137             }
1138            
1139             return \%canonical, \@errors, \@$append_blocks;
1140             }
1141             sub get_declarations_from_block {
1142             my $block = shift;
1143            
1144             my $splitter = qr{
1145             ^
1146             \s*
1147             (? [^:]+? )
1148             \s* \: \s*
1149             (? [^;]+ )
1150             \;?
1151             }sx;
1152            
1153             my @declarations;
1154             while ( $block =~ s{$splitter}{}sx ) {
1155             my %match = %+;
1156             push @declarations, \%match;
1157             }
1158            
1159             return $block, @declarations;
1160             }
1161             sub parse_declaration {
1162             my $self = shift;
1163             my $has_hack = shift;
1164             my $location = shift;
1165             my %declaration = @_;
1166            
1167             my @parsers;
1168             map { push @parsers, "CSS::Prepare::Property::${_}::parse" }
1169             @MODULES;
1170             if ( $self->has_plugins() ) {
1171             map { push @parsers, "${_}::parse" }
1172             $self->plugins();
1173             }
1174            
1175             PARSER:
1176             foreach my $module ( @parsers ) {
1177             my $parsed_as;
1178             my $errors;
1179            
1180             eval {
1181             no strict 'refs';
1182            
1183             ( $parsed_as, $errors )
1184             = &$module( $self, $has_hack, $location, %declaration );
1185             };
1186             say STDERR $@ if $@;
1187            
1188             next PARSER
1189             unless ( defined $parsed_as || defined $errors );
1190            
1191             return( $parsed_as, $errors )
1192             if %$parsed_as or @$errors;
1193             }
1194            
1195             return;
1196             }
1197             sub expand_declarations {
1198             my $self = shift;
1199             my $declarations = shift;
1200             my $selectors = shift;
1201            
1202             my @filtered;
1203             my @append;
1204             if ( $self->has_plugins ) {
1205             DECLARATION:
1206             foreach my $declaration ( @$declarations ) {
1207             my $property = $declaration->{'property'};
1208             my $value = $declaration->{'value'};
1209            
1210             PLUGIN:
1211             foreach my $plugin ( $self->plugins() ) {
1212             no strict 'refs';
1213            
1214             my $try_with = "${plugin}::expand";
1215             my( $filtered, $new_rulesets, $new_chunks )
1216             = &$try_with( $self, $property, $value, $selectors );
1217            
1218             push @filtered, @{$filtered}
1219             if defined $filtered;
1220             push @$declarations, @$new_rulesets
1221             if defined $new_rulesets;
1222             push @append, @$new_chunks
1223             if defined $new_chunks;
1224            
1225             next DECLARATION
1226             if defined $filtered;
1227             }
1228            
1229             # if we get here, no plugin has dealt with the property
1230             push @filtered, $declaration;
1231             }
1232             }
1233             else {
1234             @filtered = @$declarations;
1235             }
1236            
1237             return \@filtered, \@append;
1238             }
1239              
1240             sub optimise {
1241             my $self = shift;
1242             my @data = @_;
1243            
1244             my @blocks;
1245             my @complete;
1246             while ( my $block = shift @data ) {
1247             my $type = $block->{'type'};
1248             my $query = $block->{'query'};
1249            
1250             if ( defined $type ) {
1251             # process any previously collected blocks
1252             my( $savings, @optimised ) = $self->optimise_blocks( @blocks );
1253             undef @blocks;
1254             push @complete, @optimised
1255             if @optimised;
1256            
1257             # process this block
1258             ( $savings, @optimised )
1259             = $self->optimise_blocks( @{$block->{'blocks'}} );
1260            
1261             my $output_as_block = 'at-media' eq $type
1262             || ( 'import' eq $type && defined $query );
1263             if ( $output_as_block ) {
1264             push @complete, {
1265             type => $type,
1266             query => $block->{'query'},
1267             blocks => [ @optimised ],
1268             };
1269             }
1270             elsif ( 'verbatim' eq $type ) {
1271             push @complete, $block;
1272             }
1273             elsif ( 'boundary' eq $type ) {
1274             # nothing extra to do
1275             }
1276             else {
1277             push @complete, @optimised
1278             if @optimised;
1279             }
1280             }
1281             else {
1282             # collect block for later processing
1283             push @blocks, $block;
1284             }
1285             }
1286            
1287             # process any remaining collected blocks
1288             my( $savings, @optimised ) = $self->optimise_blocks( @blocks );
1289             push @complete, @optimised
1290             if @optimised;
1291            
1292             return @complete;
1293             }
1294             sub optimise_blocks {
1295             my $self = shift;
1296             my @blocks = @_;
1297            
1298             return 0 unless @blocks;
1299            
1300             my %styles = $self->sort_blocks_into_hash( @blocks );
1301             my @properties = $self->array_of_properties( %styles );
1302             # my $before = output_as_string( @properties );
1303            
1304             my $declaration_count = scalar @properties;
1305             $self->status( " Found ${declaration_count} declarations." );
1306            
1307             my ( $savings, %state ) = $self->get_optimal_state( @properties );
1308            
1309             my @optimised = $self->get_blocks_from_state( %state );
1310             # my $after = output_as_string( @optimised );
1311             # my $savings = length( $before ) - length( $after );
1312             #
1313             # say STDERR "Saved $savings bytes.";
1314             # # TODO - calculate savings, even when suboptimal has been used
1315             # my $savings = 0;
1316            
1317             return( $savings, @optimised );
1318             }
1319             sub sort_blocks_into_hash {
1320             my $self = shift;
1321             my @data = @_;
1322            
1323             my %styles;
1324             foreach my $block ( @data ) {
1325             foreach my $property ( keys %{ $block->{'block'} } ) {
1326             my $value = $block->{'block'}{ $property };
1327            
1328             foreach my $selector ( @{ $block->{'selectors'} } ) {
1329             $styles{ $selector }{ $property } = $value;
1330             }
1331             }
1332             }
1333            
1334             return %styles;
1335             }
1336             sub array_of_properties {
1337             my $self = shift;
1338             my %styles = @_;
1339            
1340             my @properties;
1341             foreach my $selector ( keys %styles ) {
1342             my %properties = $self->output_properties( $styles{ $selector } );
1343            
1344             foreach my $property ( keys %properties ) {
1345             push @properties, $selector, $property;
1346             }
1347             }
1348            
1349             return @properties;
1350             }
1351             sub get_suboptimal_state {
1352             my %by_property = @_;
1353            
1354             # combine all properties by their selector -- makes a "margin:0;" property
1355             # with an "li" selector and a "padding:0;" property with an "li" selector
1356             # into an "li" selector with both "margin:0;" and "padding:0;" properties
1357             my %by_selector;
1358             foreach my $property ( keys %by_property ) {
1359             foreach my $selector ( keys %{$by_property{ $property }} ) {
1360             $by_selector{ $selector }{ $property } = 1;
1361             }
1362             }
1363            
1364             # combine selectors by shared properties -- makes a "div" and an "li"
1365             # which both have "margin:0;" and "padding:0;" properties into a
1366             # "margin:0;padding:0;" property with a "div" and "li" selector
1367             undef %by_property;
1368             foreach my $selector ( sort keys %by_selector ) {
1369             my $properties = join '', sort keys %{$by_selector{ $selector }};
1370            
1371             $by_property{ $properties }{ $selector } = 1;
1372             }
1373            
1374             return %by_property;
1375             }
1376             sub get_optimal_state {
1377             my $self = shift;
1378             my @properties = @_;
1379            
1380             my %by_property = get_selectors_by_property( @properties );
1381             my $found_savings = 1;
1382             my $total_savings = 0;
1383            
1384             # if only one thing has that property, the only thing it can be
1385             # successfully combined with is something with the same selector,
1386             # so don't even bother calculating possible savings on them, just
1387             # combine them at the end
1388             my( %multiples, %singles );
1389             foreach my $property ( keys %by_property ) {
1390             my $selectors = scalar keys %{$by_property{ $property }};
1391            
1392             if ( $selectors > 1 ) {
1393             $multiples{ $property } = $by_property{ $property };
1394             }
1395             else {
1396             $singles{ $property } = $by_property{ $property };
1397             }
1398             }
1399            
1400             my $do_suboptimal_pass = 0;
1401             if ( scalar keys %multiples ) {
1402             my $start_time = time();
1403             my $time_out_after = $start_time + $self->suboptimal_threshold;
1404             my $check_for_timeout = $self->suboptimal_threshold > 0;
1405             my $count = 0;
1406             my %cache;
1407            
1408             MIX:
1409             while ( $found_savings ) {
1410             # adopt a faster strategy if there are too many properties
1411             # to deal with, or the code tends towards infinite
1412             # time taken to calculate the results
1413             my $timed_out = $check_for_timeout
1414             && ( time() >= $time_out_after );
1415            
1416             if ( $timed_out ) {
1417             $do_suboptimal_pass = 1;
1418             $self->status( "\r Time threshold reached -- switching "
1419             . 'to suboptimal optimisation.' );
1420             last MIX;
1421             }
1422            
1423             ( $found_savings, %multiples )
1424             = mix_biggest_properties( \%cache, %multiples );
1425            
1426             $total_savings += $found_savings;
1427             $count++;
1428             $self->status( " [$count] savings $total_savings", 'line' );
1429             }
1430             }
1431            
1432             %by_property = (
1433             %singles,
1434             %multiples
1435             );
1436            
1437             %by_property = get_suboptimal_state( %by_property )
1438             if $do_suboptimal_pass;
1439            
1440             return( $total_savings, %by_property);
1441             }
1442             sub get_selectors_by_property {
1443             my @properties = @_;
1444            
1445             my %by_property;
1446             while ( @properties ) {
1447             my $selector = shift @properties;
1448             my $property = shift @properties;
1449            
1450             $by_property{ $property }{ $selector } = 1;
1451             }
1452            
1453             return %by_property;
1454             }
1455             sub mix_biggest_properties {
1456             my $cache = shift;
1457             my %by_property = @_;
1458            
1459             my $num_children = sub {
1460             my $a_children = scalar keys %{$by_property{ $a }};
1461             my $b_children = scalar keys %{$by_property{ $b }};
1462             return $b_children <=> $a_children;
1463             };
1464             my @sorted_properties = sort $num_children keys %by_property;
1465            
1466             foreach my $property ( @sorted_properties ) {
1467             my( $mix_with, $saving )
1468             = get_biggest_saving_if_mixed( $property, $cache, %by_property );
1469            
1470             if ( defined $mix_with ) {
1471             my %properties
1472             = mix_properties( $property, $mix_with, $cache, %by_property );
1473             return( $saving, %properties );
1474             }
1475             }
1476            
1477             return( 0, %by_property );
1478             }
1479             sub get_biggest_saving_if_mixed {
1480             my $property = shift;
1481             my $cache = shift;
1482             my %properties = @_;
1483            
1484             return if defined $cache->{'no_savings'}{ $property };
1485            
1486             my $unmixed_property_length
1487             = output_string_length( $property, keys %{$properties{ $property }} );
1488             my $largest_value = 0;
1489             my $largest;
1490            
1491             EXAMINE:
1492             foreach my $examine ( keys %properties ) {
1493             next if $property eq $examine;
1494             next if 1 == scalar( keys %{$properties{ $examine }} );
1495            
1496             my( $a, $b ) = sort( $property, $examine );
1497             my $saving = $cache->{ $a }{ $b };
1498            
1499             if ( !defined $saving ) {
1500             my @common_selectors
1501             = get_common_selectors( $property, $examine, %properties );
1502            
1503             if ( 0 == scalar @common_selectors ) {
1504             $cache->{ $a }{ $b } = 0;
1505             next EXAMINE;
1506             }
1507            
1508             my @property_remaining
1509             = get_remaining_selectors( $examine, $property, %properties );
1510             my @examine_remaining
1511             = get_remaining_selectors( $property, $examine, %properties );
1512            
1513             my $unmixed_examine_length
1514             = output_string_length(
1515             $examine, keys %{$properties{ $examine }} );
1516             my $mixed_common_length
1517             = output_string_length(
1518             "${property},${examine}", @common_selectors );
1519             my $mixed_selector_length
1520             = output_string_length( $property, @property_remaining );
1521             my $mixed_examine_length
1522             = output_string_length( $examine, @examine_remaining );
1523            
1524             my $unmixed = $unmixed_property_length + $unmixed_examine_length;
1525             my $mixed = $mixed_common_length
1526             + $mixed_selector_length
1527             + $mixed_examine_length;
1528            
1529             $saving = $unmixed - $mixed;
1530             $cache->{ $a }{ $b } = $saving;
1531             }
1532            
1533             if ( $saving > $largest_value ) {
1534             $largest_value = $saving;
1535             $largest = $examine;
1536             }
1537             }
1538            
1539             $cache->{'no_savings'}{ $property } = 1
1540             unless $largest_value;
1541            
1542             return( $largest, $largest_value );
1543             }
1544             sub output_string_length {
1545             my $property = shift;
1546             my @selectors = @_;
1547            
1548             return 0
1549             unless scalar @selectors;
1550            
1551             my $string = sprintf '%s{%s}',
1552             join( ',', @selectors ),
1553             $property;
1554            
1555             return length $string;
1556             }
1557             sub get_common_selectors {
1558             my $property = shift;
1559             my $examine = shift;
1560             my %properties = @_;
1561            
1562             my @common = grep {
1563             $_ if defined $properties{ $property }{ $_};
1564             } keys %{$properties{ $examine }};
1565            
1566             return @common;
1567             }
1568             sub get_remaining_selectors {
1569             my $property = shift;
1570             my $examine = shift;
1571             my %properties = @_;
1572            
1573             my @remaining = grep {
1574             $_ if !defined $properties{ $property }{ $_};
1575             } keys %{$properties{ $examine }};
1576            
1577             return @remaining;
1578             }
1579             sub mix_properties {
1580             my $property = shift;
1581             my $mix_with = shift;
1582             my $cache = shift;
1583             my %properties = @_;
1584            
1585             my $mixed_property = join '', sort( $property, $mix_with );
1586             my @common_selectors
1587             = get_common_selectors( $property, $mix_with, %properties );
1588            
1589             delete $cache->{ $property };
1590             delete $cache->{'no_savings'}{ $property };
1591             delete $cache->{ $mix_with };
1592             delete $cache->{'no_savings'}{ $mix_with };
1593            
1594             foreach my $selector ( @common_selectors ) {
1595             $properties{ $mixed_property }{ $selector } = 1;
1596             delete $properties{ $property }{ $selector };
1597             delete $properties{ $mix_with }{ $selector };
1598             }
1599            
1600             delete $properties{ $property }
1601             unless scalar keys %{$properties{ $property }};
1602             delete $properties{ $mix_with }
1603             unless scalar keys %{$properties{ $mix_with }};
1604            
1605             return %properties;
1606             }
1607             sub get_blocks_from_state {
1608             my $self = shift;
1609             my %by_property = @_;
1610            
1611             my $elements_first = sub {
1612             my $a_element = ( $a =~ m{^[a-z]}i );
1613             my $b_element = ( $b =~ m{^[a-z]}i );
1614             my $element_count = $a_element + $b_element;
1615            
1616             return ( $a_element ? -1 : 1 )
1617             if 1 == $element_count;
1618             return $a cmp $b;
1619             };
1620            
1621             my %by_selector;
1622             foreach my $property ( keys %by_property ) {
1623             my @selectors
1624             = sort $elements_first keys %{$by_property{ $property }};
1625             my $selector = join ',', @selectors;
1626            
1627             $by_selector{ $selector }{ $property } = 1;
1628             }
1629            
1630             my @blocks;
1631             foreach my $selector ( sort $elements_first keys %by_selector ) {
1632             my @properties = sort keys %{$by_selector{ $selector }};
1633             my $properties = join '', @properties;
1634             my $css = "${selector}{${properties}}";
1635             push @blocks, $self->parse_string( $css )
1636             }
1637            
1638             return @blocks;
1639             }
1640              
1641             sub is_valid_selector {
1642             my $test = shift;
1643            
1644             $test = lc $test;
1645            
1646             my $nmchar = qr{ (?: [_a-z0-9-] ) }x;
1647             my $ident = qr{ -? [_a-z] $nmchar * }x;
1648             my $element = qr{ (?: $ident | \* ) }x;
1649             my $hash = qr{ \# $nmchar + }x;
1650             my $class = qr{ \. $ident }x;
1651             my $string = qr{ (?: \' $ident \' | \" $ident \" ) }x;
1652             my $pseudo = qr{
1653             \:
1654             (?:
1655             # TODO - I am deliberately ignoring FUNCTION here for now
1656             # FUNCTION \s* (?: $ident \s* )? \)
1657             $ident \( .* \)
1658             |
1659             $ident
1660             )
1661             }x;
1662             my $attrib = qr{
1663             \[
1664             \s* $ident \s*
1665             (?:
1666             (?: \= | \~\= | \|\= ) \s*
1667             (?: $ident | $string ) \s*
1668             )?
1669             \s*
1670             \]
1671             }x;
1672             my $parts = qr{ (?: $pseudo | $hash | $class | $attrib ) }x;
1673             my $simple_selector = qr{ (?: $element $parts * | $parts + ) }x;
1674             my $combinator = qr{ (?: [\+\~] \s* | \> \s* ) }x;
1675             my $next_selector = qr{
1676             \s* (?: $combinator )? $simple_selector \s*
1677             }x;
1678            
1679             while ( $test =~ s{^ $next_selector }{}x ) {
1680             # do nothing, already validated by the regexp
1681             }
1682            
1683             return 0 if length $test;
1684             return 1;
1685             }
1686              
1687             sub status {
1688             my $self = shift;
1689             my $text = shift;
1690             my $temp = shift;
1691            
1692             no strict 'refs';
1693            
1694             my $status = $self->{'status'};
1695             &$status( $text, $temp );
1696             }
1697             sub status_to_stderr {
1698             my $text = shift;
1699             my $temp = shift;
1700            
1701             print STDERR ( $temp ? "\r" : '' ) . $text;
1702             }
1703              
1704             1;
1705              
1706             __END__