Initial commit
diff --git a/mustache.dart b/mustache.dart
new file mode 100644
index 0000000..fce8a7c
--- /dev/null
+++ b/mustache.dart
@@ -0,0 +1,188 @@
+library mustache;

+

+import 'scanner.dart';

+

+var scannerTests = [

+'_{{variable}}_',

+'_{{variable}}',

+'{{variable}}_',

+'{{variable}}',

+' { ',

+' } ',

+' {} ',

+' }{} ',

+'{{{escaped text}}}',

+'{{&escaped text}}',

+'{{!comment}}',

+'{{#section}}oi{{/section}}',

+'{{^section}}oi{{/section}}',

+'{{>partial}}'

+];

+

+main2() {

+	for (var src in scannerTests) {

+		print('${_pad(src, 40)}${scan(src)}');

+	}	

+}

+

+main() {

+	var source = '{{#section}}_{{var}}_{{/section}}';

+	var tokens = scan(source);

+	var node = _parse(tokens);

+	//var output = render(node, {"section": {"var": "bob"}});

+	

+	var t = new Template(node);

+	var output = t.render({"section": {"var": "bob"}});

+

+	//_visit(node, (n) => print('visit: $n'));

+	print(source);

+	print(tokens);

+	print(node);

+	print(output);

+}

+

+_pad(String s, int len) {

+	for (int i = s.length; i < len; i++)

+		s = s + ' ';

+	return s;

+}

+

+// http://mustache.github.com/mustache.5.html

+

+class Node {

+	Node(this.type, this.value);

+	final int type;

+	final String value;

+	final List<Node> children = new List<Node>();

+

+	//FIXME

+	toString() => stringify(0);

+

+	stringify(int indent) {

+		var pad = '';

+		for (int i = 0; i < indent; i++)

+			pad = '$pad-';

+		var s = '$pad${tokenTypeString(type)} $value\n';		

+		++indent;

+		for (var c in children) {

+			s += c.stringify(indent);

+		}

+		return s;

+	}

+}

+

+Node _parse(List<Token> tokens) {

+	var stack = new List<Node>()..add(new Node(OPEN_SECTION, 'root'));

+	for (var t in tokens) {

+		if (t.type == TEXT || t.type == VARIABLE) {

+			stack.last.children.add(new Node(t.type, t.value));

+		} else if (t.type == OPEN_SECTION || t.type == OPEN_INV_SECTION) {

+			var child = new Node(t.type, t.value);

+			stack.last.children.add(child);

+			stack.add(child);

+		} else if (t.type == CLOSE_SECTION) {

+			assert(stack.last.value == t.value); //FIXME throw an exception if these don't match.

+			stack.removeLast();

+		} else {

+			throw new UnimplementedError();

+		}

+	}

+

+	return stack.last;

+}

+

+class Template {

+	Template(this._root);

+	final Node _root;

+	final ctl = new List(); //TODO use streams.

+	final stack = new List();

+

+	render(values) {

+		ctl.clear();

+		stack.clear();

+		stack.add(values);	

+		_root.children.forEach(_renderNode);

+		return ctl;

+	}

+

+	_renderNode(node) {

+		switch (node.type) {

+			case TEXT:

+				_renderText(node);

+				break;

+			case VARIABLE:

+				_renderVariable(node);

+				break;

+			case OPEN_SECTION:

+				_renderSection(node);

+				break;

+			case OPEN_INV_SECTION:

+				_renderInvSection(node);

+				break;

+			default:

+				throw new UnimplementedError();

+		}

+	}

+

+	_renderText(node) {

+		ctl.add(node.value);

+	}

+

+	_renderVariable(node) {

+		final value = stack.last[node.value]; //TODO optional warning if variable is null or missing.

+		final s = _htmlEscape(value.toString());

+		ctl.add(s);

+	}

+

+	_renderSectionWithValue(node, value) {

+		stack.add(value);

+		node.children.forEach(_renderNode);

+		stack.removeLast();

+	}

+

+	_renderSection(node) {

+		final value = stack.last[node.value];

+		if (value is List) {

+			value.forEach((v) => _renderSectionWithValue(node, v));

+		} else if (value is Map) {

+			_renderSectionWithValue(node, value);

+		} else if (value == true) {

+			_renderSectionWithValue(node, {});

+		} else {

+			print('boom!'); //FIXME

+		}

+	}

+

+	_renderInvSection(node) {

+		final val = stack.last[node.value];

+		if ((val is List && val.isEmpty)

+				|| val == null

+				|| val == false) {

+			_renderSectionWithValue(node, {});

+		}

+	}

+

+	/*

+	escape

+

+	& --> &amp;

+	< --> &lt;

+	> --> &gt;

+	" --> &quot;

+	' --> &#x27;     &apos; not recommended because its not in the HTML spec (See: section 24.4.1) &apos; is in the XML and XHTML specs.

+	/ --> &#x2F; 

+	*/

+	//TODO

+	String _htmlEscape(String s) {

+		return s;

+	}

+}

+

+_visit(Node root, visitor(Node n)) {

+	var stack = new List<Node>()..add(root);

+	while (!stack.isEmpty) {

+		var node = stack.removeLast();

+		stack.addAll(node.children);

+		visitor(node);

+	}

+}

diff --git a/scanner.dart b/scanner.dart
new file mode 100644
index 0000000..cc34e58
--- /dev/null
+++ b/scanner.dart
@@ -0,0 +1,212 @@
+library mustache.scanner;

+

+List<Token> scan(String source) => new _Scanner(source).scan();

+

+abstract class Token {

+	int get type;

+	String get value;

+}

+

+const int TEXT = 1;

