Files
sdk/utils/css/parser.dart
T
terry@google.com 91dd628bd8 More changes for CSS to run under VM.
Need unique name for library collision with template parser.

Few more changes to work under VM.

More changes for lang.dart seperation.

Seperation from lang.dart (tokenizer can't access private fields of base class in another library).  This was allowed in frog but not in the VM.  VM is right so need to sever dependency.

BUG=
TEST=

Review URL: https://chromiumcodereview.appspot.com//9695048

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@5544 260f80e4-7a28-3924-810f-c04153c831b5
2012-03-15 21:19:54 +00:00

1017 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 (final 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 pseudoClass = _peek() != 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 pseudoClass ?
new PseudoClassSelector(name, _makeSpan(start)) :
new PseudoElementSelector(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));
} catch (HexNumberException hne) {
_error('Bad hex number', _makeSpan(start));
}
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);
try {
colorValue = parseHex(rgbColor);
} catch (HexNumberException hne) {
_error('Bad hex number', _makeSpan(start));
}
return new HexColorTerm(colorValue, rgbColor, _makeSpan(start));
} catch (final 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();
}