File: | blib/lib/CSS/Grammar/Core.pm |
Coverage: | 100.0% |
line | stmt | bran | cond | sub | time | code |
---|---|---|---|---|---|---|
1 | package CSS::Grammar::Core; | |||||
2 | ||||||
3 | 6 6 6 | 25 14 41 | use strict; | |||
4 | 6 6 6 | 40 10 35 | use warnings; | |||
5 | ||||||
6 | 6 6 6 | 36 11 46 | use base 'CSS::Grammar'; | |||
7 | ||||||
8 | 6 6 6 | 37 10 30 | use CSS::Stylesheet; | |||
9 | 6 6 6 | 41 16 87 | use CSS::Ruleset; | |||
10 | 6 6 6 | 39 15 40 | use CSS::Declaration; | |||
11 | 6 6 6 | 42 13 41 | use CSS::Selector; | |||
12 | 6 6 6 | 48 14 41 | use CSS::AtRule; | |||
13 | ||||||
14 | ||||||
15 | # | |||||
16 | # http://www.w3.org/TR/REC-CSS2/syndata.html#tokenization | |||||
17 | # | |||||
18 | ||||||
19 | sub init { | |||||
20 | 15 | 45 | my ($self) = @_; | |||
21 | ||||||
22 | 15 | 32 | my %rx; | |||
23 | ||||||
24 | ##################################################################################### | |||||
25 | ||||||
26 | 15 | 45 | $self->{case_insensitive} = 1; | |||
27 | ||||||
28 | #ident {nmstart}{nmchar}* | |||||
29 | #name {nmchar}+ | |||||
30 | #nmstart [a-zA-Z]|{nonascii}|{escape} | |||||
31 | #nonascii [^\0-\177] | |||||
32 | #unicode \\[0-9a-f]{1,6}[ \n\r\t\f]? | |||||
33 | #escape {unicode}|\\[ -~\200-\4177777] | |||||
34 | #nmchar [a-z0-9-]|{nonascii}|{escape} | |||||
35 | #num [0-9]+|[0-9]*\.[0-9]+ | |||||
36 | #string {string1}|{string2} | |||||
37 | #string1 \"([\t !#$%&(-~]|\\{nl}|\'|{nonascii}|{escape})*\" | |||||
38 | #string2 \'([\t !#$%&(-~]|\\{nl}|\"|{nonascii}|{escape})*\' | |||||
39 | #nl \n|\r\n|\r|\f | |||||
40 | #w [ \t\r\n\f]* | |||||
41 | ||||||
42 | 15 | 39 | $rx{w} = '[ \\t\\r\\n\\f]*'; | |||
43 | 15 | 45 | $rx{nl} = '(\\n|\\r\\n|\\r|\\f)'; | |||
44 | 15 | 45 | $rx{unicode} = '(\\[0-9a-f]{1,6}[ \\n\\r\\t\\f]?)'; | |||
45 | 15 | 68 | $rx{escape} = '('.$rx{unicode}.'|\\\\[ -~\\x80-\\xff])'; | |||
46 | 15 | 46 | $rx{nonascii} = '[\\x80-\\xff]'; | |||
47 | 15 | 40 | $rx{num} = '([0-9]+|[0-9]*\\.[0-9]+)'; | |||
48 | ||||||
49 | 15 | 89 | $rx{nmstart} = '([a-z]|'.$rx{nonascii}.'|'.$rx{escape}.')'; | |||
50 | 15 | 89 | $rx{nmchar} = '([a-z0-9-]|'.$rx{nonascii}.'|'.$rx{escape}.')'; | |||
51 | 15 | 114 | $rx{string1} = '("([\\t !#$%&(-~]|\\\\('.$rx{nl}.')|\'|('.$rx{nonascii}.')|('.$rx{escape}.'))*")'; | |||
52 | 15 | 112 | $rx{string2} = '(\'([\\t !#$%&(-~]|\\\\('.$rx{nl}.')|"|('.$rx{nonascii}.')|('.$rx{escape}.'))*\')'; | |||
53 | ||||||
54 | 15 | 88 | $rx{string} = "($rx{string1}|$rx{string2})"; | |||
55 | 15 | 78 | $rx{ident} = "$rx{nmstart}$rx{nmchar}*"; | |||
56 | 15 | 58 | $rx{name} = "$rx{nmchar}+"; | |||
57 | ||||||
58 | ||||||
59 | ##################################################################################### | |||||
60 | ||||||
61 | #IDENT {ident} | |||||
62 | #ATKEYWORD @{ident} | |||||
63 | #STRING {string} | |||||
64 | #HASH #{name} | |||||
65 | #NUMBER {num} | |||||
66 | #PERCENTAGE {num}% | |||||
67 | #DIMENSION {num}{ident} | |||||
68 | ||||||
69 | 15 | 134 | $self->add_toke_rule('IDENT' , $rx{ident}); | |||
70 | 15 | 78 | $self->add_toke_rule('ATKEYWORD' , "\@$rx{ident}"); | |||
71 | 15 | 64 | $self->add_toke_rule('STRING' , $rx{string}); | |||
72 | 15 | 80 | $self->add_toke_rule('HASH' , "#$rx{name}"); | |||
73 | 15 | 69 | $self->add_toke_rule('NUMBER' , "$rx{num}"); | |||
74 | 15 | 74 | $self->add_toke_rule('PERCENTAGE' , "$rx{num}%"); | |||
75 | 15 | 82 | $self->add_toke_rule('DIMENSION' , "$rx{num}$rx{ident}"); | |||
76 | ||||||
77 | ||||||
78 | #URI url\({w}{string}{w}\) | |||||
79 | #|url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}\) | |||||
80 | #UNICODE-RANGE U\+[0-9A-F?]{1,6}(-[0-9A-F]{1,6})? | |||||
81 | #CDO <!-- | |||||
82 | #CDC --> | |||||
83 | ||||||
84 | 15 | 263 | $self->add_toke_rule('URI' , "(url\\($rx{w}$rx{string}$rx{w}\\))|(url\\($rx{w}([!#$%&*-~]|$rx{nonascii}|$rx{escape})*$rx{w}\\))"); | |||
85 | 15 | 59 | $self->add_toke_rule('UNICODE-RANGE' , "U\\+[0-9A-F?]{1,6}(-[0-9A-F]{1,6})?"); | |||
86 | 15 | 103 | $self->add_toke_rule('CDO' , '<!--'); | |||
87 | 15 | 53 | $self->add_toke_rule('CDC' , '-->'); | |||
88 | ||||||
89 | ||||||
90 | ##################################################################################### | |||||
91 | ||||||
92 | 15 | 55 | $self->add_toke_rule('_COLON' , ':'); | |||
93 | 15 | 52 | $self->add_toke_rule('_SEMICOLON' , ';'); | |||
94 | 15 | 51 | $self->add_toke_rule('_BRACE_OPEN' , '{'); | |||
95 | 15 | 54 | $self->add_toke_rule('_BRACE_CLOSE' , '}'); | |||
96 | 15 | 54 | $self->add_toke_rule('_ROUND_OPEN' , '\\('); | |||
97 | 15 | 56 | $self->add_toke_rule('_ROUND_CLOSE' , '\\)'); | |||
98 | 15 | 54 | $self->add_toke_rule('_SQUARE_OPEN' , '\\['); | |||
99 | 15 | 49 | $self->add_toke_rule('_SQUARE_CLOSE' , '\\]'); | |||
100 | ||||||
101 | ||||||
102 | ##################################################################################### | |||||
103 | ||||||
104 | # | |||||
105 | # this is after the _BLAH tokens since DELIM will match them (oops) | |||||
106 | # | |||||
107 | ||||||
108 | #S [ \t\r\n\f]+ | |||||
109 | #COMMENT \/\*[^*]*\*+([^/][^*]*\*+)*\/ | |||||
110 | #FUNCTION {ident}\( | |||||
111 | #INCLUDES ~= | |||||
112 | #DASHMATCH |= | |||||
113 | #DELIM any other character not matched by the above rules | |||||
114 | ||||||
115 | 15 | 56 | $self->add_toke_rule('S' , '[ \\t\\r\\n\\f]'); | |||
116 | # COMMENT \/\*[^*]*\*+([^/][^*]*\*+)*\/ | |||||
117 | 15 | 74 | $self->add_toke_rule('FUNCTION' , "$rx{ident}\\("); | |||
118 | 15 | 54 | $self->add_toke_rule('INCLUDES' , '~='); | |||
119 | 15 | 57 | $self->add_toke_rule('DASHMATCH' , '\\|='); | |||
120 | 15 | 55 | $self->add_toke_rule('DELIM' , '.'); | |||
121 | ||||||
122 | ||||||
123 | ##################################################################################### | |||||
124 | ||||||
125 | #stylesheet : [ CDO | CDC | S | statement ]*; | |||||
126 | #statement : ruleset | at-rule; | |||||
127 | #at-rule : ATKEYWORD S* any* [ block | ';' S* ]; | |||||
128 | #block : '{' S* [ any | block | ATKEYWORD S* | ';' ]* '}' S*; | |||||
129 | #ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; | |||||
130 | #selector : any+; | |||||
131 | #declaration : property ':' S* value; | |||||
132 | #property : IDENT S*; | |||||
133 | #value : [ any | block | ATKEYWORD S* ]+; | |||||
134 | #any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING | |||||
135 | # | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES | |||||
136 | # | FUNCTION | DASHMATCH | '(' any* ')' | '[' any* ']' ] S*; | |||||
137 | ||||||
138 | 15 | 78 | $self->add_lex_rule('stylesheet', '[ CDO | CDC | S | statement ]*'); | |||
139 | 15 | 54 | $self->add_lex_rule('statement', 'ruleset | at-rule'); | |||
140 | 15 | 59 | $self->add_lex_rule('at-rule', 'ATKEYWORD S* any* [ block | _SEMICOLON S* ]'); | |||
141 | 15 | 60 | $self->add_lex_rule('block', '_BRACE_OPEN S* [ any | block | ATKEYWORD S* | _SEMICOLON ]* _BRACE_CLOSE S*'); | |||
142 | 15 | 61 | $self->add_lex_rule('ruleset', 'selector? _BRACE_OPEN S* declaration? [ _SEMICOLON S* declaration? ]* _BRACE_CLOSE S*'); | |||
143 | 15 | 57 | $self->add_lex_rule('selector', 'any+'); | |||
144 | 15 | 62 | $self->add_lex_rule('declaration', 'property _COLON S* value'); | |||
145 | 15 | 56 | $self->add_lex_rule('property', 'IDENT S*'); | |||
146 | 15 | 57 | $self->add_lex_rule('value', '[ any | block | ATKEYWORD S* ]+'); | |||
147 | 15 | 58 | $self->add_lex_rule('any', '[ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING | DELIM | _COLON | URI | HASH | '. | |||
148 | 'UNICODE-RANGE | INCLUDES | FUNCTION | DASHMATCH | _ROUND_OPEN any* _ROUND_CLOSE | _SQUARE_OPEN any* _SQUARE_CLOSE ] S*'); | |||||
149 | ||||||
150 | ||||||
151 | ##################################################################################### | |||||
152 | ||||||
153 | 15 | 77 | $self->set_base('stylesheet'); | |||
154 | } | |||||
155 | ||||||
156 | sub toke { | |||||
157 | 13 | 38 | my ($self, $input) = @_; | |||
158 | ||||||
159 | # strip comments first | |||||
160 | 13 | 57 | $input =~ s!/\*[^*]*\*+([^/*][^*]*\*+)*/!!sg; | |||
161 | ||||||
162 | 13 | 99 | $self->SUPER::toke($input); | |||
163 | } | |||||
164 | ||||||
165 | sub walk_stylesheet { | |||||
166 | 14 | 47 | my ($self, $stylesheet, $submatches) = @_; | |||
167 | ||||||
168 | 14 14 | 26 41 | for my $statement (@{$submatches}){ | |||
169 | ||||||
170 | 43 | 155 | next unless defined $statement->{subrule}; | |||
171 | 42 | 154 | next unless $statement->{subrule} eq 'statement'; | |||
172 | ||||||
173 | 41 | 127 | my $sub = $statement->{submatches}->[0]; | |||
174 | ||||||
175 | 41 | 159 | if ($sub->{subrule} eq 'ruleset'){ | |||
176 | ||||||
177 | 38 | 116 | my $ruleset = $self->walk_ruleset($sub); | |||
178 | ||||||
179 | 38 38 | 63 117 | push @{$stylesheet->{rulesets}}, $ruleset; | |||
180 | 38 38 | 62 125 | push @{$stylesheet->{items}}, $ruleset; | |||
181 | } | |||||
182 | ||||||
183 | 41 | 185 | if ($sub->{subrule} eq 'at-rule'){ | |||
184 | ||||||
185 | 3 | 12 | my $atrule = $self->walk_atrule($sub); | |||
186 | ||||||
187 | 3 3 | 6 11 | push @{$stylesheet->{atrules}}, $atrule; | |||
188 | 3 3 | 6 13 | push @{$stylesheet->{items}}, $atrule; | |||
189 | } | |||||
190 | ||||||
191 | #print "subrule: $sub->{subrule}\n"; | |||||
192 | } | |||||
193 | ||||||
194 | 14 | 39 | return $stylesheet; | |||
195 | } | |||||
196 | ||||||
197 | sub walk_ruleset { | |||||
198 | 38 | 97 | my ($self, $match) = @_; | |||
199 | ||||||
200 | # | |||||
201 | # first match token should be a selector, if we have one | |||||
202 | # | |||||
203 | ||||||
204 | 38 | 130 | my $ruleset = new CSS::Ruleset; | |||
205 | ||||||
206 | 38 38 | 63 182 | for my $submatch (@{$match->{submatches}}){ | |||
207 | ||||||
208 | 97 | 404 | if ($submatch->{subrule} eq 'selector'){ | |||
209 | ||||||
210 | 38 | 161 | $ruleset->{selector} = $submatch->{matched_text}; | |||
211 | 38 | 252 | $ruleset->{selector} =~ s/^\s*(.*?)\s*/$1/s; | |||
212 | ||||||
213 | 38 | 128 | $self->walk_selectors($ruleset, $submatch); | |||
214 | } | |||||
215 | ||||||
216 | 97 | 445 | if ($submatch->{subrule} eq 'declaration'){ | |||
217 | ||||||
218 | 59 | 175 | my $declaration = $self->walk_declaration($submatch); | |||
219 | ||||||
220 | 59 59 | 101 253 | push @{$ruleset->{declarations}}, $declaration; | |||
221 | ||||||
222 | } | |||||
223 | } | |||||
224 | ||||||
225 | 38 | 98 | return $ruleset; | |||
226 | } | |||||
227 | ||||||
228 | sub walk_declaration { | |||||
229 | 59 | 147 | my ($self, $match) = @_; | |||
230 | ||||||
231 | 59 | 193 | my $declaration = new CSS::Declaration; | |||
232 | ||||||
233 | 59 59 | 104 198 | for my $submatch (@{$match->{submatches}}){ | |||
234 | ||||||
235 | 236 | 870 | next unless defined $submatch->{subrule}; | |||
236 | ||||||
237 | 118 | 575 | if ($submatch->{subrule} eq 'property'){ | |||
238 | ||||||
239 | 59 | 192 | $declaration->{property} = $submatch->{matched_text}; | |||
240 | } | |||||
241 | ||||||
242 | 118 | 492 | if ($submatch->{subrule} eq 'value'){ | |||
243 | ||||||
244 | 59 | 184 | $declaration->{value} = $submatch->{matched_text}; | |||
245 | 59 | 312 | $declaration->{simple_value} = $submatch->{matched_text}; | |||
246 | } | |||||
247 | } | |||||
248 | ||||||
249 | 59 | 151 | return $declaration; | |||
250 | } | |||||
251 | ||||||
252 | sub walk_atrule { | |||||
253 | 4 | 13 | my ($self, $match) = @_; | |||
254 | ||||||
255 | 4 | 7 | my %rx; | |||
256 | 4 | 13 | $rx{unicode} = '(?:\\[0-9a-f]{1,6}[ \\n\\r\\t\\f]?)'; | |||
257 | 4 | 20 | $rx{escape} = '(?:'.$rx{unicode}.'|\\\\[ -~\\x80-\\xff])'; | |||
258 | 4 | 10 | $rx{nonascii} = '[\\x80-\\xff]'; | |||
259 | 4 | 23 | $rx{nmstart} = '(?:[a-z]|'.$rx{nonascii}.'|'.$rx{escape}.')'; | |||
260 | 4 | 23 | $rx{nmchar} = '(?:[a-z0-9-]|'.$rx{nonascii}.'|'.$rx{escape}.')'; | |||
261 | 4 | 19 | $rx{ident} = "$rx{nmstart}$rx{nmchar}*"; | |||
262 | ||||||
263 | 4 | 134 | if ($match->{matched_text} =~ m/^\@($rx{ident})(.*?)$/s){ | |||
264 | ||||||
265 | 3 | 10 | my $rule = $1; | |||
266 | 3 | 9 | my $value = $2; | |||
267 | ||||||
268 | 3 | 22 | $value =~ s!^\s*(.*?);?\s*$!$1!s; | |||
269 | ||||||
270 | 3 | 64 | return new CSS::AtRule($rule, $value); | |||
271 | } | |||||
272 | ||||||
273 | 1 | 6 | return new CSS::AtRule(); | |||
274 | } | |||||
275 | ||||||
276 | sub walk_selectors { | |||||
277 | 38 | 117 | my ($self, $ruleset, $match) = @_; | |||
278 | ||||||
279 | 38 | 71 | my $buffer = ''; | |||
280 | ||||||
281 | 38 38 | 64 135 | for my $submatch (@{$match->{submatches}}){ | |||
282 | ||||||
283 | 126 | 469 | if ($submatch->{matched_text} =~ m!\s*,\s*!s){ | |||
284 | ||||||
285 | 30 | 87 | $self->commit_selector($ruleset, $buffer); | |||
286 | 30 | 90 | $buffer = ''; | |||
287 | }else{ | |||||
288 | 96 | 395 | $buffer .= $submatch->{matched_text}; | |||
289 | } | |||||
290 | } | |||||
291 | ||||||
292 | 38 | 126 | $self->commit_selector($ruleset, $buffer); | |||
293 | } | |||||
294 | ||||||
295 | sub commit_selector { | |||||
296 | 70 | 210 | my ($self, $ruleset, $text) = @_; | |||
297 | ||||||
298 | 70 | 383 | $text =~ s/^\s*(.*?)\s*$/$1/s; | |||
299 | 70 69 | 339 339 | push @{$ruleset->{selectors}}, new CSS::Selector($text) if length $text; | |||
300 | } | |||||
301 | ||||||
302 | 1; |