| 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 | |
| 4 /** | |
| 5 * A simple recursive descent parser for CSS. | |
| 6 */ | |
| 7 class Parser { | |
| 8 Tokenizer tokenizer; | |
| 9 | |
| 10 var _fs; // If non-null filesystem to read files. | |
| 11 String _basePath; // Base path of CSS file. | |
| 12 | |
| 13 final SourceFile source; | |
| 14 | |
| 15 Token _previousToken; | |
| 16 Token _peekToken; | |
| 17 | |
| 18 // Communicating errors back to template parser. | |
| 19 // TODO(terry): Need a better mechanism (e.g., common World). | |
| 20 var _erroMsgRedirector; | |
| 21 | |
| 22 Parser(this.source, [int start = 0, this._fs = null, this._basePath = null]) { | |
| 23 tokenizer = new Tokenizer(source, true, start); | |
| 24 _peekToken = tokenizer.next(); | |
| 25 _previousToken = null; | |
| 26 } | |
| 27 | |
| 28 // Main entry point for parsing an entire CSS file. | |
| 29 // If nestedCSS is true when we're back at processing directives from top and | |
| 30 // we encounter a } then stop we're inside of a template e.g., | |
| 31 // | |
| 32 // template ... { | |
| 33 // css { | |
| 34 // .item { | |
| 35 // left: 10px; | |
| 36 // } | |
| 37 // } | |
| 38 // <div>...</div> | |
| 39 // } | |
| 40 // | |
| 41 Stylesheet parse([bool nestedCSS = false, var erroMsgRedirector = null]) { | |
| 42 // TODO(terry): Hack for migrating CSS errors back to template errors. | |
| 43 _erroMsgRedirector = erroMsgRedirector; | |
| 44 | |
| 45 List<ASTNode> productions = []; | |
| 46 | |
| 47 int start = _peekToken.start; | |
| 48 while (!_maybeEat(TokenKind.END_OF_FILE) && | |
| 49 (!nestedCSS && !_peekKind(TokenKind.RBRACE))) { | |
| 50 // TODO(terry): Need to handle charset, import, media and page. | |
| 51 var directive = processDirective(); | |
| 52 if (directive != null) { | |
| 53 productions.add(directive); | |
| 54 } else { | |
| 55 RuleSet ruleset = processRuleSet(); | |
| 56 if (ruleset != null) { | |
| 57 productions.add(ruleset); | |
| 58 } else { | |
| 59 break; | |
| 60 } | |
| 61 } | |
| 62 } | |
| 63 | |
| 64 return new Stylesheet(productions, _makeSpan(start)); | |
| 65 } | |
| 66 | |
| 67 /** Generate an error if [source] has not been completely consumed. */ | |
| 68 void checkEndOfFile() { | |
| 69 _eat(TokenKind.END_OF_FILE); | |
| 70 } | |
| 71 | |
| 72 /** Guard to break out of parser when an unexpected end of file is found. */ | |
| 73 // TODO(jimhug): Failure to call this method can lead to inifinite parser | |
| 74 // loops. Consider embracing exceptions for more errors to reduce | |
| 75 // the danger here. | |
| 76 bool isPrematureEndOfFile() { | |
| 77 if (_maybeEat(TokenKind.END_OF_FILE)) { | |
| 78 _error('unexpected end of file', _peekToken.span); | |
| 79 return true; | |
| 80 } else { | |
| 81 return false; | |
| 82 } | |
| 83 } | |
| 84 | |
| 85 /////////////////////////////////////////////////////////////////// | |
| 86 // Basic support methods | |
| 87 /////////////////////////////////////////////////////////////////// | |
| 88 int _peek() { | |
| 89 return _peekToken.kind; | |
| 90 } | |
| 91 | |
| 92 Token _next() { | |
| 93 _previousToken = _peekToken; | |
| 94 _peekToken = tokenizer.next(); | |
| 95 return _previousToken; | |
| 96 } | |
| 97 | |
| 98 bool _peekKind(int kind) { | |
| 99 return _peekToken.kind == kind; | |
| 100 } | |
| 101 | |
| 102 /* Is the next token a legal identifier? This includes pseudo-keywords. */ | |
| 103 bool _peekIdentifier() { | |
| 104 return TokenKind.isIdentifier(_peekToken.kind); | |
| 105 } | |
| 106 | |
| 107 bool _maybeEat(int kind) { | |
| 108 if (_peekToken.kind == kind) { | |
| 109 _previousToken = _peekToken; | |
| 110 _peekToken = tokenizer.next(); | |
| 111 return true; | |
| 112 } else { | |
| 113 return false; | |
| 114 } | |
| 115 } | |
| 116 | |
| 117 void _eat(int kind) { | |
| 118 if (!_maybeEat(kind)) { | |
| 119 _errorExpected(TokenKind.kindToString(kind)); | |
| 120 } | |
| 121 } | |
| 122 | |
| 123 void _eatSemicolon() { | |
| 124 _eat(TokenKind.SEMICOLON); | |
| 125 } | |
| 126 | |
| 127 void _errorExpected(String expected) { | |
| 128 var tok = _next(); | |
| 129 var message; | |
| 130 try { | |
| 131 message = 'expected $expected, but found $tok'; | |
| 132 } catch (e) { | |
| 133 message = 'parsing error expected $expected'; | |
| 134 } | |
| 135 _error(message, tok.span); | |
| 136 } | |
| 137 | |
| 138 void _error(String message, [SourceSpan location=null]) { | |
| 139 if (location == null) { | |
| 140 location = _peekToken.span; | |
| 141 } | |
| 142 | |
| 143 if (_erroMsgRedirector == null) { | |
| 144 world.fatal(message, location); // syntax errors are fatal for now | |
| 145 } else { | |
| 146 String text = ""; | |
| 147 if (location != null) { | |
| 148 text = location.toMessageString(""); | |
| 149 } | |
| 150 _erroMsgRedirector.displayError("CSS error: \r${text}\r${message}"); | |
| 151 } | |
| 152 } | |
| 153 | |
| 154 void _warning(String message, [SourceSpan location=null]) { | |
| 155 if (location == null) { | |
| 156 location = _peekToken.span; | |
| 157 } | |
| 158 | |
| 159 world.warning(message, location); | |
| 160 } | |
| 161 | |
| 162 SourceSpan _makeSpan(int start) { | |
| 163 return new SourceSpan(source, start, _previousToken.end); | |
| 164 } | |
| 165 | |
| 166 /////////////////////////////////////////////////////////////////// | |
| 167 // Top level productions | |
| 168 /////////////////////////////////////////////////////////////////// | |
| 169 | |
| 170 // Templates are @{selectors} single line nothing else. | |
| 171 SelectorGroup parseTemplate() { | |
| 172 SelectorGroup selectorGroup = null; | |
| 173 if (!isPrematureEndOfFile()) { | |
| 174 selectorGroup = templateExpression(); | |
| 175 } | |
| 176 | |
| 177 return selectorGroup; | |
| 178 } | |
| 179 | |
| 180 /* | |
| 181 * Expect @{css_expression} | |
| 182 */ | |
| 183 templateExpression() { | |
| 184 List<Selector> selectors = []; | |
| 185 | |
| 186 int start = _peekToken.start; | |
| 187 | |
| 188 _eat(TokenKind.AT); | |
| 189 _eat(TokenKind.LBRACE); | |
| 190 | |
| 191 selectors.add(processSelector()); | |
| 192 SelectorGroup group = new SelectorGroup(selectors, _makeSpan(start)); | |
| 193 | |
| 194 _eat(TokenKind.RBRACE); | |
| 195 | |
| 196 return group; | |
| 197 } | |
| 198 | |
| 199 /////////////////////////////////////////////////////////////////// | |
| 200 // Productions | |
| 201 /////////////////////////////////////////////////////////////////// | |
| 202 | |
| 203 processMedia([bool oneRequired = false]) { | |
| 204 List<String> media = []; | |
| 205 | |
| 206 while (_peekIdentifier()) { | |
| 207 // We have some media types. | |
| 208 var medium = identifier(); // Medium ident. | |
| 209 media.add(medium); | |
| 210 if (!_maybeEat(TokenKind.COMMA)) { | |
| 211 // No more media types exit now. | |
| 212 break; | |
| 213 } | |
| 214 } | |
| 215 | |
| 216 if (oneRequired && media.length == 0) { | |
| 217 _error('at least one media type required', _peekToken.span); | |
| 218 } | |
| 219 | |
| 220 return media; | |
| 221 } | |
| 222 | |
| 223 // Directive grammar: | |
| 224 // | |
| 225 // import: '@import' [string | URI] media_list? | |
| 226 // media: '@media' media_list '{' ruleset '}' | |
| 227 // page: '@page' [':' IDENT]? '{' declarations '}' | |
| 228 // include: '@include' [string | URI] | |
| 229 // stylet: '@stylet' IDENT '{' ruleset '}' | |
| 230 // media_list: IDENT [',' IDENT] | |
| 231 // keyframes: '@-webkit-keyframes ...' (see grammar below). | |
| 232 // font_face: '@font-face' '{' declarations '}' | |
| 233 // | |
| 234 processDirective() { | |
| 235 int start = _peekToken.start; | |
| 236 | |
| 237 if (_maybeEat(TokenKind.AT)) { | |
| 238 switch (_peek()) { | |
| 239 case TokenKind.DIRECTIVE_IMPORT: | |
| 240 _next(); | |
| 241 | |
| 242 String importStr; | |
| 243 if (_peekIdentifier()) { | |
| 244 var func = processFunction(identifier()); | |
| 245 if (func is UriTerm) { | |
| 246 importStr = func.text; | |
| 247 } | |
| 248 } else { | |
| 249 importStr = processQuotedString(false); | |
| 250 } | |
| 251 | |
| 252 // Any medias? | |
| 253 List<String> medias = processMedia(); | |
| 254 | |
| 255 if (importStr == null) { | |
| 256 _error('missing import string', _peekToken.span); | |
| 257 } | |
| 258 return new ImportDirective(importStr, medias, _makeSpan(start)); | |
| 259 case TokenKind.DIRECTIVE_MEDIA: | |
| 260 _next(); | |
| 261 | |
| 262 // Any medias? | |
| 263 List<String> media = processMedia(true); | |
| 264 RuleSet ruleset; | |
| 265 | |
| 266 if (_maybeEat(TokenKind.LBRACE)) { | |
| 267 ruleset = processRuleSet(); | |
| 268 if (!_maybeEat(TokenKind.RBRACE)) { | |
| 269 _error('expected } after ruleset for @media', _peekToken.span); | |
| 270 } | |
| 271 } else { | |
| 272 _error('expected { after media before ruleset', _peekToken.span); | |
| 273 } | |
| 274 return new MediaDirective(media, ruleset, _makeSpan(start)); | |
| 275 case TokenKind.DIRECTIVE_PAGE: | |
| 276 _next(); | |
| 277 | |
| 278 // Any pseudo page? | |
| 279 var pseudoPage; | |
| 280 if (_maybeEat(TokenKind.COLON)) { | |
| 281 if (_peekIdentifier()) { | |
| 282 pseudoPage = identifier(); | |
| 283 } | |
| 284 } | |
| 285 return new PageDirective(pseudoPage, processDeclarations(), | |
| 286 _makeSpan(start)); | |
| 287 case TokenKind.DIRECTIVE_KEYFRAMES: | |
| 288 /* Key frames grammar: | |
| 289 * | |
| 290 * @-webkit-keyframes [IDENT|STRING] '{' keyframes-blocks '}'; | |
| 291 * | |
| 292 * keyframes-blocks: | |
| 293 * [keyframe-selectors '{' declarations '}']* ; | |
| 294 * | |
| 295 * keyframe-selectors: | |
| 296 * ['from'|'to'|PERCENTAGE] [',' ['from'|'to'|PERCENTAGE] ]* ; | |
| 297 */ | |
| 298 _next(); | |
| 299 | |
| 300 var name; | |
| 301 if (_peekIdentifier()) { | |
| 302 name = identifier(); | |
| 303 } | |
| 304 | |
| 305 _eat(TokenKind.LBRACE); | |
| 306 | |
| 307 KeyFrameDirective kf = new KeyFrameDirective(name, _makeSpan(start)); | |
| 308 | |
| 309 do { | |
| 310 Expressions selectors = new Expressions(_makeSpan(start)); | |
| 311 | |
| 312 do { | |
| 313 var term = processTerm(); | |
| 314 | |
| 315 // TODO(terry): Only allow from, to and PERCENTAGE ... | |
| 316 | |
| 317 selectors.add(term); | |
| 318 } while (_maybeEat(TokenKind.COMMA)); | |
| 319 | |
| 320 kf.add(new KeyFrameBlock(selectors, processDeclarations(), | |
| 321 _makeSpan(start))); | |
| 322 | |
| 323 } while (!_maybeEat(TokenKind.RBRACE)); | |
| 324 | |
| 325 return kf; | |
| 326 case TokenKind.DIRECTIVE_FONTFACE: | |
| 327 _next(); | |
| 328 | |
| 329 List<Declaration> decls = []; | |
| 330 | |
| 331 // TODO(terry): To Be Implemented | |
| 332 | |
| 333 return new FontFaceDirective(decls, _makeSpan(start)); | |
| 334 case TokenKind.DIRECTIVE_INCLUDE: | |
| 335 _next(); | |
| 336 String filename = processQuotedString(false); | |
| 337 if (_fs != null) { | |
| 338 // Does CSS file exist? | |
| 339 if (_fs.fileExists('${_basePath}${filename}')) { | |
| 340 String basePath = ""; | |
| 341 int idx = filename.lastIndexOf('/'); | |
| 342 if (idx >= 0) { | |
| 343 basePath = filename.substring(0, idx + 1); | |
| 344 } | |
| 345 basePath = '${_basePath}${basePath}'; | |
| 346 // Yes, let's parse this file as well. | |
| 347 String fullFN = '${basePath}${filename}'; | |
| 348 String contents = _fs.readAll(fullFN); | |
| 349 Parser parser = new Parser(new SourceFile(fullFN, contents), 0, | |
| 350 _fs, basePath); | |
| 351 Stylesheet stylesheet = parser.parse(); | |
| 352 return new IncludeDirective(filename, stylesheet, _makeSpan(start)); | |
| 353 } | |
| 354 | |
| 355 _error('file doesn\'t exist ${filename}', _peekToken.span); | |
| 356 } | |
| 357 | |
| 358 print("WARNING: @include doesn't work for uitest"); | |
| 359 return new IncludeDirective(filename, null, _makeSpan(start)); | |
| 360 case TokenKind.DIRECTIVE_STYLET: | |
| 361 /* Stylet grammar: | |
| 362 * | |
| 363 * @stylet IDENT '{' | |
| 364 * ruleset | |
| 365 * '}' | |
| 366 */ | |
| 367 _next(); | |
| 368 | |
| 369 var name; | |
| 370 if (_peekIdentifier()) { | |
| 371 name = identifier(); | |
| 372 } | |
| 373 | |
| 374 _eat(TokenKind.LBRACE); | |
| 375 | |
| 376 List<ASTNode> productions = []; | |
| 377 | |
| 378 start = _peekToken.start; | |
| 379 while (!_maybeEat(TokenKind.END_OF_FILE)) { | |
| 380 RuleSet ruleset = processRuleSet(); | |
| 381 if (ruleset == null) { | |
| 382 break; | |
| 383 } | |
| 384 productions.add(ruleset); | |
| 385 } | |
| 386 | |
| 387 _eat(TokenKind.RBRACE); | |
| 388 | |
| 389 return new StyletDirective(name, productions, _makeSpan(start)); | |
| 390 default: | |
| 391 _error('unknown directive, found $_peekToken', _peekToken.span); | |
| 392 } | |
| 393 } | |
| 394 } | |
| 395 | |
| 396 RuleSet processRuleSet() { | |
| 397 int start = _peekToken.start; | |
| 398 | |
| 399 SelectorGroup selGroup = processSelectorGroup(); | |
| 400 if (selGroup != null) { | |
| 401 return new RuleSet(selGroup, processDeclarations(), _makeSpan(start)); | |
| 402 } | |
| 403 } | |
| 404 | |
| 405 DeclarationGroup processDeclarations() { | |
| 406 int start = _peekToken.start; | |
| 407 | |
| 408 _eat(TokenKind.LBRACE); | |
| 409 | |
| 410 List<Declaration> decls = []; | |
| 411 do { | |
| 412 Declaration decl = processDeclaration(); | |
| 413 if (decl != null) { | |
| 414 decls.add(decl); | |
| 415 } | |
| 416 } while (_maybeEat(TokenKind.SEMICOLON)); | |
| 417 | |
| 418 _eat(TokenKind.RBRACE); | |
| 419 | |
| 420 return new DeclarationGroup(decls, _makeSpan(start)); | |
| 421 } | |
| 422 | |
| 423 SelectorGroup processSelectorGroup() { | |
| 424 List<Selector> selectors = []; | |
| 425 int start = _peekToken.start; | |
| 426 do { | |
| 427 Selector selector = processSelector(); | |
| 428 if (selector != null) { | |
| 429 selectors.add(selector); | |
| 430 } | |
| 431 } while (_maybeEat(TokenKind.COMMA)); | |
| 432 | |
| 433 if (selectors.length > 0) { | |
| 434 return new SelectorGroup(selectors, _makeSpan(start)); | |
| 435 } | |
| 436 } | |
| 437 | |
| 438 /* Return list of selectors | |
| 439 * | |
| 440 */ | |
| 441 processSelector() { | |
| 442 List<SimpleSelectorSequence> simpleSequences = []; | |
| 443 int start = _peekToken.start; | |
| 444 while (true) { | |
| 445 // First item is never descendant make sure it's COMBINATOR_NONE. | |
| 446 var selectorItem = simpleSelectorSequence(simpleSequences.length == 0); | |
| 447 if (selectorItem != null) { | |
| 448 simpleSequences.add(selectorItem); | |
| 449 } else { | |
| 450 break; | |
| 451 } | |
| 452 } | |
| 453 | |
| 454 if (simpleSequences.length > 0) { | |
| 455 return new Selector(simpleSequences, _makeSpan(start)); | |
| 456 } | |
| 457 } | |
| 458 | |
| 459 simpleSelectorSequence(bool forceCombinatorNone) { | |
| 460 int start = _peekToken.start; | |
| 461 int combinatorType = TokenKind.COMBINATOR_NONE; | |
| 462 | |
| 463 switch (_peek()) { | |
| 464 case TokenKind.PLUS: | |
| 465 _eat(TokenKind.PLUS); | |
| 466 combinatorType = TokenKind.COMBINATOR_PLUS; | |
| 467 break; | |
| 468 case TokenKind.GREATER: | |
| 469 _eat(TokenKind.GREATER); | |
| 470 combinatorType = TokenKind.COMBINATOR_GREATER; | |
| 471 break; | |
| 472 case TokenKind.TILDE: | |
| 473 _eat(TokenKind.TILDE); | |
| 474 combinatorType = TokenKind.COMBINATOR_TILDE; | |
| 475 break; | |
| 476 } | |
| 477 | |
| 478 // Check if WHITESPACE existed between tokens if so we're descendent. | |
| 479 if (combinatorType == TokenKind.COMBINATOR_NONE && !forceCombinatorNone) { | |
| 480 if (this._previousToken != null && | |
| 481 this._previousToken.end != this._peekToken.start) { | |
| 482 combinatorType = TokenKind.COMBINATOR_DESCENDANT; | |
| 483 } | |
| 484 } | |
| 485 | |
| 486 var simpleSel = simpleSelector(); | |
| 487 if (simpleSel != null) { | |
| 488 return new SimpleSelectorSequence(simpleSel, _makeSpan(start), | |
| 489 combinatorType); | |
| 490 } | |
| 491 } | |
| 492 | |
| 493 /** | |
| 494 * Simple selector grammar: | |
| 495 * | |
| 496 * simple_selector_sequence | |
| 497 * : [ type_selector | universal ] | |
| 498 * [ HASH | class | attrib | pseudo | negation ]* | |
| 499 * | [ HASH | class | attrib | pseudo | negation ]+ | |
| 500 * type_selector | |
| 501 * : [ namespace_prefix ]? element_name | |
| 502 * namespace_prefix | |
| 503 * : [ IDENT | '*' ]? '|' | |
| 504 * element_name | |
| 505 * : IDENT | |
| 506 * universal | |
| 507 * : [ namespace_prefix ]? '*' | |
| 508 * class | |
| 509 * : '.' IDENT | |
| 510 */ | |
| 511 simpleSelector() { | |
| 512 // TODO(terry): Nathan makes a good point parsing of namespace and element | |
| 513 // are essentially the same (asterisk or identifier) other | |
| 514 // than the error message for element. Should consolidate the | |
| 515 // code. | |
| 516 // TODO(terry): Need to handle attribute namespace too. | |
| 517 var first; | |
| 518 int start = _peekToken.start; | |
| 519 switch (_peek()) { | |
| 520 case TokenKind.ASTERISK: | |
| 521 // Mark as universal namespace. | |
| 522 var tok = _next(); | |
| 523 first = new Wildcard(_makeSpan(tok.start)); | |
| 524 break; | |
| 525 case TokenKind.IDENTIFIER: | |
| 526 int startIdent = _peekToken.start; | |
| 527 first = identifier(); | |
| 528 break; | |
| 529 } | |
| 530 | |
| 531 if (_maybeEat(TokenKind.NAMESPACE)) { | |
| 532 var element; | |
| 533 switch (_peek()) { | |
| 534 case TokenKind.ASTERISK: | |
| 535 // Mark as universal element | |
| 536 var tok = _next(); | |
| 537 element = new Wildcard(_makeSpan(tok.start)); | |
| 538 break; | |
| 539 case TokenKind.IDENTIFIER: | |
| 540 element = identifier(); | |
| 541 break; | |
| 542 default: | |
| 543 _error('expected element name or universal(*), but found $_peekToken', | |
| 544 _peekToken.span); | |
| 545 } | |
| 546 | |
| 547 return new NamespaceSelector(first, | |
| 548 new ElementSelector(element, element.span), _makeSpan(start)); | |
| 549 } else if (first != null) { | |
| 550 return new ElementSelector(first, _makeSpan(start)); | |
| 551 } else { | |
| 552 // Check for HASH | class | attrib | pseudo | negation | |
| 553 return simpleSelectorTail(); | |
| 554 } | |
| 555 } | |
| 556 | |
| 557 simpleSelectorTail() { | |
| 558 // Check for HASH | class | attrib | pseudo | negation | |
| 559 int start = _peekToken.start; | |
| 560 switch (_peek()) { | |
| 561 case TokenKind.HASH: | |
| 562 _eat(TokenKind.HASH); | |
| 563 return new IdSelector(identifier(), _makeSpan(start)); | |
| 564 case TokenKind.DOT: | |
| 565 _eat(TokenKind.DOT); | |
| 566 return new ClassSelector(identifier(), _makeSpan(start)); | |
| 567 case TokenKind.COLON: | |
| 568 // :pseudo-class ::pseudo-element | |
| 569 // TODO(terry): '::' should be token. | |
| 570 _eat(TokenKind.COLON); | |
| 571 bool pseudoElement = _maybeEat(TokenKind.COLON); | |
| 572 var name = identifier(); | |
| 573 // TODO(terry): Need to handle specific pseudo class/element name and | |
| 574 // backward compatible names that are : as well as :: as well as | |
| 575 // parameters. | |
| 576 return pseudoElement ? | |
| 577 new PseudoElementSelector(name, _makeSpan(start)) : | |
| 578 new PseudoClassSelector(name, _makeSpan(start)); | |
| 579 case TokenKind.LBRACK: | |
| 580 return processAttribute(); | |
| 581 } | |
| 582 } | |
| 583 | |
| 584 // Attribute grammar: | |
| 585 // | |
| 586 // attributes : | |
| 587 // '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']' | |
| 588 // | |
| 589 // ATTRIB_MATCHES : | |
| 590 // [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ] | |
| 591 // | |
| 592 // INCLUDES: '~=' | |
| 593 // | |
| 594 // DASHMATCH: '|=' | |
| 595 // | |
| 596 // PREFIXMATCH: '^=' | |
| 597 // | |
| 598 // SUFFIXMATCH: '$=' | |
| 599 // | |
| 600 // SUBSTRMATCH: '*=' | |
| 601 // | |
| 602 // | |
| 603 processAttribute() { | |
| 604 int start = _peekToken.start; | |
| 605 | |
| 606 if (_maybeEat(TokenKind.LBRACK)) { | |
| 607 var attrName = identifier(); | |
| 608 | |
| 609 int op = TokenKind.NO_MATCH; | |
| 610 switch (_peek()) { | |
| 611 case TokenKind.EQUALS: | |
| 612 case TokenKind.INCLUDES: // ~= | |
| 613 case TokenKind.DASH_MATCH: // |= | |
| 614 case TokenKind.PREFIX_MATCH: // ^= | |
| 615 case TokenKind.SUFFIX_MATCH: // $= | |
| 616 case TokenKind.SUBSTRING_MATCH: // *= | |
| 617 op = _peek(); | |
| 618 _next(); | |
| 619 break; | |
| 620 } | |
| 621 | |
| 622 String value; | |
| 623 if (op != TokenKind.NO_MATCH) { | |
| 624 // Operator hit so we require a value too. | |
| 625 if (_peekIdentifier()) { | |
| 626 value = identifier(); | |
| 627 } else { | |
| 628 value = processQuotedString(false); | |
| 629 } | |
| 630 | |
| 631 if (value == null) { | |
| 632 _error('expected attribute value string or ident', _peekToken.span); | |
| 633 } | |
| 634 } | |
| 635 | |
| 636 _eat(TokenKind.RBRACK); | |
| 637 | |
| 638 return new AttributeSelector(attrName, op, value, _makeSpan(start)); | |
| 639 } | |
| 640 } | |
| 641 | |
| 642 // Declaration grammar: | |
| 643 // | |
| 644 // declaration: property ':' expr prio? | |
| 645 // | |
| 646 // property: IDENT | |
| 647 // prio: !important | |
| 648 // expr: (see processExpr) | |
| 649 // | |
| 650 processDeclaration() { | |
| 651 Declaration decl; | |
| 652 | |
| 653 int start = _peekToken.start; | |
| 654 | |
| 655 // IDENT ':' expr '!important'? | |
| 656 if (TokenKind.isIdentifier(_peekToken.kind)) { | |
| 657 var propertyIdent = identifier(); | |
| 658 _eat(TokenKind.COLON); | |
| 659 | |
| 660 decl = new Declaration(propertyIdent, processExpr(), _makeSpan(start)); | |
| 661 | |
| 662 // Handle !important (prio) | |
| 663 decl.important = _maybeEat(TokenKind.IMPORTANT); | |
| 664 } | |
| 665 | |
| 666 return decl; | |
| 667 } | |
| 668 | |
| 669 // Expression grammar: | |
| 670 // | |
| 671 // expression: term [ operator? term]* | |
| 672 // | |
| 673 // operator: '/' | ',' | |
| 674 // term: (see processTerm) | |
| 675 // | |
| 676 processExpr() { | |
| 677 int start = _peekToken.start; | |
| 678 Expressions expressions = new Expressions(_makeSpan(start)); | |
| 679 | |
| 680 bool keepGoing = true; | |
| 681 var expr; | |
| 682 while (keepGoing && (expr = processTerm()) != null) { | |
| 683 var op; | |
| 684 | |
| 685 int opStart = _peekToken.start; | |
| 686 | |
| 687 switch (_peek()) { | |
| 688 case TokenKind.SLASH: | |
| 689 op = new OperatorSlash(_makeSpan(opStart)); | |
| 690 break; | |
| 691 case TokenKind.COMMA: | |
| 692 op = new OperatorComma(_makeSpan(opStart)); | |
| 693 break; | |
| 694 } | |
| 695 | |
| 696 if (expr != null) { | |
| 697 expressions.add(expr); | |
| 698 } else { | |
| 699 keepGoing = false; | |
| 700 } | |
| 701 | |
| 702 if (op != null) { | |
| 703 expressions.add(op); | |
| 704 _next(); | |
| 705 } | |
| 706 } | |
| 707 | |
| 708 return expressions; | |
| 709 } | |
| 710 | |
| 711 // Term grammar: | |
| 712 // | |
| 713 // term: | |
| 714 // unary_operator? | |
| 715 // [ term_value ] | |
| 716 // | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor | |
| 717 // | |
| 718 // term_value: | |
| 719 // NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* | | |
| 720 // TIME S* | FREQ S* | function | |
| 721 // | |
| 722 // NUMBER: {num} | |
| 723 // PERCENTAGE: {num}% | |
| 724 // LENGTH: {num}['px' | 'cm' | 'mm' | 'in' | 'pt' | 'pc'] | |
| 725 // EMS: {num}'em' | |
| 726 // EXS: {num}'ex' | |
| 727 // ANGLE: {num}['deg' | 'rad' | 'grad'] | |
| 728 // TIME: {num}['ms' | 's'] | |
| 729 // FREQ: {num}['hz' | 'khz'] | |
| 730 // function: IDENT '(' expr ')' | |
| 731 // | |
| 732 processTerm() { | |
| 733 int start = _peekToken.start; | |
| 734 Token t; // token for term's value | |
| 735 var value; // value of term (numeric values) | |
| 736 | |
| 737 var unary = ""; | |
| 738 | |
| 739 switch (_peek()) { | |
| 740 case TokenKind.HASH: | |
| 741 this._eat(TokenKind.HASH); | |
| 742 String hexText; | |
| 743 if (_peekKind(TokenKind.INTEGER)) { | |
| 744 String hexText1 = _peekToken.text; | |
| 745 _next(); | |
| 746 if (_peekIdentifier()) { | |
| 747 hexText = '${hexText1}${identifier().name}'; | |
| 748 } else { | |
| 749 hexText = hexText1; | |
| 750 } | |
| 751 } else if (_peekIdentifier()) { | |
| 752 hexText = identifier().name; | |
| 753 } else { | |
| 754 _errorExpected("hex number"); | |
| 755 } | |
| 756 | |
| 757 try { | |
| 758 int hexValue = parseHex(hexText); | |
| 759 return new HexColorTerm(hexValue, hexText, _makeSpan(start)); | |
| 760 } on HexNumberException catch (hne) { | |
| 761 _error('Bad hex number', _makeSpan(start)); | |
| 762 } | |
| 763 break; | |
| 764 case TokenKind.INTEGER: | |
| 765 t = _next(); | |
| 766 value = int.parse("${unary}${t.text}"); | |
| 767 break; | |
| 768 case TokenKind.DOUBLE: | |
| 769 t = _next(); | |
| 770 value = double.parse("${unary}${t.text}"); | |
| 771 break; | |
| 772 case TokenKind.SINGLE_QUOTE: | |
| 773 case TokenKind.DOUBLE_QUOTE: | |
| 774 value = processQuotedString(false); | |
| 775 value = '"${value}"'; | |
| 776 return new LiteralTerm(value, value, _makeSpan(start)); | |
| 777 case TokenKind.LPAREN: | |
| 778 _next(); | |
| 779 | |
| 780 GroupTerm group = new GroupTerm(_makeSpan(start)); | |
| 781 | |
| 782 do { | |
| 783 var term = processTerm(); | |
| 784 if (term != null && term is LiteralTerm) { | |
| 785 group.add(term); | |
| 786 } | |
| 787 } while (!_maybeEat(TokenKind.RPAREN)); | |
| 788 | |
| 789 return group; | |
| 790 case TokenKind.LBRACK: | |
| 791 _next(); | |
| 792 | |
| 793 var term = processTerm(); | |
| 794 if (!(term is NumberTerm)) { | |
| 795 _error('Expecting a positive number', _makeSpan(start)); | |
| 796 } | |
| 797 | |
| 798 _eat(TokenKind.RBRACK); | |
| 799 | |
| 800 return new ItemTerm(term.value, term.text, _makeSpan(start)); | |
| 801 case TokenKind.IDENTIFIER: | |
| 802 var nameValue = identifier(); // Snarf up the ident we'll remap, maybe. | |
| 803 | |
| 804 if (_maybeEat(TokenKind.LPAREN)) { | |
| 805 // FUNCTION | |
| 806 return processFunction(nameValue); | |
| 807 } else { | |
| 808 // TODO(terry): Need to have a list of known identifiers today only | |
| 809 // 'from' is special. | |
| 810 if (nameValue.name == 'from') { | |
| 811 return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start)); | |
| 812 } | |
| 813 | |
| 814 // What kind of identifier is it? | |
| 815 try { | |
| 816 // Named color? | |
| 817 int colorValue = TokenKind.matchColorName(nameValue.name); | |
| 818 | |
| 819 // Yes, process the color as an RGB value. | |
| 820 String rgbColor = TokenKind.decimalToHex(colorValue, 3); | |
| 821 try { | |
| 822 colorValue = parseHex(rgbColor); | |
| 823 } on HexNumberException catch (hne) { | |
| 824 _error('Bad hex number', _makeSpan(start)); | |
| 825 } | |
| 826 return new HexColorTerm(colorValue, rgbColor, _makeSpan(start)); | |
| 827 } catch (error) { | |
| 828 if (error is NoColorMatchException) { | |
| 829 // TODO(terry): Other named things to match with validator? | |
| 830 | |
| 831 // TODO(terry): Disable call to _warning need one World class for | |
| 832 // both CSS parser and other parser (e.g., template) | |
| 833 // so all warnings, errors, options, etc. are driven | |
| 834 // from the one World. | |
| 835 // _warning('Unknown property value ${error.name}', _makeSpan(start)); | |
| 836 return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start)); | |
| 837 } | |
| 838 } | |
| 839 } | |
| 840 } | |
| 841 | |
| 842 var term; | |
| 843 var unitType = this._peek(); | |
| 844 | |
| 845 switch (unitType) { | |
| 846 case TokenKind.UNIT_EM: | |
| 847 term = new EmTerm(value, t.text, _makeSpan(start)); | |
| 848 _next(); // Skip the unit | |
| 849 break; | |
| 850 case TokenKind.UNIT_EX: | |
| 851 term = new ExTerm(value, t.text, _makeSpan(start)); | |
| 852 _next(); // Skip the unit | |
| 853 break; | |
| 854 case TokenKind.UNIT_LENGTH_PX: | |
| 855 case TokenKind.UNIT_LENGTH_CM: | |
| 856 case TokenKind.UNIT_LENGTH_MM: | |
| 857 case TokenKind.UNIT_LENGTH_IN: | |
| 858 case TokenKind.UNIT_LENGTH_PT: | |
| 859 case TokenKind.UNIT_LENGTH_PC: | |
| 860 term = new LengthTerm(value, t.text, _makeSpan(start), unitType); | |
| 861 _next(); // Skip the unit | |
| 862 break; | |
| 863 case TokenKind.UNIT_ANGLE_DEG: | |
| 864 case TokenKind.UNIT_ANGLE_RAD: | |
| 865 case TokenKind.UNIT_ANGLE_GRAD: | |
| 866 term = new AngleTerm(value, t.text, _makeSpan(start), unitType); | |
| 867 _next(); // Skip the unit | |
| 868 break; | |
| 869 case TokenKind.UNIT_TIME_MS: | |
| 870 case TokenKind.UNIT_TIME_S: | |
| 871 term = new TimeTerm(value, t.text, _makeSpan(start), unitType); | |
| 872 _next(); // Skip the unit | |
| 873 break; | |
| 874 case TokenKind.UNIT_FREQ_HZ: | |
| 875 case TokenKind.UNIT_FREQ_KHZ: | |
| 876 term = new FreqTerm(value, t.text, _makeSpan(start), unitType); | |
| 877 _next(); // Skip the unit | |
| 878 break; | |
| 879 case TokenKind.PERCENT: | |
| 880 term = new PercentageTerm(value, t.text, _makeSpan(start)); | |
| 881 _next(); // Skip the % | |
| 882 break; | |
| 883 case TokenKind.UNIT_FRACTION: | |
| 884 term = new FractionTerm(value, t.text, _makeSpan(start)); | |
| 885 _next(); // Skip the unit | |
| 886 break; | |
| 887 default: | |
| 888 if (value != null) { | |
| 889 term = new NumberTerm(value, t.text, _makeSpan(start)); | |
| 890 } | |
| 891 } | |
| 892 | |
| 893 return term; | |
| 894 } | |
| 895 | |
| 896 processQuotedString([bool urlString = false]) { | |
| 897 int start = _peekToken.start; | |
| 898 | |
| 899 // URI term sucks up everything inside of quotes(' or ") or between parens | |
| 900 int stopToken = urlString ? TokenKind.RPAREN : -1; | |
| 901 switch (_peek()) { | |
| 902 case TokenKind.SINGLE_QUOTE: | |
| 903 stopToken = TokenKind.SINGLE_QUOTE; | |
| 904 _next(); // Skip the SINGLE_QUOTE. | |
| 905 break; | |
| 906 case TokenKind.DOUBLE_QUOTE: | |
| 907 stopToken = TokenKind.DOUBLE_QUOTE; | |
| 908 _next(); // Skip the DOUBLE_QUOTE. | |
| 909 break; | |
| 910 default: | |
| 911 if (urlString) { | |
| 912 if (_peek() == TokenKind.LPAREN) { | |
| 913 _next(); // Skip the LPAREN. | |
| 914 } | |
| 915 stopToken = TokenKind.RPAREN; | |
| 916 } else { | |
| 917 _error('unexpected string', _makeSpan(start)); | |
| 918 } | |
| 919 } | |
| 920 | |
| 921 StringBuffer stringValue = new StringBuffer(); | |
| 922 | |
| 923 // Gobble up everything until we hit our stop token. | |
| 924 int runningStart = _peekToken.start; | |
| 925 while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) { | |
| 926 var tok = _next(); | |
| 927 stringValue.write(tok.text); | |
| 928 } | |
| 929 | |
| 930 if (stopToken != TokenKind.RPAREN) { | |
| 931 _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE; | |
| 932 } | |
| 933 | |
| 934 return stringValue.toString(); | |
| 935 } | |
| 936 | |
| 937 // Function grammar: | |
| 938 // | |
| 939 // function: IDENT '(' expr ')' | |
| 940 // | |
| 941 processFunction(Identifier func) { | |
| 942 int start = _peekToken.start; | |
| 943 | |
| 944 String name = func.name; | |
| 945 | |
| 946 switch (name) { | |
| 947 case 'url': | |
| 948 // URI term sucks up everything inside of quotes(' or ") or between parens | |
| 949 String urlParam = processQuotedString(true); | |
| 950 | |
| 951 // TODO(terry): Better error messge and checking for mismatched quotes. | |
| 952 if (_peek() == TokenKind.END_OF_FILE) { | |
| 953 _error("problem parsing URI", _peekToken.span); | |
| 954 } | |
| 955 | |
| 956 if (_peek() == TokenKind.RPAREN) { | |
| 957 _next(); | |
| 958 } | |
| 959 | |
| 960 return new UriTerm(urlParam, _makeSpan(start)); | |
| 961 case 'calc': | |
| 962 // TODO(terry): Implement expression handling... | |
| 963 break; | |
| 964 default: | |
| 965 var expr = processExpr(); | |
| 966 if (!_maybeEat(TokenKind.RPAREN)) { | |
| 967 _error("problem parsing function expected ), ", _peekToken.span); | |
| 968 } | |
| 969 | |
| 970 return new FunctionTerm(name, name, expr, _makeSpan(start)); | |
| 971 } | |
| 972 | |
| 973 return null; | |
| 974 } | |
| 975 | |
| 976 identifier() { | |
| 977 var tok = _next(); | |
| 978 if (!TokenKind.isIdentifier(tok.kind)) { | |
| 979 _error('expected identifier, but found $tok', tok.span); | |
| 980 } | |
| 981 | |
| 982 return new Identifier(tok.text, _makeSpan(tok.start)); | |
| 983 } | |
| 984 | |
| 985 // TODO(terry): Move this to base <= 36 and into shared code. | |
| 986 static int _hexDigit(int c) { | |
| 987 if(c >= 48/*0*/ && c <= 57/*9*/) { | |
| 988 return c - 48; | |
| 989 } else if (c >= 97/*a*/ && c <= 102/*f*/) { | |
| 990 return c - 87; | |
| 991 } else if (c >= 65/*A*/ && c <= 70/*F*/) { | |
| 992 return c - 55; | |
| 993 } else { | |
| 994 return -1; | |
| 995 } | |
| 996 } | |
| 997 | |
| 998 static int parseHex(String hex) { | |
| 999 var result = 0; | |
| 1000 | |
| 1001 for (int i = 0; i < hex.length; i++) { | |
| 1002 var digit = _hexDigit(hex.codeUnitAt(i)); | |
| 1003 if (digit < 0) { | |
| 1004 throw new HexNumberException(); | |
| 1005 } | |
| 1006 result = (result << 4) + digit; | |
| 1007 } | |
| 1008 | |
| 1009 return result; | |
| 1010 } | |
| 1011 } | |
| 1012 | |
| 1013 /** Not a hex number. */ | |
| 1014 class HexNumberException implements Exception { | |
| 1015 HexNumberException(); | |
| 1016 } | |
| 1017 | |
| OLD | NEW |