File Coverage

blib/lib/Crayon.pm
Criterion Covered Total %
statement 178 214 83.1
branch 49 62 79.0
condition 24 40 60.0
subroutine 23 27 85.1
pod 5 8 62.5
total 279 351 79.4


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