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