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

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

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

Powered by Google App Engine
This is Rietveld 408576698