Index: utils/template/parser.dart |
diff --git a/utils/template/parser.dart b/utils/template/parser.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..64feffe8685b751cd4c299b609417d2a80c6ce6f |
--- /dev/null |
+++ b/utils/template/parser.dart |
@@ -0,0 +1,567 @@ |
+// 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 |
+ |
+class TagStack { |
+ List<ASTNode> _stack; |
+ |
+ TagStack(var elem) : _stack = [] { |
+ _stack.add(elem); |
+ } |
+ |
+ void push(var elem) { |
+ _stack.add(elem); |
+ } |
+ |
+ ASTNode pop() { |
+ return _stack.removeLast(); |
+ } |
+ |
+ top() { |
+ return _stack.last(); |
+ } |
+} |
+ |
+// TODO(terry): Cleanup returning errors from CSS to common World error |
+// handler. |
+class ErrorMsgRedirector { |
+ void displayError(String msg) { |
+ if (world.printHandler != null) { |
+ world.printHandler(msg); |
+ } else { |
+ print("Unhandler Error: ${msg}"); |
+ } |
+ world.errors++; |
+ } |
+} |
+ |
+/** |
+ * A simple recursive descent parser for HTML. |
+ */ |
+class Parser { |
+ Tokenizer tokenizer; |
+ |
+ var _fs; // If non-null filesystem to read files. |
+ |
+ final SourceFile source; |
+ |
+ Token _previousToken; |
+ Token _peekToken; |
+ |
+ PrintHandler printHandler; |
+ |
+ Parser(this.source, [int start = 0, this._fs = null]) { |
+ tokenizer = new Tokenizer(source, true, start); |
+ _peekToken = tokenizer.next(); |
+ _previousToken = null; |
+ } |
+ |
+ // Main entry point for parsing an entire HTML file. |
+ List<Template> parse([PrintHandler handler = null]) { |
+ printHandler = handler; |
+ |
+ List<Template> productions = []; |
+ |
+ int start = _peekToken.start; |
+ while (!_maybeEat(TokenKind.END_OF_FILE)) { |
+ Template template = processTemplate(); |
+ if (template != null) { |
+ productions.add(template); |
+ } |
+ } |
+ |
+ return productions; |
+ } |
+ |
+ /** 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([bool inTag = true]) { |
+ _previousToken = _peekToken; |
+ _peekToken = tokenizer.next(inTag); |
+ 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 (printHandler == null) { |
+ world.fatal(message, location); // syntax errors are fatal for now |
+ } else { |
+ // TODO(terry): Need common World view for css and template parser. |
+ // For now this is how we return errors from CSS - ugh. |
+ printHandler(message); |
+ } |
+ } |
+ |
+ void _warning(String message, [SourceSpan location=null]) { |
+ if (location === null) { |
+ location = _peekToken.span; |
+ } |
+ |
+ if (printHandler == null) { |
+ world.warning(message, location); |
+ } else { |
+ // TODO(terry): Need common World view for css and template parser. |
+ // For now this is how we return errors from CSS - ugh. |
+ printHandler(message); |
+ } |
+ } |
+ |
+ SourceSpan _makeSpan(int start) { |
+ return new SourceSpan(source, start, _previousToken.end); |
+ } |
+ |
+ /////////////////////////////////////////////////////////////////// |
+ // Top level productions |
+ /////////////////////////////////////////////////////////////////// |
+ |
+ Template processTemplate() { |
+ var template; |
+ |
+ int start = _peekToken.start; |
+ |
+ // Handle the template keyword followed by template signature. |
+ _eat(TokenKind.TEMPLATE_KEYWORD); |
+ |
+ if (_peekIdentifier()) { |
+ final templateName = identifier(); |
+ |
+ List<Map<Identifier, Identifier>> params = |
+ new List<Map<Identifier, Identifier>>(); |
+ |
+ _eat(TokenKind.LPAREN); |
+ |
+ start = _peekToken.start; |
+ while (true) { |
+ // TODO(terry): Need robust Dart argument parser (e.g., |
+ // List<String> arg1, etc). |
+ var type = processAsIdentifier(); |
+ var paramName = processAsIdentifier(); |
+ if (type != null && paramName != null) { |
+ params.add({'type': type, 'name' : paramName}); |
+ |
+ if (!_maybeEat(TokenKind.COMMA)) { |
+ break; |
+ } |
+ } else { |
+ _error("Template paramter missing type and name", _makeSpan(start)); |
+ break; |
+ } |
+ } |
+ |
+ _eat(TokenKind.RPAREN); |
+ |
+ TemplateSignature sig = |
+ new TemplateSignature(templateName.name, params, _makeSpan(start)); |
+ |
+ TemplateContent content = processTemplateContent(); |
+ |
+ template = new Template(sig, content, _makeSpan(start)); |
+ } |
+ |
+ return template; |
+ } |
+ |
+ // All tokens are identifiers tokenizer is geared to HTML if identifiers are |
+ // HTML element or attribute names we need them as an identifier. Used by |
+ // template signatures and expressions in ${...} |
+ Identifier processAsIdentifier() { |
+ int start = _peekToken.start; |
+ |
+ if (_peekIdentifier()) { |
+ return identifier(); |
+ } else if (TokenKind.validTagName(_peek())) { |
+ var tok = _next(); |
+ return new Identifier(TokenKind.tagNameFromTokenId(tok.kind), |
+ _makeSpan(start)); |
+ } |
+ } |
+ |
+ css.Stylesheet processCSS() { |
+ // Is there a CSS block? |
+ if (_peekIdentifier()) { |
+ int start = _peekToken.start; |
+ if (identifier().name == 'css') { |
+ _eat(TokenKind.LBRACE); |
+ |
+ css.Stylesheet cssCtx = processCSSContent(source, tokenizer.startIndex); |
+ |
+ // TODO(terry): Hack, restart template parser where CSS parser stopped. |
+ tokenizer.index = lastCSSIndexParsed; |
+ _next(false); |
+ |
+ _eat(TokenKind.RBRACE); // close } of css block |
+ |
+ return cssCtx; |
+ } |
+ } |
+ } |
+ TemplateContent processTemplateContent() { |
+ css.Stylesheet stylesheet; |
+ |
+ _eat(TokenKind.LBRACE); |
+ |
+ int start = _peekToken.start; |
+ |
+ stylesheet = processCSS(); |
+ |
+ var elems = new TemplateElement.fragment(_makeSpan(_peekToken.start)); |
+ var templateDoc = processHTML(elems); |
+ |
+ // TODO(terry): Should allow css { } to be at beginning or end of the |
+ // template's content. Today css only allow at beginning |
+ // because the css {...} is sucked in as a text node. We'll |
+ // need a special escape for css maybe: |
+ // |
+ // ${#css} |
+ // ${/css} |
+ // |
+ // uggggly! |
+ |
+ _eat(TokenKind.RBRACE); |
+ |
+ return new TemplateContent(stylesheet, templateDoc, _makeSpan(start)); |
+ } |
+ |
+ int lastCSSIndexParsed; // TODO(terry): Hack, last good CSS parsed. |
+ |
+ css.Stylesheet processCSSContent(var cssSource, int start) { |
+ try { |
+ css.Parser parser = new css.Parser(new SourceFile( |
+ SourceFile.IN_MEMORY_FILE, cssSource.text), start); |
+ |
+ css.Stylesheet stylesheet = parser.parse(false, new ErrorMsgRedirector()); |
+ |
+ var lastParsedChar = parser.tokenizer.startIndex; |
+ |
+ lastCSSIndexParsed = lastParsedChar; |
+ |
+ return stylesheet; |
+ } catch (final cssParseException) { |
+ // TODO(terry): Need SourceSpan from CSS parser to pass onto _error. |
+ _error("Unexcepted CSS error: ${cssParseException.toString()}"); |
+ } |
+ } |
+ |
+ /* TODO(terry): Assume template { }, single close curley as a text node |
+ * inside of the template would need to be escaped maybe \} |
+ */ |
+ processHTML(TemplateElement root) { |
+ assert(root.isFragment); |
+ TagStack stack = new TagStack(root); |
+ |
+ int start = _peekToken.start; |
+ |
+ bool done = false; |
+ while (!done) { |
+ if (_maybeEat(TokenKind.LESS_THAN)) { |
+ // Open tag |
+ start = _peekToken.start; |
+ |
+ if (TokenKind.validTagName(_peek())) { |
+ Token tagToken = _next(); |
+ |
+ Map<String, TemplateAttribute> attrs = processAttributes(); |
+ |
+ String varName; |
+ if (attrs.containsKey('var')) { |
+ varName = attrs['var'].value; |
+ attrs.remove('var'); |
+ } |
+ |
+ int scopeType; // 1 implies scoped, 2 implies non-scoped element. |
+ if (_maybeEat(TokenKind.GREATER_THAN)) { |
+ scopeType = 1; |
+ } else if (_maybeEat(TokenKind.END_NO_SCOPE_TAG)) { |
+ scopeType = 2; |
+ } |
+ if (scopeType > 0) { |
+ var elem = new TemplateElement.attributes(tagToken.kind, |
+ attrs.getValues(), varName, _makeSpan(start)); |
+ stack.top().add(elem); |
+ |
+ if (scopeType == 1) { |
+ // Maybe more nested tags/text? |
+ stack.push(elem); |
+ } |
+ } |
+ } else { |
+ // Close tag |
+ _eat(TokenKind.SLASH); |
+ if (TokenKind.validTagName(_peek())) { |
+ Token tagToken = _next(); |
+ |
+ _eat(TokenKind.GREATER_THAN); |
+ |
+ var elem = stack.pop(); |
+ if (elem is TemplateElement && !elem.isFragment) { |
+ if (elem.tagTokenId != tagToken.kind) { |
+ _error('Tag doesn\'t match expected </${elem.tagName}> got ' + |
+ '</${TokenKind.tagNameFromTokenId(tagToken.kind)}>'); |
+ } |
+ } else { |
+ // Too many end tags. |
+ _error('Too many end tags at ' + |
+ '</${TokenKind.tagNameFromTokenId(tagToken.kind)}>'); |
+ } |
+ } |
+ } |
+ } else if (_maybeEat(TokenKind.START_COMMAND)) { |
+ if (_peekIdentifier()) { |
+ var commandName = identifier(); |
+ switch (commandName.name) { |
+ case "each": |
+ case "with": |
+ if (_peekIdentifier()) { |
+ var listName = identifier(); |
+ |
+ _eat(TokenKind.RBRACE); |
+ |
+ var frag = new TemplateElement.fragment( |
+ _makeSpan(_peekToken.start)); |
+ TemplateDocument docFrag = processHTML(frag); |
+ |
+ if (docFrag != null) { |
+ var span = _makeSpan(start); |
+ var cmd; |
+ if (commandName.name == "each") { |
+ cmd = new TemplateEachCommand(listName, docFrag, span); |
+ } else if (commandName.name == "with") { |
+ cmd = new TemplateWithCommand(listName, docFrag, span); |
+ } |
+ |
+ stack.top().add(cmd); |
+ stack.push(cmd); |
+ } |
+ |
+ // Process ${/commandName} |
+ _eat(TokenKind.END_COMMAND); |
+ |
+ // Close command ${/commandName} |
+ if (_peekIdentifier()) { |
+ commandName = identifier(); |
+ switch (commandName.name) { |
+ case "each": |
+ case "with": |
+ case "if": |
+ case "else": |
+ break; |
+ default: |
+ _error('Unknown command \${#${commandName}}'); |
+ } |
+ var elem = stack.pop(); |
+ if (elem is TemplateEachCommand && |
+ commandName.name == "each") { |
+ |
+ } else if (elem is TemplateWithCommand && |
+ commandName.name == "with") { |
+ |
+ } /*else if (elem is TemplateIfCommand && commandName == "if") { |
+ |
+ } |
+ */else { |
+ String expectedCmd; |
+ if (elem is TemplateEachCommand) { |
+ expectedCmd = "\${/each}"; |
+ } /* TODO(terry): else other commands as well */ |
+ _error('mismatched command expected ${expectedCmd} got...'); |
+ return; |
+ } |
+ _eat(TokenKind.RBRACE); |
+ } else { |
+ _error('Missing command name \${/commandName}'); |
+ } |
+ } else { |
+ _error("Missing listname for #each command"); |
+ } |
+ break; |
+ case "if": |
+ break; |
+ case "else": |
+ break; |
+ default: |
+ _error("Unknown template command"); |
+ } |
+ } |
+ } else if (_peekKind(TokenKind.END_COMMAND)) { |
+ break; |
+ } else { |
+ // Any text or expression nodes? |
+ var nodes = processTextNodes(); |
+ if (nodes.length > 0) { |
+ assert(stack.top() != null); |
+ for (var node in nodes) { |
+ stack.top().add(node); |
+ } |
+ } else { |
+ break; |
+ } |
+ } |
+ } |
+/* |
+ if (elems.children.length != 1) { |
+ print("ERROR: No closing end-tag for elems ${elems[elems.length - 1]}"); |
+ } |
+*/ |
+ var docChildren = new List<ASTNode>(); |
+ docChildren.add(stack.pop()); |
+ return new TemplateDocument(docChildren, _makeSpan(start)); |
+ } |
+ |
+ /* Map is used so only last unique attribute name is remembered and to quickly |
+ * find the var attribute. |
+ */ |
+ Map<String, TemplateAttribute> processAttributes() { |
+ Map<String, TemplateAttribute> attrs = new Map(); |
+ |
+ int start = _peekToken.start; |
+ String elemName; |
+ while (_peekIdentifier() || |
+ (elemName = TokenKind.elementsToName(_peek())) != null) { |
+ var attrName; |
+ if (elemName == null) { |
+ attrName = identifier(); |
+ } else { |
+ attrName = new Identifier(elemName, _makeSpan(start)); |
+ _next(); |
+ } |
+ |
+ var attrValue; |
+ |
+ // Attribute value? |
+ if (_peek() == TokenKind.ATTR_VALUE) { |
+ var tok = _next(); |
+ attrValue = new StringValue(tok.value, _makeSpan(tok.start)); |
+ } |
+ |
+ attrs[attrName.name] = |
+ new TemplateAttribute(attrName, attrValue, _makeSpan(start)); |
+ |
+ start = _peekToken.start; |
+ elemName = null; |
+ } |
+ |
+ return attrs; |
+ } |
+ |
+ 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)); |
+ } |
+ |
+ List<ASTNode> processTextNodes() { |
+ // May contain TemplateText and TemplateExpression. |
+ List<ASTNode> nodes = []; |
+ |
+ int start = _peekToken.start; |
+ bool inExpression = false; |
+ StringBuffer stringValue = new StringBuffer(); |
+ |
+ // Gobble up everything until we hit < |
+ int runningStart = _peekToken.start; |
+ while (_peek() != TokenKind.LESS_THAN && |
+ (_peek() != TokenKind.RBRACE || |
+ (_peek() == TokenKind.RBRACE && inExpression)) && |
+ _peek() != TokenKind.END_OF_FILE) { |
+ |
+ // Beginning of expression? |
+ if (_peek() == TokenKind.START_EXPRESSION) { |
+ if (stringValue.length > 0) { |
+ // We have a real text node create the text node. |
+ nodes.add(new TemplateText(stringValue.toString(), _makeSpan(start))); |
+ stringValue = new StringBuffer(); |
+ start = _peekToken.start; |
+ } |
+ inExpression = true; |
+ } |
+ |
+ var tok = _next(false); |
+ if (tok.kind == TokenKind.RBRACE && inExpression) { |
+ // We have an expression create the expression node, don't save the } |
+ inExpression = false; |
+ nodes.add(new TemplateExpression(stringValue.toString(), |
+ _makeSpan(start))); |
+ stringValue = new StringBuffer(); |
+ start = _peekToken.start; |
+ } else if (tok.kind != TokenKind.START_EXPRESSION) { |
+ // Only save the the contents between ${ and } |
+ stringValue.add(tok.text); |
+ } |
+ } |
+ |
+ if (stringValue.length > 0) { |
+ nodes.add(new TemplateText(stringValue.toString(), _makeSpan(start))); |
+ } |
+ |
+ return nodes; |
+ } |
+ |
+} |