blob: d0b8aea1fd35a73ae29c0414aca0022bf60b7b6b [file] [log] [blame]
// 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 'src/messages.dart';
import 'src/options.dart';
import 'visitor.dart';
export 'src/messages.dart' show Message;
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';
enum ClauseType {
none,
conjunction,
disjunction,
}
/** 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.fromString(source);
var tree = new _Parser(file, source).parse();
analyze([tree], errors: errors, options: options);
if (polyfill) {
var processCss = new PolyFill(messages);
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.fromString(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.fromString(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.fromString(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.
// 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);
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;
/**
* 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})
: this.file = file,
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.
final rule = processRule();
if (rule != null) {
productions.add(rule);
} 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 = <MediaQuery>[];
do {
var mediaQuery = processMediaQuery();
if (mediaQuery != null) {
mediaQueries.add(mediaQuery);
} else {
break;
}
} while (_maybeEat(TokenKind.COMMA));
return mediaQueries;
}
MediaQuery processMediaQuery() {
// 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 (unaryOp != TokenKind.MEDIA_OP_NOT ||
unaryOp != TokenKind.MEDIA_OP_ONLY) {
_warning("Only the unary operators NOT and ONLY allowed",
_makeSpan(start));
}
}
_next();
start = _peekToken.span;
}
var type;
// Get the media type.
if (_peekIdentifier()) type = identifier();
var exprs = <MediaExpression>[];
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;
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) {
return new MediaQuery(unaryOp, type, exprs, _makeSpan(start));
}
return null;
}
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.
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));
}
}
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'
* -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;
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> rules = [];
if (_maybeEat(TokenKind.LBRACE)) {
while (!_maybeEat(TokenKind.END_OF_FILE)) {
final rule = processRule();
if (rule == null) break;
rules.add(rule);
}
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, rules, _makeSpan(start));
case TokenKind.DIRECTIVE_HOST:
_next();
List<TreeNode> rules = [];
if (_maybeEat(TokenKind.LBRACE)) {
while (!_maybeEat(TokenKind.END_OF_FILE)) {
final rule = processRule();
if (rule == null) break;
rules.add(rule);
}
if (!_maybeEat(TokenKind.RBRACE)) {
_error('expected } after ruleset for @host', _peekToken.span);
}
} else {
_error('expected { after host before ruleset', _peekToken.span);
}
return new HostDirective(rules, _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)) {
final rule = processRule();
if (rule == null) {
break;
}
productions.add(rule);
}
_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;
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;
}
/**
* 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();
var params = <TreeNode>[];
// 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 = [];
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 = <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.
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 = <List<Expression>>[];
// 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 = <Expression>[];
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);
}
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));
}
TreeNode processRule([SelectorGroup selectorGroup]) {
if (selectorGroup == null) {
final directive = processDirective();
if (directive != null) {
_maybeEat(TokenKind.SEMICOLON);
return directive;
}
selectorGroup = processSelectorGroup();
}
if (selectorGroup != null) {
return new RuleSet(
selectorGroup, processDeclarations(), selectorGroup.span);
}
return null;
}
List<TreeNode> processGroupRuleBody() {
var nodes = <TreeNode>[];
while (!(_peekKind(TokenKind.RBRACE) || _peekKind(TokenKind.END_OF_FILE))) {
var rule = processRule();
if (rule != null) {
nodes.add(rule);
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'
* 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);
var decls = <TreeNode>[];
var dartStyles = []; // List of latest styles exposed to Dart.
do {
var selectorGroup = _nestedSelector();
while (selectorGroup != null) {
// Nested selector so process as a ruleset.
var ruleset = processRule(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() {
var groups = <DeclarationGroup>[];
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 null;
}
/**
* 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.isEmpty);
if (selectorItem != null) {
simpleSequences.add(selectorItem);
} else {
break;
}
}
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() {
var selector = processSelector();
if (selector != null) {
for (var sequence in selector.simpleSelectorSequences) {
if (!sequence.isCombinatorNone) {
_error('compound selector can not contain combinator', sequence.span);
}
}
}
return selector;
}
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:
// Parse > or >>>
_eat(TokenKind.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;
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;
}
var name = pseudoName.name.toLowerCase();
// Functional pseudo?
if (_peekToken.kind == TokenKind.LPAREN) {
if (!pseudoElement && name == 'not') {
_eat(TokenKind.LPAREN);
// Negation : ':NOT(' S* negation_arg S* ')'
var negArg = simpleSelector();
_eat(TokenKind.RPAREN);
return new NegationSelector(negArg, _makeSpan(start));
} else if (!pseudoElement && (name == 'host' || name == 'host-context')) {
_eat(TokenKind.LPAREN);
var selector = processCompoundSelector();
if (selector == null) {
_errorExpected('a selector argument');
return null;
}
_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.
// 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);
}
}
// 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));
}
/**
* 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 = <Expression>[];
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));
}
return null;
}
// 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);
}
return null;
}
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;
}
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);
}
}
return null;
}
/**
* 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();
// Append identifier only if there's no delimiting whitespace.
if (_peekIdentifier() && _previousToken.end == _peekToken.start) {
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)) {
var calc = processCalc(nameValue);
if (calc != null) return calc;
// 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 as LiteralTerm).text, [], _makeSpan(start));
expr.expressions[0] = varUsage;
return expr.expressions;
}
break;
}
return t != null ? processDimension(t, value, _makeSpan(start)) : null;
}
/** 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) {
// 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:
_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());
}
}
}
// TODO(terry): Hack to gobble up the calc expression as a string looking
// for the matching RPAREN the expression is not parsed into the
// AST.
//
// grammar should be:
//
// <calc()> = calc( <calc-sum> )
// <calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
// <calc-product> = <calc-value> [ '*' <calc-value> | '/' <number> ]*
// <calc-value> = <number> | <dimension> | <percentage> | ( <calc-sum> )
//
String processCalcExpression() {
var inString = tokenizer._inString;
tokenizer._inString = false;
// Gobble up everything until we hit our stop token.
var stringValue = new StringBuffer();
var left = 1;
var matchingParens = false;
while (_peek() != TokenKind.END_OF_FILE && !matchingParens) {
var token = _peek();
if (token == TokenKind.LPAREN)
left++;
else if (token == TokenKind.RPAREN) left--;
matchingParens = left == 0;
if (!matchingParens) stringValue.write(_next().text);
}
if (!matchingParens) {
_error("problem parsing function expected ), ", _peekToken.span);
}
tokenizer._inString = inString;
return stringValue.toString();
}
CalcTerm processCalc(Identifier func) {
var start = _peekToken.span;
var name = func.name;
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));
if (!_maybeEat(TokenKind.RPAREN)) {
_error("problem parsing function expected ), ", _peekToken.span);
}
return new CalcTerm(name, name, calcExpr, _makeSpan(start));
}
return null;
}
// 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 message 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 '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] as LiteralTerm).text;
// [0] - var name, [1] - OperatorComma, [2] - default value.
var defaultValues = expr.expressions.length >= 3
? expr.expressions.sublist(2)
: <Expression>[];
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));
}
}
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();
}