File Coverage

blib/lib/Crayon.pm
Criterion Covered Total %
statement 212 224 94.6
branch 60 72 83.3
condition 26 40 65.0
subroutine 26 28 92.8
pod 5 9 55.5
total 329 373 88.2


line stmt bran cond sub pod time code
1             package Crayon;
2             our $VERSION = '1.05';
3 9     9   1242454 use 5.006;
  9         36  
4 9     9   58 use strict;
  9         19  
  9         398  
5 9     9   79 use warnings;
  9         51  
  9         759  
6 9     9   5657 use Struct::Match qw/match/;
  9         242719  
  9         96  
7 9     9   6066 use Colouring::In;
  9         196797  
  9         67  
8 9     9   5430 use Blessed::Merge;
  9         62906  
  9         6615  
9              
10             our ($LINES, $GLOBAL, $NESTED_GLOBAL, $NESTED_VARIABLE, $VARIABLE, $COMMENT, $CI);
11             BEGIN {
12 9     9   98 $LINES = qr{ ([\{]( (?: (?> [^\{\}]+ ) | (??{ $LINES }) )*) [\}]) }x;
13 9         31 $GLOBAL = qr{ (\$([^:\n]+)\:([^;\n]+);) }x;
14 9         48 $VARIABLE = qr{ (\$(.*)) }x;
15 9         87 $COMMENT = qr{ (\/\*[^*]*\*+([^/*][^*]*\*+)*\/) }x;
16 9         42 $NESTED_GLOBAL = qr{ (\%([^\:\(]+)[\:\s\(]+( (?: (?> [^\(\)]+ ) | (??{ $NESTED_GLOBAL }) )*) [\)];) }x;
17 9         35 $NESTED_VARIABLE = qr{ (\$([^\{]+)[\{]( (?: (?> [^\{\}]+ ) | (??{ $NESTED_VARIABLE }) )*) [\}]) }x;
18 9         35731 $CI = qr{ ((mix|lighten|darken|fade|fadeout|fadein|tint|shade|saturate|desaturate|greyscale)[\(]( (?: (?> [^\(\)]+ ) | (??{ $CI }) )*) [\)]) }x;
19             }
20              
21             sub new {
22 20     20 1 1905402 my ($pkg, %args) = @_;
23 20   50     180 $args{css} ||= {};
24 20   33     244 $args{bm} ||= Blessed::Merge->new();
25 20         679 return bless \%args, $pkg;
26             }
27              
28 0     0 0 0 sub css { $_[0]->{css} }
29              
30 99     99 0 603 sub bm { $_[0]->{bm} }
31              
32             sub parse {
33 19     19 1 105 my ($self, $string, $css) = @_;
34 19   33     135 $css ||= $self->{css};
35 19         72 return $self->_parse_content($self->_strip_comments($string), $css);
36             }
37              
38             sub parse_file {
39 2     2 1 14 my ($self, $file, $css) = @_;
40 2 50       50 die "Can't find the relative file: ${file}" unless -f $file;
41 2 50       89 open my $fh, '<', $file or die "cannot open file:$file $!";
42 2         5 my $string = do { local $/; <$fh> };
  2         12  
  2         213  
43 2         42 close $fh;
44 2         9 $self->parse($string, $css);
45             }
46              
47             sub parse_directory {
48 1     1 0 8 my ($self, $dir, $css) = @_;
49 1 50       39 die "Can't find a relative directory: ${dir}" unless -d $dir;
50 1 50       91 opendir my $d, $dir or die "Cannot read the directory: ${dir} $!";
51 1         45 for (sort readdir $d) {
52 4 100       17 next if $_ =~ m/^\./;
53 2 50       15 next unless $_ =~ m/css$/;
54 2         14 $self->parse_file("$dir/$_");
55             }
56 1         19 closedir $d;
57 1         6 return $self;
58             }
59              
60             sub compile {
61 13     13 1 20784 my ($self, $struct) = @_;
62 13   66     60 $struct ||= $self->{css};
63 13         56 my $flat = $self->_dedupe_struct(
64             $self->_flattern_struct($struct)
65             );
66 13 100       182 $self->{pretty} ? $self->_pretty_compile($flat) : $self->_compile($flat);
67             }
68              
69             sub compile_file {
70 0     0 1 0 my ($self, $file, $struct) = @_;
71 0         0 my $string = $self->compile($struct);
72 0 0       0 open my $fh, '>', $file or die "cannot open file:$file $!";
73 0         0 print $fh $string;
74 0         0 close $fh;
75             }
76              
77             sub _strip_comments {
78 19     19   72 my ($self, $string) = @_;
79              
80 19         191 while ($string =~ m/$COMMENT/g) {
81 6         139 $string =~ s/\Q$1\E//g;
82             }
83 19         87 return $string;
84             }
85              
86             sub _parse_globals {
87 113     113   315 my ($self, $string) = @_;
88 113         218 my %globals;
89 113         633 while ($string =~ m/$GLOBAL/g) {
90 19         63 my ($match, $class, $props) = ($1, cws($2), cws($3));
91 19 50 33     84 next unless $class && $props;
92 19         48 $globals{$class} = $props;
93 19         369 $string =~ s/\Q$match\E//;
94             }
95 113         553 while ($string =~ m/$NESTED_GLOBAL/g) {
96 7         21 my ($match, $class, $props) = ($1, cws($2), cws($3));
97 7         21 my %props = $self->_parse_props($props);
98 7         22 $globals{$class} = \%props;
99 7         137 $string =~ s/\Q$match\E//;
100             }
101              
102 113         455 return (\%globals, $string);
103             }
104              
105             sub _parse_content {
106 54     54   144 my ($self, $string, $css) = @_;
107              
108 54         131 my $globals = {};
109 54         3262 while ( $string =~ m/(([^{]+)$LINES)/g ) {
110 67         459 my ($match, $class, $props) = ($1, $2, $4);
111            
112 67         130 my $nested = {};
113 67 100       463 ($nested, $props) = $self->_parse_content($props, {})
114             if ($props =~ m/$LINES/);
115              
116 67         195 my $ri = rindex($class, ';');
117 67 100       194 if ($ri > 0) {
118 25         69 my $p = substr $class, 0, $ri + 1, '';
119 25         132 $string .= $p;
120             }
121 67 100       243 return ($css, $string) if ($class =~ m/^[^@]+:\s*\$/);
122 63         171 ($globals, $props) = $self->_parse_globals($props);
123              
124 63         222 my @classes = $self->_parse_classes($class);
125 63         220 my %props = $self->_parse_props($props);
126 63         228 for (@classes) {
127 72         605 my $current = $css;
128 72         111 for (@{$_}) {
  72         158  
129 93   100     535 $current = $current->{$_} ||= {};
130             }
131 72         171 %{$current} = %{$self->bm->merge($current, $nested, \%props)};
  72         21631  
  72         213  
132 72 100 50     291 $current->{VARIABLES} = $self->bm->merge($current->{VARIABLES} || {}, $globals) if keys %{$globals};
  72         322  
133             }
134              
135 63         7369 $string =~ s/\Q$match\E//;
136             }
137              
138 50         324 ($globals, $string) = $self->_parse_globals($string);
139 50 100       119 $css->{VARIABLES} = $globals if keys %{$globals};
  50         182  
140              
141 50         250 return ($css, $string);
142             }
143              
144             sub _parse_classes {
145 63     63   143 my ($self, $class) = @_;
146 63         203 my @parts = split /,/, $class;
147             return map {
148 63         165 my $p = $_;
  72         129  
149             [
150 72 100 33     299 $p =~ m/^\s*\@/ ? cws($p) : do { $p =~ s/\:/ &/g; 1 } && grep {$_} split /\s+/, $p
151             ]
152             } @parts
153             }
154              
155             sub _parse_props {
156 70     70   151 my ($self, $line) = @_;
157 70         120 my %props;
158 70         379 while ($line =~ m/(([^:]+)\:([^;]+);)/) {
159 121         464 my ($match, $key, $val) = (quotemeta($1), cws($2), cws($3));
160 121         390 $props{$key} = $val;
161 121         2527 $line =~ s/$match//;
162             }
163 70         206 while ($line =~ m/((\%[^;]+);)/) {
164 1         6 my ($match, $key, $val) = (quotemeta($1), cws($2), cws($3));
165 1         3 $props{$key} = 1;
166 1         20 $line =~ s/$match//;
167             }
168 70         377 return %props;
169             }
170              
171             sub _dedupe_struct {
172 14     14   36 my ($self, $struct) = @_;
173 14         25 for my $class (sort keys %{$struct}) {
  14         61  
174 34 100       173 next unless $struct->{$class};
175 24         63 my $new_class = $class;
176 24 100       72 if ($class =~ m/^\@/) {
177 1         6 $struct->{$new_class} = $self->_dedupe_struct($struct->{$class});
178             } else {
179 23         30 for my $inner (sort keys %{$struct}) {
  23         101  
180 64 100       1914 next if $class eq $inner;
181 41 100       122 if (match($struct->{$class}, $struct->{$inner})) {
182 10         1699 delete $struct->{$inner};
183 10         37 $new_class .= ", $inner";
184             }
185             }
186 23         773 $struct->{$new_class} = delete $struct->{$class};
187             }
188             }
189 14         56 return $struct;
190             }
191              
192             sub _flattern_struct {
193 57     57   146 my ($self, $struct, $key, $flat) = @_;
194 57   100     181 $key ||= '';
195 57   100     138 $flat ||= {};
196 57         98 my $scp;
197              
198 57 100       141 if ($struct->{VARIABLES}) {
199 14   100     67 $flat->{$key || 'GLOBAL'}->{VARIABLES} = delete $struct->{VARIABLES};
200 14 100       40 $scp = $flat->{$key}->{VARIABLES} if $key;
201             }
202 57         86 for my $s (keys %{$struct}) {
  57         139  
203 95 100       1417 if ( $s =~ m/^\@/ ) {
    100          
204 1         4 $flat->{$s} = $self->_flattern_struct($struct->{$s}, '', {});
205             }
206             elsif (ref $struct->{$s}) {
207 43 100       156 my $k = $key ? $s =~ m/^\&(.*)/ ? $key . ':' . $1 : $key . ' ' . $s : $s;
    100          
208 43         233 $self->_flattern_struct($struct->{$s}, $k, $flat);
209 43 100 50     143 $flat->{$k}->{VARIABLES} = $self->bm->merge($scp, $flat->{$k}->{VARIABLES} || {}) if $scp;
210             }
211             else {
212 51         164 $flat->{$key}->{$s} = $struct->{$s};
213             }
214             }
215 57         362 return $flat;
216             }
217              
218             sub _expand_nested_variables {
219 16     16   46 my ($self, $struct, $variables) = @_;
220 16         33 for my $key (keys %{$struct}) {
  16         49  
221 29 100       101 if ($key =~ m/^\%(.*)/) {
222 1         3 delete $struct->{$key};
223 1         3 $struct = $self->bm->merge($struct, $variables->{$1});
224             }
225             }
226 16         217 return $struct;
227             }
228              
229              
230             sub _compile {
231 1     1   5 my ($self, $flat) = @_;
232 1         3 my $string = '';
233 1 50       2 my %global = %{ delete $flat->{GLOBAL} || {} };
  1         9  
234 1         2 for my $class (sort keys %{$flat}) {
  1         5  
235             my $variables = $self->bm->merge(
236             $global{VARIABLES} || {},
237             delete $flat->{$class}->{VARIABLES} || {}
238 1   50     4 );
      50        
239 1         216 $string .= $class . "{";
240 1         6 $flat->{$class} = $self->_expand_nested_variables($flat->{$class}, $variables);
241 1 50       2 next unless keys %{$flat->{$class}};
  1         5  
242 1         2 for my $prop ( sort keys %{$flat->{$class}} ) {
  1         7  
243 4 50       11 if ( ref $flat->{$class}->{$prop} ) {
244 0         0 $string .= $prop . "{";
245 0         0 for my $attr ( sort keys %{$flat->{$class}->{$prop}} ) {
  0         0  
246             $string .= sprintf(
247             "%s:%s;",
248             $attr,
249             $self->_recurse_extensions(
250 0         0 $flat->{$class}->{$prop}->{$attr},
251             $variables
252             )
253             );
254             }
255 0         0 $string .= "}";
256             } else {
257             $string .= sprintf(
258             "%s:%s;",
259             $prop,
260             $self->_recurse_extensions(
261 4         14 $flat->{$class}->{$prop},
262             $variables
263             )
264             );
265             }
266             }
267 1         5 $string .= "}";
268             }
269 1         8 return $string;
270             }
271              
272             sub _pretty_compile {
273 12     12   47 my ($self, $flat) = @_;
274 12         25 my $string = '';
275 12 100       23 my %global = %{ delete $flat->{GLOBAL} || {} };
  12         129  
276 12         29 for my $class (sort keys %{$flat}) {
  12         43  
277             my $variables = $self->bm->merge(
278             $global{VARIABLES} || {},
279             delete $flat->{$class}->{VARIABLES} || {}
280 15   100     66 );
      100        
281 15         2493 $flat->{$class} = $self->_expand_nested_variables($flat->{$class}, $variables);
282 15 100       38 next unless keys %{$flat->{$class}};
  15         52  
283 13         77 $string .= join(",\n", split(", ", $class)) . " {\n";
284 13         28 for my $prop ( sort keys %{$flat->{$class}} ) {
  13         55  
285 26 100       81 if ( ref $flat->{$class}->{$prop} ) {
286 2         8 $string .= "\t" . join(",\n\t", split(", ", $prop)) . " {\n";
287 2         5 for my $attr ( sort keys %{$flat->{$class}->{$prop}} ) {
  2         8  
288             $string .= sprintf(
289             "\t\t%s: %s;\n",
290             $attr,
291 5         15 $self->_recurse_extensions($flat->{$class}->{$prop}->{$attr}, $variables)
292             );
293             }
294 2         5 $string .= "\t}\n";
295             } else {
296             $string .= sprintf(
297             "\t%s: %s;\n",
298             $prop,
299 24         72 $self->_recurse_extensions($flat->{$class}->{$prop}, $variables)
300             );
301             }
302             }
303 13         74 $string .= "}\n";
304             }
305 12         92 return $string;
306             }
307              
308             sub _recurse_extensions {
309 33     33   72 my ($self, $value, $variables) = @_;
310 33   100     414 while ($value =~ m/$NESTED_VARIABLE/g || $value =~ m/$VARIABLE/g) {
311 19         54 my ($match, $meth, $args) = ($1, cws($2), cws($3));
312 19 100       52 my $val = $args ? $variables->{$meth}->{$args} : $variables->{$meth};
313 19         413 $value =~ s/\Q$match\E/$val/;
314             }
315 33         145 while ($value =~ m/$CI/g) {
316 2         10 my ($match, $meth, $args) = ($1, $2, $3);
317 2 50       10 if ($args =~ m/$CI/) {
318 0         0 $args = $self->_recurse_extensions($args);
319             }
320 2         8 my @params = map { cws($_) } split /,/, $args;
  4         10  
321 9     9   125 no strict 'refs';
  9         35  
  9         2659  
322 2         6 my $ci = *{"Colouring::In::$meth"}->(@params)->toCSS;
  2         28  
323 2         4212 $value =~ s/\Q$match\E/$ci/;
324             }
325 33         179 return $value;
326             }
327              
328             sub cws {
329 340     340 0 722 my $string = shift;
330 340 100       2433 $string && $string =~ s/^\s*|\s*$//g;
331 340         956 return $string;
332             }
333              
334             1;
335              
336             __END__