Index: frog/css/parser_css.dart |
diff --git a/frog/css/parser_css.dart b/frog/css/parser_css.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..6249aff081a1ac40098d208ac2d533b420f46540 |
--- /dev/null |
+++ b/frog/css/parser_css.dart |
@@ -0,0 +1,427 @@ |
+// 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 the dart language. |
jimhug
2011/11/09 16:04:03
Old comment.
terry
2011/11/09 21:56:06
Done.
|
+ */ |
+class Parser { |
+ // If true throw CSSParserExceptions for any tokenizing/parsing problems. |
+ bool _errorsAsException; |
+ Tokenizer tokenizer; |
+ |
+ final SourceFile source; |
+ |
+ // TODO(jimhug): 1. Try to kill initializers, if fails, clean this up. |
+ bool _inInitializers; |
nweiz
2011/11/10 00:04:39
Unused variable.
terry
2011/11/16 14:00:22
Done.
|
+ |
+ Token _previousToken; |
+ Token _peekToken; |
+ |
+ Parser(this.source, [int startOffset = 0]) { |
+ tokenizer = new Tokenizer(source, true, startOffset); |
+ _peekToken = tokenizer.next(); |
+ _previousToken = null; |
+ _inInitializers = false; |
+ _errorsAsException = false; |
+ } |
+ |
+ /** 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); |
nweiz
2011/11/10 00:04:39
Why can't you use _peekKind(TokenKind.IDENTIFIER)?
terry
2011/11/16 14:00:22
This is mimicked after frog's parser.
On 2011/11/
nweiz
2011/11/23 00:59:44
Frog's parser needs it because there are multiple
|
+ } |
+ |
+ 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() { |
nweiz
2011/11/10 00:04:39
Unused method.
terry
2011/11/16 14:00:22
Unused now will use later.
On 2011/11/10 00:04:39
nweiz
2011/11/23 00:59:44
I don't think it'll be useful, actually... there a
|
+ _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, [SourceSpan location=null]) { |
+ if (location === null) { |
+ location = _peekToken.span; |
+ } |
+ |
+ // TODO(terry): Should use below world.fatal: |
+ // world.fatal(message, location); // syntax errors are fatal for now |
+ |
+ // TODO(terry): Beginning of temp code until world.fatal enabled. |
+ var text = message; |
+ if (location != null) { |
+ text = location.toMessageString(message); |
+ } |
+ print("FATAL: $text"); |
+ |
+ // Any parsing problem throw exception too. |
+ if (this._errorsAsException) { |
+ throw new CssParserException(message, location); |
+ } |
+ // TODO(terry): End of temp code until world.fatal enabled. |
+ } |
+ |
+ SourceSpan _makeSpan(int start) { |
+ return new SourceSpan(source, start, _previousToken.end); |
+ } |
+ |
+ /////////////////////////////////////////////////////////////////// |
+ // Top level productions |
+ /////////////////////////////////////////////////////////////////// |
+ |
+ List<SelectorGroup> expression([bool throwErrors = false]) { |
nweiz
2011/11/10 00:04:39
I think your terminology is confusing here. "expre
terry
2011/11/16 14:00:22
I see your point, expression was probably a bad na
|
+ this._errorsAsException = throwErrors; |
+ List<SelectorGroup> groups = []; |
+ while (!_maybeEat(TokenKind.END_OF_FILE)) { |
+ do { |
+ int start = _peekToken.start; |
+ groups.add(new SelectorGroup(selectorExpression(), start)); |
+ } while (_maybeEat(TokenKind.COMMA)); |
+ } |
+ |
+ return groups; |
+ } |
+ |
+ // Templates are @{selectors} single line nothing else. |
+ SelectorGroup template([bool throwErrors = false]) { |
+ this._errorsAsException = throwErrors; |
+ SelectorGroup selectorGroup; |
+ if (!_maybeEat(TokenKind.END_OF_FILE)) { |
+ selectorGroup = templateExpression(); |
+ if (!_maybeEat(TokenKind.END_OF_FILE)) { |
nweiz
2011/11/10 00:04:39
Can you just call isPrematureEndOfFile?
terry
2011/11/22 16:40:47
Done.
|
+ // TODO(terry): Error should be done. |
+ } |
+ } |
+ |
+ return selectorGroup; |
+ } |
+ |
+ /* |
+ * Expect @{css_expression} |
+ */ |
+ templateExpression() { |
+ SelectorGroup selectorGroup = null; |
+ int start = _peekToken.start; |
+ |
+ if (_peek() == TokenKind.AT) { |
nweiz
2011/11/10 00:04:39
Is there any reason to do all these if checks, rat
terry
2011/11/16 14:00:22
Main reason was more exact error. However, I thin
|
+ _eat(TokenKind.AT); |
+ if (_peek() == TokenKind.LBRACE) { |
+ _eat(TokenKind.LBRACE); |
+ List<SimpleSelector> selectors= selectorExpression(); |
+ SelectorGroup exprResult = new SelectorGroup(selectors, start); |
+ |
+ if (_peek() == TokenKind.RBRACE) { |
+ _eat(TokenKind.RBRACE); |
+ selectorGroup = exprResult; |
+ } |
+ } |
+ } |
+ |
+ if (selectorGroup == null) { |
+ _errorExpected('css template @{css selector}'); |
+ } else { |
+ return selectorGroup; |
+ } |
+ } |
+ |
+ int classNameCheck(var selector, int matches) { |
+ if (selector.isCombinatorNone()) { |
+ if (matches < 0) { |
+ String tooMany = selector.toString(); |
+ throw new CssSelectorException( |
+ 'Can not mix Id selector with class selector(s). Id ' + |
nweiz
2011/11/10 00:04:39
This isn't actually true. "#foo.bar" is a valid se
terry
2011/11/16 14:00:22
Of course, however this is used by the validator f
nweiz
2011/11/23 00:59:44
Why aren't we allowing arbitrary selectors in sele
|
+ 'selector must be singleton too many starting at $tooMany', |
+ selector.span); |
+ } |
+ |
+ return ++matches; |
nweiz
2011/11/10 00:04:39
Style nit: matches + 1, since you aren't actually
terry
2011/11/22 16:40:47
Done.
|
+ } else { |
+ String error = selector.toString(); |
+ throw new CssSelectorException( |
+ 'Selectors can not have combinators (>, +, or ~) before $error', |
+ selector.span); |
+ } |
+ } |
+ |
+ int elementIdCheck(var selector, int matches) { |
+ if (selector.isCombinatorNone()) { |
+ if (matches != 0) { |
+ String tooMany = selector.toString(); |
+ throw new CssSelectorException( |
+ 'Use of Id selector must be singleton starting at $tooMany', |
+ selector.span); |
+ } |
+ return --matches; |
nweiz
2011/11/10 00:04:39
Style nit: matches - 1
terry
2011/11/22 16:40:47
Done.
|
+ } else { |
+ String error = selector.toString(); |
+ throw new CssSelectorException( |
+ 'Selectors can not have combinators (>, +, or ~) before $error', |
+ selector.span); |
+ } |
+ } |
+ |
+ // Validate the @{css expression} only .class and #elementId are valid inside |
+ // of @{...}. |
+ validateTemplate(List<Node> selectors, CssWorld cssWorld) { |
nweiz
2011/11/10 00:04:39
Does this really belong in the parser? It's not ac
terry
2011/11/22 16:40:47
Will be moving this out in the next checkin.
On 2
|
+ var errorSelector; |
+ bool found = false; |
+ |
+ bool matches = 0; // < 0 IdSelectors, > 0 ClassSelector |
nweiz
2011/11/10 00:04:39
You're declaring an int as a bool here. Also, it s
terry
2011/11/22 16:40:47
Thanks, caught this earlier when I merged with the
|
+ 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('_')) { |
+ for (className in cssWorld.classes) { |
nweiz
2011/11/10 00:04:39
Perhaps cssWorld.classes should be a hash table so
terry
2011/11/22 16:40:47
You're probably right but I've err'd on the side o
|
+ if (selector.name == className) { |
+ matches = classNameCheck(selector, matches); |
+ found = true; |
+ break; |
nweiz
2011/11/10 00:04:39
If you break after you find a single valid selecto
terry
2011/11/22 16:40:47
No each selector is validated if any selector isn'
|
+ } |
+ } |
+ } 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; |
+ } |
+ } 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; |
+ 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; |
+ } |
+ } else { |
+ String badSelector = selector.toString(); |
+ throw new CssSelectorException( |
+ 'Invalid selector $badSelector', selector.span); |
nweiz
2011/11/10 00:04:39
This is a confusing error message; it implies that
terry
2011/11/22 16:40:47
Good point I'll change the error.
Done.
|
+ } |
+ |
+ if (!found) { |
+ errorSelector = selector; // Flag the problem selector. |
nweiz
2011/11/10 00:04:39
If this is the only place you set errorSelector, w
terry
2011/11/22 16:40:47
Done.
|
+ break; |
+ } |
+ } |
+ |
+ assert(matches >= 0 || matches == -1); |
+ |
+ if (!found && errorSelector != null) { |
+ String unknownName = errorSelector.toString(); |
+ throw new CssSelectorException('Unknown selector name $unknownName', |
+ errorSelector.span); |
+ } |
+ } |
+ |
+ /////////////////////////////////////////////////////////////////// |
+ // Productions |
+ /////////////////////////////////////////////////////////////////// |
+ |
+ selectorExpression() { |
nweiz
2011/11/10 00:04:39
This method doesn't seem to do anything.
terry
2011/11/22 16:40:47
It's gone.
On 2011/11/10 00:04:39, nweiz wrote:
|
+ return simpleSelectorSequence(); |
+ } |
+ |
+ simpleSelectorSequence() { |
nweiz
2011/11/10 00:04:39
selector()
terry
2011/11/22 16:40:47
Done.
|
+ List<SimpleSelector> simpleSelectors = []; |
+ while (true) { |
+ var selectorItem = combinator(); |
+ if (selectorItem != null) { |
+ simpleSelectors.add(selectorItem); |
+ } else { |
+ break; |
+ } |
+ } |
+ |
+ return simpleSelectors; |
+ } |
+ |
+ combinator() { |
nweiz
2011/11/10 00:04:39
simpleSelectorSequence()
terry
2011/11/22 16:40:47
Done.
|
+ 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; |
+ } |
nweiz
2011/11/10 00:04:39
You never seem to be parsing a sequence of simple
terry
2011/11/22 16:40:47
I had't completed the code but have now so sequenc
|
+ |
+ return namespaceElementUniversal(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 |
+ */ |
+ namespaceElementUniversal(Token combinator) { |
nweiz
2011/11/10 00:04:39
simpleSelector()
terry
2011/11/22 16:40:47
Done.
|
+ String first; |
+ switch (_peek()) { |
+ case TokenKind.ASTERISK: |
+ first = '*'; // Mark as universal namespace. |
+ _next(); |
+ break; |
+ case TokenKind.IDENTIFIER: |
+ int startIdent = _peekToken.start; |
+ first = identifier(); |
+ break; |
+ } |
+ |
+ if (first != null) { |
nweiz
2011/11/10 00:04:39
Would be cleaner to return immediately if first ==
terry
2011/11/22 16:40:47
Done.
|
+ // Could be a namespace? |
+ var isNamespace = _maybeEat(TokenKind.NAMESPACE); |
+ if (isNamespace) { |
+ var element; |
+ switch (_peek()) { |
nweiz
2011/11/10 00:04:39
I don't like this duplicated logic. Could you chec
terry
2011/11/22 16:40:47
For now I like the element vs/ namespace parsing I
|
+ case TokenKind.ASTERISK: |
+ element = '*'; // Mark as universal. |
+ _next(); |
+ break; |
+ case TokenKind.IDENTIFIER: |
+ int startIdent = _peekToken.start; |
+ element = identifier(); |
+ break; |
+ default: |
+ _error('expected element name or universal, but found $_peekToken', _peekToken.span); |
jimhug
2011/11/09 16:04:03
Nit: too long line.
terry
2011/11/09 21:56:06
Done.
|
+ } |
+ |
+ return new NamespaceSelector(first, new ElementSelector(element), |
+ combinator); |
+ } else { |
+ return new ElementSelector(first, combinator); |
+ } |
+ } else { |
+ // Check for HASH | class | attrib | pseudo | negation |
+ return selectorNameType(combinator); |
+ } |
+ } |
+ |
+ selectorNameType(Token combinator) { |
nweiz
2011/11/10 00:04:39
simpleSelectorTail()?
terry
2011/11/22 16:40:47
Done.
|
+ // Check for HASH | class | attrib | pseudo | negation |
+ switch (_peek()) { |
+ case TokenKind.HASH: |
+ int startHash = _peekToken.start; |
nweiz
2011/11/10 00:04:39
Unused variable.
terry
2011/11/22 16:40:47
Done.
|
+ _eat(TokenKind.HASH); |
+ var name = identifier(); |
+ return new IdSelector(name, combinator); |
+ case TokenKind.DOT: |
+ _eat(TokenKind.DOT); |
+ var name = identifier(); |
+ return new ClassSelector(name, combinator); |
+ case TokenKind.PSEUDO: |
+ // :pseudo-class ::pseudo-element |
+ _eat(TokenKind.PSEUDO); |
+ bool pseudoClass = _peek() != TokenKind.PSEUDO; |
nweiz
2011/11/10 00:04:39
Seems like parsing : vs :: is a job for the tokeni
terry
2011/11/22 16:40:47
Your right. I'll fix in the next checkin.
|
+ var name = identifier(); |
+ // TODO(terry): Need to handle specific pseudo class/element name and |
+ // backward compatible names that are : as well as ::. |
+ return pseudoClass ? |
+ new PseudoClassSelector(name, combinator) : |
+ new PseudoElementSelector(name, combinator); |
nweiz
2011/11/10 00:04:39
TODO: Some pseudo-class and -element selectors hav
terry
2011/11/22 16:40:47
Yep, I haven't finished this yet I'll add a TODO.
|
+ |
+ // TODO(terry): attrib, negation. |
+ } |
+ } |
+ |
+ identifier() { |
+ var tok = _next(); |
+ if (!TokenKind.isIdentifier(tok.kind)) { |
nweiz
2011/11/10 00:04:39
Why aren't you just using _eat here?
terry
2011/11/22 16:40:47
Why for better error recovery? Currently, error r
|
+ try { |
+ _error('expected identifier, but found $tok', tok.span); |
+ } catch (var e) { |
+ _error('expected identifier', tok.span); |
+ } |
+ } |
+ |
+ return new Identifier(tok.text, _makeSpan(tok.start)); |
+ } |
+} |