diff --git a/.idea/runConfigurations/tests_in_dsx_test_dart.xml b/.idea/runConfigurations/tests_in_dsx_test_dart.xml
new file mode 100644
index 00000000..9c1ec0de
--- /dev/null
+++ b/.idea/runConfigurations/tests_in_dsx_test_dart.xml
@@ -0,0 +1,7 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="tests in dsx_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
+    <option name="filePath" value="$PROJECT_DIR$/jael/test/render/dsx_test.dart" />
+    <option name="testRunnerOptions" value="-j4" />
+    <method />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/jael.iml b/jael.iml
index cef8c4b3..449f16ce 100644
--- a/jael.iml
+++ b/jael.iml
@@ -13,6 +13,7 @@
     <content url="file://$MODULE_DIR$/dsx" />
     <content url="file://$MODULE_DIR$/dsx_generator" />
     <content url="file://$MODULE_DIR$/jael">
+      <excludeFolder url="file://$MODULE_DIR$/jael/.dart_tool" />
       <excludeFolder url="file://$MODULE_DIR$/jael/.pub" />
       <excludeFolder url="file://$MODULE_DIR$/jael/build" />
     </content>
diff --git a/jael/README.md b/jael/README.md
index 6c287e9f..2e78c2a9 100644
--- a/jael/README.md
+++ b/jael/README.md
@@ -34,7 +34,7 @@ void myFunction() {
 ''';
 
     var buf = new CodeBuffer();
-    var document = jael.parseDocument(template, sourceUrl: 'test.jl');
+    var document = jael.parseDocument(template, sourceUrl: 'test.jl', asDSX: false);
     var scope = new SymbolTable(values: {
       'profile': {
         'avatar': 'thosakwe.png',
diff --git a/jael/lib/src/ast/element.dart b/jael/lib/src/ast/element.dart
index f9200db5..a8b8c2a0 100644
--- a/jael/lib/src/ast/element.dart
+++ b/jael/lib/src/ast/element.dart
@@ -33,8 +33,13 @@ abstract class Element extends ElementChild {
   ];
 
   Identifier get tagName;
+
   Iterable<Attribute> get attributes;
+
   Iterable<ElementChild> get children;
+
+  Attribute getAttribute(String name) =>
+      attributes.firstWhere((a) => a.name == name, orElse: () => null);
 }
 
 class SelfClosingElement extends Element {
diff --git a/jael/lib/src/ast/token.dart b/jael/lib/src/ast/token.dart
index 57c95308..0e4d4a04 100644
--- a/jael/lib/src/ast/token.dart
+++ b/jael/lib/src/ast/token.dart
@@ -34,8 +34,8 @@ enum TokenType {
    */
   lBracket,
   rBracket,
-  doubleCurlyL,
-  doubleCurlyR,
+  lDoubleCurly,
+  rDoubleCurly,
   lCurly,
   rCurly,
   lParen,
diff --git a/jael/lib/src/renderer.dart b/jael/lib/src/renderer.dart
index aa4a3870..a7299eb5 100644
--- a/jael/lib/src/renderer.dart
+++ b/jael/lib/src/renderer.dart
@@ -7,8 +7,8 @@ import 'text/scanner.dart';
 
 /// Parses a Jael document.
 Document parseDocument(String text,
-    {sourceUrl, void onError(JaelError error)}) {
-  var scanner = scan(text, sourceUrl: sourceUrl);
+    {sourceUrl, bool asDSX: false, void onError(JaelError error)}) {
+  var scanner = scan(text, sourceUrl: sourceUrl, asDSX: asDSX);
 
   //scanner.tokens.forEach(print);
 
@@ -16,7 +16,7 @@ Document parseDocument(String text,
     scanner.errors.forEach(onError);
   else if (scanner.errors.isNotEmpty) throw scanner.errors.first;
 
-  var parser = new Parser(scanner);
+  var parser = new Parser(scanner, asDSX: asDSX);
   var doc = parser.parseDocument();
 
   if (parser.errors.isNotEmpty && onError != null)
diff --git a/jael/lib/src/text/parser.dart b/jael/lib/src/text/parser.dart
index 52ae9aa0..3a162072 100644
--- a/jael/lib/src/text/parser.dart
+++ b/jael/lib/src/text/parser.dart
@@ -5,11 +5,12 @@ import 'scanner.dart';
 class Parser {
   final List<JaelError> errors = [];
   final Scanner scanner;
+  final bool asDSX;
 
   Token _current;
   int _index = -1;
 
-  Parser(this.scanner);
+  Parser(this.scanner, {this.asDSX: false});
 
   Token get current => _current;
 
@@ -148,7 +149,7 @@ class Parser {
   Text parseText() => next(TokenType.text) ? new Text(_current) : null;
 
   Interpolation parseInterpolation() {
-    if (!next(TokenType.doubleCurlyL)) return null;
+    if (!next(asDSX ? TokenType.lCurly : TokenType.lDoubleCurly)) return null;
     var doubleCurlyL = _current;
 
     var expression = parseExpression(0);
@@ -159,9 +160,10 @@ class Parser {
       return null;
     }
 
-    if (!next(TokenType.doubleCurlyR)) {
+    if (!next(asDSX ? TokenType.rCurly : TokenType.rDoubleCurly)) {
+      var expected = asDSX ? '}' : '}}';
       errors.add(new JaelError(JaelErrorSeverity.error,
-          'Missing closing "}}" in interpolation.', expression.span));
+          'Missing closing "$expected" in interpolation.', expression.span));
       return null;
     }
 
@@ -260,7 +262,8 @@ class Parser {
     if (tagName2.name != tagName.name) {
       errors.add(new JaelError(
           JaelErrorSeverity.error,
-          'Mismatched closing tags. Expected "${tagName.span.text}"; got "${tagName2.name}" instead.',
+          'Mismatched closing tags. Expected "${tagName.span
+              .text}"; got "${tagName2.name}" instead.',
           lt2.span));
       return null;
     }
@@ -291,21 +294,41 @@ class Parser {
 
     if (next(TokenType.equals)) {
       equals = _current;
-    } else if (next(TokenType.nequ)) {
+    } else if (!asDSX && next(TokenType.nequ)) {
       nequ = _current;
     } else {
       return new Attribute(id, string, null, null, null);
     }
 
-    var value = parseExpression(0);
+    if (!asDSX) {
+      var value = parseExpression(0);
+
+      if (value == null) {
+        errors.add(new JaelError(JaelErrorSeverity.error,
+            'Missing expression in attribute.', equals?.span ?? nequ.span));
+        return null;
+      }
+
+      return new Attribute(id, string, equals, nequ, value);
+    } else {
+      // Find either a string, or an interpolation.
+      var value = implicitString();
+
+      if (value != null) {
+        return new Attribute(id, string, equals, nequ, value);
+      }
+
+      var interpolation = parseInterpolation();
+
+      if (interpolation != null) {
+        return new Attribute(
+            id, string, equals, nequ, interpolation.expression);
+      }
 
-    if (value == null) {
       errors.add(new JaelError(JaelErrorSeverity.error,
           'Missing expression in attribute.', equals?.span ?? nequ.span));
       return null;
     }
-
-    return new Attribute(id, string, equals, nequ, value);
   }
 
   Expression parseExpression(int precedence) {
diff --git a/jael/lib/src/text/scanner.dart b/jael/lib/src/text/scanner.dart
index c8de1cac..ea8d8ef9 100644
--- a/jael/lib/src/text/scanner.dart
+++ b/jael/lib/src/text/scanner.dart
@@ -12,7 +12,8 @@ final RegExp _string1 = new RegExp(
 final RegExp _string2 = new RegExp(
     r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"');
 
-Scanner scan(String text, {sourceUrl}) => new _Scanner(text, sourceUrl)..scan();
+Scanner scan(String text, {sourceUrl, bool asDSX: false}) =>
+    new _Scanner(text, sourceUrl)..scan(asDSX: asDSX);
 
 abstract class Scanner {
   List<JaelError> get errors;
@@ -24,8 +25,8 @@ final RegExp _htmlComment = new RegExp(r'<!--[^$]*-->');
 
 final Map<Pattern, TokenType> _expressionPatterns = {
 //final Map<Pattern, TokenType> _htmlPatterns = {
-  '{{': TokenType.doubleCurlyL,
-  '{{-': TokenType.doubleCurlyL,
+  '{{': TokenType.lDoubleCurly,
+  '{{-': TokenType.lDoubleCurly,
 
   //
   _htmlComment: TokenType.htmlComment,
@@ -42,7 +43,7 @@ final Map<Pattern, TokenType> _expressionPatterns = {
 //};
 
 //final Map<Pattern, TokenType> _expressionPatterns = {
-  '}}': TokenType.doubleCurlyR,
+  '}}': TokenType.rDoubleCurly,
 
   // Keywords
   'new': TokenType.$new,
@@ -92,10 +93,10 @@ class _Scanner implements Scanner {
     _scanner = new SpanScanner(text, sourceUrl: sourceUrl);
   }
 
-  void scan() {
+  void scan({bool asDSX: false}) {
     while (!_scanner.isDone) {
       if (state == _ScannerState.html) {
-        scanHtml();
+        scanHtml(asDSX);
       } else if (state == _ScannerState.freeText) {
         // Just keep parsing until we hit "</"
         var start = _scanner.state, end = start;
@@ -104,8 +105,8 @@ class _Scanner implements Scanner {
           // Skip through comments
           if (_scanner.scan(_htmlComment)) continue;
 
-          // Break on {{
-          if (_scanner.matches('{{')) {
+          // Break on {{ or {
+          if (_scanner.matches(asDSX ? '{' : '{{')) {
             state = _ScannerState.html;
             //_scanner.position--;
             break;
@@ -169,7 +170,7 @@ class _Scanner implements Scanner {
     }
   }
 
-  void scanHtml() {
+  void scanHtml(bool asDSX) {
     var brackets = new Queue<Token>();
 
     do {
@@ -247,7 +248,8 @@ class _Scanner implements Scanner {
                 break;
               }
             }
-          } else if (token.type == TokenType.doubleCurlyR) {
+          } else if (token.type ==
+              (asDSX ? TokenType.rCurly : TokenType.rDoubleCurly)) {
             state = _ScannerState.freeText;
             break;
           }
diff --git a/jael/test/render/dsx_test.dart b/jael/test/render/dsx_test.dart
new file mode 100644
index 00000000..87925998
--- /dev/null
+++ b/jael/test/render/dsx_test.dart
@@ -0,0 +1,43 @@
+import 'package:jael/jael.dart';
+import 'package:symbol_table/symbol_table.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('attributes', () {
+    var doc = parseDSX('''
+    <foo bar="baz" yes={no} />
+    ''');
+
+    var foo = doc.root as SelfClosingElement;
+    expect(foo.tagName.name, 'foo');
+    expect(foo.attributes, hasLength(2));
+    expect(foo.getAttribute('bar'), isNotNull);
+    expect(foo.getAttribute('yes'), isNotNull);
+    expect(foo.getAttribute('bar').value.compute(null), 'baz');
+    expect(
+        foo
+            .getAttribute('yes')
+            .value
+            .compute(new SymbolTable(values: {'no': 'maybe'})),
+        'maybe');
+  });
+
+  test('children', () {
+    var doc = parseDSX('''
+    <foo bar="baz" yes={no}>
+      <bar>{24 * 3}</bar>
+    </foo>
+    ''');
+
+    var bar = doc.root.children.first as RegularElement;
+    expect(bar.tagName.name, 'bar');
+
+    var interpolation = bar.children.first as Interpolation;
+    expect(interpolation.expression.compute(null), 24 * 3);
+  });
+}
+
+Document parseDSX(String text) {
+  return parseDocument(text,
+      sourceUrl: 'test.dsx', asDSX: true, onError: (e) => throw e);
+}
diff --git a/jael/test/text/scan_test.dart b/jael/test/text/scan_test.dart
index 70bba9c1..080aace7 100644
--- a/jael/test/text/scan_test.dart
+++ b/jael/test/text/scan_test.dart
@@ -64,13 +64,13 @@ main() {
     expect(tokens[6], isToken(TokenType.number, '2'));
     expect(tokens[7], isToken(TokenType.gt));
     expect(tokens[8], isToken(TokenType.text, 'three'));
-    expect(tokens[9], isToken(TokenType.doubleCurlyL));
+    expect(tokens[9], isToken(TokenType.lDoubleCurly));
     expect(tokens[10], isToken(TokenType.id, 'four'));
     expect(tokens[11], isToken(TokenType.gt));
     expect(tokens[12], isToken(TokenType.id, 'five'));
     expect(tokens[13], isToken(TokenType.dot));
     expect(tokens[14], isToken(TokenType.id, 'six'));
-    expect(tokens[15], isToken(TokenType.doubleCurlyR));
+    expect(tokens[15], isToken(TokenType.rDoubleCurly));
     expect(tokens[16], isToken(TokenType.lt));
     expect(tokens[17], isToken(TokenType.slash));
     expect(tokens[18], isToken(TokenType.id, 'ul'));