File Coverage

blib/lib/CSS/Tidy.pm
Criterion Covered Total %
statement 80 110 72.7
branch 13 22 59.0
condition 3 3 100.0
subroutine 11 13 84.6
pod 2 7 28.5
total 109 155 70.3


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