Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(29)

Unified Diff: utils/template/parser.dart

Issue 9695048: Template parser (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Siggi's comments Created 8 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « utils/template/htmltree.dart ('k') | utils/template/source.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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;
+ }
+
+}
« no previous file with comments | « utils/template/htmltree.dart ('k') | utils/template/source.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698