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 = '1.00';
3 8     8   476523 use 5.006;
  8         78  
4 8     8   36 use strict;
  8         12  
  8         193  
5 8     8   35 use warnings;
  8         10  
  8         300  
6 8     8   3129 use Struct::Match qw/match/;
  8         142056  
  8         57  
7 8     8   4210 use Colouring::In;
  8         113350  
  8         37  
8 8     8   3982 use Blessed::Merge;
  8         33064  
  8         3125  
9              
10             our ($LINES, $GLOBAL, $NESTED_GLOBAL, $NESTED_VARIABLE, $VARIABLE, $COMMENT, $CI);
11             BEGIN {
12 8     8   68 $LINES = qr{ ([\{]( (?: (?> [^\{\}]+ ) | (??{ $LINES }) )*) [\}]) }x;
13 8         23 $GLOBAL = qr{ (\$([^:\n]+)\:([^;\n]+);) }x;
14 8         25 $VARIABLE = qr{ (\$(.*)) }x;
15 8         17 $COMMENT = qr{ (\/\*[^*]*\*+([^/*][^*]*\*+)*\/) }x;
16 8         24 $NESTED_GLOBAL = qr{ (\%([^\:\(]+)[\:\s\(]+( (?: (?> [^\(\)]+ ) | (??{ $NESTED_GLOBAL }) )*) [\)];) }x;
17 8         22 $NESTED_VARIABLE = qr{ (\$([^\{]+)[\{]( (?: (?> [^\{\}]+ ) | (??{ $NESTED_VARIABLE }) )*) [\}]) }x;
18 8         19092 $CI = qr{ ((mix|lighten|darken|fade|fadeout|fadein|tint|shade|saturate|desaturate|greyscale)[\(]( (?: (?> [^\(\)]+ ) | (??{ $CI }) )*) [\)]) }x;
19             }
20              
21             sub new {
22 19     19 1 10161 my ($pkg, %args) = @_;
23 19   50     115 $args{css} ||= {};
24 19   33     150 $args{bm} ||= Blessed::Merge->new();
25 19         476 return bless \%args, $pkg;
26             }
27              
28 0     0 0 0 sub css { $_[0]->{css} }
29              
30 96     96 0 370 sub bm { $_[0]->{bm} }
31              
32             sub parse {
33 17     17 1 84 my ($self, $string, $css) = @_;
34 17   33     138 $css ||= $self->{css};
35 17         76 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 12466 my ($self, $struct) = @_;
48 12   66     51 $struct ||= $self->{css};
49 12         44 my $flat = $self->_dedupe_struct(
50             $self->_flattern_struct($struct)
51             );
52 12 50       51 $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   37 my ($self, $string) = @_;
65              
66 17         113 while ($string =~ m/$COMMENT/g) {
67 6         90 $string =~ s/\Q$1\E//g;
68             }
69 17         58 return $string;
70             }
71              
72             sub _parse_globals {
73 109     109   180 my ($self, $string) = @_;
74 109         121 my %globals;
75 109         371 while ($string =~ m/$GLOBAL/g) {
76 15         27 my ($match, $class, $props) = ($1, cws($2), cws($3));
77 15 50 33     47 next unless $class && $props;
78 15         26 $globals{$class} = $props;
79 15         162 $string =~ s/\Q$match\E//;
80             }
81 109         296 while ($string =~ m/$NESTED_GLOBAL/g) {
82 7         14 my ($match, $class, $props) = ($1, cws($2), cws($3));
83 7         16 my %props = $self->_parse_props($props);
84 7         13 $globals{$class} = \%props;
85 7         68 $string =~ s/\Q$match\E//;
86             }
87              
88 109         262 return (\%globals, $string);
89             }
90              
91             sub _parse_content {
92 52     52   97 my ($self, $string, $css) = @_;
93              
94 52         69 my $globals = {};
95 52         1379 while ( $string =~ m/(([^{]+)$LINES)/g ) {
96 65         266 my ($match, $class, $props) = ($1, $2, $4);
97            
98 65         90 my $nested = {};
99 65 100       268 ($nested, $props) = $self->_parse_content($props, {})
100             if ($props =~ m/$LINES/);
101              
102 65         135 my $ri = rindex($class, ';');
103 65 100       124 if ($ri > 0) {
104 23         54 my $p = substr $class, 0, $ri + 1, '';
105 23         56 $string .= $p;
106             }
107 65 100       145 return ($css, $string) if ($class =~ m/^[^@]+:\s*\$/);
108 61         117 ($globals, $props) = $self->_parse_globals($props);
109              
110 61         128 my @classes = $self->_parse_classes($class);
111 61         125 my %props = $self->_parse_props($props);
112 61         117 for (@classes) {
113 70         387 my $current = $css;
114 70         83 for (@{$_}) {
  70         110  
115 91   100     319 $current = $current->{$_} ||= {};
116             }
117 70         92 %{$current} = %{$self->bm->merge($current, $nested, \%props)};
  70         13734  
  70         128  
118 70 100 50     178 $current->{VARIABLES} = $self->bm->merge($current->{VARIABLES} || {}, $globals) if keys %{$globals};
  70         190  
119             }
120              
121 61         2994 $string =~ s/\Q$match\E//;
122             }
123              
124 48         156 ($globals, $string) = $self->_parse_globals($string);
125 48 100       74 $css->{VARIABLES} = $globals if keys %{$globals};
  48         123  
126              
127 48         139 return ($css, $string);
128             }
129              
130             sub _parse_classes {
131 61     61   103 my ($self, $class) = @_;
132 61         161 my @parts = split /,/, $class;
133             return map {
134 61         105 my $p = $_;
  70         88  
135             [
136 70 100 33     154 $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   106 my ($self, $line) = @_;
143 68         78 my %props;
144 68         211 while ($line =~ m/(([^:]+)\:([^;]+);)/) {
145 114         314 my ($match, $key, $val) = (quotemeta($1), cws($2), cws($3));
146 114         206 $props{$key} = $val;
147 114         1125 $line =~ s/$match//;
148             }
149 68         150 while ($line =~ m/((\%[^;]+);)/) {
150 1         3 my ($match, $key, $val) = (quotemeta($1), cws($2), cws($3));
151 1         3 $props{$key} = 1;
152 1         11 $line =~ s/$match//;
153             }
154 68         236 return %props;
155             }
156              
157             sub _dedupe_struct {
158 13     13   24 my ($self, $struct) = @_;
159 13         21 for my $class (sort keys %{$struct}) {
  13         54  
160 32 100       84 next unless $struct->{$class};
161 22         43 my $new_class = $class;
162 22 100       46 if ($class =~ m/^\@/) {
163 1         5 $struct->{$new_class} = $self->_dedupe_struct($struct->{$class});
164             } else {
165 21         31 for my $inner (sort keys %{$struct}) {
  21         53  
166 60 100       1411 next if $class eq $inner;
167 39 100       91 if (match($struct->{$class}, $struct->{$inner})) {
168 10         1198 delete $struct->{$inner};
169 10         34 $new_class .= ", $inner";
170             }
171             }
172 21         638 $struct->{$new_class} = delete $struct->{$class};
173             }
174             }
175 13         27 return $struct;
176             }
177              
178             sub _flattern_struct {
179 55     55   84 my ($self, $struct, $key, $flat) = @_;
180 55   100     114 $key ||= '';
181 55   100     103 $flat ||= {};
182 55         54 my $scp;
183              
184 55 100       87 if ($struct->{VARIABLES}) {
185 13   100     40 $flat->{$key || 'GLOBAL'}->{VARIABLES} = delete $struct->{VARIABLES};
186 13 100       25 $scp = $flat->{$key}->{VARIABLES} if $key;
187             }
188 55         53 for my $s (keys %{$struct}) {
  55         102  
189 90 100       430 if ( $s =~ m/^\@/ ) {
    100          
190 1         5 $flat->{$s} = $self->_flattern_struct($struct->{$s}, '', {});
191             }
192             elsif (ref $struct->{$s}) {
193 42 100       115 my $k = $key ? $s =~ m/^\&(.*)/ ? $key . ':' . $1 : $key . ' ' . $s : $s;
    100          
194 42         121 $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         104 $flat->{$key}->{$s} = $struct->{$s};
199             }
200             }
201 55         296 return $flat;
202             }
203              
204             sub _expand_nested_variables {
205 15     15   33 my ($self, $struct, $variables) = @_;
206 15         26 for my $key (keys %{$struct}) {
  15         32  
207 25 100       61 if ($key =~ m/^\%(.*)/) {
208 1         2 delete $struct->{$key};
209 1         2 $struct = $self->bm->merge($struct, $variables->{$1});
210             }
211             }
212 15         135 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   27 my ($self, $flat) = @_;
260 12         20 my $string = '';
261 12 100       13 my %global = %{ delete $flat->{GLOBAL} || {} };
  12         57  
262 12         31 for my $class (sort keys %{$flat}) {
  12         32  
263             my $variables = $self->bm->merge(
264             $global{VARIABLES} || {},
265             delete $flat->{$class}->{VARIABLES} || {}
266 15   100     32 );
      100        
267 15         1762 $flat->{$class} = $self->_expand_nested_variables($flat->{$class}, $variables);
268 15 100       21 next unless keys %{$flat->{$class}};
  15         40  
269 13         75 $string .= join(",\n", split(", ", $class)) . " {\n";
270 13         22 for my $prop ( sort keys %{$flat->{$class}} ) {
  13         40  
271 26 100       74 if ( ref $flat->{$class}->{$prop} ) {
272 2         6 $string .= "\t" . join(",\n\t", split(", ", $prop)) . " {\n";
273 2         5 for my $attr ( sort keys %{$flat->{$class}->{$prop}} ) {
  2         6  
274             $string .= sprintf(
275             "\t\t%s: %s;\n",
276             $attr,
277 5         10 $self->_recurse_extensions($flat->{$class}->{$prop}->{$attr}, $variables)
278             );
279             }
280 2         4 $string .= "\t}\n";
281             } else {
282             $string .= sprintf(
283             "\t%s: %s;\n",
284             $prop,
285 24         58 $self->_recurse_extensions($flat->{$class}->{$prop}, $variables)
286             );
287             }
288             }
289 13         49 $string .= "}\n";
290             }
291 12         61 return $string;
292             }
293              
294             sub _recurse_extensions {
295 29     29   63 my ($self, $value, $variables) = @_;
296 29   100     222 while ($value =~ m/$NESTED_VARIABLE/g || $value =~ m/$VARIABLE/g) {
297 17         43 my ($match, $meth, $args) = ($1, cws($2), cws($3));
298 17 100       35 my $val = $args ? $variables->{$meth}->{$args} : $variables->{$meth};
299 17         236 $value =~ s/\Q$match\E/$val/;
300             }
301 29         94 while ($value =~ m/$CI/g) {
302 2         7 my ($match, $meth, $args) = ($1, $2, $3);
303 2 50       6 if ($args =~ m/$CI/) {
304 0         0 $args = $self->_recurse_extensions($args);
305             }
306 2         7 my @params = map { cws($_) } split /,/, $args;
  4         6  
307 8     8   94 no strict 'refs';
  8         22  
  8         1351  
308 2         3 my $ci = *{"Colouring::In::$meth"}->(@params)->toCSS;
  2         12  
309 2         2756 $value =~ s/\Q$match\E/$ci/;
310             }
311 29         152 return $value;
312             }
313              
314             sub cws {
315 314     314 0 450 my $string = shift;
316 314 100       1293 $string && $string =~ s/^\s*|\s*$//g;
317 314         649 return $string;
318             }
319              
320             1;
321              
322             __END__