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

Side by Side Diff: observatory_pub_packages/csslib/parser.dart

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

Powered by Google App Engine
This is Rietveld 408576698