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
+
+ & --> &
+ < --> <
+ > --> >
+ " --> "
+ ' --> ' ' not recommended because its not in the HTML spec (See: section 24.4.1) ' is in the XML and XHTML specs.
+ / --> /
+ */
+ //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