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

Unified Diff: pkg/csslib/lib/parser.dart

Issue 23168002: move csslib into dart svn (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/csslib/lib/css.dart ('k') | pkg/csslib/lib/src/analyzer.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: pkg/csslib/lib/parser.dart
diff --git a/pkg/csslib/lib/parser.dart b/pkg/csslib/lib/parser.dart
new file mode 100644
index 0000000000000000000000000000000000000000..a1cd7158da23e7846555c79815108cc46a231401
--- /dev/null
+++ b/pkg/csslib/lib/parser.dart
@@ -0,0 +1,2402 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library csslib.parser;
+
+import 'dart:math' as math;
+
+import 'package:source_maps/span.dart' show SourceFile, Span, FileSpan;
+
+import "visitor.dart";
+import 'src/messages.dart';
+import 'src/options.dart';
+
+part 'src/analyzer.dart';
+part 'src/property.dart';
+part 'src/token.dart';
+part 'src/tokenizer_base.dart';
+part 'src/tokenizer.dart';
+part 'src/tokenkind.dart';
+
+
+/** Used for parser lookup ahead (used for nested selectors Less support). */
+class ParserState extends TokenizerState {
+ final Token peekToken;
+ final Token previousToken;
+
+ ParserState(this.peekToken, this.previousToken, Tokenizer tokenizer)
+ : super(tokenizer);
+}
+
+void _createMessages({List errors, List options}) {
+ if (errors == null) errors = [];
+
+ if (options == null) {
+ options = ['--no-colors', 'memory'];
+ }
+ var opt = PreprocessorOptions.parse(options);
+ messages = new Messages(options: opt, printHandler: errors.add);
+}
+
+/** CSS checked mode enabled. */
+bool get isChecked => messages.options.checked;
+
+// TODO(terry): Remove nested name parameter.
+/** Parse and analyze the CSS file. */
+StyleSheet compile(var input, {List errors, List options, bool nested: true}) {
+ var source = _inputAsString(input);
+
+ _createMessages(errors: errors, options: options);
+
+ var file = new SourceFile.text(null, source);
+
+ var tree = new Parser(file, source).parse();
+
+ analyze([tree], errors: errors, options: options);
+
+ return tree;
+}
+
+/** Analyze the CSS file. */
+void analyze(List<StyleSheet> styleSheets, {List errors, List options}) {
+ _createMessages(errors: errors, options: options);
+ new Analyzer(styleSheets, messages).run();
+}
+
+/**
+ * Parse the [input] CSS stylesheet into a tree. The [input] can be a [String],
+ * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional
+ * [errors] list will contain each error/warning as a [Message].
+ */
+StyleSheet parse(var input, {List errors, List options}) {
+ var source = _inputAsString(input);
+
+ _createMessages(errors: errors, options: options);
+
+ var file = new SourceFile.text(null, source);
+
+ return new Parser(file, source).parse();
+}
+
+/**
+ * Parse the [input] CSS selector into a tree. The [input] can be a [String],
+ * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional
+ * [errors] list will contain each error/warning as a [Message].
+ */
+StyleSheet selector(var input, {List errors}) {
+ var source = _inputAsString(input);
+
+ _createMessages(errors: errors);
+
+ var file = new SourceFile.text(null, source);
+
+ return new Parser(file, source).parseSelector();
+}
+
+String _inputAsString(var input) {
+ String source;
+
+ if (input is String) {
+ source = input;
+ } else if (input is List<int>) {
+ // TODO(terry): The parse function needs an "encoding" argument and will
+ // default to whatever encoding CSS defaults to.
+ //
+ // Here's some info about CSS encodings:
+ // http://www.w3.org/International/questions/qa-css-charset.en.php
+ //
+ // As JMesserly suggests it will probably need a "preparser" html5lib
+ // (encoding_parser.dart) that interprets the bytes as ASCII and scans for
+ // @charset. But for now an "encoding" argument would work. Often the
+ // HTTP header will indicate the correct encoding.
+ //
+ // See encoding helpers at: package:html5lib/lib/src/char_encodings.dart
+ // These helpers can decode in different formats given an encoding name
+ // (mostly unicode, ascii, windows-1252 which is html5 default encoding).
+ source = new String.fromCharCodes(input);
+ } else {
+ // TODO(terry): Support RandomAccessFile using console.
+ throw new ArgumentError("'source' must be a String or "
+ "List<int> (of bytes). RandomAccessFile not supported from this "
+ "simple interface");
+ }
+
+ return source;
+}
+
+/** A simple recursive descent parser for CSS. */
+class Parser {
+ Tokenizer tokenizer;
+
+ /** Base url of CSS file. */
+ final String _baseUrl;
+
+ /**
+ * File containing the source being parsed, used to report errors with
+ * source-span locations.
+ */
+ final SourceFile file;
+
+ Token _previousToken;
+ Token _peekToken;
+
+ Parser(SourceFile file, String text, {int start: 0, String baseUrl})
+ : this.file = file,
+ _baseUrl = baseUrl,
+ tokenizer = new Tokenizer(file, text, true, start) {
+ _peekToken = tokenizer.next();
+ }
+
+ /** Main entry point for parsing an entire CSS file. */
+ StyleSheet parse() {
+ List<TreeNode> productions = [];
+
+ int start = _peekToken.start;
+ while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) {
+ // TODO(terry): Need to handle charset.
+ var directive = processDirective();
+ if (directive != null) {
+ productions.add(directive);
+ _maybeEat(TokenKind.SEMICOLON);
+ } else {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset != null) {
+ productions.add(ruleset);
+ } else {
+ break;
+ }
+ }
+ }
+
+ checkEndOfFile();
+
+ return new StyleSheet(productions, _makeSpan(start));
+ }
+
+ /** Main entry point for parsing a simple selector sequence. */
+ StyleSheet parseSelector() {
+ List<TreeNode> productions = [];
+
+ int start = _peekToken.start;
+ while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) {
+ var selector = processSelector();
+ if (selector != null) {
+ productions.add(selector);
+ }
+ }
+
+ checkEndOfFile();
+
+ return new StyleSheet.selector(productions, _makeSpan(start));
+ }
+
+ /** Generate an error if [file] has not been completely consumed. */
+ void checkEndOfFile() {
+ if (!(_peekKind(TokenKind.END_OF_FILE) ||
+ _peekKind(TokenKind.INCOMPLETE_COMMENT))) {
+ _error('premature end of file unknown CSS', _peekToken.span);
+ }
+ }
+
+ /** Guard to break out of parser when an unexpected end of file is found. */
+ // TODO(jimhug): Failure to call this method can lead to inifinite parser
+ // loops. Consider embracing exceptions for more errors to reduce
+ // the danger here.
+ bool isPrematureEndOfFile() {
+ if (_maybeEat(TokenKind.END_OF_FILE)) {
+ _error('unexpected end of file', _peekToken.span);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ // Basic support methods
+ ///////////////////////////////////////////////////////////////////
+ int _peek() {
+ return _peekToken.kind;
+ }
+
+ Token _next({unicodeRange : false}) {
+ _previousToken = _peekToken;
+ _peekToken = tokenizer.next(unicodeRange: unicodeRange);
+ return _previousToken;
+ }
+
+ bool _peekKind(int kind) {
+ return _peekToken.kind == kind;
+ }
+
+ /* Is the next token a legal identifier? This includes pseudo-keywords. */
+ bool _peekIdentifier() {
+ return TokenKind.isIdentifier(_peekToken.kind);
+ }
+
+ /** Marks the parser/tokenizer look ahead to support Less nested selectors. */
+ ParserState get _mark =>
+ new ParserState(_peekToken, _previousToken, tokenizer);
+
+ /** Restores the parser/tokenizer state to state remembered by _mark. */
+ void _restore(ParserState markedData) {
+ tokenizer.restore(markedData);
+ _peekToken = markedData.peekToken;
+ _previousToken = markedData.previousToken;
+ }
+
+ bool _maybeEat(int kind, {unicodeRange : false}) {
+ if (_peekToken.kind == kind) {
+ _previousToken = _peekToken;
+ _peekToken = tokenizer.next(unicodeRange: unicodeRange);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void _eat(int kind, {unicodeRange : false}) {
+ if (!_maybeEat(kind, unicodeRange: unicodeRange)) {
+ _errorExpected(TokenKind.kindToString(kind));
+ }
+ }
+
+ void _eatSemicolon() {
+ _eat(TokenKind.SEMICOLON);
+ }
+
+ void _errorExpected(String expected) {
+ var tok = _next();
+ var message;
+ try {
+ message = 'expected $expected, but found $tok';
+ } catch (e) {
+ message = 'parsing error expected $expected';
+ }
+ _error(message, tok.span);
+ }
+
+ void _error(String message, Span location) {
+ if (location == null) {
+ location = _peekToken.span;
+ }
+ messages.error(message, location);
+ }
+
+ void _warning(String message, Span location) {
+ if (location == null) {
+ location = _peekToken.span;
+ }
+ messages.warning(message, location);
+ }
+
+ Span _makeSpan(int start) {
+ // TODO(terry): there are places where we are creating spans before we eat
+ // the tokens, so using _previousToken.end is not always valid.
+ var end = _previousToken != null && _previousToken.end >= start
+ ? _previousToken.end : _peekToken.end;
+ return file.span(start, end);
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ // Top level productions
+ ///////////////////////////////////////////////////////////////////
+
+ /**
+ * The media_query_list production below replaces the media_list production
+ * from CSS2 the new grammar is:
+ *
+ * media_query_list
+ * : S* [media_query [ ',' S* media_query ]* ]?
+ * media_query
+ * : [ONLY | NOT]? S* media_type S* [ AND S* expression ]*
+ * | expression [ AND S* expression ]*
+ * media_type
+ * : IDENT
+ * expression
+ * : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
+ * media_feature
+ * : IDENT
+ */
+ List<MediaQuery> processMediaQueryList() {
+ var mediaQueries = [];
+
+ bool firstTime = true;
+ var mediaQuery;
+ do {
+ mediaQuery = processMediaQuery(firstTime == true);
+ if (mediaQuery != null) {
+ mediaQueries.add(mediaQuery);
+ firstTime = false;
+ continue;
+ }
+
+ // Any more more media types separated by comma.
+ if (!_maybeEat(TokenKind.COMMA)) break;
+
+ // Yep more media types start again.
+ firstTime = true;
+ } while ((!firstTime && mediaQuery != null) || firstTime);
+
+ return mediaQueries;
+ }
+
+ MediaQuery processMediaQuery([bool startQuery = true]) {
+ // Grammar: [ONLY | NOT]? S* media_type S*
+ // [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]*
+
+ int start = _peekToken.start;
+
+ // Is it a unary media operator?
+ var op = _peekToken.text;
+ var opLen = op.length;
+ var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen);
+ if (unaryOp != -1) {
+ if (isChecked) {
+ if (startQuery &&
+ unaryOp != TokenKind.MEDIA_OP_NOT ||
+ unaryOp != TokenKind.MEDIA_OP_ONLY) {
+ _warning("Only the unary operators NOT and ONLY allowed",
+ _makeSpan(start));
+ }
+ if (!startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
+ _warning("Only the binary AND operator allowed", _makeSpan(start));
+ }
+ }
+ _next();
+ start = _peekToken.start;
+ }
+
+ var type;
+ if (startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
+ // Get the media type.
+ if (_peekIdentifier()) type = identifier();
+ }
+
+ var exprs = [];
+
+ if (unaryOp == -1 || unaryOp == TokenKind.MEDIA_OP_AND) {
+ var andOp = false;
+ while (true) {
+ var expr = processMediaExpression(andOp);
+ if (expr == null) break;
+
+ exprs.add(expr);
+ op = _peekToken.text;
+ opLen = op.length;
+ andOp = TokenKind.matchMediaOperator(op, 0, opLen) ==
+ TokenKind.MEDIA_OP_AND;
+ if (!andOp) break;
+ _next();
+ }
+ }
+
+ if (unaryOp != -1 || type != null || exprs.length > 0) {
+ return new MediaQuery(unaryOp, type, exprs, _makeSpan(start));
+ }
+ }
+
+ MediaExpression processMediaExpression([bool andOperator = false]) {
+ int start = _peekToken.start;
+
+ // Grammar: '(' S* media_feature S* [ ':' S* expr ]? ')' S*
+ if (_maybeEat(TokenKind.LPAREN)) {
+ if (_peekIdentifier()) {
+ var feature = identifier(); // Media feature.
+ while (_maybeEat(TokenKind.COLON)) {
+ int startExpr = _peekToken.start;
+ var exprs = processExpr();
+ if (_maybeEat(TokenKind.RPAREN)) {
+ return new MediaExpression(andOperator, feature, exprs,
+ _makeSpan(startExpr));
+ } else if (isChecked) {
+ _warning("Missing parenthesis around media expression",
+ _makeSpan(start));
+ return null;
+ }
+ }
+ } else if (isChecked) {
+ _warning("Missing media feature in media expression", _makeSpan(start));
+ return null;
+ }
+ }
+ }
+
+ // Directive grammar:
+ //
+ // import: '@import' [string | URI] media_list?
+ // media: '@media' media_query_list '{' ruleset '}'
+ // page: '@page' [':' IDENT]? '{' declarations '}'
+ // stylet: '@stylet' IDENT '{' ruleset '}'
+ // media_query_list: IDENT [',' IDENT]
+ // keyframes: '@-webkit-keyframes ...' (see grammar below).
+ // font_face: '@font-face' '{' declarations '}'
+ // namespace: '@namespace name url("xmlns")
+ // host: '@host '{' ruleset '}'
+ processDirective() {
+ int start = _peekToken.start;
+
+ var tokId = _peek();
+ // Handle case for @ directive (where there's a whitespace between the @
+ // sign and the directive name. Technically, it's not valid grammar but
+ // a number of CSS tests test for whitespace between @ and name.
+ if (tokId == TokenKind.AT) {
+ Token tok = _next();
+ tokId = _peek();
+ if (_peekIdentifier()) {
+ // Is it a directive?
+ var directive = _peekToken.text;
+ var directiveLen = directive.length;
+ tokId = TokenKind.matchDirectives(directive, 0, directiveLen);
+ if (tokId == -1) {
+ tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen);
+ }
+ }
+
+ if (tokId == -1) {
+ if (messages.options.lessSupport) {
+ // Less compatibility:
+ // @name: value; => var-name: value; (VarDefinition)
+ // property: @name; => property: var(name); (VarUsage)
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ _eat(TokenKind.COLON);
+
+ Expressions exprs = processExpr();
+
+ var span = _makeSpan(start);
+ return new VarDefinitionDirective(
+ new VarDefinition(name, exprs, span), span);
+ } else if (isChecked) {
+ _error('unexpected directive @$_peekToken', _peekToken.span);
+ }
+ }
+ }
+
+ switch (tokId) {
+ case TokenKind.DIRECTIVE_IMPORT:
+ _next();
+
+ // @import "uri_string" or @import url("uri_string") are identical; only
+ // a url can follow an @import.
+ String importStr;
+ if (_peekIdentifier()) {
+ var func = processFunction(identifier());
+ if (func is UriTerm) {
+ importStr = func.text;
+ }
+ } else {
+ importStr = processQuotedString(false);
+ }
+
+ // Any medias?
+ var medias = processMediaQueryList();
+
+ if (importStr == null) {
+ _error('missing import string', _peekToken.span);
+ }
+
+ return new ImportDirective(importStr.trim(), medias, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_MEDIA:
+ _next();
+
+ // Any medias?
+ var media = processMediaQueryList();
+
+ List<TreeNode> rulesets = [];
+ if (_maybeEat(TokenKind.LBRACE)) {
+ while (!_maybeEat(TokenKind.END_OF_FILE)) {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset == null) break;
+ rulesets.add(ruleset);
+ }
+
+ if (!_maybeEat(TokenKind.RBRACE)) {
+ _error('expected } after ruleset for @media', _peekToken.span);
+ }
+ } else {
+ _error('expected { after media before ruleset', _peekToken.span);
+ }
+ return new MediaDirective(media, rulesets, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_HOST:
+ _next();
+
+ List<TreeNode> rulesets = [];
+ if (_maybeEat(TokenKind.LBRACE)) {
+ while (!_maybeEat(TokenKind.END_OF_FILE)) {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset == null) break;
+ rulesets.add(ruleset);
+ }
+
+ if (!_maybeEat(TokenKind.RBRACE)) {
+ _error('expected } after ruleset for @host', _peekToken.span);
+ }
+ } else {
+ _error('expected { after host before ruleset', _peekToken.span);
+ }
+ return new HostDirective(rulesets, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_PAGE:
+ /*
+ * @page S* IDENT? pseudo_page?
+ * S* '{' S*
+ * [ declaration | margin ]?
+ * [ ';' S* [ declaration | margin ]? ]* '}' S*
+ *
+ * pseudo_page :
+ * ':' [ "left" | "right" | "first" ]
+ *
+ * margin :
+ * margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
+ *
+ * margin_sym : @top-left-corner, @top-left, @bottom-left, etc.
+ *
+ * See http://www.w3.org/TR/css3-page/#CSS21
+ */
+ _next();
+
+ // Page name
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ // Any pseudo page?
+ var pseudoPage;
+ if (_maybeEat(TokenKind.COLON)) {
+ if (_peekIdentifier()) {
+ pseudoPage = identifier();
+ // TODO(terry): Normalize pseudoPage to lowercase.
+ if (isChecked &&
+ !(pseudoPage.name == 'left' ||
+ pseudoPage.name == 'right' ||
+ pseudoPage.name == 'first')) {
+ _warning("Pseudo page must be left, top or first",
+ pseudoPage.span);
+ return;
+ }
+ }
+ }
+
+ String pseudoName = pseudoPage is Identifier ? pseudoPage.name : '';
+ String ident = name is Identifier ? name.name : '';
+ return new PageDirective(ident, pseudoName,
+ processMarginsDeclarations(), _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_CHARSET:
+ // @charset S* STRING S* ';'
+ _next();
+
+ var charEncoding = processQuotedString(false);
+ if (isChecked && charEncoding == null) {
+ // Missing character encoding.
+ _warning('missing character encoding string', _makeSpan(start));
+ }
+
+ return new CharsetDirective(charEncoding, _makeSpan(start));
+
+ // TODO(terry): Workaround Dart2js bug continue not implemented in switch
+ // see https://code.google.com/p/dart/issues/detail?id=8270
+ /*
+ case TokenKind.DIRECTIVE_MS_KEYFRAMES:
+ // TODO(terry): For now only IE 10 (are base level) supports @keyframes,
+ // -moz- has only been optional since Oct 2012 release of Firefox, not
+ // all versions of webkit support @keyframes and opera doesn't yet
+ // support w/o -o- prefix. Add more warnings for other prefixes when
+ // they become optional.
+ if (isChecked) {
+ _warning('@-ms-keyframes should be @keyframes', _makeSpan(start));
+ }
+ continue keyframeDirective;
+
+ keyframeDirective:
+ */
+ case TokenKind.DIRECTIVE_KEYFRAMES:
+ case TokenKind.DIRECTIVE_WEB_KIT_KEYFRAMES:
+ case TokenKind.DIRECTIVE_MOZ_KEYFRAMES:
+ case TokenKind.DIRECTIVE_O_KEYFRAMES:
+ // TODO(terry): Remove workaround when bug 8270 is fixed.
+ case TokenKind.DIRECTIVE_MS_KEYFRAMES:
+ if (tokId == TokenKind.DIRECTIVE_MS_KEYFRAMES && isChecked) {
+ _warning('@-ms-keyframes should be @keyframes', _makeSpan(start));
+ }
+ // TODO(terry): End of workaround.
+
+ /* Key frames grammar:
+ *
+ * @[browser]? keyframes [IDENT|STRING] '{' keyframes-blocks '}';
+ *
+ * browser: [-webkit-, -moz-, -ms-, -o-]
+ *
+ * keyframes-blocks:
+ * [keyframe-selectors '{' declarations '}']* ;
+ *
+ * keyframe-selectors:
+ * ['from'|'to'|PERCENTAGE] [',' ['from'|'to'|PERCENTAGE] ]* ;
+ */
+ _next();
+
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ _eat(TokenKind.LBRACE);
+
+ var keyframe = new KeyFrameDirective(tokId, name, _makeSpan(start));
+
+ do {
+ Expressions selectors = new Expressions(_makeSpan(start));
+
+ do {
+ var term = processTerm();
+
+ // TODO(terry): Only allow from, to and PERCENTAGE ...
+
+ selectors.add(term);
+ } while (_maybeEat(TokenKind.COMMA));
+
+ keyframe.add(new KeyFrameBlock(selectors, processDeclarations(),
+ _makeSpan(start)));
+
+ } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile());
+
+ return keyframe;
+
+ case TokenKind.DIRECTIVE_FONTFACE:
+ _next();
+ return new FontFaceDirective(processDeclarations(), _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_STYLET:
+ /* Stylet grammar:
+ *
+ * @stylet IDENT '{'
+ * ruleset
+ * '}'
+ */
+ _next();
+
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ _eat(TokenKind.LBRACE);
+
+ List<TreeNode> productions = [];
+
+ start = _peekToken.start;
+ while (!_maybeEat(TokenKind.END_OF_FILE)) {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset == null) {
+ break;
+ }
+ productions.add(ruleset);
+ }
+
+ _eat(TokenKind.RBRACE);
+
+ return new StyletDirective(name, productions, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_NAMESPACE:
+ /* Namespace grammar:
+ *
+ * @namespace S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
+ * namespace_prefix : IDENT
+ *
+ */
+ _next();
+
+ var prefix;
+ if (_peekIdentifier()) {
+ prefix = identifier();
+ }
+
+ // The namespace URI can be either a quoted string url("uri_string")
+ // are identical.
+ String namespaceUri;
+ if (_peekIdentifier()) {
+ var func = processFunction(identifier());
+ if (func is UriTerm) {
+ namespaceUri = func.text;
+ }
+ } else {
+ if (prefix != null && prefix.name == 'url') {
+ var func = processFunction(prefix);
+ if (func is UriTerm) {
+ // @namespace url("");
+ namespaceUri = func.text;
+ prefix = null;
+ }
+ } else {
+ namespaceUri = processQuotedString(false);
+ }
+ }
+
+ return new NamespaceDirective(prefix != null ? prefix.name : '',
+ namespaceUri, _makeSpan(start));
+ }
+ }
+
+ RuleSet processRuleSet([SelectorGroup selectorGroup]) {
+ if (selectorGroup == null) {
+ selectorGroup = processSelectorGroup();
+ }
+ if (selectorGroup != null) {
+ return new RuleSet(selectorGroup, processDeclarations(),
+ selectorGroup.span);
+ }
+ }
+
+ /**
+ * Look ahead to see if what should be a declaration is really a selector.
+ * If it's a selector than it's a nested selector. This support's Less'
+ * nested selector syntax (requires a look ahead). E.g.,
+ *
+ * div {
+ * width : 20px;
+ * span {
+ * color: red;
+ * }
+ * }
+ *
+ * Two tag name selectors div and span equivalent to:
+ *
+ * div {
+ * width: 20px;
+ * }
+ * div span {
+ * color: red;
+ * }
+ *
+ * Return [null] if no selector or [SelectorGroup] if a selector was parsed.
+ */
+ SelectorGroup _nestedSelector() {
+ Messages oldMessages = messages;
+ _createMessages();
+
+ var markedData = _mark;
+
+ // Look a head do we have a nested selector instead of a declaration?
+ SelectorGroup selGroup = processSelectorGroup();
+
+ var nestedSelector = selGroup != null && _peekKind(TokenKind.LBRACE) &&
+ messages.messages.isEmpty;
+
+ if (!nestedSelector) {
+ // Not a selector so restore the world.
+ _restore(markedData);
+ messages = oldMessages;
+ return null;
+ } else {
+ // Remember any messages from look ahead.
+ oldMessages.mergeMessages(messages);
+ messages = oldMessages;
+ return selGroup;
+ }
+ }
+
+ processDeclarations({bool checkBrace: true}) {
+ int start = _peekToken.start;
+
+ if (checkBrace) _eat(TokenKind.LBRACE);
+
+ List decls = [];
+ List dartStyles = []; // List of latest styles exposed to Dart.
+
+ do {
+ var selectorGroup = _nestedSelector();
+ while (selectorGroup != null) {
+ // Nested selector so process as a ruleset.
+ var ruleset = processRuleSet(selectorGroup);
+ decls.add(ruleset);
+ selectorGroup = _nestedSelector();
+ }
+
+ Declaration decl = processDeclaration(dartStyles);
+ if (decl != null) {
+ if (decl.hasDartStyle) {
+ var newDartStyle = decl.dartStyle;
+
+ // Replace or add latest Dart style.
+ bool replaced = false;
+ for (var i = 0; i < dartStyles.length; i++) {
+ var dartStyle = dartStyles[i];
+ if (dartStyle.isSame(newDartStyle)) {
+ dartStyles[i] = newDartStyle;
+ replaced = true;
+ break;
+ }
+ }
+ if (!replaced) {
+ dartStyles.add(newDartStyle);
+ }
+ }
+ decls.add(decl);
+ }
+ } while (_maybeEat(TokenKind.SEMICOLON));
+
+ if (checkBrace) _eat(TokenKind.RBRACE);
+
+ // Fixup declaration to only have dartStyle that are live for this set of
+ // declarations.
+ for (var decl in decls) {
+ if (decl is Declaration) {
+ if (decl.hasDartStyle && dartStyles.indexOf(decl.dartStyle) < 0) {
+ // Dart style not live, ignore these styles in this Declarations.
+ decl.dartStyle = null;
+ }
+ }
+ }
+
+ return new DeclarationGroup(decls, _makeSpan(start));
+ }
+
+ List<DeclarationGroup> processMarginsDeclarations() {
+ List groups = [];
+
+ int start = _peekToken.start;
+
+ _eat(TokenKind.LBRACE);
+
+ List<Declaration> decls = [];
+ List dartStyles = []; // List of latest styles exposed to Dart.
+
+ do {
+ switch (_peek()) {
+ case TokenKind.MARGIN_DIRECTIVE_TOPLEFTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_TOPLEFT:
+ case TokenKind.MARGIN_DIRECTIVE_TOPCENTER:
+ case TokenKind.MARGIN_DIRECTIVE_TOPRIGHT:
+ case TokenKind.MARGIN_DIRECTIVE_TOPRIGHTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFT:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMCENTER:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHT:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_LEFTTOP:
+ case TokenKind.MARGIN_DIRECTIVE_LEFTMIDDLE:
+ case TokenKind.MARGIN_DIRECTIVE_LEFTBOTTOM:
+ case TokenKind.MARGIN_DIRECTIVE_RIGHTTOP:
+ case TokenKind.MARGIN_DIRECTIVE_RIGHTMIDDLE:
+ case TokenKind.MARGIN_DIRECTIVE_RIGHTBOTTOM:
+ // Margin syms processed.
+ // margin :
+ // margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
+ //
+ // margin_sym : @top-left-corner, @top-left, @bottom-left, etc.
+ var marginSym = _peek();
+
+ _next();
+
+ var declGroup = processDeclarations();
+ if (declGroup != null) {
+ groups.add(new MarginGroup(marginSym, declGroup.declarations,
+ _makeSpan(start)));
+ }
+ break;
+ default:
+ Declaration decl = processDeclaration(dartStyles);
+ if (decl != null) {
+ if (decl.hasDartStyle) {
+ var newDartStyle = decl.dartStyle;
+
+ // Replace or add latest Dart style.
+ bool replaced = false;
+ for (var i = 0; i < dartStyles.length; i++) {
+ var dartStyle = dartStyles[i];
+ if (dartStyle.isSame(newDartStyle)) {
+ dartStyles[i] = newDartStyle;
+ replaced = true;
+ break;
+ }
+ }
+ if (!replaced) {
+ dartStyles.add(newDartStyle);
+ }
+ }
+ decls.add(decl);
+ }
+ _maybeEat(TokenKind.SEMICOLON);
+ break;
+ }
+ } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile());
+
+ // Fixup declaration to only have dartStyle that are live for this set of
+ // declarations.
+ for (var decl in decls) {
+ if (decl.hasDartStyle && dartStyles.indexOf(decl.dartStyle) < 0) {
+ // Dart style not live, ignore these styles in this Declarations.
+ decl.dartStyle = null;
+ }
+ }
+
+ if (decls.length > 0) {
+ groups.add(new DeclarationGroup(decls, _makeSpan(start)));
+ }
+
+ return groups;
+ }
+
+ SelectorGroup processSelectorGroup() {
+ List<Selector> selectors = [];
+ int start = _peekToken.start;
+ do {
+ Selector selector = processSelector();
+ if (selector != null) {
+ selectors.add(selector);
+ }
+ } while (_maybeEat(TokenKind.COMMA));
+
+ if (selectors.length > 0) {
+ return new SelectorGroup(selectors, _makeSpan(start));
+ }
+ }
+
+ /**
+ * Return list of selectors
+ */
+ processSelector() {
+ List<SimpleSelectorSequence> simpleSequences = [];
+ int start = _peekToken.start;
+ while (true) {
+ // First item is never descendant make sure it's COMBINATOR_NONE.
+ var selectorItem = simpleSelectorSequence(simpleSequences.length == 0);
+ if (selectorItem != null) {
+ simpleSequences.add(selectorItem);
+ } else {
+ break;
+ }
+ }
+
+ if (simpleSequences.length > 0) {
+ return new Selector(simpleSequences, _makeSpan(start));
+ }
+ }
+
+ simpleSelectorSequence(bool forceCombinatorNone) {
+ var start = _peekToken.start;
+ var combinatorType = TokenKind.COMBINATOR_NONE;
+ var thisOperator = false;
+
+ switch (_peek()) {
+ case TokenKind.PLUS:
+ _eat(TokenKind.PLUS);
+ combinatorType = TokenKind.COMBINATOR_PLUS;
+ break;
+ case TokenKind.GREATER:
+ _eat(TokenKind.GREATER);
+ combinatorType = TokenKind.COMBINATOR_GREATER;
+ break;
+ case TokenKind.TILDE:
+ _eat(TokenKind.TILDE);
+ combinatorType = TokenKind.COMBINATOR_TILDE;
+ break;
+ case TokenKind.AMPERSAND:
+ _eat(TokenKind.AMPERSAND);
+ thisOperator = true;
+ break;
+ }
+
+ // Check if WHITESPACE existed between tokens if so we're descendent.
+ if (combinatorType == TokenKind.COMBINATOR_NONE && !forceCombinatorNone) {
+ if (this._previousToken != null &&
+ this._previousToken.end != this._peekToken.start) {
+ combinatorType = TokenKind.COMBINATOR_DESCENDANT;
+ }
+ }
+
+ var span = _makeSpan(start);
+ var simpleSel = thisOperator ?
+ new ElementSelector(new ThisOperator(span), span) : simpleSelector();
+ if (simpleSel == null &&
+ (combinatorType == TokenKind.COMBINATOR_PLUS ||
+ combinatorType == TokenKind.COMBINATOR_GREATER ||
+ combinatorType == TokenKind.COMBINATOR_TILDE)) {
+ // For "+ &", "~ &" or "> &" a selector sequence with no name is needed
+ // so that the & will have a combinator too. This is needed to
+ // disambiguate selector expressions:
+ // .foo&:hover combinator before & is NONE
+ // .foo & combinator before & is DESCDENDANT
+ // .foo > & combinator before & is GREATER
+ simpleSel = new ElementSelector(new Identifier("", span), span);
+ }
+ if (simpleSel != null) {
+ return new SimpleSelectorSequence(simpleSel, span, combinatorType);
+ }
+ }
+
+ /**
+ * Simple selector grammar:
+ *
+ * simple_selector_sequence
+ * : [ type_selector | universal ]
+ * [ HASH | class | attrib | pseudo | negation ]*
+ * | [ HASH | class | attrib | pseudo | negation ]+
+ * type_selector
+ * : [ namespace_prefix ]? element_name
+ * namespace_prefix
+ * : [ IDENT | '*' ]? '|'
+ * element_name
+ * : IDENT
+ * universal
+ * : [ namespace_prefix ]? '*'
+ * class
+ * : '.' IDENT
+ */
+ simpleSelector() {
+ // TODO(terry): Nathan makes a good point parsing of namespace and element
+ // are essentially the same (asterisk or identifier) other
+ // than the error message for element. Should consolidate the
+ // code.
+ // TODO(terry): Need to handle attribute namespace too.
+ var first;
+ int start = _peekToken.start;
+ switch (_peek()) {
+ case TokenKind.ASTERISK:
+ // Mark as universal namespace.
+ var tok = _next();
+ first = new Wildcard(_makeSpan(tok.start));
+ break;
+ case TokenKind.IDENTIFIER:
+ first = identifier();
+ break;
+ default:
+ // Expecting simple selector.
+ // TODO(terry): Could be a synthesized token like value, etc.
+ if (TokenKind.isKindIdentifier(_peek())) {
+ first = identifier();
+ } else if (_peekKind(TokenKind.SEMICOLON)) {
+ // Can't be a selector if we found a semi-colon.
+ return null;
+ }
+ break;
+ }
+
+ if (_maybeEat(TokenKind.NAMESPACE)) {
+ var element;
+ switch (_peek()) {
+ case TokenKind.ASTERISK:
+ // Mark as universal element
+ var tok = _next();
+ element = new Wildcard(_makeSpan(tok.start));
+ break;
+ case TokenKind.IDENTIFIER:
+ element = identifier();
+ break;
+ default:
+ _error('expected element name or universal(*), but found $_peekToken',
+ _peekToken.span);
+ break;
+ }
+
+ return new NamespaceSelector(first,
+ new ElementSelector(element, element.span), _makeSpan(start));
+ } else if (first != null) {
+ return new ElementSelector(first, _makeSpan(start));
+ } else {
+ // Check for HASH | class | attrib | pseudo | negation
+ return simpleSelectorTail();
+ }
+ }
+
+ bool _anyWhiteSpaceBeforePeekToken(int kind) {
+ if (_previousToken != null && _peekToken != null &&
+ _previousToken.kind == kind) {
+ // If end of previous token isn't same as the start of peek token then
+ // there's something between these tokens probably whitespace.
+ return _previousToken.end != _peekToken.start;
+ }
+
+ return false;
+ }
+
+ /**
+ * type_selector | universal | HASH | class | attrib | pseudo
+ */
+ simpleSelectorTail() {
+ // Check for HASH | class | attrib | pseudo | negation
+ int start = _peekToken.start;
+ switch (_peek()) {
+ case TokenKind.HASH:
+ _eat(TokenKind.HASH);
+
+ bool hasWhiteSpace = false;
+ if (_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) {
+ _warning("Not a valid ID selector expected #id", _makeSpan(start));
+ hasWhiteSpace = true;
+ }
+ if (_peekIdentifier()) {
+ var id = identifier();
+ if (hasWhiteSpace) {
+ // Generate bad selector id (normalized).
+ id.name = " ${id.name}";
+ }
+ return new IdSelector(id, _makeSpan(start));
+ }
+ return null;
+ case TokenKind.DOT:
+ _eat(TokenKind.DOT);
+
+ bool hasWhiteSpace = false;
+ if (_anyWhiteSpaceBeforePeekToken(TokenKind.DOT)) {
+ _warning("Not a valid class selector expected .className",
+ _makeSpan(start));
+ hasWhiteSpace = true;
+ }
+ var id = identifier();
+ if (hasWhiteSpace) {
+ // Generate bad selector class (normalized).
+ id.name = " ${id.name}";
+ }
+ return new ClassSelector(id, _makeSpan(start));
+ case TokenKind.COLON:
+ // :pseudo-class ::pseudo-element
+ // TODO(terry): '::' should be token.
+ _eat(TokenKind.COLON);
+ bool pseudoElement = _maybeEat(TokenKind.COLON);
+
+ // TODO(terry): If no identifier specified consider optimizing out the
+ // : or :: and making this a normal selector. For now,
+ // create an empty pseudoName.
+ var pseudoName;
+ if (_peekIdentifier()) {
+ pseudoName = identifier();
+ } else {
+ return null;
+ }
+
+ // Functional pseudo?
+ if (_maybeEat(TokenKind.LPAREN)) {
+ if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') {
+ // Negation : ':NOT(' S* negation_arg S* ')'
+ var negArg = simpleSelector();
+
+ _eat(TokenKind.RPAREN);
+ return new NegationSelector(negArg, _makeSpan(start));
+ } else {
+ // Handle function expression.
+ var span = _makeSpan(start);
+ var expr = processSelectorExpression();
+
+ // Used during selector look-a-head if not a SelectorExpression is
+ // bad.
+ if (expr is! SelectorExpression) {
+ _errorExpected("CSS expression");
+ return null;
+ }
+
+ _eat(TokenKind.RPAREN);
+ return (pseudoElement) ?
+ new PseudoElementFunctionSelector(pseudoName, expr, span) :
+ new PseudoClassFunctionSelector(pseudoName, expr, span);
+ }
+ }
+
+ // TODO(terry): Need to handle specific pseudo class/element name and
+ // backward compatible names that are : as well as :: as well as
+ // parameters. Current, spec uses :: for pseudo-element and : for
+ // pseudo-class. However, CSS2.1 allows for : to specify old
+ // pseudo-elements (:first-line, :first-letter, :before and :after) any
+ // new pseudo-elements defined would require a ::.
+ return pseudoElement ?
+ new PseudoElementSelector(pseudoName, _makeSpan(start)) :
+ new PseudoClassSelector(pseudoName, _makeSpan(start));
+ case TokenKind.LBRACK:
+ return processAttribute();
+ case TokenKind.DOUBLE:
+ _error('name must start with a alpha character, but found a number',
+ _peekToken.span);
+ _next();
+ break;
+ }
+ }
+
+ /**
+ * In CSS3, the expressions are identifiers, strings, or of the form "an+b".
+ *
+ * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
+ *
+ * num [0-9]+|[0-9]*\.[0-9]+
+ * PLUS '+'
+ * DIMENSION {num}{ident}
+ * NUMBER {num}
+ */
+ processSelectorExpression() {
+ int start = _peekToken.start;
+
+ var expression = new SelectorExpression(_makeSpan(start));
+
+ Token termToken;
+ var value;
+
+ // Special parsing for expressions in pseudo functions. Minus is used as
+ // operator not identifier.
+ tokenizer.selectorExpression = true;
+
+ bool keepParsing = true;
+ while (keepParsing) {
+ switch (_peek()) {
+ case TokenKind.PLUS:
+ start = _peekToken.start;
+ termToken = _next();
+ expression.add(new OperatorPlus(_makeSpan(start)));
+ break;
+ case TokenKind.MINUS:
+ start = _peekToken.start;
+ termToken = _next();
+ expression.add(new OperatorMinus(_makeSpan(start)));
+ break;
+ case TokenKind.INTEGER:
+ termToken = _next();
+ value = int.parse(termToken.text);
+ break;
+ case TokenKind.DOUBLE:
+ termToken = _next();
+ value = double.parse(termToken.text);
+ break;
+ case TokenKind.SINGLE_QUOTE:
+ case TokenKind.DOUBLE_QUOTE:
+ value = processQuotedString(false);
+ value = '"${_escapeString(value)}"';
+ return new LiteralTerm(value, value, _makeSpan(start));
+ case TokenKind.IDENTIFIER:
+ value = identifier(); // Snarf up the ident we'll remap, maybe.
+ break;
+ default:
+ keepParsing = false;
+ }
+
+ if (keepParsing && value != null) {
+ var unitTerm;
+ // Don't process the dimension if MINUS or PLUS is next.
+ if (_peek() != TokenKind.MINUS && _peek() != TokenKind.PLUS) {
+ unitTerm = processDimension(termToken, value, _makeSpan(start));
+ }
+ if (unitTerm == null) {
+ unitTerm = new LiteralTerm(value, value.name, _makeSpan(start));
+ }
+ expression.add(unitTerm);
+
+ value = null;
+ }
+ }
+
+ tokenizer.selectorExpression = false;
+
+ return expression;
+ }
+
+ // Attribute grammar:
+ //
+ // attributes :
+ // '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']'
+ //
+ // ATTRIB_MATCHES :
+ // [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ]
+ //
+ // INCLUDES: '~='
+ //
+ // DASHMATCH: '|='
+ //
+ // PREFIXMATCH: '^='
+ //
+ // SUFFIXMATCH: '$='
+ //
+ // SUBSTRMATCH: '*='
+ //
+ //
+ processAttribute() {
+ int start = _peekToken.start;
+
+ if (_maybeEat(TokenKind.LBRACK)) {
+ var attrName = identifier();
+
+ int op;
+ switch (_peek()) {
+ case TokenKind.EQUALS:
+ case TokenKind.INCLUDES: // ~=
+ case TokenKind.DASH_MATCH: // |=
+ case TokenKind.PREFIX_MATCH: // ^=
+ case TokenKind.SUFFIX_MATCH: // $=
+ case TokenKind.SUBSTRING_MATCH: // *=
+ op = _peek();
+ _next();
+ break;
+ default:
+ op = TokenKind.NO_MATCH;
+ }
+
+ var value;
+ if (op != TokenKind.NO_MATCH) {
+ // Operator hit so we require a value too.
+ if (_peekIdentifier()) {
+ value = identifier();
+ } else {
+ value = processQuotedString(false);
+ }
+
+ if (value == null) {
+ _error('expected attribute value string or ident', _peekToken.span);
+ }
+ }
+
+ _eat(TokenKind.RBRACK);
+
+ return new AttributeSelector(attrName, op, value, _makeSpan(start));
+ }
+ }
+
+ // Declaration grammar:
+ //
+ // declaration: property ':' expr prio?
+ //
+ // property: IDENT [or IE hacks]
+ // prio: !important
+ // expr: (see processExpr)
+ //
+ // Here are the ugly IE hacks we need to support:
+ // property: expr prio? \9; - IE8 and below property, /9 before semi-colon
+ // *IDENT - IE7 or below
+ // _IDENT - IE6 property (automatically a valid ident)
+ //
+ processDeclaration(List dartStyles) {
+ Declaration decl;
+
+ int start = _peekToken.start;
+
+ // IE7 hack of * before property name if so the property is IE7 or below.
+ var ie7 = _peekKind(TokenKind.ASTERISK);
+ if (ie7) {
+ _next();
+ }
+
+ // IDENT ':' expr '!important'?
+ if (TokenKind.isIdentifier(_peekToken.kind)) {
+ var propertyIdent = identifier();
+
+ var ieFilterProperty = propertyIdent.name.toLowerCase() == 'filter';
+
+ _eat(TokenKind.COLON);
+
+ Expressions exprs = processExpr(ieFilterProperty);
+
+ var dartComposite = _styleForDart(propertyIdent, exprs, dartStyles);
+
+ // Handle !important (prio)
+ var importantPriority = _maybeEat(TokenKind.IMPORTANT);
+
+ decl = new Declaration(propertyIdent, exprs, dartComposite,
+ _makeSpan(start), important: importantPriority, ie7: ie7);
+ } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) {
+ _next();
+ var definedName;
+ if (_peekIdentifier()) definedName = identifier();
+
+ _eat(TokenKind.COLON);
+
+ Expressions exprs = processExpr();
+
+ decl = new VarDefinition(definedName, exprs, _makeSpan(start));
+ }
+
+ return decl;
+ }
+
+ /** List of styles exposed to the Dart UI framework. */
+ static const int _fontPartFont= 0;
+ static const int _fontPartVariant = 1;
+ static const int _fontPartWeight = 2;
+ static const int _fontPartSize = 3;
+ static const int _fontPartFamily = 4;
+ static const int _fontPartStyle = 5;
+ static const int _marginPartMargin = 6;
+ static const int _marginPartLeft = 7;
+ static const int _marginPartTop = 8;
+ static const int _marginPartRight = 9;
+ static const int _marginPartBottom = 10;
+ static const int _lineHeightPart = 11;
+ static const int _borderPartBorder = 12;
+ static const int _borderPartLeft = 13;
+ static const int _borderPartTop = 14;
+ static const int _borderPartRight = 15;
+ static const int _borderPartBottom = 16;
+ static const int _borderPartWidth = 17;
+ static const int _borderPartLeftWidth = 18;
+ static const int _borderPartTopWidth = 19;
+ static const int _borderPartRightWidth = 20;
+ static const int _borderPartBottomWidth = 21;
+ static const int _heightPart = 22;
+ static const int _widthPart = 23;
+ static const int _paddingPartPadding = 24;
+ static const int _paddingPartLeft = 25;
+ static const int _paddingPartTop = 26;
+ static const int _paddingPartRight = 27;
+ static const int _paddingPartBottom = 28;
+
+ static const Map<String, int> _stylesToDart = const {
+ 'font': _fontPartFont,
+ 'font-family': _fontPartFamily,
+ 'font-size': _fontPartSize,
+ 'font-style': _fontPartStyle,
+ 'font-variant': _fontPartVariant,
+ 'font-weight': _fontPartWeight,
+ 'line-height': _lineHeightPart,
+ 'margin': _marginPartMargin,
+ 'margin-left': _marginPartLeft,
+ 'margin-right': _marginPartRight,
+ 'margin-top': _marginPartTop,
+ 'margin-bottom': _marginPartBottom,
+ 'border': _borderPartBorder,
+ 'border-left': _borderPartLeft,
+ 'border-right': _borderPartRight,
+ 'border-top': _borderPartTop,
+ 'border-bottom': _borderPartBottom,
+ 'border-width': _borderPartWidth,
+ 'border-left-width': _borderPartLeftWidth,
+ 'border-top-width': _borderPartTopWidth,
+ 'border-right-width': _borderPartRightWidth,
+ 'border-bottom-width': _borderPartBottomWidth,
+ 'height': _heightPart,
+ 'width': _widthPart,
+ 'padding': _paddingPartPadding,
+ 'padding-left': _paddingPartLeft,
+ 'padding-top': _paddingPartTop,
+ 'padding-right': _paddingPartRight,
+ 'padding-bottom': _paddingPartBottom
+ };
+
+ static const Map<String, int> _nameToFontWeight = const {
+ 'bold' : FontWeight.bold,
+ 'normal' : FontWeight.normal
+ };
+
+ static _findStyle(String styleName) {
+ if (_stylesToDart.containsKey(styleName)) {
+ return _stylesToDart[styleName];
+ }
+ }
+
+ _styleForDart(Identifier property, Expressions exprs, List dartStyles) {
+ int styleType = _findStyle(property.name.toLowerCase());
+ if (styleType != null) {
+ return buildDartStyleNode(styleType, exprs, dartStyles);
+ }
+ }
+
+ FontExpression _mergeFontStyles(FontExpression fontExpr, List dartStyles) {
+ // Merge all font styles for this class selector.
+ for (var dartStyle in dartStyles) {
+ if (dartStyle.isFont) {
+ fontExpr = new FontExpression.merge(dartStyle, fontExpr);
+ }
+ }
+
+ return fontExpr;
+ }
+
+ buildDartStyleNode(int styleType, Expressions exprs, List dartStyles) {
+ switch (styleType) {
+ /*
+ * Properties in order:
+ *
+ * font-style font-variant font-weight font-size/line-height font-family
+ *
+ * The font-size and font-family values are required. If other values are
+ * missing; a default, if it exist, will be used.
+ */
+ case _fontPartFont:
+ var processor = new ExpressionsProcessor(exprs);
+ return _mergeFontStyles(processor.processFont(), dartStyles);
+ case _fontPartFamily:
+ var processor = new ExpressionsProcessor(exprs);
+
+ try {
+ return _mergeFontStyles(processor.processFontFamily(), dartStyles);
+ } catch (fontException) {
+ _error(fontException, _peekToken.span);
+ }
+ break;
+ case _fontPartSize:
+ var processor = new ExpressionsProcessor(exprs);
+ return _mergeFontStyles(processor.processFontSize(), dartStyles);
+ case _fontPartStyle:
+ /* Possible style values:
+ * normal [default]
+ * italic
+ * oblique
+ * inherit
+ */
+ // TODO(terry): TBD
+ break;
+ case _fontPartVariant:
+ /* Possible variant values:
+ * normal [default]
+ * small-caps
+ * inherit
+ */
+ // TODO(terry): TBD
+ break;
+ case _fontPartWeight:
+ /* Possible weight values:
+ * normal [default]
+ * bold
+ * bolder
+ * lighter
+ * 100 - 900
+ * inherit
+ */
+ // TODO(terry): Only 'normal', 'bold', or values of 100-900 supoorted
+ // need to handle bolder, lighter, and inherit. See
+ // https://github.com/dart-lang/csslib/issues/1
+ var expr = exprs.expressions[0];
+ if (expr is NumberTerm) {
+ var fontExpr = new FontExpression(expr.span,
+ weight: expr.value);
+ return _mergeFontStyles(fontExpr, dartStyles);
+ } else if (expr is LiteralTerm) {
+ int weight = _nameToFontWeight[expr.value.toString()];
+ if (weight != null) {
+ var fontExpr = new FontExpression(expr.span, weight: weight);
+ return _mergeFontStyles(fontExpr, dartStyles);
+ }
+ }
+ break;
+ case _lineHeightPart:
+ num lineHeight;
+ if (exprs.expressions.length == 1) {
+ var expr = exprs.expressions[0];
+ if (expr is UnitTerm) {
+ UnitTerm unitTerm = expr;
+ // TODO(terry): Need to handle other units and LiteralTerm normal
+ // See https://github.com/dart-lang/csslib/issues/2.
+ if (unitTerm.unit == TokenKind.UNIT_LENGTH_PX ||
+ unitTerm.unit == TokenKind.UNIT_LENGTH_PT) {
+ var fontExpr = new FontExpression(expr.span,
+ lineHeight: new LineHeight(expr.value, inPixels: true));
+ return _mergeFontStyles(fontExpr, dartStyles);
+ } else if (isChecked) {
+ _warning("Unexpected unit for line-height", expr.span);
+ }
+ } else if (expr is NumberTerm) {
+ var fontExpr = new FontExpression(expr.span,
+ lineHeight: new LineHeight(expr.value, inPixels: false));
+ return _mergeFontStyles(fontExpr, dartStyles);
+ } else if (isChecked) {
+ _warning("Unexpected value for line-height", expr.span);
+ }
+ }
+ break;
+ case _marginPartMargin:
+ return new MarginExpression.boxEdge(exprs.span, processFourNums(exprs));
+ case _borderPartBorder:
+ for (var expr in exprs.expressions) {
+ var v = marginValue(expr);
+ if (v != null) {
+ final box = new BoxEdge.uniform(v);
+ return new BorderExpression.boxEdge(exprs.span, box);
+ }
+ }
+ break;
+ case _borderPartWidth:
+ var v = marginValue(exprs.expressions[0]);
+ if (v != null) {
+ final box = new BoxEdge.uniform(v);
+ return new BorderExpression.boxEdge(exprs.span, box);
+ }
+ break;
+ case _paddingPartPadding:
+ return new PaddingExpression.boxEdge(exprs.span,
+ processFourNums(exprs));
+ case _marginPartLeft:
+ case _marginPartTop:
+ case _marginPartRight:
+ case _marginPartBottom:
+ case _borderPartLeft:
+ case _borderPartTop:
+ case _borderPartRight:
+ case _borderPartBottom:
+ case _borderPartLeftWidth:
+ case _borderPartTopWidth:
+ case _borderPartRightWidth:
+ case _borderPartBottomWidth:
+ case _heightPart:
+ case _widthPart:
+ case _paddingPartLeft:
+ case _paddingPartTop:
+ case _paddingPartRight:
+ case _paddingPartBottom:
+ if (exprs.expressions.length > 0) {
+ return processOneNumber(exprs, styleType);
+ }
+ break;
+ default:
+ // Don't handle it.
+ return;
+ }
+ }
+
+ // TODO(terry): Look at handling width of thin, thick, etc. any none numbers
+ // to convert to a number.
+ processOneNumber(Expressions exprs, int part) {
+ var value = marginValue(exprs.expressions[0]);
+ if (value != null) {
+ switch (part) {
+ case _marginPartLeft:
+ return new MarginExpression(exprs.span, left: value);
+ case _marginPartTop:
+ return new MarginExpression(exprs.span, top: value);
+ case _marginPartRight:
+ return new MarginExpression(exprs.span, right: value);
+ case _marginPartBottom:
+ return new MarginExpression(exprs.span, bottom: value);
+ case _borderPartLeft:
+ case _borderPartLeftWidth:
+ return new BorderExpression(exprs.span, left: value);
+ case _borderPartTop:
+ case _borderPartTopWidth:
+ return new BorderExpression(exprs.span, top: value);
+ case _borderPartRight:
+ case _borderPartRightWidth:
+ return new BorderExpression(exprs.span, right: value);
+ case _borderPartBottom:
+ case _borderPartBottomWidth:
+ return new BorderExpression(exprs.span, bottom: value);
+ case _heightPart:
+ return new HeightExpression(exprs.span, value);
+ case _widthPart:
+ return new WidthExpression(exprs.span, value);
+ case _paddingPartLeft:
+ return new PaddingExpression(exprs.span, left: value);
+ case _paddingPartTop:
+ return new PaddingExpression(exprs.span, top: value);
+ case _paddingPartRight:
+ return new PaddingExpression(exprs.span, right: value);
+ case _paddingPartBottom:
+ return new PaddingExpression(exprs.span, bottom: value);
+ }
+ }
+ }
+
+ /**
+ * Margins are of the format:
+ *
+ * top,right,bottom,left (4 parameters)
+ * top,right/left, bottom (3 parameters)
+ * top/bottom,right/left (2 parameters)
+ * top/right/bottom/left (1 parameter)
+ *
+ * The values of the margins can be a unit or unitless or auto.
+ */
+ processFourNums(Expressions exprs) {
+ num top;
+ num right;
+ num bottom;
+ num left;
+
+ int totalExprs = exprs.expressions.length;
+ switch (totalExprs) {
+ case 1:
+ top = marginValue(exprs.expressions[0]);
+ right = top;
+ bottom = top;
+ left = top;
+ break;
+ case 2:
+ top = marginValue(exprs.expressions[0]);
+ bottom = top;
+ right = marginValue(exprs.expressions[1]);
+ left = right;
+ break;
+ case 3:
+ top = marginValue(exprs.expressions[0]);
+ right = marginValue(exprs.expressions[1]);
+ left = right;
+ bottom = marginValue(exprs.expressions[2]);
+ break;
+ case 4:
+ top = marginValue(exprs.expressions[0]);
+ right = marginValue(exprs.expressions[1]);
+ bottom = marginValue(exprs.expressions[2]);
+ left = marginValue(exprs.expressions[3]);
+ break;
+ default:
+ return;
+ }
+
+ return new BoxEdge.clockwiseFromTop(top, right, bottom, left);
+ }
+
+ // TODO(terry): Need to handle auto.
+ marginValue(var exprTerm) {
+ if (exprTerm is UnitTerm || exprTerm is NumberTerm) {
+ return exprTerm.value;
+ }
+ }
+
+ // Expression grammar:
+ //
+ // expression: term [ operator? term]*
+ //
+ // operator: '/' | ','
+ // term: (see processTerm)
+ //
+ processExpr([bool ieFilter = false]) {
+ int start = _peekToken.start;
+ Expressions expressions = new Expressions(_makeSpan(start));
+
+ bool keepGoing = true;
+ var expr;
+ while (keepGoing && (expr = processTerm(ieFilter)) != null) {
+ var op;
+
+ int opStart = _peekToken.start;
+
+ switch (_peek()) {
+ case TokenKind.SLASH:
+ op = new OperatorSlash(_makeSpan(opStart));
+ break;
+ case TokenKind.COMMA:
+ op = new OperatorComma(_makeSpan(opStart));
+ break;
+ case TokenKind.BACKSLASH:
+ // Backslash outside of string; detected IE8 or older signaled by \9 at
+ // end of an expression.
+ var ie8Start = _peekToken.start;
+
+ _next();
+ if (_peekKind(TokenKind.INTEGER)) {
+ var numToken = _next();
+ var value = int.parse(numToken.text);
+ if (value == 9) {
+ op = new IE8Term(_makeSpan(ie8Start));
+ } else if (isChecked) {
+ _warning("\$value is not valid in an expression", _makeSpan(start));
+ }
+ }
+ break;
+ }
+
+ if (expr != null) {
+ expressions.add(expr);
+ } else {
+ keepGoing = false;
+ }
+
+ if (op != null) {
+ expressions.add(op);
+ if (op is IE8Term) {
+ keepGoing = false;
+ } else {
+ _next();
+ }
+ }
+ }
+
+ return expressions;
+ }
+
+ static int MAX_UNICODE = int.parse('0x10FFFF');
+
+ // Term grammar:
+ //
+ // term:
+ // unary_operator?
+ // [ term_value ]
+ // | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor
+ //
+ // term_value:
+ // NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* |
+ // TIME S* | FREQ S* | function
+ //
+ // NUMBER: {num}
+ // PERCENTAGE: {num}%
+ // LENGTH: {num}['px' | 'cm' | 'mm' | 'in' | 'pt' | 'pc']
+ // EMS: {num}'em'
+ // EXS: {num}'ex'
+ // ANGLE: {num}['deg' | 'rad' | 'grad']
+ // TIME: {num}['ms' | 's']
+ // FREQ: {num}['hz' | 'khz']
+ // function: IDENT '(' expr ')'
+ //
+ processTerm([bool ieFilter = false]) {
+ int start = _peekToken.start;
+ Token t; // token for term's value
+ var value; // value of term (numeric values)
+
+ var unary = "";
+ switch (_peek()) {
+ case TokenKind.HASH:
+ this._eat(TokenKind.HASH);
+ if (!_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) {
+ String hexText;
+ if (_peekKind(TokenKind.INTEGER)) {
+ String hexText1 = _peekToken.text;
+ _next();
+ if (_peekIdentifier()) {
+ hexText = '$hexText1${identifier().name}';
+ } else {
+ hexText = hexText1;
+ }
+ } else if (_peekIdentifier()) {
+ hexText = identifier().name;
+ }
+ if (hexText != null) {
+ return _parseHex(hexText, _makeSpan(start));
+ }
+ }
+
+ if (isChecked) {
+ _warning("Expected hex number", _makeSpan(start));
+ }
+ // Construct the bad hex value with a #<space>number.
+ return _parseHex(" ${processTerm().text}", _makeSpan(start));
+ case TokenKind.INTEGER:
+ t = _next();
+ value = int.parse("${unary}${t.text}");
+ break;
+ case TokenKind.DOUBLE:
+ t = _next();
+ value = double.parse("${unary}${t.text}");
+ break;
+ case TokenKind.SINGLE_QUOTE:
+ case TokenKind.DOUBLE_QUOTE:
+ value = processQuotedString(false);
+ value = '"${_escapeString(value)}"';
+ return new LiteralTerm(value, value, _makeSpan(start));
+ case TokenKind.LPAREN:
+ _next();
+
+ GroupTerm group = new GroupTerm(_makeSpan(start));
+
+ var term;
+ do {
+ term = processTerm();
+ if (term != null && term is LiteralTerm) {
+ group.add(term);
+ }
+ } while (term != null && !_maybeEat(TokenKind.RPAREN) &&
+ !isPrematureEndOfFile());
+
+ return group;
+ case TokenKind.LBRACK:
+ _next();
+
+ var term = processTerm();
+ if (!(term is NumberTerm)) {
+ _error('Expecting a positive number', _makeSpan(start));
+ }
+
+ _eat(TokenKind.RBRACK);
+
+ return new ItemTerm(term.value, term.text, _makeSpan(start));
+ case TokenKind.IDENTIFIER:
+ var nameValue = identifier(); // Snarf up the ident we'll remap, maybe.
+
+ if (!ieFilter && _maybeEat(TokenKind.LPAREN)) {
+ // FUNCTION
+ return processFunction(nameValue);
+ } if (ieFilter) {
+ if (_maybeEat(TokenKind.COLON) &&
+ nameValue.name.toLowerCase() == 'progid') {
+ // IE filter:progid:
+ return processIEFilter(start);
+ } else {
+ // Handle filter:<name> where name is any filter e.g., alpha, chroma,
+ // Wave, blur, etc.
+ return processIEFilter(start);
+ }
+ }
+
+ // TODO(terry): Need to have a list of known identifiers today only
+ // 'from' is special.
+ if (nameValue.name == 'from') {
+ return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start));
+ }
+
+ // What kind of identifier is it, named color?
+ var colorEntry = TokenKind.matchColorName(nameValue.name);
+ if (colorEntry == null) {
+ if (isChecked) {
+ var propName = nameValue.name;
+ var errMsg = TokenKind.isPredefinedName(propName) ?
+ "Improper use of property value ${propName}" :
+ "Unknown property value ${propName}";
+ _warning(errMsg, _makeSpan(start));
+ }
+ return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start));
+ }
+
+ // Yes, process the color as an RGB value.
+ String rgbColor = TokenKind.decimalToHex(
+ TokenKind.colorValue(colorEntry), 6);
+ return _parseHex(rgbColor, _makeSpan(start));
+ case TokenKind.UNICODE_RANGE:
+ var first;
+ var second;
+ var firstNumber;
+ var secondNumber;
+ _eat(TokenKind.UNICODE_RANGE, unicodeRange: true);
+ if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) {
+ first = _previousToken.text;
+ firstNumber = int.parse('0x$first');
+ if (firstNumber > MAX_UNICODE) {
+ _error("unicode range must be less than 10FFFF", _makeSpan(start));
+ }
+ if (_maybeEat(TokenKind.MINUS, unicodeRange: true)) {
+ if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) {
+ second = _previousToken.text;
+ secondNumber = int.parse('0x$second');
+ if (secondNumber > MAX_UNICODE) {
+ _error("unicode range must be less than 10FFFF",
+ _makeSpan(start));
+ }
+ if (firstNumber > secondNumber) {
+ _error("unicode first range can not be greater than last",
+ _makeSpan(start));
+ }
+ }
+ }
+ } else if (_maybeEat(TokenKind.HEX_RANGE, unicodeRange: true)) {
+ first = _previousToken.text;
+ }
+
+ return new UnicodeRangeTerm(first, second, _makeSpan(start));
+ case TokenKind.AT:
+ if (messages.options.lessSupport) {
+ _next();
+
+ var expr = processExpr();
+ if (isChecked && expr.expressions.length > 1) {
+ _error("only @name for Less syntax", _peekToken.span);
+ }
+
+ var param = expr.expressions[0];
+ return new VarUsage(param.text, [], _makeSpan(start));
+ }
+ break;
+ }
+
+ return processDimension(t, value, _makeSpan(start));
+ }
+
+ /** Process all dimension units. */
+ processDimension(Token t, var value, Span span) {
+ var term;
+ var unitType = this._peek();
+
+ switch (unitType) {
+ case TokenKind.UNIT_EM:
+ term = new EmTerm(value, t.text, span);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_EX:
+ term = new ExTerm(value, t.text, span);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_LENGTH_PX:
+ case TokenKind.UNIT_LENGTH_CM:
+ case TokenKind.UNIT_LENGTH_MM:
+ case TokenKind.UNIT_LENGTH_IN:
+ case TokenKind.UNIT_LENGTH_PT:
+ case TokenKind.UNIT_LENGTH_PC:
+ term = new LengthTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_ANGLE_DEG:
+ case TokenKind.UNIT_ANGLE_RAD:
+ case TokenKind.UNIT_ANGLE_GRAD:
+ case TokenKind.UNIT_ANGLE_TURN:
+ term = new AngleTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_TIME_MS:
+ case TokenKind.UNIT_TIME_S:
+ term = new TimeTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_FREQ_HZ:
+ case TokenKind.UNIT_FREQ_KHZ:
+ term = new FreqTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.PERCENT:
+ term = new PercentageTerm(value, t.text, span);
+ _next(); // Skip the %
+ break;
+ case TokenKind.UNIT_FRACTION:
+ term = new FractionTerm(value, t.text, span);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_RESOLUTION_DPI:
+ case TokenKind.UNIT_RESOLUTION_DPCM:
+ case TokenKind.UNIT_RESOLUTION_DPPX:
+ term = new ResolutionTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_CH:
+ term = new ChTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_REM:
+ term = new RemTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_VIEWPORT_VW:
+ case TokenKind.UNIT_VIEWPORT_VH:
+ case TokenKind.UNIT_VIEWPORT_VMIN:
+ case TokenKind.UNIT_VIEWPORT_VMAX:
+ term = new ViewportTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ default:
+ if (value != null && t != null) {
+ term = (value is Identifier)
+ ? new LiteralTerm(value, value.name, span)
+ : new NumberTerm(value, t.text, span);
+ }
+ break;
+ }
+
+ return term;
+ }
+
+ processQuotedString([bool urlString = false]) {
+ int start = _peekToken.start;
+
+ // URI term sucks up everything inside of quotes(' or ") or between parens
+ int stopToken = urlString ? TokenKind.RPAREN : -1;
+ switch (_peek()) {
+ case TokenKind.SINGLE_QUOTE:
+ stopToken = TokenKind.SINGLE_QUOTE;
+ start = _peekToken.start + 1; // Skip the quote might have whitespace.
+ _next(); // Skip the SINGLE_QUOTE.
+ break;
+ case TokenKind.DOUBLE_QUOTE:
+ stopToken = TokenKind.DOUBLE_QUOTE;
+ start = _peekToken.start + 1; // Skip the quote might have whitespace.
+ _next(); // Skip the DOUBLE_QUOTE.
+ break;
+ default:
+ if (urlString) {
+ if (_peek() == TokenKind.LPAREN) {
+ _next(); // Skip the LPAREN.
+ start = _peekToken.start;
+ }
+ stopToken = TokenKind.RPAREN;
+ } else {
+ _error('unexpected string', _makeSpan(start));
+ }
+ break;
+ }
+
+ // Gobble up everything until we hit our stop token.
+ int runningStart = _peekToken.start;
+ while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) {
+ var tok = _next();
+ }
+
+ // All characters between quotes is the string.
+ int end = _peekToken.end;
+ var stringValue = (_peekToken.span as FileSpan).file.getText(start,
+ end - 1);
+
+ if (stopToken != TokenKind.RPAREN) {
+ _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE;
+ }
+
+ return stringValue;
+ }
+
+ // TODO(terry): Should probably understand IE's non-standard filter syntax to
+ // fully support calc, var(), etc.
+ /**
+ * IE's filter property breaks CSS value parsing. IE's format can be:
+ *
+ * filter: progid:DXImageTransform.MS.gradient(Type=0, Color='#9d8b83');
+ *
+ * We'll just parse everything after the 'progid:' look for the left paren
+ * then parse to the right paren ignoring everything in between.
+ */
+ processIEFilter(int startAfterProgidColon) {
+ int parens = 0;
+
+ while (_peek() != TokenKind.END_OF_FILE) {
+ switch (_peek()) {
+ case TokenKind.LPAREN:
+ _eat(TokenKind.LPAREN);
+ parens++;
+ break;
+ case TokenKind.RPAREN:
+ _eat(TokenKind.RPAREN);
+ if (--parens == 0) {
+ var tok = tokenizer.makeIEFilter(startAfterProgidColon,
+ _peekToken.start);
+ return new LiteralTerm(tok.text, tok.text, tok.span);
+ }
+ break;
+ default:
+ _eat(_peek());
+ }
+ }
+ }
+
+ // Function grammar:
+ //
+ // function: IDENT '(' expr ')'
+ //
+ processFunction(Identifier func) {
+ int start = _peekToken.start;
+
+ String name = func.name;
+
+ switch (name) {
+ case 'url':
+ // URI term sucks up everything inside of quotes(' or ") or between parens
+ String urlParam = processQuotedString(true);
+
+ // TODO(terry): Better error messge and checking for mismatched quotes.
+ if (_peek() == TokenKind.END_OF_FILE) {
+ _error("problem parsing URI", _peekToken.span);
+ }
+
+ if (_peek() == TokenKind.RPAREN) {
+ _next();
+ }
+
+ return new UriTerm(urlParam, _makeSpan(start));
+ case 'calc':
+ // TODO(terry): Implement expression handling...
+ break;
+ case 'var':
+ // TODO(terry): Consider handling var in IE specific filter/progid. This
+ // will require parsing entire IE specific syntax e.g.,
+ // param = value or progid:com_id, etc. for example:
+ //
+ // var-blur: Blur(Add = 0, Direction = 225, Strength = 10);
+ // var-gradient: progid:DXImageTransform.Microsoft.gradient"
+ // (GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
+ var expr = processExpr();
+ if (!_maybeEat(TokenKind.RPAREN)) {
+ _error("problem parsing var expected ), ", _peekToken.span);
+ }
+ if (isChecked &&
+ expr.expressions.where((e) => e is OperatorComma).length > 1) {
+ _error("too many parameters to var()", _peekToken.span);
+ }
+
+ var paramName = expr.expressions[0].text;
+
+ // [0] - var name, [1] - OperatorComma, [2] - default value.
+ var defaultValues = expr.expressions.length >= 3
+ ? expr.expressions.sublist(2) : [];
+ return new VarUsage(paramName, defaultValues, _makeSpan(start));
+ default:
+ var expr = processExpr();
+ if (!_maybeEat(TokenKind.RPAREN)) {
+ _error("problem parsing function expected ), ", _peekToken.span);
+ }
+
+ return new FunctionTerm(name, name, expr, _makeSpan(start));
+ }
+
+ return null;
+ }
+
+ identifier() {
+ var tok = _next();
+
+ if (!TokenKind.isIdentifier(tok.kind) &&
+ !TokenKind.isKindIdentifier(tok.kind)) {
+ if (isChecked) {
+ _warning('expected identifier, but found $tok', tok.span);
+ }
+ return new Identifier("", _makeSpan(tok.start));
+ }
+
+ return new Identifier(tok.text, _makeSpan(tok.start));
+ }
+
+ // TODO(terry): Move this to base <= 36 and into shared code.
+ static int _hexDigit(int c) {
+ if(c >= 48/*0*/ && c <= 57/*9*/) {
+ return c - 48;
+ } else if (c >= 97/*a*/ && c <= 102/*f*/) {
+ return c - 87;
+ } else if (c >= 65/*A*/ && c <= 70/*F*/) {
+ return c - 55;
+ } else {
+ return -1;
+ }
+ }
+
+ HexColorTerm _parseHex(String hexText, Span span) {
+ int hexValue = 0;
+
+ for (int i = 0; i < hexText.length; i++) {
+ var digit = _hexDigit(hexText.codeUnitAt(i));
+ if (digit < 0) {
+ _warning('Bad hex number', span);
+ return new HexColorTerm(new BAD_HEX_VALUE(), hexText, span);
+ }
+ hexValue = (hexValue << 4) + digit;
+ }
+
+ // Make 3 character hex value #RRGGBB => #RGB iff:
+ // high/low nibble of RR is the same, high/low nibble of GG is the same and
+ // high/low nibble of BB is the same.
+ if (hexText.length == 6 &&
+ hexText[0] == hexText[1] &&
+ hexText[2] == hexText[3] &&
+ hexText[4] == hexText[5]) {
+ hexText = '${hexText[0]}${hexText[2]}${hexText[4]}';
+ } else if (hexText.length == 4 &&
+ hexText[0] == hexText[1] &&
+ hexText[2] == hexText[3]) {
+ hexText = '${hexText[0]}${hexText[2]}';
+ } else if (hexText.length == 2 && hexText[0] == hexText[1]) {
+ hexText = '${hexText[0]}';
+ }
+ return new HexColorTerm(hexValue, hexText, span);
+ }
+}
+
+class ExpressionsProcessor {
+ final Expressions _exprs;
+ int _index = 0;
+
+ ExpressionsProcessor(this._exprs);
+
+ // TODO(terry): Only handles ##px unit.
+ processFontSize() {
+ /* font-size[/line-height]
+ *
+ * Possible size values:
+ * xx-small
+ * small
+ * medium [default]
+ * large
+ * x-large
+ * xx-large
+ * smaller
+ * larger
+ * ##length in px, pt, etc.
+ * ##%, percent of parent elem's font-size
+ * inherit
+ */
+ LengthTerm size;
+ LineHeight lineHt;
+ bool nextIsLineHeight = false;
+ for (; _index < _exprs.expressions.length; _index++) {
+ var expr = _exprs.expressions[_index];
+ if (size == null && expr is LengthTerm) {
+ // font-size part.
+ size = expr;
+ } else if (size != null) {
+ if (expr is OperatorSlash) {
+ // LineHeight could follow?
+ nextIsLineHeight = true;
+ } else if (nextIsLineHeight && expr is LengthTerm) {
+ assert(expr.unit == TokenKind.UNIT_LENGTH_PX);
+ lineHt = new LineHeight(expr.value, inPixels: true);
+ nextIsLineHeight = false;
+ _index++;
+ break;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ return new FontExpression(_exprs.span, size: size, lineHeight: lineHt);
+ }
+
+ processFontFamily() {
+ final List<String> family = <String>[];
+
+ /* Possible family values:
+ * font-family: arial, Times new roman ,Lucida Sans Unicode,Courier;
+ * font-family: "Times New Roman", arial, Lucida Sans Unicode, Courier;
+ */
+ bool moreFamilies = false;
+
+ for (; _index < _exprs.expressions.length; _index++) {
+ Expression expr = _exprs.expressions[_index];
+ if (expr is LiteralTerm) {
+ if (family.length == 0 || moreFamilies) {
+ // It's font-family now.
+ family.add(expr.toString());
+ moreFamilies = false;
+ } else if (isChecked) {
+ messages.warning('Only font-family can be a list', _exprs.span);
+ }
+ } else if (expr is OperatorComma && family.length > 0) {
+ moreFamilies = true;
+ } else {
+ break;
+ }
+ }
+
+ return new FontExpression(_exprs.span, family: family);
+ }
+
+ processFont() {
+ var family;
+
+ // Process all parts of the font expression.
+ FontExpression fontSize;
+ FontExpression fontFamily;
+ for (; _index < _exprs.expressions.length; _index++) {
+ var expr = _exprs.expressions[_index];
+ // Order is font-size font-family
+ if (fontSize == null) {
+ fontSize = processFontSize();
+ }
+ if (fontFamily == null) {
+ fontFamily = processFontFamily();
+ }
+ //TODO(terry): Handle font-weight, font-style, and font-variant. See
+ // https://github.com/dart-lang/csslib/issues/3
+ // https://github.com/dart-lang/csslib/issues/4
+ // https://github.com/dart-lang/csslib/issues/5
+ }
+
+ return new FontExpression(_exprs.span,
+ size: fontSize.font.size,
+ lineHeight: fontSize.font.lineHeight,
+ family: fontFamily.font.family);
+ }
+}
+
+/**
+ * Escapes [text] for use in a CSS string.
+ * [single] specifies single quote `'` vs double quote `"`.
+ */
+String _escapeString(String text, {bool single: false}) {
+ StringBuffer result = null;
+
+ for (int i = 0; i < text.length; i++) {
+ int code = text.codeUnitAt(i);
+ var replace = null;
+ switch (code) {
+ case 34/*'"'*/: if (!single) replace = r'\"'; break;
+ case 39/*"'"*/: if (single) replace = r"\'"; break;
+ }
+
+ if (replace != null && result == null) {
+ result = new StringBuffer(text.substring(0, i));
+ }
+
+ if (result != null) result.write(replace != null ? replace : text[i]);
+ }
+
+ return result == null ? text : result.toString();
+}
« no previous file with comments | « pkg/csslib/lib/css.dart ('k') | pkg/csslib/lib/src/analyzer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698