blob: a865bdf2760b62390c041d0bfa8b4ff4cad85e23 [file] [log] [blame]
// 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 = int.parse("${unary}${t.text}");
break;
case TokenKind.DOUBLE:
t = _next();
value = double.parse("${unary}${t.text}");
break;
case TokenKind.SINGLE_QUOTE:
case TokenKind.DOUBLE_QUOTE:
value = processQuotedString(false);
value = '"${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.write(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.codeUnitAt(i));
if (digit < 0) {
throw new HexNumberException();
}
result = (result << 4) + digit;
}
return result;
}
}
/** Not a hex number. */
class HexNumberException implements Exception {
HexNumberException();
}