OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * A simple recursive descent parser for CSS. |
| 7 */ |
| 8 class Parser { |
| 9 Tokenizer tokenizer; |
| 10 |
| 11 final lang.SourceFile source; |
| 12 |
| 13 lang.Token _previousToken; |
| 14 lang.Token _peekToken; |
| 15 |
| 16 Parser(this.source, [int startOffset = 0]) { |
| 17 tokenizer = new Tokenizer(source, true, startOffset); |
| 18 _peekToken = tokenizer.next(); |
| 19 _previousToken = null; |
| 20 } |
| 21 |
| 22 /** Generate an error if [source] has not been completely consumed. */ |
| 23 void checkEndOfFile() { |
| 24 _eat(TokenKind.END_OF_FILE); |
| 25 } |
| 26 |
| 27 /** Guard to break out of parser when an unexpected end of file is found. */ |
| 28 // TODO(jimhug): Failure to call this method can lead to inifinite parser |
| 29 // loops. Consider embracing exceptions for more errors to reduce |
| 30 // the danger here. |
| 31 bool isPrematureEndOfFile() { |
| 32 if (_maybeEat(TokenKind.END_OF_FILE)) { |
| 33 _error('unexpected end of file', _peekToken.span); |
| 34 return true; |
| 35 } else { |
| 36 return false; |
| 37 } |
| 38 } |
| 39 |
| 40 /////////////////////////////////////////////////////////////////// |
| 41 // Basic support methods |
| 42 /////////////////////////////////////////////////////////////////// |
| 43 int _peek() { |
| 44 return _peekToken.kind; |
| 45 } |
| 46 |
| 47 lang.Token _next() { |
| 48 _previousToken = _peekToken; |
| 49 _peekToken = tokenizer.next(); |
| 50 return _previousToken; |
| 51 } |
| 52 |
| 53 bool _peekKind(int kind) { |
| 54 return _peekToken.kind == kind; |
| 55 } |
| 56 |
| 57 /* Is the next token a legal identifier? This includes pseudo-keywords. */ |
| 58 bool _peekIdentifier() { |
| 59 return TokenKind.isIdentifier(_peekToken.kind); |
| 60 } |
| 61 |
| 62 bool _maybeEat(int kind) { |
| 63 if (_peekToken.kind == kind) { |
| 64 _previousToken = _peekToken; |
| 65 _peekToken = tokenizer.next(); |
| 66 return true; |
| 67 } else { |
| 68 return false; |
| 69 } |
| 70 } |
| 71 |
| 72 void _eat(int kind) { |
| 73 if (!_maybeEat(kind)) { |
| 74 _errorExpected(TokenKind.kindToString(kind)); |
| 75 } |
| 76 } |
| 77 |
| 78 void _eatSemicolon() { |
| 79 _eat(TokenKind.SEMICOLON); |
| 80 } |
| 81 |
| 82 void _errorExpected(String expected) { |
| 83 var tok = _next(); |
| 84 var message; |
| 85 try { |
| 86 message = 'expected $expected, but found $tok'; |
| 87 } catch (var e) { |
| 88 message = 'parsing error expected $expected'; |
| 89 } |
| 90 _error(message, tok.span); |
| 91 } |
| 92 |
| 93 void _error(String message, [lang.SourceSpan location=null]) { |
| 94 if (location === null) { |
| 95 location = _peekToken.span; |
| 96 } |
| 97 |
| 98 lang.world.fatal(message, location); // syntax errors are fatal for now |
| 99 } |
| 100 |
| 101 lang.SourceSpan _makeSpan(int start) { |
| 102 return new lang.SourceSpan(source, start, _previousToken.end); |
| 103 } |
| 104 |
| 105 /////////////////////////////////////////////////////////////////// |
| 106 // Top level productions |
| 107 /////////////////////////////////////////////////////////////////// |
| 108 |
| 109 List<SelectorGroup> expression() { |
| 110 List<SelectorGroup> groups = []; |
| 111 while (!_maybeEat(TokenKind.END_OF_FILE)) { |
| 112 do { |
| 113 int start = _peekToken.start; |
| 114 groups.add(new SelectorGroup(selectorExpression(), _makeSpan(start))); |
| 115 } while (_maybeEat(TokenKind.COMMA)); |
| 116 } |
| 117 |
| 118 return groups; |
| 119 } |
| 120 |
| 121 // Templates are @{selectors} single line nothing else. |
| 122 SelectorGroup template() { |
| 123 SelectorGroup selectorGroup; |
| 124 if (!_maybeEat(TokenKind.END_OF_FILE)) { |
| 125 selectorGroup = templateExpression(); |
| 126 if (!_maybeEat(TokenKind.END_OF_FILE)) { |
| 127 // TODO(terry): Error should be done. |
| 128 } |
| 129 } |
| 130 |
| 131 return selectorGroup; |
| 132 } |
| 133 |
| 134 /* |
| 135 * Expect @{css_expression} |
| 136 */ |
| 137 templateExpression() { |
| 138 int start = _peekToken.start; |
| 139 |
| 140 _eat(TokenKind.AT); |
| 141 _eat(TokenKind.LBRACE); |
| 142 |
| 143 SelectorGroup group = new SelectorGroup(selectorExpression(), |
| 144 _makeSpan(start)); |
| 145 |
| 146 _eat(TokenKind.RBRACE); |
| 147 |
| 148 return group; |
| 149 } |
| 150 |
| 151 int classNameCheck(var selector, int matches) { |
| 152 if (selector.isCombinatorNone()) { |
| 153 if (matches < 0) { |
| 154 String tooMany = selector.toString(); |
| 155 throw new CssSelectorException( |
| 156 'Can not mix Id selector with class selector(s). Id ' + |
| 157 'selector must be singleton too many starting at $tooMany', |
| 158 selector.span); |
| 159 } |
| 160 |
| 161 return ++matches; |
| 162 } else { |
| 163 String error = selector.toString(); |
| 164 throw new CssSelectorException( |
| 165 'Selectors can not have combinators (>, +, or ~) before $error', |
| 166 selector.span); |
| 167 } |
| 168 } |
| 169 |
| 170 int elementIdCheck(var selector, int matches) { |
| 171 if (selector.isCombinatorNone()) { |
| 172 if (matches != 0) { |
| 173 String tooMany = selector.toString(); |
| 174 throw new CssSelectorException( |
| 175 'Use of Id selector must be singleton starting at $tooMany', |
| 176 selector.span); |
| 177 } |
| 178 return --matches; |
| 179 } else { |
| 180 String error = selector.toString(); |
| 181 throw new CssSelectorException( |
| 182 'Selectors can not have combinators (>, +, or ~) before $error', |
| 183 selector.span); |
| 184 } |
| 185 } |
| 186 |
| 187 // Validate the @{css expression} only .class and #elementId are valid inside |
| 188 // of @{...}. |
| 189 validateTemplate(List<lang.Node> selectors, CssWorld cssWorld) { |
| 190 var errorSelector; |
| 191 bool found = false; |
| 192 |
| 193 int matches = 0; // < 0 IdSelectors, > 0 ClassSelector |
| 194 for (selector in selectors) { |
| 195 found = false; |
| 196 if (selector is ClassSelector) { |
| 197 // Any class name starting with an underscore is a private class name |
| 198 // that doesn't have to match the world of known classes. |
| 199 if (!selector.name.startsWith('_')) { |
| 200 for (className in cssWorld.classes) { |
| 201 if (selector.name == className) { |
| 202 matches = classNameCheck(selector, matches); |
| 203 found = true; |
| 204 break; |
| 205 } |
| 206 } |
| 207 } else { |
| 208 // Don't check any class name that is prefixed with an underscore. |
| 209 // However, signal as found and bump up matches; it's a valid class |
| 210 // name. |
| 211 matches = classNameCheck(selector, matches); |
| 212 found = true; |
| 213 } |
| 214 } else if (selector is IdSelector) { |
| 215 // Any element id starting with an underscore is a private element id |
| 216 // that doesn't have to match the world of known elemtn ids. |
| 217 if (!selector.name.startsWith('_')) { |
| 218 for (id in cssWorld.ids) { |
| 219 if (selector.name == id) { |
| 220 matches = elementIdCheck(selector, matches); |
| 221 found = true; |
| 222 break; |
| 223 } |
| 224 } |
| 225 } else { |
| 226 // Don't check any element ID that is prefixed with an underscore. |
| 227 // However, signal as found and bump up matches; it's a valid element |
| 228 // ID. |
| 229 matches = elementIdCheck(selector, matches); |
| 230 found = true; |
| 231 } |
| 232 } else { |
| 233 String badSelector = selector.toString(); |
| 234 throw new CssSelectorException( |
| 235 'Invalid selector $badSelector', selector.span); |
| 236 } |
| 237 |
| 238 if (!found) { |
| 239 errorSelector = selector; // Flag the problem selector. |
| 240 break; |
| 241 } |
| 242 } |
| 243 |
| 244 assert(matches >= 0 || matches == -1); |
| 245 |
| 246 if (!found && errorSelector != null) { |
| 247 String unknownName = errorSelector.toString(); |
| 248 throw new CssSelectorException('Unknown selector name $unknownName', |
| 249 errorSelector.span); |
| 250 } |
| 251 } |
| 252 |
| 253 /////////////////////////////////////////////////////////////////// |
| 254 // Productions |
| 255 /////////////////////////////////////////////////////////////////// |
| 256 |
| 257 selectorExpression() { |
| 258 return simpleSelectorSequence(); |
| 259 } |
| 260 |
| 261 simpleSelectorSequence() { |
| 262 List<SimpleSelector> simpleSelectors = []; |
| 263 while (true) { |
| 264 var selectorItem = combinator(); |
| 265 if (selectorItem != null) { |
| 266 simpleSelectors.add(selectorItem); |
| 267 } else { |
| 268 break; |
| 269 } |
| 270 } |
| 271 |
| 272 return simpleSelectors; |
| 273 } |
| 274 |
| 275 combinator() { |
| 276 int combinatorType = TokenKind.COMBINATOR_NONE; |
| 277 switch (_peek()) { |
| 278 case TokenKind.COMBINATOR_PLUS: |
| 279 _eat(TokenKind.COMBINATOR_PLUS); |
| 280 combinatorType = TokenKind.COMBINATOR_PLUS; |
| 281 break; |
| 282 case TokenKind.COMBINATOR_GREATER: |
| 283 _eat(TokenKind.COMBINATOR_GREATER); |
| 284 combinatorType = TokenKind.COMBINATOR_GREATER; |
| 285 break; |
| 286 case TokenKind.COMBINATOR_TILDE: |
| 287 _eat(TokenKind.COMBINATOR_TILDE); |
| 288 combinatorType = TokenKind.COMBINATOR_TILDE; |
| 289 break; |
| 290 } |
| 291 |
| 292 return namespaceElementUniversal(combinatorType); |
| 293 } |
| 294 |
| 295 /** |
| 296 * Simple selector grammar: |
| 297 * simple_selector_sequence |
| 298 * : [ type_selector | universal ] |
| 299 * [ HASH | class | attrib | pseudo | negation ]* |
| 300 * | [ HASH | class | attrib | pseudo | negation ]+ |
| 301 * type_selector |
| 302 * : [ namespace_prefix ]? element_name |
| 303 * namespace_prefix |
| 304 * : [ IDENT | '*' ]? '|' |
| 305 * element_name |
| 306 * : IDENT |
| 307 * universal |
| 308 * : [ namespace_prefix ]? '*' |
| 309 * class |
| 310 * : '.' IDENT |
| 311 */ |
| 312 namespaceElementUniversal(int combinator) { |
| 313 String first; |
| 314 switch (_peek()) { |
| 315 case TokenKind.ASTERISK: |
| 316 first = '*'; // Mark as universal namespace. |
| 317 _next(); |
| 318 break; |
| 319 case TokenKind.IDENTIFIER: |
| 320 int startIdent = _peekToken.start; |
| 321 first = identifier(); |
| 322 break; |
| 323 } |
| 324 |
| 325 if (first != null) { |
| 326 // Could be a namespace? |
| 327 var isNamespace = _maybeEat(TokenKind.NAMESPACE); |
| 328 if (isNamespace) { |
| 329 var element; |
| 330 switch (_peek()) { |
| 331 case TokenKind.ASTERISK: |
| 332 element = '*'; // Mark as universal. |
| 333 _next(); |
| 334 break; |
| 335 case TokenKind.IDENTIFIER: |
| 336 int startIdent = _peekToken.start; |
| 337 element = identifier(); |
| 338 break; |
| 339 default: |
| 340 _error('expected element name or universal, but found $_peekToken', |
| 341 _peekToken.span); |
| 342 } |
| 343 |
| 344 return new NamespaceSelector(first, new ElementSelector(element), |
| 345 combinator); |
| 346 } else { |
| 347 return new ElementSelector(first, combinator); |
| 348 } |
| 349 } else { |
| 350 // Check for HASH | class | attrib | pseudo | negation |
| 351 return selectorNameType(combinator); |
| 352 } |
| 353 } |
| 354 |
| 355 selectorNameType(int combinator) { |
| 356 // Check for HASH | class | attrib | pseudo | negation |
| 357 switch (_peek()) { |
| 358 case TokenKind.HASH: |
| 359 int startHash = _peekToken.start; |
| 360 _eat(TokenKind.HASH); |
| 361 var name = identifier(); |
| 362 return new IdSelector(name, combinator); |
| 363 case TokenKind.DOT: |
| 364 _eat(TokenKind.DOT); |
| 365 var name = identifier(); |
| 366 return new ClassSelector(name, combinator); |
| 367 case TokenKind.PSEUDO: |
| 368 // :pseudo-class ::pseudo-element |
| 369 _eat(TokenKind.PSEUDO); |
| 370 bool pseudoClass = _peek() != TokenKind.PSEUDO; |
| 371 var name = identifier(); |
| 372 // TODO(terry): Need to handle specific pseudo class/element name and |
| 373 // backward compatible names that are : as well as ::. |
| 374 return pseudoClass ? |
| 375 new PseudoClassSelector(name, combinator) : |
| 376 new PseudoElementSelector(name, combinator); |
| 377 |
| 378 // TODO(terry): attrib, negation. |
| 379 } |
| 380 } |
| 381 |
| 382 identifier() { |
| 383 var tok = _next(); |
| 384 if (!TokenKind.isIdentifier(tok.kind)) { |
| 385 try { |
| 386 _error('expected identifier, but found $tok', tok.span); |
| 387 } catch (var e) { |
| 388 _error('expected identifier', tok.span); |
| 389 } |
| 390 } |
| 391 |
| 392 return new Identifier(tok.text, _makeSpan(tok.start)); |
| 393 } |
| 394 } |
OLD | NEW |