File Coverage

blib/lib/CSS/Tidy.pm
Criterion Covered Total %
statement 83 111 74.7
branch 11 20 55.0
condition 3 3 100.0
subroutine 11 13 84.6
pod 2 7 28.5
total 110 154 71.4


line stmt bran cond sub pod time code
1             package CSS::Tidy;
2 1     1   55898 use warnings;
  1         2  
  1         26  
3 1     1   5 use strict;
  1         1  
  1         15  
4 1     1   4 use Carp;
  1         1  
  1         39  
5 1     1   4 use utf8;
  1         2  
  1         14  
6              
7             require Exporter;
8              
9             our @ISA = qw(Exporter);
10             our @EXPORT_OK = qw/
11             copy_css
12             tidy_css
13             /;
14              
15             our %EXPORT_TAGS = (
16             all => \@EXPORT_OK,
17             );
18              
19             our $VERSION = '0.04';
20              
21 1     1   487 use C::Tokenize '$comment_re';
  1         3647  
  1         120  
22 1     1   370 use File::Slurper qw!read_text write_text!;
  1         11104  
  1         1155  
23              
24             sub copy_css
25             {
26 0     0 1 0 my (%options) = @_;
27 0         0 my $in = get_option (\%options, 'in');
28 0 0       0 if (! $in) {
29 0         0 carp "Specify the input css file with in => 'file'";
30 0         0 return;
31             }
32 0         0 my $out = get_option (\%options, 'out');
33 0 0       0 if (! $out) {
34 0         0 carp "Specify the output css file with out => 'file'";
35 0         0 return;
36             }
37 0         0 my $css = read_text ($in);
38 0         0 $css = tidy_css ($css, %options);
39 0         0 write_text ($out, $css);
40             }
41              
42             sub get_option
43             {
44 16     16 0 25 my ($o, $name) = @_;
45 16         19 my $value = $o->{$name};
46 16         19 delete $o->{$name};
47 16         18 return $value;
48             }
49              
50             sub check_options
51             {
52 8     8 0 9 my ($o) = @_;
53 8         14 my @k = keys %$o;
54 8         13 for my $k (@k) {
55 0         0 carp "Unknown option $k";
56 0         0 delete $o->{$k};
57             }
58             }
59              
60             sub tidy_css
61             {
62 8     8 1 2985 my ($text, %options) = @_;
63              
64 8         16 my $decomment = get_option (\%options, 'decomment');
65             #my $verbose =
66             # Discard this at the moment, we have no verbose output yet.
67 8         14 get_option (\%options, 'verbose');
68 8         15 check_options (\%options);
69              
70             # Store for comments during processing. They are then restored.
71 8         10 my $comments;
72 8 50       11 if ($decomment) {
73 0         0 $text = rm_comments ($text);
74             }
75             else {
76 8         14 ($text, $comments) = strip_comments ($text);
77             }
78 8         85 $text =~ s!(\{|\}|;)(\s*\S)!$1\n$2!g;
79 8         78 $text =~ s!(\S\s*)(\})!$1\n$2!g;
80 8         17 $text =~ s!(\S)(\{)!$1 $2!g;
81 8         91 $text =~ s!(\S*)(\})!$1\n$2!g;
82              
83 8         39 my @lines = split /\n/, $text;
84              
85 8         11 my @tidy;
86              
87             # Line number, but this could be wrong due to comment removal.
88             my $i;
89              
90             # Depth of nested { }
91 8         14 my $depth = 0;
92              
93 8         13 for (@lines) {
94 74         95 $i++;
95             # {} on the same line.
96              
97             # It would be better to deal with these beforehand.
98              
99             # If done before processing it will break the line numbers.
100              
101             # Need to also add then remove line number fake information.
102              
103 74 50       107 if (/^\s*(.*)\{(.*?)\}(.*)$/) {
104 0         0 my ($before, $between, $after) = ($1, $2, $3);
105 0         0 my $indent = ' ' x ($depth + 1);
106 0         0 push @tidy, "$indent$before {";
107 0         0 my @between = split /;\s*/, $between;
108 0         0 for my $b (@between) {
109 0         0 push @tidy, "$indent $b";
110             }
111 0         0 push @tidy, "$indent}";
112 0         0 push @tidy, "$indent$after";
113 0         0 next;
114             }
115 74         110 while (/\}/g) {
116 10         11 $depth--;
117 10 50       17 if ($depth < 0) {
118 0         0 warn "$i: depth = $depth\n";
119             }
120             }
121 74         76 my $initial = '';
122 74 50       152 if (/^(\s*)/) {
123 74         87 $initial = $1;
124             }
125 74 100       118 if (length ($initial) != $depth * 4) {
126 45         193 s/^$initial/' ' x $depth/e;
  45         89  
127             }
128             # If not a CSS pseudoclass or pseudoelement
129 74 100 100     227 if (! /(?:\.|#)\w+.*?:/ && ! /^\s*:+/) {
130             # Insert a space after a colon
131 69         96 s/([^:]):([^:\s])/$1: $2/;
132             }
133 74         146 s/\s+$//;
134 74         113 push @tidy, $_;
135 74 100       123 if (/\{/) {
136 10         14 $depth++;
137             }
138             }
139              
140 8         21 my $out = join ("\n", @tidy);
141             # Reduce multiple blank lines to a single one.
142 8         45 $out =~ s/\n+/\n/g;
143             # Add a blank after }
144 8         45 $out =~ s/^(\s*\})/$1\n/gsm;
145              
146 8         15 $out =~ s/^\}\n(\S)/\}\n\n$1/gsm;
147             # Remove a blank line before }. This also tidies up the
148             # aftereffects of the above regex, which puts too many blank
149             # lines.
150 8         17 $out =~ s/\n\n(\s*\})/\n$1/g;
151              
152             # Add a semicolon after the final CSS instruction if there is not
153             # one.
154              
155 8         69 $out =~ s!([^\};])\s*(\n\s*\})!$1;$2!g;
156              
157 8 50       16 if (! $decomment) {
158 8         16 $out = restore_comments ($out, $comments);
159             }
160             # Add a blank line after comments.
161 8         15 $out =~ s!(\*/)!$1\n!g;
162 8         52 return $out;
163             }
164              
165             # Completely remove all the comments.
166              
167             sub rm_comments
168             {
169 0     0 0 0 my ($text) = @_;
170 0         0 $text =~ s!$comment_re!!sm;
171 0         0 return $text;
172             }
173              
174             my $string_re = qr!"(\\"|[^"])*"!;
175              
176             # Strip the comments out in such a way that they can be restored.
177              
178             sub strip_comments
179             {
180 8     8 0 12 my ($text) = @_;
181              
182             # Remove and store all strings so that "http://example.com"
183             # doesn't get turned into a comment.
184              
185 8         8 my @strings;
186 8         9 my $s = 0;
187 8         68 while ($text =~ s!($string_re)!\@\@ string_#$s \@\@!sm) {
188 1         3 $s++;
189 1         6 push @strings, $1;
190             }
191              
192             # Remove and store comments.
193              
194 8         9 my @comments;
195 8         9 my $n = 0;
196 8         111 while ($text =~ s!($comment_re)!/\@ css_tidy_#$n \@/!sm) {
197 1         3 $n++;
198 1         7 push @comments, $1;
199             }
200              
201             # Restore the strings.
202              
203 8         38 $text =~ s!\@\@ string_#([0-9]+) \@\@!$strings[$1]!g;
204              
205 8         23 return ($text, \@comments);
206             }
207              
208             sub restore_comments
209             {
210 8     8 0 15 my ($text, $comments) = @_;
211 8         15 $text =~ s!/\@ css_tidy_#([0-9]+) \@/!$comments->[$1]!g;
212 8         27 return $text;
213             }
214              
215             1;