OLD | NEW |
| (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 } | |
OLD | NEW |