OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library csslib.parser; | 5 library csslib.parser; |
6 | 6 |
7 import 'dart:math' as math; | 7 import 'dart:math' as math; |
8 | 8 |
9 import 'package:source_maps/span.dart' show SourceFile, Span, FileSpan; | 9 import 'package:source_maps/span.dart' show SourceFile, Span, FileSpan; |
10 | 10 |
(...skipping 27 matching lines...) Expand all Loading... |
38 } | 38 } |
39 var opt = PreprocessorOptions.parse(options); | 39 var opt = PreprocessorOptions.parse(options); |
40 messages = new Messages(options: opt, printHandler: errors.add); | 40 messages = new Messages(options: opt, printHandler: errors.add); |
41 } | 41 } |
42 | 42 |
43 /** CSS checked mode enabled. */ | 43 /** CSS checked mode enabled. */ |
44 bool get isChecked => messages.options.checked; | 44 bool get isChecked => messages.options.checked; |
45 | 45 |
46 // TODO(terry): Remove nested name parameter. | 46 // TODO(terry): Remove nested name parameter. |
47 /** Parse and analyze the CSS file. */ | 47 /** Parse and analyze the CSS file. */ |
48 StyleSheet compile(var input, | 48 StyleSheet compile(var input, {List errors, List options, bool nested: true, |
49 {List errors, List options, bool nested: true, bool polyfill: false}) { | 49 bool polyfill: false, List<StyleSheet> includes: null}) { |
| 50 if (includes == null) { |
| 51 includes = []; |
| 52 } |
| 53 |
50 var source = _inputAsString(input); | 54 var source = _inputAsString(input); |
51 | 55 |
52 _createMessages(errors: errors, options: options); | 56 _createMessages(errors: errors, options: options); |
53 | 57 |
54 var file = new SourceFile.text(null, source); | 58 var file = new SourceFile.text(null, source); |
55 | 59 |
56 var tree = new Parser(file, source).parse(); | 60 var tree = new _Parser(file, source).parse(); |
57 | 61 |
58 analyze([tree], errors: errors, options: options); | 62 analyze([tree], errors: errors, options: options); |
59 | 63 |
60 if (polyfill) { | 64 if (polyfill) { |
61 var processCss = new PolyFill(messages, true); | 65 var processCss = new PolyFill(messages, true); |
62 processCss.process(tree); | 66 processCss.process(tree, includes: includes); |
63 } | 67 } |
64 | 68 |
65 return tree; | 69 return tree; |
66 } | 70 } |
67 | 71 |
68 /** Analyze the CSS file. */ | 72 /** Analyze the CSS file. */ |
69 void analyze(List<StyleSheet> styleSheets, {List errors, List options}) { | 73 void analyze(List<StyleSheet> styleSheets, {List errors, List options}) { |
70 _createMessages(errors: errors, options: options); | 74 _createMessages(errors: errors, options: options); |
71 new Analyzer(styleSheets, messages).run(); | 75 new Analyzer(styleSheets, messages).run(); |
72 } | 76 } |
73 | 77 |
74 /** | 78 /** |
75 * Parse the [input] CSS stylesheet into a tree. The [input] can be a [String], | 79 * Parse the [input] CSS stylesheet into a tree. The [input] can be a [String], |
76 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional | 80 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional |
77 * [errors] list will contain each error/warning as a [Message]. | 81 * [errors] list will contain each error/warning as a [Message]. |
78 */ | 82 */ |
79 StyleSheet parse(var input, {List errors, List options}) { | 83 StyleSheet parse(var input, {List errors, List options}) { |
80 var source = _inputAsString(input); | 84 var source = _inputAsString(input); |
81 | 85 |
82 _createMessages(errors: errors, options: options); | 86 _createMessages(errors: errors, options: options); |
83 | 87 |
84 var file = new SourceFile.text(null, source); | 88 var file = new SourceFile.text(null, source); |
85 | 89 |
86 return new Parser(file, source).parse(); | 90 return new _Parser(file, source).parse(); |
87 } | 91 } |
88 | 92 |
89 /** | 93 /** |
90 * Parse the [input] CSS selector into a tree. The [input] can be a [String], | 94 * Parse the [input] CSS selector into a tree. The [input] can be a [String], |
91 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional | 95 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional |
92 * [errors] list will contain each error/warning as a [Message]. | 96 * [errors] list will contain each error/warning as a [Message]. |
93 */ | 97 */ |
94 StyleSheet selector(var input, {List errors}) { | 98 StyleSheet selector(var input, {List errors}) { |
95 var source = _inputAsString(input); | 99 var source = _inputAsString(input); |
96 | 100 |
97 _createMessages(errors: errors); | 101 _createMessages(errors: errors); |
98 | 102 |
99 var file = new SourceFile.text(null, source); | 103 var file = new SourceFile.text(null, source); |
100 | 104 |
101 return new Parser(file, source).parseSelector(); | 105 return new _Parser(file, source).parseSelector(); |
102 } | 106 } |
103 | 107 |
104 String _inputAsString(var input) { | 108 String _inputAsString(var input) { |
105 String source; | 109 String source; |
106 | 110 |
107 if (input is String) { | 111 if (input is String) { |
108 source = input; | 112 source = input; |
109 } else if (input is List<int>) { | 113 } else if (input is List<int>) { |
110 // TODO(terry): The parse function needs an "encoding" argument and will | 114 // TODO(terry): The parse function needs an "encoding" argument and will |
111 // default to whatever encoding CSS defaults to. | 115 // default to whatever encoding CSS defaults to. |
(...skipping 13 matching lines...) Expand all Loading... |
125 } else { | 129 } else { |
126 // TODO(terry): Support RandomAccessFile using console. | 130 // TODO(terry): Support RandomAccessFile using console. |
127 throw new ArgumentError("'source' must be a String or " | 131 throw new ArgumentError("'source' must be a String or " |
128 "List<int> (of bytes). RandomAccessFile not supported from this " | 132 "List<int> (of bytes). RandomAccessFile not supported from this " |
129 "simple interface"); | 133 "simple interface"); |
130 } | 134 } |
131 | 135 |
132 return source; | 136 return source; |
133 } | 137 } |
134 | 138 |
| 139 // TODO(terry): Consider removing this class when all usages can be eliminated |
| 140 // or replaced with compile API. |
| 141 /** Public parsing interface for csslib. */ |
| 142 class Parser { |
| 143 final _Parser _parser; |
| 144 |
| 145 Parser(SourceFile file, String text, {int start: 0, String baseUrl}) : |
| 146 _parser = new _Parser(file, text, start: start, baseUrl: baseUrl); |
| 147 |
| 148 StyleSheet parse() => _parser.parse(); |
| 149 } |
| 150 |
135 /** A simple recursive descent parser for CSS. */ | 151 /** A simple recursive descent parser for CSS. */ |
136 class Parser { | 152 class _Parser { |
137 Tokenizer tokenizer; | 153 Tokenizer tokenizer; |
138 | 154 |
139 /** Base url of CSS file. */ | 155 /** Base url of CSS file. */ |
140 final String _baseUrl; | 156 final String _baseUrl; |
141 | 157 |
142 /** | 158 /** |
143 * File containing the source being parsed, used to report errors with | 159 * File containing the source being parsed, used to report errors with |
144 * source-span locations. | 160 * source-span locations. |
145 */ | 161 */ |
146 final SourceFile file; | 162 final SourceFile file; |
147 | 163 |
148 Token _previousToken; | 164 Token _previousToken; |
149 Token _peekToken; | 165 Token _peekToken; |
150 | 166 |
151 Parser(SourceFile file, String text, {int start: 0, String baseUrl}) | 167 _Parser(SourceFile file, String text, {int start: 0, String baseUrl}) |
152 : this.file = file, | 168 : this.file = file, |
153 _baseUrl = baseUrl, | 169 _baseUrl = baseUrl, |
154 tokenizer = new Tokenizer(file, text, true, start) { | 170 tokenizer = new Tokenizer(file, text, true, start) { |
155 _peekToken = tokenizer.next(); | 171 _peekToken = tokenizer.next(); |
156 } | 172 } |
157 | 173 |
158 /** Main entry point for parsing an entire CSS file. */ | 174 /** Main entry point for parsing an entire CSS file. */ |
159 StyleSheet parse() { | 175 StyleSheet parse() { |
160 List<TreeNode> productions = []; | 176 List<TreeNode> productions = []; |
161 | 177 |
(...skipping 260 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
422 return null; | 438 return null; |
423 } | 439 } |
424 } | 440 } |
425 } else if (isChecked) { | 441 } else if (isChecked) { |
426 _warning("Missing media feature in media expression", _makeSpan(start)); | 442 _warning("Missing media feature in media expression", _makeSpan(start)); |
427 return null; | 443 return null; |
428 } | 444 } |
429 } | 445 } |
430 } | 446 } |
431 | 447 |
432 // Directive grammar: | 448 /** |
433 // | 449 * Directive grammar: |
434 // import: '@import' [string | URI] media_list? | 450 * |
435 // media: '@media' media_query_list '{' ruleset '}' | 451 * import: '@import' [string | URI] media_list? |
436 // page: '@page' [':' IDENT]? '{' declarations '}' | 452 * media: '@media' media_query_list '{' ruleset '}' |
437 // stylet: '@stylet' IDENT '{' ruleset '}' | 453 * page: '@page' [':' IDENT]? '{' declarations '}' |
438 // media_query_list: IDENT [',' IDENT] | 454 * stylet: '@stylet' IDENT '{' ruleset '}' |
439 // keyframes: '@-webkit-keyframes ...' (see grammar below). | 455 * media_query_list: IDENT [',' IDENT] |
440 // font_face: '@font-face' '{' declarations '}' | 456 * keyframes: '@-webkit-keyframes ...' (see grammar below). |
441 // namespace: '@namespace name url("xmlns") | 457 * font_face: '@font-face' '{' declarations '}' |
442 // host: '@host '{' ruleset '}' | 458 * namespace: '@namespace name url("xmlns") |
| 459 * host: '@host '{' ruleset '}' |
| 460 * mixin: '@mixin name [(args,...)] '{' declarations/ruleset '}' |
| 461 * include: '@include name [(@arg,@arg1)] |
| 462 * '@include name [(@arg...)] |
| 463 * content '@content' |
| 464 */ |
443 processDirective() { | 465 processDirective() { |
444 int start = _peekToken.start; | 466 int start = _peekToken.start; |
445 | 467 |
446 var tokId = _peek(); | 468 var tokId = processVariableOrDirective(); |
447 // Handle case for @ directive (where there's a whitespace between the @ | 469 if (tokId is VarDefinitionDirective) return tokId; |
448 // sign and the directive name. Technically, it's not valid grammar but | |
449 // a number of CSS tests test for whitespace between @ and name. | |
450 if (tokId == TokenKind.AT) { | |
451 Token tok = _next(); | |
452 tokId = _peek(); | |
453 if (_peekIdentifier()) { | |
454 // Is it a directive? | |
455 var directive = _peekToken.text; | |
456 var directiveLen = directive.length; | |
457 tokId = TokenKind.matchDirectives(directive, 0, directiveLen); | |
458 if (tokId == -1) { | |
459 tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen); | |
460 } | |
461 } | |
462 | |
463 if (tokId == -1) { | |
464 if (messages.options.lessSupport) { | |
465 // Less compatibility: | |
466 // @name: value; => var-name: value; (VarDefinition) | |
467 // property: @name; => property: var(name); (VarUsage) | |
468 var name; | |
469 if (_peekIdentifier()) { | |
470 name = identifier(); | |
471 } | |
472 | |
473 _eat(TokenKind.COLON); | |
474 | |
475 Expressions exprs = processExpr(); | |
476 | |
477 var span = _makeSpan(start); | |
478 return new VarDefinitionDirective( | |
479 new VarDefinition(name, exprs, span), span); | |
480 } else if (isChecked) { | |
481 _error('unexpected directive @$_peekToken', _peekToken.span); | |
482 } | |
483 } | |
484 } | |
485 | |
486 switch (tokId) { | 470 switch (tokId) { |
487 case TokenKind.DIRECTIVE_IMPORT: | 471 case TokenKind.DIRECTIVE_IMPORT: |
488 _next(); | 472 _next(); |
489 | 473 |
490 // @import "uri_string" or @import url("uri_string") are identical; only | 474 // @import "uri_string" or @import url("uri_string") are identical; only |
491 // a url can follow an @import. | 475 // a url can follow an @import. |
492 String importStr; | 476 String importStr; |
493 if (_peekIdentifier()) { | 477 if (_peekIdentifier()) { |
494 var func = processFunction(identifier()); | 478 var func = processFunction(identifier()); |
495 if (func is UriTerm) { | 479 if (func is UriTerm) { |
(...skipping 245 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
741 namespaceUri = func.text; | 725 namespaceUri = func.text; |
742 prefix = null; | 726 prefix = null; |
743 } | 727 } |
744 } else { | 728 } else { |
745 namespaceUri = processQuotedString(false); | 729 namespaceUri = processQuotedString(false); |
746 } | 730 } |
747 } | 731 } |
748 | 732 |
749 return new NamespaceDirective(prefix != null ? prefix.name : '', | 733 return new NamespaceDirective(prefix != null ? prefix.name : '', |
750 namespaceUri, _makeSpan(start)); | 734 namespaceUri, _makeSpan(start)); |
751 } | 735 |
| 736 case TokenKind.DIRECTIVE_MIXIN: |
| 737 return processMixin(start); |
| 738 |
| 739 case TokenKind.DIRECTIVE_INCLUDE: |
| 740 return processInclude( _makeSpan(start)); |
| 741 |
| 742 case TokenKind.DIRECTIVE_CONTENT: |
| 743 // TODO(terry): TBD |
| 744 _warning("@content not implemented.", _makeSpan(start)); |
| 745 return; |
| 746 } |
| 747 } |
| 748 |
| 749 /** |
| 750 * Parse the mixin beginning token offset [start]. Returns a [MixinDefinition] |
| 751 * node. |
| 752 * |
| 753 * Mixin grammar: |
| 754 * |
| 755 * @mixin IDENT [(args,...)] '{' |
| 756 * [ruleset | property | directive]* |
| 757 * '}' |
| 758 */ |
| 759 MixinDefinition processMixin(int start) { |
| 760 _next(); |
| 761 |
| 762 var name = identifier(); |
| 763 |
| 764 List<VarDefinitionDirective> params = []; |
| 765 // Any parameters? |
| 766 if (_maybeEat(TokenKind.LPAREN)) { |
| 767 var mustHaveParam = false; |
| 768 var keepGoing = true; |
| 769 while (keepGoing) { |
| 770 var varDef = processVariableOrDirective(mixinParameter: true); |
| 771 if (varDef is VarDefinitionDirective || varDef is VarDefinition) { |
| 772 params.add(varDef); |
| 773 } else if (mustHaveParam) { |
| 774 _warning("Expecting parameter", _makeSpan(_peekToken.start)); |
| 775 keepGoing = false; |
| 776 } |
| 777 if (_maybeEat(TokenKind.COMMA)) { |
| 778 mustHaveParam = true; |
| 779 continue; |
| 780 } |
| 781 keepGoing = !_maybeEat(TokenKind.RPAREN); |
| 782 } |
| 783 } |
| 784 |
| 785 _eat(TokenKind.LBRACE); |
| 786 |
| 787 List<TreeNode> productions = []; |
| 788 List<TreeNode> declarations = []; |
| 789 var mixinDirective; |
| 790 |
| 791 start = _peekToken.start; |
| 792 while (!_maybeEat(TokenKind.END_OF_FILE)) { |
| 793 var directive = processDirective(); |
| 794 if (directive != null) { |
| 795 productions.add(directive); |
| 796 continue; |
| 797 } |
| 798 |
| 799 var declGroup = processDeclarations(checkBrace: false); |
| 800 var decls = []; |
| 801 if (declGroup.declarations.any((decl) { |
| 802 return decl is Declaration && |
| 803 decl is! IncludeMixinAtDeclaration; |
| 804 })) { |
| 805 var newDecls = []; |
| 806 productions.forEach((include) { |
| 807 // If declGroup has items that are declarations then we assume |
| 808 // this mixin is a declaration mixin not a top-level mixin. |
| 809 if (include is IncludeDirective) { |
| 810 newDecls.add(new IncludeMixinAtDeclaration(include, |
| 811 include.span)); |
| 812 } else { |
| 813 _warning("Error mixing of top-level vs declarations mixins", |
| 814 _makeSpan(include)); |
| 815 } |
| 816 }); |
| 817 declGroup.declarations.insertAll(0, newDecls); |
| 818 productions = []; |
| 819 } else { |
| 820 // Declarations are just @includes make it a list of productions |
| 821 // not a declaration group (anything else is a ruleset). Make it a |
| 822 // list of productions, not a declaration group. |
| 823 for (var decl in declGroup.declarations) { |
| 824 productions.add(decl is IncludeMixinAtDeclaration ? |
| 825 decl.include : decl); |
| 826 }; |
| 827 declGroup.declarations.clear(); |
| 828 } |
| 829 |
| 830 if (declGroup.declarations.isNotEmpty) { |
| 831 if (productions.isEmpty) { |
| 832 mixinDirective = new MixinDeclarationDirective(name.name, params, |
| 833 false, declGroup, _makeSpan(start)); |
| 834 break; |
| 835 } else { |
| 836 for (var decl in declGroup.declarations) { |
| 837 productions.add(decl is IncludeMixinAtDeclaration ? |
| 838 decl.include : decl); |
| 839 } |
| 840 } |
| 841 } else { |
| 842 mixinDirective = new MixinRulesetDirective(name.name, params, |
| 843 false, productions, _makeSpan(start)); |
| 844 break; |
| 845 } |
| 846 } |
| 847 |
| 848 if (productions.isNotEmpty) { |
| 849 mixinDirective = new MixinRulesetDirective(name.name, params, |
| 850 false, productions, _makeSpan(start)); |
| 851 } |
| 852 |
| 853 _eat(TokenKind.RBRACE); |
| 854 |
| 855 return mixinDirective; |
| 856 } |
| 857 |
| 858 /** |
| 859 * Returns a VarDefinitionDirective or VarDefinition if a varaible otherwise |
| 860 * return the token id of a directive or -1 if neither. |
| 861 */ |
| 862 processVariableOrDirective({bool mixinParameter: false}) { |
| 863 int start = _peekToken.start; |
| 864 |
| 865 var tokId = _peek(); |
| 866 // Handle case for @ directive (where there's a whitespace between the @ |
| 867 // sign and the directive name. Technically, it's not valid grammar but |
| 868 // a number of CSS tests test for whitespace between @ and name. |
| 869 if (tokId == TokenKind.AT) { |
| 870 Token tok = _next(); |
| 871 tokId = _peek(); |
| 872 if (_peekIdentifier()) { |
| 873 // Is it a directive? |
| 874 var directive = _peekToken.text; |
| 875 var directiveLen = directive.length; |
| 876 tokId = TokenKind.matchDirectives(directive, 0, directiveLen); |
| 877 if (tokId == -1) { |
| 878 tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen); |
| 879 } |
| 880 } |
| 881 |
| 882 if (tokId == -1) { |
| 883 if (messages.options.lessSupport) { |
| 884 // Less compatibility: |
| 885 // @name: value; => var-name: value; (VarDefinition) |
| 886 // property: @name; => property: var(name); (VarUsage) |
| 887 var name; |
| 888 if (_peekIdentifier()) { |
| 889 name = identifier(); |
| 890 } |
| 891 |
| 892 Expressions exprs; |
| 893 if (mixinParameter && _maybeEat(TokenKind.COLON)) { |
| 894 exprs = processExpr(); |
| 895 } else if (!mixinParameter) { |
| 896 _eat(TokenKind.COLON); |
| 897 exprs = processExpr(); |
| 898 } |
| 899 |
| 900 var span = _makeSpan(start); |
| 901 return new VarDefinitionDirective( |
| 902 new VarDefinition(name, exprs, span), span); |
| 903 } else if (isChecked) { |
| 904 _error('unexpected directive @$_peekToken', _peekToken.span); |
| 905 } |
| 906 } |
| 907 } else if (mixinParameter && _peekToken.kind == TokenKind.VAR_DEFINITION) { |
| 908 _next(); |
| 909 var definedName; |
| 910 if (_peekIdentifier()) definedName = identifier(); |
| 911 |
| 912 Expressions exprs; |
| 913 if (_maybeEat(TokenKind.COLON)) { |
| 914 exprs = processExpr(); |
| 915 } |
| 916 |
| 917 return new VarDefinition(definedName, exprs, _makeSpan(start)); |
| 918 } |
| 919 |
| 920 return tokId; |
| 921 } |
| 922 |
| 923 IncludeDirective processInclude(Span span, {bool eatSemiColon: true}) { |
| 924 /* Stylet grammar: |
| 925 * |
| 926 * @include IDENT [(args,...)]; |
| 927 */ |
| 928 _next(); |
| 929 |
| 930 var name; |
| 931 if (_peekIdentifier()) { |
| 932 name = identifier(); |
| 933 } |
| 934 |
| 935 var params = []; |
| 936 |
| 937 // Any parameters? Parameters can be multiple terms per argument e.g., |
| 938 // 3px solid yellow, green is two parameters: |
| 939 // 1. 3px solid yellow |
| 940 // 2. green |
| 941 // the first has 3 terms and the second has 1 term. |
| 942 if (_maybeEat(TokenKind.LPAREN)) { |
| 943 var terms = []; |
| 944 var expr; |
| 945 var keepGoing = true; |
| 946 while (keepGoing && (expr = processTerm()) != null) { |
| 947 // VarUsage is returns as a list |
| 948 terms.add(expr is List ? expr[0] : expr); |
| 949 keepGoing = !_peekKind(TokenKind.RPAREN); |
| 950 if (keepGoing) { |
| 951 if (_maybeEat(TokenKind.COMMA)) { |
| 952 params.add(terms); |
| 953 terms = []; |
| 954 } |
| 955 } |
| 956 } |
| 957 params.add(terms); |
| 958 _maybeEat(TokenKind.RPAREN); |
| 959 } |
| 960 |
| 961 if (eatSemiColon) { |
| 962 _eat(TokenKind.SEMICOLON); |
| 963 } |
| 964 |
| 965 return new IncludeDirective(name.name, params, span); |
752 } | 966 } |
753 | 967 |
754 RuleSet processRuleSet([SelectorGroup selectorGroup]) { | 968 RuleSet processRuleSet([SelectorGroup selectorGroup]) { |
755 if (selectorGroup == null) { | 969 if (selectorGroup == null) { |
756 selectorGroup = processSelectorGroup(); | 970 selectorGroup = processSelectorGroup(); |
757 } | 971 } |
758 if (selectorGroup != null) { | 972 if (selectorGroup != null) { |
759 return new RuleSet(selectorGroup, processDeclarations(), | 973 return new RuleSet(selectorGroup, processDeclarations(), |
760 selectorGroup.span); | 974 selectorGroup.span); |
761 } | 975 } |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
802 messages = oldMessages; | 1016 messages = oldMessages; |
803 return null; | 1017 return null; |
804 } else { | 1018 } else { |
805 // Remember any messages from look ahead. | 1019 // Remember any messages from look ahead. |
806 oldMessages.mergeMessages(messages); | 1020 oldMessages.mergeMessages(messages); |
807 messages = oldMessages; | 1021 messages = oldMessages; |
808 return selGroup; | 1022 return selGroup; |
809 } | 1023 } |
810 } | 1024 } |
811 | 1025 |
812 processDeclarations({bool checkBrace: true}) { | 1026 DeclarationGroup processDeclarations({bool checkBrace: true}) { |
813 int start = _peekToken.start; | 1027 int start = _peekToken.start; |
814 | 1028 |
815 if (checkBrace) _eat(TokenKind.LBRACE); | 1029 if (checkBrace) _eat(TokenKind.LBRACE); |
816 | 1030 |
817 List decls = []; | 1031 List decls = []; |
818 List dartStyles = []; // List of latest styles exposed to Dart. | 1032 List dartStyles = []; // List of latest styles exposed to Dart. |
819 | 1033 |
820 do { | 1034 do { |
821 var selectorGroup = _nestedSelector(); | 1035 var selectorGroup = _nestedSelector(); |
822 while (selectorGroup != null) { | 1036 while (selectorGroup != null) { |
(...skipping 335 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1158 hasWhiteSpace = true; | 1372 hasWhiteSpace = true; |
1159 } | 1373 } |
1160 var id = identifier(); | 1374 var id = identifier(); |
1161 if (hasWhiteSpace) { | 1375 if (hasWhiteSpace) { |
1162 // Generate bad selector class (normalized). | 1376 // Generate bad selector class (normalized). |
1163 id.name = " ${id.name}"; | 1377 id.name = " ${id.name}"; |
1164 } | 1378 } |
1165 return new ClassSelector(id, _makeSpan(start)); | 1379 return new ClassSelector(id, _makeSpan(start)); |
1166 case TokenKind.COLON: | 1380 case TokenKind.COLON: |
1167 // :pseudo-class ::pseudo-element | 1381 // :pseudo-class ::pseudo-element |
1168 // TODO(terry): '::' should be token. | 1382 return processPseudoSelector(start); |
1169 _eat(TokenKind.COLON); | |
1170 bool pseudoElement = _maybeEat(TokenKind.COLON); | |
1171 | |
1172 // TODO(terry): If no identifier specified consider optimizing out the | |
1173 // : or :: and making this a normal selector. For now, | |
1174 // create an empty pseudoName. | |
1175 var pseudoName; | |
1176 if (_peekIdentifier()) { | |
1177 pseudoName = identifier(); | |
1178 } else { | |
1179 return null; | |
1180 } | |
1181 | |
1182 // Functional pseudo? | |
1183 if (_maybeEat(TokenKind.LPAREN)) { | |
1184 if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') { | |
1185 // Negation : ':NOT(' S* negation_arg S* ')' | |
1186 var negArg = simpleSelector(); | |
1187 | |
1188 _eat(TokenKind.RPAREN); | |
1189 return new NegationSelector(negArg, _makeSpan(start)); | |
1190 } else { | |
1191 // Handle function expression. | |
1192 var span = _makeSpan(start); | |
1193 var expr = processSelectorExpression(); | |
1194 | |
1195 // Used during selector look-a-head if not a SelectorExpression is | |
1196 // bad. | |
1197 if (expr is! SelectorExpression) { | |
1198 _errorExpected("CSS expression"); | |
1199 return null; | |
1200 } | |
1201 | |
1202 _eat(TokenKind.RPAREN); | |
1203 return (pseudoElement) ? | |
1204 new PseudoElementFunctionSelector(pseudoName, expr, span) : | |
1205 new PseudoClassFunctionSelector(pseudoName, expr, span); | |
1206 } | |
1207 } | |
1208 | |
1209 // TODO(terry): Need to handle specific pseudo class/element name and | |
1210 // backward compatible names that are : as well as :: as well as | |
1211 // parameters. Current, spec uses :: for pseudo-element and : for | |
1212 // pseudo-class. However, CSS2.1 allows for : to specify old | |
1213 // pseudo-elements (:first-line, :first-letter, :before and :after) any | |
1214 // new pseudo-elements defined would require a ::. | |
1215 return pseudoElement ? | |
1216 new PseudoElementSelector(pseudoName, _makeSpan(start)) : | |
1217 new PseudoClassSelector(pseudoName, _makeSpan(start)); | |
1218 case TokenKind.LBRACK: | 1383 case TokenKind.LBRACK: |
1219 return processAttribute(); | 1384 return processAttribute(); |
1220 case TokenKind.DOUBLE: | 1385 case TokenKind.DOUBLE: |
1221 _error('name must start with a alpha character, but found a number', | 1386 _error('name must start with a alpha character, but found a number', |
1222 _peekToken.span); | 1387 _peekToken.span); |
1223 _next(); | 1388 _next(); |
1224 break; | 1389 break; |
1225 } | 1390 } |
1226 } | 1391 } |
1227 | 1392 |
| 1393 processPseudoSelector(int start) { |
| 1394 // :pseudo-class ::pseudo-element |
| 1395 // TODO(terry): '::' should be token. |
| 1396 _eat(TokenKind.COLON); |
| 1397 bool pseudoElement = _maybeEat(TokenKind.COLON); |
| 1398 |
| 1399 // TODO(terry): If no identifier specified consider optimizing out the |
| 1400 // : or :: and making this a normal selector. For now, |
| 1401 // create an empty pseudoName. |
| 1402 var pseudoName; |
| 1403 if (_peekIdentifier()) { |
| 1404 pseudoName = identifier(); |
| 1405 } else { |
| 1406 return null; |
| 1407 } |
| 1408 |
| 1409 // Functional pseudo? |
| 1410 if (_maybeEat(TokenKind.LPAREN)) { |
| 1411 if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') { |
| 1412 // Negation : ':NOT(' S* negation_arg S* ')' |
| 1413 var negArg = simpleSelector(); |
| 1414 |
| 1415 _eat(TokenKind.RPAREN); |
| 1416 return new NegationSelector(negArg, _makeSpan(start)); |
| 1417 } else { |
| 1418 // Handle function expression. |
| 1419 var span = _makeSpan(start); |
| 1420 var expr = processSelectorExpression(); |
| 1421 |
| 1422 // Used during selector look-a-head if not a SelectorExpression is |
| 1423 // bad. |
| 1424 if (expr is! SelectorExpression) { |
| 1425 _errorExpected("CSS expression"); |
| 1426 return null; |
| 1427 } |
| 1428 |
| 1429 _eat(TokenKind.RPAREN); |
| 1430 return (pseudoElement) ? |
| 1431 new PseudoElementFunctionSelector(pseudoName, expr, span) : |
| 1432 new PseudoClassFunctionSelector(pseudoName, expr, span); |
| 1433 } |
| 1434 } |
| 1435 |
| 1436 // TODO(terry): Need to handle specific pseudo class/element name and |
| 1437 // backward compatible names that are : as well as :: as well as |
| 1438 // parameters. Current, spec uses :: for pseudo-element and : for |
| 1439 // pseudo-class. However, CSS2.1 allows for : to specify old |
| 1440 // pseudo-elements (:first-line, :first-letter, :before and :after) any |
| 1441 // new pseudo-elements defined would require a ::. |
| 1442 return pseudoElement ? |
| 1443 new PseudoElementSelector(pseudoName, _makeSpan(start)) : |
| 1444 new PseudoClassSelector(pseudoName, _makeSpan(start)); |
| 1445 } |
| 1446 |
1228 /** | 1447 /** |
1229 * In CSS3, the expressions are identifiers, strings, or of the form "an+b". | 1448 * In CSS3, the expressions are identifiers, strings, or of the form "an+b". |
1230 * | 1449 * |
1231 * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ | 1450 * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ |
1232 * | 1451 * |
1233 * num [0-9]+|[0-9]*\.[0-9]+ | 1452 * num [0-9]+|[0-9]*\.[0-9]+ |
1234 * PLUS '+' | 1453 * PLUS '+' |
1235 * DIMENSION {num}{ident} | 1454 * DIMENSION {num}{ident} |
1236 * NUMBER {num} | 1455 * NUMBER {num} |
1237 */ | 1456 */ |
(...skipping 166 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1404 } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) { | 1623 } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) { |
1405 _next(); | 1624 _next(); |
1406 var definedName; | 1625 var definedName; |
1407 if (_peekIdentifier()) definedName = identifier(); | 1626 if (_peekIdentifier()) definedName = identifier(); |
1408 | 1627 |
1409 _eat(TokenKind.COLON); | 1628 _eat(TokenKind.COLON); |
1410 | 1629 |
1411 Expressions exprs = processExpr(); | 1630 Expressions exprs = processExpr(); |
1412 | 1631 |
1413 decl = new VarDefinition(definedName, exprs, _makeSpan(start)); | 1632 decl = new VarDefinition(definedName, exprs, _makeSpan(start)); |
| 1633 } else if (_peekToken.kind == TokenKind.DIRECTIVE_INCLUDE) { |
| 1634 // @include mixinName in the declaration area. |
| 1635 var span = _makeSpan(start); |
| 1636 var include = processInclude(span, eatSemiColon: false); |
| 1637 decl = new IncludeMixinAtDeclaration(include, span); |
| 1638 } else if (_peekToken.kind == TokenKind.DIRECTIVE_EXTEND) { |
| 1639 List<SimpleSelectorSequence> simpleSequences = []; |
| 1640 |
| 1641 _next(); |
| 1642 var span = _makeSpan(start); |
| 1643 var selector = simpleSelector(); |
| 1644 if (selector == null) { |
| 1645 _warning("@extends expecting simple selector name", span); |
| 1646 } else { |
| 1647 simpleSequences.add(selector); |
| 1648 } |
| 1649 if (_peekKind(TokenKind.COLON)) { |
| 1650 var pseudoSelector = processPseudoSelector(_peekToken.start); |
| 1651 if (pseudoSelector is PseudoElementSelector || |
| 1652 pseudoSelector is PseudoClassSelector) { |
| 1653 simpleSequences.add(pseudoSelector); |
| 1654 } else { |
| 1655 _warning("not a valid selector", span); |
| 1656 } |
| 1657 } |
| 1658 decl = new ExtendDeclaration(simpleSequences, span); |
1414 } | 1659 } |
1415 | 1660 |
1416 return decl; | 1661 return decl; |
1417 } | 1662 } |
1418 | 1663 |
1419 /** List of styles exposed to the Dart UI framework. */ | 1664 /** List of styles exposed to the Dart UI framework. */ |
1420 static const int _fontPartFont= 0; | 1665 static const int _fontPartFont= 0; |
1421 static const int _fontPartVariant = 1; | 1666 static const int _fontPartVariant = 1; |
1422 static const int _fontPartWeight = 2; | 1667 static const int _fontPartWeight = 2; |
1423 static const int _fontPartSize = 3; | 1668 static const int _fontPartSize = 3; |
(...skipping 360 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1784 if (value == 9) { | 2029 if (value == 9) { |
1785 op = new IE8Term(_makeSpan(ie8Start)); | 2030 op = new IE8Term(_makeSpan(ie8Start)); |
1786 } else if (isChecked) { | 2031 } else if (isChecked) { |
1787 _warning("\$value is not valid in an expression", _makeSpan(start)); | 2032 _warning("\$value is not valid in an expression", _makeSpan(start)); |
1788 } | 2033 } |
1789 } | 2034 } |
1790 break; | 2035 break; |
1791 } | 2036 } |
1792 | 2037 |
1793 if (expr != null) { | 2038 if (expr != null) { |
1794 expressions.add(expr); | 2039 if (expr is List) { |
| 2040 expr.forEach((exprItem) { |
| 2041 expressions.add(exprItem); |
| 2042 }); |
| 2043 } else { |
| 2044 expressions.add(expr); |
| 2045 } |
1795 } else { | 2046 } else { |
1796 keepGoing = false; | 2047 keepGoing = false; |
1797 } | 2048 } |
1798 | 2049 |
1799 if (op != null) { | 2050 if (op != null) { |
1800 expressions.add(op); | 2051 expressions.add(op); |
1801 if (op is IE8Term) { | 2052 if (op is IE8Term) { |
1802 keepGoing = false; | 2053 keepGoing = false; |
1803 } else { | 2054 } else { |
1804 _next(); | 2055 _next(); |
(...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1978 case TokenKind.AT: | 2229 case TokenKind.AT: |
1979 if (messages.options.lessSupport) { | 2230 if (messages.options.lessSupport) { |
1980 _next(); | 2231 _next(); |
1981 | 2232 |
1982 var expr = processExpr(); | 2233 var expr = processExpr(); |
1983 if (isChecked && expr.expressions.length > 1) { | 2234 if (isChecked && expr.expressions.length > 1) { |
1984 _error("only @name for Less syntax", _peekToken.span); | 2235 _error("only @name for Less syntax", _peekToken.span); |
1985 } | 2236 } |
1986 | 2237 |
1987 var param = expr.expressions[0]; | 2238 var param = expr.expressions[0]; |
1988 return new VarUsage(param.text, [], _makeSpan(start)); | 2239 var varUsage = new VarUsage(param.text, [], _makeSpan(start)); |
| 2240 expr.expressions[0] = varUsage; |
| 2241 return expr.expressions; |
1989 } | 2242 } |
1990 break; | 2243 break; |
1991 } | 2244 } |
1992 | 2245 |
1993 return processDimension(t, value, _makeSpan(start)); | 2246 return processDimension(t, value, _makeSpan(start)); |
1994 } | 2247 } |
1995 | 2248 |
1996 /** Process all dimension units. */ | 2249 /** Process all dimension units. */ |
1997 processDimension(Token t, var value, Span span) { | 2250 processDimension(Token t, var value, Span span) { |
1998 var term; | 2251 var term; |
(...skipping 401 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2400 | 2653 |
2401 if (replace != null && result == null) { | 2654 if (replace != null && result == null) { |
2402 result = new StringBuffer(text.substring(0, i)); | 2655 result = new StringBuffer(text.substring(0, i)); |
2403 } | 2656 } |
2404 | 2657 |
2405 if (result != null) result.write(replace != null ? replace : text[i]); | 2658 if (result != null) result.write(replace != null ? replace : text[i]); |
2406 } | 2659 } |
2407 | 2660 |
2408 return result == null ? text : result.toString(); | 2661 return result == null ? text : result.toString(); |
2409 } | 2662 } |
OLD | NEW |