| 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; | |||||