| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, 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 library csslib.parser; | |
| 6 | |
| 7 import 'dart:math' as math; | |
| 8 | |
| 9 import 'package:source_span/source_span.dart'; | |
| 10 | |
| 11 import "visitor.dart"; | |
| 12 import 'src/messages.dart'; | |
| 13 import 'src/options.dart'; | |
| 14 | |
| 15 part 'src/analyzer.dart'; | |
| 16 part 'src/polyfill.dart'; | |
| 17 part 'src/property.dart'; | |
| 18 part 'src/token.dart'; | |
| 19 part 'src/tokenizer_base.dart'; | |
| 20 part 'src/tokenizer.dart'; | |
| 21 part 'src/tokenkind.dart'; | |
| 22 | |
| 23 | |
| 24 /** Used for parser lookup ahead (used for nested selectors Less support). */ | |
| 25 class ParserState extends TokenizerState { | |
| 26 final Token peekToken; | |
| 27 final Token previousToken; | |
| 28 | |
| 29 ParserState(this.peekToken, this.previousToken, Tokenizer tokenizer) | |
| 30 : super(tokenizer); | |
| 31 } | |
| 32 | |
| 33 // TODO(jmesserly): this should not be global | |
| 34 void _createMessages({List<Message> errors, List<String> options}) { | |
| 35 if (errors == null) errors = []; | |
| 36 | |
| 37 if (options == null) { | |
| 38 options = ['--no-colors', 'memory']; | |
| 39 } | |
| 40 var opt = PreprocessorOptions.parse(options); | |
| 41 messages = new Messages(options: opt, printHandler: errors.add); | |
| 42 } | |
| 43 | |
| 44 /** CSS checked mode enabled. */ | |
| 45 bool get isChecked => messages.options.checked; | |
| 46 | |
| 47 // TODO(terry): Remove nested name parameter. | |
| 48 /** Parse and analyze the CSS file. */ | |
| 49 StyleSheet compile(input, {List<Message> errors, List<String> options, | |
| 50 bool nested: true, | |
| 51 bool polyfill: false, | |
| 52 List<StyleSheet> includes: null}) { | |
| 53 | |
| 54 if (includes == null) { | |
| 55 includes = []; | |
| 56 } | |
| 57 | |
| 58 var source = _inputAsString(input); | |
| 59 | |
| 60 _createMessages(errors: errors, options: options); | |
| 61 | |
| 62 var file = new SourceFile(source); | |
| 63 | |
| 64 var tree = new _Parser(file, source).parse(); | |
| 65 | |
| 66 analyze([tree], errors: errors, options: options); | |
| 67 | |
| 68 if (polyfill) { | |
| 69 var processCss = new PolyFill(messages, true); | |
| 70 processCss.process(tree, includes: includes); | |
| 71 } | |
| 72 | |
| 73 return tree; | |
| 74 } | |
| 75 | |
| 76 /** Analyze the CSS file. */ | |
| 77 void analyze(List<StyleSheet> styleSheets, | |
| 78 {List<Message> errors, List<String> options}) { | |
| 79 | |
| 80 _createMessages(errors: errors, options: options); | |
| 81 new Analyzer(styleSheets, messages).run(); | |
| 82 } | |
| 83 | |
| 84 /** | |
| 85 * Parse the [input] CSS stylesheet into a tree. The [input] can be a [String], | |
| 86 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional | |
| 87 * [errors] list will contain each error/warning as a [Message]. | |
| 88 */ | |
| 89 StyleSheet parse(input, {List<Message> errors, List<String> options}) { | |
| 90 var source = _inputAsString(input); | |
| 91 | |
| 92 _createMessages(errors: errors, options: options); | |
| 93 | |
| 94 var file = new SourceFile(source); | |
| 95 return new _Parser(file, source).parse(); | |
| 96 } | |
| 97 | |
| 98 /** | |
| 99 * Parse the [input] CSS selector into a tree. The [input] can be a [String], | |
| 100 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional | |
| 101 * [errors] list will contain each error/warning as a [Message]. | |
| 102 */ | |
| 103 // TODO(jmesserly): should rename "parseSelector" and return Selector | |
| 104 StyleSheet selector(input, {List<Message> errors}) { | |
| 105 var source = _inputAsString(input); | |
| 106 | |
| 107 _createMessages(errors: errors); | |
| 108 | |
| 109 var file = new SourceFile(source); | |
| 110 return (new _Parser(file, source) | |
| 111 ..tokenizer.inSelector = true) | |
| 112 .parseSelector(); | |
| 113 } | |
| 114 | |
| 115 SelectorGroup parseSelectorGroup(input, {List<Message> errors}) { | |
| 116 var source = _inputAsString(input); | |
| 117 | |
| 118 _createMessages(errors: errors); | |
| 119 | |
| 120 var file = new SourceFile(source); | |
| 121 return (new _Parser(file, source) | |
| 122 // TODO(jmesserly): this fix should be applied to the parser. It's tricky | |
| 123 // because by the time the flag is set one token has already been fetched. | |
| 124 ..tokenizer.inSelector = true) | |
| 125 .processSelectorGroup(); | |
| 126 } | |
| 127 | |
| 128 String _inputAsString(input) { | |
| 129 String source; | |
| 130 | |
| 131 if (input is String) { | |
| 132 source = input; | |
| 133 } else if (input is List<int>) { | |
| 134 // TODO(terry): The parse function needs an "encoding" argument and will | |
| 135 // default to whatever encoding CSS defaults to. | |
| 136 // | |
| 137 // Here's some info about CSS encodings: | |
| 138 // http://www.w3.org/International/questions/qa-css-charset.en.php | |
| 139 // | |
| 140 // As JMesserly suggests it will probably need a "preparser" html5lib | |
| 141 // (encoding_parser.dart) that interprets the bytes as ASCII and scans for | |
| 142 // @charset. But for now an "encoding" argument would work. Often the | |
| 143 // HTTP header will indicate the correct encoding. | |
| 144 // | |
| 145 // See encoding helpers at: package:html5lib/lib/src/char_encodings.dart | |
| 146 // These helpers can decode in different formats given an encoding name | |
| 147 // (mostly unicode, ascii, windows-1252 which is html5 default encoding). | |
| 148 source = new String.fromCharCodes(input); | |
| 149 } else { | |
| 150 // TODO(terry): Support RandomAccessFile using console. | |
| 151 throw new ArgumentError("'source' must be a String or " | |
| 152 "List<int> (of bytes). RandomAccessFile not supported from this " | |
| 153 "simple interface"); | |
| 154 } | |
| 155 | |
| 156 return source; | |
| 157 } | |
| 158 | |
| 159 // TODO(terry): Consider removing this class when all usages can be eliminated | |
| 160 // or replaced with compile API. | |
| 161 /** Public parsing interface for csslib. */ | |
| 162 class Parser { | |
| 163 final _Parser _parser; | |
| 164 | |
| 165 // TODO(jmesserly): having file and text is redundant. | |
| 166 Parser(SourceFile file, String text, {int start: 0, String baseUrl}) : | |
| 167 _parser = new _Parser(file, text, start: start, baseUrl: baseUrl); | |
| 168 | |
| 169 StyleSheet parse() => _parser.parse(); | |
| 170 } | |
| 171 | |
| 172 /** A simple recursive descent parser for CSS. */ | |
| 173 class _Parser { | |
| 174 final Tokenizer tokenizer; | |
| 175 | |
| 176 /** Base url of CSS file. */ | |
| 177 final String _baseUrl; | |
| 178 | |
| 179 /** | |
| 180 * File containing the source being parsed, used to report errors with | |
| 181 * source-span locations. | |
| 182 */ | |
| 183 final SourceFile file; | |
| 184 | |
| 185 Token _previousToken; | |
| 186 Token _peekToken; | |
| 187 | |
| 188 _Parser(SourceFile file, String text, {int start: 0, String baseUrl}) | |
| 189 : this.file = file, | |
| 190 _baseUrl = baseUrl, | |
| 191 tokenizer = new Tokenizer(file, text, true, start) { | |
| 192 _peekToken = tokenizer.next(); | |
| 193 } | |
| 194 | |
| 195 /** Main entry point for parsing an entire CSS file. */ | |
| 196 StyleSheet parse() { | |
| 197 List<TreeNode> productions = []; | |
| 198 | |
| 199 var start = _peekToken.span; | |
| 200 while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) { | |
| 201 // TODO(terry): Need to handle charset. | |
| 202 var directive = processDirective(); | |
| 203 if (directive != null) { | |
| 204 productions.add(directive); | |
| 205 _maybeEat(TokenKind.SEMICOLON); | |
| 206 } else { | |
| 207 RuleSet ruleset = processRuleSet(); | |
| 208 if (ruleset != null) { | |
| 209 productions.add(ruleset); | |
| 210 } else { | |
| 211 break; | |
| 212 } | |
| 213 } | |
| 214 } | |
| 215 | |
| 216 checkEndOfFile(); | |
| 217 | |
| 218 return new StyleSheet(productions, _makeSpan(start)); | |
| 219 } | |
| 220 | |
| 221 /** Main entry point for parsing a simple selector sequence. */ | |
| 222 StyleSheet parseSelector() { | |
| 223 List<TreeNode> productions = []; | |
| 224 | |
| 225 var start = _peekToken.span; | |
| 226 while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) { | |
| 227 var selector = processSelector(); | |
| 228 if (selector != null) { | |
| 229 productions.add(selector); | |
| 230 } | |
| 231 } | |
| 232 | |
| 233 checkEndOfFile(); | |
| 234 | |
| 235 return new StyleSheet.selector(productions, _makeSpan(start)); | |
| 236 } | |
| 237 | |
| 238 /** Generate an error if [file] has not been completely consumed. */ | |
| 239 void checkEndOfFile() { | |
| 240 if (!(_peekKind(TokenKind.END_OF_FILE) || | |
| 241 _peekKind(TokenKind.INCOMPLETE_COMMENT))) { | |
| 242 _error('premature end of file unknown CSS', _peekToken.span); | |
| 243 } | |
| 244 } | |
| 245 | |
| 246 /** Guard to break out of parser when an unexpected end of file is found. */ | |
| 247 // TODO(jimhug): Failure to call this method can lead to inifinite parser | |
| 248 // loops. Consider embracing exceptions for more errors to reduce | |
| 249 // the danger here. | |
| 250 bool isPrematureEndOfFile() { | |
| 251 if (_maybeEat(TokenKind.END_OF_FILE)) { | |
| 252 _error('unexpected end of file', _peekToken.span); | |
| 253 return true; | |
| 254 } else { | |
| 255 return false; | |
| 256 } | |
| 257 } | |
| 258 | |
| 259 /////////////////////////////////////////////////////////////////// | |
| 260 // Basic support methods | |
| 261 /////////////////////////////////////////////////////////////////// | |
| 262 int _peek() { | |
| 263 return _peekToken.kind; | |
| 264 } | |
| 265 | |
| 266 Token _next({unicodeRange : false}) { | |
| 267 _previousToken = _peekToken; | |
| 268 _peekToken = tokenizer.next(unicodeRange: unicodeRange); | |
| 269 return _previousToken; | |
| 270 } | |
| 271 | |
| 272 bool _peekKind(int kind) { | |
| 273 return _peekToken.kind == kind; | |
| 274 } | |
| 275 | |
| 276 /* Is the next token a legal identifier? This includes pseudo-keywords. */ | |
| 277 bool _peekIdentifier() { | |
| 278 return TokenKind.isIdentifier(_peekToken.kind); | |
| 279 } | |
| 280 | |
| 281 /** Marks the parser/tokenizer look ahead to support Less nested selectors. */ | |
| 282 ParserState get _mark => | |
| 283 new ParserState(_peekToken, _previousToken, tokenizer); | |
| 284 | |
| 285 /** Restores the parser/tokenizer state to state remembered by _mark. */ | |
| 286 void _restore(ParserState markedData) { | |
| 287 tokenizer.restore(markedData); | |
| 288 _peekToken = markedData.peekToken; | |
| 289 _previousToken = markedData.previousToken; | |
| 290 } | |
| 291 | |
| 292 bool _maybeEat(int kind, {unicodeRange : false}) { | |
| 293 if (_peekToken.kind == kind) { | |
| 294 _previousToken = _peekToken; | |
| 295 _peekToken = tokenizer.next(unicodeRange: unicodeRange); | |
| 296 return true; | |
| 297 } else { | |
| 298 return false; | |
| 299 } | |
| 300 } | |
| 301 | |
| 302 void _eat(int kind, {unicodeRange : false}) { | |
| 303 if (!_maybeEat(kind, unicodeRange: unicodeRange)) { | |
| 304 _errorExpected(TokenKind.kindToString(kind)); | |
| 305 } | |
| 306 } | |
| 307 | |
| 308 void _eatSemicolon() { | |
| 309 _eat(TokenKind.SEMICOLON); | |
| 310 } | |
| 311 | |
| 312 void _errorExpected(String expected) { | |
| 313 var tok = _next(); | |
| 314 var message; | |
| 315 try { | |
| 316 message = 'expected $expected, but found $tok'; | |
| 317 } catch (e) { | |
| 318 message = 'parsing error expected $expected'; | |
| 319 } | |
| 320 _error(message, tok.span); | |
| 321 } | |
| 322 | |
| 323 void _error(String message, SourceSpan location) { | |
| 324 if (location == null) { | |
| 325 location = _peekToken.span; | |
| 326 } | |
| 327 messages.error(message, location); | |
| 328 } | |
| 329 | |
| 330 void _warning(String message, SourceSpan location) { | |
| 331 if (location == null) { | |
| 332 location = _peekToken.span; | |
| 333 } | |
| 334 messages.warning(message, location); | |
| 335 } | |
| 336 | |
| 337 SourceSpan _makeSpan(FileSpan start) { | |
| 338 // TODO(terry): there are places where we are creating spans before we eat | |
| 339 // the tokens, so using _previousToken is not always valid. | |
| 340 // TODO(nweiz): use < rather than compareTo when SourceSpan supports it. | |
| 341 if (_previousToken == null || _previousToken.span.compareTo(start) < 0) { | |
| 342 return start; | |
| 343 } | |
| 344 return start.expand(_previousToken.span); | |
| 345 } | |
| 346 | |
| 347 /////////////////////////////////////////////////////////////////// | |
| 348 // Top level productions | |
| 349 /////////////////////////////////////////////////////////////////// | |
| 350 | |
| 351 /** | |
| 352 * The media_query_list production below replaces the media_list production | |
| 353 * from CSS2 the new grammar is: | |
| 354 * | |
| 355 * media_query_list | |
| 356 * : S* [media_query [ ',' S* media_query ]* ]? | |
| 357 * media_query | |
| 358 * : [ONLY | NOT]? S* media_type S* [ AND S* expression ]* | |
| 359 * | expression [ AND S* expression ]* | |
| 360 * media_type | |
| 361 * : IDENT | |
| 362 * expression | |
| 363 * : '(' S* media_feature S* [ ':' S* expr ]? ')' S* | |
| 364 * media_feature | |
| 365 * : IDENT | |
| 366 */ | |
| 367 List<MediaQuery> processMediaQueryList() { | |
| 368 var mediaQueries = []; | |
| 369 | |
| 370 bool firstTime = true; | |
| 371 var mediaQuery; | |
| 372 do { | |
| 373 mediaQuery = processMediaQuery(firstTime == true); | |
| 374 if (mediaQuery != null) { | |
| 375 mediaQueries.add(mediaQuery); | |
| 376 firstTime = false; | |
| 377 continue; | |
| 378 } | |
| 379 | |
| 380 // Any more more media types separated by comma. | |
| 381 if (!_maybeEat(TokenKind.COMMA)) break; | |
| 382 | |
| 383 // Yep more media types start again. | |
| 384 firstTime = true; | |
| 385 } while ((!firstTime && mediaQuery != null) || firstTime); | |
| 386 | |
| 387 return mediaQueries; | |
| 388 } | |
| 389 | |
| 390 MediaQuery processMediaQuery([bool startQuery = true]) { | |
| 391 // Grammar: [ONLY | NOT]? S* media_type S* | |
| 392 // [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]* | |
| 393 | |
| 394 var start = _peekToken.span; | |
| 395 | |
| 396 // Is it a unary media operator? | |
| 397 var op = _peekToken.text; | |
| 398 var opLen = op.length; | |
| 399 var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen); | |
| 400 if (unaryOp != -1) { | |
| 401 if (isChecked) { | |
| 402 if (startQuery && | |
| 403 unaryOp != TokenKind.MEDIA_OP_NOT || | |
| 404 unaryOp != TokenKind.MEDIA_OP_ONLY) { | |
| 405 _warning("Only the unary operators NOT and ONLY allowed", | |
| 406 _makeSpan(start)); | |
| 407 } | |
| 408 if (!startQuery && unaryOp != TokenKind.MEDIA_OP_AND) { | |
| 409 _warning("Only the binary AND operator allowed", _makeSpan(start)); | |
| 410 } | |
| 411 } | |
| 412 _next(); | |
| 413 start = _peekToken.span; | |
| 414 } | |
| 415 | |
| 416 var type; | |
| 417 if (startQuery && unaryOp != TokenKind.MEDIA_OP_AND) { | |
| 418 // Get the media type. | |
| 419 if (_peekIdentifier()) type = identifier(); | |
| 420 } | |
| 421 | |
| 422 var exprs = []; | |
| 423 | |
| 424 if (unaryOp == -1 || unaryOp == TokenKind.MEDIA_OP_AND) { | |
| 425 var andOp = false; | |
| 426 while (true) { | |
| 427 var expr = processMediaExpression(andOp); | |
| 428 if (expr == null) break; | |
| 429 | |
| 430 exprs.add(expr); | |
| 431 op = _peekToken.text; | |
| 432 opLen = op.length; | |
| 433 andOp = TokenKind.matchMediaOperator(op, 0, opLen) == | |
| 434 TokenKind.MEDIA_OP_AND; | |
| 435 if (!andOp) break; | |
| 436 _next(); | |
| 437 } | |
| 438 } | |
| 439 | |
| 440 if (unaryOp != -1 || type != null || exprs.length > 0) { | |
| 441 return new MediaQuery(unaryOp, type, exprs, _makeSpan(start)); | |
| 442 } | |
| 443 } | |
| 444 | |
| 445 MediaExpression processMediaExpression([bool andOperator = false]) { | |
| 446 var start = _peekToken.span; | |
| 447 | |
| 448 // Grammar: '(' S* media_feature S* [ ':' S* expr ]? ')' S* | |
| 449 if (_maybeEat(TokenKind.LPAREN)) { | |
| 450 if (_peekIdentifier()) { | |
| 451 var feature = identifier(); // Media feature. | |
| 452 while (_maybeEat(TokenKind.COLON)) { | |
| 453 var startExpr = _peekToken.span; | |
| 454 var exprs = processExpr(); | |
| 455 if (_maybeEat(TokenKind.RPAREN)) { | |
| 456 return new MediaExpression(andOperator, feature, exprs, | |
| 457 _makeSpan(startExpr)); | |
| 458 } else if (isChecked) { | |
| 459 _warning("Missing parenthesis around media expression", | |
| 460 _makeSpan(start)); | |
| 461 return null; | |
| 462 } | |
| 463 } | |
| 464 } else if (isChecked) { | |
| 465 _warning("Missing media feature in media expression", _makeSpan(start)); | |
| 466 return null; | |
| 467 } | |
| 468 } | |
| 469 } | |
| 470 | |
| 471 /** | |
| 472 * Directive grammar: | |
| 473 * | |
| 474 * import: '@import' [string | URI] media_list? | |
| 475 * media: '@media' media_query_list '{' ruleset '}' | |
| 476 * page: '@page' [':' IDENT]? '{' declarations '}' | |
| 477 * stylet: '@stylet' IDENT '{' ruleset '}' | |
| 478 * media_query_list: IDENT [',' IDENT] | |
| 479 * keyframes: '@-webkit-keyframes ...' (see grammar below). | |
| 480 * font_face: '@font-face' '{' declarations '}' | |
| 481 * namespace: '@namespace name url("xmlns") | |
| 482 * host: '@host '{' ruleset '}' | |
| 483 * mixin: '@mixin name [(args,...)] '{' declarations/ruleset '}' | |
| 484 * include: '@include name [(@arg,@arg1)] | |
| 485 * '@include name [(@arg...)] | |
| 486 * content '@content' | |
| 487 */ | |
| 488 processDirective() { | |
| 489 var start = _peekToken.span; | |
| 490 | |
| 491 var tokId = processVariableOrDirective(); | |
| 492 if (tokId is VarDefinitionDirective) return tokId; | |
| 493 switch (tokId) { | |
| 494 case TokenKind.DIRECTIVE_IMPORT: | |
| 495 _next(); | |
| 496 | |
| 497 // @import "uri_string" or @import url("uri_string") are identical; only | |
| 498 // a url can follow an @import. | |
| 499 String importStr; | |
| 500 if (_peekIdentifier()) { | |
| 501 var func = processFunction(identifier()); | |
| 502 if (func is UriTerm) { | |
| 503 importStr = func.text; | |
| 504 } | |
| 505 } else { | |
| 506 importStr = processQuotedString(false); | |
| 507 } | |
| 508 | |
| 509 // Any medias? | |
| 510 var medias = processMediaQueryList(); | |
| 511 | |
| 512 if (importStr == null) { | |
| 513 _error('missing import string', _peekToken.span); | |
| 514 } | |
| 515 | |
| 516 return new ImportDirective(importStr.trim(), medias, _makeSpan(start)); | |
| 517 | |
| 518 case TokenKind.DIRECTIVE_MEDIA: | |
| 519 _next(); | |
| 520 | |
| 521 // Any medias? | |
| 522 var media = processMediaQueryList(); | |
| 523 | |
| 524 List<TreeNode> rulesets = []; | |
| 525 if (_maybeEat(TokenKind.LBRACE)) { | |
| 526 while (!_maybeEat(TokenKind.END_OF_FILE)) { | |
| 527 RuleSet ruleset = processRuleSet(); | |
| 528 if (ruleset == null) break; | |
| 529 rulesets.add(ruleset); | |
| 530 } | |
| 531 | |
| 532 if (!_maybeEat(TokenKind.RBRACE)) { | |
| 533 _error('expected } after ruleset for @media', _peekToken.span); | |
| 534 } | |
| 535 } else { | |
| 536 _error('expected { after media before ruleset', _peekToken.span); | |
| 537 } | |
| 538 return new MediaDirective(media, rulesets, _makeSpan(start)); | |
| 539 | |
| 540 case TokenKind.DIRECTIVE_HOST: | |
| 541 _next(); | |
| 542 | |
| 543 List<TreeNode> rulesets = []; | |
| 544 if (_maybeEat(TokenKind.LBRACE)) { | |
| 545 while (!_maybeEat(TokenKind.END_OF_FILE)) { | |
| 546 RuleSet ruleset = processRuleSet(); | |
| 547 if (ruleset == null) break; | |
| 548 rulesets.add(ruleset); | |
| 549 } | |
| 550 | |
| 551 if (!_maybeEat(TokenKind.RBRACE)) { | |
| 552 _error('expected } after ruleset for @host', _peekToken.span); | |
| 553 } | |
| 554 } else { | |
| 555 _error('expected { after host before ruleset', _peekToken.span); | |
| 556 } | |
| 557 return new HostDirective(rulesets, _makeSpan(start)); | |
| 558 | |
| 559 case TokenKind.DIRECTIVE_PAGE: | |
| 560 /* | |
| 561 * @page S* IDENT? pseudo_page? | |
| 562 * S* '{' S* | |
| 563 * [ declaration | margin ]? | |
| 564 * [ ';' S* [ declaration | margin ]? ]* '}' S* | |
| 565 * | |
| 566 * pseudo_page : | |
| 567 * ':' [ "left" | "right" | "first" ] | |
| 568 * | |
| 569 * margin : | |
| 570 * margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* | |
| 571 * | |
| 572 * margin_sym : @top-left-corner, @top-left, @bottom-left, etc. | |
| 573 * | |
| 574 * See http://www.w3.org/TR/css3-page/#CSS21 | |
| 575 */ | |
| 576 _next(); | |
| 577 | |
| 578 // Page name | |
| 579 var name; | |
| 580 if (_peekIdentifier()) { | |
| 581 name = identifier(); | |
| 582 } | |
| 583 | |
| 584 // Any pseudo page? | |
| 585 var pseudoPage; | |
| 586 if (_maybeEat(TokenKind.COLON)) { | |
| 587 if (_peekIdentifier()) { | |
| 588 pseudoPage = identifier(); | |
| 589 // TODO(terry): Normalize pseudoPage to lowercase. | |
| 590 if (isChecked && | |
| 591 !(pseudoPage.name == 'left' || | |
| 592 pseudoPage.name == 'right' || | |
| 593 pseudoPage.name == 'first')) { | |
| 594 _warning("Pseudo page must be left, top or first", | |
| 595 pseudoPage.span); | |
| 596 return null; | |
| 597 } | |
| 598 } | |
| 599 } | |
| 600 | |
| 601 String pseudoName = pseudoPage is Identifier ? pseudoPage.name : ''; | |
| 602 String ident = name is Identifier ? name.name : ''; | |
| 603 return new PageDirective(ident, pseudoName, | |
| 604 processMarginsDeclarations(), _makeSpan(start)); | |
| 605 | |
| 606 case TokenKind.DIRECTIVE_CHARSET: | |
| 607 // @charset S* STRING S* ';' | |
| 608 _next(); | |
| 609 | |
| 610 var charEncoding = processQuotedString(false); | |
| 611 if (isChecked && charEncoding == null) { | |
| 612 // Missing character encoding. | |
| 613 _warning('missing character encoding string', _makeSpan(start)); | |
| 614 } | |
| 615 | |
| 616 return new CharsetDirective(charEncoding, _makeSpan(start)); | |
| 617 | |
| 618 // TODO(terry): Workaround Dart2js bug continue not implemented in switch | |
| 619 // see https://code.google.com/p/dart/issues/detail?id=8270 | |
| 620 /* | |
| 621 case TokenKind.DIRECTIVE_MS_KEYFRAMES: | |
| 622 // TODO(terry): For now only IE 10 (are base level) supports @keyframes, | |
| 623 // -moz- has only been optional since Oct 2012 release of Firefox, not | |
| 624 // all versions of webkit support @keyframes and opera doesn't yet | |
| 625 // support w/o -o- prefix. Add more warnings for other prefixes when | |
| 626 // they become optional. | |
| 627 if (isChecked) { | |
| 628 _warning('@-ms-keyframes should be @keyframes', _makeSpan(start)); | |
| 629 } | |
| 630 continue keyframeDirective; | |
| 631 | |
| 632 keyframeDirective: | |
| 633 */ | |
| 634 case TokenKind.DIRECTIVE_KEYFRAMES: | |
| 635 case TokenKind.DIRECTIVE_WEB_KIT_KEYFRAMES: | |
| 636 case TokenKind.DIRECTIVE_MOZ_KEYFRAMES: | |
| 637 case TokenKind.DIRECTIVE_O_KEYFRAMES: | |
| 638 // TODO(terry): Remove workaround when bug 8270 is fixed. | |
| 639 case TokenKind.DIRECTIVE_MS_KEYFRAMES: | |
| 640 if (tokId == TokenKind.DIRECTIVE_MS_KEYFRAMES && isChecked) { | |
| 641 _warning('@-ms-keyframes should be @keyframes', _makeSpan(start)); | |
| 642 } | |
| 643 // TODO(terry): End of workaround. | |
| 644 | |
| 645 /* Key frames grammar: | |
| 646 * | |
| 647 * @[browser]? keyframes [IDENT|STRING] '{' keyframes-blocks '}'; | |
| 648 * | |
| 649 * browser: [-webkit-, -moz-, -ms-, -o-] | |
| 650 * | |
| 651 * keyframes-blocks: | |
| 652 * [keyframe-selectors '{' declarations '}']* ; | |
| 653 * | |
| 654 * keyframe-selectors: | |
| 655 * ['from'|'to'|PERCENTAGE] [',' ['from'|'to'|PERCENTAGE] ]* ; | |
| 656 */ | |
| 657 _next(); | |
| 658 | |
| 659 var name; | |
| 660 if (_peekIdentifier()) { | |
| 661 name = identifier(); | |
| 662 } | |
| 663 | |
| 664 _eat(TokenKind.LBRACE); | |
| 665 | |
| 666 var keyframe = new KeyFrameDirective(tokId, name, _makeSpan(start)); | |
| 667 | |
| 668 do { | |
| 669 Expressions selectors = new Expressions(_makeSpan(start)); | |
| 670 | |
| 671 do { | |
| 672 var term = processTerm(); | |
| 673 | |
| 674 // TODO(terry): Only allow from, to and PERCENTAGE ... | |
| 675 | |
| 676 selectors.add(term); | |
| 677 } while (_maybeEat(TokenKind.COMMA)); | |
| 678 | |
| 679 keyframe.add(new KeyFrameBlock(selectors, processDeclarations(), | |
| 680 _makeSpan(start))); | |
| 681 | |
| 682 } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile()); | |
| 683 | |
| 684 return keyframe; | |
| 685 | |
| 686 case TokenKind.DIRECTIVE_FONTFACE: | |
| 687 _next(); | |
| 688 return new FontFaceDirective(processDeclarations(), _makeSpan(start)); | |
| 689 | |
| 690 case TokenKind.DIRECTIVE_STYLET: | |
| 691 /* Stylet grammar: | |
| 692 * | |
| 693 * @stylet IDENT '{' | |
| 694 * ruleset | |
| 695 * '}' | |
| 696 */ | |
| 697 _next(); | |
| 698 | |
| 699 var name; | |
| 700 if (_peekIdentifier()) { | |
| 701 name = identifier(); | |
| 702 } | |
| 703 | |
| 704 _eat(TokenKind.LBRACE); | |
| 705 | |
| 706 List<TreeNode> productions = []; | |
| 707 | |
| 708 start = _peekToken.span; | |
| 709 while (!_maybeEat(TokenKind.END_OF_FILE)) { | |
| 710 RuleSet ruleset = processRuleSet(); | |
| 711 if (ruleset == null) { | |
| 712 break; | |
| 713 } | |
| 714 productions.add(ruleset); | |
| 715 } | |
| 716 | |
| 717 _eat(TokenKind.RBRACE); | |
| 718 | |
| 719 return new StyletDirective(name, productions, _makeSpan(start)); | |
| 720 | |
| 721 case TokenKind.DIRECTIVE_NAMESPACE: | |
| 722 /* Namespace grammar: | |
| 723 * | |
| 724 * @namespace S* [namespace_prefix S*]? [STRING|URI] S* ';' S* | |
| 725 * namespace_prefix : IDENT | |
| 726 * | |
| 727 */ | |
| 728 _next(); | |
| 729 | |
| 730 var prefix; | |
| 731 if (_peekIdentifier()) { | |
| 732 prefix = identifier(); | |
| 733 } | |
| 734 | |
| 735 // The namespace URI can be either a quoted string url("uri_string") | |
| 736 // are identical. | |
| 737 String namespaceUri; | |
| 738 if (_peekIdentifier()) { | |
| 739 var func = processFunction(identifier()); | |
| 740 if (func is UriTerm) { | |
| 741 namespaceUri = func.text; | |
| 742 } | |
| 743 } else { | |
| 744 if (prefix != null && prefix.name == 'url') { | |
| 745 var func = processFunction(prefix); | |
| 746 if (func is UriTerm) { | |
| 747 // @namespace url(""); | |
| 748 namespaceUri = func.text; | |
| 749 prefix = null; | |
| 750 } | |
| 751 } else { | |
| 752 namespaceUri = processQuotedString(false); | |
| 753 } | |
| 754 } | |
| 755 | |
| 756 return new NamespaceDirective(prefix != null ? prefix.name : '', | |
| 757 namespaceUri, _makeSpan(start)); | |
| 758 | |
| 759 case TokenKind.DIRECTIVE_MIXIN: | |
| 760 return processMixin(); | |
| 761 | |
| 762 case TokenKind.DIRECTIVE_INCLUDE: | |
| 763 return processInclude( _makeSpan(start)); | |
| 764 | |
| 765 case TokenKind.DIRECTIVE_CONTENT: | |
| 766 // TODO(terry): TBD | |
| 767 _warning("@content not implemented.", _makeSpan(start)); | |
| 768 return null; | |
| 769 } | |
| 770 return null; | |
| 771 } | |
| 772 | |
| 773 /** | |
| 774 * Parse the mixin beginning token offset [start]. Returns a [MixinDefinition] | |
| 775 * node. | |
| 776 * | |
| 777 * Mixin grammar: | |
| 778 * | |
| 779 * @mixin IDENT [(args,...)] '{' | |
| 780 * [ruleset | property | directive]* | |
| 781 * '}' | |
| 782 */ | |
| 783 MixinDefinition processMixin() { | |
| 784 _next(); | |
| 785 | |
| 786 var name = identifier(); | |
| 787 | |
| 788 List<VarDefinitionDirective> params = []; | |
| 789 // Any parameters? | |
| 790 if (_maybeEat(TokenKind.LPAREN)) { | |
| 791 var mustHaveParam = false; | |
| 792 var keepGoing = true; | |
| 793 while (keepGoing) { | |
| 794 var varDef = processVariableOrDirective(mixinParameter: true); | |
| 795 if (varDef is VarDefinitionDirective || varDef is VarDefinition) { | |
| 796 params.add(varDef); | |
| 797 } else if (mustHaveParam) { | |
| 798 _warning("Expecting parameter", _makeSpan(_peekToken.span)); | |
| 799 keepGoing = false; | |
| 800 } | |
| 801 if (_maybeEat(TokenKind.COMMA)) { | |
| 802 mustHaveParam = true; | |
| 803 continue; | |
| 804 } | |
| 805 keepGoing = !_maybeEat(TokenKind.RPAREN); | |
| 806 } | |
| 807 } | |
| 808 | |
| 809 _eat(TokenKind.LBRACE); | |
| 810 | |
| 811 List<TreeNode> productions = []; | |
| 812 List<TreeNode> declarations = []; | |
| 813 var mixinDirective; | |
| 814 | |
| 815 var start = _peekToken.span; | |
| 816 while (!_maybeEat(TokenKind.END_OF_FILE)) { | |
| 817 var directive = processDirective(); | |
| 818 if (directive != null) { | |
| 819 productions.add(directive); | |
| 820 continue; | |
| 821 } | |
| 822 | |
| 823 var declGroup = processDeclarations(checkBrace: false); | |
| 824 var decls = []; | |
| 825 if (declGroup.declarations.any((decl) { | |
| 826 return decl is Declaration && | |
| 827 decl is! IncludeMixinAtDeclaration; | |
| 828 })) { | |
| 829 var newDecls = []; | |
| 830 productions.forEach((include) { | |
| 831 // If declGroup has items that are declarations then we assume | |
| 832 // this mixin is a declaration mixin not a top-level mixin. | |
| 833 if (include is IncludeDirective) { | |
| 834 newDecls.add(new IncludeMixinAtDeclaration(include, | |
| 835 include.span)); | |
| 836 } else { | |
| 837 _warning("Error mixing of top-level vs declarations mixins", | |
| 838 _makeSpan(include.span)); | |
| 839 } | |
| 840 }); | |
| 841 declGroup.declarations.insertAll(0, newDecls); | |
| 842 productions = []; | |
| 843 } else { | |
| 844 // Declarations are just @includes make it a list of productions | |
| 845 // not a declaration group (anything else is a ruleset). Make it a | |
| 846 // list of productions, not a declaration group. | |
| 847 for (var decl in declGroup.declarations) { | |
| 848 productions.add(decl is IncludeMixinAtDeclaration ? | |
| 849 decl.include : decl); | |
| 850 }; | |
| 851 declGroup.declarations.clear(); | |
| 852 } | |
| 853 | |
| 854 if (declGroup.declarations.isNotEmpty) { | |
| 855 if (productions.isEmpty) { | |
| 856 mixinDirective = new MixinDeclarationDirective(name.name, params, | |
| 857 false, declGroup, _makeSpan(start)); | |
| 858 break; | |
| 859 } else { | |
| 860 for (var decl in declGroup.declarations) { | |
| 861 productions.add(decl is IncludeMixinAtDeclaration ? | |
| 862 decl.include : decl); | |
| 863 } | |
| 864 } | |
| 865 } else { | |
| 866 mixinDirective = new MixinRulesetDirective(name.name, params, | |
| 867 false, productions, _makeSpan(start)); | |
| 868 break; | |
| 869 } | |
| 870 } | |
| 871 | |
| 872 if (productions.isNotEmpty) { | |
| 873 mixinDirective = new MixinRulesetDirective(name.name, params, | |
| 874 false, productions, _makeSpan(start)); | |
| 875 } | |
| 876 | |
| 877 _eat(TokenKind.RBRACE); | |
| 878 | |
| 879 return mixinDirective; | |
| 880 } | |
| 881 | |
| 882 /** | |
| 883 * Returns a VarDefinitionDirective or VarDefinition if a varaible otherwise | |
| 884 * return the token id of a directive or -1 if neither. | |
| 885 */ | |
| 886 processVariableOrDirective({bool mixinParameter: false}) { | |
| 887 var start = _peekToken.span; | |
| 888 | |
| 889 var tokId = _peek(); | |
| 890 // Handle case for @ directive (where there's a whitespace between the @ | |
| 891 // sign and the directive name. Technically, it's not valid grammar but | |
| 892 // a number of CSS tests test for whitespace between @ and name. | |
| 893 if (tokId == TokenKind.AT) { | |
| 894 Token tok = _next(); | |
| 895 tokId = _peek(); | |
| 896 if (_peekIdentifier()) { | |
| 897 // Is it a directive? | |
| 898 var directive = _peekToken.text; | |
| 899 var directiveLen = directive.length; | |
| 900 tokId = TokenKind.matchDirectives(directive, 0, directiveLen); | |
| 901 if (tokId == -1) { | |
| 902 tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen); | |
| 903 } | |
| 904 } | |
| 905 | |
| 906 if (tokId == -1) { | |
| 907 if (messages.options.lessSupport) { | |
| 908 // Less compatibility: | |
| 909 // @name: value; => var-name: value; (VarDefinition) | |
| 910 // property: @name; => property: var(name); (VarUsage) | |
| 911 var name; | |
| 912 if (_peekIdentifier()) { | |
| 913 name = identifier(); | |
| 914 } | |
| 915 | |
| 916 Expressions exprs; | |
| 917 if (mixinParameter && _maybeEat(TokenKind.COLON)) { | |
| 918 exprs = processExpr(); | |
| 919 } else if (!mixinParameter) { | |
| 920 _eat(TokenKind.COLON); | |
| 921 exprs = processExpr(); | |
| 922 } | |
| 923 | |
| 924 var span = _makeSpan(start); | |
| 925 return new VarDefinitionDirective( | |
| 926 new VarDefinition(name, exprs, span), span); | |
| 927 } else if (isChecked) { | |
| 928 _error('unexpected directive @$_peekToken', _peekToken.span); | |
| 929 } | |
| 930 } | |
| 931 } else if (mixinParameter && _peekToken.kind == TokenKind.VAR_DEFINITION) { | |
| 932 _next(); | |
| 933 var definedName; | |
| 934 if (_peekIdentifier()) definedName = identifier(); | |
| 935 | |
| 936 Expressions exprs; | |
| 937 if (_maybeEat(TokenKind.COLON)) { | |
| 938 exprs = processExpr(); | |
| 939 } | |
| 940 | |
| 941 return new VarDefinition(definedName, exprs, _makeSpan(start)); | |
| 942 } | |
| 943 | |
| 944 return tokId; | |
| 945 } | |
| 946 | |
| 947 IncludeDirective processInclude(SourceSpan span, {bool eatSemiColon: true}) { | |
| 948 /* Stylet grammar: | |
| 949 * | |
| 950 * @include IDENT [(args,...)]; | |
| 951 */ | |
| 952 _next(); | |
| 953 | |
| 954 var name; | |
| 955 if (_peekIdentifier()) { | |
| 956 name = identifier(); | |
| 957 } | |
| 958 | |
| 959 var params = []; | |
| 960 | |
| 961 // Any parameters? Parameters can be multiple terms per argument e.g., | |
| 962 // 3px solid yellow, green is two parameters: | |
| 963 // 1. 3px solid yellow | |
| 964 // 2. green | |
| 965 // the first has 3 terms and the second has 1 term. | |
| 966 if (_maybeEat(TokenKind.LPAREN)) { | |
| 967 var terms = []; | |
| 968 var expr; | |
| 969 var keepGoing = true; | |
| 970 while (keepGoing && (expr = processTerm()) != null) { | |
| 971 // VarUsage is returns as a list | |
| 972 terms.add(expr is List ? expr[0] : expr); | |
| 973 keepGoing = !_peekKind(TokenKind.RPAREN); | |
| 974 if (keepGoing) { | |
| 975 if (_maybeEat(TokenKind.COMMA)) { | |
| 976 params.add(terms); | |
| 977 terms = []; | |
| 978 } | |
| 979 } | |
| 980 } | |
| 981 params.add(terms); | |
| 982 _maybeEat(TokenKind.RPAREN); | |
| 983 } | |
| 984 | |
| 985 if (eatSemiColon) { | |
| 986 _eat(TokenKind.SEMICOLON); | |
| 987 } | |
| 988 | |
| 989 return new IncludeDirective(name.name, params, span); | |
| 990 } | |
| 991 | |
| 992 RuleSet processRuleSet([SelectorGroup selectorGroup]) { | |
| 993 if (selectorGroup == null) { | |
| 994 selectorGroup = processSelectorGroup(); | |
| 995 } | |
| 996 if (selectorGroup != null) { | |
| 997 return new RuleSet(selectorGroup, processDeclarations(), | |
| 998 selectorGroup.span); | |
| 999 } | |
| 1000 } | |
| 1001 | |
| 1002 /** | |
| 1003 * Look ahead to see if what should be a declaration is really a selector. | |
| 1004 * If it's a selector than it's a nested selector. This support's Less' | |
| 1005 * nested selector syntax (requires a look ahead). E.g., | |
| 1006 * | |
| 1007 * div { | |
| 1008 * width : 20px; | |
| 1009 * span { | |
| 1010 * color: red; | |
| 1011 * } | |
| 1012 * } | |
| 1013 * | |
| 1014 * Two tag name selectors div and span equivalent to: | |
| 1015 * | |
| 1016 * div { | |
| 1017 * width: 20px; | |
| 1018 * } | |
| 1019 * div span { | |
| 1020 * color: red; | |
| 1021 * } | |
| 1022 * | |
| 1023 * Return [:null:] if no selector or [SelectorGroup] if a selector was parsed. | |
| 1024 */ | |
| 1025 SelectorGroup _nestedSelector() { | |
| 1026 Messages oldMessages = messages; | |
| 1027 _createMessages(); | |
| 1028 | |
| 1029 var markedData = _mark; | |
| 1030 | |
| 1031 // Look a head do we have a nested selector instead of a declaration? | |
| 1032 SelectorGroup selGroup = processSelectorGroup(); | |
| 1033 | |
| 1034 var nestedSelector = selGroup != null && _peekKind(TokenKind.LBRACE) && | |
| 1035 messages.messages.isEmpty; | |
| 1036 | |
| 1037 if (!nestedSelector) { | |
| 1038 // Not a selector so restore the world. | |
| 1039 _restore(markedData); | |
| 1040 messages = oldMessages; | |
| 1041 return null; | |
| 1042 } else { | |
| 1043 // Remember any messages from look ahead. | |
| 1044 oldMessages.mergeMessages(messages); | |
| 1045 messages = oldMessages; | |
| 1046 return selGroup; | |
| 1047 } | |
| 1048 } | |
| 1049 | |
| 1050 DeclarationGroup processDeclarations({bool checkBrace: true}) { | |
| 1051 var start = _peekToken.span; | |
| 1052 | |
| 1053 if (checkBrace) _eat(TokenKind.LBRACE); | |
| 1054 | |
| 1055 List decls = []; | |
| 1056 List dartStyles = []; // List of latest styles exposed to Dart. | |
| 1057 | |
| 1058 do { | |
| 1059 var selectorGroup = _nestedSelector(); | |
| 1060 while (selectorGroup != null) { | |
| 1061 // Nested selector so process as a ruleset. | |
| 1062 var ruleset = processRuleSet(selectorGroup); | |
| 1063 decls.add(ruleset); | |
| 1064 selectorGroup = _nestedSelector(); | |
| 1065 } | |
| 1066 | |
| 1067 Declaration decl = processDeclaration(dartStyles); | |
| 1068 if (decl != null) { | |
| 1069 if (decl.hasDartStyle) { | |
| 1070 var newDartStyle = decl.dartStyle; | |
| 1071 | |
| 1072 // Replace or add latest Dart style. | |
| 1073 bool replaced = false; | |
| 1074 for (var i = 0; i < dartStyles.length; i++) { | |
| 1075 var dartStyle = dartStyles[i]; | |
| 1076 if (dartStyle.isSame(newDartStyle)) { | |
| 1077 dartStyles[i] = newDartStyle; | |
| 1078 replaced = true; | |
| 1079 break; | |
| 1080 } | |
| 1081 } | |
| 1082 if (!replaced) { | |
| 1083 dartStyles.add(newDartStyle); | |
| 1084 } | |
| 1085 } | |
| 1086 decls.add(decl); | |
| 1087 } | |
| 1088 } while (_maybeEat(TokenKind.SEMICOLON)); | |
| 1089 | |
| 1090 if (checkBrace) _eat(TokenKind.RBRACE); | |
| 1091 | |
| 1092 // Fixup declaration to only have dartStyle that are live for this set of | |
| 1093 // declarations. | |
| 1094 for (var decl in decls) { | |
| 1095 if (decl is Declaration) { | |
| 1096 if (decl.hasDartStyle && dartStyles.indexOf(decl.dartStyle) < 0) { | |
| 1097 // Dart style not live, ignore these styles in this Declarations. | |
| 1098 decl.dartStyle = null; | |
| 1099 } | |
| 1100 } | |
| 1101 } | |
| 1102 | |
| 1103 return new DeclarationGroup(decls, _makeSpan(start)); | |
| 1104 } | |
| 1105 | |
| 1106 List<DeclarationGroup> processMarginsDeclarations() { | |
| 1107 List groups = []; | |
| 1108 | |
| 1109 var start = _peekToken.span; | |
| 1110 | |
| 1111 _eat(TokenKind.LBRACE); | |
| 1112 | |
| 1113 List<Declaration> decls = []; | |
| 1114 List dartStyles = []; // List of latest styles exposed to Dart. | |
| 1115 | |
| 1116 do { | |
| 1117 switch (_peek()) { | |
| 1118 case TokenKind.MARGIN_DIRECTIVE_TOPLEFTCORNER: | |
| 1119 case TokenKind.MARGIN_DIRECTIVE_TOPLEFT: | |
| 1120 case TokenKind.MARGIN_DIRECTIVE_TOPCENTER: | |
| 1121 case TokenKind.MARGIN_DIRECTIVE_TOPRIGHT: | |
| 1122 case TokenKind.MARGIN_DIRECTIVE_TOPRIGHTCORNER: | |
| 1123 case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFTCORNER: | |
| 1124 case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFT: | |
| 1125 case TokenKind.MARGIN_DIRECTIVE_BOTTOMCENTER: | |
| 1126 case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHT: | |
| 1127 case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHTCORNER: | |
| 1128 case TokenKind.MARGIN_DIRECTIVE_LEFTTOP: | |
| 1129 case TokenKind.MARGIN_DIRECTIVE_LEFTMIDDLE: | |
| 1130 case TokenKind.MARGIN_DIRECTIVE_LEFTBOTTOM: | |
| 1131 case TokenKind.MARGIN_DIRECTIVE_RIGHTTOP: | |
| 1132 case TokenKind.MARGIN_DIRECTIVE_RIGHTMIDDLE: | |
| 1133 case TokenKind.MARGIN_DIRECTIVE_RIGHTBOTTOM: | |
| 1134 // Margin syms processed. | |
| 1135 // margin : | |
| 1136 // margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* | |
| 1137 // | |
| 1138 // margin_sym : @top-left-corner, @top-left, @bottom-left, etc. | |
| 1139 var marginSym = _peek(); | |
| 1140 | |
| 1141 _next(); | |
| 1142 | |
| 1143 var declGroup = processDeclarations(); | |
| 1144 if (declGroup != null) { | |
| 1145 groups.add(new MarginGroup(marginSym, declGroup.declarations, | |
| 1146 _makeSpan(start))); | |
| 1147 } | |
| 1148 break; | |
| 1149 default: | |
| 1150 Declaration decl = processDeclaration(dartStyles); | |
| 1151 if (decl != null) { | |
| 1152 if (decl.hasDartStyle) { | |
| 1153 var newDartStyle = decl.dartStyle; | |
| 1154 | |
| 1155 // Replace or add latest Dart style. | |
| 1156 bool replaced = false; | |
| 1157 for (var i = 0; i < dartStyles.length; i++) { | |
| 1158 var dartStyle = dartStyles[i]; | |
| 1159 if (dartStyle.isSame(newDartStyle)) { | |
| 1160 dartStyles[i] = newDartStyle; | |
| 1161 replaced = true; | |
| 1162 break; | |
| 1163 } | |
| 1164 } | |
| 1165 if (!replaced) { | |
| 1166 dartStyles.add(newDartStyle); | |
| 1167 } | |
| 1168 } | |
| 1169 decls.add(decl); | |
| 1170 } | |
| 1171 _maybeEat(TokenKind.SEMICOLON); | |
| 1172 break; | |
| 1173 } | |
| 1174 } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile()); | |
| 1175 | |
| 1176 // Fixup declaration to only have dartStyle that are live for this set of | |
| 1177 // declarations. | |
| 1178 for (var decl in decls) { | |
| 1179 if (decl.hasDartStyle && dartStyles.indexOf(decl.dartStyle) < 0) { | |
| 1180 // Dart style not live, ignore these styles in this Declarations. | |
| 1181 decl.dartStyle = null; | |
| 1182 } | |
| 1183 } | |
| 1184 | |
| 1185 if (decls.length > 0) { | |
| 1186 groups.add(new DeclarationGroup(decls, _makeSpan(start))); | |
| 1187 } | |
| 1188 | |
| 1189 return groups; | |
| 1190 } | |
| 1191 | |
| 1192 SelectorGroup processSelectorGroup() { | |
| 1193 List<Selector> selectors = []; | |
| 1194 var start = _peekToken.span; | |
| 1195 | |
| 1196 do { | |
| 1197 Selector selector = processSelector(); | |
| 1198 if (selector != null) { | |
| 1199 selectors.add(selector); | |
| 1200 } | |
| 1201 } while (_maybeEat(TokenKind.COMMA)); | |
| 1202 | |
| 1203 if (selectors.length > 0) { | |
| 1204 return new SelectorGroup(selectors, _makeSpan(start)); | |
| 1205 } | |
| 1206 } | |
| 1207 | |
| 1208 /** | |
| 1209 * Return list of selectors | |
| 1210 */ | |
| 1211 Selector processSelector() { | |
| 1212 var simpleSequences = <SimpleSelectorSequence>[]; | |
| 1213 var start = _peekToken.span; | |
| 1214 while (true) { | |
| 1215 // First item is never descendant make sure it's COMBINATOR_NONE. | |
| 1216 var selectorItem = simpleSelectorSequence(simpleSequences.length == 0); | |
| 1217 if (selectorItem != null) { | |
| 1218 simpleSequences.add(selectorItem); | |
| 1219 } else { | |
| 1220 break; | |
| 1221 } | |
| 1222 } | |
| 1223 | |
| 1224 if (simpleSequences.length > 0) { | |
| 1225 return new Selector(simpleSequences, _makeSpan(start)); | |
| 1226 } | |
| 1227 } | |
| 1228 | |
| 1229 simpleSelectorSequence(bool forceCombinatorNone) { | |
| 1230 var start = _peekToken.span; | |
| 1231 var combinatorType = TokenKind.COMBINATOR_NONE; | |
| 1232 var thisOperator = false; | |
| 1233 | |
| 1234 switch (_peek()) { | |
| 1235 case TokenKind.PLUS: | |
| 1236 _eat(TokenKind.PLUS); | |
| 1237 combinatorType = TokenKind.COMBINATOR_PLUS; | |
| 1238 break; | |
| 1239 case TokenKind.GREATER: | |
| 1240 _eat(TokenKind.GREATER); | |
| 1241 combinatorType = TokenKind.COMBINATOR_GREATER; | |
| 1242 break; | |
| 1243 case TokenKind.TILDE: | |
| 1244 _eat(TokenKind.TILDE); | |
| 1245 combinatorType = TokenKind.COMBINATOR_TILDE; | |
| 1246 break; | |
| 1247 case TokenKind.AMPERSAND: | |
| 1248 _eat(TokenKind.AMPERSAND); | |
| 1249 thisOperator = true; | |
| 1250 break; | |
| 1251 } | |
| 1252 | |
| 1253 // Check if WHITESPACE existed between tokens if so we're descendent. | |
| 1254 if (combinatorType == TokenKind.COMBINATOR_NONE && !forceCombinatorNone) { | |
| 1255 if (this._previousToken != null && | |
| 1256 this._previousToken.end != this._peekToken.start) { | |
| 1257 combinatorType = TokenKind.COMBINATOR_DESCENDANT; | |
| 1258 } | |
| 1259 } | |
| 1260 | |
| 1261 var span = _makeSpan(start); | |
| 1262 var simpleSel = thisOperator ? | |
| 1263 new ElementSelector(new ThisOperator(span), span) : simpleSelector(); | |
| 1264 if (simpleSel == null && | |
| 1265 (combinatorType == TokenKind.COMBINATOR_PLUS || | |
| 1266 combinatorType == TokenKind.COMBINATOR_GREATER || | |
| 1267 combinatorType == TokenKind.COMBINATOR_TILDE)) { | |
| 1268 // For "+ &", "~ &" or "> &" a selector sequence with no name is needed | |
| 1269 // so that the & will have a combinator too. This is needed to | |
| 1270 // disambiguate selector expressions: | |
| 1271 // .foo&:hover combinator before & is NONE | |
| 1272 // .foo & combinator before & is DESCDENDANT | |
| 1273 // .foo > & combinator before & is GREATER | |
| 1274 simpleSel = new ElementSelector(new Identifier("", span), span); | |
| 1275 } | |
| 1276 if (simpleSel != null) { | |
| 1277 return new SimpleSelectorSequence(simpleSel, span, combinatorType); | |
| 1278 } | |
| 1279 } | |
| 1280 | |
| 1281 /** | |
| 1282 * Simple selector grammar: | |
| 1283 * | |
| 1284 * simple_selector_sequence | |
| 1285 * : [ type_selector | universal ] | |
| 1286 * [ HASH | class | attrib | pseudo | negation ]* | |
| 1287 * | [ HASH | class | attrib | pseudo | negation ]+ | |
| 1288 * type_selector | |
| 1289 * : [ namespace_prefix ]? element_name | |
| 1290 * namespace_prefix | |
| 1291 * : [ IDENT | '*' ]? '|' | |
| 1292 * element_name | |
| 1293 * : IDENT | |
| 1294 * universal | |
| 1295 * : [ namespace_prefix ]? '*' | |
| 1296 * class | |
| 1297 * : '.' IDENT | |
| 1298 */ | |
| 1299 simpleSelector() { | |
| 1300 // TODO(terry): Natalie makes a good point parsing of namespace and element | |
| 1301 // are essentially the same (asterisk or identifier) other | |
| 1302 // than the error message for element. Should consolidate the | |
| 1303 // code. | |
| 1304 // TODO(terry): Need to handle attribute namespace too. | |
| 1305 var first; | |
| 1306 var start = _peekToken.span; | |
| 1307 switch (_peek()) { | |
| 1308 case TokenKind.ASTERISK: | |
| 1309 // Mark as universal namespace. | |
| 1310 var tok = _next(); | |
| 1311 first = new Wildcard(_makeSpan(tok.span)); | |
| 1312 break; | |
| 1313 case TokenKind.IDENTIFIER: | |
| 1314 first = identifier(); | |
| 1315 break; | |
| 1316 default: | |
| 1317 // Expecting simple selector. | |
| 1318 // TODO(terry): Could be a synthesized token like value, etc. | |
| 1319 if (TokenKind.isKindIdentifier(_peek())) { | |
| 1320 first = identifier(); | |
| 1321 } else if (_peekKind(TokenKind.SEMICOLON)) { | |
| 1322 // Can't be a selector if we found a semi-colon. | |
| 1323 return null; | |
| 1324 } | |
| 1325 break; | |
| 1326 } | |
| 1327 | |
| 1328 if (_maybeEat(TokenKind.NAMESPACE)) { | |
| 1329 var element; | |
| 1330 switch (_peek()) { | |
| 1331 case TokenKind.ASTERISK: | |
| 1332 // Mark as universal element | |
| 1333 var tok = _next(); | |
| 1334 element = new Wildcard(_makeSpan(tok.span)); | |
| 1335 break; | |
| 1336 case TokenKind.IDENTIFIER: | |
| 1337 element = identifier(); | |
| 1338 break; | |
| 1339 default: | |
| 1340 _error('expected element name or universal(*), but found $_peekToken', | |
| 1341 _peekToken.span); | |
| 1342 break; | |
| 1343 } | |
| 1344 | |
| 1345 return new NamespaceSelector(first, | |
| 1346 new ElementSelector(element, element.span), _makeSpan(start)); | |
| 1347 } else if (first != null) { | |
| 1348 return new ElementSelector(first, _makeSpan(start)); | |
| 1349 } else { | |
| 1350 // Check for HASH | class | attrib | pseudo | negation | |
| 1351 return simpleSelectorTail(); | |
| 1352 } | |
| 1353 } | |
| 1354 | |
| 1355 bool _anyWhiteSpaceBeforePeekToken(int kind) { | |
| 1356 if (_previousToken != null && _peekToken != null && | |
| 1357 _previousToken.kind == kind) { | |
| 1358 // If end of previous token isn't same as the start of peek token then | |
| 1359 // there's something between these tokens probably whitespace. | |
| 1360 return _previousToken.end != _peekToken.start; | |
| 1361 } | |
| 1362 | |
| 1363 return false; | |
| 1364 } | |
| 1365 | |
| 1366 /** | |
| 1367 * type_selector | universal | HASH | class | attrib | pseudo | |
| 1368 */ | |
| 1369 simpleSelectorTail() { | |
| 1370 // Check for HASH | class | attrib | pseudo | negation | |
| 1371 var start = _peekToken.span; | |
| 1372 switch (_peek()) { | |
| 1373 case TokenKind.HASH: | |
| 1374 _eat(TokenKind.HASH); | |
| 1375 | |
| 1376 var hasWhiteSpace = false; | |
| 1377 if (_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) { | |
| 1378 _warning("Not a valid ID selector expected #id", _makeSpan(start)); | |
| 1379 hasWhiteSpace = true; | |
| 1380 } | |
| 1381 if (_peekIdentifier()) { | |
| 1382 var id = identifier(); | |
| 1383 if (hasWhiteSpace) { | |
| 1384 // Generate bad selector id (normalized). | |
| 1385 id.name = " ${id.name}"; | |
| 1386 } | |
| 1387 return new IdSelector(id, _makeSpan(start)); | |
| 1388 } | |
| 1389 return null; | |
| 1390 case TokenKind.DOT: | |
| 1391 _eat(TokenKind.DOT); | |
| 1392 | |
| 1393 bool hasWhiteSpace = false; | |
| 1394 if (_anyWhiteSpaceBeforePeekToken(TokenKind.DOT)) { | |
| 1395 _warning("Not a valid class selector expected .className", | |
| 1396 _makeSpan(start)); | |
| 1397 hasWhiteSpace = true; | |
| 1398 } | |
| 1399 var id = identifier(); | |
| 1400 if (hasWhiteSpace) { | |
| 1401 // Generate bad selector class (normalized). | |
| 1402 id.name = " ${id.name}"; | |
| 1403 } | |
| 1404 return new ClassSelector(id, _makeSpan(start)); | |
| 1405 case TokenKind.COLON: | |
| 1406 // :pseudo-class ::pseudo-element | |
| 1407 return processPseudoSelector(start); | |
| 1408 case TokenKind.LBRACK: | |
| 1409 return processAttribute(); | |
| 1410 case TokenKind.DOUBLE: | |
| 1411 _error('name must start with a alpha character, but found a number', | |
| 1412 _peekToken.span); | |
| 1413 _next(); | |
| 1414 break; | |
| 1415 } | |
| 1416 } | |
| 1417 | |
| 1418 processPseudoSelector(FileSpan start) { | |
| 1419 // :pseudo-class ::pseudo-element | |
| 1420 // TODO(terry): '::' should be token. | |
| 1421 _eat(TokenKind.COLON); | |
| 1422 var pseudoElement = _maybeEat(TokenKind.COLON); | |
| 1423 | |
| 1424 // TODO(terry): If no identifier specified consider optimizing out the | |
| 1425 // : or :: and making this a normal selector. For now, | |
| 1426 // create an empty pseudoName. | |
| 1427 var pseudoName; | |
| 1428 if (_peekIdentifier()) { | |
| 1429 pseudoName = identifier(); | |
| 1430 } else { | |
| 1431 return null; | |
| 1432 } | |
| 1433 | |
| 1434 // Functional pseudo? | |
| 1435 | |
| 1436 if (_peekToken.kind == TokenKind.LPAREN) { | |
| 1437 | |
| 1438 if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') { | |
| 1439 _eat(TokenKind.LPAREN); | |
| 1440 | |
| 1441 // Negation : ':NOT(' S* negation_arg S* ')' | |
| 1442 var negArg = simpleSelector(); | |
| 1443 | |
| 1444 _eat(TokenKind.RPAREN); | |
| 1445 return new NegationSelector(negArg, _makeSpan(start)); | |
| 1446 } else { | |
| 1447 // Special parsing for expressions in pseudo functions. Minus is used | |
| 1448 // as operator not identifier. | |
| 1449 // TODO(jmesserly): we need to flip this before we eat the "(" as the | |
| 1450 // next token will be fetched when we do that. I think we should try to | |
| 1451 // refactor so we don't need this boolean; it seems fragile. | |
| 1452 tokenizer.inSelectorExpression = true; | |
| 1453 _eat(TokenKind.LPAREN); | |
| 1454 | |
| 1455 // Handle function expression. | |
| 1456 var span = _makeSpan(start); | |
| 1457 var expr = processSelectorExpression(); | |
| 1458 | |
| 1459 tokenizer.inSelectorExpression = false; | |
| 1460 | |
| 1461 // Used during selector look-a-head if not a SelectorExpression is | |
| 1462 // bad. | |
| 1463 if (expr is! SelectorExpression) { | |
| 1464 _errorExpected("CSS expression"); | |
| 1465 return null; | |
| 1466 } | |
| 1467 | |
| 1468 _eat(TokenKind.RPAREN); | |
| 1469 return (pseudoElement) ? | |
| 1470 new PseudoElementFunctionSelector(pseudoName, expr, span) : | |
| 1471 new PseudoClassFunctionSelector(pseudoName, expr, span); | |
| 1472 } | |
| 1473 } | |
| 1474 | |
| 1475 // TODO(terry): Need to handle specific pseudo class/element name and | |
| 1476 // backward compatible names that are : as well as :: as well as | |
| 1477 // parameters. Current, spec uses :: for pseudo-element and : for | |
| 1478 // pseudo-class. However, CSS2.1 allows for : to specify old | |
| 1479 // pseudo-elements (:first-line, :first-letter, :before and :after) any | |
| 1480 // new pseudo-elements defined would require a ::. | |
| 1481 return pseudoElement ? | |
| 1482 new PseudoElementSelector(pseudoName, _makeSpan(start)) : | |
| 1483 new PseudoClassSelector(pseudoName, _makeSpan(start)); | |
| 1484 } | |
| 1485 | |
| 1486 /** | |
| 1487 * In CSS3, the expressions are identifiers, strings, or of the form "an+b". | |
| 1488 * | |
| 1489 * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ | |
| 1490 * | |
| 1491 * num [0-9]+|[0-9]*\.[0-9]+ | |
| 1492 * PLUS '+' | |
| 1493 * DIMENSION {num}{ident} | |
| 1494 * NUMBER {num} | |
| 1495 */ | |
| 1496 processSelectorExpression() { | |
| 1497 var start = _peekToken.span; | |
| 1498 | |
| 1499 var expressions = []; | |
| 1500 | |
| 1501 Token termToken; | |
| 1502 var value; | |
| 1503 | |
| 1504 var keepParsing = true; | |
| 1505 while (keepParsing) { | |
| 1506 switch (_peek()) { | |
| 1507 case TokenKind.PLUS: | |
| 1508 start = _peekToken.span; | |
| 1509 termToken = _next(); | |
| 1510 expressions.add(new OperatorPlus(_makeSpan(start))); | |
| 1511 break; | |
| 1512 case TokenKind.MINUS: | |
| 1513 start = _peekToken.span; | |
| 1514 termToken = _next(); | |
| 1515 expressions.add(new OperatorMinus(_makeSpan(start))); | |
| 1516 break; | |
| 1517 case TokenKind.INTEGER: | |
| 1518 termToken = _next(); | |
| 1519 value = int.parse(termToken.text); | |
| 1520 break; | |
| 1521 case TokenKind.DOUBLE: | |
| 1522 termToken = _next(); | |
| 1523 value = double.parse(termToken.text); | |
| 1524 break; | |
| 1525 case TokenKind.SINGLE_QUOTE: | |
| 1526 value = processQuotedString(false); | |
| 1527 value = "'${_escapeString(value, single: true)}'"; | |
| 1528 return new LiteralTerm(value, value, _makeSpan(start)); | |
| 1529 case TokenKind.DOUBLE_QUOTE: | |
| 1530 value = processQuotedString(false); | |
| 1531 value = '"${_escapeString(value)}"'; | |
| 1532 return new LiteralTerm(value, value, _makeSpan(start)); | |
| 1533 case TokenKind.IDENTIFIER: | |
| 1534 value = identifier(); // Snarf up the ident we'll remap, maybe. | |
| 1535 break; | |
| 1536 default: | |
| 1537 keepParsing = false; | |
| 1538 } | |
| 1539 | |
| 1540 if (keepParsing && value != null) { | |
| 1541 var unitTerm; | |
| 1542 // Don't process the dimension if MINUS or PLUS is next. | |
| 1543 if (_peek() != TokenKind.MINUS && _peek() != TokenKind.PLUS) { | |
| 1544 unitTerm = processDimension(termToken, value, _makeSpan(start)); | |
| 1545 } | |
| 1546 if (unitTerm == null) { | |
| 1547 unitTerm = new LiteralTerm(value, value.name, _makeSpan(start)); | |
| 1548 } | |
| 1549 expressions.add(unitTerm); | |
| 1550 | |
| 1551 value = null; | |
| 1552 } | |
| 1553 } | |
| 1554 | |
| 1555 return new SelectorExpression(expressions, _makeSpan(start)); | |
| 1556 } | |
| 1557 | |
| 1558 // Attribute grammar: | |
| 1559 // | |
| 1560 // attributes : | |
| 1561 // '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']' | |
| 1562 // | |
| 1563 // ATTRIB_MATCHES : | |
| 1564 // [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ] | |
| 1565 // | |
| 1566 // INCLUDES: '~=' | |
| 1567 // | |
| 1568 // DASHMATCH: '|=' | |
| 1569 // | |
| 1570 // PREFIXMATCH: '^=' | |
| 1571 // | |
| 1572 // SUFFIXMATCH: '$=' | |
| 1573 // | |
| 1574 // SUBSTRMATCH: '*=' | |
| 1575 // | |
| 1576 // | |
| 1577 AttributeSelector processAttribute() { | |
| 1578 var start = _peekToken.span; | |
| 1579 | |
| 1580 if (_maybeEat(TokenKind.LBRACK)) { | |
| 1581 var attrName = identifier(); | |
| 1582 | |
| 1583 int op; | |
| 1584 switch (_peek()) { | |
| 1585 case TokenKind.EQUALS: | |
| 1586 case TokenKind.INCLUDES: // ~= | |
| 1587 case TokenKind.DASH_MATCH: // |= | |
| 1588 case TokenKind.PREFIX_MATCH: // ^= | |
| 1589 case TokenKind.SUFFIX_MATCH: // $= | |
| 1590 case TokenKind.SUBSTRING_MATCH: // *= | |
| 1591 op = _peek(); | |
| 1592 _next(); | |
| 1593 break; | |
| 1594 default: | |
| 1595 op = TokenKind.NO_MATCH; | |
| 1596 } | |
| 1597 | |
| 1598 var value; | |
| 1599 if (op != TokenKind.NO_MATCH) { | |
| 1600 // Operator hit so we require a value too. | |
| 1601 if (_peekIdentifier()) { | |
| 1602 value = identifier(); | |
| 1603 } else { | |
| 1604 value = processQuotedString(false); | |
| 1605 } | |
| 1606 | |
| 1607 if (value == null) { | |
| 1608 _error('expected attribute value string or ident', _peekToken.span); | |
| 1609 } | |
| 1610 } | |
| 1611 | |
| 1612 _eat(TokenKind.RBRACK); | |
| 1613 | |
| 1614 return new AttributeSelector(attrName, op, value, _makeSpan(start)); | |
| 1615 } | |
| 1616 } | |
| 1617 | |
| 1618 // Declaration grammar: | |
| 1619 // | |
| 1620 // declaration: property ':' expr prio? | |
| 1621 // | |
| 1622 // property: IDENT [or IE hacks] | |
| 1623 // prio: !important | |
| 1624 // expr: (see processExpr) | |
| 1625 // | |
| 1626 // Here are the ugly IE hacks we need to support: | |
| 1627 // property: expr prio? \9; - IE8 and below property, /9 before semi-colon | |
| 1628 // *IDENT - IE7 or below | |
| 1629 // _IDENT - IE6 property (automatically a valid ident) | |
| 1630 // | |
| 1631 Declaration processDeclaration(List dartStyles) { | |
| 1632 Declaration decl; | |
| 1633 | |
| 1634 var start = _peekToken.span; | |
| 1635 | |
| 1636 // IE7 hack of * before property name if so the property is IE7 or below. | |
| 1637 var ie7 = _peekKind(TokenKind.ASTERISK); | |
| 1638 if (ie7) { | |
| 1639 _next(); | |
| 1640 } | |
| 1641 | |
| 1642 // IDENT ':' expr '!important'? | |
| 1643 if (TokenKind.isIdentifier(_peekToken.kind)) { | |
| 1644 var propertyIdent = identifier(); | |
| 1645 | |
| 1646 var ieFilterProperty = propertyIdent.name.toLowerCase() == 'filter'; | |
| 1647 | |
| 1648 _eat(TokenKind.COLON); | |
| 1649 | |
| 1650 Expressions exprs = processExpr(ieFilterProperty); | |
| 1651 | |
| 1652 var dartComposite = _styleForDart(propertyIdent, exprs, dartStyles); | |
| 1653 | |
| 1654 // Handle !important (prio) | |
| 1655 var importantPriority = _maybeEat(TokenKind.IMPORTANT); | |
| 1656 | |
| 1657 decl = new Declaration(propertyIdent, exprs, dartComposite, | |
| 1658 _makeSpan(start), important: importantPriority, ie7: ie7); | |
| 1659 } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) { | |
| 1660 _next(); | |
| 1661 var definedName; | |
| 1662 if (_peekIdentifier()) definedName = identifier(); | |
| 1663 | |
| 1664 _eat(TokenKind.COLON); | |
| 1665 | |
| 1666 Expressions exprs = processExpr(); | |
| 1667 | |
| 1668 decl = new VarDefinition(definedName, exprs, _makeSpan(start)); | |
| 1669 } else if (_peekToken.kind == TokenKind.DIRECTIVE_INCLUDE) { | |
| 1670 // @include mixinName in the declaration area. | |
| 1671 var span = _makeSpan(start); | |
| 1672 var include = processInclude(span, eatSemiColon: false); | |
| 1673 decl = new IncludeMixinAtDeclaration(include, span); | |
| 1674 } else if (_peekToken.kind == TokenKind.DIRECTIVE_EXTEND) { | |
| 1675 var simpleSequences = <TreeNode>[]; | |
| 1676 | |
| 1677 _next(); | |
| 1678 var span = _makeSpan(start); | |
| 1679 var selector = simpleSelector(); | |
| 1680 if (selector == null) { | |
| 1681 _warning("@extends expecting simple selector name", span); | |
| 1682 } else { | |
| 1683 simpleSequences.add(selector); | |
| 1684 } | |
| 1685 if (_peekKind(TokenKind.COLON)) { | |
| 1686 var pseudoSelector = processPseudoSelector(_peekToken.span); | |
| 1687 if (pseudoSelector is PseudoElementSelector || | |
| 1688 pseudoSelector is PseudoClassSelector) { | |
| 1689 simpleSequences.add(pseudoSelector); | |
| 1690 } else { | |
| 1691 _warning("not a valid selector", span); | |
| 1692 } | |
| 1693 } | |
| 1694 decl = new ExtendDeclaration(simpleSequences, span); | |
| 1695 } | |
| 1696 | |
| 1697 return decl; | |
| 1698 } | |
| 1699 | |
| 1700 /** List of styles exposed to the Dart UI framework. */ | |
| 1701 static const int _fontPartFont= 0; | |
| 1702 static const int _fontPartVariant = 1; | |
| 1703 static const int _fontPartWeight = 2; | |
| 1704 static const int _fontPartSize = 3; | |
| 1705 static const int _fontPartFamily = 4; | |
| 1706 static const int _fontPartStyle = 5; | |
| 1707 static const int _marginPartMargin = 6; | |
| 1708 static const int _marginPartLeft = 7; | |
| 1709 static const int _marginPartTop = 8; | |
| 1710 static const int _marginPartRight = 9; | |
| 1711 static const int _marginPartBottom = 10; | |
| 1712 static const int _lineHeightPart = 11; | |
| 1713 static const int _borderPartBorder = 12; | |
| 1714 static const int _borderPartLeft = 13; | |
| 1715 static const int _borderPartTop = 14; | |
| 1716 static const int _borderPartRight = 15; | |
| 1717 static const int _borderPartBottom = 16; | |
| 1718 static const int _borderPartWidth = 17; | |
| 1719 static const int _borderPartLeftWidth = 18; | |
| 1720 static const int _borderPartTopWidth = 19; | |
| 1721 static const int _borderPartRightWidth = 20; | |
| 1722 static const int _borderPartBottomWidth = 21; | |
| 1723 static const int _heightPart = 22; | |
| 1724 static const int _widthPart = 23; | |
| 1725 static const int _paddingPartPadding = 24; | |
| 1726 static const int _paddingPartLeft = 25; | |
| 1727 static const int _paddingPartTop = 26; | |
| 1728 static const int _paddingPartRight = 27; | |
| 1729 static const int _paddingPartBottom = 28; | |
| 1730 | |
| 1731 static const Map<String, int> _stylesToDart = const { | |
| 1732 'font': _fontPartFont, | |
| 1733 'font-family': _fontPartFamily, | |
| 1734 'font-size': _fontPartSize, | |
| 1735 'font-style': _fontPartStyle, | |
| 1736 'font-variant': _fontPartVariant, | |
| 1737 'font-weight': _fontPartWeight, | |
| 1738 'line-height': _lineHeightPart, | |
| 1739 'margin': _marginPartMargin, | |
| 1740 'margin-left': _marginPartLeft, | |
| 1741 'margin-right': _marginPartRight, | |
| 1742 'margin-top': _marginPartTop, | |
| 1743 'margin-bottom': _marginPartBottom, | |
| 1744 'border': _borderPartBorder, | |
| 1745 'border-left': _borderPartLeft, | |
| 1746 'border-right': _borderPartRight, | |
| 1747 'border-top': _borderPartTop, | |
| 1748 'border-bottom': _borderPartBottom, | |
| 1749 'border-width': _borderPartWidth, | |
| 1750 'border-left-width': _borderPartLeftWidth, | |
| 1751 'border-top-width': _borderPartTopWidth, | |
| 1752 'border-right-width': _borderPartRightWidth, | |
| 1753 'border-bottom-width': _borderPartBottomWidth, | |
| 1754 'height': _heightPart, | |
| 1755 'width': _widthPart, | |
| 1756 'padding': _paddingPartPadding, | |
| 1757 'padding-left': _paddingPartLeft, | |
| 1758 'padding-top': _paddingPartTop, | |
| 1759 'padding-right': _paddingPartRight, | |
| 1760 'padding-bottom': _paddingPartBottom | |
| 1761 }; | |
| 1762 | |
| 1763 static const Map<String, int> _nameToFontWeight = const { | |
| 1764 'bold' : FontWeight.bold, | |
| 1765 'normal' : FontWeight.normal | |
| 1766 }; | |
| 1767 | |
| 1768 static int _findStyle(String styleName) => _stylesToDart[styleName]; | |
| 1769 | |
| 1770 DartStyleExpression _styleForDart(Identifier property, Expressions exprs, | |
| 1771 List dartStyles) { | |
| 1772 var styleType = _findStyle(property.name.toLowerCase()); | |
| 1773 if (styleType != null) { | |
| 1774 return buildDartStyleNode(styleType, exprs, dartStyles); | |
| 1775 } | |
| 1776 } | |
| 1777 | |
| 1778 FontExpression _mergeFontStyles(FontExpression fontExpr, List dartStyles) { | |
| 1779 // Merge all font styles for this class selector. | |
| 1780 for (var dartStyle in dartStyles) { | |
| 1781 if (dartStyle.isFont) { | |
| 1782 fontExpr = new FontExpression.merge(dartStyle, fontExpr); | |
| 1783 } | |
| 1784 } | |
| 1785 | |
| 1786 return fontExpr; | |
| 1787 } | |
| 1788 | |
| 1789 DartStyleExpression buildDartStyleNode(int styleType, Expressions exprs, | |
| 1790 List dartStyles) { | |
| 1791 | |
| 1792 switch (styleType) { | |
| 1793 /* | |
| 1794 * Properties in order: | |
| 1795 * | |
| 1796 * font-style font-variant font-weight font-size/line-height font-family | |
| 1797 * | |
| 1798 * The font-size and font-family values are required. If other values are | |
| 1799 * missing; a default, if it exist, will be used. | |
| 1800 */ | |
| 1801 case _fontPartFont: | |
| 1802 var processor = new ExpressionsProcessor(exprs); | |
| 1803 return _mergeFontStyles(processor.processFont(), dartStyles); | |
| 1804 case _fontPartFamily: | |
| 1805 var processor = new ExpressionsProcessor(exprs); | |
| 1806 | |
| 1807 try { | |
| 1808 return _mergeFontStyles(processor.processFontFamily(), dartStyles); | |
| 1809 } catch (fontException) { | |
| 1810 _error(fontException, _peekToken.span); | |
| 1811 } | |
| 1812 break; | |
| 1813 case _fontPartSize: | |
| 1814 var processor = new ExpressionsProcessor(exprs); | |
| 1815 return _mergeFontStyles(processor.processFontSize(), dartStyles); | |
| 1816 case _fontPartStyle: | |
| 1817 /* Possible style values: | |
| 1818 * normal [default] | |
| 1819 * italic | |
| 1820 * oblique | |
| 1821 * inherit | |
| 1822 */ | |
| 1823 // TODO(terry): TBD | |
| 1824 break; | |
| 1825 case _fontPartVariant: | |
| 1826 /* Possible variant values: | |
| 1827 * normal [default] | |
| 1828 * small-caps | |
| 1829 * inherit | |
| 1830 */ | |
| 1831 // TODO(terry): TBD | |
| 1832 break; | |
| 1833 case _fontPartWeight: | |
| 1834 /* Possible weight values: | |
| 1835 * normal [default] | |
| 1836 * bold | |
| 1837 * bolder | |
| 1838 * lighter | |
| 1839 * 100 - 900 | |
| 1840 * inherit | |
| 1841 */ | |
| 1842 // TODO(terry): Only 'normal', 'bold', or values of 100-900 supoorted | |
| 1843 // need to handle bolder, lighter, and inherit. See | |
| 1844 // https://github.com/dart-lang/csslib/issues/1 | |
| 1845 var expr = exprs.expressions[0]; | |
| 1846 if (expr is NumberTerm) { | |
| 1847 var fontExpr = new FontExpression(expr.span, | |
| 1848 weight: expr.value); | |
| 1849 return _mergeFontStyles(fontExpr, dartStyles); | |
| 1850 } else if (expr is LiteralTerm) { | |
| 1851 int weight = _nameToFontWeight[expr.value.toString()]; | |
| 1852 if (weight != null) { | |
| 1853 var fontExpr = new FontExpression(expr.span, weight: weight); | |
| 1854 return _mergeFontStyles(fontExpr, dartStyles); | |
| 1855 } | |
| 1856 } | |
| 1857 break; | |
| 1858 case _lineHeightPart: | |
| 1859 num lineHeight; | |
| 1860 if (exprs.expressions.length == 1) { | |
| 1861 var expr = exprs.expressions[0]; | |
| 1862 if (expr is UnitTerm) { | |
| 1863 UnitTerm unitTerm = expr; | |
| 1864 // TODO(terry): Need to handle other units and LiteralTerm normal | |
| 1865 // See https://github.com/dart-lang/csslib/issues/2. | |
| 1866 if (unitTerm.unit == TokenKind.UNIT_LENGTH_PX || | |
| 1867 unitTerm.unit == TokenKind.UNIT_LENGTH_PT) { | |
| 1868 var fontExpr = new FontExpression(expr.span, | |
| 1869 lineHeight: new LineHeight(expr.value, inPixels: true)); | |
| 1870 return _mergeFontStyles(fontExpr, dartStyles); | |
| 1871 } else if (isChecked) { | |
| 1872 _warning("Unexpected unit for line-height", expr.span); | |
| 1873 } | |
| 1874 } else if (expr is NumberTerm) { | |
| 1875 var fontExpr = new FontExpression(expr.span, | |
| 1876 lineHeight: new LineHeight(expr.value, inPixels: false)); | |
| 1877 return _mergeFontStyles(fontExpr, dartStyles); | |
| 1878 } else if (isChecked) { | |
| 1879 _warning("Unexpected value for line-height", expr.span); | |
| 1880 } | |
| 1881 } | |
| 1882 break; | |
| 1883 case _marginPartMargin: | |
| 1884 return new MarginExpression.boxEdge(exprs.span, processFourNums(exprs)); | |
| 1885 case _borderPartBorder: | |
| 1886 for (var expr in exprs.expressions) { | |
| 1887 var v = marginValue(expr); | |
| 1888 if (v != null) { | |
| 1889 final box = new BoxEdge.uniform(v); | |
| 1890 return new BorderExpression.boxEdge(exprs.span, box); | |
| 1891 } | |
| 1892 } | |
| 1893 break; | |
| 1894 case _borderPartWidth: | |
| 1895 var v = marginValue(exprs.expressions[0]); | |
| 1896 if (v != null) { | |
| 1897 final box = new BoxEdge.uniform(v); | |
| 1898 return new BorderExpression.boxEdge(exprs.span, box); | |
| 1899 } | |
| 1900 break; | |
| 1901 case _paddingPartPadding: | |
| 1902 return new PaddingExpression.boxEdge(exprs.span, | |
| 1903 processFourNums(exprs)); | |
| 1904 case _marginPartLeft: | |
| 1905 case _marginPartTop: | |
| 1906 case _marginPartRight: | |
| 1907 case _marginPartBottom: | |
| 1908 case _borderPartLeft: | |
| 1909 case _borderPartTop: | |
| 1910 case _borderPartRight: | |
| 1911 case _borderPartBottom: | |
| 1912 case _borderPartLeftWidth: | |
| 1913 case _borderPartTopWidth: | |
| 1914 case _borderPartRightWidth: | |
| 1915 case _borderPartBottomWidth: | |
| 1916 case _heightPart: | |
| 1917 case _widthPart: | |
| 1918 case _paddingPartLeft: | |
| 1919 case _paddingPartTop: | |
| 1920 case _paddingPartRight: | |
| 1921 case _paddingPartBottom: | |
| 1922 if (exprs.expressions.length > 0) { | |
| 1923 return processOneNumber(exprs, styleType); | |
| 1924 } | |
| 1925 break; | |
| 1926 default: | |
| 1927 // Don't handle it. | |
| 1928 return null; | |
| 1929 } | |
| 1930 } | |
| 1931 | |
| 1932 // TODO(terry): Look at handling width of thin, thick, etc. any none numbers | |
| 1933 // to convert to a number. | |
| 1934 DartStyleExpression processOneNumber(Expressions exprs, int part) { | |
| 1935 var value = marginValue(exprs.expressions[0]); | |
| 1936 if (value != null) { | |
| 1937 switch (part) { | |
| 1938 case _marginPartLeft: | |
| 1939 return new MarginExpression(exprs.span, left: value); | |
| 1940 case _marginPartTop: | |
| 1941 return new MarginExpression(exprs.span, top: value); | |
| 1942 case _marginPartRight: | |
| 1943 return new MarginExpression(exprs.span, right: value); | |
| 1944 case _marginPartBottom: | |
| 1945 return new MarginExpression(exprs.span, bottom: value); | |
| 1946 case _borderPartLeft: | |
| 1947 case _borderPartLeftWidth: | |
| 1948 return new BorderExpression(exprs.span, left: value); | |
| 1949 case _borderPartTop: | |
| 1950 case _borderPartTopWidth: | |
| 1951 return new BorderExpression(exprs.span, top: value); | |
| 1952 case _borderPartRight: | |
| 1953 case _borderPartRightWidth: | |
| 1954 return new BorderExpression(exprs.span, right: value); | |
| 1955 case _borderPartBottom: | |
| 1956 case _borderPartBottomWidth: | |
| 1957 return new BorderExpression(exprs.span, bottom: value); | |
| 1958 case _heightPart: | |
| 1959 return new HeightExpression(exprs.span, value); | |
| 1960 case _widthPart: | |
| 1961 return new WidthExpression(exprs.span, value); | |
| 1962 case _paddingPartLeft: | |
| 1963 return new PaddingExpression(exprs.span, left: value); | |
| 1964 case _paddingPartTop: | |
| 1965 return new PaddingExpression(exprs.span, top: value); | |
| 1966 case _paddingPartRight: | |
| 1967 return new PaddingExpression(exprs.span, right: value); | |
| 1968 case _paddingPartBottom: | |
| 1969 return new PaddingExpression(exprs.span, bottom: value); | |
| 1970 } | |
| 1971 } | |
| 1972 } | |
| 1973 | |
| 1974 /** | |
| 1975 * Margins are of the format: | |
| 1976 * | |
| 1977 * top,right,bottom,left (4 parameters) | |
| 1978 * top,right/left, bottom (3 parameters) | |
| 1979 * top/bottom,right/left (2 parameters) | |
| 1980 * top/right/bottom/left (1 parameter) | |
| 1981 * | |
| 1982 * The values of the margins can be a unit or unitless or auto. | |
| 1983 */ | |
| 1984 BoxEdge processFourNums(Expressions exprs) { | |
| 1985 num top; | |
| 1986 num right; | |
| 1987 num bottom; | |
| 1988 num left; | |
| 1989 | |
| 1990 int totalExprs = exprs.expressions.length; | |
| 1991 switch (totalExprs) { | |
| 1992 case 1: | |
| 1993 top = marginValue(exprs.expressions[0]); | |
| 1994 right = top; | |
| 1995 bottom = top; | |
| 1996 left = top; | |
| 1997 break; | |
| 1998 case 2: | |
| 1999 top = marginValue(exprs.expressions[0]); | |
| 2000 bottom = top; | |
| 2001 right = marginValue(exprs.expressions[1]); | |
| 2002 left = right; | |
| 2003 break; | |
| 2004 case 3: | |
| 2005 top = marginValue(exprs.expressions[0]); | |
| 2006 right = marginValue(exprs.expressions[1]); | |
| 2007 left = right; | |
| 2008 bottom = marginValue(exprs.expressions[2]); | |
| 2009 break; | |
| 2010 case 4: | |
| 2011 top = marginValue(exprs.expressions[0]); | |
| 2012 right = marginValue(exprs.expressions[1]); | |
| 2013 bottom = marginValue(exprs.expressions[2]); | |
| 2014 left = marginValue(exprs.expressions[3]); | |
| 2015 break; | |
| 2016 default: | |
| 2017 return null; | |
| 2018 } | |
| 2019 | |
| 2020 return new BoxEdge.clockwiseFromTop(top, right, bottom, left); | |
| 2021 } | |
| 2022 | |
| 2023 // TODO(terry): Need to handle auto. | |
| 2024 marginValue(var exprTerm) { | |
| 2025 if (exprTerm is UnitTerm || exprTerm is NumberTerm) { | |
| 2026 return exprTerm.value; | |
| 2027 } | |
| 2028 } | |
| 2029 | |
| 2030 // Expression grammar: | |
| 2031 // | |
| 2032 // expression: term [ operator? term]* | |
| 2033 // | |
| 2034 // operator: '/' | ',' | |
| 2035 // term: (see processTerm) | |
| 2036 // | |
| 2037 Expressions processExpr([bool ieFilter = false]) { | |
| 2038 var start = _peekToken.span; | |
| 2039 var expressions = new Expressions(_makeSpan(start)); | |
| 2040 | |
| 2041 var keepGoing = true; | |
| 2042 var expr; | |
| 2043 while (keepGoing && (expr = processTerm(ieFilter)) != null) { | |
| 2044 var op; | |
| 2045 | |
| 2046 var opStart = _peekToken.span; | |
| 2047 | |
| 2048 switch (_peek()) { | |
| 2049 case TokenKind.SLASH: | |
| 2050 op = new OperatorSlash(_makeSpan(opStart)); | |
| 2051 break; | |
| 2052 case TokenKind.COMMA: | |
| 2053 op = new OperatorComma(_makeSpan(opStart)); | |
| 2054 break; | |
| 2055 case TokenKind.BACKSLASH: | |
| 2056 // Backslash outside of string; detected IE8 or older signaled by \9 at | |
| 2057 // end of an expression. | |
| 2058 var ie8Start = _peekToken.span; | |
| 2059 | |
| 2060 _next(); | |
| 2061 if (_peekKind(TokenKind.INTEGER)) { | |
| 2062 var numToken = _next(); | |
| 2063 var value = int.parse(numToken.text); | |
| 2064 if (value == 9) { | |
| 2065 op = new IE8Term(_makeSpan(ie8Start)); | |
| 2066 } else if (isChecked) { | |
| 2067 _warning("\$value is not valid in an expression", _makeSpan(start)); | |
| 2068 } | |
| 2069 } | |
| 2070 break; | |
| 2071 } | |
| 2072 | |
| 2073 if (expr != null) { | |
| 2074 if (expr is List) { | |
| 2075 expr.forEach((exprItem) { | |
| 2076 expressions.add(exprItem); | |
| 2077 }); | |
| 2078 } else { | |
| 2079 expressions.add(expr); | |
| 2080 } | |
| 2081 } else { | |
| 2082 keepGoing = false; | |
| 2083 } | |
| 2084 | |
| 2085 if (op != null) { | |
| 2086 expressions.add(op); | |
| 2087 if (op is IE8Term) { | |
| 2088 keepGoing = false; | |
| 2089 } else { | |
| 2090 _next(); | |
| 2091 } | |
| 2092 } | |
| 2093 } | |
| 2094 | |
| 2095 return expressions; | |
| 2096 } | |
| 2097 | |
| 2098 static const int MAX_UNICODE = 0x10FFFF; | |
| 2099 | |
| 2100 // Term grammar: | |
| 2101 // | |
| 2102 // term: | |
| 2103 // unary_operator? | |
| 2104 // [ term_value ] | |
| 2105 // | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor | |
| 2106 // | |
| 2107 // term_value: | |
| 2108 // NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* | | |
| 2109 // TIME S* | FREQ S* | function | |
| 2110 // | |
| 2111 // NUMBER: {num} | |
| 2112 // PERCENTAGE: {num}% | |
| 2113 // LENGTH: {num}['px' | 'cm' | 'mm' | 'in' | 'pt' | 'pc'] | |
| 2114 // EMS: {num}'em' | |
| 2115 // EXS: {num}'ex' | |
| 2116 // ANGLE: {num}['deg' | 'rad' | 'grad'] | |
| 2117 // TIME: {num}['ms' | 's'] | |
| 2118 // FREQ: {num}['hz' | 'khz'] | |
| 2119 // function: IDENT '(' expr ')' | |
| 2120 // | |
| 2121 processTerm([bool ieFilter = false]) { | |
| 2122 var start = _peekToken.span; | |
| 2123 Token t; // token for term's value | |
| 2124 var value; // value of term (numeric values) | |
| 2125 | |
| 2126 var unary = ""; | |
| 2127 switch (_peek()) { | |
| 2128 case TokenKind.HASH: | |
| 2129 this._eat(TokenKind.HASH); | |
| 2130 if (!_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) { | |
| 2131 String hexText; | |
| 2132 if (_peekKind(TokenKind.INTEGER)) { | |
| 2133 String hexText1 = _peekToken.text; | |
| 2134 _next(); | |
| 2135 if (_peekIdentifier()) { | |
| 2136 hexText = '$hexText1${identifier().name}'; | |
| 2137 } else { | |
| 2138 hexText = hexText1; | |
| 2139 } | |
| 2140 } else if (_peekIdentifier()) { | |
| 2141 hexText = identifier().name; | |
| 2142 } | |
| 2143 if (hexText != null) { | |
| 2144 return _parseHex(hexText, _makeSpan(start)); | |
| 2145 } | |
| 2146 } | |
| 2147 | |
| 2148 if (isChecked) { | |
| 2149 _warning("Expected hex number", _makeSpan(start)); | |
| 2150 } | |
| 2151 // Construct the bad hex value with a #<space>number. | |
| 2152 return _parseHex(" ${processTerm().text}", _makeSpan(start)); | |
| 2153 case TokenKind.INTEGER: | |
| 2154 t = _next(); | |
| 2155 value = int.parse("${unary}${t.text}"); | |
| 2156 break; | |
| 2157 case TokenKind.DOUBLE: | |
| 2158 t = _next(); | |
| 2159 value = double.parse("${unary}${t.text}"); | |
| 2160 break; | |
| 2161 case TokenKind.SINGLE_QUOTE: | |
| 2162 value = processQuotedString(false); | |
| 2163 value = "'${_escapeString(value, single: true)}'"; | |
| 2164 return new LiteralTerm(value, value, _makeSpan(start)); | |
| 2165 case TokenKind.DOUBLE_QUOTE: | |
| 2166 value = processQuotedString(false); | |
| 2167 value = '"${_escapeString(value)}"'; | |
| 2168 return new LiteralTerm(value, value, _makeSpan(start)); | |
| 2169 case TokenKind.LPAREN: | |
| 2170 _next(); | |
| 2171 | |
| 2172 GroupTerm group = new GroupTerm(_makeSpan(start)); | |
| 2173 | |
| 2174 var term; | |
| 2175 do { | |
| 2176 term = processTerm(); | |
| 2177 if (term != null && term is LiteralTerm) { | |
| 2178 group.add(term); | |
| 2179 } | |
| 2180 } while (term != null && !_maybeEat(TokenKind.RPAREN) && | |
| 2181 !isPrematureEndOfFile()); | |
| 2182 | |
| 2183 return group; | |
| 2184 case TokenKind.LBRACK: | |
| 2185 _next(); | |
| 2186 | |
| 2187 var term = processTerm(); | |
| 2188 if (!(term is NumberTerm)) { | |
| 2189 _error('Expecting a positive number', _makeSpan(start)); | |
| 2190 } | |
| 2191 | |
| 2192 _eat(TokenKind.RBRACK); | |
| 2193 | |
| 2194 return new ItemTerm(term.value, term.text, _makeSpan(start)); | |
| 2195 case TokenKind.IDENTIFIER: | |
| 2196 var nameValue = identifier(); // Snarf up the ident we'll remap, maybe. | |
| 2197 | |
| 2198 if (!ieFilter && _maybeEat(TokenKind.LPAREN)) { | |
| 2199 // FUNCTION | |
| 2200 return processFunction(nameValue); | |
| 2201 } if (ieFilter) { | |
| 2202 if (_maybeEat(TokenKind.COLON) && | |
| 2203 nameValue.name.toLowerCase() == 'progid') { | |
| 2204 // IE filter:progid: | |
| 2205 return processIEFilter(start); | |
| 2206 } else { | |
| 2207 // Handle filter:<name> where name is any filter e.g., alpha, chroma, | |
| 2208 // Wave, blur, etc. | |
| 2209 return processIEFilter(start); | |
| 2210 } | |
| 2211 } | |
| 2212 | |
| 2213 // TODO(terry): Need to have a list of known identifiers today only | |
| 2214 // 'from' is special. | |
| 2215 if (nameValue.name == 'from') { | |
| 2216 return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start)); | |
| 2217 } | |
| 2218 | |
| 2219 // What kind of identifier is it, named color? | |
| 2220 var colorEntry = TokenKind.matchColorName(nameValue.name); | |
| 2221 if (colorEntry == null) { | |
| 2222 if (isChecked) { | |
| 2223 var propName = nameValue.name; | |
| 2224 var errMsg = TokenKind.isPredefinedName(propName) ? | |
| 2225 "Improper use of property value ${propName}" : | |
| 2226 "Unknown property value ${propName}"; | |
| 2227 _warning(errMsg, _makeSpan(start)); | |
| 2228 } | |
| 2229 return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start)); | |
| 2230 } | |
| 2231 | |
| 2232 // Yes, process the color as an RGB value. | |
| 2233 var rgbColor = | |
| 2234 TokenKind.decimalToHex(TokenKind.colorValue(colorEntry), 6); | |
| 2235 return _parseHex(rgbColor, _makeSpan(start)); | |
| 2236 case TokenKind.UNICODE_RANGE: | |
| 2237 var first; | |
| 2238 var second; | |
| 2239 var firstNumber; | |
| 2240 var secondNumber; | |
| 2241 _eat(TokenKind.UNICODE_RANGE, unicodeRange: true); | |
| 2242 if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) { | |
| 2243 first = _previousToken.text; | |
| 2244 firstNumber = int.parse('0x$first'); | |
| 2245 if (firstNumber > MAX_UNICODE) { | |
| 2246 _error("unicode range must be less than 10FFFF", _makeSpan(start)); | |
| 2247 } | |
| 2248 if (_maybeEat(TokenKind.MINUS, unicodeRange: true)) { | |
| 2249 if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) { | |
| 2250 second = _previousToken.text; | |
| 2251 secondNumber = int.parse('0x$second'); | |
| 2252 if (secondNumber > MAX_UNICODE) { | |
| 2253 _error("unicode range must be less than 10FFFF", | |
| 2254 _makeSpan(start)); | |
| 2255 } | |
| 2256 if (firstNumber > secondNumber) { | |
| 2257 _error("unicode first range can not be greater than last", | |
| 2258 _makeSpan(start)); | |
| 2259 } | |
| 2260 } | |
| 2261 } | |
| 2262 } else if (_maybeEat(TokenKind.HEX_RANGE, unicodeRange: true)) { | |
| 2263 first = _previousToken.text; | |
| 2264 } | |
| 2265 | |
| 2266 return new UnicodeRangeTerm(first, second, _makeSpan(start)); | |
| 2267 case TokenKind.AT: | |
| 2268 if (messages.options.lessSupport) { | |
| 2269 _next(); | |
| 2270 | |
| 2271 var expr = processExpr(); | |
| 2272 if (isChecked && expr.expressions.length > 1) { | |
| 2273 _error("only @name for Less syntax", _peekToken.span); | |
| 2274 } | |
| 2275 | |
| 2276 var param = expr.expressions[0]; | |
| 2277 var varUsage = new VarUsage(param.text, [], _makeSpan(start)); | |
| 2278 expr.expressions[0] = varUsage; | |
| 2279 return expr.expressions; | |
| 2280 } | |
| 2281 break; | |
| 2282 } | |
| 2283 | |
| 2284 return processDimension(t, value, _makeSpan(start)); | |
| 2285 } | |
| 2286 | |
| 2287 /** Process all dimension units. */ | |
| 2288 LiteralTerm processDimension(Token t, var value, SourceSpan span) { | |
| 2289 LiteralTerm term; | |
| 2290 var unitType = this._peek(); | |
| 2291 | |
| 2292 switch (unitType) { | |
| 2293 case TokenKind.UNIT_EM: | |
| 2294 term = new EmTerm(value, t.text, span); | |
| 2295 _next(); // Skip the unit | |
| 2296 break; | |
| 2297 case TokenKind.UNIT_EX: | |
| 2298 term = new ExTerm(value, t.text, span); | |
| 2299 _next(); // Skip the unit | |
| 2300 break; | |
| 2301 case TokenKind.UNIT_LENGTH_PX: | |
| 2302 case TokenKind.UNIT_LENGTH_CM: | |
| 2303 case TokenKind.UNIT_LENGTH_MM: | |
| 2304 case TokenKind.UNIT_LENGTH_IN: | |
| 2305 case TokenKind.UNIT_LENGTH_PT: | |
| 2306 case TokenKind.UNIT_LENGTH_PC: | |
| 2307 term = new LengthTerm(value, t.text, span, unitType); | |
| 2308 _next(); // Skip the unit | |
| 2309 break; | |
| 2310 case TokenKind.UNIT_ANGLE_DEG: | |
| 2311 case TokenKind.UNIT_ANGLE_RAD: | |
| 2312 case TokenKind.UNIT_ANGLE_GRAD: | |
| 2313 case TokenKind.UNIT_ANGLE_TURN: | |
| 2314 term = new AngleTerm(value, t.text, span, unitType); | |
| 2315 _next(); // Skip the unit | |
| 2316 break; | |
| 2317 case TokenKind.UNIT_TIME_MS: | |
| 2318 case TokenKind.UNIT_TIME_S: | |
| 2319 term = new TimeTerm(value, t.text, span, unitType); | |
| 2320 _next(); // Skip the unit | |
| 2321 break; | |
| 2322 case TokenKind.UNIT_FREQ_HZ: | |
| 2323 case TokenKind.UNIT_FREQ_KHZ: | |
| 2324 term = new FreqTerm(value, t.text, span, unitType); | |
| 2325 _next(); // Skip the unit | |
| 2326 break; | |
| 2327 case TokenKind.PERCENT: | |
| 2328 term = new PercentageTerm(value, t.text, span); | |
| 2329 _next(); // Skip the % | |
| 2330 break; | |
| 2331 case TokenKind.UNIT_FRACTION: | |
| 2332 term = new FractionTerm(value, t.text, span); | |
| 2333 _next(); // Skip the unit | |
| 2334 break; | |
| 2335 case TokenKind.UNIT_RESOLUTION_DPI: | |
| 2336 case TokenKind.UNIT_RESOLUTION_DPCM: | |
| 2337 case TokenKind.UNIT_RESOLUTION_DPPX: | |
| 2338 term = new ResolutionTerm(value, t.text, span, unitType); | |
| 2339 _next(); // Skip the unit | |
| 2340 break; | |
| 2341 case TokenKind.UNIT_CH: | |
| 2342 term = new ChTerm(value, t.text, span, unitType); | |
| 2343 _next(); // Skip the unit | |
| 2344 break; | |
| 2345 case TokenKind.UNIT_REM: | |
| 2346 term = new RemTerm(value, t.text, span, unitType); | |
| 2347 _next(); // Skip the unit | |
| 2348 break; | |
| 2349 case TokenKind.UNIT_VIEWPORT_VW: | |
| 2350 case TokenKind.UNIT_VIEWPORT_VH: | |
| 2351 case TokenKind.UNIT_VIEWPORT_VMIN: | |
| 2352 case TokenKind.UNIT_VIEWPORT_VMAX: | |
| 2353 term = new ViewportTerm(value, t.text, span, unitType); | |
| 2354 _next(); // Skip the unit | |
| 2355 break; | |
| 2356 default: | |
| 2357 if (value != null && t != null) { | |
| 2358 term = (value is Identifier) | |
| 2359 ? new LiteralTerm(value, value.name, span) | |
| 2360 : new NumberTerm(value, t.text, span); | |
| 2361 } | |
| 2362 break; | |
| 2363 } | |
| 2364 | |
| 2365 return term; | |
| 2366 } | |
| 2367 | |
| 2368 String processQuotedString([bool urlString = false]) { | |
| 2369 var start = _peekToken.span; | |
| 2370 | |
| 2371 // URI term sucks up everything inside of quotes(' or ") or between parens | |
| 2372 var stopToken = urlString ? TokenKind.RPAREN : -1; | |
| 2373 | |
| 2374 // Note: disable skipping whitespace tokens inside a string. | |
| 2375 // TODO(jmesserly): the layering here feels wrong. | |
| 2376 var skipWhitespace = tokenizer._skipWhitespace; | |
| 2377 tokenizer._skipWhitespace = false; | |
| 2378 | |
| 2379 switch (_peek()) { | |
| 2380 case TokenKind.SINGLE_QUOTE: | |
| 2381 stopToken = TokenKind.SINGLE_QUOTE; | |
| 2382 _next(); // Skip the SINGLE_QUOTE. | |
| 2383 start = _peekToken.span; | |
| 2384 break; | |
| 2385 case TokenKind.DOUBLE_QUOTE: | |
| 2386 stopToken = TokenKind.DOUBLE_QUOTE; | |
| 2387 _next(); // Skip the DOUBLE_QUOTE. | |
| 2388 start = _peekToken.span; | |
| 2389 break; | |
| 2390 default: | |
| 2391 if (urlString) { | |
| 2392 if (_peek() == TokenKind.LPAREN) { | |
| 2393 _next(); // Skip the LPAREN. | |
| 2394 start = _peekToken.span; | |
| 2395 } | |
| 2396 stopToken = TokenKind.RPAREN; | |
| 2397 } else { | |
| 2398 _error('unexpected string', _makeSpan(start)); | |
| 2399 } | |
| 2400 break; | |
| 2401 } | |
| 2402 | |
| 2403 // Gobble up everything until we hit our stop token. | |
| 2404 var stringValue = new StringBuffer(); | |
| 2405 while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) { | |
| 2406 stringValue.write(_next().text); | |
| 2407 } | |
| 2408 | |
| 2409 tokenizer._skipWhitespace = skipWhitespace; | |
| 2410 | |
| 2411 // All characters between quotes is the string. | |
| 2412 if (stopToken != TokenKind.RPAREN) { | |
| 2413 _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE; | |
| 2414 } | |
| 2415 | |
| 2416 return stringValue.toString(); | |
| 2417 } | |
| 2418 | |
| 2419 // TODO(terry): Should probably understand IE's non-standard filter syntax to | |
| 2420 // fully support calc, var(), etc. | |
| 2421 /** | |
| 2422 * IE's filter property breaks CSS value parsing. IE's format can be: | |
| 2423 * | |
| 2424 * filter: progid:DXImageTransform.MS.gradient(Type=0, Color='#9d8b83'); | |
| 2425 * | |
| 2426 * We'll just parse everything after the 'progid:' look for the left paren | |
| 2427 * then parse to the right paren ignoring everything in between. | |
| 2428 */ | |
| 2429 processIEFilter(FileSpan startAfterProgidColon) { | |
| 2430 var parens = 0; | |
| 2431 | |
| 2432 while (_peek() != TokenKind.END_OF_FILE) { | |
| 2433 switch (_peek()) { | |
| 2434 case TokenKind.LPAREN: | |
| 2435 _eat(TokenKind.LPAREN); | |
| 2436 parens++; | |
| 2437 break; | |
| 2438 case TokenKind.RPAREN: | |
| 2439 _eat(TokenKind.RPAREN); | |
| 2440 if (--parens == 0) { | |
| 2441 var tok = tokenizer.makeIEFilter(startAfterProgidColon.start.offset, | |
| 2442 _peekToken.start); | |
| 2443 return new LiteralTerm(tok.text, tok.text, tok.span); | |
| 2444 } | |
| 2445 break; | |
| 2446 default: | |
| 2447 _eat(_peek()); | |
| 2448 } | |
| 2449 } | |
| 2450 } | |
| 2451 | |
| 2452 // Function grammar: | |
| 2453 // | |
| 2454 // function: IDENT '(' expr ')' | |
| 2455 // | |
| 2456 processFunction(Identifier func) { | |
| 2457 var start = _peekToken.span; | |
| 2458 | |
| 2459 var name = func.name; | |
| 2460 | |
| 2461 switch (name) { | |
| 2462 case 'url': | |
| 2463 // URI term sucks up everything inside of quotes(' or ") or between parens | |
| 2464 var urlParam = processQuotedString(true); | |
| 2465 | |
| 2466 // TODO(terry): Better error messge and checking for mismatched quotes. | |
| 2467 if (_peek() == TokenKind.END_OF_FILE) { | |
| 2468 _error("problem parsing URI", _peekToken.span); | |
| 2469 } | |
| 2470 | |
| 2471 if (_peek() == TokenKind.RPAREN) { | |
| 2472 _next(); | |
| 2473 } | |
| 2474 | |
| 2475 return new UriTerm(urlParam, _makeSpan(start)); | |
| 2476 case 'calc': | |
| 2477 // TODO(terry): Implement expression handling... | |
| 2478 break; | |
| 2479 case 'var': | |
| 2480 // TODO(terry): Consider handling var in IE specific filter/progid. This | |
| 2481 // will require parsing entire IE specific syntax e.g., | |
| 2482 // param = value or progid:com_id, etc. for example: | |
| 2483 // | |
| 2484 // var-blur: Blur(Add = 0, Direction = 225, Strength = 10); | |
| 2485 // var-gradient: progid:DXImageTransform.Microsoft.gradient" | |
| 2486 // (GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); | |
| 2487 var expr = processExpr(); | |
| 2488 if (!_maybeEat(TokenKind.RPAREN)) { | |
| 2489 _error("problem parsing var expected ), ", _peekToken.span); | |
| 2490 } | |
| 2491 if (isChecked && | |
| 2492 expr.expressions.where((e) => e is OperatorComma).length > 1) { | |
| 2493 _error("too many parameters to var()", _peekToken.span); | |
| 2494 } | |
| 2495 | |
| 2496 var paramName = expr.expressions[0].text; | |
| 2497 | |
| 2498 // [0] - var name, [1] - OperatorComma, [2] - default value. | |
| 2499 var defaultValues = expr.expressions.length >= 3 | |
| 2500 ? expr.expressions.sublist(2) : []; | |
| 2501 return new VarUsage(paramName, defaultValues, _makeSpan(start)); | |
| 2502 default: | |
| 2503 var expr = processExpr(); | |
| 2504 if (!_maybeEat(TokenKind.RPAREN)) { | |
| 2505 _error("problem parsing function expected ), ", _peekToken.span); | |
| 2506 } | |
| 2507 | |
| 2508 return new FunctionTerm(name, name, expr, _makeSpan(start)); | |
| 2509 } | |
| 2510 | |
| 2511 return null; | |
| 2512 } | |
| 2513 | |
| 2514 Identifier identifier() { | |
| 2515 var tok = _next(); | |
| 2516 | |
| 2517 if (!TokenKind.isIdentifier(tok.kind) && | |
| 2518 !TokenKind.isKindIdentifier(tok.kind)) { | |
| 2519 if (isChecked) { | |
| 2520 _warning('expected identifier, but found $tok', tok.span); | |
| 2521 } | |
| 2522 return new Identifier("", _makeSpan(tok.span)); | |
| 2523 } | |
| 2524 | |
| 2525 return new Identifier(tok.text, _makeSpan(tok.span)); | |
| 2526 } | |
| 2527 | |
| 2528 // TODO(terry): Move this to base <= 36 and into shared code. | |
| 2529 static int _hexDigit(int c) { | |
| 2530 if (c >= 48/*0*/ && c <= 57/*9*/) { | |
| 2531 return c - 48; | |
| 2532 } else if (c >= 97/*a*/ && c <= 102/*f*/) { | |
| 2533 return c - 87; | |
| 2534 } else if (c >= 65/*A*/ && c <= 70/*F*/) { | |
| 2535 return c - 55; | |
| 2536 } else { | |
| 2537 return -1; | |
| 2538 } | |
| 2539 } | |
| 2540 | |
| 2541 HexColorTerm _parseHex(String hexText, SourceSpan span) { | |
| 2542 var hexValue = 0; | |
| 2543 | |
| 2544 for (var i = 0; i < hexText.length; i++) { | |
| 2545 var digit = _hexDigit(hexText.codeUnitAt(i)); | |
| 2546 if (digit < 0) { | |
| 2547 _warning('Bad hex number', span); | |
| 2548 return new HexColorTerm(new BAD_HEX_VALUE(), hexText, span); | |
| 2549 } | |
| 2550 hexValue = (hexValue << 4) + digit; | |
| 2551 } | |
| 2552 | |
| 2553 // Make 3 character hex value #RRGGBB => #RGB iff: | |
| 2554 // high/low nibble of RR is the same, high/low nibble of GG is the same and | |
| 2555 // high/low nibble of BB is the same. | |
| 2556 if (hexText.length == 6 && | |
| 2557 hexText[0] == hexText[1] && | |
| 2558 hexText[2] == hexText[3] && | |
| 2559 hexText[4] == hexText[5]) { | |
| 2560 hexText = '${hexText[0]}${hexText[2]}${hexText[4]}'; | |
| 2561 } else if (hexText.length == 4 && | |
| 2562 hexText[0] == hexText[1] && | |
| 2563 hexText[2] == hexText[3]) { | |
| 2564 hexText = '${hexText[0]}${hexText[2]}'; | |
| 2565 } else if (hexText.length == 2 && hexText[0] == hexText[1]) { | |
| 2566 hexText = '${hexText[0]}'; | |
| 2567 } | |
| 2568 return new HexColorTerm(hexValue, hexText, span); | |
| 2569 } | |
| 2570 } | |
| 2571 | |
| 2572 class ExpressionsProcessor { | |
| 2573 final Expressions _exprs; | |
| 2574 int _index = 0; | |
| 2575 | |
| 2576 ExpressionsProcessor(this._exprs); | |
| 2577 | |
| 2578 // TODO(terry): Only handles ##px unit. | |
| 2579 FontExpression processFontSize() { | |
| 2580 /* font-size[/line-height] | |
| 2581 * | |
| 2582 * Possible size values: | |
| 2583 * xx-small | |
| 2584 * small | |
| 2585 * medium [default] | |
| 2586 * large | |
| 2587 * x-large | |
| 2588 * xx-large | |
| 2589 * smaller | |
| 2590 * larger | |
| 2591 * ##length in px, pt, etc. | |
| 2592 * ##%, percent of parent elem's font-size | |
| 2593 * inherit | |
| 2594 */ | |
| 2595 LengthTerm size; | |
| 2596 LineHeight lineHt; | |
| 2597 var nextIsLineHeight = false; | |
| 2598 for (; _index < _exprs.expressions.length; _index++) { | |
| 2599 var expr = _exprs.expressions[_index]; | |
| 2600 if (size == null && expr is LengthTerm) { | |
| 2601 // font-size part. | |
| 2602 size = expr; | |
| 2603 } else if (size != null) { | |
| 2604 if (expr is OperatorSlash) { | |
| 2605 // LineHeight could follow? | |
| 2606 nextIsLineHeight = true; | |
| 2607 } else if (nextIsLineHeight && expr is LengthTerm) { | |
| 2608 assert(expr.unit == TokenKind.UNIT_LENGTH_PX); | |
| 2609 lineHt = new LineHeight(expr.value, inPixels: true); | |
| 2610 nextIsLineHeight = false; | |
| 2611 _index++; | |
| 2612 break; | |
| 2613 } else { | |
| 2614 break; | |
| 2615 } | |
| 2616 } else { | |
| 2617 break; | |
| 2618 } | |
| 2619 } | |
| 2620 | |
| 2621 return new FontExpression(_exprs.span, size: size, lineHeight: lineHt); | |
| 2622 } | |
| 2623 | |
| 2624 FontExpression processFontFamily() { | |
| 2625 var family = <String>[]; | |
| 2626 | |
| 2627 /* Possible family values: | |
| 2628 * font-family: arial, Times new roman ,Lucida Sans Unicode,Courier; | |
| 2629 * font-family: "Times New Roman", arial, Lucida Sans Unicode, Courier; | |
| 2630 */ | |
| 2631 var moreFamilies = false; | |
| 2632 | |
| 2633 for (; _index < _exprs.expressions.length; _index++) { | |
| 2634 Expression expr = _exprs.expressions[_index]; | |
| 2635 if (expr is LiteralTerm) { | |
| 2636 if (family.length == 0 || moreFamilies) { | |
| 2637 // It's font-family now. | |
| 2638 family.add(expr.toString()); | |
| 2639 moreFamilies = false; | |
| 2640 } else if (isChecked) { | |
| 2641 messages.warning('Only font-family can be a list', _exprs.span); | |
| 2642 } | |
| 2643 } else if (expr is OperatorComma && family.length > 0) { | |
| 2644 moreFamilies = true; | |
| 2645 } else { | |
| 2646 break; | |
| 2647 } | |
| 2648 } | |
| 2649 | |
| 2650 return new FontExpression(_exprs.span, family: family); | |
| 2651 } | |
| 2652 | |
| 2653 FontExpression processFont() { | |
| 2654 List<String> family; | |
| 2655 | |
| 2656 // Process all parts of the font expression. | |
| 2657 FontExpression fontSize; | |
| 2658 FontExpression fontFamily; | |
| 2659 for (; _index < _exprs.expressions.length; _index++) { | |
| 2660 var expr = _exprs.expressions[_index]; | |
| 2661 // Order is font-size font-family | |
| 2662 if (fontSize == null) { | |
| 2663 fontSize = processFontSize(); | |
| 2664 } | |
| 2665 if (fontFamily == null) { | |
| 2666 fontFamily = processFontFamily(); | |
| 2667 } | |
| 2668 //TODO(terry): Handle font-weight, font-style, and font-variant. See | |
| 2669 // https://github.com/dart-lang/csslib/issues/3 | |
| 2670 // https://github.com/dart-lang/csslib/issues/4 | |
| 2671 // https://github.com/dart-lang/csslib/issues/5 | |
| 2672 } | |
| 2673 | |
| 2674 return new FontExpression(_exprs.span, | |
| 2675 size: fontSize.font.size, | |
| 2676 lineHeight: fontSize.font.lineHeight, | |
| 2677 family: fontFamily.font.family); | |
| 2678 } | |
| 2679 } | |
| 2680 | |
| 2681 /** | |
| 2682 * Escapes [text] for use in a CSS string. | |
| 2683 * [single] specifies single quote `'` vs double quote `"`. | |
| 2684 */ | |
| 2685 String _escapeString(String text, {bool single: false}) { | |
| 2686 StringBuffer result = null; | |
| 2687 | |
| 2688 for (int i = 0; i < text.length; i++) { | |
| 2689 var code = text.codeUnitAt(i); | |
| 2690 String replace = null; | |
| 2691 switch (code) { | |
| 2692 case 34/*'"'*/: if (!single) replace = r'\"'; break; | |
| 2693 case 39/*"'"*/: if (single) replace = r"\'"; break; | |
| 2694 } | |
| 2695 | |
| 2696 if (replace != null && result == null) { | |
| 2697 result = new StringBuffer(text.substring(0, i)); | |
| 2698 } | |
| 2699 | |
| 2700 if (result != null) result.write(replace != null ? replace : text[i]); | |
| 2701 } | |
| 2702 | |
| 2703 return result == null ? text : result.toString(); | |
| 2704 } | |
| OLD | NEW |