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.01';
3 9     9   603838 use 5.006;
  9         495  
4 9     9   48 use strict;
  9         17  
  9         202  
5 9     9   45 use warnings;
  9         15  
  9         388  
6 9     9   4082 use Struct::Match qw/match/;
  9         179789  
  9         68  
7 9     9   4981 use Colouring::In;
  9         144058  
  9         42  
8 9     9   4550 use Blessed::Merge;
  9         41660  
  9         4091  
9              
10             our ($LINES, $GLOBAL, $NESTED_GLOBAL, $NESTED_VARIABLE, $VARIABLE, $COMMENT, $CI);
11             BEGIN {
12 9     9   124 $LINES = qr{ ([\{]( (?: (?> [^\{\}]+ ) | (??{ $LINES }) )*) [\}]) }x;
13 9         31 $GLOBAL = qr{ (\$([^:\n]+)\:([^;\n]+);) }x;
14 9         29 $VARIABLE = qr{ (\$(.*)) }x;
15 9         25 $COMMENT = qr{ (\/\*[^*]*\*+([^/*][^*]*\*+)*\/) }x;
16 9         32 $NESTED_GLOBAL = qr{ (\%([^\:\(]+)[\:\s\(]+( (?: (?> [^\(\)]+ ) | (??{ $NESTED_GLOBAL }) )*) [\)];) }x;
17 9         28 $NESTED_VARIABLE = qr{ (\$([^\{]+)[\{]( (?: (?> [^\{\}]+ ) | (??{ $NESTED_VARIABLE }) )*) [\}]) }x;
18 9         26132 $CI = qr{ ((mix|lighten|darken|fade|fadeout|fadein|tint|shade|saturate|desaturate|greyscale)[\(]( (?: (?> [^\(\)]+ ) | (??{ $CI }) )*) [\)]) }x;
19             }
20              
21             sub new {
22 20     20 1 10937 my ($pkg, %args) = @_;
23 20   50     144 $args{css} ||= {};
24 20   33     149 $args{bm} ||= Blessed::Merge->new();
25 20         548 return bless \%args, $pkg;
26             }
27              
28 0     0 0 0 sub css { $_[0]->{css} }
29              
30 99     99 0 467 sub bm { $_[0]->{bm} }
31              
32             sub parse {
33 19     19 1 97 my ($self, $string, $css) = @_;
34 19   33     180 $css ||= $self->{css};
35 19         104 return $self->_parse_content($self->_strip_comments($string), $css);
36             }
37              
38             sub parse_file {
39 2     2 1 6 my ($self, $file, $css) = @_;
40 2 50       36 die "Can't find the relative file: ${file}" unless -f $file;
41 2 50       76 open my $fh, '<', $file or die "cannot open file:$file $!";
42 2         5 my $string = do { local $/; <$fh> };
  2         8  
  2         92  
43 2         22 close $fh;
44 2         10 $self->parse($string, $css);
45             }
46              
47             sub parse_directory {
48 1     1 0 6 my ($self, $dir, $css) = @_;
49 1 50       40 die "Can't find a relative directory: ${dir}" unless -d $dir;
50 1 50       56 opendir my $d, $dir or die "Cannot read the directory: ${dir} $!";
51 1         59 for (sort readdir $d) {
52 4 100       18 next if $_ =~ m/^\./;
53 2 50       11 next unless $_ =~ m/css$/;
54 2         15 $self->parse_file("$dir/$_");
55             }
56 1         21 closedir $d;
57 1         6 return $self;
58             }
59              
60             sub compile {
61 13     13 1 13850 my ($self, $struct) = @_;
62 13   66     64 $struct ||= $self->{css};
63 13         53 my $flat = $self->_dedupe_struct(
64             $self->_flattern_struct($struct)
65             );
66 13 100       78 $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   47 my ($self, $string) = @_;
79              
80 19         141 while ($string =~ m/$COMMENT/g) {
81 6         117 $string =~ s/\Q$1\E//g;
82             }
83 19         67 return $string;
84             }
85              
86             sub _parse_globals {
87 113     113   226 my ($self, $string) = @_;
88 113         146 my %globals;
89 113         475 while ($string =~ m/$GLOBAL/g) {
90 19         45 my ($match, $class, $props) = ($1, cws($2), cws($3));
91 19 50 33     78 next unless $class && $props;
92 19         42 $globals{$class} = $props;
93 19         291 $string =~ s/\Q$match\E//;
94             }
95 113         365 while ($string =~ m/$NESTED_GLOBAL/g) {
96 7         14 my ($match, $class, $props) = ($1, cws($2), cws($3));
97 7         22 my %props = $self->_parse_props($props);
98 7         17 $globals{$class} = \%props;
99 7         77 $string =~ s/\Q$match\E//;
100             }
101              
102 113         343 return (\%globals, $string);
103             }
104              
105             sub _parse_content {
106 54     54   123 my ($self, $string, $css) = @_;
107              
108 54         78 my $globals = {};
109 54         1724 while ( $string =~ m/(([^{]+)$LINES)/g ) {
110 67         343 my ($match, $class, $props) = ($1, $2, $4);
111            
112 67         121 my $nested = {};
113 67 100       303 ($nested, $props) = $self->_parse_content($props, {})
114             if ($props =~ m/$LINES/);
115              
116 67         158 my $ri = rindex($class, ';');
117 67 100       166 if ($ri > 0) {
118 25         90 my $p = substr $class, 0, $ri + 1, '';
119 25         77 $string .= $p;
120             }
121 67 100       191 return ($css, $string) if ($class =~ m/^[^@]+:\s*\$/);
122 63         135 ($globals, $props) = $self->_parse_globals($props);
123              
124 63         157 my @classes = $self->_parse_classes($class);
125 63         159 my %props = $self->_parse_props($props);
126 63         161 for (@classes) {
127 72         514 my $current = $css;
128 72         107 for (@{$_}) {
  72         127  
129 93   100     416 $current = $current->{$_} ||= {};
130             }
131 72         112 %{$current} = %{$self->bm->merge($current, $nested, \%props)};
  72         16822  
  72         192  
132 72 100 50     244 $current->{VARIABLES} = $self->bm->merge($current->{VARIABLES} || {}, $globals) if keys %{$globals};
  72         256  
133             }
134              
135 63         3527 $string =~ s/\Q$match\E//;
136             }
137              
138 50         201 ($globals, $string) = $self->_parse_globals($string);
139 50 100       97 $css->{VARIABLES} = $globals if keys %{$globals};
  50         145  
140              
141 50         175 return ($css, $string);
142             }
143              
144             sub _parse_classes {
145 63     63   114 my ($self, $class) = @_;
146 63         213 my @parts = split /,/, $class;
147             return map {
148 63         123 my $p = $_;
  72         98  
149             [
150 72 100 33     174 $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   126 my ($self, $line) = @_;
157 70         93 my %props;
158 70         277 while ($line =~ m/(([^:]+)\:([^;]+);)/) {
159 121         377 my ($match, $key, $val) = (quotemeta($1), cws($2), cws($3));
160 121         263 $props{$key} = $val;
161 121         1434 $line =~ s/$match//;
162             }
163 70         175 while ($line =~ m/((\%[^;]+);)/) {
164 1         5 my ($match, $key, $val) = (quotemeta($1), cws($2), cws($3));
165 1         3 $props{$key} = 1;
166 1         13 $line =~ s/$match//;
167             }
168 70         312 return %props;
169             }
170              
171             sub _dedupe_struct {
172 14     14   36 my ($self, $struct) = @_;
173 14         28 for my $class (sort keys %{$struct}) {
  14         73  
174 34 100       94 next unless $struct->{$class};
175 24         42 my $new_class = $class;
176 24 100       127 if ($class =~ m/^\@/) {
177 1         4 $struct->{$new_class} = $self->_dedupe_struct($struct->{$class});
178             } else {
179 23         39 for my $inner (sort keys %{$struct}) {
  23         73  
180 64 100       1760 next if $class eq $inner;
181 41 100       143 if (match($struct->{$class}, $struct->{$inner})) {
182 10         1531 delete $struct->{$inner};
183 10         62 $new_class .= ", $inner";
184             }
185             }
186 23         1164 $struct->{$new_class} = delete $struct->{$class};
187             }
188             }
189 14         36 return $struct;
190             }
191              
192             sub _flattern_struct {
193 57     57   111 my ($self, $struct, $key, $flat) = @_;
194 57   100     138 $key ||= '';
195 57   100     147 $flat ||= {};
196 57         79 my $scp;
197              
198 57 100       112 if ($struct->{VARIABLES}) {
199 14   100     58 $flat->{$key || 'GLOBAL'}->{VARIABLES} = delete $struct->{VARIABLES};
200 14 100       38 $scp = $flat->{$key}->{VARIABLES} if $key;
201             }
202 57         76 for my $s (keys %{$struct}) {
  57         158  
203 95 100       529 if ( $s =~ m/^\@/ ) {
    100          
204 1         4 $flat->{$s} = $self->_flattern_struct($struct->{$s}, '', {});
205             }
206             elsif (ref $struct->{$s}) {
207 43 100       153 my $k = $key ? $s =~ m/^\&(.*)/ ? $key . ':' . $1 : $key . ' ' . $s : $s;
    100          
208 43         151 $self->_flattern_struct($struct->{$s}, $k, $flat);
209 43 100 50     89 $flat->{$k}->{VARIABLES} = $self->bm->merge($scp, $flat->{$k}->{VARIABLES} || {}) if $scp;
210             }
211             else {
212 51         126 $flat->{$key}->{$s} = $struct->{$s};
213             }
214             }
215 57         345 return $flat;
216             }
217              
218             sub _expand_nested_variables {
219 16     16   44 my ($self, $struct, $variables) = @_;
220 16         19 for my $key (keys %{$struct}) {
  16         47  
221 29 100       79 if ($key =~ m/^\%(.*)/) {
222 1         20 delete $struct->{$key};
223 1         4 $struct = $self->bm->merge($struct, $variables->{$1});
224             }
225             }
226 16         185 return $struct;
227             }
228              
229              
230             sub _compile {
231 1     1   4 my ($self, $flat) = @_;
232 1         3 my $string = '';
233 1 50       3 my %global = %{ delete $flat->{GLOBAL} || {} };
  1         5  
234 1         3 for my $class (sort keys %{$flat}) {
  1         4  
235             my $variables = $self->bm->merge(
236             $global{VARIABLES} || {},
237             delete $flat->{$class}->{VARIABLES} || {}
238 1   50     4 );
      50        
239 1         144 $string .= $class . "{";
240 1         4 $flat->{$class} = $self->_expand_nested_variables($flat->{$class}, $variables);
241 1 50       2 next unless keys %{$flat->{$class}};
  1         4  
242 1         26 for my $prop ( sort keys %{$flat->{$class}} ) {
  1         15  
243 4 50       10 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         12 $flat->{$class}->{$prop},
262             $variables
263             )
264             );
265             }
266             }
267 1         5 $string .= "}";
268             }
269 1         6 return $string;
270             }
271              
272             sub _pretty_compile {
273 12     12   30 my ($self, $flat) = @_;
274 12         25 my $string = '';
275 12 100       17 my %global = %{ delete $flat->{GLOBAL} || {} };
  12         74  
276 12         37 for my $class (sort keys %{$flat}) {
  12         51  
277             my $variables = $self->bm->merge(
278             $global{VARIABLES} || {},
279             delete $flat->{$class}->{VARIABLES} || {}
280 15   100     45 );
      100        
281 15         2151 $flat->{$class} = $self->_expand_nested_variables($flat->{$class}, $variables);
282 15 100       26 next unless keys %{$flat->{$class}};
  15         57  
283 13         80 $string .= join(",\n", split(", ", $class)) . " {\n";
284 13         27 for my $prop ( sort keys %{$flat->{$class}} ) {
  13         44  
285 26 100       70 if ( ref $flat->{$class}->{$prop} ) {
286 2         10 $string .= "\t" . join(",\n\t", split(", ", $prop)) . " {\n";
287 2         5 for my $attr ( sort keys %{$flat->{$class}->{$prop}} ) {
  2         5  
288             $string .= sprintf(
289             "\t\t%s: %s;\n",
290             $attr,
291 5         43 $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         66 $self->_recurse_extensions($flat->{$class}->{$prop}, $variables)
300             );
301             }
302             }
303 13         47 $string .= "}\n";
304             }
305 12         58 return $string;
306             }
307              
308             sub _recurse_extensions {
309 33     33   70 my ($self, $value, $variables) = @_;
310 33   100     303 while ($value =~ m/$NESTED_VARIABLE/g || $value =~ m/$VARIABLE/g) {
311 19         62 my ($match, $meth, $args) = ($1, cws($2), cws($3));
312 19 100       59 my $val = $args ? $variables->{$meth}->{$args} : $variables->{$meth};
313 19         334 $value =~ s/\Q$match\E/$val/;
314             }
315 33         150 while ($value =~ m/$CI/g) {
316 2         10 my ($match, $meth, $args) = ($1, $2, $3);
317 2 50       7 if ($args =~ m/$CI/) {
318 0         0 $args = $self->_recurse_extensions($args);
319             }
320 2         6 my @params = map { cws($_) } split /,/, $args;
  4         8  
321 9     9   85 no strict 'refs';
  9         26  
  9         1590  
322 2         5 my $ci = *{"Colouring::In::$meth"}->(@params)->toCSS;
  2         19  
323 2         3401 $value =~ s/\Q$match\E/$ci/;
324             }
325 33         184 return $value;
326             }
327              
328             sub cws {
329 340     340 0 697 my $string = shift;
330 340 100       1771 $string && $string =~ s/^\s*|\s*$//g;
331 340         854 return $string;
332             }
333              
334             1;
335              
336             __END__