| Index: packages/csslib/lib/parser.dart
|
| diff --git a/packages/csslib/lib/parser.dart b/packages/csslib/lib/parser.dart
|
| index b3a22d6752a187fe5c15717c86c5d8872832b386..cd4f6c00b56e40627ef40ab84b7e42900643ba89 100644
|
| --- a/packages/csslib/lib/parser.dart
|
| +++ b/packages/csslib/lib/parser.dart
|
| @@ -8,10 +8,11 @@ import 'dart:math' as math;
|
|
|
| import 'package:source_span/source_span.dart';
|
|
|
| -import "visitor.dart";
|
| +import 'visitor.dart';
|
| import 'src/messages.dart';
|
| import 'src/options.dart';
|
|
|
| +export 'src/messages.dart' show Message;
|
| export 'src/options.dart';
|
|
|
| part 'src/analyzer.dart';
|
| @@ -22,6 +23,12 @@ part 'src/tokenizer_base.dart';
|
| part 'src/tokenizer.dart';
|
| part 'src/tokenkind.dart';
|
|
|
| +enum ClauseType {
|
| + none,
|
| + conjunction,
|
| + disjunction,
|
| +}
|
| +
|
| /** Used for parser lookup ahead (used for nested selectors Less support). */
|
| class ParserState extends TokenizerState {
|
| final Token peekToken;
|
| @@ -47,8 +54,12 @@ 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}) {
|
| +StyleSheet compile(input,
|
| + {List<Message> errors,
|
| + PreprocessorOptions options,
|
| + bool nested: true,
|
| + bool polyfill: false,
|
| + List<StyleSheet> includes: null}) {
|
| if (includes == null) {
|
| includes = [];
|
| }
|
| @@ -115,9 +126,10 @@ SelectorGroup parseSelectorGroup(input, {List<Message> 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();
|
| + // 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) {
|
| @@ -158,19 +170,25 @@ class Parser {
|
| final _Parser _parser;
|
|
|
| // TODO(jmesserly): having file and text is redundant.
|
| + // TODO(rnystrom): baseUrl isn't used. Remove from API.
|
| Parser(SourceFile file, String text, {int start: 0, String baseUrl})
|
| - : _parser = new _Parser(file, text, start: start, baseUrl: baseUrl);
|
| + : _parser = new _Parser(file, text, start: start);
|
|
|
| StyleSheet parse() => _parser.parse();
|
| }
|
|
|
| +// CSS2.1 pseudo-elements which were defined with a single ':'.
|
| +final _legacyPseudoElements = new Set<String>.from(const [
|
| + 'after',
|
| + 'before',
|
| + 'first-letter',
|
| + 'first-line',
|
| +]);
|
| +
|
| /** 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.
|
| @@ -180,9 +198,8 @@ class _Parser {
|
| Token _previousToken;
|
| Token _peekToken;
|
|
|
| - _Parser(SourceFile file, String text, {int start: 0, String baseUrl})
|
| + _Parser(SourceFile file, String text, {int start: 0})
|
| : this.file = file,
|
| - _baseUrl = baseUrl,
|
| tokenizer = new Tokenizer(file, text, true, start) {
|
| _peekToken = tokenizer.next();
|
| }
|
| @@ -356,29 +373,21 @@ class _Parser {
|
| * : IDENT
|
| */
|
| List<MediaQuery> processMediaQueryList() {
|
| - var mediaQueries = [];
|
| + var mediaQueries = <MediaQuery>[];
|
|
|
| - bool firstTime = true;
|
| - var mediaQuery;
|
| do {
|
| - mediaQuery = processMediaQuery(firstTime == true);
|
| + var mediaQuery = processMediaQuery();
|
| if (mediaQuery != null) {
|
| mediaQueries.add(mediaQuery);
|
| - firstTime = false;
|
| - continue;
|
| + } else {
|
| + break;
|
| }
|
| -
|
| - // 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);
|
| + } while (_maybeEat(TokenKind.COMMA));
|
|
|
| return mediaQueries;
|
| }
|
|
|
| - MediaQuery processMediaQuery([bool startQuery = true]) {
|
| + MediaQuery processMediaQuery() {
|
| // Grammar: [ONLY | NOT]? S* media_type S*
|
| // [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]*
|
|
|
| @@ -390,41 +399,39 @@ class _Parser {
|
| var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen);
|
| if (unaryOp != -1) {
|
| if (isChecked) {
|
| - if (startQuery && unaryOp != TokenKind.MEDIA_OP_NOT ||
|
| + if (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 = [];
|
| + // Get the media type.
|
| + if (_peekIdentifier()) type = identifier();
|
|
|
| - if (unaryOp == -1 || unaryOp == TokenKind.MEDIA_OP_AND) {
|
| - var andOp = false;
|
| - while (true) {
|
| - var expr = processMediaExpression(andOp);
|
| - if (expr == null) break;
|
| + var exprs = <MediaExpression>[];
|
|
|
| - exprs.add(expr);
|
| + while (true) {
|
| + // Parse AND if query has a media_type or previous expression.
|
| + var andOp = exprs.isNotEmpty || type != null;
|
| + if (andOp) {
|
| op = _peekToken.text;
|
| opLen = op.length;
|
| - andOp = TokenKind.matchMediaOperator(op, 0, opLen) ==
|
| - TokenKind.MEDIA_OP_AND;
|
| - if (!andOp) break;
|
| + if (TokenKind.matchMediaOperator(op, 0, opLen) !=
|
| + TokenKind.MEDIA_OP_AND) {
|
| + break;
|
| + }
|
| _next();
|
| }
|
| +
|
| + var expr = processMediaExpression(andOp);
|
| + if (expr == null) break;
|
| +
|
| + exprs.add(expr);
|
| }
|
|
|
| if (unaryOp != -1 || type != null || exprs.length > 0) {
|
| @@ -440,17 +447,16 @@ class _Parser {
|
| 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;
|
| - }
|
| + var exprs = _maybeEat(TokenKind.COLON)
|
| + ? processExpr()
|
| + : new Expressions(_makeSpan(_peekToken.span));
|
| + if (_maybeEat(TokenKind.RPAREN)) {
|
| + return new MediaExpression(
|
| + andOperator, feature, exprs, _makeSpan(start));
|
| + } 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));
|
| @@ -474,7 +480,12 @@ class _Parser {
|
| * mixin: '@mixin name [(args,...)] '{' declarations/ruleset '}'
|
| * include: '@include name [(@arg,@arg1)]
|
| * '@include name [(@arg...)]
|
| - * content '@content'
|
| + * content: '@content'
|
| + * -moz-document: '@-moz-document' [ <url> | url-prefix(<string>) |
|
| + * domain(<string>) | regexp(<string) ]# '{'
|
| + * declarations
|
| + * '}'
|
| + * supports: '@supports' supports_condition group_rule_body
|
| */
|
| processDirective() {
|
| var start = _peekToken.span;
|
| @@ -751,11 +762,17 @@ class _Parser {
|
|
|
| case TokenKind.DIRECTIVE_INCLUDE:
|
| return processInclude(_makeSpan(start));
|
| -
|
| case TokenKind.DIRECTIVE_CONTENT:
|
| // TODO(terry): TBD
|
| _warning("@content not implemented.", _makeSpan(start));
|
| return null;
|
| + case TokenKind.DIRECTIVE_MOZ_DOCUMENT:
|
| + return processDocumentDirective();
|
| + case TokenKind.DIRECTIVE_SUPPORTS:
|
| + return processSupportsDirective();
|
| + case TokenKind.DIRECTIVE_VIEWPORT:
|
| + case TokenKind.DIRECTIVE_MS_VIEWPORT:
|
| + return processViewportDirective();
|
| }
|
| return null;
|
| }
|
| @@ -775,7 +792,7 @@ class _Parser {
|
|
|
| var name = identifier();
|
|
|
| - List<VarDefinitionDirective> params = [];
|
| + var params = <TreeNode>[];
|
| // Any parameters?
|
| if (_maybeEat(TokenKind.LPAREN)) {
|
| var mustHaveParam = false;
|
| @@ -813,7 +830,7 @@ class _Parser {
|
| if (declGroup.declarations.any((decl) {
|
| return decl is Declaration && decl is! IncludeMixinAtDeclaration;
|
| })) {
|
| - var newDecls = [];
|
| + var newDecls = <Declaration>[];
|
| productions.forEach((include) {
|
| // If declGroup has items that are declarations then we assume
|
| // this mixin is a declaration mixin not a top-level mixin.
|
| @@ -943,7 +960,7 @@ class _Parser {
|
| name = identifier();
|
| }
|
|
|
| - var params = [];
|
| + var params = <List<Expression>>[];
|
|
|
| // Any parameters? Parameters can be multiple terms per argument e.g.,
|
| // 3px solid yellow, green is two parameters:
|
| @@ -951,7 +968,7 @@ class _Parser {
|
| // 2. green
|
| // the first has 3 terms and the second has 1 term.
|
| if (_maybeEat(TokenKind.LPAREN)) {
|
| - var terms = [];
|
| + var terms = <Expression>[];
|
| var expr;
|
| var keepGoing = true;
|
| while (keepGoing && (expr = processTerm()) != null) {
|
| @@ -976,6 +993,134 @@ class _Parser {
|
| return new IncludeDirective(name.name, params, span);
|
| }
|
|
|
| + DocumentDirective processDocumentDirective() {
|
| + var start = _peekToken.span;
|
| + _next(); // '@-moz-document'
|
| + var functions = <LiteralTerm>[];
|
| + do {
|
| + var function;
|
| +
|
| + // Consume function token: IDENT '('
|
| + var ident = identifier();
|
| + _eat(TokenKind.LPAREN);
|
| +
|
| + // Consume function arguments.
|
| + if (ident.name == 'url-prefix' || ident.name == 'domain') {
|
| + // @-moz-document allows the 'url-prefix' and 'domain' functions to
|
| + // omit quotations around their argument, contrary to the standard
|
| + // in which they must be strings. To support this we consume a
|
| + // string with optional quotation marks, then reapply quotation
|
| + // marks so they're present in the emitted CSS.
|
| + var argumentStart = _peekToken.span;
|
| + var value = processQuotedString(true);
|
| + // Don't quote the argument if it's empty. '@-moz-document url-prefix()'
|
| + // is a common pattern used for browser detection.
|
| + var argument = value.isNotEmpty ? '"$value"' : '';
|
| + var argumentSpan = _makeSpan(argumentStart);
|
| +
|
| + _eat(TokenKind.RPAREN);
|
| +
|
| + var arguments = new Expressions(_makeSpan(argumentSpan))
|
| + ..add(new LiteralTerm(argument, argument, argumentSpan));
|
| + function = new FunctionTerm(
|
| + ident.name, ident.name, arguments, _makeSpan(ident.span));
|
| + } else {
|
| + function = processFunction(ident);
|
| + }
|
| +
|
| + functions.add(function);
|
| + } while (_maybeEat(TokenKind.COMMA));
|
| +
|
| + _eat(TokenKind.LBRACE);
|
| + var groupRuleBody = processGroupRuleBody();
|
| + _eat(TokenKind.RBRACE);
|
| + return new DocumentDirective(functions, groupRuleBody, _makeSpan(start));
|
| + }
|
| +
|
| + SupportsDirective processSupportsDirective() {
|
| + var start = _peekToken.span;
|
| + _next(); // '@supports'
|
| + var condition = processSupportsCondition();
|
| + _eat(TokenKind.LBRACE);
|
| + var groupRuleBody = processGroupRuleBody();
|
| + _eat(TokenKind.RBRACE);
|
| + return new SupportsDirective(condition, groupRuleBody, _makeSpan(start));
|
| + }
|
| +
|
| + SupportsCondition processSupportsCondition() {
|
| + if (_peekKind(TokenKind.IDENTIFIER)) {
|
| + return processSupportsNegation();
|
| + }
|
| +
|
| + var start = _peekToken.span;
|
| + var conditions = <SupportsConditionInParens>[];
|
| + var clauseType = ClauseType.none;
|
| +
|
| + while (true) {
|
| + conditions.add(processSupportsConditionInParens());
|
| +
|
| + var type;
|
| + var text = _peekToken.text.toLowerCase();
|
| +
|
| + if (text == 'and') {
|
| + type = ClauseType.conjunction;
|
| + } else if (text == 'or') {
|
| + type = ClauseType.disjunction;
|
| + } else {
|
| + break; // Done parsing clause.
|
| + }
|
| +
|
| + if (clauseType == ClauseType.none) {
|
| + clauseType = type; // First operand and operator of clause.
|
| + } else if (clauseType != type) {
|
| + _error("Operators can't be mixed without a layer of parentheses",
|
| + _peekToken.span);
|
| + break;
|
| + }
|
| +
|
| + _next(); // Consume operator.
|
| + }
|
| +
|
| + if (clauseType == ClauseType.conjunction) {
|
| + return new SupportsConjunction(conditions, _makeSpan(start));
|
| + } else if (clauseType == ClauseType.disjunction) {
|
| + return new SupportsDisjunction(conditions, _makeSpan(start));
|
| + } else {
|
| + return conditions.first;
|
| + }
|
| + }
|
| +
|
| + SupportsNegation processSupportsNegation() {
|
| + var start = _peekToken.span;
|
| + var text = _peekToken.text.toLowerCase();
|
| + if (text != 'not') return null;
|
| + _next(); // 'not'
|
| + var condition = processSupportsConditionInParens();
|
| + return new SupportsNegation(condition, _makeSpan(start));
|
| + }
|
| +
|
| + SupportsConditionInParens processSupportsConditionInParens() {
|
| + var start = _peekToken.span;
|
| + _eat(TokenKind.LPAREN);
|
| + // Try to parse a condition.
|
| + var condition = processSupportsCondition();
|
| + if (condition != null) {
|
| + _eat(TokenKind.RPAREN);
|
| + return new SupportsConditionInParens.nested(condition, _makeSpan(start));
|
| + }
|
| + // Otherwise, parse a declaration.
|
| + var declaration = processDeclaration([]);
|
| + _eat(TokenKind.RPAREN);
|
| + return new SupportsConditionInParens(declaration, _makeSpan(start));
|
| + }
|
| +
|
| + ViewportDirective processViewportDirective() {
|
| + var start = _peekToken.span;
|
| + var name = _next().text;
|
| + var declarations = processDeclarations();
|
| + return new ViewportDirective(name, declarations, _makeSpan(start));
|
| + }
|
| +
|
| RuleSet processRuleSet([SelectorGroup selectorGroup]) {
|
| if (selectorGroup == null) {
|
| selectorGroup = processSelectorGroup();
|
| @@ -987,6 +1132,24 @@ class _Parser {
|
| return null;
|
| }
|
|
|
| + List<TreeNode> processGroupRuleBody() {
|
| + var nodes = <TreeNode>[];
|
| + while (!(_peekKind(TokenKind.RBRACE) || _peekKind(TokenKind.END_OF_FILE))) {
|
| + var directive = processDirective();
|
| + if (directive != null) {
|
| + nodes.add(directive);
|
| + continue;
|
| + }
|
| + var ruleSet = processRuleSet();
|
| + if (ruleSet != null) {
|
| + nodes.add(ruleSet);
|
| + continue;
|
| + }
|
| + break;
|
| + }
|
| + return nodes;
|
| + }
|
| +
|
| /**
|
| * 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'
|
| @@ -1041,8 +1204,8 @@ class _Parser {
|
|
|
| if (checkBrace) _eat(TokenKind.LBRACE);
|
|
|
| - List decls = [];
|
| - List dartStyles = []; // List of latest styles exposed to Dart.
|
| + var decls = <TreeNode>[];
|
| + var dartStyles = []; // List of latest styles exposed to Dart.
|
|
|
| do {
|
| var selectorGroup = _nestedSelector();
|
| @@ -1093,7 +1256,7 @@ class _Parser {
|
| }
|
|
|
| List<DeclarationGroup> processMarginsDeclarations() {
|
| - List groups = [];
|
| + var groups = <DeclarationGroup>[];
|
|
|
| var start = _peekToken.span;
|
|
|
| @@ -1203,7 +1366,7 @@ class _Parser {
|
| var start = _peekToken.span;
|
| while (true) {
|
| // First item is never descendant make sure it's COMBINATOR_NONE.
|
| - var selectorItem = simpleSelectorSequence(simpleSequences.length == 0);
|
| + var selectorItem = simpleSelectorSequence(simpleSequences.isEmpty);
|
| if (selectorItem != null) {
|
| simpleSequences.add(selectorItem);
|
| } else {
|
| @@ -1211,9 +1374,23 @@ class _Parser {
|
| }
|
| }
|
|
|
| - if (simpleSequences.length > 0) {
|
| - return new Selector(simpleSequences, _makeSpan(start));
|
| - }
|
| + if (simpleSequences.isEmpty) return null;
|
| +
|
| + return new Selector(simpleSequences, _makeSpan(start));
|
| + }
|
| +
|
| + /// Same as [processSelector] but reports an error for each combinator.
|
| + ///
|
| + /// This is a quick fix for parsing <compound-selectors> until the parser
|
| + /// supports Selector Level 4 grammar:
|
| + /// https://drafts.csswg.org/selectors-4/#typedef-compound-selector
|
| + Selector processCompoundSelector() {
|
| + return processSelector()
|
| + ..simpleSelectorSequences.forEach((sequence) {
|
| + if (!sequence.isCombinatorNone) {
|
| + _error('compound selector can not contain combinator', sequence.span);
|
| + }
|
| + });
|
| }
|
|
|
| simpleSelectorSequence(bool forceCombinatorNone) {
|
| @@ -1227,13 +1404,30 @@ class _Parser {
|
| combinatorType = TokenKind.COMBINATOR_PLUS;
|
| break;
|
| case TokenKind.GREATER:
|
| + // Parse > or >>>
|
| _eat(TokenKind.GREATER);
|
| - combinatorType = TokenKind.COMBINATOR_GREATER;
|
| + if (_maybeEat(TokenKind.GREATER)) {
|
| + _eat(TokenKind.GREATER);
|
| + combinatorType = TokenKind.COMBINATOR_SHADOW_PIERCING_DESCENDANT;
|
| + } else {
|
| + combinatorType = TokenKind.COMBINATOR_GREATER;
|
| + }
|
| break;
|
| case TokenKind.TILDE:
|
| _eat(TokenKind.TILDE);
|
| combinatorType = TokenKind.COMBINATOR_TILDE;
|
| break;
|
| + case TokenKind.SLASH:
|
| + // Parse /deep/
|
| + _eat(TokenKind.SLASH);
|
| + var ate = _maybeEat(TokenKind.IDENTIFIER);
|
| + var tok = ate ? _previousToken : _peekToken;
|
| + if (!(ate && tok.text == 'deep')) {
|
| + _error('expected deep, but found ${tok.text}', tok.span);
|
| + }
|
| + _eat(TokenKind.SLASH);
|
| + combinatorType = TokenKind.COMBINATOR_DEEP;
|
| + break;
|
| case TokenKind.AMPERSAND:
|
| _eat(TokenKind.AMPERSAND);
|
| thisOperator = true;
|
| @@ -1422,11 +1616,11 @@ class _Parser {
|
| } else {
|
| return null;
|
| }
|
| + var name = pseudoName.name.toLowerCase();
|
|
|
| // Functional pseudo?
|
| -
|
| if (_peekToken.kind == TokenKind.LPAREN) {
|
| - if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') {
|
| + if (!pseudoElement && name == 'not') {
|
| _eat(TokenKind.LPAREN);
|
|
|
| // Negation : ':NOT(' S* negation_arg S* ')'
|
| @@ -1434,6 +1628,12 @@ class _Parser {
|
|
|
| _eat(TokenKind.RPAREN);
|
| return new NegationSelector(negArg, _makeSpan(start));
|
| + } else if (!pseudoElement && (name == 'host' || name == 'host-context')) {
|
| + _eat(TokenKind.LPAREN);
|
| + var selector = processCompoundSelector();
|
| + _eat(TokenKind.RPAREN);
|
| + var span = _makeSpan(start);
|
| + return new PseudoClassFunctionSelector(pseudoName, selector, span);
|
| } else {
|
| // Special parsing for expressions in pseudo functions. Minus is used
|
| // as operator not identifier.
|
| @@ -1463,14 +1663,11 @@ class _Parser {
|
| }
|
| }
|
|
|
| - // 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))
|
| + // Treat CSS2.1 pseudo-elements defined with pseudo class syntax as pseudo-
|
| + // elements for backwards compatibility.
|
| + return pseudoElement || _legacyPseudoElements.contains(name)
|
| + ? new PseudoElementSelector(pseudoName, _makeSpan(start),
|
| + isLegacy: !pseudoElement)
|
| : new PseudoClassSelector(pseudoName, _makeSpan(start));
|
| }
|
|
|
| @@ -1487,7 +1684,7 @@ class _Parser {
|
| processSelectorExpression() {
|
| var start = _peekToken.span;
|
|
|
| - var expressions = [];
|
| + var expressions = <Expression>[];
|
|
|
| Token termToken;
|
| var value;
|
| @@ -2123,7 +2320,8 @@ class _Parser {
|
| if (_peekKind(TokenKind.INTEGER)) {
|
| String hexText1 = _peekToken.text;
|
| _next();
|
| - if (_peekIdentifier()) {
|
| + // Append identifier only if there's no delimiting whitespace.
|
| + if (_peekIdentifier() && _previousToken.end == _peekToken.start) {
|
| hexText = '$hexText1${identifier().name}';
|
| } else {
|
| hexText = hexText1;
|
| @@ -2269,14 +2467,15 @@ class _Parser {
|
| }
|
|
|
| var param = expr.expressions[0];
|
| - var varUsage = new VarUsage(param.text, [], _makeSpan(start));
|
| + var varUsage =
|
| + new VarUsage((param as LiteralTerm).text, [], _makeSpan(start));
|
| expr.expressions[0] = varUsage;
|
| return expr.expressions;
|
| }
|
| break;
|
| }
|
|
|
| - return processDimension(t, value, _makeSpan(start));
|
| + return t != null ? processDimension(t, value, _makeSpan(start)) : null;
|
| }
|
|
|
| /** Process all dimension units. */
|
| @@ -2422,8 +2621,15 @@ class _Parser {
|
| * then parse to the right paren ignoring everything in between.
|
| */
|
| processIEFilter(FileSpan startAfterProgidColon) {
|
| - var parens = 0;
|
| + // Support non-functional filters (i.e. filter: FlipH)
|
| + var kind = _peek();
|
| + if (kind == TokenKind.SEMICOLON || kind == TokenKind.RBRACE) {
|
| + var tok = tokenizer.makeIEFilter(
|
| + startAfterProgidColon.start.offset, _peekToken.start);
|
| + return new LiteralTerm(tok.text, tok.text, tok.span);
|
| + }
|
|
|
| + var parens = 0;
|
| while (_peek() != TokenKind.END_OF_FILE) {
|
| switch (_peek()) {
|
| case TokenKind.LPAREN:
|
| @@ -2467,8 +2673,7 @@ class _Parser {
|
| var token = _peek();
|
| if (token == TokenKind.LPAREN)
|
| left++;
|
| - else if (token == TokenKind.RPAREN)
|
| - left--;
|
| + else if (token == TokenKind.RPAREN) left--;
|
|
|
| matchingParens = left == 0;
|
| if (!matchingParens) stringValue.write(_next().text);
|
| @@ -2487,7 +2692,7 @@ class _Parser {
|
| var start = _peekToken.span;
|
|
|
| var name = func.name;
|
| - if (name == 'calc') {
|
| + if (name == 'calc' || name == '-webkit-calc' || name == '-moz-calc') {
|
| // TODO(terry): Implement expression parsing properly.
|
| String expression = processCalcExpression();
|
| var calcExpr = new LiteralTerm(expression, expression, _makeSpan(start));
|
| @@ -2508,7 +2713,6 @@ class _Parser {
|
| //
|
| processFunction(Identifier func) {
|
| var start = _peekToken.span;
|
| -
|
| var name = func.name;
|
|
|
| switch (name) {
|
| @@ -2516,7 +2720,7 @@ class _Parser {
|
| // 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.
|
| + // TODO(terry): Better error message and checking for mismatched quotes.
|
| if (_peek() == TokenKind.END_OF_FILE) {
|
| _error("problem parsing URI", _peekToken.span);
|
| }
|
| @@ -2543,11 +2747,12 @@ class _Parser {
|
| _error("too many parameters to var()", _peekToken.span);
|
| }
|
|
|
| - var paramName = expr.expressions[0].text;
|
| + var paramName = (expr.expressions[0] as LiteralTerm).text;
|
|
|
| // [0] - var name, [1] - OperatorComma, [2] - default value.
|
| - var defaultValues =
|
| - expr.expressions.length >= 3 ? expr.expressions.sublist(2) : [];
|
| + var defaultValues = expr.expressions.length >= 3
|
| + ? expr.expressions.sublist(2)
|
| + : <Expression>[];
|
| return new VarUsage(paramName, defaultValues, _makeSpan(start));
|
| default:
|
| var expr = processExpr();
|
| @@ -2557,8 +2762,6 @@ class _Parser {
|
|
|
| return new FunctionTerm(name, name, expr, _makeSpan(start));
|
| }
|
| -
|
| - return null;
|
| }
|
|
|
| Identifier identifier() {
|
|
|