64784aa1b1
Change for in memory buffers. BUG= TEST= Review URL: http://codereview.chromium.org//8498020 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@1752 260f80e4-7a28-3924-810f-c04153c831b5
395 lines
12 KiB
Dart
395 lines
12 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
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
/**
|
|
* A simple recursive descent parser for CSS.
|
|
*/
|
|
class Parser {
|
|
Tokenizer tokenizer;
|
|
|
|
final lang.SourceFile source;
|
|
|
|
lang.Token _previousToken;
|
|
lang.Token _peekToken;
|
|
|
|
Parser(this.source, [int startOffset = 0]) {
|
|
tokenizer = new Tokenizer(source, true, startOffset);
|
|
_peekToken = tokenizer.next();
|
|
_previousToken = null;
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
lang.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 (var e) {
|
|
message = 'parsing error expected $expected';
|
|
}
|
|
_error(message, tok.span);
|
|
}
|
|
|
|
void _error(String message, [lang.SourceSpan location=null]) {
|
|
if (location === null) {
|
|
location = _peekToken.span;
|
|
}
|
|
|
|
lang.world.fatal(message, location); // syntax errors are fatal for now
|
|
}
|
|
|
|
lang.SourceSpan _makeSpan(int start) {
|
|
return new lang.SourceSpan(source, start, _previousToken.end);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Top level productions
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
List<SelectorGroup> preprocess() {
|
|
List<SelectorGroup> groups = [];
|
|
while (!_maybeEat(TokenKind.END_OF_FILE)) {
|
|
do {
|
|
int start = _peekToken.start;
|
|
groups.add(new SelectorGroup(selector(),
|
|
_makeSpan(start)));
|
|
} while (_maybeEat(TokenKind.COMMA));
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
// Templates are @{selectors} single line nothing else.
|
|
SelectorGroup template() {
|
|
SelectorGroup selectorGroup = null;
|
|
if (!isPrematureEndOfFile()) {
|
|
selectorGroup = templateExpression();
|
|
}
|
|
|
|
return selectorGroup;
|
|
}
|
|
|
|
/*
|
|
* Expect @{css_expression}
|
|
*/
|
|
templateExpression() {
|
|
int start = _peekToken.start;
|
|
|
|
_eat(TokenKind.AT);
|
|
_eat(TokenKind.LBRACE);
|
|
|
|
SelectorGroup group = new SelectorGroup(selector(),
|
|
_makeSpan(start));
|
|
|
|
_eat(TokenKind.RBRACE);
|
|
|
|
return group;
|
|
}
|
|
|
|
int classNameCheck(var selector, int matches) {
|
|
if (selector.isCombinatorDescendant() ||
|
|
(selector.isCombinatorNone() && matches == 0)) {
|
|
if (matches < 0) {
|
|
String tooMany = selector.toString();
|
|
throw new CssSelectorException(
|
|
'Can not mix Id selector with class selector(s). Id ' +
|
|
'selector must be singleton too many starting at $tooMany');
|
|
}
|
|
|
|
return matches + 1;
|
|
} else {
|
|
String error = selector.toString();
|
|
throw new CssSelectorException(
|
|
'Selectors can not have combinators (>, +, or ~) before $error');
|
|
}
|
|
}
|
|
|
|
int elementIdCheck(var selector, int matches) {
|
|
if (selector.isCombinatorNone() && matches == 0) {
|
|
// Perfect just one element id returns matches of -1.
|
|
return -1;
|
|
} else if (selector.isCombinatorDescendant()) {
|
|
String tooMany = selector.toString();
|
|
throw new CssSelectorException(
|
|
'Use of Id selector must be singleton starting at $tooMany');
|
|
} else {
|
|
String error = selector.toString();
|
|
throw new CssSelectorException(
|
|
'Selectors can not have combinators (>, +, or ~) before $error');
|
|
}
|
|
}
|
|
|
|
// Validate the @{css expression} only .class and #elementId are valid inside
|
|
// of @{...}.
|
|
validateTemplate(List<lang.Node> selectors, CssWorld cssWorld) {
|
|
var errorSelector; // signal which selector didn't match.
|
|
bool found = false; // signal if a selector is matched.
|
|
|
|
int matches = 0; // < 0 IdSelectors, > 0 ClassSelector
|
|
for (selector in selectors) {
|
|
found = false;
|
|
if (selector is ClassSelector) {
|
|
// Any class name starting with an underscore is a private class name
|
|
// that doesn't have to match the world of known classes.
|
|
if (!selector.name.startsWith('_')) {
|
|
// TODO(terry): For now iterate through all classes look for faster
|
|
// mechanism hash map, etc.
|
|
for (className in cssWorld.classes) {
|
|
if (selector.name == className) {
|
|
matches = classNameCheck(selector, matches);
|
|
found = true; // .class found.
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Don't check any class name that is prefixed with an underscore.
|
|
// However, signal as found and bump up matches; it's a valid class
|
|
// name.
|
|
matches = classNameCheck(selector, matches);
|
|
found = true; // ._class are always okay.
|
|
}
|
|
} else if (selector is IdSelector) {
|
|
// Any element id starting with an underscore is a private element id
|
|
// that doesn't have to match the world of known elemtn ids.
|
|
if (!selector.name.startsWith('_')) {
|
|
for (id in cssWorld.ids) {
|
|
if (selector.name == id) {
|
|
matches = elementIdCheck(selector, matches);
|
|
found = true; // #id found.
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Don't check any element ID that is prefixed with an underscore.
|
|
// However, signal as found and bump up matches; it's a valid element
|
|
// ID.
|
|
matches = elementIdCheck(selector, matches);
|
|
found = true; // #_id are always okay
|
|
}
|
|
} else {
|
|
String badSelector = selector.toString();
|
|
throw new CssSelectorException(
|
|
'Invalid template selector $badSelector');
|
|
}
|
|
|
|
if (!found) {
|
|
String unknownName = selector.toString();
|
|
throw new CssSelectorException('Unknown selector name $unknownName');
|
|
}
|
|
}
|
|
|
|
// Every selector must match.
|
|
assert((matches >= 0 ? matches : -matches) == selectors.length);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Productions
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
selector() {
|
|
List<SimpleSelector> simpleSelectors = [];
|
|
while (true) {
|
|
// First item is never descendant make sure it's COMBINATOR_NONE.
|
|
var selectorItem = simpleSelectorSequence(simpleSelectors.length == 0);
|
|
if (selectorItem != null) {
|
|
simpleSelectors.add(selectorItem);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return simpleSelectors;
|
|
}
|
|
|
|
simpleSelectorSequence(bool forceCombinatorNone) {
|
|
int combinatorType = TokenKind.COMBINATOR_NONE;
|
|
switch (_peek()) {
|
|
case TokenKind.COMBINATOR_PLUS:
|
|
_eat(TokenKind.COMBINATOR_PLUS);
|
|
combinatorType = TokenKind.COMBINATOR_PLUS;
|
|
break;
|
|
case TokenKind.COMBINATOR_GREATER:
|
|
_eat(TokenKind.COMBINATOR_GREATER);
|
|
combinatorType = TokenKind.COMBINATOR_GREATER;
|
|
break;
|
|
case TokenKind.COMBINATOR_TILDE:
|
|
_eat(TokenKind.COMBINATOR_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;
|
|
}
|
|
}
|
|
|
|
return simpleSelector(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(int combinator) {
|
|
// 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.
|
|
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 (first == null) {
|
|
// Check for HASH | class | attrib | pseudo | negation
|
|
return simpleSelectorTail(combinator);
|
|
}
|
|
|
|
// Could be a namespace?
|
|
var isNamespace = _maybeEat(TokenKind.NAMESPACE);
|
|
if (isNamespace) {
|
|
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), combinator);
|
|
} else {
|
|
return new ElementSelector(first, _makeSpan(start), combinator);
|
|
}
|
|
}
|
|
|
|
simpleSelectorTail(int combinator) {
|
|
// 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), combinator);
|
|
case TokenKind.DOT:
|
|
_eat(TokenKind.DOT);
|
|
return new ClassSelector(identifier(), _makeSpan(start), combinator);
|
|
case TokenKind.PSEUDO:
|
|
// :pseudo-class ::pseudo-element
|
|
// TODO(terry): '::' should be token.
|
|
_eat(TokenKind.PSEUDO);
|
|
bool pseudoClass = _peek() != TokenKind.PSEUDO;
|
|
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), combinator) :
|
|
new PseudoElementSelector(name, _makeSpan(start), combinator);
|
|
|
|
// TODO(terry): attrib, negation.
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|