Chromium Code Reviews| Index: utils/css/parser.dart |
| diff --git a/utils/css/parser.dart b/utils/css/parser.dart |
| index aa36c49612861a68247fb0b236a47231fc2d9bf0..7a81cfde7ba772e455b8c144504f387ee20a143f 100644 |
| --- a/utils/css/parser.dart |
| +++ b/utils/css/parser.dart |
| @@ -1,6 +1,5 @@ |
| // 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. |
|
nweiz
2012/01/04 19:05:41
Typo?
|
| /** |
| * A simple recursive descent parser for CSS. |
| @@ -8,17 +7,38 @@ |
| class Parser { |
| Tokenizer tokenizer; |
| + var _fs; // If non-null filesystem to read files. |
| + String _basePath; // Base path of CSS file. |
| + |
| final lang.SourceFile source; |
| lang.Token _previousToken; |
| lang.Token _peekToken; |
| - Parser(this.source, [int startOffset = 0]) { |
| - tokenizer = new Tokenizer(source, true, startOffset); |
| + 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. |
| + Stylesheet parse() { |
| + List<lang.Node> productions = []; |
| + |
| + int start = _peekToken.start; |
| + while (!_maybeEat(TokenKind.END_OF_FILE)) { |
| + // TODO(terry): Need to handle charset, import, media and page. |
|
nweiz
2012/01/04 19:05:41
I think you'll eventually want to parse unknown di
|
| + var directive = processDirective(); |
| + if (directive != null) { |
| + productions.add(directive); |
| + } else { |
| + productions.add(processRuleSet()); |
| + } |
| + } |
| + |
| + return new Stylesheet(productions, _makeSpan(start)); |
| + } |
| + |
| /** Generate an error if [source] has not been completely consumed. */ |
| void checkEndOfFile() { |
| _eat(TokenKind.END_OF_FILE); |
| @@ -106,21 +126,8 @@ class Parser { |
| // 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 parseTemplate() { |
|
nweiz
2012/01/04 19:05:41
Now that we're parsing things other than just sele
|
| SelectorGroup selectorGroup = null; |
| if (!isPrematureEndOfFile()) { |
| selectorGroup = templateExpression(); |
| @@ -133,147 +140,296 @@ class Parser { |
| * Expect @{css_expression} |
| */ |
| templateExpression() { |
| + List<Selector> selectors = []; |
| + |
| int start = _peekToken.start; |
| _eat(TokenKind.AT); |
| _eat(TokenKind.LBRACE); |
| - SelectorGroup group = new SelectorGroup(selector(), |
| - _makeSpan(start)); |
| + selectors.add(processSelector()); |
| + SelectorGroup group = new SelectorGroup(selectors, _makeSpan(start)); |
|
nweiz
2012/01/04 19:05:41
Why not just new SelectorGroup([processSelector()]
|
| _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'); |
| - } |
| + /////////////////////////////////////////////////////////////////// |
| + // Productions |
| + /////////////////////////////////////////////////////////////////// |
| - return matches + 1; |
| - } else { |
| - String error = selector.toString(); |
| - throw new CssSelectorException( |
| - 'Selectors can not have combinators (>, +, or ~) before $error'); |
| + processMedia([bool oneRequired = false]) { |
|
nweiz
2012/01/04 19:05:41
Why are all the names prefixed with "process"? See
nweiz
2012/01/04 19:05:41
This should be called processMediaQuery, since it
|
| + List<String> media = []; |
| + |
| + while (_peekIdentifier()) { |
|
nweiz
2012/01/04 19:05:41
Add a TODO here about supporting the full media qu
|
| + // We have some media types. |
| + var medium = identifier(); // Medium ident. |
|
nweiz
2012/01/04 19:05:41
What does "medium" mean here?
|
| + 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; |
| } |
| - 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; |
| - } |
| + // 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). |
|
nweiz
2012/01/04 19:05:41
Also @-moz-keyframes, and probably also @keyframes
|
| + // font_face: '@font-face' '{' declarations '}' |
| + // |
| + processDirective() { |
| + int start = _peekToken.start; |
| + |
| + if (_maybeEat(TokenKind.AT)) { |
|
nweiz
2012/01/04 19:05:41
if (!_maybeEat(TokenKind.AT)) return;
|
| + switch (_peek()) { |
| + case TokenKind.DIRECTIVE_IMPORT: |
| + _next(); |
| + |
| + String importStr; |
| + if (_peekIdentifier()) { |
| + var func = processFunction(identifier()); |
| + if (func is UriTerm) { |
|
nweiz
2012/01/04 19:05:41
This needs better error handling.
|
| + importStr = func.text; |
| } |
| } 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. |
| + importStr = processQuotedString(false); |
| } |
| - } 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; |
| - } |
| + |
| + // Any medias? |
|
nweiz
2012/01/04 19:05:41
Grammar nit: "media" is already plural
|
| + 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(); |
|
nweiz
2012/01/04 19:05:41
@media directives can contain multiple rulesets.
|
| + if (!_maybeEat(TokenKind.RBRACE)) { |
| + _error('expected } after ruleset for @media', _peekToken.span); |
| } |
| } 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 |
| + _error('expected { after media before ruleset', _peekToken.span); |
| } |
| - } else { |
| - String badSelector = selector.toString(); |
| - throw new CssSelectorException( |
| - 'Invalid template selector $badSelector'); |
| - } |
| + return new MediaDirective(media, ruleset, _makeSpan(start)); |
| + case TokenKind.DIRECTIVE_PAGE: |
| + _next(); |
| + |
| + // Any pseudo page? |
| + var pseudoPage; |
| + if (_maybeEat(TokenKind.COLON)) { |
| + if (_peekIdentifier()) { |
|
nweiz
2012/01/04 19:05:41
What if there is no identifier after the colon?
|
| + 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()) { |
|
nweiz
2012/01/04 19:05:41
Needs error handling.
|
| + 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 |
| - if (!found) { |
| - String unknownName = selector.toString(); |
| - throw new CssSelectorException('Unknown selector name $unknownName'); |
| + 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}')) { |
|
nweiz
2012/01/04 19:05:41
I really don't like resolving the @include in the
|
| + 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 lang.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()) { |
|
nweiz
2012/01/04 19:05:41
Needs error handling.
|
| + name = identifier(); |
| + } |
| + |
| + _eat(TokenKind.LBRACE); |
| + |
| + List<lang.Node> productions = []; |
| + |
| + int 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); |
| } |
| } |
| + } |
| + |
| + processRuleSet() { |
| + int start = _peekToken.start; |
| - // Every selector must match. |
| - assert((matches >= 0 ? matches : -matches) == selectors.length); |
| + SelectorGroup selGroup = processSelectorGroup(); |
| + if (selGroup != null) { |
| + return new RuleSet(selGroup, processDeclarations(), _makeSpan(start)); |
| + } |
| } |
| - /////////////////////////////////////////////////////////////////// |
| - // Productions |
| - /////////////////////////////////////////////////////////////////// |
| + DeclarationGroup processDeclarations() { |
| + int start = _peekToken.start; |
| + |
| + _eat(TokenKind.LBRACE); |
| - selector() { |
| - List<SimpleSelector> simpleSelectors = []; |
| + 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(simpleSelectors.length == 0); |
| + var selectorItem = simpleSelectorSequence(simpleSequences.length == 0); |
| if (selectorItem != null) { |
| - simpleSelectors.add(selectorItem); |
| + simpleSequences.add(selectorItem); |
| } else { |
| break; |
| } |
| } |
| - return simpleSelectors; |
| + 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.COMBINATOR_PLUS: |
| - _eat(TokenKind.COMBINATOR_PLUS); |
| + case TokenKind.PLUS: |
| + _eat(TokenKind.PLUS); |
| combinatorType = TokenKind.COMBINATOR_PLUS; |
| break; |
| - case TokenKind.COMBINATOR_GREATER: |
| - _eat(TokenKind.COMBINATOR_GREATER); |
| + case TokenKind.GREATER: |
| + _eat(TokenKind.GREATER); |
| combinatorType = TokenKind.COMBINATOR_GREATER; |
| break; |
| - case TokenKind.COMBINATOR_TILDE: |
| - _eat(TokenKind.COMBINATOR_TILDE); |
| + case TokenKind.TILDE: |
| + _eat(TokenKind.TILDE); |
| combinatorType = TokenKind.COMBINATOR_TILDE; |
| break; |
| } |
| @@ -286,11 +442,16 @@ class Parser { |
| } |
| } |
| - return simpleSelector(combinatorType); |
| + 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 ]* |
| @@ -306,11 +467,12 @@ class Parser { |
| * class |
| * : '.' IDENT |
| */ |
| - simpleSelector(int combinator) { |
| + 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()) { |
| @@ -325,14 +487,7 @@ class Parser { |
| 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) { |
| + if (_maybeEat(TokenKind.NAMESPACE)) { |
| var element; |
| switch (_peek()) { |
| case TokenKind.ASTERISK: |
| @@ -349,38 +504,424 @@ class Parser { |
| } |
| return new NamespaceSelector(first, |
| - new ElementSelector(element, element.span), |
| - _makeSpan(start), combinator); |
| + new ElementSelector(element, element.span), _makeSpan(start)); |
| + } else if (first != null) { |
| + return new ElementSelector(first, _makeSpan(start)); |
| } else { |
| - return new ElementSelector(first, _makeSpan(start), combinator); |
| + // Check for HASH | class | attrib | pseudo | negation |
| + return simpleSelectorTail(); |
| } |
| } |
| - simpleSelectorTail(int combinator) { |
| + 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), combinator); |
| + return new IdSelector(identifier(), _makeSpan(start)); |
| case TokenKind.DOT: |
| _eat(TokenKind.DOT); |
| - return new ClassSelector(identifier(), _makeSpan(start), combinator); |
| - case TokenKind.PSEUDO: |
| + return new ClassSelector(identifier(), _makeSpan(start)); |
| + case TokenKind.COLON: |
| // :pseudo-class ::pseudo-element |
| // TODO(terry): '::' should be token. |
| - _eat(TokenKind.PSEUDO); |
| - bool pseudoClass = _peek() != TokenKind.PSEUDO; |
| + _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), combinator) : |
| - new PseudoElementSelector(name, _makeSpan(start), combinator); |
| + 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)) { |
|
nweiz
2012/01/04 19:05:41
if (!...) return;
|
| + var attrName = identifier(); |
| + |
| + int op = TokenKind.NO_MATCH; |
|
nweiz
2012/01/04 19:05:41
This would be clearer if you set op to NO_MATCH in
|
| + 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)) { |
|
nweiz
2012/01/04 19:05:41
if (!...) return null;
|
| + 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; |
| + } |
| - // TODO(terry): attrib, negation. |
| + 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; |
| + lang.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)) { |
|
nweiz
2012/01/04 19:05:41
This seems really nasty. Shouldn't you be parsing
|
| + 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}"'; |
|
nweiz
2012/01/04 19:05:41
This will break if the original string was single-
|
| + return new LiteralTerm(value, value, _makeSpan(start)); |
| + case TokenKind.LPAREN: |
|
nweiz
2012/01/04 19:05:41
Is there somewhere the semantics of additions like
|
| + _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 { |
| + // What kind of identifier is it? |
| + int value; |
| + try { |
| + // Named color? |
| + value = TokenKind.matchColorName(nameValue.name); |
| + |
| + // Yes, process the color as an RGB value. |
| + String rgbColor = TokenKind.decimalToHex(value); |
| + int value; |
| + try { |
| + value = parseHex(rgbColor); |
| + } catch (HexNumberException hne) { |
| + _error('Bad hex number', _makeSpan(start)); |
| + } |
| + return new HexColorTerm(value, rgbColor, _makeSpan(start)); |
| + } catch (var error) { |
| + if (error is NoColorMatchException) { |
| + // Other named things to match with validator? |
| + // TODO(terry): TBD |
| +// _error('Unknown property value ${error.name}', _makeSpan(start)); |
| + |
| + value = nameValue.name; |
| + print('Warning: unknown property value ${error.name}'); |
|
nweiz
2012/01/04 19:05:41
I don't understand this warning. There are tons of
|
| + return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start)); |
| + |
| + } |
| + } |
| + } |
| + } |
| + |
| + var term; |
| + var unitType = this._peek(); |
| + |
| + switch (unitType) { |
|
nweiz
2012/01/04 19:05:41
Manually enumerating all the possible unit types,
|
| + 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]) { |
|
nweiz
2012/01/04 19:05:41
Why are you parsing strings in the parser and not
|
| + 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) { |
| + 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': |
|
nweiz
2012/01/04 19:05:41
It seems wrong that url() is being parsed in proce
|
| + // 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() { |
| @@ -391,4 +932,37 @@ class Parser { |
| 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(); |
| +} |
| + |