| Index: mojo/public/dart/third_party/csslib/lib/parser.dart
|
| diff --git a/mojo/public/dart/third_party/csslib/lib/parser.dart b/mojo/public/dart/third_party/csslib/lib/parser.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..86926938de2ad53ead4c047da78e4137974f0e28
|
| --- /dev/null
|
| +++ b/mojo/public/dart/third_party/csslib/lib/parser.dart
|
| @@ -0,0 +1,2695 @@
|
| +// 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_span/source_span.dart';
|
| +
|
| +import "visitor.dart";
|
| +import 'src/messages.dart';
|
| +import 'src/options.dart';
|
| +
|
| +export 'src/options.dart';
|
| +
|
| +part 'src/analyzer.dart';
|
| +part 'src/polyfill.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);
|
| +}
|
| +
|
| +// TODO(jmesserly): this should not be global
|
| +void _createMessages({List<Message> errors, PreprocessorOptions options}) {
|
| + if (errors == null) errors = [];
|
| +
|
| + if (options == null) {
|
| + options = new PreprocessorOptions(useColors: false, inputFile: 'memory');
|
| + }
|
| +
|
| + messages = new Messages(options: options, 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(input, {List<Message> errors, PreprocessorOptions options,
|
| + bool nested: true, bool polyfill: false, List<StyleSheet> includes: null}) {
|
| + if (includes == null) {
|
| + includes = [];
|
| + }
|
| +
|
| + var source = _inputAsString(input);
|
| +
|
| + _createMessages(errors: errors, options: options);
|
| +
|
| + var file = new SourceFile(source);
|
| +
|
| + var tree = new _Parser(file, source).parse();
|
| +
|
| + analyze([tree], errors: errors, options: options);
|
| +
|
| + if (polyfill) {
|
| + var processCss = new PolyFill(messages, true);
|
| + processCss.process(tree, includes: includes);
|
| + }
|
| +
|
| + return tree;
|
| +}
|
| +
|
| +/** Analyze the CSS file. */
|
| +void analyze(List<StyleSheet> styleSheets,
|
| + {List<Message> errors, PreprocessorOptions 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(input, {List<Message> errors, PreprocessorOptions options}) {
|
| + var source = _inputAsString(input);
|
| +
|
| + _createMessages(errors: errors, options: options);
|
| +
|
| + var file = new SourceFile(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].
|
| + */
|
| +// TODO(jmesserly): should rename "parseSelector" and return Selector
|
| +StyleSheet selector(input, {List<Message> errors}) {
|
| + var source = _inputAsString(input);
|
| +
|
| + _createMessages(errors: errors);
|
| +
|
| + var file = new SourceFile(source);
|
| + return (new _Parser(file, source)..tokenizer.inSelector = true)
|
| + .parseSelector();
|
| +}
|
| +
|
| +SelectorGroup parseSelectorGroup(input, {List<Message> errors}) {
|
| + var source = _inputAsString(input);
|
| +
|
| + _createMessages(errors: errors);
|
| +
|
| + var file = new SourceFile(source);
|
| + return (new _Parser(file, source)
|
| + // TODO(jmesserly): this fix should be applied to the parser. It's tricky
|
| + // because by the time the flag is set one token has already been fetched.
|
| + ..tokenizer.inSelector = true).processSelectorGroup();
|
| +}
|
| +
|
| +String _inputAsString(input) {
|
| + String source;
|
| +
|
| + if (input is String) {
|
| + source = input;
|
| + } else if (input is List) {
|
| + // 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 as List<int>);
|
| + } 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;
|
| +}
|
| +
|
| +// TODO(terry): Consider removing this class when all usages can be eliminated
|
| +// or replaced with compile API.
|
| +/** Public parsing interface for csslib. */
|
| +class Parser {
|
| + final _Parser _parser;
|
| +
|
| + // TODO(jmesserly): having file and text is redundant.
|
| + Parser(SourceFile file, String text, {int start: 0, String baseUrl})
|
| + : _parser = new _Parser(file, text, start: start, baseUrl: baseUrl);
|
| +
|
| + StyleSheet parse() => _parser.parse();
|
| +}
|
| +
|
| +/** A simple recursive descent parser for CSS. */
|
| +class _Parser {
|
| + final 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 = [];
|
| +
|
| + var start = _peekToken.span;
|
| + 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 = [];
|
| +
|
| + var start = _peekToken.span;
|
| + 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 _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, SourceSpan location) {
|
| + if (location == null) {
|
| + location = _peekToken.span;
|
| + }
|
| + messages.error(message, location);
|
| + }
|
| +
|
| + void _warning(String message, SourceSpan location) {
|
| + if (location == null) {
|
| + location = _peekToken.span;
|
| + }
|
| + messages.warning(message, location);
|
| + }
|
| +
|
| + SourceSpan _makeSpan(FileSpan start) {
|
| + // TODO(terry): there are places where we are creating spans before we eat
|
| + // the tokens, so using _previousToken is not always valid.
|
| + // TODO(nweiz): use < rather than compareTo when SourceSpan supports it.
|
| + if (_previousToken == null || _previousToken.span.compareTo(start) < 0) {
|
| + return start;
|
| + }
|
| + return start.expand(_previousToken.span);
|
| + }
|
| +
|
| + ///////////////////////////////////////////////////////////////////
|
| + // 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 ]*
|
| +
|
| + var start = _peekToken.span;
|
| +
|
| + // 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.span;
|
| + }
|
| +
|
| + 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]) {
|
| + var start = _peekToken.span;
|
| +
|
| + // Grammar: '(' S* media_feature S* [ ':' S* expr ]? ')' S*
|
| + if (_maybeEat(TokenKind.LPAREN)) {
|
| + if (_peekIdentifier()) {
|
| + var feature = identifier(); // Media feature.
|
| + while (_maybeEat(TokenKind.COLON)) {
|
| + var startExpr = _peekToken.span;
|
| + 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 '}'
|
| + * mixin: '@mixin name [(args,...)] '{' declarations/ruleset '}'
|
| + * include: '@include name [(@arg,@arg1)]
|
| + * '@include name [(@arg...)]
|
| + * content '@content'
|
| + */
|
| + processDirective() {
|
| + var start = _peekToken.span;
|
| +
|
| + var tokId = processVariableOrDirective();
|
| + if (tokId is VarDefinitionDirective) return tokId;
|
| + 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 null;
|
| + }
|
| + }
|
| + }
|
| +
|
| + 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.span;
|
| + 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));
|
| +
|
| + case TokenKind.DIRECTIVE_MIXIN:
|
| + return processMixin();
|
| +
|
| + case TokenKind.DIRECTIVE_INCLUDE:
|
| + return processInclude(_makeSpan(start));
|
| +
|
| + case TokenKind.DIRECTIVE_CONTENT:
|
| + // TODO(terry): TBD
|
| + _warning("@content not implemented.", _makeSpan(start));
|
| + return null;
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + /**
|
| + * Parse the mixin beginning token offset [start]. Returns a [MixinDefinition]
|
| + * node.
|
| + *
|
| + * Mixin grammar:
|
| + *
|
| + * @mixin IDENT [(args,...)] '{'
|
| + * [ruleset | property | directive]*
|
| + * '}'
|
| + */
|
| + MixinDefinition processMixin() {
|
| + _next();
|
| +
|
| + var name = identifier();
|
| +
|
| + List<VarDefinitionDirective> params = [];
|
| + // Any parameters?
|
| + if (_maybeEat(TokenKind.LPAREN)) {
|
| + var mustHaveParam = false;
|
| + var keepGoing = true;
|
| + while (keepGoing) {
|
| + var varDef = processVariableOrDirective(mixinParameter: true);
|
| + if (varDef is VarDefinitionDirective || varDef is VarDefinition) {
|
| + params.add(varDef);
|
| + } else if (mustHaveParam) {
|
| + _warning("Expecting parameter", _makeSpan(_peekToken.span));
|
| + keepGoing = false;
|
| + }
|
| + if (_maybeEat(TokenKind.COMMA)) {
|
| + mustHaveParam = true;
|
| + continue;
|
| + }
|
| + keepGoing = !_maybeEat(TokenKind.RPAREN);
|
| + }
|
| + }
|
| +
|
| + _eat(TokenKind.LBRACE);
|
| +
|
| + List<TreeNode> productions = [];
|
| + List<TreeNode> declarations = [];
|
| + var mixinDirective;
|
| +
|
| + var start = _peekToken.span;
|
| + while (!_maybeEat(TokenKind.END_OF_FILE)) {
|
| + var directive = processDirective();
|
| + if (directive != null) {
|
| + productions.add(directive);
|
| + continue;
|
| + }
|
| +
|
| + var declGroup = processDeclarations(checkBrace: false);
|
| + if (declGroup.declarations.any((decl) {
|
| + return decl is Declaration && decl is! IncludeMixinAtDeclaration;
|
| + })) {
|
| + var newDecls = [];
|
| + productions.forEach((include) {
|
| + // If declGroup has items that are declarations then we assume
|
| + // this mixin is a declaration mixin not a top-level mixin.
|
| + if (include is IncludeDirective) {
|
| + newDecls.add(new IncludeMixinAtDeclaration(include, include.span));
|
| + } else {
|
| + _warning("Error mixing of top-level vs declarations mixins",
|
| + _makeSpan(include.span));
|
| + }
|
| + });
|
| + declGroup.declarations.insertAll(0, newDecls);
|
| + productions = [];
|
| + } else {
|
| + // Declarations are just @includes make it a list of productions
|
| + // not a declaration group (anything else is a ruleset). Make it a
|
| + // list of productions, not a declaration group.
|
| + for (var decl in declGroup.declarations) {
|
| + productions
|
| + .add(decl is IncludeMixinAtDeclaration ? decl.include : decl);
|
| + }
|
| + ;
|
| + declGroup.declarations.clear();
|
| + }
|
| +
|
| + if (declGroup.declarations.isNotEmpty) {
|
| + if (productions.isEmpty) {
|
| + mixinDirective = new MixinDeclarationDirective(
|
| + name.name, params, false, declGroup, _makeSpan(start));
|
| + break;
|
| + } else {
|
| + for (var decl in declGroup.declarations) {
|
| + productions
|
| + .add(decl is IncludeMixinAtDeclaration ? decl.include : decl);
|
| + }
|
| + }
|
| + } else {
|
| + mixinDirective = new MixinRulesetDirective(
|
| + name.name, params, false, productions, _makeSpan(start));
|
| + break;
|
| + }
|
| + }
|
| +
|
| + if (productions.isNotEmpty) {
|
| + mixinDirective = new MixinRulesetDirective(
|
| + name.name, params, false, productions, _makeSpan(start));
|
| + }
|
| +
|
| + _eat(TokenKind.RBRACE);
|
| +
|
| + return mixinDirective;
|
| + }
|
| +
|
| + /**
|
| + * Returns a VarDefinitionDirective or VarDefinition if a varaible otherwise
|
| + * return the token id of a directive or -1 if neither.
|
| + */
|
| + processVariableOrDirective({bool mixinParameter: false}) {
|
| + var start = _peekToken.span;
|
| +
|
| + 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) {
|
| + _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();
|
| + }
|
| +
|
| + Expressions exprs;
|
| + if (mixinParameter && _maybeEat(TokenKind.COLON)) {
|
| + exprs = processExpr();
|
| + } else if (!mixinParameter) {
|
| + _eat(TokenKind.COLON);
|
| + exprs = processExpr();
|
| + }
|
| +
|
| + var span = _makeSpan(start);
|
| + return new VarDefinitionDirective(
|
| + new VarDefinition(name, exprs, span), span);
|
| + } else if (isChecked) {
|
| + _error('unexpected directive @$_peekToken', _peekToken.span);
|
| + }
|
| + }
|
| + } else if (mixinParameter && _peekToken.kind == TokenKind.VAR_DEFINITION) {
|
| + _next();
|
| + var definedName;
|
| + if (_peekIdentifier()) definedName = identifier();
|
| +
|
| + Expressions exprs;
|
| + if (_maybeEat(TokenKind.COLON)) {
|
| + exprs = processExpr();
|
| + }
|
| +
|
| + return new VarDefinition(definedName, exprs, _makeSpan(start));
|
| + }
|
| +
|
| + return tokId;
|
| + }
|
| +
|
| + IncludeDirective processInclude(SourceSpan span, {bool eatSemiColon: true}) {
|
| + /* Stylet grammar:
|
| + *
|
| + * @include IDENT [(args,...)];
|
| + */
|
| + _next();
|
| +
|
| + var name;
|
| + if (_peekIdentifier()) {
|
| + name = identifier();
|
| + }
|
| +
|
| + var params = [];
|
| +
|
| + // Any parameters? Parameters can be multiple terms per argument e.g.,
|
| + // 3px solid yellow, green is two parameters:
|
| + // 1. 3px solid yellow
|
| + // 2. green
|
| + // the first has 3 terms and the second has 1 term.
|
| + if (_maybeEat(TokenKind.LPAREN)) {
|
| + var terms = [];
|
| + var expr;
|
| + var keepGoing = true;
|
| + while (keepGoing && (expr = processTerm()) != null) {
|
| + // VarUsage is returns as a list
|
| + terms.add(expr is List ? expr[0] : expr);
|
| + keepGoing = !_peekKind(TokenKind.RPAREN);
|
| + if (keepGoing) {
|
| + if (_maybeEat(TokenKind.COMMA)) {
|
| + params.add(terms);
|
| + terms = [];
|
| + }
|
| + }
|
| + }
|
| + params.add(terms);
|
| + _maybeEat(TokenKind.RPAREN);
|
| + }
|
| +
|
| + if (eatSemiColon) {
|
| + _eat(TokenKind.SEMICOLON);
|
| + }
|
| +
|
| + return new IncludeDirective(name.name, params, span);
|
| + }
|
| +
|
| + 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;
|
| + }
|
| + }
|
| +
|
| + DeclarationGroup processDeclarations({bool checkBrace: true}) {
|
| + var start = _peekToken.span;
|
| +
|
| + 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 = [];
|
| +
|
| + var start = _peekToken.span;
|
| +
|
| + _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 = [];
|
| + var start = _peekToken.span;
|
| +
|
| + 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
|
| + */
|
| + Selector processSelector() {
|
| + var simpleSequences = <SimpleSelectorSequence>[];
|
| + var start = _peekToken.span;
|
| + 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.span;
|
| + 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): Natalie 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;
|
| + var start = _peekToken.span;
|
| + switch (_peek()) {
|
| + case TokenKind.ASTERISK:
|
| + // Mark as universal namespace.
|
| + var tok = _next();
|
| + first = new Wildcard(_makeSpan(tok.span));
|
| + 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.span));
|
| + 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
|
| + var start = _peekToken.span;
|
| + switch (_peek()) {
|
| + case TokenKind.HASH:
|
| + _eat(TokenKind.HASH);
|
| +
|
| + var 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
|
| + return processPseudoSelector(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;
|
| + }
|
| + }
|
| +
|
| + processPseudoSelector(FileSpan start) {
|
| + // :pseudo-class ::pseudo-element
|
| + // TODO(terry): '::' should be token.
|
| + _eat(TokenKind.COLON);
|
| + var 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 (_peekToken.kind == TokenKind.LPAREN) {
|
| + if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') {
|
| + _eat(TokenKind.LPAREN);
|
| +
|
| + // Negation : ':NOT(' S* negation_arg S* ')'
|
| + var negArg = simpleSelector();
|
| +
|
| + _eat(TokenKind.RPAREN);
|
| + return new NegationSelector(negArg, _makeSpan(start));
|
| + } else {
|
| + // Special parsing for expressions in pseudo functions. Minus is used
|
| + // as operator not identifier.
|
| + // TODO(jmesserly): we need to flip this before we eat the "(" as the
|
| + // next token will be fetched when we do that. I think we should try to
|
| + // refactor so we don't need this boolean; it seems fragile.
|
| + tokenizer.inSelectorExpression = true;
|
| + _eat(TokenKind.LPAREN);
|
| +
|
| + // Handle function expression.
|
| + var span = _makeSpan(start);
|
| + var expr = processSelectorExpression();
|
| +
|
| + tokenizer.inSelectorExpression = false;
|
| +
|
| + // 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));
|
| + }
|
| +
|
| + /**
|
| + * 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() {
|
| + var start = _peekToken.span;
|
| +
|
| + var expressions = [];
|
| +
|
| + Token termToken;
|
| + var value;
|
| +
|
| + var keepParsing = true;
|
| + while (keepParsing) {
|
| + switch (_peek()) {
|
| + case TokenKind.PLUS:
|
| + start = _peekToken.span;
|
| + termToken = _next();
|
| + expressions.add(new OperatorPlus(_makeSpan(start)));
|
| + break;
|
| + case TokenKind.MINUS:
|
| + start = _peekToken.span;
|
| + termToken = _next();
|
| + expressions.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:
|
| + value = processQuotedString(false);
|
| + value = "'${_escapeString(value, single: true)}'";
|
| + return new LiteralTerm(value, value, _makeSpan(start));
|
| + 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));
|
| + }
|
| + expressions.add(unitTerm);
|
| +
|
| + value = null;
|
| + }
|
| + }
|
| +
|
| + return new SelectorExpression(expressions, _makeSpan(start));
|
| + }
|
| +
|
| + // Attribute grammar:
|
| + //
|
| + // attributes :
|
| + // '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']'
|
| + //
|
| + // ATTRIB_MATCHES :
|
| + // [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ]
|
| + //
|
| + // INCLUDES: '~='
|
| + //
|
| + // DASHMATCH: '|='
|
| + //
|
| + // PREFIXMATCH: '^='
|
| + //
|
| + // SUFFIXMATCH: '$='
|
| + //
|
| + // SUBSTRMATCH: '*='
|
| + //
|
| + //
|
| + AttributeSelector processAttribute() {
|
| + var start = _peekToken.span;
|
| +
|
| + 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)
|
| + //
|
| + Declaration processDeclaration(List dartStyles) {
|
| + Declaration decl;
|
| +
|
| + var start = _peekToken.span;
|
| +
|
| + // 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));
|
| + } else if (_peekToken.kind == TokenKind.DIRECTIVE_INCLUDE) {
|
| + // @include mixinName in the declaration area.
|
| + var span = _makeSpan(start);
|
| + var include = processInclude(span, eatSemiColon: false);
|
| + decl = new IncludeMixinAtDeclaration(include, span);
|
| + } else if (_peekToken.kind == TokenKind.DIRECTIVE_EXTEND) {
|
| + var simpleSequences = <TreeNode>[];
|
| +
|
| + _next();
|
| + var span = _makeSpan(start);
|
| + var selector = simpleSelector();
|
| + if (selector == null) {
|
| + _warning("@extends expecting simple selector name", span);
|
| + } else {
|
| + simpleSequences.add(selector);
|
| + }
|
| + if (_peekKind(TokenKind.COLON)) {
|
| + var pseudoSelector = processPseudoSelector(_peekToken.span);
|
| + if (pseudoSelector is PseudoElementSelector ||
|
| + pseudoSelector is PseudoClassSelector) {
|
| + simpleSequences.add(pseudoSelector);
|
| + } else {
|
| + _warning("not a valid selector", span);
|
| + }
|
| + }
|
| + decl = new ExtendDeclaration(simpleSequences, span);
|
| + }
|
| +
|
| + 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 int _findStyle(String styleName) => _stylesToDart[styleName];
|
| +
|
| + DartStyleExpression _styleForDart(
|
| + Identifier property, Expressions exprs, List dartStyles) {
|
| + var 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;
|
| + }
|
| +
|
| + DartStyleExpression 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:
|
| + 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 null;
|
| + }
|
| + }
|
| +
|
| + // TODO(terry): Look at handling width of thin, thick, etc. any none numbers
|
| + // to convert to a number.
|
| + DartStyleExpression 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.
|
| + */
|
| + BoxEdge 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 null;
|
| + }
|
| +
|
| + 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)
|
| + //
|
| + Expressions processExpr([bool ieFilter = false]) {
|
| + var start = _peekToken.span;
|
| + var expressions = new Expressions(_makeSpan(start));
|
| +
|
| + var keepGoing = true;
|
| + var expr;
|
| + while (keepGoing && (expr = processTerm(ieFilter)) != null) {
|
| + var op;
|
| +
|
| + var opStart = _peekToken.span;
|
| +
|
| + 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.span;
|
| +
|
| + _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) {
|
| + if (expr is List) {
|
| + expr.forEach((exprItem) {
|
| + expressions.add(exprItem);
|
| + });
|
| + } else {
|
| + expressions.add(expr);
|
| + }
|
| + } else {
|
| + keepGoing = false;
|
| + }
|
| +
|
| + if (op != null) {
|
| + expressions.add(op);
|
| + if (op is IE8Term) {
|
| + keepGoing = false;
|
| + } else {
|
| + _next();
|
| + }
|
| + }
|
| + }
|
| +
|
| + return expressions;
|
| + }
|
| +
|
| + static const int MAX_UNICODE = 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]) {
|
| + var start = _peekToken.span;
|
| + 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:
|
| + value = processQuotedString(false);
|
| + value = "'${_escapeString(value, single: true)}'";
|
| + return new LiteralTerm(value, value, _makeSpan(start));
|
| + 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.
|
| + var 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];
|
| + var varUsage = new VarUsage(param.text, [], _makeSpan(start));
|
| + expr.expressions[0] = varUsage;
|
| + return expr.expressions;
|
| + }
|
| + break;
|
| + }
|
| +
|
| + return processDimension(t, value, _makeSpan(start));
|
| + }
|
| +
|
| + /** Process all dimension units. */
|
| + LiteralTerm processDimension(Token t, var value, SourceSpan span) {
|
| + LiteralTerm 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;
|
| + }
|
| +
|
| + String processQuotedString([bool urlString = false]) {
|
| + var start = _peekToken.span;
|
| +
|
| + // URI term sucks up everything inside of quotes(' or ") or between parens
|
| + var stopToken = urlString ? TokenKind.RPAREN : -1;
|
| +
|
| + // Note: disable skipping whitespace tokens inside a string.
|
| + // TODO(jmesserly): the layering here feels wrong.
|
| + var inString = tokenizer._inString;
|
| + tokenizer._inString = false;
|
| +
|
| + switch (_peek()) {
|
| + case TokenKind.SINGLE_QUOTE:
|
| + stopToken = TokenKind.SINGLE_QUOTE;
|
| + _next(); // Skip the SINGLE_QUOTE.
|
| + start = _peekToken.span;
|
| + break;
|
| + case TokenKind.DOUBLE_QUOTE:
|
| + stopToken = TokenKind.DOUBLE_QUOTE;
|
| + _next(); // Skip the DOUBLE_QUOTE.
|
| + start = _peekToken.span;
|
| + break;
|
| + default:
|
| + if (urlString) {
|
| + if (_peek() == TokenKind.LPAREN) {
|
| + _next(); // Skip the LPAREN.
|
| + start = _peekToken.span;
|
| + }
|
| + stopToken = TokenKind.RPAREN;
|
| + } else {
|
| + _error('unexpected string', _makeSpan(start));
|
| + }
|
| + break;
|
| + }
|
| +
|
| + // Gobble up everything until we hit our stop token.
|
| + var stringValue = new StringBuffer();
|
| + while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) {
|
| + stringValue.write(_next().text);
|
| + }
|
| +
|
| + tokenizer._inString = inString;
|
| +
|
| + // All characters between quotes is the string.
|
| + if (stopToken != TokenKind.RPAREN) {
|
| + _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE;
|
| + }
|
| +
|
| + return stringValue.toString();
|
| + }
|
| +
|
| + // 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(FileSpan startAfterProgidColon) {
|
| + var 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.start.offset, _peekToken.start);
|
| + return new LiteralTerm(tok.text, tok.text, tok.span);
|
| + }
|
| + break;
|
| + default:
|
| + _eat(_peek());
|
| + }
|
| + }
|
| + }
|
| +
|
| + // Function grammar:
|
| + //
|
| + // function: IDENT '(' expr ')'
|
| + //
|
| + processFunction(Identifier func) {
|
| + var start = _peekToken.span;
|
| +
|
| + var name = func.name;
|
| +
|
| + switch (name) {
|
| + case 'url':
|
| + // URI term sucks up everything inside of quotes(' or ") or between parens
|
| + var 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 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.span));
|
| + }
|
| +
|
| + return new Identifier(tok.text, _makeSpan(tok.span));
|
| + }
|
| +
|
| + // 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, SourceSpan span) {
|
| + var hexValue = 0;
|
| +
|
| + for (var 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.
|
| + FontExpression 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;
|
| + var 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);
|
| + }
|
| +
|
| + FontExpression processFontFamily() {
|
| + var 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;
|
| + */
|
| + var 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);
|
| + }
|
| +
|
| + FontExpression processFont() {
|
| + // Process all parts of the font expression.
|
| + FontExpression fontSize;
|
| + FontExpression fontFamily;
|
| + for (; _index < _exprs.expressions.length; _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++) {
|
| + var code = text.codeUnitAt(i);
|
| + String 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();
|
| +}
|
|
|