+const int VARIABLE = 2;

+const int PARTIAL = 3;

+const int OPEN_SECTION = 4;

+const int OPEN_INV_SECTION = 5;

+const int CLOSE_SECTION = 6;

+const int COMMENT = 7;

+

+tokenTypeString(int type) => ['?', 'Text', 'Var', 'Par', 'Open', 'OpenInv', 'Close', 'Comment'][type];

+

+const int _EOF = -1;

+const int _NEWLINE = 10;

+const int _EXCLAIM = 33;

+const int _QUOTE = 34;

+const int _HASH = 35;

+const int _AMP = 38;

+const int _APOS = 39;

+const int _FORWARD_SLASH = 47;

+const int _LT = 60;

+const int _GT = 62;

+const int _CARET = 94;

+

+const int _OPEN_MUSTACHE = 123;

+const int _CLOSE_MUSTACHE = 125;

+

+class _Token implements Token {

+	_Token(this.type, this.value);

+	_Token.fromChar(this.type, int charCode)

+		: value = new String.fromCharCode(charCode);

+	final int type;

+	final String value;

+	toString() => "${tokenTypeString(type)}: \"${value.replaceAll('\n', '\\n')}\"";

+}

+

+class _Scanner {

+	_Scanner(String source) : _r = new _CharReader(source);

+

+	_CharReader _r;

+	List<Token> _tokens = new List<Token>();

+

+	int _read() => _r.read();

+	int _peek() => _r.peek();

+

+	_add(Token t) => _tokens.add(t);

+

+	_expect(int c) {

+		if (c != _read())

+			throw new FormatException('Expected character: ${new String.fromCharCode(c)}');

+	}

+

+	String _readString() => _r.readWhile(

+		(c) => c != _OPEN_MUSTACHE && c != _CLOSE_MUSTACHE && c != _EOF);

+

+	List<Token> scan() {

+		while(true) {

+			switch(_peek()) {

+				case _EOF:

+					return _tokens;

+				case _OPEN_MUSTACHE:

+					_scanMustacheTag();

+					break;

+				default:

+					_scanText();

+			}

+		}

+	}

+

+	_scanText() {

+		while(true) {

+			switch(_peek()) {

+				case _EOF:

+					return;

+				case _OPEN_MUSTACHE:

+					return;

+				case _CLOSE_MUSTACHE:

+					_read();

+					_add(new _Token.fromChar(TEXT, _CLOSE_MUSTACHE));

+					break;

+				default:

+					_add(new _Token(TEXT, _readString()));

+			}

+		}	

+	}

+

+	_scanMustacheTag() {

+		assert(_peek() == _OPEN_MUSTACHE);

+		_read();

+

+		if (_peek() != _OPEN_MUSTACHE) {

+			_add(new _Token.fromChar(TEXT, _OPEN_MUSTACHE));

+			return;

+		}

+

+		_read();

+		switch(_peek()) {

+			case _EOF:

+				throw new FormatException('Unexpected EOF.');

+

+			// Escaped text {{{ ... }}}

+			case _OPEN_MUSTACHE:

+				_read();

+				_add(new _Token(TEXT, _readString()));

+				_expect(_CLOSE_MUSTACHE);

+				break;

+      			

+			// Escaped text {{& ... }}

+			case _AMP:

+				_read();

+				_add(new _Token(TEXT, _readString()));

+				break;

+

+			// Comment {{! ... }}

+			case _EXCLAIM:

+				_read();

+				_add(new _Token(COMMENT, _readString()));

+				break;

+

+			// Partial {{> ... }}

+			case _GT:

+				_read();

+				_add(new _Token(PARTIAL, _readString()));

+				break;

+

+			// Open section {{# ... }}

+			case _HASH:

+				_read();

+				_add(new _Token(OPEN_SECTION, _readString()));

+				break;

+

+			// Open inverted section {{^ ... }}

+			case _CARET:

+				_read();

+				_add(new _Token(OPEN_INV_SECTION, _readString()));

+				break;

+

+			// Close section {{/ ... }}

+			case _FORWARD_SLASH:

+				_read();

+				_add(new _Token(CLOSE_SECTION, _readString()));

+				break;

+

+			// Variable {{ ... }}

+			default:

+				_add(new _Token(VARIABLE, _readString()));

+		}

+

+		_expect(_CLOSE_MUSTACHE);

+		_expect(_CLOSE_MUSTACHE);

+	}

+}

+

+

+//FIXME return _EOF

+class _CharReader {

+  _CharReader(String source)

+      : _source = source,

+        _itr = source.runes.iterator {  //FIXME runes etc. Not sure if this is the right count.

+        

+    if (source == null)

+      throw new ArgumentError('Source is null.');

+    

+    _i = 0;

+    

+    if (source == '') {

+    	_c = _EOF;

+    } else {

+    	_itr.moveNext();

+    	_c = _itr.current;

+    }

+  }

+  

+  String _source;

+  Iterator<int> _itr;

+  int _i, _c;

+  

+  int read() {

+    var c = _c;

+    if (_itr.moveNext()) {

+    	_i++;

+    	_c = _itr.current;

+    } else {

+    	_c = _EOF;

+    }

+    return c;

+  }

+  

+  int peek() => _c;

+  

+  String readWhile(bool test(int charCode)) {

+    

+    if (peek() == _EOF)

+      throw new FormatException('Unexpected end of input: $_i');

+    

+    int start = _i;

+    

+    while (peek() != _EOF && test(peek())) {

+      read();

+    }

+    

+    int end = peek() == _EOF ? _source.length : _i;

+    return _source.slice(start, end);

+  }

+}
\ No newline at end of file