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 12 matching lines...) Expand all Loading... |
23 | 23 |
24 /** Used for parser lookup ahead (used for nested selectors Less support). */ | 24 /** Used for parser lookup ahead (used for nested selectors Less support). */ |
25 class ParserState extends TokenizerState { | 25 class ParserState extends TokenizerState { |
26 final Token peekToken; | 26 final Token peekToken; |
27 final Token previousToken; | 27 final Token previousToken; |
28 | 28 |
29 ParserState(this.peekToken, this.previousToken, Tokenizer tokenizer) | 29 ParserState(this.peekToken, this.previousToken, Tokenizer tokenizer) |
30 : super(tokenizer); | 30 : super(tokenizer); |
31 } | 31 } |
32 | 32 |
| 33 // TODO(jmesserly): this should not be global |
33 void _createMessages({List<Message> errors, List<String> options}) { | 34 void _createMessages({List<Message> errors, List<String> options}) { |
34 if (errors == null) errors = []; | 35 if (errors == null) errors = []; |
35 | 36 |
36 if (options == null) { | 37 if (options == null) { |
37 options = ['--no-colors', 'memory']; | 38 options = ['--no-colors', 'memory']; |
38 } | 39 } |
39 var opt = PreprocessorOptions.parse(options); | 40 var opt = PreprocessorOptions.parse(options); |
40 messages = new Messages(options: opt, printHandler: errors.add); | 41 messages = new Messages(options: opt, printHandler: errors.add); |
41 } | 42 } |
42 | 43 |
43 /** CSS checked mode enabled. */ | 44 /** CSS checked mode enabled. */ |
44 bool get isChecked => messages.options.checked; | 45 bool get isChecked => messages.options.checked; |
45 | 46 |
46 // TODO(terry): Remove nested name parameter. | 47 // TODO(terry): Remove nested name parameter. |
47 /** Parse and analyze the CSS file. */ | 48 /** Parse and analyze the CSS file. */ |
48 StyleSheet compile(var input, {List<Message> errors, List<String> options, | 49 StyleSheet compile(input, {List<Message> errors, List<String> options, |
49 bool nested: true, | 50 bool nested: true, |
50 bool polyfill: false, | 51 bool polyfill: false, |
51 List<StyleSheet> includes: null}) { | 52 List<StyleSheet> includes: null}) { |
52 | 53 |
53 if (includes == null) { | 54 if (includes == null) { |
54 includes = []; | 55 includes = []; |
55 } | 56 } |
56 | 57 |
57 var source = _inputAsString(input); | 58 var source = _inputAsString(input); |
58 | 59 |
(...skipping 19 matching lines...) Expand all Loading... |
78 | 79 |
79 _createMessages(errors: errors, options: options); | 80 _createMessages(errors: errors, options: options); |
80 new Analyzer(styleSheets, messages).run(); | 81 new Analyzer(styleSheets, messages).run(); |
81 } | 82 } |
82 | 83 |
83 /** | 84 /** |
84 * Parse the [input] CSS stylesheet into a tree. The [input] can be a [String], | 85 * Parse the [input] CSS stylesheet into a tree. The [input] can be a [String], |
85 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional | 86 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional |
86 * [errors] list will contain each error/warning as a [Message]. | 87 * [errors] list will contain each error/warning as a [Message]. |
87 */ | 88 */ |
88 StyleSheet parse(var input, {List<Message> errors, List<String> options}) { | 89 StyleSheet parse(input, {List<Message> errors, List<String> options}) { |
89 var source = _inputAsString(input); | 90 var source = _inputAsString(input); |
90 | 91 |
91 _createMessages(errors: errors, options: options); | 92 _createMessages(errors: errors, options: options); |
92 | 93 |
93 var file = new SourceFile.text(null, source); | 94 var file = new SourceFile.text(null, source); |
94 | |
95 return new _Parser(file, source).parse(); | 95 return new _Parser(file, source).parse(); |
96 } | 96 } |
97 | 97 |
98 /** | 98 /** |
99 * Parse the [input] CSS selector into a tree. The [input] can be a [String], | 99 * Parse the [input] CSS selector into a tree. The [input] can be a [String], |
100 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional | 100 * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional |
101 * [errors] list will contain each error/warning as a [Message]. | 101 * [errors] list will contain each error/warning as a [Message]. |
102 */ | 102 */ |
103 StyleSheet selector(var input, {List<Message> errors}) { | 103 // TODO(jmesserly): should rename "parseSelector" and return Selector |
| 104 StyleSheet selector(input, {List<Message> errors}) { |
104 var source = _inputAsString(input); | 105 var source = _inputAsString(input); |
105 | 106 |
106 _createMessages(errors: errors); | 107 _createMessages(errors: errors); |
107 | 108 |
108 var file = new SourceFile.text(null, source); | 109 var file = new SourceFile.text(null, source); |
109 | 110 return (new _Parser(file, source) |
110 return new _Parser(file, source).parseSelector(); | 111 ..tokenizer.inSelector = true) |
| 112 .parseSelector(); |
111 } | 113 } |
112 | 114 |
113 String _inputAsString(var input) { | 115 SelectorGroup parseSelectorGroup(input, {List<Message> errors}) { |
| 116 var source = _inputAsString(input); |
| 117 |
| 118 _createMessages(errors: errors); |
| 119 |
| 120 var file = new SourceFile.text(null, source); |
| 121 return (new _Parser(file, source) |
| 122 // TODO(jmesserly): this fix should be applied to the parser. It's tricky |
| 123 // because by the time the flag is set one token has already been fetched. |
| 124 ..tokenizer.inSelector = true) |
| 125 .processSelectorGroup(); |
| 126 } |
| 127 |
| 128 String _inputAsString(input) { |
114 String source; | 129 String source; |
115 | 130 |
116 if (input is String) { | 131 if (input is String) { |
117 source = input; | 132 source = input; |
118 } else if (input is List<int>) { | 133 } else if (input is List<int>) { |
119 // TODO(terry): The parse function needs an "encoding" argument and will | 134 // TODO(terry): The parse function needs an "encoding" argument and will |
120 // default to whatever encoding CSS defaults to. | 135 // default to whatever encoding CSS defaults to. |
121 // | 136 // |
122 // Here's some info about CSS encodings: | 137 // Here's some info about CSS encodings: |
123 // http://www.w3.org/International/questions/qa-css-charset.en.php | 138 // http://www.w3.org/International/questions/qa-css-charset.en.php |
(...skipping 16 matching lines...) Expand all Loading... |
140 | 155 |
141 return source; | 156 return source; |
142 } | 157 } |
143 | 158 |
144 // TODO(terry): Consider removing this class when all usages can be eliminated | 159 // TODO(terry): Consider removing this class when all usages can be eliminated |
145 // or replaced with compile API. | 160 // or replaced with compile API. |
146 /** Public parsing interface for csslib. */ | 161 /** Public parsing interface for csslib. */ |
147 class Parser { | 162 class Parser { |
148 final _Parser _parser; | 163 final _Parser _parser; |
149 | 164 |
| 165 // TODO(jmesserly): having file and text is redundant. |
150 Parser(SourceFile file, String text, {int start: 0, String baseUrl}) : | 166 Parser(SourceFile file, String text, {int start: 0, String baseUrl}) : |
151 _parser = new _Parser(file, text, start: start, baseUrl: baseUrl); | 167 _parser = new _Parser(file, text, start: start, baseUrl: baseUrl); |
152 | 168 |
153 StyleSheet parse() => _parser.parse(); | 169 StyleSheet parse() => _parser.parse(); |
154 } | 170 } |
155 | 171 |
156 /** A simple recursive descent parser for CSS. */ | 172 /** A simple recursive descent parser for CSS. */ |
157 class _Parser { | 173 class _Parser { |
158 final Tokenizer tokenizer; | 174 final Tokenizer tokenizer; |
159 | 175 |
(...skipping 1007 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1167 if (decls.length > 0) { | 1183 if (decls.length > 0) { |
1168 groups.add(new DeclarationGroup(decls, _makeSpan(start))); | 1184 groups.add(new DeclarationGroup(decls, _makeSpan(start))); |
1169 } | 1185 } |
1170 | 1186 |
1171 return groups; | 1187 return groups; |
1172 } | 1188 } |
1173 | 1189 |
1174 SelectorGroup processSelectorGroup() { | 1190 SelectorGroup processSelectorGroup() { |
1175 List<Selector> selectors = []; | 1191 List<Selector> selectors = []; |
1176 int start = _peekToken.start; | 1192 int start = _peekToken.start; |
| 1193 |
1177 do { | 1194 do { |
1178 Selector selector = processSelector(); | 1195 Selector selector = processSelector(); |
1179 if (selector != null) { | 1196 if (selector != null) { |
1180 selectors.add(selector); | 1197 selectors.add(selector); |
1181 } | 1198 } |
1182 } while (_maybeEat(TokenKind.COMMA)); | 1199 } while (_maybeEat(TokenKind.COMMA)); |
1183 | 1200 |
1184 if (selectors.length > 0) { | 1201 if (selectors.length > 0) { |
1185 return new SelectorGroup(selectors, _makeSpan(start)); | 1202 return new SelectorGroup(selectors, _makeSpan(start)); |
1186 } | 1203 } |
(...skipping 219 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1406 // : or :: and making this a normal selector. For now, | 1423 // : or :: and making this a normal selector. For now, |
1407 // create an empty pseudoName. | 1424 // create an empty pseudoName. |
1408 var pseudoName; | 1425 var pseudoName; |
1409 if (_peekIdentifier()) { | 1426 if (_peekIdentifier()) { |
1410 pseudoName = identifier(); | 1427 pseudoName = identifier(); |
1411 } else { | 1428 } else { |
1412 return null; | 1429 return null; |
1413 } | 1430 } |
1414 | 1431 |
1415 // Functional pseudo? | 1432 // Functional pseudo? |
1416 if (_maybeEat(TokenKind.LPAREN)) { | 1433 |
| 1434 if (_peekToken.kind == TokenKind.LPAREN) { |
| 1435 |
1417 if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') { | 1436 if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') { |
| 1437 _eat(TokenKind.LPAREN); |
| 1438 |
1418 // Negation : ':NOT(' S* negation_arg S* ')' | 1439 // Negation : ':NOT(' S* negation_arg S* ')' |
1419 var negArg = simpleSelector(); | 1440 var negArg = simpleSelector(); |
1420 | 1441 |
1421 _eat(TokenKind.RPAREN); | 1442 _eat(TokenKind.RPAREN); |
1422 return new NegationSelector(negArg, _makeSpan(start)); | 1443 return new NegationSelector(negArg, _makeSpan(start)); |
1423 } else { | 1444 } else { |
| 1445 // Special parsing for expressions in pseudo functions. Minus is used |
| 1446 // as operator not identifier. |
| 1447 // TODO(jmesserly): we need to flip this before we eat the "(" as the |
| 1448 // next token will be fetched when we do that. I think we should try to |
| 1449 // refactor so we don't need this boolean; it seems fragile. |
| 1450 tokenizer.inSelectorExpression = true; |
| 1451 _eat(TokenKind.LPAREN); |
| 1452 |
1424 // Handle function expression. | 1453 // Handle function expression. |
1425 var span = _makeSpan(start); | 1454 var span = _makeSpan(start); |
1426 var expr = processSelectorExpression(); | 1455 var expr = processSelectorExpression(); |
1427 | 1456 |
| 1457 tokenizer.inSelectorExpression = false; |
| 1458 |
1428 // Used during selector look-a-head if not a SelectorExpression is | 1459 // Used during selector look-a-head if not a SelectorExpression is |
1429 // bad. | 1460 // bad. |
1430 if (expr is! SelectorExpression) { | 1461 if (expr is! SelectorExpression) { |
1431 _errorExpected("CSS expression"); | 1462 _errorExpected("CSS expression"); |
1432 return null; | 1463 return null; |
1433 } | 1464 } |
1434 | 1465 |
1435 _eat(TokenKind.RPAREN); | 1466 _eat(TokenKind.RPAREN); |
1436 return (pseudoElement) ? | 1467 return (pseudoElement) ? |
1437 new PseudoElementFunctionSelector(pseudoName, expr, span) : | 1468 new PseudoElementFunctionSelector(pseudoName, expr, span) : |
(...skipping 18 matching lines...) Expand all Loading... |
1456 * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ | 1487 * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ |
1457 * | 1488 * |
1458 * num [0-9]+|[0-9]*\.[0-9]+ | 1489 * num [0-9]+|[0-9]*\.[0-9]+ |
1459 * PLUS '+' | 1490 * PLUS '+' |
1460 * DIMENSION {num}{ident} | 1491 * DIMENSION {num}{ident} |
1461 * NUMBER {num} | 1492 * NUMBER {num} |
1462 */ | 1493 */ |
1463 processSelectorExpression() { | 1494 processSelectorExpression() { |
1464 var start = _peekToken.start; | 1495 var start = _peekToken.start; |
1465 | 1496 |
1466 var expression = new SelectorExpression(_makeSpan(start)); | 1497 var expressions = []; |
1467 | 1498 |
1468 Token termToken; | 1499 Token termToken; |
1469 var value; | 1500 var value; |
1470 | 1501 |
1471 // Special parsing for expressions in pseudo functions. Minus is used as | |
1472 // operator not identifier. | |
1473 tokenizer.selectorExpression = true; | |
1474 | |
1475 var keepParsing = true; | 1502 var keepParsing = true; |
1476 while (keepParsing) { | 1503 while (keepParsing) { |
1477 switch (_peek()) { | 1504 switch (_peek()) { |
1478 case TokenKind.PLUS: | 1505 case TokenKind.PLUS: |
1479 start = _peekToken.start; | 1506 start = _peekToken.start; |
1480 termToken = _next(); | 1507 termToken = _next(); |
1481 expression.add(new OperatorPlus(_makeSpan(start))); | 1508 expressions.add(new OperatorPlus(_makeSpan(start))); |
1482 break; | 1509 break; |
1483 case TokenKind.MINUS: | 1510 case TokenKind.MINUS: |
1484 start = _peekToken.start; | 1511 start = _peekToken.start; |
1485 termToken = _next(); | 1512 termToken = _next(); |
1486 expression.add(new OperatorMinus(_makeSpan(start))); | 1513 expressions.add(new OperatorMinus(_makeSpan(start))); |
1487 break; | 1514 break; |
1488 case TokenKind.INTEGER: | 1515 case TokenKind.INTEGER: |
1489 termToken = _next(); | 1516 termToken = _next(); |
1490 value = int.parse(termToken.text); | 1517 value = int.parse(termToken.text); |
1491 break; | 1518 break; |
1492 case TokenKind.DOUBLE: | 1519 case TokenKind.DOUBLE: |
1493 termToken = _next(); | 1520 termToken = _next(); |
1494 value = double.parse(termToken.text); | 1521 value = double.parse(termToken.text); |
1495 break; | 1522 break; |
1496 case TokenKind.SINGLE_QUOTE: | 1523 case TokenKind.SINGLE_QUOTE: |
(...skipping 13 matching lines...) Expand all Loading... |
1510 | 1537 |
1511 if (keepParsing && value != null) { | 1538 if (keepParsing && value != null) { |
1512 var unitTerm; | 1539 var unitTerm; |
1513 // Don't process the dimension if MINUS or PLUS is next. | 1540 // Don't process the dimension if MINUS or PLUS is next. |
1514 if (_peek() != TokenKind.MINUS && _peek() != TokenKind.PLUS) { | 1541 if (_peek() != TokenKind.MINUS && _peek() != TokenKind.PLUS) { |
1515 unitTerm = processDimension(termToken, value, _makeSpan(start)); | 1542 unitTerm = processDimension(termToken, value, _makeSpan(start)); |
1516 } | 1543 } |
1517 if (unitTerm == null) { | 1544 if (unitTerm == null) { |
1518 unitTerm = new LiteralTerm(value, value.name, _makeSpan(start)); | 1545 unitTerm = new LiteralTerm(value, value.name, _makeSpan(start)); |
1519 } | 1546 } |
1520 expression.add(unitTerm); | 1547 expressions.add(unitTerm); |
1521 | 1548 |
1522 value = null; | 1549 value = null; |
1523 } | 1550 } |
1524 } | 1551 } |
1525 | 1552 |
1526 tokenizer.selectorExpression = false; | 1553 return new SelectorExpression(expressions, _makeSpan(start)); |
1527 | |
1528 return expression; | |
1529 } | 1554 } |
1530 | 1555 |
1531 // Attribute grammar: | 1556 // Attribute grammar: |
1532 // | 1557 // |
1533 // attributes : | 1558 // attributes : |
1534 // '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']' | 1559 // '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']' |
1535 // | 1560 // |
1536 // ATTRIB_MATCHES : | 1561 // ATTRIB_MATCHES : |
1537 // [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ] | 1562 // [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ] |
1538 // | 1563 // |
(...skipping 797 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2336 } | 2361 } |
2337 | 2362 |
2338 return term; | 2363 return term; |
2339 } | 2364 } |
2340 | 2365 |
2341 String processQuotedString([bool urlString = false]) { | 2366 String processQuotedString([bool urlString = false]) { |
2342 var start = _peekToken.start; | 2367 var start = _peekToken.start; |
2343 | 2368 |
2344 // URI term sucks up everything inside of quotes(' or ") or between parens | 2369 // URI term sucks up everything inside of quotes(' or ") or between parens |
2345 var stopToken = urlString ? TokenKind.RPAREN : -1; | 2370 var stopToken = urlString ? TokenKind.RPAREN : -1; |
| 2371 |
| 2372 // Note: disable skipping whitespace tokens inside a string. |
| 2373 // TODO(jmesserly): the layering here feels wrong. |
| 2374 var skipWhitespace = tokenizer._skipWhitespace; |
| 2375 tokenizer._skipWhitespace = false; |
| 2376 |
2346 switch (_peek()) { | 2377 switch (_peek()) { |
2347 case TokenKind.SINGLE_QUOTE: | 2378 case TokenKind.SINGLE_QUOTE: |
2348 stopToken = TokenKind.SINGLE_QUOTE; | 2379 stopToken = TokenKind.SINGLE_QUOTE; |
2349 start = _peekToken.start + 1; // Skip the quote might have whitespace. | 2380 start = _peekToken.start + 1; // Skip the quote might have whitespace. |
2350 _next(); // Skip the SINGLE_QUOTE. | 2381 _next(); // Skip the SINGLE_QUOTE. |
2351 break; | 2382 break; |
2352 case TokenKind.DOUBLE_QUOTE: | 2383 case TokenKind.DOUBLE_QUOTE: |
2353 stopToken = TokenKind.DOUBLE_QUOTE; | 2384 stopToken = TokenKind.DOUBLE_QUOTE; |
2354 start = _peekToken.start + 1; // Skip the quote might have whitespace. | 2385 start = _peekToken.start + 1; // Skip the quote might have whitespace. |
2355 _next(); // Skip the DOUBLE_QUOTE. | 2386 _next(); // Skip the DOUBLE_QUOTE. |
2356 break; | 2387 break; |
2357 default: | 2388 default: |
2358 if (urlString) { | 2389 if (urlString) { |
2359 if (_peek() == TokenKind.LPAREN) { | 2390 if (_peek() == TokenKind.LPAREN) { |
2360 _next(); // Skip the LPAREN. | 2391 _next(); // Skip the LPAREN. |
2361 start = _peekToken.start; | 2392 start = _peekToken.start; |
2362 } | 2393 } |
2363 stopToken = TokenKind.RPAREN; | 2394 stopToken = TokenKind.RPAREN; |
2364 } else { | 2395 } else { |
2365 _error('unexpected string', _makeSpan(start)); | 2396 _error('unexpected string', _makeSpan(start)); |
2366 } | 2397 } |
2367 break; | 2398 break; |
2368 } | 2399 } |
2369 | 2400 |
2370 // Gobble up everything until we hit our stop token. | 2401 // Gobble up everything until we hit our stop token. |
2371 var runningStart = _peekToken.start; | 2402 var runningStart = _peekToken.start; |
| 2403 |
| 2404 var stringValue = new StringBuffer(); |
2372 while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) { | 2405 while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) { |
2373 var tok = _next(); | 2406 stringValue.write(_next().text); |
2374 } | 2407 } |
2375 | 2408 |
| 2409 tokenizer._skipWhitespace = skipWhitespace; |
| 2410 |
2376 // All characters between quotes is the string. | 2411 // All characters between quotes is the string. |
2377 var end = _peekToken.end; | |
2378 var stringValue = (_peekToken.span as FileSpan).file.getText(start, | |
2379 end - 1); | |
2380 | |
2381 if (stopToken != TokenKind.RPAREN) { | 2412 if (stopToken != TokenKind.RPAREN) { |
2382 _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE; | 2413 _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE; |
2383 } | 2414 } |
2384 | 2415 |
2385 return stringValue; | 2416 return stringValue.toString(); |
2386 } | 2417 } |
2387 | 2418 |
2388 // TODO(terry): Should probably understand IE's non-standard filter syntax to | 2419 // TODO(terry): Should probably understand IE's non-standard filter syntax to |
2389 // fully support calc, var(), etc. | 2420 // fully support calc, var(), etc. |
2390 /** | 2421 /** |
2391 * IE's filter property breaks CSS value parsing. IE's format can be: | 2422 * IE's filter property breaks CSS value parsing. IE's format can be: |
2392 * | 2423 * |
2393 * filter: progid:DXImageTransform.MS.gradient(Type=0, Color='#9d8b83'); | 2424 * filter: progid:DXImageTransform.MS.gradient(Type=0, Color='#9d8b83'); |
2394 * | 2425 * |
2395 * We'll just parse everything after the 'progid:' look for the left paren | 2426 * We'll just parse everything after the 'progid:' look for the left paren |
(...skipping 268 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2664 | 2695 |
2665 if (replace != null && result == null) { | 2696 if (replace != null && result == null) { |
2666 result = new StringBuffer(text.substring(0, i)); | 2697 result = new StringBuffer(text.substring(0, i)); |
2667 } | 2698 } |
2668 | 2699 |
2669 if (result != null) result.write(replace != null ? replace : text[i]); | 2700 if (result != null) result.write(replace != null ? replace : text[i]); |
2670 } | 2701 } |
2671 | 2702 |
2672 return result == null ? text : result.toString(); | 2703 return result == null ? text : result.toString(); |
2673 } | 2704 } |
OLD | NEW |