8e6f238f86
Replace === null with == null. BUG=http://dartbug.com/6380 Review URL: https://codereview.chromium.org//11361190 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@14794 260f80e4-7a28-3924-810f-c04153c831b5
1018 lines
28 KiB
Dart
1018 lines
28 KiB
Dart
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
|
|
/**
|
|
* A simple recursive descent parser for CSS.
|
|
*/
|
|
class Parser {
|
|
Tokenizer tokenizer;
|
|
|
|
var _fs; // If non-null filesystem to read files.
|
|
String _basePath; // Base path of CSS file.
|
|
|
|
final SourceFile source;
|
|
|
|
Token _previousToken;
|
|
Token _peekToken;
|
|
|
|
// Communicating errors back to template parser.
|
|
// TODO(terry): Need a better mechanism (e.g., common World).
|
|
var _erroMsgRedirector;
|
|
|
|
Parser(this.source, [int start = 0, this._fs = null, this._basePath = null]) {
|
|
tokenizer = new Tokenizer(source, true, start);
|
|
_peekToken = tokenizer.next();
|
|
_previousToken = null;
|
|
}
|
|
|
|
// Main entry point for parsing an entire CSS file.
|
|
// If nestedCSS is true when we're back at processing directives from top and
|
|
// we encounter a } then stop we're inside of a template e.g.,
|
|
//
|
|
// template ... {
|
|
// css {
|
|
// .item {
|
|
// left: 10px;
|
|
// }
|
|
// }
|
|
// <div>...</div>
|
|
// }
|
|
//
|
|
Stylesheet parse([bool nestedCSS = false, var erroMsgRedirector = null]) {
|
|
// TODO(terry): Hack for migrating CSS errors back to template errors.
|
|
_erroMsgRedirector = erroMsgRedirector;
|
|
|
|
List<ASTNode> productions = [];
|
|
|
|
int start = _peekToken.start;
|
|
while (!_maybeEat(TokenKind.END_OF_FILE) &&
|
|
(!nestedCSS && !_peekKind(TokenKind.RBRACE))) {
|
|
// TODO(terry): Need to handle charset, import, media and page.
|
|
var directive = processDirective();
|
|
if (directive != null) {
|
|
productions.add(directive);
|
|
} else {
|
|
RuleSet ruleset = processRuleSet();
|
|
if (ruleset != null) {
|
|
productions.add(ruleset);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Stylesheet(productions, _makeSpan(start));
|
|
}
|
|
|
|
/** Generate an error if [source] has not been completely consumed. */
|
|
void checkEndOfFile() {
|
|
_eat(TokenKind.END_OF_FILE);
|
|
}
|
|
|
|
/** 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() {
|
|
_previousToken = _peekToken;
|
|
_peekToken = tokenizer.next();
|
|
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);
|
|
}
|
|
|
|
bool _maybeEat(int kind) {
|
|
if (_peekToken.kind == kind) {
|
|
_previousToken = _peekToken;
|
|
_peekToken = tokenizer.next();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void _eat(int kind) {
|
|
if (!_maybeEat(kind)) {
|
|
_errorExpected(TokenKind.kindToString(kind));
|
|
}
|
|
}
|
|
|
|
void _eatSemicolon() {
|
|
_eat(TokenKind.SEMICOLON);
|
|
}
|
|
|
|
void _errorExpected(String expected) {
|
|
var tok = _next();
|
|
var message;
|
|
try {
|
|
message = 'expected $expected, but found $tok';
|
|
} catch (e) {
|
|
message = 'parsing error expected $expected';
|
|
}
|
|
_error(message, tok.span);
|
|
}
|
|
|
|
void _error(String message, [SourceSpan location=null]) {
|
|
if (location == null) {
|
|
location = _peekToken.span;
|
|
}
|
|
|
|
if (_erroMsgRedirector == null) {
|
|
world.fatal(message, location); // syntax errors are fatal for now
|
|
} else {
|
|
String text = "";
|
|
if (location != null) {
|
|
text = location.toMessageString("");
|
|
}
|
|
_erroMsgRedirector.displayError("CSS error: \r${text}\r${message}");
|
|
}
|
|
}
|
|
|
|
void _warning(String message, [SourceSpan location=null]) {
|
|
if (location == null) {
|
|
location = _peekToken.span;
|
|
}
|
|
|
|
world.warning(message, location);
|
|
}
|
|
|
|
SourceSpan _makeSpan(int start) {
|
|
return new SourceSpan(source, start, _previousToken.end);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Top level productions
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
// Templates are @{selectors} single line nothing else.
|
|
SelectorGroup parseTemplate() {
|
|
SelectorGroup selectorGroup = null;
|
|
if (!isPrematureEndOfFile()) {
|
|
selectorGroup = templateExpression();
|
|
}
|
|
|
|
return selectorGroup;
|
|
}
|
|
|
|
/*
|
|
* Expect @{css_expression}
|
|
*/
|
|
templateExpression() {
|
|
List<Selector> selectors = [];
|
|
|
|
int start = _peekToken.start;
|
|
|
|
_eat(TokenKind.AT);
|
|
_eat(TokenKind.LBRACE);
|
|
|
|
selectors.add(processSelector());
|
|
SelectorGroup group = new SelectorGroup(selectors, _makeSpan(start));
|
|
|
|
_eat(TokenKind.RBRACE);
|
|
|
|
return group;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Productions
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
processMedia([bool oneRequired = false]) {
|
|
List<String> media = [];
|
|
|
|
while (_peekIdentifier()) {
|
|
// We have some media types.
|
|
var medium = identifier(); // Medium ident.
|
|
media.add(medium);
|
|
if (!_maybeEat(TokenKind.COMMA)) {
|
|
// No more media types exit now.
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (oneRequired && media.length == 0) {
|
|
_error('at least one media type required', _peekToken.span);
|
|
}
|
|
|
|
return media;
|
|
}
|
|
|
|
// Directive grammar:
|
|
//
|
|
// import: '@import' [string | URI] media_list?
|
|
// media: '@media' media_list '{' ruleset '}'
|
|
// page: '@page' [':' IDENT]? '{' declarations '}'
|
|
// include: '@include' [string | URI]
|
|
// stylet: '@stylet' IDENT '{' ruleset '}'
|
|
// media_list: IDENT [',' IDENT]
|
|
// keyframes: '@-webkit-keyframes ...' (see grammar below).
|
|
// font_face: '@font-face' '{' declarations '}'
|
|
//
|
|
processDirective() {
|
|
int start = _peekToken.start;
|
|
|
|
if (_maybeEat(TokenKind.AT)) {
|
|
switch (_peek()) {
|
|
case TokenKind.DIRECTIVE_IMPORT:
|
|
_next();
|
|
|
|
String importStr;
|
|
if (_peekIdentifier()) {
|
|
var func = processFunction(identifier());
|
|
if (func is UriTerm) {
|
|
importStr = func.text;
|
|
}
|
|
} else {
|
|
importStr = processQuotedString(false);
|
|
}
|
|
|
|
// Any medias?
|
|
List<String> medias = processMedia();
|
|
|
|
if (importStr == null) {
|
|
_error('missing import string', _peekToken.span);
|
|
}
|
|
return new ImportDirective(importStr, medias, _makeSpan(start));
|
|
case TokenKind.DIRECTIVE_MEDIA:
|
|
_next();
|
|
|
|
// Any medias?
|
|
List<String> media = processMedia(true);
|
|
RuleSet ruleset;
|
|
|
|
if (_maybeEat(TokenKind.LBRACE)) {
|
|
ruleset = processRuleSet();
|
|
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, ruleset, _makeSpan(start));
|
|
case TokenKind.DIRECTIVE_PAGE:
|
|
_next();
|
|
|
|
// Any pseudo page?
|
|
var pseudoPage;
|
|
if (_maybeEat(TokenKind.COLON)) {
|
|
if (_peekIdentifier()) {
|
|
pseudoPage = identifier();
|
|
}
|
|
}
|
|
return new PageDirective(pseudoPage, processDeclarations(),
|
|
_makeSpan(start));
|
|
case TokenKind.DIRECTIVE_KEYFRAMES:
|
|
/* Key frames grammar:
|
|
*
|
|
* @-webkit-keyframes [IDENT|STRING] '{' keyframes-blocks '}';
|
|
*
|
|
* keyframes-blocks:
|
|
* [keyframe-selectors '{' declarations '}']* ;
|
|
*
|
|
* keyframe-selectors:
|
|
* ['from'|'to'|PERCENTAGE] [',' ['from'|'to'|PERCENTAGE] ]* ;
|
|
*/
|
|
_next();
|
|
|
|
var name;
|
|
if (_peekIdentifier()) {
|
|
name = identifier();
|
|
}
|
|
|
|
_eat(TokenKind.LBRACE);
|
|
|
|
KeyFrameDirective kf = new KeyFrameDirective(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));
|
|
|
|
kf.add(new KeyFrameBlock(selectors, processDeclarations(),
|
|
_makeSpan(start)));
|
|
|
|
} while (!_maybeEat(TokenKind.RBRACE));
|
|
|
|
return kf;
|
|
case TokenKind.DIRECTIVE_FONTFACE:
|
|
_next();
|
|
|
|
List<Declaration> decls = [];
|
|
|
|
// TODO(terry): To Be Implemented
|
|
|
|
return new FontFaceDirective(decls, _makeSpan(start));
|
|
case TokenKind.DIRECTIVE_INCLUDE:
|
|
_next();
|
|
String filename = processQuotedString(false);
|
|
if (_fs != null) {
|
|
// Does CSS file exist?
|
|
if (_fs.fileExists('${_basePath}${filename}')) {
|
|
String basePath = "";
|
|
int idx = filename.lastIndexOf('/');
|
|
if (idx >= 0) {
|
|
basePath = filename.substring(0, idx + 1);
|
|
}
|
|
basePath = '${_basePath}${basePath}';
|
|
// Yes, let's parse this file as well.
|
|
String fullFN = '${basePath}${filename}';
|
|
String contents = _fs.readAll(fullFN);
|
|
Parser parser = new Parser(new SourceFile(fullFN, contents), 0,
|
|
_fs, basePath);
|
|
Stylesheet stylesheet = parser.parse();
|
|
return new IncludeDirective(filename, stylesheet, _makeSpan(start));
|
|
}
|
|
|
|
_error('file doesn\'t exist ${filename}', _peekToken.span);
|
|
}
|
|
|
|
print("WARNING: @include doesn't work for uitest");
|
|
return new IncludeDirective(filename, null, _makeSpan(start));
|
|
case TokenKind.DIRECTIVE_STYLET:
|
|
/* Stylet grammar:
|
|
*
|
|
* @stylet IDENT '{'
|
|
* ruleset
|
|
* '}'
|
|
*/
|
|
_next();
|
|
|
|
var name;
|
|
if (_peekIdentifier()) {
|
|
name = identifier();
|
|
}
|
|
|
|
_eat(TokenKind.LBRACE);
|
|
|
|
List<ASTNode> productions = [];
|
|
|
|
start = _peekToken.start;
|
|
while (!_maybeEat(TokenKind.END_OF_FILE)) {
|
|
RuleSet ruleset = processRuleSet();
|
|
if (ruleset == null) {
|
|
break;
|
|
}
|
|
productions.add(ruleset);
|
|
}
|
|
|
|
_eat(TokenKind.RBRACE);
|
|
|
|
return new StyletDirective(name, productions, _makeSpan(start));
|
|
default:
|
|
_error('unknown directive, found $_peekToken', _peekToken.span);
|
|
}
|
|
}
|
|
}
|
|
|
|
RuleSet processRuleSet() {
|
|
int start = _peekToken.start;
|
|
|
|
SelectorGroup selGroup = processSelectorGroup();
|
|
if (selGroup != null) {
|
|
return new RuleSet(selGroup, processDeclarations(), _makeSpan(start));
|
|
}
|
|
}
|
|
|
|
DeclarationGroup processDeclarations() {
|
|
int start = _peekToken.start;
|
|
|
|
_eat(TokenKind.LBRACE);
|
|
|
|
List<Declaration> decls = [];
|
|
do {
|
|
Declaration decl = processDeclaration();
|
|
if (decl != null) {
|
|
decls.add(decl);
|
|
}
|
|
} while (_maybeEat(TokenKind.SEMICOLON));
|
|
|
|
_eat(TokenKind.RBRACE);
|
|
|
|
return new DeclarationGroup(decls, _makeSpan(start));
|
|
}
|
|
|
|
SelectorGroup processSelectorGroup() {
|
|
List<Selector> selectors = [];
|
|
int start = _peekToken.start;
|
|
do {
|
|
Selector selector = processSelector();
|
|
if (selector != null) {
|
|
selectors.add(selector);
|
|
}
|
|
} while (_maybeEat(TokenKind.COMMA));
|
|
|
|
if (selectors.length > 0) {
|
|
return new SelectorGroup(selectors, _makeSpan(start));
|
|
}
|
|
}
|
|
|
|
/* Return list of selectors
|
|
*
|
|
*/
|
|
processSelector() {
|
|
List<SimpleSelectorSequence> simpleSequences = [];
|
|
int start = _peekToken.start;
|
|
while (true) {
|
|
// First item is never descendant make sure it's COMBINATOR_NONE.
|
|
var selectorItem = simpleSelectorSequence(simpleSequences.length == 0);
|
|
if (selectorItem != null) {
|
|
simpleSequences.add(selectorItem);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (simpleSequences.length > 0) {
|
|
return new Selector(simpleSequences, _makeSpan(start));
|
|
}
|
|
}
|
|
|
|
simpleSelectorSequence(bool forceCombinatorNone) {
|
|
int start = _peekToken.start;
|
|
int combinatorType = TokenKind.COMBINATOR_NONE;
|
|
|
|
switch (_peek()) {
|
|
case TokenKind.PLUS:
|
|
_eat(TokenKind.PLUS);
|
|
combinatorType = TokenKind.COMBINATOR_PLUS;
|
|
break;
|
|
case TokenKind.GREATER:
|
|
_eat(TokenKind.GREATER);
|
|
combinatorType = TokenKind.COMBINATOR_GREATER;
|
|
break;
|
|
case TokenKind.TILDE:
|
|
_eat(TokenKind.TILDE);
|
|
combinatorType = TokenKind.COMBINATOR_TILDE;
|
|
break;
|
|
}
|
|
|
|
// 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 simpleSel = simpleSelector();
|
|
if (simpleSel != null) {
|
|
return new SimpleSelectorSequence(simpleSel, _makeSpan(start),
|
|
combinatorType);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple selector grammar:
|
|
*
|
|
* simple_selector_sequence
|
|
* : [ type_selector | universal ]
|
|
* [ HASH | class | attrib | pseudo | negation ]*
|
|
* | [ HASH | class | attrib | pseudo | negation ]+
|
|
* type_selector
|
|
* : [ namespace_prefix ]? element_name
|
|
* namespace_prefix
|
|
* : [ IDENT | '*' ]? '|'
|
|
* element_name
|
|
* : IDENT
|
|
* universal
|
|
* : [ namespace_prefix ]? '*'
|
|
* class
|
|
* : '.' IDENT
|
|
*/
|
|
simpleSelector() {
|
|
// TODO(terry): Nathan makes a good point parsing of namespace and element
|
|
// are essentially the same (asterisk or identifier) other
|
|
// than the error message for element. Should consolidate the
|
|
// code.
|
|
// TODO(terry): Need to handle attribute namespace too.
|
|
var first;
|
|
int start = _peekToken.start;
|
|
switch (_peek()) {
|
|
case TokenKind.ASTERISK:
|
|
// Mark as universal namespace.
|
|
var tok = _next();
|
|
first = new Wildcard(_makeSpan(tok.start));
|
|
break;
|
|
case TokenKind.IDENTIFIER:
|
|
int startIdent = _peekToken.start;
|
|
first = identifier();
|
|
break;
|
|
}
|
|
|
|
if (_maybeEat(TokenKind.NAMESPACE)) {
|
|
var element;
|
|
switch (_peek()) {
|
|
case TokenKind.ASTERISK:
|
|
// Mark as universal element
|
|
var tok = _next();
|
|
element = new Wildcard(_makeSpan(tok.start));
|
|
break;
|
|
case TokenKind.IDENTIFIER:
|
|
element = identifier();
|
|
break;
|
|
default:
|
|
_error('expected element name or universal(*), but found $_peekToken',
|
|
_peekToken.span);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
simpleSelectorTail() {
|
|
// Check for HASH | class | attrib | pseudo | negation
|
|
int start = _peekToken.start;
|
|
switch (_peek()) {
|
|
case TokenKind.HASH:
|
|
_eat(TokenKind.HASH);
|
|
return new IdSelector(identifier(), _makeSpan(start));
|
|
case TokenKind.DOT:
|
|
_eat(TokenKind.DOT);
|
|
return new ClassSelector(identifier(), _makeSpan(start));
|
|
case TokenKind.COLON:
|
|
// :pseudo-class ::pseudo-element
|
|
// TODO(terry): '::' should be token.
|
|
_eat(TokenKind.COLON);
|
|
bool pseudoElement = _maybeEat(TokenKind.COLON);
|
|
var name = identifier();
|
|
// TODO(terry): Need to handle specific pseudo class/element name and
|
|
// backward compatible names that are : as well as :: as well as
|
|
// parameters.
|
|
return pseudoElement ?
|
|
new PseudoElementSelector(name, _makeSpan(start)) :
|
|
new PseudoClassSelector(name, _makeSpan(start));
|
|
case TokenKind.LBRACK:
|
|
return processAttribute();
|
|
}
|
|
}
|
|
|
|
// Attribute grammar:
|
|
//
|
|
// attributes :
|
|
// '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']'
|
|
//
|
|
// ATTRIB_MATCHES :
|
|
// [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ]
|
|
//
|
|
// INCLUDES: '~='
|
|
//
|
|
// DASHMATCH: '|='
|
|
//
|
|
// PREFIXMATCH: '^='
|
|
//
|
|
// SUFFIXMATCH: '$='
|
|
//
|
|
// SUBSTRMATCH: '*='
|
|
//
|
|
//
|
|
processAttribute() {
|
|
int start = _peekToken.start;
|
|
|
|
if (_maybeEat(TokenKind.LBRACK)) {
|
|
var attrName = identifier();
|
|
|
|
int op = TokenKind.NO_MATCH;
|
|
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;
|
|
}
|
|
|
|
String value;
|
|
if (op != TokenKind.NO_MATCH) {
|
|
// Operator hit so we require a value too.
|
|
if (_peekIdentifier()) {
|
|
value = identifier();
|
|
} else {
|
|
value = processQuotedString(false);
|
|
}
|
|
|
|
if (value == null) {
|
|
_error('expected attribute value string or ident', _peekToken.span);
|
|
}
|
|
}
|
|
|
|
_eat(TokenKind.RBRACK);
|
|
|
|
return new AttributeSelector(attrName, op, value, _makeSpan(start));
|
|
}
|
|
}
|
|
|
|
// Declaration grammar:
|
|
//
|
|
// declaration: property ':' expr prio?
|
|
//
|
|
// property: IDENT
|
|
// prio: !important
|
|
// expr: (see processExpr)
|
|
//
|
|
processDeclaration() {
|
|
Declaration decl;
|
|
|
|
int start = _peekToken.start;
|
|
|
|
// IDENT ':' expr '!important'?
|
|
if (TokenKind.isIdentifier(_peekToken.kind)) {
|
|
var propertyIdent = identifier();
|
|
_eat(TokenKind.COLON);
|
|
|
|
decl = new Declaration(propertyIdent, processExpr(), _makeSpan(start));
|
|
|
|
// Handle !important (prio)
|
|
decl.important = _maybeEat(TokenKind.IMPORTANT);
|
|
}
|
|
|
|
return decl;
|
|
}
|
|
|
|
// Expression grammar:
|
|
//
|
|
// expression: term [ operator? term]*
|
|
//
|
|
// operator: '/' | ','
|
|
// term: (see processTerm)
|
|
//
|
|
processExpr() {
|
|
int start = _peekToken.start;
|
|
Expressions expressions = new Expressions(_makeSpan(start));
|
|
|
|
bool keepGoing = true;
|
|
var expr;
|
|
while (keepGoing && (expr = processTerm()) != null) {
|
|
var op;
|
|
|
|
int opStart = _peekToken.start;
|
|
|
|
switch (_peek()) {
|
|
case TokenKind.SLASH:
|
|
op = new OperatorSlash(_makeSpan(opStart));
|
|
break;
|
|
case TokenKind.COMMA:
|
|
op = new OperatorComma(_makeSpan(opStart));
|
|
break;
|
|
}
|
|
|
|
if (expr != null) {
|
|
expressions.add(expr);
|
|
} else {
|
|
keepGoing = false;
|
|
}
|
|
|
|
if (op != null) {
|
|
expressions.add(op);
|
|
_next();
|
|
}
|
|
}
|
|
|
|
return expressions;
|
|
}
|
|
|
|
// 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() {
|
|
int start = _peekToken.start;
|
|
Token t; // token for term's value
|
|
var value; // value of term (numeric values)
|
|
|
|
var unary = "";
|
|
|
|
switch (_peek()) {
|
|
case TokenKind.HASH:
|
|
this._eat(TokenKind.HASH);
|
|
String hexText;
|
|
if (_peekKind(TokenKind.INTEGER)) {
|
|
String hexText1 = _peekToken.text;
|
|
_next();
|
|
if (_peekIdentifier()) {
|
|
hexText = '${hexText1}${identifier().name}';
|
|
} else {
|
|
hexText = hexText1;
|
|
}
|
|
} else if (_peekIdentifier()) {
|
|
hexText = identifier().name;
|
|
} else {
|
|
_errorExpected("hex number");
|
|
}
|
|
|
|
try {
|
|
int hexValue = parseHex(hexText);
|
|
return new HexColorTerm(hexValue, hexText, _makeSpan(start));
|
|
} on HexNumberException catch (hne) {
|
|
_error('Bad hex number', _makeSpan(start));
|
|
}
|
|
break;
|
|
case TokenKind.INTEGER:
|
|
t = _next();
|
|
value = Math.parseInt("${unary}${t.text}");
|
|
break;
|
|
case TokenKind.DOUBLE:
|
|
t = _next();
|
|
value = Math.parseDouble("${unary}${t.text}");
|
|
break;
|
|
case TokenKind.SINGLE_QUOTE:
|
|
case TokenKind.DOUBLE_QUOTE:
|
|
value = processQuotedString(false);
|
|
value = '"${value}"';
|
|
return new LiteralTerm(value, value, _makeSpan(start));
|
|
case TokenKind.LPAREN:
|
|
_next();
|
|
|
|
GroupTerm group = new GroupTerm(_makeSpan(start));
|
|
|
|
do {
|
|
var term = processTerm();
|
|
if (term != null && term is LiteralTerm) {
|
|
group.add(term);
|
|
}
|
|
} while (!_maybeEat(TokenKind.RPAREN));
|
|
|
|
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 (_maybeEat(TokenKind.LPAREN)) {
|
|
// FUNCTION
|
|
return processFunction(nameValue);
|
|
} else {
|
|
// 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?
|
|
try {
|
|
// Named color?
|
|
int colorValue = TokenKind.matchColorName(nameValue.name);
|
|
|
|
// Yes, process the color as an RGB value.
|
|
String rgbColor = TokenKind.decimalToHex(colorValue, 3);
|
|
try {
|
|
colorValue = parseHex(rgbColor);
|
|
} on HexNumberException catch (hne) {
|
|
_error('Bad hex number', _makeSpan(start));
|
|
}
|
|
return new HexColorTerm(colorValue, rgbColor, _makeSpan(start));
|
|
} catch (error) {
|
|
if (error is NoColorMatchException) {
|
|
// TODO(terry): Other named things to match with validator?
|
|
|
|
// TODO(terry): Disable call to _warning need one World class for
|
|
// both CSS parser and other parser (e.g., template)
|
|
// so all warnings, errors, options, etc. are driven
|
|
// from the one World.
|
|
// _warning('Unknown property value ${error.name}', _makeSpan(start));
|
|
return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var term;
|
|
var unitType = this._peek();
|
|
|
|
switch (unitType) {
|
|
case TokenKind.UNIT_EM:
|
|
term = new EmTerm(value, t.text, _makeSpan(start));
|
|
_next(); // Skip the unit
|
|
break;
|
|
case TokenKind.UNIT_EX:
|
|
term = new ExTerm(value, t.text, _makeSpan(start));
|
|
_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, _makeSpan(start), unitType);
|
|
_next(); // Skip the unit
|
|
break;
|
|
case TokenKind.UNIT_ANGLE_DEG:
|
|
case TokenKind.UNIT_ANGLE_RAD:
|
|
case TokenKind.UNIT_ANGLE_GRAD:
|
|
term = new AngleTerm(value, t.text, _makeSpan(start), unitType);
|
|
_next(); // Skip the unit
|
|
break;
|
|
case TokenKind.UNIT_TIME_MS:
|
|
case TokenKind.UNIT_TIME_S:
|
|
term = new TimeTerm(value, t.text, _makeSpan(start), unitType);
|
|
_next(); // Skip the unit
|
|
break;
|
|
case TokenKind.UNIT_FREQ_HZ:
|
|
case TokenKind.UNIT_FREQ_KHZ:
|
|
term = new FreqTerm(value, t.text, _makeSpan(start), unitType);
|
|
_next(); // Skip the unit
|
|
break;
|
|
case TokenKind.PERCENT:
|
|
term = new PercentageTerm(value, t.text, _makeSpan(start));
|
|
_next(); // Skip the %
|
|
break;
|
|
case TokenKind.UNIT_FRACTION:
|
|
term = new FractionTerm(value, t.text, _makeSpan(start));
|
|
_next(); // Skip the unit
|
|
break;
|
|
default:
|
|
if (value != null) {
|
|
term = new NumberTerm(value, t.text, _makeSpan(start));
|
|
}
|
|
}
|
|
|
|
return term;
|
|
}
|
|
|
|
processQuotedString([bool urlString = false]) {
|
|
int start = _peekToken.start;
|
|
|
|
// URI term sucks up everything inside of quotes(' or ") or between parens
|
|
int stopToken = urlString ? TokenKind.RPAREN : -1;
|
|
switch (_peek()) {
|
|
case TokenKind.SINGLE_QUOTE:
|
|
stopToken = TokenKind.SINGLE_QUOTE;
|
|
_next(); // Skip the SINGLE_QUOTE.
|
|
break;
|
|
case TokenKind.DOUBLE_QUOTE:
|
|
stopToken = TokenKind.DOUBLE_QUOTE;
|
|
_next(); // Skip the DOUBLE_QUOTE.
|
|
break;
|
|
default:
|
|
if (urlString) {
|
|
if (_peek() == TokenKind.LPAREN) {
|
|
_next(); // Skip the LPAREN.
|
|
}
|
|
stopToken = TokenKind.RPAREN;
|
|
} else {
|
|
_error('unexpected string', _makeSpan(start));
|
|
}
|
|
}
|
|
|
|
StringBuffer stringValue = new StringBuffer();
|
|
|
|
// Gobble up everything until we hit our stop token.
|
|
int runningStart = _peekToken.start;
|
|
while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) {
|
|
var tok = _next();
|
|
stringValue.add(tok.text);
|
|
}
|
|
|
|
if (stopToken != TokenKind.RPAREN) {
|
|
_next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE;
|
|
}
|
|
|
|
return stringValue.toString();
|
|
}
|
|
|
|
// Function grammar:
|
|
//
|
|
// function: IDENT '(' expr ')'
|
|
//
|
|
processFunction(Identifier func) {
|
|
int start = _peekToken.start;
|
|
|
|
String name = func.name;
|
|
|
|
switch (name) {
|
|
case 'url':
|
|
// URI term sucks up everything inside of quotes(' or ") or between parens
|
|
String urlParam = processQuotedString(true);
|
|
|
|
// TODO(terry): Better error messge and checking for mismatched quotes.
|
|
if (_peek() == TokenKind.END_OF_FILE) {
|
|
_error("problem parsing URI", _peekToken.span);
|
|
}
|
|
|
|
if (_peek() == TokenKind.RPAREN) {
|
|
_next();
|
|
}
|
|
|
|
return new UriTerm(urlParam, _makeSpan(start));
|
|
case 'calc':
|
|
// TODO(terry): Implement expression handling...
|
|
break;
|
|
default:
|
|
var expr = processExpr();
|
|
if (!_maybeEat(TokenKind.RPAREN)) {
|
|
_error("problem parsing function expected ), ", _peekToken.span);
|
|
}
|
|
|
|
return new FunctionTerm(name, name, expr, _makeSpan(start));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
identifier() {
|
|
var tok = _next();
|
|
if (!TokenKind.isIdentifier(tok.kind)) {
|
|
_error('expected identifier, but found $tok', tok.span);
|
|
}
|
|
|
|
return new Identifier(tok.text, _makeSpan(tok.start));
|
|
}
|
|
|
|
// TODO(terry): Move this to base <= 36 and into shared code.
|
|
static int _hexDigit(int c) {
|
|
if(c >= 48/*0*/ && c <= 57/*9*/) {
|
|
return c - 48;
|
|
} else if (c >= 97/*a*/ && c <= 102/*f*/) {
|
|
return c - 87;
|
|
} else if (c >= 65/*A*/ && c <= 70/*F*/) {
|
|
return c - 55;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
static int parseHex(String hex) {
|
|
var result = 0;
|
|
|
|
for (int i = 0; i < hex.length; i++) {
|
|
var digit = _hexDigit(hex.charCodeAt(i));
|
|
if (digit < 0) {
|
|
throw new HexNumberException();
|
|
}
|
|
result = (result << 4) + digit;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/** Not a hex number. */
|
|
class HexNumberException implements Exception {
|
|
HexNumberException();
|
|
}
|
|
|