Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(374)

Side by Side Diff: pkg/csslib/lib/parser.dart

Issue 23168002: move csslib into dart svn (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/csslib/lib/css.dart ('k') | pkg/csslib/lib/src/analyzer.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « pkg/csslib/lib/css.dart ('k') | pkg/csslib/lib/src/analyzer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698