File Coverage

lib/CSS/LESS/Filter.pm
Criterion Covered Total %
statement 88 93 94.6
branch 43 56 76.7
condition 15 26 57.6
subroutine 10 12 83.3
pod 3 3 100.0
total 159 190 83.6


line stmt bran cond sub pod time code
1             package CSS::LESS::Filter;
2              
3 6     6   224831 use strict;
  6         15  
  6         242  
4 6     6   36 use warnings;
  6         14  
  6         169  
5 6     6   32 use Carp;
  6         16  
  6         564  
6 6     6   12279 use Parse::RecDescent;
  6         459288  
  6         54  
7              
8             our $VERSION = '0.04';
9              
10             sub new {
11 16     16 1 170048 my $class = shift;
12              
13 16 50       102 my $parser = Parse::RecDescent->new($class->_less_grammar)
14             or die "Bad Grammar";
15              
16 16         10159937 my $self = bless { parser => $parser, filters => [] }, $class;
17              
18 16 50       92 $self->add(@_) if @_;
19              
20 16         80 $self;
21             }
22              
23             sub process {
24 16     16 1 142 my ($self, $less, $opts) = @_;
25              
26 16   100     142 my $mode = ($opts || {})->{mode} || 'warn';
27              
28 16         40 $_->[2] = 0 for @{$self->{filters}}; # clear "used" flag.
  16         108  
29              
30 16         97 my $res = $self->_apply($self->_parse($less), "");
31              
32 16         77 for (@{$self->{filters}}) {
  16         62  
33 16 100       88 next if $_->[2];
34 7 100       30 if ($mode eq 'warn') {
35 3         610 carp "Filter '$_->[0]' ($_->[1]) is not used at all.";
36 3         236 next;
37             }
38 4 50       16 if ($mode eq 'append') {
39 4         16 my ($id, $filter) = ($_->[0], $_->[1]);
40 4 50       14 if (ref $id) {
41 0         0 carp "Can't append ambiguous '$id' ($filter).";
42 0         0 next;
43             }
44 4         12 my $type = substr($id, -1);
45 4         20 my $depth =()= $id =~ /\{/g;
46 4         14 my $closing_braces = '}' x $depth;
47 4 100       18 my $value = !ref $filter ? $filter : $filter->('');
48 4 50 66     37 $res .= "\n" if length $res && substr($res, -1) ne "\n";
49 4 100       22 if ($id eq '') {
    100          
50 1         6 $res .= "$value\n";
51             }
52             elsif ($type eq '{') { # ruleset
53 1         2 $res .= "$id\n$value";
54 1 50       5 $res .= "\n" if substr($res, -1) ne "\n";
55 1         4 $res .= "$closing_braces\n";
56             }
57             else { # declaration, at rule
58 2 100       9 $closing_braces = " $closing_braces" if $closing_braces;
59 2         15 $res .= "$id $value;$closing_braces\n";
60             }
61             }
62             }
63              
64 16         78 $res;
65             }
66              
67             sub add {
68 16     16 1 202 my $self = shift;
69              
70 16 50 33     197 croak "Odd number of args" if @_ && @_ % 2;
71              
72 16         260 while(my ($id, $value) = splice @_, 0, 2) {
73 16         37 push @{$self->{filters}}, [$id, $value];
  16         150  
74             }
75              
76 16         42 $self;
77             }
78              
79             sub _parse {
80 16     16   36 my ($self, $less) = @_;
81              
82 16         222 $self->{parser}->less($less);
83             }
84              
85             sub _apply {
86 22     22   3028545 my ($self, $parts, $id) = @_;
87              
88 22         66 my $str = '';
89 22         66 for my $part (@$parts) {
90 40 100       178 if (!ref $part) {
    50          
91 22         92 $str .= $part;
92             }
93             elsif (ref $part eq ref {}) {
94 18 100       85 if (ref $part->{value} eq ref []) {
95 6 50       45 my $new_id = (length $id ? "$id " : "") . "$part->{key} {";
96 6         47 $new_id =~ s/\s+/ /gs;
97 6         61 my $ruleset .= $part->{key} . $part->{brace_open};
98 6         52 my $inside = $self->_apply($part->{value}, $new_id);
99              
100 6         14 for (@{$self->{filters}}) {
  6         21  
101 6 50       26 next if $_->[0] eq '';
102 6 100 66     169 if (
      33        
      66        
103             (!ref $_->[0] and $new_id eq $_->[0]) or
104             (ref $_->[0] eq ref qr// and $new_id =~ /$_->[0]/)
105             ) {
106 2 100       15 if (!ref $_->[1]) {
    50          
107 1         4 $inside = $_->[1];
108             }
109 0     0   0 elsif (ref $_->[1] eq ref sub {}) {
110 1         7 $inside = $_->[1]->($inside);
111             }
112 2         42 $_->[2] = 1;
113             }
114             }
115 6 50       29 next unless defined $inside;
116 6         13 $ruleset .= $inside;
117 6         16 $ruleset .= $part->{brace_close};
118 6         21 $str .= $ruleset;
119             }
120             else {
121 12         82 (my $sep = $part->{sep}) =~ s/\s+//gs;
122 12 100       79 my $cur_id = (length $id ? "$id " : "") . "$part->{key}$sep";
123 12         27 for (@{$self->{filters}}) {
  12         53  
124 12 100       147 next if $_->[0] eq '';
125 11 100 66     181 if (
      33        
      66        
126             (!ref $_->[0] and $cur_id eq $_->[0]) or
127             (ref $_->[0] eq ref qr// and $cur_id =~ /$_->[0]/)
128             ) {
129 8 100       48 if (!ref $_->[1]) {
    50          
130 6         21 $part->{value} = $_->[1];
131             }
132 0     0   0 elsif (ref $_->[1] eq ref sub {}) {
133 2         15 $part->{value} = $_->[1]->($part->{value});
134             }
135 8         58 $_->[2] = 1;
136             }
137             }
138 12 100       64 next unless defined $part->{value};
139 9         74 $str .= join '', @$part{qw/key sep value semicolon/};
140             }
141             }
142             else {
143 0         0 warn "illegal format: $part";
144             }
145             }
146              
147 22         74 $str;
148             }
149              
150             sub _less_grammar { return <<'GRAMMAR'
151             {
152             use Data::Dump 'dump';
153             *debug = sub {};
154             if ($ENV{CSS_LESS_FILTER_DEBUG}) {
155             *debug = sub { warn(shift, ': ', dump @_) }
156             }
157             }
158              
159             less: (at_rule | ruleset | declaration | mixin | selectors | comments | sp | unknown)(s)
160             { $return = $item[2] }
161              
162             comments: less_comment | css_comment
163             { debug($thisline, @item); $return = $item[1] }
164              
165             less_comment: /\/\/.*$/m
166             { debug($thisline, @item); $return = $item[1] }
167              
168             css_comment: /\/\*[^*]*\*+([^\/*][^*]*\*+)*\//
169             { debug($thisline, @item); $return = $item[1] }
170              
171             ruleset: selectors brace_open (
172             comments | ruleset | function | declaration | at_rule |
173             selectors | semicolon | sp
174             )(s?) brace_close
175             { debug($thisline, @item); $return = { key => $item[1], brace_open => $item[2], value => $item[3], brace_close => $item[4] } }
176              
177             at_rule: at_keyword sp (string | url | ident | parens | sp)(s) semicolon
178             { debug($thisline, @item); $return = {key => $item[1], sep => $item[2], value => join('', @{$item[3]}), semicolon => $item[4]} }
179              
180             selectors: (function|selector) (function | selector | parens | comments | ',' | sp)(s?)
181             { debug($thisline, @item); $return = join '', $item[1], @{$item[2]} }
182              
183             at_keyword: '@' ident
184             { debug($thisline, @item); $return = join '', @item[1..2] }
185              
186             selector: namespace_prefix(?) (
187             ident
188             | id_selector
189             | class_selector
190             | at_keyword
191             | '&'
192             | '%'
193             | pseudo_class
194             | pseudo_element
195             | attribute_selector
196             | child_selector
197             | adjacent_selector
198             | universal_selector
199             | parens # for interpolation
200             | interpolated_variable
201             | color
202             | percent
203             | important
204             )
205             { debug($thisline, @item); $return = join '', @{$item[1]}, $item[2] }
206              
207             namespace_prefix: (ident | '*')(?) '|'
208             { debug($thisline, @item); $return = join '', @{$item[1]}, $item[2] }
209              
210             id_selector: '#' ident
211             { debug($thisline, @item); $return = join '', @item[1..2] }
212              
213             class_selector: '.' ident
214             { debug($thisline, @item); $return = join '', @item[1..2] }
215              
216             pseudo_class:
217             ':' ident (parens)(?)
218             { debug($thisline, @item); $return = join '', @item[1..2], @{$item[3]} }
219             | ':' ident
220             { debug($thisline, @item); $return = join '', @item[1..2] }
221              
222             pseudo_element: '::' ident
223             { debug($thisline, @item); $return = join '', @item[1..2] }
224              
225             universal_selector: '*'
226             { debug($thisline, @item); $return = $item[1] }
227              
228             child_selector: '>'
229             { debug($thisline, @item); $return = $item[1] }
230              
231             adjacent_selector: '+'
232             { debug($thisline, @item); $return = $item[1] }
233              
234             attribute_selector: /\[.+?\]/
235             { debug($thisline, @item); $return = $item[1] }
236              
237             ident: (word | interpolated_variable | escape)(s)
238             { debug($thisline, @item); $return = join '', @{$item[1]} }
239              
240             interpolated_variable:
241             '@{' word '}'
242             { debug($thisline, @item); $return = join '', @item[1..3] }
243             | '@@' word
244             { debug($thisline, @item); $return = join '', @item[1..2] }
245              
246             mixin: selectors semicolon
247             { debug($thisline, @item); $return = join '', @item[1..2] }
248              
249             brace_open: sp '{' sp
250             { debug($thisline, @item); $return = join '', @item[1..3] }
251              
252             brace_close: sp '}' sp
253             { debug($thisline, @item); $return = join '', @item[1..3] }
254              
255             semicolon: sp ';' sp
256             { debug($thisline, @item); $return = join '', @item[1..3] }
257              
258             colon: sp ':' sp
259             { debug($thisline, @item); $return = join '', @item[1..3] }
260              
261             declaration: (property|variable) colon values (semicolon)(?)
262             { debug($thisline, @item); $return = {key => $item[1], sep => $item[2], value => $item[3], semicolon => join '', @{$item[4]}} }
263              
264             property: /[\*]?/ ident
265             { debug($thisline, @item); $return = join'', @item[1..2] }
266              
267             unicode_range: /U\+[0-9a-fA-F?]{1,6}/
268             { debug($thisline, @item); $return = $item[1] }
269              
270             iefilter: 'progid:DXImageTransform.Microsoft.' ident parens
271             { debug($thisline, @item); $return = join'', @item[1..3] }
272              
273             javascript: /~?`[^`]+?`/s
274             { debug($thisline, @item); $return = $item[1] }
275              
276             values: (value | comments | /[, ]/ )(s)
277             { debug($thisline, @item); $return = join '', @{$item[1]} }
278              
279             value: (
280             string | url | variable | color | iefilter | javascript
281             | unicode_range | expression | percent
282             | px | num | function | ident | important
283             | sp | unknown
284             )(s)
285             { debug($thisline, @item); $return = join'', @{$item[1]} }
286              
287             variable: at_keyword
288             { debug($thisline, @item); $return = $item[1] }
289              
290             important: sp '!' sp 'important'
291             { debug($thisline, @item); $return = join'', @item[1..4] }
292              
293             url_string: (/[!#$%&*\-\[:\/\.\?=\]~,]|[a-zA-Z0-9_]/ | nonascii | escape)(s)
294             { debug($thisline, @item); $return = join'', @{$item[1]} }
295              
296             url: 'url(' sp (url_string | string) sp ')'
297             { debug($thisline, @item); $return = join'', @item[1..5] }
298              
299             function: selector parens
300             { debug($thisline, @item); $return = join '', @item[1..2] }
301              
302             num: /[0-9\.\-]+[a-z]*/
303             { debug($thisline, @item); $return = $item[1] }
304              
305             op: /\s*[\+\*\/\-,<>=]\s*/
306             { debug($thisline, @item); $return = $item[1] }
307              
308             url: /url\([^)]+?\)/
309             { debug($thisline, @item); $return = $item[1] }
310              
311             paren_open: '('
312             { debug($thisline, @item); $return = $item[1] }
313              
314             paren_close: ')'
315             { debug($thisline, @item); $return = $item[1] }
316              
317             arg: parens | variable | op | color | px | percent | num | function | string | ident | colon | attribute_selector
318             { debug($thisline, @item); $return = $item[1] }
319              
320             parens: paren_open (arg | /[;, ]/ | sp)(s?) paren_close
321             { debug($thisline, @item); $return = join '', $item[1], @{$item[2]}, $item[3] }
322             expression: (variable | percent | px | num | op | parens)(s)
323             { debug($thisline, @item); $return = join '', @{$item[1]} }
324              
325             px: /[0-9]+px/
326             { debug($thisline, @item); $return = $item[1] }
327              
328             percent: /[0-9\.\-]+%/
329             { debug($thisline, @item); $return = $item[1] }
330              
331             color: /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/
332             { debug($thisline, @item); $return = $item[1] }
333              
334             string: string1 | string2
335             { debug($thisline, @item); $return = $item[1] }
336              
337             string1: /~?"/ ( /[^\n\r\f\\"]/ | escaped_nl | escape )(s?) /"/
338             { debug($thisline, @item); $return = join '', $item[1], @{$item[2]}, $item[3] }
339              
340             string2: /~?'/ ( /[^\n\r\f\\']/ | escaped_nl | escape )(s?) /'/
341             { debug($thisline, @item); $return = join '', $item[1], @{$item[2]}, $item[3] }
342              
343             unicode: '\\' /[0-9a-fA-F]{1,6}(\r\n|[ \n\r\f\t])?/
344             { debug($thisline, @item); $return = join '', @item[1..2] }
345              
346             word: /[-]?/ nmstart nmchar(s?)
347             { debug($thisline, @item); $return = join '', @item[1..2], @{$item[3]} }
348              
349             nmstart: /[_a-zA-Z]/ | nonascii | escape
350             { debug($thisline, @item); $return = $item[1] }
351              
352             name: nmchar(s)
353             { debug($thisline, @item); $return = join '', @{$item[1]} }
354              
355             nmchar: /[_a-zA-Z0-9\-]/ | nonascii | escape
356             { debug($thisline, @item); $return = $item[1] }
357              
358             nonascii: /[^\0-\237]/
359             { debug($thisline, @item); $return = $item[1] }
360              
361             escape:
362             unicode
363             { debug($thisline, @item); $return = $item[1] }
364             | '\\' /[^\n\r\f0-9a-fA-F]/
365             { debug($thisline, @item); $return = join '', @item[1..2] }
366              
367             escaped_nl: '\\' nl
368             { debug($thisline, @item); $return = join '', @item[1..2] }
369              
370             nl: "\n" | "\r\n" | "\r" | "\f"
371             { debug($thisline, @item); $return = $item[1] }
372              
373             sp: /[ \t\r\n]*/
374             { debug($thisline, @item); $return = $item[1] }
375              
376             unknown: /./s
377             { warn dump @item; $return = $item[1] }
378              
379             GRAMMAR
380 16     16   195 }
381              
382             1;
383              
384             __END__