| Index: utils/css/parser.dart
|
| diff --git a/utils/css/parser.dart b/utils/css/parser.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..54e28d9186306b32b866dff522f71d9a695bf454
|
| --- /dev/null
|
| +++ b/utils/css/parser.dart
|
| @@ -0,0 +1,394 @@
|
| +// 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> expression() {
|
| + List<SelectorGroup> groups = [];
|
| + while (!_maybeEat(TokenKind.END_OF_FILE)) {
|
| + do {
|
| + int start = _peekToken.start;
|
| + groups.add(new SelectorGroup(selectorExpression(), _makeSpan(start)));
|
| + } while (_maybeEat(TokenKind.COMMA));
|
| + }
|
| +
|
| + return groups;
|
| + }
|
| +
|
| + // Templates are @{selectors} single line nothing else.
|
| + SelectorGroup template() {
|
| + SelectorGroup selectorGroup;
|
| + if (!_maybeEat(TokenKind.END_OF_FILE)) {
|
| + selectorGroup = templateExpression();
|
| + if (!_maybeEat(TokenKind.END_OF_FILE)) {
|
| + // TODO(terry): Error should be done.
|
| + }
|
| + }
|
| +
|
| + return selectorGroup;
|
| + }
|
| +
|
| + /*
|
| + * Expect @{css_expression}
|
| + */
|
| + templateExpression() {
|
| + int start = _peekToken.start;
|
| +
|
| + _eat(TokenKind.AT);
|
| + _eat(TokenKind.LBRACE);
|
| +
|
| + SelectorGroup group = new SelectorGroup(selectorExpression(),
|
| + _makeSpan(start));
|
| +
|
| + _eat(TokenKind.RBRACE);
|
| +
|
| + return group;
|
| + }
|
| +
|
| + 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 ' +
|
| + 'selector must be singleton too many starting at $tooMany',
|
| + selector.span);
|
| + }
|
| +
|
| + return ++matches;
|
| + } 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;
|
| + } 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<lang.Node> selectors, CssWorld cssWorld) {
|
| + var errorSelector;
|
| + bool found = false;
|
| +
|
| + 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('_')) {
|
| + for (className in cssWorld.classes) {
|
| + if (selector.name == className) {
|
| + matches = classNameCheck(selector, matches);
|
| + found = true;
|
| + 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;
|
| + }
|
| + } 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);
|
| + }
|
| +
|
| + if (!found) {
|
| + errorSelector = selector; // Flag the problem selector.
|
| + 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() {
|
| + return simpleSelectorSequence();
|
| + }
|
| +
|
| + simpleSelectorSequence() {
|
| + List<SimpleSelector> simpleSelectors = [];
|
| + while (true) {
|
| + var selectorItem = combinator();
|
| + if (selectorItem != null) {
|
| + simpleSelectors.add(selectorItem);
|
| + } else {
|
| + break;
|
| + }
|
| + }
|
| +
|
| + return simpleSelectors;
|
| + }
|
| +
|
| + combinator() {
|
| + 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;
|
| + }
|
| +
|
| + 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(int combinator) {
|
| + 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) {
|
| + // Could be a namespace?
|
| + var isNamespace = _maybeEat(TokenKind.NAMESPACE);
|
| + if (isNamespace) {
|
| + var element;
|
| + switch (_peek()) {
|
| + 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);
|
| + }
|
| +
|
| + 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(int combinator) {
|
| + // Check for HASH | class | attrib | pseudo | negation
|
| + switch (_peek()) {
|
| + case TokenKind.HASH:
|
| + int startHash = _peekToken.start;
|
| + _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;
|
| + 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);
|
| +
|
| + // TODO(terry): attrib, negation.
|
| + }
|
| + }
|
| +
|
| + identifier() {
|
| + var tok = _next();
|
| + if (!TokenKind.isIdentifier(tok.kind)) {
|
| + 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));
|
| + }
|
| +}
|
|
|