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