Restructure YAML package suitable for pub lish
After this I can use tools/publish_pkg.py upload script to fix dartbug.com/4126

Review URL: https://codereview.chromium.org//11622011

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/yaml@17685 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/yaml/README.md b/pkgs/yaml/README.md
new file mode 100644
index 0000000..ad7678f
--- /dev/null
+++ b/pkgs/yaml/README.md
@@ -0,0 +1,24 @@
+A parser for [YAML](http://www.yaml.org/).
+
+Use `loadYaml` to load a single document, or `loadYamlStream` to load a
+stream of documents. For example:
+
+    import 'package:yaml/yaml.dart';
+    main() {
+      var doc = loadYaml("YAML: YAML Ain't Markup Language");
+      print(doc['YAML']);
+    }
+
+This library currently doesn't support dumping to YAML. You should use
+`stringify` from `dart:json` instead:
+
+    import 'dart:json' as json;
+    import 'package:yaml/yaml.dart';
+    main() {
+      var doc = loadYaml("YAML: YAML Ain't Markup Language");
+      print(json.stringify(doc));
+    }
+
+The source code for this package is at <http://code.google.com/p/dart>.
+Please file issues at <http://dartbug.com>. Other questions or comments can be
+directed to the Dart mailing list at <mailto:misc@dartlang.org>.
diff --git a/pkgs/yaml/lib/composer.dart b/pkgs/yaml/lib/composer.dart
new file mode 100644
index 0000000..e9c1879
--- /dev/null
+++ b/pkgs/yaml/lib/composer.dart
@@ -0,0 +1,173 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of yaml;
+
+/// Takes a parsed YAML document (what the spec calls the "serialization tree")
+/// and resolves aliases, resolves tags, and parses scalars to produce the
+/// "representation graph".
+class _Composer extends _Visitor {
+  /// The root node of the serialization tree.
+  _Node root;
+
+  /// Map from anchor names to the most recent representation graph node with
+  /// that anchor.
+  Map<String, _Node> anchors;
+
+  /// The next id to use for the represenation graph's anchors. The spec doesn't
+  /// use anchors in the representation graph, but we do so that the constructor
+  /// can ensure that the same node in the representation graph produces the
+  /// same native object.
+  int idCounter;
+
+  _Composer(this.root) : this.anchors = <String, _Node>{}, this.idCounter = 0;
+
+  /// Runs the Composer to produce a representation graph.
+  _Node compose() => root.visit(this);
+
+  /// Returns the anchor to which an alias node refers.
+  _Node visitAlias(_AliasNode alias) {
+    if (!anchors.containsKey(alias.anchor)) {
+      throw new YamlException("no anchor for alias ${alias.anchor}");
+    }
+    return anchors[alias.anchor];
+  }
+
+  /// Parses a scalar node according to its tag, or auto-detects the type if no
+  /// tag exists. Currently this only supports the YAML core type schema.
+  _Node visitScalar(_ScalarNode scalar) {
+    if (scalar.tag.name == "!") {
+      return setAnchor(scalar, parseString(scalar.content));
+    } else if (scalar.tag.name == "?") {
+      for (var fn in [parseNull, parseBool, parseInt, parseFloat]) {
+        var result = fn(scalar.content);
+        if (result != null) return result;
+      }
+      return setAnchor(scalar, parseString(scalar.content));
+    }
+
+    // TODO(nweiz): support the full YAML type repository
+    var tagParsers = {
+      'null': parseNull, 'bool': parseBool, 'int': parseInt,
+      'float': parseFloat, 'str': parseString
+    };
+
+    for (var key in tagParsers.keys) {
+      if (scalar.tag.name != _Tag.yaml(key)) continue;
+      var result = tagParsers[key](scalar.content);
+      if (result != null) return setAnchor(scalar, result);
+      throw new YamlException('invalid literal for $key: "${scalar.content}"');
+    }
+
+    throw new YamlException('undefined tag: "${scalar.tag.name}"');
+  }
+
+  /// Assigns a tag to the sequence and recursively composes its contents.
+  _Node visitSequence(_SequenceNode seq) {
+    var tagName = seq.tag.name;
+    if (tagName != "!" && tagName != "?" && tagName != _Tag.yaml("seq")) {
+      throw new YamlException("invalid tag for sequence: ${tagName}");
+    }
+
+    var result = setAnchor(seq, new _SequenceNode(_Tag.yaml("seq"), null));
+    result.content = super.visitSequence(seq);
+    return result;
+  }
+
+  /// Assigns a tag to the mapping and recursively composes its contents.
+  _Node visitMapping(_MappingNode map) {
+    var tagName = map.tag.name;
+    if (tagName != "!" && tagName != "?" && tagName != _Tag.yaml("map")) {
+      throw new YamlException("invalid tag for mapping: ${tagName}");
+    }
+
+    var result = setAnchor(map, new _MappingNode(_Tag.yaml("map"), null));
+    result.content = super.visitMapping(map);
+    return result;
+  }
+
+  /// If the serialization tree node [anchored] has an anchor, records that
+  /// that anchor is pointing to the representation graph node [result].
+  _Node setAnchor(_Node anchored, _Node result) {
+    if (anchored.anchor == null) return result;
+    result.anchor = '${idCounter++}';
+    anchors[anchored.anchor] = result;
+    return result;
+  }
+
+  /// Parses a null scalar.
+  _ScalarNode parseNull(String content) {
+    if (!new RegExp("^(null|Null|NULL|~|)\$").hasMatch(content)) return null;
+    return new _ScalarNode(_Tag.yaml("null"), value: null);
+  }
+
+  /// Parses a boolean scalar.
+  _ScalarNode parseBool(String content) {
+    var match = new RegExp("^(?:(true|True|TRUE)|(false|False|FALSE))\$").
+      firstMatch(content);
+    if (match == null) return null;
+    return new _ScalarNode(_Tag.yaml("bool"), value: match.group(1) != null);
+  }
+
+  /// Parses an integer scalar.
+  _ScalarNode parseInt(String content) {
+    var match = new RegExp("^[-+]?[0-9]+\$").firstMatch(content);
+    if (match != null) {
+      return new _ScalarNode(_Tag.yaml("int"),
+          value: int.parse(match.group(0)));
+    }
+
+    match = new RegExp("^0o([0-7]+)\$").firstMatch(content);
+    if (match != null) {
+      // TODO(nweiz): clean this up when Dart can parse an octal string
+      var n = 0;
+      for (var c in match.group(1).charCodes) {
+        n *= 8;
+        n += c - 48;
+      }
+      return new _ScalarNode(_Tag.yaml("int"), value: n);
+    }
+
+    match = new RegExp("^0x[0-9a-fA-F]+\$").firstMatch(content);
+    if (match != null) {
+      return new _ScalarNode(_Tag.yaml("int"),
+          value: int.parse(match.group(0)));
+    }
+
+    return null;
+  }
+
+  /// Parses a floating-point scalar.
+  _ScalarNode parseFloat(String content) {
+    var match = new RegExp(
+        "^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?\$").
+      firstMatch(content);
+    if (match != null) {
+      // YAML allows floats of the form "0.", but Dart does not. Fix up those
+      // floats by removing the trailing dot.
+      var matchStr = match.group(0).replaceAll(new RegExp(r"\.$"), "");
+      return new _ScalarNode(_Tag.yaml("float"),
+          value: double.parse(matchStr));
+    }
+
+    match = new RegExp("^([+-]?)\.(inf|Inf|INF)\$").firstMatch(content);
+    if (match != null) {
+      var infinityStr = match.group(1) == "-" ? "-Infinity" : "Infinity";
+      return new _ScalarNode(_Tag.yaml("float"),
+          value: double.parse(infinityStr));
+    }
+
+    match = new RegExp("^\.(nan|NaN|NAN)\$").firstMatch(content);
+    if (match != null) {
+      return new _ScalarNode(_Tag.yaml("float"),
+          value: double.parse("NaN"));
+    }
+
+    return null;
+  }
+
+  /// Parses a string scalar.
+  _ScalarNode parseString(String content) =>
+    new _ScalarNode(_Tag.yaml("str"), value: content);
+}
diff --git a/pkgs/yaml/lib/constructor.dart b/pkgs/yaml/lib/constructor.dart
new file mode 100644
index 0000000..73a62a8
--- /dev/null
+++ b/pkgs/yaml/lib/constructor.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of yaml;
+
+/// Takes a parsed and composed YAML document (what the spec calls the
+/// "representation graph") and creates native Dart objects that represent that
+/// document.
+class _Constructor extends _Visitor {
+  /// The root node of the representation graph.
+  _Node root;
+
+  /// Map from anchor names to the most recent Dart node with that anchor.
+  Map<String, dynamic> anchors;
+
+  _Constructor(this.root) : this.anchors = {};
+
+  /// Runs the Constructor to produce a Dart object.
+  construct() => root.visit(this);
+
+  /// Returns the value of a scalar.
+  visitScalar(_ScalarNode scalar) => scalar.value;
+
+  /// Converts a sequence into a List of Dart objects.
+  visitSequence(_SequenceNode seq) {
+    var anchor = getAnchor(seq);
+    if (anchor != null) return anchor;
+    var dartSeq = setAnchor(seq, []);
+    dartSeq.addAll(super.visitSequence(seq));
+    return dartSeq;
+  }
+
+  /// Converts a mapping into a Map of Dart objects.
+  visitMapping(_MappingNode map) {
+    var anchor = getAnchor(map);
+    if (anchor != null) return anchor;
+    var dartMap = setAnchor(map, new YamlMap());
+    super.visitMapping(map).forEach((k, v) { dartMap[k] = v; });
+    return dartMap;
+  }
+
+  /// Returns the Dart object that already represents [anchored], if such a
+  /// thing exists.
+  getAnchor(_Node anchored) {
+    if (anchored.anchor == null) return null;
+    if (anchors.containsKey(anchored.anchor)) return anchors[anchored.anchor];
+  }
+
+  /// Records that [value] is the Dart object representing [anchored].
+  setAnchor(_Node anchored, value) {
+    if (anchored.anchor == null) return value;
+    anchors[anchored.anchor] = value;
+    return value;
+  }
+}
diff --git a/pkgs/yaml/lib/deep_equals.dart b/pkgs/yaml/lib/deep_equals.dart
new file mode 100644
index 0000000..bce4aad
--- /dev/null
+++ b/pkgs/yaml/lib/deep_equals.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library deep_equals;
+
+/// Returns whether two objects are structurally equivalent. This considers NaN
+/// values to be equivalent. It also handles self-referential structures.
+bool deepEquals(obj1, obj2, [List parents1, List parents2]) {
+  if (identical(obj1, obj2)) return true;
+  if (parents1 == null) {
+    parents1 = [];
+    parents2 = [];
+  }
+
+  // parents1 and parents2 are guaranteed to be the same size.
+  for (var i = 0; i < parents1.length; i++) {
+    var loop1 = identical(obj1, parents1[i]);
+    var loop2 = identical(obj2, parents2[i]);
+    // If both structures loop in the same place, they're equal at that point in
+    // the structure. If one loops and the other doesn't, they're not equal.
+    if (loop1 && loop2) return true;
+    if (loop1 || loop2) return false;
+  }
+
+  parents1.add(obj1);
+  parents2.add(obj2);
+  try {
+    if (obj1 is List && obj2 is List) {
+      return _listEquals(obj1, obj2, parents1, parents2);
+    } else if (obj1 is Map && obj2 is Map) {
+      return _mapEquals(obj1, obj2, parents1, parents2);
+    } else if (obj1 is double && obj2 is double) {
+      return _doubleEquals(obj1, obj2);
+    } else {
+      return obj1 == obj2;
+    }
+  } finally {
+    parents1.removeLast();
+    parents2.removeLast();
+  }
+}
+
+/// Returns whether [list1] and [list2] are structurally equal. 
+bool _listEquals(List list1, List list2, List parents1, List parents2) {
+  if (list1.length != list2.length) return false;
+
+  for (var i = 0; i < list1.length; i++) {
+    if (!deepEquals(list1[i], list2[i], parents1, parents2)) return false;
+  }
+
+  return true;
+}
+
+/// Returns whether [map1] and [map2] are structurally equal. 
+bool _mapEquals(Map map1, Map map2, List parents1, List parents2) {
+  if (map1.length != map2.length) return false;
+
+  for (var key in map1.keys) {
+    if (!map2.containsKey(key)) return false;
+    if (!deepEquals(map1[key], map2[key], parents1, parents2)) return false;
+  }
+
+  return true;
+}
+
+/// Returns whether two doubles are equivalent. This differs from `d1 == d2` in
+/// that it considers NaN to be equal to itself.
+bool _doubleEquals(double d1, double d2) {
+  if (d1.isNaN && d2.isNaN) return true;
+  return d1 == d2;
+}
diff --git a/pkgs/yaml/lib/model.dart b/pkgs/yaml/lib/model.dart
new file mode 100644
index 0000000..8c111af
--- /dev/null
+++ b/pkgs/yaml/lib/model.dart
@@ -0,0 +1,234 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of yaml;
+
+// This file contains the node classes for the internal representations of YAML
+// documents. These nodes are used for both the serialization tree and the
+// representation graph.
+
+/// A tag that indicates the type of a YAML node.
+class _Tag {
+  // TODO(nweiz): it would better match the semantics of the spec if there were
+  // a singleton instance of this class for each tag.
+
+  static const SCALAR_KIND = 0;
+  static const SEQUENCE_KIND = 1;
+  static const MAPPING_KIND = 2;
+
+  static const String YAML_URI_PREFIX = 'tag:yaml.org,2002:';
+
+  /// The name of the tag, either a URI or a local tag beginning with "!".
+  final String name;
+
+  /// The kind of the tag: SCALAR_KIND, SEQUENCE_KIND, or MAPPING_KIND.
+  final int kind;
+
+  _Tag(this.name, this.kind);
+
+  _Tag.scalar(String name) : this(name, SCALAR_KIND);
+  _Tag.sequence(String name) : this(name, SEQUENCE_KIND);
+  _Tag.mapping(String name) : this(name, MAPPING_KIND);
+
+  /// Returns the standard YAML tag URI for [type].
+  static String yaml(String type) => "tag:yaml.org,2002:$type";
+
+  /// Two tags are equal if their URIs are equal.
+  operator ==(other) {
+    if (other is! _Tag) return false;
+    return name == other.name;
+  }
+
+  String toString() {
+    if (name.startsWith(YAML_URI_PREFIX)) {
+      return '!!${name.substring(YAML_URI_PREFIX.length)}';
+    } else {
+      return '!<$name>';
+    }
+  }
+
+  int get hashCode => name.hashCode;
+}
+
+/// The abstract class for YAML nodes.
+abstract class _Node {
+  /// Every YAML node has a tag that describes its type.
+  _Tag tag;
+
+  /// Any YAML node can have an anchor associated with it.
+  String anchor;
+
+  _Node(this.tag, [this.anchor]);
+
+  bool operator ==(other) {
+    if (other is! _Node) return false;
+    return tag == other.tag;
+  }
+
+  int get hashCode => _hashCode([tag, anchor]);
+
+  visit(_Visitor v);
+}
+
+/// A sequence node represents an ordered list of nodes.
+class _SequenceNode extends _Node {
+  /// The nodes in the sequence.
+  List<_Node> content;
+
+  _SequenceNode(String tagName, this.content)
+    : super(new _Tag.sequence(tagName));
+
+  /// Two sequences are equal if their tags and contents are equal.
+  bool operator ==(other) {
+    // Should be super != other; bug 2554
+    if (!(super == other) || other is! _SequenceNode) return false;
+    if (content.length != other.content.length) return false;
+    for (var i = 0; i < content.length; i++) {
+      if (content[i] != other.content[i]) return false;
+    }
+    return true;
+  }
+
+  String toString() =>
+      '$tag [${Strings.join(content.mappedBy((e) => '$e'), ', ')}]';
+
+  int get hashCode => super.hashCode ^ _hashCode(content);
+
+  visit(_Visitor v) => v.visitSequence(this);
+}
+
+/// An alias node is a reference to an anchor.
+class _AliasNode extends _Node {
+  _AliasNode(String anchor) : super(new _Tag.scalar(_Tag.yaml("str")), anchor);
+
+  visit(_Visitor v) => v.visitAlias(this);
+}
+
+/// A scalar node represents all YAML nodes that have a single value.
+class _ScalarNode extends _Node {
+  /// The string value of the scalar node, if it was created by the parser.
+  final String _content;
+
+  /// The Dart value of the scalar node, if it was created by the composer.
+  final value;
+
+  /// Creates a new Scalar node.
+  ///
+  /// Exactly one of [content] and [value] should be specified. Content should
+  /// be specified for a newly-parsed scalar that hasn't yet been composed.
+  /// Value should be specified for a composed scalar, although `null` is a
+  /// valid value.
+  _ScalarNode(String tagName, {String content, this.value})
+   : _content = content,
+     super(new _Tag.scalar(tagName));
+
+  /// Two scalars are equal if their string representations are equal.
+  bool operator ==(other) {
+    // Should be super != other; bug 2554
+    if (!(super == other) || other is! _ScalarNode) return false;
+    return content == other.content;
+  }
+
+  /// Returns the string representation of the scalar. After composition, this
+  /// is equal to the canonical serialization of the value of the scalar.
+  String get content => _content != null ? _content : canonicalContent;
+
+  /// Returns the canonical serialization of the value of the scalar. If the
+  /// value isn't given, the result of this will be "null".
+  String get canonicalContent {
+    if (value == null || value is bool || value is int) return '$value';
+
+    if (value is num) {
+      // 20 is the maximum value for this argument, which we use since YAML
+      // doesn't specify a maximum.
+      return value.toStringAsExponential(20).
+        replaceFirst(new RegExp("0+e"), "e");
+    }
+
+    if (value is String) {
+      // TODO(nweiz): This could be faster if we used a RegExp to check for
+      // special characters and short-circuited if they didn't exist.
+
+      var escapedValue = value.charCodes.mappedBy((c) {
+        switch (c) {
+        case _Parser.TAB: return "\\t";
+        case _Parser.LF: return "\\n";
+        case _Parser.CR: return "\\r";
+        case _Parser.DOUBLE_QUOTE: return '\\"';
+        case _Parser.NULL: return "\\0";
+        case _Parser.BELL: return "\\a";
+        case _Parser.BACKSPACE: return "\\b";
+        case _Parser.VERTICAL_TAB: return "\\v";
+        case _Parser.FORM_FEED: return "\\f";
+        case _Parser.ESCAPE: return "\\e";
+        case _Parser.BACKSLASH: return "\\\\";
+        case _Parser.NEL: return "\\N";
+        case _Parser.NBSP: return "\\_";
+        case _Parser.LINE_SEPARATOR: return "\\L";
+        case _Parser.PARAGRAPH_SEPARATOR: return "\\P";
+        default:
+          if (c < 0x20 || (c >= 0x7f && c < 0x100)) {
+            return "\\x${zeroPad(c.toRadixString(16).toUpperCase(), 2)}";
+          } else if (c >= 0x100 && c < 0x10000) {
+            return "\\u${zeroPad(c.toRadixString(16).toUpperCase(), 4)}";
+          } else if (c >= 0x10000) {
+            return "\\u${zeroPad(c.toRadixString(16).toUpperCase(), 8)}";
+          } else {
+            return new String.fromCharCodes([c]);
+          }
+        }
+      });
+      return '"${Strings.join(escapedValue, '')}"';
+    }
+
+    throw new YamlException("unknown scalar value: $value");
+  }
+
+  String toString() => '$tag "$content"';
+
+  /// Left-pads [str] with zeros so that it's at least [length] characters
+  /// long.
+  String zeroPad(String str, int length) {
+    assert(length >= str.length);
+    var prefix = [];
+    prefix.insertRange(0, length - str.length, '0');
+    return '${Strings.join(prefix, '')}$str';
+  }
+
+  int get hashCode => super.hashCode ^ content.hashCode;
+
+  visit(_Visitor v) => v.visitScalar(this);
+}
+
+/// A mapping node represents an unordered map of nodes to nodes.
+class _MappingNode extends _Node {
+  /// The node map.
+  Map<_Node, _Node> content;
+
+  _MappingNode(String tagName, this.content)
+    : super(new _Tag.mapping(tagName));
+
+  /// Two mappings are equal if their tags and contents are equal.
+  bool operator ==(other) {
+    // Should be super != other; bug 2554
+    if (!(super == other) || other is! _MappingNode) return false;
+    if (content.length != other.content.length) return false;
+    for (var key in content.keys) {
+      if (!other.content.containsKey(key)) return false;
+      if (content[key] != other.content[key]) return false;
+    }
+    return true;
+  }
+
+  String toString() {
+    var strContent = content.keys
+        .mappedBy((k) => '${k}: ${content[k]}')
+        .join(', ');
+    return '$tag {$strContent}';
+  }
+
+  int get hashCode => super.hashCode ^ _hashCode(content);
+
+  visit(_Visitor v) => v.visitMapping(this);
+}
diff --git a/pkgs/yaml/lib/parser.dart b/pkgs/yaml/lib/parser.dart
new file mode 100644
index 0000000..f2f255e
--- /dev/null
+++ b/pkgs/yaml/lib/parser.dart
@@ -0,0 +1,1939 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of yaml;
+
+/// Translates a string of characters into a YAML serialization tree.
+///
+/// This parser is designed to closely follow the spec. All productions in the
+/// spec are numbered, and the corresponding methods in the parser have the same
+/// numbers. This is certainly not the most efficient way of parsing YAML, but
+/// it is the easiest to write and read in the context of the spec.
+///
+/// Methods corresponding to productions are also named as in the spec,
+/// translating the name of the method (although not the annotation characters)
+/// into camel-case for dart style.. For example, the spec has a production
+/// named `nb-ns-plain-in-line`, and the method implementing it is named
+/// `nb_ns_plainInLine`. The exception to that rule is methods that just
+/// recognize character classes; these are named `is*`.
+class _Parser {
+  static const TAB = 0x9;
+  static const LF = 0xA;
+  static const CR = 0xD;
+  static const SP = 0x20;
+  static const TILDE = 0x7E;
+  static const NEL = 0x85;
+  static const PLUS = 0x2B;
+  static const HYPHEN = 0x2D;
+  static const QUESTION_MARK = 0x3F;
+  static const COLON = 0x3A;
+  static const COMMA = 0x2C;
+  static const LEFT_BRACKET = 0x5B;
+  static const RIGHT_BRACKET = 0x5D;
+  static const LEFT_BRACE = 0x7B;
+  static const RIGHT_BRACE = 0x7D;
+  static const HASH = 0x23;
+  static const AMPERSAND = 0x26;
+  static const ASTERISK = 0x2A;
+  static const EXCLAMATION = 0x21;
+  static const VERTICAL_BAR = 0x7C;
+  static const GREATER_THAN = 0x3E;
+  static const SINGLE_QUOTE = 0x27;
+  static const DOUBLE_QUOTE = 0x22;
+  static const PERCENT = 0x25;
+  static const AT = 0x40;
+  static const GRAVE_ACCENT = 0x60;
+
+  static const NULL = 0x0;
+  static const BELL = 0x7;
+  static const BACKSPACE = 0x8;
+  static const VERTICAL_TAB = 0xB;
+  static const FORM_FEED = 0xC;
+  static const ESCAPE = 0x1B;
+  static const SLASH = 0x2F;
+  static const BACKSLASH = 0x5C;
+  static const UNDERSCORE = 0x5F;
+  static const NBSP = 0xA0;
+  static const LINE_SEPARATOR = 0x2028;
+  static const PARAGRAPH_SEPARATOR = 0x2029;
+
+  static const NUMBER_0 = 0x30;
+  static const NUMBER_9 = 0x39;
+
+  static const LETTER_A = 0x61;
+  static const LETTER_B = 0x62;
+  static const LETTER_E = 0x65;
+  static const LETTER_F = 0x66;
+  static const LETTER_N = 0x6E;
+  static const LETTER_R = 0x72;
+  static const LETTER_T = 0x74;
+  static const LETTER_U = 0x75;
+  static const LETTER_V = 0x76;
+  static const LETTER_X = 0x78;
+
+  static const LETTER_CAP_A = 0x41;
+  static const LETTER_CAP_F = 0x46;
+  static const LETTER_CAP_L = 0x4C;
+  static const LETTER_CAP_N = 0x4E;
+  static const LETTER_CAP_P = 0x50;
+  static const LETTER_CAP_U = 0x55;
+  static const LETTER_CAP_X = 0x58;
+
+  static const C_SEQUENCE_ENTRY = 4;
+  static const C_MAPPING_KEY = 5;
+  static const C_MAPPING_VALUE = 6;
+  static const C_COLLECT_ENTRY = 7;
+  static const C_SEQUENCE_START = 8;
+  static const C_SEQUENCE_END = 9;
+  static const C_MAPPING_START = 10;
+  static const C_MAPPING_END = 11;
+  static const C_COMMENT = 12;
+  static const C_ANCHOR = 13;
+  static const C_ALIAS = 14;
+  static const C_TAG = 15;
+  static const C_LITERAL = 16;
+  static const C_FOLDED = 17;
+  static const C_SINGLE_QUOTE = 18;
+  static const C_DOUBLE_QUOTE = 19;
+  static const C_DIRECTIVE = 20;
+  static const C_RESERVED = 21;
+
+  static const BLOCK_OUT = 0;
+  static const BLOCK_IN = 1;
+  static const FLOW_OUT = 2;
+  static const FLOW_IN = 3;
+  static const BLOCK_KEY = 4;
+  static const FLOW_KEY = 5;
+
+  static const CHOMPING_STRIP = 0;
+  static const CHOMPING_KEEP = 1;
+  static const CHOMPING_CLIP = 2;
+
+  /// The source string being parsed.
+  final String s;
+
+  /// The current position in the source string.
+  int pos = 0;
+
+  /// The length of the string being parsed.
+  final int len;
+
+  /// The current (0-based) line in the source string.
+  int line = 0;
+
+  /// The current (0-based) column in the source string.
+  int column = 0;
+
+  /// Whether we're parsing a bare document (that is, one that doesn't begin
+  /// with `---`). Bare documents don't allow `%` immediately following
+  /// newlines.
+  bool inBareDocument = false;
+
+  /// The line number of the farthest position that has been parsed successfully
+  /// before backtracking. Used for error reporting.
+  int farthestLine = 0;
+
+  /// The column number of the farthest position that has been parsed
+  /// successfully before backtracking. Used for error reporting.
+  int farthestColumn = 0;
+
+  /// The farthest position in the source string that has been parsed
+  /// successfully before backtracking. Used for error reporting.
+  int farthestPos = 0;
+
+  /// The name of the context of the farthest position that has been parsed
+  /// successfully before backtracking. Used for error reporting.
+  String farthestContext = "document";
+
+  /// A stack of the names of parse contexts. Used for error reporting.
+  List<String> contextStack;
+
+  /// Annotations attached to ranges of the source string that add extra
+  /// information to any errors that occur in the annotated range.
+  _RangeMap<String> errorAnnotations;
+
+  /// The buffer containing the string currently being captured.
+  StringBuffer capturedString;
+
+  /// The beginning of the current section of the captured string.
+  int captureStart;
+
+  /// Whether the current string capture is being overridden.
+  bool capturingAs = false;
+
+  _Parser(String s)
+    : this.s = s,
+      len = s.length,
+      contextStack = <String>["document"],
+      errorAnnotations = new _RangeMap();
+
+  /// Return the character at the current position, then move that position
+  /// forward one character. Also updates the current line and column numbers.
+  int next() {
+    if (pos == len) return -1;
+    var char = s.charCodeAt(pos++);
+    if (isBreak(char)) {
+      line++;
+      column = 0;
+    } else {
+      column++;
+    }
+
+    if (farthestLine < line) {
+      farthestLine = line;
+      farthestColumn = column;
+      farthestContext = contextStack.last;
+    } else if (farthestLine == line && farthestColumn < column) {
+      farthestColumn = column;
+      farthestContext = contextStack.last;
+    }
+    farthestPos = pos;
+
+    return char;
+  }
+
+  /// Returns the character at the current position, or the character [i]
+  /// characters after the current position.
+  ///
+  /// Returns -1 if this would return a character after the end or before the
+  /// beginning of the input string.
+  int peek([int i = 0]) {
+    var peekPos = pos + i;
+    return (peekPos >= len || peekPos < 0) ? -1 : s.charCodeAt(peekPos);
+  }
+
+  /// The truthiness operator. Returns `false` if [obj] is `null` or `false`,
+  /// `true` otherwise.
+  bool truth(obj) => obj != null && obj != false;
+
+  /// Consumes the current character if it matches [matcher]. Returns the result
+  /// of [matcher].
+  bool consume(bool matcher(int)) {
+    if (matcher(peek())) {
+      next();
+      return true;
+    }
+    return false;
+  }
+
+  /// Consumes the current character if it equals [char].
+  bool consumeChar(int char) => consume((c) => c == char);
+
+  /// Calls [consumer] until it returns a falsey value. Returns a list of all
+  /// truthy return values of [consumer], or null if it didn't consume anything.
+  ///
+  /// Conceptually, repeats a production one or more times.
+  List oneOrMore(consumer()) {
+    var first = consumer();
+    if (!truth(first)) return null;
+    var out = [first];
+    while (true) {
+      var el = consumer();
+      if (!truth(el)) return out;
+      out.add(el);
+    }
+    return null; // Unreachable.
+  }
+
+  /// Calls [consumer] until it returns a falsey value. Returns a list of all
+  /// truthy return values of [consumer], or the empty list if it didn't consume
+  /// anything.
+  ///
+  /// Conceptually, repeats a production any number of times.
+  List zeroOrMore(consumer()) {
+    var out = [];
+    var oldPos = pos;
+    while (true) {
+      var el = consumer();
+      if (!truth(el) || oldPos == pos) return out;
+      oldPos = pos;
+      out.add(el);
+    }
+    return null; // Unreachable.
+  }
+
+  /// Just calls [consumer] and returns its result. Used to make it explicit
+  /// that a production is intended to be optional.
+  zeroOrOne(consumer()) => consumer();
+
+  /// Calls each function in [consumers] until one returns a truthy value, then
+  /// returns that.
+  or(List<Function> consumers) {
+    for (var c in consumers) {
+      var res = c();
+      if (truth(res)) return res;
+    }
+    return null;
+  }
+
+  /// Calls [consumer] and returns its result, but rolls back the parser state
+  /// if [consumer] returns a falsey value.
+  transaction(consumer()) {
+    var oldPos = pos;
+    var oldLine = line;
+    var oldColumn = column;
+    var oldCaptureStart = captureStart;
+    String capturedSoFar = capturedString == null ? null :
+      capturedString.toString();
+    var res = consumer();
+    if (truth(res)) return res;
+
+    pos = oldPos;
+    line = oldLine;
+    column = oldColumn;
+    captureStart = oldCaptureStart;
+    capturedString = capturedSoFar == null ? null :
+      new StringBuffer(capturedSoFar);
+    return res;
+  }
+
+  /// Consumes [n] characters matching [matcher], or none if there isn't a
+  /// complete match. The first argument to [matcher] is the character code, the
+  /// second is the index (from 0 to [n] - 1).
+  ///
+  /// Returns whether or not the characters were consumed.
+  bool nAtOnce(int n, bool matcher(int c, int i)) => transaction(() {
+    for (int i = 0; i < n; i++) {
+      if (!consume((c) => matcher(c, i))) return false;
+    }
+    return true;
+  });
+
+  /// Consumes the exact characters in [str], or nothing.
+  ///
+  /// Returns whether or not the string was consumed.
+  bool rawString(String str) =>
+    nAtOnce(str.length, (c, i) => str.charCodeAt(i) == c);
+
+  /// Consumes and returns a string of characters matching [matcher], or null if
+  /// there are no such characters.
+  String stringOf(bool matcher(int)) =>
+    captureString(() => oneOrMore(() => consume(matcher)));
+
+  /// Calls [consumer] and returns the string that was consumed while doing so,
+  /// or null if [consumer] returned a falsey value. Automatically wraps
+  /// [consumer] in `transaction`.
+  String captureString(consumer()) {
+    // captureString calls may not be nested
+    assert(capturedString == null);
+
+    captureStart = pos;
+    capturedString = new StringBuffer();
+    var res = transaction(consumer);
+    if (!truth(res)) {
+      captureStart = null;
+      capturedString = null;
+      return null;
+    }
+
+    flushCapture();
+    var result = capturedString.toString();
+    captureStart = null;
+    capturedString = null;
+    return result;
+  }
+
+  captureAs(String replacement, consumer()) =>
+      captureAndTransform(consumer, (_) => replacement);
+
+  captureAndTransform(consumer(), String transformation(String captured)) {
+    if (capturedString == null) return consumer();
+    if (capturingAs) return consumer();
+
+    flushCapture();
+    capturingAs = true;
+    var res = consumer();
+    capturingAs = false;
+    if (!truth(res)) return res;
+
+    capturedString.add(transformation(s.substring(captureStart, pos)));
+    captureStart = pos;
+    return res;
+  }
+
+  void flushCapture() {
+    capturedString.add(s.substring(captureStart, pos));
+    captureStart = pos;
+  }
+
+  /// Adds a tag and an anchor to [node], if they're defined.
+  _Node addProps(_Node node, _Pair<_Tag, String> props) {
+    if (props == null || node == null) return node;
+    if (truth(props.first)) node.tag = props.first;
+    if (truth(props.last)) node.anchor = props.last;
+    return node;
+  }
+
+  /// Creates a MappingNode from [pairs].
+  _MappingNode map(List<_Pair<_Node, _Node>> pairs) {
+    var content = new Map<_Node, _Node>();
+    pairs.forEach((pair) => content[pair.first] = pair.last);
+    return new _MappingNode("?", content);
+  }
+
+  /// Runs [fn] in a context named [name]. Used for error reporting.
+  context(String name, fn()) {
+    try {
+      contextStack.add(name);
+      return fn();
+    } finally {
+      var popped = contextStack.removeLast();
+      assert(popped == name);
+    }
+  }
+
+  /// Adds [message] as extra information to any errors that occur between the
+  /// current position and the position of the cursor after running [fn]. The
+  /// cursor is reset after [fn] is run.
+  annotateError(String message, fn()) {
+    var start = pos;
+    var end;
+    transaction(() {
+      fn();
+      end = pos;
+      return false;
+    });
+    errorAnnotations[new _Range(start, end)] = message;
+  }
+
+  /// Throws an error with additional context information.
+  error(String message) {
+    // Line and column should be one-based.
+    throw new SyntaxError(line + 1, column + 1,
+        "$message (in $farthestContext)");
+  }
+
+  /// If [result] is falsey, throws an error saying that [expected] was
+  /// expected.
+  expect(result, String expected) {
+    if (truth(result)) return result;
+    error("expected $expected");
+  }
+
+  /// Throws an error saying that the parse failed. Uses [farthestLine],
+  /// [farthestColumn], and [farthestContext] to provide additional information.
+  parseFailed() {
+    var message = "invalid YAML in $farthestContext";
+    var extraError = errorAnnotations[farthestPos];
+    if (extraError != null) message = "$message ($extraError)";
+    throw new SyntaxError(farthestLine + 1, farthestColumn + 1, message);
+  }
+
+  /// Returns the number of spaces after the current position.
+  int countIndentation() {
+    var i = 0;
+    while (peek(i) == SP) i++;
+    return i;
+  }
+
+  /// Returns the indentation for a block scalar.
+  int blockScalarAdditionalIndentation(_BlockHeader header, int indent) {
+    if (!header.autoDetectIndent) return header.additionalIndent;
+
+    var maxSpaces = 0;
+    var maxSpacesLine = 0;
+    var spaces = 0;
+    transaction(() {
+      do {
+        spaces = captureString(() => zeroOrMore(() => consumeChar(SP))).length;
+        if (spaces > maxSpaces) {
+          maxSpaces = spaces;
+          maxSpacesLine = line;
+        }
+      } while (b_break());
+      return false;
+    });
+
+    // If the next non-empty line isn't indented further than the start of the
+    // block scalar, that means the scalar is going to be empty. Returning any
+    // value > 0 will cause the parser not to consume any text.
+    if (spaces <= indent) return 1;
+
+    // It's an error for a leading empty line to be indented more than the first
+    // non-empty line.
+    if (maxSpaces > spaces) {
+      throw new SyntaxError(maxSpacesLine + 1, maxSpaces,
+          "Leading empty lines may not be indented more than the first "
+          "non-empty line.");
+    }
+
+    return spaces - indent;
+  }
+
+  /// Returns whether the current position is at the beginning of a line.
+  bool get atStartOfLine => column == 0;
+
+  /// Returns whether the current position is at the end of the input.
+  bool get atEndOfFile => pos == len;
+
+  /// Given an indicator character, returns the type of that indicator (or null
+  /// if the indicator isn't found.
+  int indicatorType(int char) {
+    switch (char) {
+    case HYPHEN: return C_SEQUENCE_ENTRY;
+    case QUESTION_MARK: return C_MAPPING_KEY;
+    case COLON: return C_MAPPING_VALUE;
+    case COMMA: return C_COLLECT_ENTRY;
+    case LEFT_BRACKET: return C_SEQUENCE_START;
+    case RIGHT_BRACKET: return C_SEQUENCE_END;
+    case LEFT_BRACE: return C_MAPPING_START;
+    case RIGHT_BRACE: return C_MAPPING_END;
+    case HASH: return C_COMMENT;
+    case AMPERSAND: return C_ANCHOR;
+    case ASTERISK: return C_ALIAS;
+    case EXCLAMATION: return C_TAG;
+    case VERTICAL_BAR: return C_LITERAL;
+    case GREATER_THAN: return C_FOLDED;
+    case SINGLE_QUOTE: return C_SINGLE_QUOTE;
+    case DOUBLE_QUOTE: return C_DOUBLE_QUOTE;
+    case PERCENT: return C_DIRECTIVE;
+    case AT:
+    case GRAVE_ACCENT:
+      return C_RESERVED;
+    default: return null;
+    }
+  }
+
+  // 1
+  bool isPrintable(int char) {
+    return char == TAB ||
+      char == LF ||
+      char == CR ||
+      (char >= SP && char <= TILDE) ||
+      char == NEL ||
+      (char >= 0xA0 && char <= 0xD7FF) ||
+      (char >= 0xE000 && char <= 0xFFFD) ||
+      (char >= 0x10000 && char <= 0x10FFFF);
+  }
+
+  // 2
+  bool isJson(int char) => char == TAB || (char >= SP && char <= 0x10FFFF);
+
+  // 22
+  bool c_indicator(int type) => consume((c) => indicatorType(c) == type);
+
+  // 23
+  bool isFlowIndicator(int char) {
+    var indicator = indicatorType(char);
+    return indicator == C_COLLECT_ENTRY ||
+      indicator == C_SEQUENCE_START ||
+      indicator == C_SEQUENCE_END ||
+      indicator == C_MAPPING_START ||
+      indicator == C_MAPPING_END;
+  }
+
+  // 26
+  bool isBreak(int char) => char == LF || char == CR;
+
+  // 27
+  bool isNonBreak(int char) => isPrintable(char) && !isBreak(char);
+
+  // 28
+  bool b_break() {
+    if (consumeChar(CR)) {
+      zeroOrOne(() => consumeChar(LF));
+      return true;
+    }
+    return consumeChar(LF);
+  }
+
+  // 29
+  bool b_asLineFeed() => captureAs("\n", () => b_break());
+
+  // 30
+  bool b_nonContent() => captureAs("", () => b_break());
+
+  // 33
+  bool isSpace(int char) => char == SP || char == TAB;
+
+  // 34
+  bool isNonSpace(int char) => isNonBreak(char) && !isSpace(char);
+
+  // 35
+  bool isDecDigit(int char) => char >= NUMBER_0 && char <= NUMBER_9;
+
+  // 36
+  bool isHexDigit(int char) {
+    return isDecDigit(char) ||
+      (char >= LETTER_A && char <= LETTER_F) ||
+      (char >= LETTER_CAP_A && char <= LETTER_CAP_F);
+  }
+
+  // 41
+  bool c_escape() => captureAs("", () => consumeChar(BACKSLASH));
+
+  // 42
+  bool ns_escNull() => captureAs("\x00", () => consumeChar(NUMBER_0));
+
+  // 43
+  bool ns_escBell() => captureAs("\x07", () => consumeChar(LETTER_A));
+
+  // 44
+  bool ns_escBackspace() => captureAs("\b", () => consumeChar(LETTER_B));
+
+  // 45
+  bool ns_escHorizontalTab() => captureAs("\t", () {
+    return consume((c) => c == LETTER_T || c == TAB);
+  });
+
+  // 46
+  bool ns_escLineFeed() => captureAs("\n", () => consumeChar(LETTER_N));
+
+  // 47
+  bool ns_escVerticalTab() => captureAs("\v", () => consumeChar(LETTER_V));
+
+  // 48
+  bool ns_escFormFeed() => captureAs("\f", () => consumeChar(LETTER_F));
+
+  // 49
+  bool ns_escCarriageReturn() => captureAs("\r", () => consumeChar(LETTER_R));
+
+  // 50
+  bool ns_escEscape() => captureAs("\x1B", () => consumeChar(LETTER_E));
+
+  // 51
+  bool ns_escSpace() => consumeChar(SP);
+
+  // 52
+  bool ns_escDoubleQuote() => consumeChar(DOUBLE_QUOTE);
+
+  // 53
+  bool ns_escSlash() => consumeChar(SLASH);
+
+  // 54
+  bool ns_escBackslash() => consumeChar(BACKSLASH);
+
+  // 55
+  bool ns_escNextLine() => captureAs("\x85", () => consumeChar(LETTER_CAP_N));
+
+  // 56
+  bool ns_escNonBreakingSpace() =>
+    captureAs("\xA0", () => consumeChar(UNDERSCORE));
+
+  // 57
+  bool ns_escLineSeparator() =>
+    captureAs("\u2028", () => consumeChar(LETTER_CAP_L));
+
+  // 58
+  bool ns_escParagraphSeparator() =>
+    captureAs("\u2029", () => consumeChar(LETTER_CAP_P));
+
+  // 59
+  bool ns_esc8Bit() => ns_escNBit(LETTER_X, 2);
+
+  // 60
+  bool ns_esc16Bit() => ns_escNBit(LETTER_U, 4);
+
+  // 61
+  bool ns_esc32Bit() => ns_escNBit(LETTER_CAP_U, 8);
+
+  // Helper method for 59 - 61
+  bool ns_escNBit(int char, int digits) {
+    if (!captureAs('', () => consumeChar(char))) return false;
+    var captured = captureAndTransform(
+        () => nAtOnce(digits, (c, _) => isHexDigit(c)),
+        (hex) => new String.fromCharCodes([int.parse("0x$hex")]));
+    return expect(captured, "$digits hexidecimal digits");
+  }
+
+  // 62
+  bool c_ns_escChar() => context('escape sequence', () => transaction(() {
+      if (!truth(c_escape())) return false;
+      return truth(or([
+        ns_escNull, ns_escBell, ns_escBackspace, ns_escHorizontalTab,
+        ns_escLineFeed, ns_escVerticalTab, ns_escFormFeed, ns_escCarriageReturn,
+        ns_escEscape, ns_escSpace, ns_escDoubleQuote, ns_escSlash,
+        ns_escBackslash, ns_escNextLine, ns_escNonBreakingSpace,
+        ns_escLineSeparator, ns_escParagraphSeparator, ns_esc8Bit, ns_esc16Bit,
+        ns_esc32Bit
+      ]));
+    }));
+
+  // 63
+  bool s_indent(int indent) {
+    var result = nAtOnce(indent, (c, i) => c == SP);
+    if (peek() == TAB) {
+      annotateError("tab characters are not allowed as indentation in YAML",
+          () => zeroOrMore(() => consume(isSpace)));
+    }
+    return result;
+  }
+
+  // 64
+  bool s_indentLessThan(int indent) {
+    for (int i = 0; i < indent - 1; i++) {
+      if (!consumeChar(SP)) {
+        if (peek() == TAB) {
+          annotateError("tab characters are not allowed as indentation in YAML",
+              () {
+            for (; i < indent - 1; i++) {
+              if (!consume(isSpace)) break;
+            }
+          });
+        }
+        break;
+      }
+    }
+    return true;
+  }
+
+  // 65
+  bool s_indentLessThanOrEqualTo(int indent) => s_indentLessThan(indent + 1);
+
+  // 66
+  bool s_separateInLine() => transaction(() {
+    return captureAs('', () =>
+        truth(oneOrMore(() => consume(isSpace))) || atStartOfLine);
+  });
+
+  // 67
+  bool s_linePrefix(int indent, int ctx) => captureAs("", () {
+    switch (ctx) {
+    case BLOCK_OUT:
+    case BLOCK_IN:
+      return s_blockLinePrefix(indent);
+    case FLOW_OUT:
+    case FLOW_IN:
+      return s_flowLinePrefix(indent);
+    }
+  });
+
+  // 68
+  bool s_blockLinePrefix(int indent) => s_indent(indent);
+
+  // 69
+  bool s_flowLinePrefix(int indent) => captureAs('', () {
+    if (!truth(s_indent(indent))) return false;
+    zeroOrOne(s_separateInLine);
+    return true;
+  });
+
+  // 70
+  bool l_empty(int indent, int ctx) => transaction(() {
+    var start = or([
+      () => s_linePrefix(indent, ctx),
+      () => s_indentLessThan(indent)
+    ]);
+    if (!truth(start)) return false;
+    return b_asLineFeed();
+  });
+
+  // 71
+  bool b_asSpace() => captureAs(" ", () => consume(isBreak));
+
+  // 72
+  bool b_l_trimmed(int indent, int ctx) => transaction(() {
+    if (!truth(b_nonContent())) return false;
+    return truth(oneOrMore(() => captureAs("\n", () => l_empty(indent, ctx))));
+  });
+
+  // 73
+  bool b_l_folded(int indent, int ctx) =>
+    or([() => b_l_trimmed(indent, ctx), b_asSpace]);
+
+  // 74
+  bool s_flowFolded(int indent) => transaction(() {
+    zeroOrOne(s_separateInLine);
+    if (!truth(b_l_folded(indent, FLOW_IN))) return false;
+    return s_flowLinePrefix(indent);
+  });
+
+  // 75
+  bool c_nb_commentText() {
+    if (!truth(c_indicator(C_COMMENT))) return false;
+    zeroOrMore(() => consume(isNonBreak));
+    return true;
+  }
+
+  // 76
+  bool b_comment() => atEndOfFile || b_nonContent();
+
+  // 77
+  bool s_b_comment() {
+    if (truth(s_separateInLine())) {
+      zeroOrOne(c_nb_commentText);
+    }
+    return b_comment();
+  }
+
+  // 78
+  bool l_comment() => transaction(() {
+    if (!truth(s_separateInLine())) return false;
+    zeroOrOne(c_nb_commentText);
+    return b_comment();
+  });
+
+  // 79
+  bool s_l_comments() {
+    if (!truth(s_b_comment()) && !atStartOfLine) return false;
+    zeroOrMore(l_comment);
+    return true;
+  }
+
+  // 80
+  bool s_separate(int indent, int ctx) {
+    switch (ctx) {
+    case BLOCK_OUT:
+    case BLOCK_IN:
+    case FLOW_OUT:
+    case FLOW_IN:
+      return s_separateLines(indent);
+    case BLOCK_KEY:
+    case FLOW_KEY:
+      return s_separateInLine();
+    default: throw 'invalid context "$ctx"';
+    }
+  }
+
+  // 81
+  bool s_separateLines(int indent) {
+    return transaction(() => s_l_comments() && s_flowLinePrefix(indent)) ||
+      s_separateInLine();
+  }
+
+  // 82
+  bool l_directive() => false; // TODO(nweiz): implement
+
+  // 96
+  _Pair<_Tag, String> c_ns_properties(int indent, int ctx) {
+    var tag, anchor;
+    tag = c_ns_tagProperty();
+    if (truth(tag)) {
+      anchor = transaction(() {
+        if (!truth(s_separate(indent, ctx))) return null;
+        return c_ns_anchorProperty();
+      });
+      return new _Pair<_Tag, String>(tag, anchor);
+    }
+
+    anchor = c_ns_anchorProperty();
+    if (truth(anchor)) {
+      tag = transaction(() {
+        if (!truth(s_separate(indent, ctx))) return null;
+        return c_ns_tagProperty();
+      });
+      return new _Pair<_Tag, String>(tag, anchor);
+    }
+
+    return null;
+  }
+
+  // 97
+  _Tag c_ns_tagProperty() => null; // TODO(nweiz): implement
+
+  // 101
+  String c_ns_anchorProperty() => null; // TODO(nweiz): implement
+
+  // 102
+  bool isAnchorChar(int char) => isNonSpace(char) && !isFlowIndicator(char);
+
+  // 103
+  String ns_anchorName() =>
+    captureString(() => oneOrMore(() => consume(isAnchorChar)));
+
+  // 104
+  _Node c_ns_aliasNode() {
+    if (!truth(c_indicator(C_ALIAS))) return null;
+    var name = expect(ns_anchorName(), 'anchor name');
+    return new _AliasNode(name);
+  }
+
+  // 105
+  _ScalarNode e_scalar() => new _ScalarNode("?", content: "");
+
+  // 106
+  _ScalarNode e_node() => e_scalar();
+
+  // 107
+  bool nb_doubleChar() => or([
+    c_ns_escChar,
+    () => consume((c) => isJson(c) && c != BACKSLASH && c != DOUBLE_QUOTE)
+  ]);
+
+  // 108
+  bool ns_doubleChar() => !isSpace(peek()) && truth(nb_doubleChar());
+
+  // 109
+  _Node c_doubleQuoted(int indent, int ctx) => context('string', () {
+    return transaction(() {
+      if (!truth(c_indicator(C_DOUBLE_QUOTE))) return null;
+      var contents = nb_doubleText(indent, ctx);
+      if (!truth(c_indicator(C_DOUBLE_QUOTE))) return null;
+      return new _ScalarNode("!", content: contents);
+    });
+  });
+
+  // 110
+  String nb_doubleText(int indent, int ctx) => captureString(() {
+    switch (ctx) {
+    case FLOW_OUT:
+    case FLOW_IN:
+      nb_doubleMultiLine(indent);
+      break;
+    case BLOCK_KEY:
+    case FLOW_KEY:
+      nb_doubleOneLine();
+      break;
+    }
+    return true;
+  });
+
+  // 111
+  void nb_doubleOneLine() {
+    zeroOrMore(nb_doubleChar);
+  }
+
+  // 112
+  bool s_doubleEscaped(int indent) => transaction(() {
+    zeroOrMore(() => consume(isSpace));
+    if (!captureAs("", () => consumeChar(BACKSLASH))) return false;
+    if (!truth(b_nonContent())) return false;
+    zeroOrMore(() => captureAs("\n", () => l_empty(indent, FLOW_IN)));
+    return s_flowLinePrefix(indent);
+  });
+
+  // 113
+  bool s_doubleBreak(int indent) => or([
+    () => s_doubleEscaped(indent),
+    () => s_flowFolded(indent)
+  ]);
+
+  // 114
+  void nb_ns_doubleInLine() {
+    zeroOrMore(() => transaction(() {
+        zeroOrMore(() => consume(isSpace));
+        return ns_doubleChar();
+      }));
+  }
+
+  // 115
+  bool s_doubleNextLine(int indent) {
+    if (!truth(s_doubleBreak(indent))) return false;
+    zeroOrOne(() {
+      if (!truth(ns_doubleChar())) return;
+      nb_ns_doubleInLine();
+      or([
+        () => s_doubleNextLine(indent),
+        () => zeroOrMore(() => consume(isSpace))
+      ]);
+    });
+    return true;
+  }
+
+  // 116
+  void nb_doubleMultiLine(int indent) {
+    nb_ns_doubleInLine();
+    or([
+      () => s_doubleNextLine(indent),
+      () => zeroOrMore(() => consume(isSpace))
+    ]);
+  }
+
+  // 117
+  bool c_quotedQuote() => captureAs("'", () => rawString("''"));
+
+  // 118
+  bool nb_singleChar() => or([
+    c_quotedQuote,
+    () => consume((c) => isJson(c) && c != SINGLE_QUOTE)
+  ]);
+
+  // 119
+  bool ns_singleChar() => !isSpace(peek()) && truth(nb_singleChar());
+
+  // 120
+  _Node c_singleQuoted(int indent, int ctx) => context('string', () {
+    return transaction(() {
+      if (!truth(c_indicator(C_SINGLE_QUOTE))) return null;
+      var contents = nb_singleText(indent, ctx);
+      if (!truth(c_indicator(C_SINGLE_QUOTE))) return null;
+      return new _ScalarNode("!", content: contents);
+    });
+  });
+
+  // 121
+  String nb_singleText(int indent, int ctx) => captureString(() {
+    switch (ctx) {
+    case FLOW_OUT:
+    case FLOW_IN:
+      nb_singleMultiLine(indent);
+      break;
+    case BLOCK_KEY:
+    case FLOW_KEY:
+      nb_singleOneLine(indent);
+      break;
+    }
+    return true;
+  });
+
+  // 122
+  void nb_singleOneLine(int indent) {
+    zeroOrMore(nb_singleChar);
+  }
+
+  // 123
+  void nb_ns_singleInLine() {
+    zeroOrMore(() => transaction(() {
+      zeroOrMore(() => consume(isSpace));
+      return ns_singleChar();
+    }));
+  }
+
+  // 124
+  bool s_singleNextLine(int indent) {
+    if (!truth(s_flowFolded(indent))) return false;
+    zeroOrOne(() {
+      if (!truth(ns_singleChar())) return;
+      nb_ns_singleInLine();
+      or([
+        () => s_singleNextLine(indent),
+        () => zeroOrMore(() => consume(isSpace))
+      ]);
+    });
+    return true;
+  }
+
+  // 125
+  void nb_singleMultiLine(int indent) {
+    nb_ns_singleInLine();
+    or([
+      () => s_singleNextLine(indent),
+      () => zeroOrMore(() => consume(isSpace))
+    ]);
+  }
+
+  // 126
+  bool ns_plainFirst(int ctx) {
+    var char = peek();
+    var indicator = indicatorType(char);
+    if (indicator == C_RESERVED) {
+      error("reserved indicators can't start a plain scalar");
+    }
+    var match = (isNonSpace(char) && indicator == null) ||
+      ((indicator == C_MAPPING_KEY ||
+        indicator == C_MAPPING_VALUE ||
+        indicator == C_SEQUENCE_ENTRY) &&
+       isPlainSafe(ctx, peek(1)));
+
+    if (match) next();
+    return match;
+  }
+
+  // 127
+  bool isPlainSafe(int ctx, int char) {
+    switch (ctx) {
+    case FLOW_OUT:
+    case BLOCK_KEY:
+      // 128
+      return isNonSpace(char);
+    case FLOW_IN:
+    case FLOW_KEY:
+      // 129
+      return isNonSpace(char) && !isFlowIndicator(char);
+    default: throw 'invalid context "$ctx"';
+    }
+  }
+
+  // 130
+  bool ns_plainChar(int ctx) {
+    var char = peek();
+    var indicator = indicatorType(char);
+    var safeChar = isPlainSafe(ctx, char) && indicator != C_MAPPING_VALUE &&
+      indicator != C_COMMENT;
+    var nonCommentHash = isNonSpace(peek(-1)) && indicator == C_COMMENT;
+    var nonMappingColon = indicator == C_MAPPING_VALUE &&
+      isPlainSafe(ctx, peek(1));
+    var match = safeChar || nonCommentHash || nonMappingColon;
+
+    if (match) next();
+    return match;
+  }
+
+  // 131
+  String ns_plain(int indent, int ctx) => context('plain scalar', () {
+    return captureString(() {
+      switch (ctx) {
+      case FLOW_OUT:
+      case FLOW_IN:
+        return ns_plainMultiLine(indent, ctx);
+      case BLOCK_KEY:
+      case FLOW_KEY:
+        return ns_plainOneLine(ctx);
+      default: throw 'invalid context "$ctx"';
+      }
+    });
+  });
+
+  // 132
+  void nb_ns_plainInLine(int ctx) {
+    zeroOrMore(() => transaction(() {
+      zeroOrMore(() => consume(isSpace));
+      return ns_plainChar(ctx);
+    }));
+  }
+
+  // 133
+  bool ns_plainOneLine(int ctx) {
+    if (truth(c_forbidden())) return false;
+    if (!truth(ns_plainFirst(ctx))) return false;
+    nb_ns_plainInLine(ctx);
+    return true;
+  }
+
+  // 134
+  bool s_ns_plainNextLine(int indent, int ctx) => transaction(() {
+    if (!truth(s_flowFolded(indent))) return false;
+    if (truth(c_forbidden())) return false;
+    if (!truth(ns_plainChar(ctx))) return false;
+    nb_ns_plainInLine(ctx);
+    return true;
+  });
+
+  // 135
+  bool ns_plainMultiLine(int indent, int ctx) {
+    if (!truth(ns_plainOneLine(ctx))) return false;
+    zeroOrMore(() => s_ns_plainNextLine(indent, ctx));
+    return true;
+  }
+
+  // 136
+  int inFlow(int ctx) {
+    switch (ctx) {
+    case FLOW_OUT:
+    case FLOW_IN:
+      return FLOW_IN;
+    case BLOCK_KEY:
+    case FLOW_KEY:
+      return FLOW_KEY;
+    }
+  }
+
+  // 137
+  _SequenceNode c_flowSequence(int indent, int ctx) => transaction(() {
+    if (!truth(c_indicator(C_SEQUENCE_START))) return null;
+    zeroOrOne(() => s_separate(indent, ctx));
+    var content = zeroOrOne(() => ns_s_flowSeqEntries(indent, inFlow(ctx)));
+    if (!truth(c_indicator(C_SEQUENCE_END))) return null;
+    return new _SequenceNode("?", new List<_Node>.from(content));
+  });
+
+  // 138
+  Collection<_Node> ns_s_flowSeqEntries(int indent, int ctx) {
+    var first = ns_flowSeqEntry(indent, ctx);
+    if (!truth(first)) return new Queue<_Node>();
+    zeroOrOne(() => s_separate(indent, ctx));
+
+    var rest;
+    if (truth(c_indicator(C_COLLECT_ENTRY))) {
+      zeroOrOne(() => s_separate(indent, ctx));
+      rest = zeroOrOne(() => ns_s_flowSeqEntries(indent, ctx));
+    }
+
+    if (rest == null) rest = new Queue<_Node>();
+    rest.addFirst(first);
+
+    return rest;
+  }
+
+  // 139
+  _Node ns_flowSeqEntry(int indent, int ctx) => or([
+    () => ns_flowPair(indent, ctx),
+    () => ns_flowNode(indent, ctx)
+  ]);
+
+  // 140
+  _Node c_flowMapping(int indent, int ctx) {
+    if (!truth(c_indicator(C_MAPPING_START))) return null;
+    zeroOrOne(() => s_separate(indent, ctx));
+    var content = zeroOrOne(() => ns_s_flowMapEntries(indent, inFlow(ctx)));
+    if (!truth(c_indicator(C_MAPPING_END))) return null;
+    return new _MappingNode("?", content);
+  }
+
+  // 141
+  YamlMap ns_s_flowMapEntries(int indent, int ctx) {
+    var first = ns_flowMapEntry(indent, ctx);
+    if (!truth(first)) return new YamlMap();
+    zeroOrOne(() => s_separate(indent, ctx));
+
+    var rest;
+    if (truth(c_indicator(C_COLLECT_ENTRY))) {
+      zeroOrOne(() => s_separate(indent, ctx));
+      rest = ns_s_flowMapEntries(indent, ctx);
+    }
+
+    if (rest == null) rest = new YamlMap();
+
+    // TODO(nweiz): Duplicate keys should be an error. This includes keys with
+    // different representations but the same value (e.g. 10 vs 0xa). To make
+    // this user-friendly we'll probably also want to associate nodes with a
+    // source range.
+    if (!rest.containsKey(first.first)) rest[first.first] = first.last;
+
+    return rest;
+  }
+
+  // 142
+  _Pair<_Node, _Node> ns_flowMapEntry(int indent, int ctx) => or([
+    () => transaction(() {
+      if (!truth(c_indicator(C_MAPPING_KEY))) return false;
+      if (!truth(s_separate(indent, ctx))) return false;
+      return ns_flowMapExplicitEntry(indent, ctx);
+    }),
+    () => ns_flowMapImplicitEntry(indent, ctx)
+  ]);
+
+  // 143
+  _Pair<_Node, _Node> ns_flowMapExplicitEntry(int indent, int ctx) => or([
+    () => ns_flowMapImplicitEntry(indent, ctx),
+    () => new _Pair<_Node, _Node>(e_node(), e_node())
+  ]);
+
+  // 144
+  _Pair<_Node, _Node> ns_flowMapImplicitEntry(int indent, int ctx) => or([
+    () => ns_flowMapYamlKeyEntry(indent, ctx),
+    () => c_ns_flowMapEmptyKeyEntry(indent, ctx),
+    () => c_ns_flowMapJsonKeyEntry(indent, ctx)
+  ]);
+
+  // 145
+  _Pair<_Node, _Node> ns_flowMapYamlKeyEntry(int indent, int ctx) {
+    var key = ns_flowYamlNode(indent, ctx);
+    if (!truth(key)) return null;
+    var value = or([
+      () => transaction(() {
+        zeroOrOne(() => s_separate(indent, ctx));
+        return c_ns_flowMapSeparateValue(indent, ctx);
+      }),
+      e_node
+    ]);
+    return new _Pair<_Node, _Node>(key, value);
+  }
+
+  // 146
+  _Pair<_Node, _Node> c_ns_flowMapEmptyKeyEntry(int indent, int ctx) {
+    var value = c_ns_flowMapSeparateValue(indent, ctx);
+    if (!truth(value)) return null;
+    return new _Pair<_Node, _Node>(e_node(), value);
+  }
+
+  // 147
+  _Node c_ns_flowMapSeparateValue(int indent, int ctx) => transaction(() {
+    if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
+    if (isPlainSafe(ctx, peek())) return null;
+
+    return or([
+      () => transaction(() {
+        if (!s_separate(indent, ctx)) return null;
+        return ns_flowNode(indent, ctx);
+      }),
+      e_node
+    ]);
+  });
+
+  // 148
+  _Pair<_Node, _Node> c_ns_flowMapJsonKeyEntry(int indent, int ctx) {
+    var key = c_flowJsonNode(indent, ctx);
+    if (!truth(key)) return null;
+    var value = or([
+      () => transaction(() {
+        zeroOrOne(() => s_separate(indent, ctx));
+        return c_ns_flowMapAdjacentValue(indent, ctx);
+      }),
+      e_node
+    ]);
+    return new _Pair<_Node, _Node>(key, value);
+  }
+
+  // 149
+  _Node c_ns_flowMapAdjacentValue(int indent, int ctx) {
+    if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
+    return or([
+      () => transaction(() {
+        zeroOrOne(() => s_separate(indent, ctx));
+        return ns_flowNode(indent, ctx);
+      }),
+      e_node
+    ]);
+  }
+
+  // 150
+  _Node ns_flowPair(int indent, int ctx) {
+    var pair = or([
+      () => transaction(() {
+        if (!truth(c_indicator(C_MAPPING_KEY))) return null;
+        if (!truth(s_separate(indent, ctx))) return null;
+        return ns_flowMapExplicitEntry(indent, ctx);
+      }),
+      () => ns_flowPairEntry(indent, ctx)
+    ]);
+    if (!truth(pair)) return null;
+
+    return map([pair]);
+  }
+
+  // 151
+  _Pair<_Node, _Node> ns_flowPairEntry(int indent, int ctx) => or([
+    () => ns_flowPairYamlKeyEntry(indent, ctx),
+    () => c_ns_flowMapEmptyKeyEntry(indent, ctx),
+    () => c_ns_flowPairJsonKeyEntry(indent, ctx)
+  ]);
+
+  // 152
+  _Pair<_Node, _Node> ns_flowPairYamlKeyEntry(int indent, int ctx) =>
+    transaction(() {
+      var key = ns_s_implicitYamlKey(FLOW_KEY);
+      if (!truth(key)) return null;
+      var value = c_ns_flowMapSeparateValue(indent, ctx);
+      if (!truth(value)) return null;
+      return new _Pair<_Node, _Node>(key, value);
+    });
+
+  // 153
+  _Pair<_Node, _Node> c_ns_flowPairJsonKeyEntry(int indent, int ctx) =>
+    transaction(() {
+      var key = c_s_implicitJsonKey(FLOW_KEY);
+      if (!truth(key)) return null;
+      var value = c_ns_flowMapAdjacentValue(indent, ctx);
+      if (!truth(value)) return null;
+      return new _Pair<_Node, _Node>(key, value);
+    });
+
+  // 154
+  _Node ns_s_implicitYamlKey(int ctx) => transaction(() {
+    // TODO(nweiz): this is supposed to be limited to 1024 characters.
+
+    // The indentation parameter is "null" since it's unused in this path
+    var node = ns_flowYamlNode(null, ctx);
+    if (!truth(node)) return null;
+    zeroOrOne(s_separateInLine);
+    return node;
+  });
+
+  // 155
+  _Node c_s_implicitJsonKey(int ctx) => transaction(() {
+    // TODO(nweiz): this is supposed to be limited to 1024 characters.
+
+    // The indentation parameter is "null" since it's unused in this path
+    var node = c_flowJsonNode(null, ctx);
+    if (!truth(node)) return null;
+    zeroOrOne(s_separateInLine);
+    return node;
+  });
+
+  // 156
+  _Node ns_flowYamlContent(int indent, int ctx) {
+    var str = ns_plain(indent, ctx);
+    if (!truth(str)) return null;
+    return new _ScalarNode("?", content: str);
+  }
+
+  // 157
+  _Node c_flowJsonContent(int indent, int ctx) => or([
+    () => c_flowSequence(indent, ctx),
+    () => c_flowMapping(indent, ctx),
+    () => c_singleQuoted(indent, ctx),
+    () => c_doubleQuoted(indent, ctx)
+  ]);
+
+  // 158
+  _Node ns_flowContent(int indent, int ctx) => or([
+    () => ns_flowYamlContent(indent, ctx),
+    () => c_flowJsonContent(indent, ctx)
+  ]);
+
+  // 159
+  _Node ns_flowYamlNode(int indent, int ctx) => or([
+    c_ns_aliasNode,
+    () => ns_flowYamlContent(indent, ctx),
+    () {
+      var props = c_ns_properties(indent, ctx);
+      if (!truth(props)) return null;
+      var node = or([
+        () => transaction(() {
+          if (!truth(s_separate(indent, ctx))) return null;
+          return ns_flowYamlContent(indent, ctx);
+        }),
+        e_scalar
+      ]);
+      return addProps(node, props);
+    }
+  ]);
+
+  // 160
+  _Node c_flowJsonNode(int indent, int ctx) => transaction(() {
+    var props;
+    zeroOrOne(() => transaction(() {
+        props = c_ns_properties(indent, ctx);
+        if (!truth(props)) return null;
+        return s_separate(indent, ctx);
+      }));
+
+    return addProps(c_flowJsonContent(indent, ctx), props);
+  });
+
+  // 161
+  _Node ns_flowNode(int indent, int ctx) => or([
+    c_ns_aliasNode,
+    () => ns_flowContent(indent, ctx),
+    () => transaction(() {
+      var props = c_ns_properties(indent, ctx);
+      if (!truth(props)) return null;
+      var node = or([
+        () => transaction(() => s_separate(indent, ctx) ?
+            ns_flowContent(indent, ctx) : null),
+        e_scalar]);
+      return addProps(node, props);
+    })
+  ]);
+
+  // 162
+  _BlockHeader c_b_blockHeader() => transaction(() {
+    var indentation = c_indentationIndicator();
+    var chomping = c_chompingIndicator();
+    if (!truth(indentation)) indentation = c_indentationIndicator();
+    if (!truth(s_b_comment())) return null;
+
+    return new _BlockHeader(indentation, chomping);
+  });
+
+  // 163
+  int c_indentationIndicator() {
+    if (!isDecDigit(peek())) return null;
+    return next() - NUMBER_0;
+  }
+
+  // 164
+  int c_chompingIndicator() {
+    switch (peek()) {
+    case HYPHEN:
+      next();
+      return CHOMPING_STRIP;
+    case PLUS:
+      next();
+      return CHOMPING_KEEP;
+    default:
+      return CHOMPING_CLIP;
+    }
+  }
+
+  // 165
+  bool b_chompedLast(int chomping) {
+    if (atEndOfFile) return true;
+    switch (chomping) {
+    case CHOMPING_STRIP:
+      return b_nonContent();
+    case CHOMPING_CLIP:
+    case CHOMPING_KEEP:
+      return b_asLineFeed();
+    }
+  }
+
+  // 166
+  void l_chompedEmpty(int indent, int chomping) {
+    switch (chomping) {
+    case CHOMPING_STRIP:
+    case CHOMPING_CLIP:
+      l_stripEmpty(indent);
+      break;
+    case CHOMPING_KEEP:
+      l_keepEmpty(indent);
+      break;
+    }
+  }
+
+  // 167
+  void l_stripEmpty(int indent) {
+    captureAs('', () {
+      zeroOrMore(() => transaction(() {
+          if (!truth(s_indentLessThanOrEqualTo(indent))) return false;
+          return b_nonContent();
+        }));
+      zeroOrOne(() => l_trailComments(indent));
+      return true;
+    });
+  }
+
+  // 168
+  void l_keepEmpty(int indent) {
+    zeroOrMore(() => captureAs('\n', () => l_empty(indent, BLOCK_IN)));
+    zeroOrOne(() => captureAs('', () => l_trailComments(indent)));
+  }
+
+  // 169
+  bool l_trailComments(int indent) => transaction(() {
+    if (!truth(s_indentLessThanOrEqualTo(indent))) return false;
+    if (!truth(c_nb_commentText())) return false;
+    if (!truth(b_comment())) return false;
+    zeroOrMore(l_comment);
+    return true;
+  });
+
+  // 170
+  _Node c_l_literal(int indent) => transaction(() {
+    if (!truth(c_indicator(C_LITERAL))) return null;
+    var header = c_b_blockHeader();
+    if (!truth(header)) return null;
+
+    var additionalIndent = blockScalarAdditionalIndentation(header, indent);
+    var content = l_literalContent(indent + additionalIndent, header.chomping);
+    if (!truth(content)) return null;
+
+    return new _ScalarNode("!", content: content);
+  });
+
+  // 171
+  bool l_nb_literalText(int indent) => transaction(() {
+    zeroOrMore(() => captureAs("\n", () => l_empty(indent, BLOCK_IN)));
+    if (!truth(captureAs("", () => s_indent(indent)))) return false;
+    return truth(oneOrMore(() => consume(isNonBreak)));
+  });
+
+  // 172
+  bool b_nb_literalNext(int indent) => transaction(() {
+    if (!truth(b_asLineFeed())) return false;
+    return l_nb_literalText(indent);
+  });
+
+  // 173
+  String l_literalContent(int indent, int chomping) => captureString(() {
+    transaction(() {
+      if (!truth(l_nb_literalText(indent))) return false;
+      zeroOrMore(() => b_nb_literalNext(indent));
+      return b_chompedLast(chomping);
+    });
+    l_chompedEmpty(indent, chomping);
+    return true;
+  });
+
+  // 174
+  _Node c_l_folded(int indent) => transaction(() {
+    if (!truth(c_indicator(C_FOLDED))) return null;
+    var header = c_b_blockHeader();
+    if (!truth(header)) return null;
+
+    var additionalIndent = blockScalarAdditionalIndentation(header, indent);
+    var content = l_foldedContent(indent + additionalIndent, header.chomping);
+    if (!truth(content)) return null;
+
+    return new _ScalarNode("!", content: content);
+  });
+
+  // 175
+  bool s_nb_foldedText(int indent) => transaction(() {
+    if (!truth(captureAs('', () => s_indent(indent)))) return false;
+    if (!truth(consume(isNonSpace))) return false;
+    zeroOrMore(() => consume(isNonBreak));
+    return true;
+  });
+
+  // 176
+  bool l_nb_foldedLines(int indent) {
+    if (!truth(s_nb_foldedText(indent))) return false;
+    zeroOrMore(() => transaction(() {
+        if (!truth(b_l_folded(indent, BLOCK_IN))) return false;
+        return s_nb_foldedText(indent);
+      }));
+    return true;
+  }
+
+  // 177
+  bool s_nb_spacedText(int indent) => transaction(() {
+    if (!truth(captureAs('', () => s_indent(indent)))) return false;
+    if (!truth(consume(isSpace))) return false;
+    zeroOrMore(() => consume(isNonBreak));
+    return true;
+  });
+
+  // 178
+  bool b_l_spaced(int indent) {
+    if (!truth(b_asLineFeed())) return false;
+    zeroOrMore(() => captureAs("\n", () => l_empty(indent, BLOCK_IN)));
+    return true;
+  }
+
+  // 179
+  bool l_nb_spacedLines(int indent) {
+    if (!truth(s_nb_spacedText(indent))) return false;
+    zeroOrMore(() => transaction(() {
+        if (!truth(b_l_spaced(indent))) return false;
+        return s_nb_spacedText(indent);
+      }));
+    return true;
+  }
+
+  // 180
+  bool l_nb_sameLines(int indent) => transaction(() {
+    zeroOrMore(() => captureAs('\n', () => l_empty(indent, BLOCK_IN)));
+    return or([
+      () => l_nb_foldedLines(indent),
+      () => l_nb_spacedLines(indent)
+    ]);
+  });
+
+  // 181
+  bool l_nb_diffLines(int indent) {
+    if (!truth(l_nb_sameLines(indent))) return false;
+    zeroOrMore(() => transaction(() {
+        if (!truth(b_asLineFeed())) return false;
+        return l_nb_sameLines(indent);
+      }));
+    return true;
+  }
+
+  // 182
+  String l_foldedContent(int indent, int chomping) => captureString(() {
+    transaction(() {
+      if (!truth(l_nb_diffLines(indent))) return false;
+      return b_chompedLast(chomping);
+    });
+    l_chompedEmpty(indent, chomping);
+    return true;
+  });
+
+  // 183
+  _SequenceNode l_blockSequence(int indent) => context('sequence', () {
+    var additionalIndent = countIndentation() - indent;
+    if (additionalIndent <= 0) return null;
+
+    var content = oneOrMore(() => transaction(() {
+      if (!truth(s_indent(indent + additionalIndent))) return null;
+      return c_l_blockSeqEntry(indent + additionalIndent);
+    }));
+    if (!truth(content)) return null;
+
+    return new _SequenceNode("?", content);
+  });
+
+  // 184
+  _Node c_l_blockSeqEntry(int indent) => transaction(() {
+    if (!truth(c_indicator(C_SEQUENCE_ENTRY))) return null;
+    if (isNonSpace(peek())) return null;
+
+    return s_l_blockIndented(indent, BLOCK_IN);
+  });
+
+  // 185
+  _Node s_l_blockIndented(int indent, int ctx) {
+    var additionalIndent = countIndentation();
+    return or([
+      () => transaction(() {
+        if (!truth(s_indent(additionalIndent))) return null;
+        return or([
+          () => ns_l_compactSequence(indent + 1 + additionalIndent),
+          () => ns_l_compactMapping(indent + 1 + additionalIndent)]);
+      }),
+      () => s_l_blockNode(indent, ctx),
+      () => s_l_comments() ? e_node() : null]);
+  }
+
+  // 186
+  _Node ns_l_compactSequence(int indent) => context('sequence', () {
+    var first = c_l_blockSeqEntry(indent);
+    if (!truth(first)) return null;
+
+    var content = zeroOrMore(() => transaction(() {
+        if (!truth(s_indent(indent))) return null;
+        return c_l_blockSeqEntry(indent);
+      }));
+    content.insertRange(0, 1, first);
+
+    return new _SequenceNode("?", content);
+  });
+
+  // 187
+  _Node l_blockMapping(int indent) => context('mapping', () {
+    var additionalIndent = countIndentation() - indent;
+    if (additionalIndent <= 0) return null;
+
+    var pairs = oneOrMore(() => transaction(() {
+      if (!truth(s_indent(indent + additionalIndent))) return null;
+      return ns_l_blockMapEntry(indent + additionalIndent);
+    }));
+    if (!truth(pairs)) return null;
+
+    return map(pairs);
+  });
+
+  // 188
+  _Pair<_Node, _Node> ns_l_blockMapEntry(int indent) => or([
+    () => c_l_blockMapExplicitEntry(indent),
+    () => ns_l_blockMapImplicitEntry(indent)
+  ]);
+
+  // 189
+  _Pair<_Node, _Node> c_l_blockMapExplicitEntry(int indent) {
+    var key = c_l_blockMapExplicitKey(indent);
+    if (!truth(key)) return null;
+
+    var value = or([
+      () => l_blockMapExplicitValue(indent),
+      e_node
+    ]);
+
+    return new _Pair<_Node, _Node>(key, value);
+  }
+
+  // 190
+  _Node c_l_blockMapExplicitKey(int indent) => transaction(() {
+    if (!truth(c_indicator(C_MAPPING_KEY))) return null;
+    return s_l_blockIndented(indent, BLOCK_OUT);
+  });
+
+  // 191
+  _Node l_blockMapExplicitValue(int indent) => transaction(() {
+    if (!truth(s_indent(indent))) return null;
+    if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
+    return s_l_blockIndented(indent, BLOCK_OUT);
+  });
+
+  // 192
+  _Pair<_Node, _Node> ns_l_blockMapImplicitEntry(int indent) => transaction(() {
+    var key = or([ns_s_blockMapImplicitKey, e_node]);
+    var value = c_l_blockMapImplicitValue(indent);
+    return truth(value) ? new _Pair<_Node, _Node>(key, value) : null;
+  });
+
+  // 193
+  _Node ns_s_blockMapImplicitKey() => context('mapping key', () => or([
+    () => c_s_implicitJsonKey(BLOCK_KEY),
+    () => ns_s_implicitYamlKey(BLOCK_KEY)
+  ]));
+
+  // 194
+  _Node c_l_blockMapImplicitValue(int indent) => context('mapping value', () =>
+    transaction(() {
+      if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
+      return or([
+        () => s_l_blockNode(indent, BLOCK_OUT),
+        () => s_l_comments() ? e_node() : null
+      ]);
+    }));
+
+  // 195
+  _Node ns_l_compactMapping(int indent) => context('mapping', () {
+    var first = ns_l_blockMapEntry(indent);
+    if (!truth(first)) return null;
+
+    var pairs = zeroOrMore(() => transaction(() {
+        if (!truth(s_indent(indent))) return null;
+        return ns_l_blockMapEntry(indent);
+      }));
+    pairs.insertRange(0, 1, first);
+
+    return map(pairs);
+  });
+
+  // 196
+  _Node s_l_blockNode(int indent, int ctx) => or([
+    () => s_l_blockInBlock(indent, ctx),
+    () => s_l_flowInBlock(indent)
+  ]);
+
+  // 197
+  _Node s_l_flowInBlock(int indent) => transaction(() {
+    if (!truth(s_separate(indent + 1, FLOW_OUT))) return null;
+    var node = ns_flowNode(indent + 1, FLOW_OUT);
+    if (!truth(node)) return null;
+    if (!truth(s_l_comments())) return null;
+    return node;
+  });
+
+  // 198
+  _Node s_l_blockInBlock(int indent, int ctx) => or([
+    () => s_l_blockScalar(indent, ctx),
+    () => s_l_blockCollection(indent, ctx)
+  ]);
+
+  // 199
+  _Node s_l_blockScalar(int indent, int ctx) => transaction(() {
+    if (!truth(s_separate(indent + 1, ctx))) return null;
+    var props = transaction(() {
+      var innerProps = c_ns_properties(indent + 1, ctx);
+      if (!truth(innerProps)) return null;
+      if (!truth(s_separate(indent + 1, ctx))) return null;
+      return innerProps;
+    });
+
+    var node = or([() => c_l_literal(indent), () => c_l_folded(indent)]);
+    if (!truth(node)) return null;
+    return addProps(node, props);
+  });
+
+  // 200
+  _Node s_l_blockCollection(int indent, int ctx) => transaction(() {
+    var props = transaction(() {
+      if (!truth(s_separate(indent + 1, ctx))) return null;
+      return c_ns_properties(indent + 1, ctx);
+    });
+
+    if (!truth(s_l_comments())) return null;
+    return or([
+      () => l_blockSequence(seqSpaces(indent, ctx)),
+      () => l_blockMapping(indent)]);
+  });
+
+  // 201
+  int seqSpaces(int indent, int ctx) => ctx == BLOCK_OUT ? indent - 1 : indent;
+
+  // 202
+  void l_documentPrefix() {
+    zeroOrMore(l_comment);
+  }
+
+  // 203
+  bool c_directivesEnd() => rawString("---");
+
+  // 204
+  bool c_documentEnd() => rawString("...");
+
+  // 205
+  bool l_documentSuffix() => transaction(() {
+    if (!truth(c_documentEnd())) return false;
+    return s_l_comments();
+  });
+
+  // 206
+  bool c_forbidden() {
+    if (!inBareDocument || !atStartOfLine) return false;
+    var forbidden = false;
+    transaction(() {
+      if (!truth(or([c_directivesEnd, c_documentEnd]))) return;
+      var char = peek();
+      forbidden = isBreak(char) || isSpace(char) || atEndOfFile;
+      return;
+    });
+    return forbidden;
+  }
+
+  // 207
+  _Node l_bareDocument() {
+    try {
+      inBareDocument = true;
+      return s_l_blockNode(-1, BLOCK_IN);
+    } finally {
+      inBareDocument = false;
+    }
+  }
+
+  // 208
+  _Node l_explicitDocument() {
+    if (!truth(c_directivesEnd())) return null;
+    var doc = l_bareDocument();
+    if (truth(doc)) return doc;
+
+    doc = e_node();
+    s_l_comments();
+    return doc;
+  }
+
+  // 209
+  _Node l_directiveDocument() {
+    if (!truth(oneOrMore(l_directive))) return null;
+    var doc = l_explicitDocument();
+    if (doc != null) return doc;
+    parseFailed();
+    return null; // Unreachable.
+  }
+
+  // 210
+  _Node l_anyDocument() =>
+    or([l_directiveDocument, l_explicitDocument, l_bareDocument]);
+
+  // 211
+  List<_Node> l_yamlStream() {
+    var docs = [];
+    zeroOrMore(l_documentPrefix);
+    var first = zeroOrOne(l_anyDocument);
+    if (!truth(first)) first = e_node();
+    docs.add(first);
+
+    zeroOrMore(() {
+      var doc;
+      if (truth(oneOrMore(l_documentSuffix))) {
+        zeroOrMore(l_documentPrefix);
+        doc = zeroOrOne(l_anyDocument);
+      } else {
+        zeroOrMore(l_documentPrefix);
+        doc = zeroOrOne(l_explicitDocument);
+      }
+      if (truth(doc)) docs.add(doc);
+      return doc;
+    });
+
+    if (!atEndOfFile) parseFailed();
+    return docs;
+  }
+}
+
+class SyntaxError extends YamlException {
+  final int line;
+  final int column;
+
+  SyntaxError(this.line, this.column, String msg) : super(msg);
+
+  String toString() => "Syntax error on line $line, column $column: $msg";
+}
+
+/// A pair of values.
+class _Pair<E, F> {
+  E first;
+  F last;
+
+  _Pair(this.first, this.last);
+
+  String toString() => '($first, $last)';
+}
+
+/// The information in the header for a block scalar.
+class _BlockHeader {
+  final int additionalIndent;
+  final int chomping;
+
+  _BlockHeader(this.additionalIndent, this.chomping);
+
+  bool get autoDetectIndent => additionalIndent == null;
+}
+
+/// A range of characters in the YAML document, from [start] to [end]
+/// (inclusive).
+class _Range {
+  /// The first character in the range.
+  final int start;
+
+  /// The last character in the range.
+  final int end;
+
+  _Range(this.start, this.end);
+
+  /// Returns whether or not [pos] lies within this range.
+  bool contains(int pos) => pos >= start && pos <= end;
+}
+
+/// A map that associates [E] values with [_Range]s. It's efficient to create
+/// new associations, but finding the value associated with a position is more
+/// expensive.
+class _RangeMap<E> {
+  /// The ranges and their associated elements.
+  final List<_Pair<_Range, E>> contents;
+
+  _RangeMap() : this.contents = <_Pair<_Range, E>>[];
+
+  /// Returns the value associated with the range in which [pos] lies, or null
+  /// if there is no such range. If there's more than one such range, the most
+  /// recently set one is used.
+  E operator[](int pos) {
+    // Iterate backwards through contents so the more recent range takes
+    // precedence. TODO(nweiz): clean this up when issue 2804 is fixed.
+    for (var i = contents.length - 1; i >= 0; i--) {
+      var pair = contents[i];
+      if (pair.first.contains(pos)) return pair.last;
+    }
+    return null;
+  }
+
+  /// Associates [value] with [range].
+  operator[]=(_Range range, E value) =>
+    contents.add(new _Pair<_Range, E>(range, value));
+}
diff --git a/pkgs/yaml/lib/visitor.dart b/pkgs/yaml/lib/visitor.dart
new file mode 100644
index 0000000..b5c14c9
--- /dev/null
+++ b/pkgs/yaml/lib/visitor.dart
@@ -0,0 +1,27 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of yaml;
+
+/// The visitor pattern for YAML documents.
+class _Visitor {
+  /// Returns [alias].
+  visitAlias(_AliasNode alias) => alias;
+
+  /// Returns [scalar].
+  visitScalar(_ScalarNode scalar) => scalar;
+
+  /// Visits each node in [seq] and returns a list of the results.
+  visitSequence(_SequenceNode seq)
+      => seq.content.mappedBy((e) => e.visit(this)).toList();
+
+  /// Visits each key and value in [map] and returns a map of the results.
+  visitMapping(_MappingNode map) {
+    var out = new YamlMap();
+    for (var key in map.content.keys) {
+      out[key.visit(this)] = map.content[key].visit(this);
+    }
+    return out;
+  }
+}
diff --git a/pkgs/yaml/lib/yaml.dart b/pkgs/yaml/lib/yaml.dart
new file mode 100644
index 0000000..ef0ac83
--- /dev/null
+++ b/pkgs/yaml/lib/yaml.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A parser for [YAML](http://www.yaml.org/).
+///
+/// Use [loadYaml] to load a single document, or [loadYamlStream] to load a
+/// stream of documents. For example:
+///
+///     import 'package:yaml/yaml.dart';
+///     main() {
+///       var doc = loadYaml("YAML: YAML Ain't Markup Language");
+///       print(doc['YAML']);
+///     }
+///
+/// This library currently doesn't support dumping to YAML. You should use
+/// `stringify` from `dart:json` instead:
+///
+///     import 'dart:json' as json;
+///     import 'package:yaml/yaml.dart';
+///     main() {
+///       var doc = loadYaml("YAML: YAML Ain't Markup Language");
+///       print(json.stringify(doc));
+///     }
+library yaml;
+
+import 'dart:math' as Math;
+import 'dart:collection' show Queue;
+
+import 'deep_equals.dart';
+
+part 'yaml_map.dart';
+part 'model.dart';
+part 'parser.dart';
+part 'visitor.dart';
+part 'composer.dart';
+part 'constructor.dart';
+
+/// Loads a single document from a YAML string. If the string contains more than
+/// one document, this throws an error.
+///
+/// The return value is mostly normal Dart objects. However, since YAML mappings
+/// support some key types that the default Dart map implementation doesn't
+/// (null, NaN, booleans, lists, and maps), all maps in the returned document
+/// are YamlMaps. These have a few small behavioral differences from the default
+/// Map implementation; for details, see the YamlMap class.
+loadYaml(String yaml) {
+  var stream = loadYamlStream(yaml);
+  if (stream.length != 1) {
+    throw new YamlException("Expected 1 document, were ${stream.length}");
+  }
+  return stream[0];
+}
+
+/// Loads a stream of documents from a YAML string.
+///
+/// The return value is mostly normal Dart objects. However, since YAML mappings
+/// support some key types that the default Dart map implementation doesn't
+/// (null, NaN, booleans, lists, and maps), all maps in the returned document
+/// are YamlMaps. These have a few small behavioral differences from the default
+/// Map implementation; for details, see the YamlMap class.
+List loadYamlStream(String yaml) {
+  return new _Parser(yaml).l_yamlStream().mappedBy((doc) =>
+      new _Constructor(new _Composer(doc).compose()).construct())
+      .toList();
+}
+
+/// An error thrown by the YAML processor.
+class YamlException implements Exception {
+  String msg;
+
+  YamlException(this.msg);
+
+  String toString() => msg;
+}
diff --git a/pkgs/yaml/lib/yaml_map.dart b/pkgs/yaml/lib/yaml_map.dart
new file mode 100644
index 0000000..65dd119
--- /dev/null
+++ b/pkgs/yaml/lib/yaml_map.dart
@@ -0,0 +1,110 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of yaml;
+
+/// This class wraps behaves almost identically to the normal Dart Map
+/// implementation, with the following differences:
+///
+///  *  It allows null, NaN, boolean, list, and map keys.
+///  *  It defines `==` structurally. That is, `yamlMap1 == yamlMap2` if they
+///     have the same contents.
+///  *  It has a compatible [hashCode] method.
+class YamlMap implements Map {
+  Map _map;
+
+  YamlMap() : _map = new Map();
+
+  YamlMap.from(Map map) : _map = new Map.from(map);
+
+  YamlMap._wrap(this._map);
+
+  bool containsValue(value) => _map.containsValue(value);
+  bool containsKey(key) => _map.containsKey(_wrapKey(key));
+  operator [](key) => _map[_wrapKey(key)];
+  operator []=(key, value) { _map[_wrapKey(key)] = value; }
+  putIfAbsent(key, ifAbsent()) => _map.putIfAbsent(_wrapKey(key), ifAbsent);
+  remove(key) => _map.remove(_wrapKey(key));
+  void clear() => _map.clear();
+  void forEach(void f(key, value)) =>
+    _map.forEach((k, v) => f(_unwrapKey(k), v));
+  Iterable get keys => _map.keys.mappedBy(_unwrapKey);
+  Iterable get values => _map.values;
+  int get length => _map.length;
+  bool get isEmpty => _map.isEmpty;
+  String toString() => _map.toString();
+
+  int get hashCode => _hashCode(_map);
+
+  bool operator ==(other) {
+    if (other is! YamlMap) return false;
+    return deepEquals(this, other);
+  }
+
+  /// Wraps an object for use as a key in the map.
+  _wrapKey(obj) {
+    if (obj != null && obj is! bool && obj is! List &&
+        (obj is! double || !obj.isNan()) &&
+        (obj is! Map || obj is YamlMap)) {
+      return obj;
+    } else if (obj is Map) {
+      return new YamlMap._wrap(obj);
+    }
+    return new _WrappedHashKey(obj);
+  }
+
+  /// Unwraps an object that was used as a key in the map.
+  _unwrapKey(obj) => obj is _WrappedHashKey ? obj.value : obj;
+}
+
+/// A class for wrapping normally-unhashable objects that are being used as keys
+/// in a YamlMap.
+class _WrappedHashKey {
+  var value;
+
+  _WrappedHashKey(this.value);
+
+  int get hashCode => _hashCode(value);
+
+  String toString() => value.toString();
+
+  /// This is defined as both values being structurally equal.
+  bool operator ==(other) {
+    if (other is! _WrappedHashKey) return false;
+    return deepEquals(this.value, other.value);
+  }
+}
+
+/// Returns the hash code for [obj]. This includes null, true, false, maps, and
+/// lists. Also handles self-referential structures.
+int _hashCode(obj, [List parents]) {
+  if (parents == null) {
+    parents = [];
+  } else if (parents.any((p) => identical(p, obj))) {
+    return -1;
+  }
+
+  parents.add(obj);
+  try {
+    if (obj == null) return 0;
+    if (obj == true) return 1;
+    if (obj == false) return 2;
+    if (obj is Map) {
+      return _hashCode(obj.keys, parents) ^
+        _hashCode(obj.values, parents);
+    }
+    if (obj is Iterable) {
+      // This is probably a really bad hash function, but presumably we'll get
+      // this in the standard library before it actually matters.
+      int hash = 0;
+      for (var e in obj) {
+        hash ^= _hashCode(e, parents);
+      }
+      return hash;
+    }
+    return obj.hashCode;
+  } finally {
+    parents.removeLast();
+  }
+}
diff --git a/pkgs/yaml/pubspec.yaml b/pkgs/yaml/pubspec.yaml
new file mode 100644
index 0000000..7c944d2
--- /dev/null
+++ b/pkgs/yaml/pubspec.yaml
@@ -0,0 +1,7 @@
+name: yaml
+author: "Dart Team <misc@dartlang.org>"
+homepage: http://www.dartlang.org
+description: A parser for YAML.
+dependencies:
+  unittest:
+    sdk: unittest
diff --git a/pkgs/yaml/test/yaml_test.dart b/pkgs/yaml/test/yaml_test.dart
new file mode 100644
index 0000000..b939b67
--- /dev/null
+++ b/pkgs/yaml/test/yaml_test.dart
@@ -0,0 +1,1879 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library yaml_test;
+
+import 'package:unittest/unittest.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml/deep_equals.dart';
+// TODO(jmesserly): we should not be reaching outside the YAML package
+// The http package has a similar problem.
+import '../../../tests/utils/test_utils.dart';
+
+/// Constructs a new yaml.YamlMap, optionally from a normal Map.
+Map yamlMap([Map from]) =>
+    from == null ? new YamlMap() : new YamlMap.from(from);
+
+/// Asserts that a string containing a single YAML document produces a given
+/// value when loaded.
+expectYamlLoads(expected, String source) {
+  var actual = loadYaml(cleanUpLiteral(source));
+  Expect.isTrue(deepEquals(expected, actual), 
+      'expectYamlLoads(expected: <$expected>, actual: <$actual>)');
+}
+
+/// Asserts that a string containing a stream of YAML documents produces a given
+/// list of values when loaded.
+expectYamlStreamLoads(List expected, String source) {
+  var actual = loadYamlStream(cleanUpLiteral(source));
+  Expect.isTrue(deepEquals(expected, actual), 
+      'expectYamlStreamLoads(expected: <$expected>, actual: <$actual>)');
+}
+
+main() {
+  var infinity = double.parse("Infinity");
+  var nan = double.parse("NaN");
+
+  group('YamlMap', () {
+    group('accepts as a key', () {
+      _expectKeyWorks(keyFn()) {
+        var map = yamlMap();
+        map[keyFn()] = 5;
+        expect(map.containsKey(keyFn()), isTrue);
+        expect(map[keyFn()], 5);
+      }
+
+      test('null', () => _expectKeyWorks(() => null));
+      test('true', () => _expectKeyWorks(() => true));
+      test('false', () => _expectKeyWorks(() => false));
+      test('a list', () => _expectKeyWorks(() => [1, 2, 3]));
+      test('a map', () => _expectKeyWorks(() => {'foo': 'bar'}));
+      test('a YAML map', () => _expectKeyWorks(() => yamlMap({'foo': 'bar'})));
+    });
+
+    test('works as a hash key', () {
+      var normalMap = new Map();
+      normalMap[yamlMap({'foo': 'bar'})] = 'baz';
+      expect(normalMap.containsKey(yamlMap({'foo': 'bar'})), isTrue);
+      expect(normalMap[yamlMap({'foo': 'bar'})], 'baz');
+    });
+
+    test('treats YamlMap keys the same as normal maps', () {
+      var map = yamlMap();
+      map[{'a': 'b'}] = 5;
+      expect(map[yamlMap({'a': 'b'})], 5);
+    });
+  });
+
+  group('has a friendly error message for', () {
+    var tabError = predicate((e) =>
+        e.toString().contains('tab characters are not allowed as indentation'));
+
+    test('using a tab as indentation', () {
+      expect(() => loadYaml('foo:\n\tbar'),
+          throwsA(tabError));
+    });
+
+    test('using a tab not as indentation', () {
+      expect(() => loadYaml('''
+          "foo
+          \tbar"
+          error'''),
+        throwsA(isNot(tabError)));
+    });
+  });
+
+  // The following tests are all taken directly from the YAML spec
+  // (http://www.yaml.org/spec/1.2/spec.html). Most of them are code examples
+  // that are directly included in the spec, but additional tests are derived
+  // from the prose.
+
+  // A few examples from the spec are deliberately excluded, because they test
+  // features that this implementation doesn't intend to support (character
+  // encoding detection and user-defined tags). More tests are commented out,
+  // because they're intended to be supported but not yet implemented.
+
+  // Chapter 2 is just a preview of various Yaml documents. It's probably not
+  // necessary to test its examples, but it would be nice to test everything in
+  // the spec.
+  group('2.1: Collections', () {
+    test('[Example 2.1]', () {
+      expectYamlLoads(["Mark McGwire", "Sammy Sosa", "Ken Griffey"],
+        """
+        - Mark McGwire
+        - Sammy Sosa
+        - Ken Griffey""");
+    });
+
+    test('[Example 2.2]', () {
+      expectYamlLoads({"hr": 65, "avg": 0.278, "rbi": 147},
+        """
+        hr:  65    # Home runs
+        avg: 0.278 # Batting average
+        rbi: 147   # Runs Batted In""");
+    });
+
+    test('[Example 2.3]', () {
+      expectYamlLoads({
+        "american": ["Boston Red Sox", "Detroit Tigers", "New York Yankees"],
+        "national": ["New York Mets", "Chicago Cubs", "Atlanta Braves"],
+      },
+        """
+        american:
+          - Boston Red Sox
+          - Detroit Tigers
+          - New York Yankees
+        national:
+          - New York Mets
+          - Chicago Cubs
+          - Atlanta Braves""");
+    });
+
+    test('[Example 2.4]', () {
+      expectYamlLoads([
+        {"name": "Mark McGwire", "hr": 65, "avg": 0.278},
+        {"name": "Sammy Sosa", "hr": 63, "avg": 0.288},
+      ],
+        """
+        -
+          name: Mark McGwire
+          hr:   65
+          avg:  0.278
+        -
+          name: Sammy Sosa
+          hr:   63
+          avg:  0.288""");
+    });
+
+    test('[Example 2.5]', () {
+      expectYamlLoads([
+        ["name", "hr", "avg"],
+        ["Mark McGwire", 65, 0.278],
+        ["Sammy Sosa", 63, 0.288]
+      ],
+        """
+        - [name        , hr, avg  ]
+        - [Mark McGwire, 65, 0.278]
+        - [Sammy Sosa  , 63, 0.288]""");
+    });
+
+    test('[Example 2.6]', () {
+      expectYamlLoads({
+        "Mark McGwire": {"hr": 65, "avg": 0.278},
+        "Sammy Sosa": {"hr": 63, "avg": 0.288}
+      },
+        """
+        Mark McGwire: {hr: 65, avg: 0.278}
+        Sammy Sosa: {
+            hr: 63,
+            avg: 0.288
+          }""");
+    });
+  });
+
+  group('2.2: Structures', () {
+    test('[Example 2.7]', () {
+      expectYamlStreamLoads([
+        ["Mark McGwire", "Sammy Sosa", "Ken Griffey"],
+        ["Chicago Cubs", "St Louis Cardinals"]
+      ],
+        """
+        # Ranking of 1998 home runs
+        ---
+        - Mark McGwire
+        - Sammy Sosa
+        - Ken Griffey
+
+        # Team ranking
+        ---
+        - Chicago Cubs
+        - St Louis Cardinals""");
+    });
+
+    test('[Example 2.8]', () {
+      expectYamlStreamLoads([
+        {"time": "20:03:20", "player": "Sammy Sosa", "action": "strike (miss)"},
+        {"time": "20:03:47", "player": "Sammy Sosa", "action": "grand slam"},
+      ],
+        """
+        ---
+        time: 20:03:20
+        player: Sammy Sosa
+        action: strike (miss)
+        ...
+        ---
+        time: 20:03:47
+        player: Sammy Sosa
+        action: grand slam
+        ...""");
+    });
+
+    test('[Example 2.9]', () {
+      expectYamlLoads({
+        "hr": ["Mark McGwire", "Sammy Sosa"],
+        "rbi": ["Sammy Sosa", "Ken Griffey"]
+      },
+        """
+        ---
+        hr: # 1998 hr ranking
+          - Mark McGwire
+          - Sammy Sosa
+        rbi:
+          # 1998 rbi ranking
+          - Sammy Sosa
+          - Ken Griffey""");
+    });
+
+  //   test('[Example 2.10]', () {
+  //     expectYamlLoads({
+  //       "hr": ["Mark McGwire", "Sammy Sosa"],
+  //       "rbi": ["Sammy Sosa", "Ken Griffey"]
+  //     },
+  //       """
+  //       ---
+  //       hr:
+  //         - Mark McGwire
+  //         # Following node labeled SS
+  //         - &SS Sammy Sosa
+  //       rbi:
+  //         - *SS # Subsequent occurrence
+  //         - Ken Griffey""");
+  //   });
+
+    test('[Example 2.11]', () {
+      var doc = yamlMap();
+      doc[["Detroit Tigers", "Chicago cubs"]] = ["2001-07-23"];
+      doc[["New York Yankees", "Atlanta Braves"]] =
+        ["2001-07-02", "2001-08-12", "2001-08-14"];
+      expectYamlLoads(doc,
+        """
+        ? - Detroit Tigers
+          - Chicago cubs
+        :
+          - 2001-07-23
+
+        ? [ New York Yankees,
+            Atlanta Braves ]
+        : [ 2001-07-02, 2001-08-12,
+            2001-08-14 ]""");
+    });
+
+    test('[Example 2.12]', () {
+      expectYamlLoads([
+        {"item": "Super Hoop", "quantity": 1},
+        {"item": "Basketball", "quantity": 4},
+        {"item": "Big Shoes", "quantity": 1},
+      ],
+        """
+        ---
+        # Products purchased
+        - item    : Super Hoop
+          quantity: 1
+        - item    : Basketball
+          quantity: 4
+        - item    : Big Shoes
+          quantity: 1""");
+    });
+  });
+
+  group('2.3: Scalars', () {
+    test('[Example 2.13]', () {
+      expectYamlLoads(
+        cleanUpLiteral(
+        """
+        \\//||\\/||
+        // ||  ||__"""),
+        """
+        # ASCII Art
+        --- |
+          \\//||\\/||
+          // ||  ||__""");
+    });
+
+    test('[Example 2.14]', () {
+      expectYamlLoads("Mark McGwire's year was crippled by a knee injury.",
+        """
+        --- >
+          Mark McGwire's
+          year was crippled
+          by a knee injury.""");
+    });
+
+    test('[Example 2.15]', () {
+      expectYamlLoads(
+        cleanUpLiteral(
+        """
+        Sammy Sosa completed another fine season with great stats.
+
+          63 Home Runs
+          0.288 Batting Average
+
+        What a year!"""),
+        """
+        >
+         Sammy Sosa completed another
+         fine season with great stats.
+
+           63 Home Runs
+           0.288 Batting Average
+
+         What a year!""");
+    });
+
+    test('[Example 2.16]', () {
+      expectYamlLoads({
+        "name": "Mark McGwire",
+        "accomplishment": "Mark set a major league home run record in 1998.\n",
+        "stats": "65 Home Runs\n0.278 Batting Average"
+      },
+        """
+        name: Mark McGwire
+        accomplishment: >
+          Mark set a major league
+          home run record in 1998.
+        stats: |
+          65 Home Runs
+          0.278 Batting Average""");
+    });
+
+    test('[Example 2.17]', () {
+      expectYamlLoads({
+        "unicode": "Sosa did fine.\u263A",
+        "control": "\b1998\t1999\t2000\n",
+        "hex esc": "\r\n is \r\n",
+        "single": '"Howdy!" he cried.',
+        "quoted": " # Not a 'comment'.",
+        "tie-fighter": "|\\-*-/|"
+      },
+        """
+        unicode: "Sosa did fine.\\u263A"
+        control: "\\b1998\\t1999\\t2000\\n"
+        hex esc: "\\x0d\\x0a is \\r\\n"
+
+        single: '"Howdy!" he cried.'
+        quoted: ' # Not a ''comment''.'
+        tie-fighter: '|\\-*-/|'""");
+    });
+
+    test('[Example 2.18]', () {
+      expectYamlLoads({
+        "plain": "This unquoted scalar spans many lines.",
+        "quoted": "So does this quoted scalar.\n"
+      },
+        '''
+        plain:
+          This unquoted scalar
+          spans many lines.
+
+        quoted: "So does this
+          quoted scalar.\\n"''');
+    });
+  });
+
+  group('2.4: Tags', () {
+    test('[Example 2.19]', () {
+      expectYamlLoads({
+        "canonical": 12345,
+        "decimal": 12345,
+        "octal": 12,
+        "hexadecimal": 12
+      },
+        """
+        canonical: 12345
+        decimal: +12345
+        octal: 0o14
+        hexadecimal: 0xC""");
+    });
+
+    test('[Example 2.20]', () {
+      expectYamlLoads({
+        "canonical": 1230.15,
+        "exponential": 1230.15,
+        "fixed": 1230.15,
+        "negative infinity": -infinity,
+        "not a number": nan
+      },
+        """
+        canonical: 1.23015e+3
+        exponential: 12.3015e+02
+        fixed: 1230.15
+        negative infinity: -.inf
+        not a number: .NaN""");
+    });
+
+    test('[Example 2.21]', () {
+      var doc = yamlMap({
+        "booleans": [true, false],
+        "string": "012345"
+      });
+      doc[null] = null;
+      expectYamlLoads(doc,
+        """
+        null:
+        booleans: [ true, false ]
+        string: '012345'""");
+    });
+
+    // Examples 2.22 through 2.26 test custom tag URIs, which this
+    // implementation currently doesn't plan to support.
+  });
+
+  group('2.5 Full Length Example', () {
+    // Example 2.27 tests custom tag URIs, which this implementation currently
+    // doesn't plan to support.
+
+    test('[Example 2.28]', () {
+      expectYamlStreamLoads([
+        {
+          "Time": "2001-11-23 15:01:42 -5",
+          "User": "ed",
+          "Warning": "This is an error message for the log file"
+        },
+        {
+          "Time": "2001-11-23 15:02:31 -5",
+          "User": "ed",
+          "Warning": "A slightly different error message."
+        },
+        {
+          "DateTime": "2001-11-23 15:03:17 -5",
+          "User": "ed",
+          "Fatal": 'Unknown variable "bar"',
+          "Stack": [
+            {
+              "file": "TopClass.py",
+              "line": 23,
+              "code": 'x = MoreObject("345\\n")\n'
+            },
+            {"file": "MoreClass.py", "line": 58, "code": "foo = bar"}
+          ]
+        }
+      ],
+        """
+        ---
+        Time: 2001-11-23 15:01:42 -5
+        User: ed
+        Warning:
+          This is an error message
+          for the log file
+        ---
+        Time: 2001-11-23 15:02:31 -5
+        User: ed
+        Warning:
+          A slightly different error
+          message.
+        ---
+        DateTime: 2001-11-23 15:03:17 -5
+        User: ed
+        Fatal:
+          Unknown variable "bar"
+        Stack:
+          - file: TopClass.py
+            line: 23
+            code: |
+              x = MoreObject("345\\n")
+          - file: MoreClass.py
+            line: 58
+            code: |-
+              foo = bar""");
+    });
+  });
+
+  // Chapter 3 just talks about the structure of loading and dumping Yaml.
+  // Chapter 4 explains conventions used in the spec.
+
+  // Chapter 5: Characters
+  group('5.1: Character Set', () {
+    expectAllowsCharacter(int charCode) {
+      var char = new String.fromCharCodes([charCode]);
+      expectYamlLoads('The character "$char" is allowed',
+          'The character "$char" is allowed');
+    }
+
+    expectAllowsQuotedCharacter(int charCode) {
+      var char = new String.fromCharCodes([charCode]);
+      expectYamlLoads("The character '$char' is allowed",
+          '"The character \'$char\' is allowed"');
+    }
+
+    expectDisallowsCharacter(int charCode) {
+      var char = new String.fromCharCodes([charCode]);
+      Expect.throws(() => loadYaml('The character "$char" is disallowed'));
+    }
+
+    test("doesn't include C0 control characters", () {
+      expectDisallowsCharacter(0x0);
+      expectDisallowsCharacter(0x8);
+      expectDisallowsCharacter(0x1F);
+    });
+
+    test("includes TAB", () => expectAllowsCharacter(0x9));
+    test("doesn't include DEL", () => expectDisallowsCharacter(0x7F));
+
+    test("doesn't include C1 control characters", () {
+      expectDisallowsCharacter(0x80);
+      expectDisallowsCharacter(0x8A);
+      expectDisallowsCharacter(0x9F);
+    });
+
+    test("includes NEL", () => expectAllowsCharacter(0x85));
+
+    group("within quoted strings", () {
+      test("includes DEL", () => expectAllowsQuotedCharacter(0x7F));
+      test("includes C1 control characters", () {
+        expectAllowsQuotedCharacter(0x80);
+        expectAllowsQuotedCharacter(0x8A);
+        expectAllowsQuotedCharacter(0x9F);
+      });
+    });
+  });
+
+  // Skipping section 5.2 (Character Encodings), since at the moment the module
+  // assumes that the client code is providing it with a string of the proper
+  // encoding.
+
+  group('5.3: Indicator Characters', () {
+    test('[Example 5.3]', () {
+      expectYamlLoads({
+        'sequence': ['one', 'two'],
+        'mapping': {'sky': 'blue', 'sea': 'green'}
+      },
+        """
+        sequence:
+        - one
+        - two
+        mapping:
+          ? sky
+          : blue
+          sea : green""");
+    });
+
+    test('[Example 5.4]', () {
+      expectYamlLoads({
+        'sequence': ['one', 'two'],
+        'mapping': {'sky': 'blue', 'sea': 'green'}
+      },
+        """
+        sequence: [ one, two, ]
+        mapping: { sky: blue, sea: green }""");
+    });
+
+    test('[Example 5.5]', () => expectYamlLoads(null, "# Comment only."));
+
+    // Skipping 5.6 because it uses an undefined tag.
+
+    test('[Example 5.7]', () {
+      expectYamlLoads({
+        'literal': "some\ntext\n",
+        'folded': "some text\n"
+      },
+        """
+        literal: |
+          some
+          text
+        folded: >
+          some
+          text
+        """);
+    });
+
+    test('[Example 5.8]', () {
+      expectYamlLoads({
+        'single': "text",
+        'double': "text"
+      },
+        """
+        single: 'text'
+        double: "text"
+        """);
+    });
+
+    // test('[Example 5.9]', () {
+    //   expectYamlLoads("text",
+    //     """
+    //     %YAML 1.2
+    //     --- text""");
+    // });
+
+    test('[Example 5.10]', () {
+      Expect.throws(() => loadYaml("commercial-at: @text"));
+      Expect.throws(() => loadYaml("commercial-at: `text"));
+    });
+  });
+
+  group('5.4: Line Break Characters', () {
+    group('include', () {
+      test('\\n', () => expectYamlLoads([1, 2], indentLiteral("- 1\n- 2")));
+      test('\\r', () => expectYamlLoads([1, 2], "- 1\r- 2"));
+    });
+
+    group('do not include', () {
+      test('form feed', () => Expect.throws(() => loadYaml("- 1\x0C- 2")));
+      test('NEL', () => expectYamlLoads(["1\x85- 2"], "- 1\x85- 2"));
+      test('0x2028', () => expectYamlLoads(["1\u2028- 2"], "- 1\u2028- 2"));
+      test('0x2029', () => expectYamlLoads(["1\u2029- 2"], "- 1\u2029- 2"));
+    });
+
+    group('in a scalar context must be normalized', () {
+      test("from \\r to \\n", () =>
+            expectYamlLoads(["foo\nbar"], indentLiteral('- |\n  foo\r  bar')));
+      test("from \\r\\n to \\n", () =>
+            expectYamlLoads(["foo\nbar"], indentLiteral('- |\n  foo\r\n  bar')));
+    });
+
+    test('[Example 5.11]', () {
+      expectYamlLoads(
+        cleanUpLiteral("""
+        Line break (no glyph)
+        Line break (glyphed)"""),
+        """
+        |
+          Line break (no glyph)
+          Line break (glyphed)""");
+    });
+  });
+
+  group('5.5: White Space Characters', () {
+    test('[Example 5.12]', () {
+      expectYamlLoads({
+        "quoted": "Quoted \t",
+        "block": 'void main() {\n\tprintf("Hello, world!\\n");\n}\n'
+      }, 
+        """
+        # Tabs and spaces
+        quoted: "Quoted \t"
+        block:\t|
+          void main() {
+          \tprintf("Hello, world!\\n");
+          }
+        """);
+    });
+  });
+
+  group('5.7: Escaped Characters', () {
+    test('[Example 5.13]', () {
+      expectYamlLoads(
+        "Fun with \x5C "
+        "\x22 \x07 \x08 \x1B \x0C "
+        "\x0A \x0D \x09 \x0B \x00 "
+        "\x20 \xA0 \x85 \u2028 \u2029 "
+        "A A A",
+        '''
+        "Fun with \\\\
+        \\" \\a \\b \\e \\f \\
+        \\n \\r \\t \\v \\0 \\
+        \\  \\_ \\N \\L \\P \\
+        \\x41 \\u0041 \\U00000041"''');
+    });
+
+    test('[Example 5.14]', () {
+      Expect.throws(() => loadYaml('Bad escape: "\\c"'));
+      Expect.throws(() => loadYaml('Bad escape: "\\xq-"'));
+    });
+  });
+
+  // Chapter 6: Basic Structures
+  group('6.1: Indentation Spaces', () {
+    test('may not include TAB characters', () {
+      Expect.throws(() => loadYaml(cleanUpLiteral(
+        """
+        -
+        \t- foo
+        \t- bar""")));
+    });
+
+    test('must be the same for all sibling nodes', () {
+      Expect.throws(() => loadYaml(cleanUpLiteral(
+        """
+        -
+          - foo
+         - bar""")));
+    });
+
+    test('may be different for the children of sibling nodes', () {
+      expectYamlLoads([["foo"], ["bar"]],
+        """
+        -
+          - foo
+        -
+         - bar""");
+    });
+
+    test('[Example 6.1]', () {
+      expectYamlLoads({
+        "Not indented": {
+          "By one space": "By four\n  spaces\n",
+          "Flow style": [
+            "By two",
+            "Also by two",
+            "Still by two"
+          ]
+        }
+      },
+        """
+          # Leading comment line spaces are
+           # neither content nor indentation.
+            
+        Not indented:
+         By one space: |
+            By four
+              spaces
+         Flow style: [    # Leading spaces
+           By two,        # in flow style
+          Also by two,    # are neither
+          \tStill by two   # content nor
+            ]             # indentation.""");
+    });
+
+    test('[Example 6.2]', () {
+      expectYamlLoads({'a': ['b', ['c', 'd']]},
+        """
+        ? a
+        : -\tb
+          -  -\tc
+             - d""");
+    });
+  });
+
+  group('6.2: Separation Spaces', () {
+    test('[Example 6.3]', () {
+      expectYamlLoads([{'foo': 'bar'}, ['baz', 'baz']],
+        """
+        - foo:\t bar
+        - - baz
+          -\tbaz""");
+    });
+  });
+
+  group('6.3: Line Prefixes', () {
+    test('[Example 6.4]', () {
+      expectYamlLoads({
+        "plain": "text lines",
+        "quoted": "text lines",
+        "block": "text\n \tlines\n"
+      }, 
+        """
+        plain: text
+          lines
+        quoted: "text
+          \tlines"
+        block: |
+          text
+           \tlines
+        """);
+    });
+  });
+
+  group('6.4: Empty Lines', () {
+    test('[Example 6.5]', () {
+      expectYamlLoads({
+        "Folding": "Empty line\nas a line feed",
+        "Chomping": "Clipped empty lines\n",
+      },
+        """
+        Folding:
+          "Empty line
+           \t
+          as a line feed"
+        Chomping: |
+          Clipped empty lines
+         """);
+    });
+  });
+
+  group('6.5: Line Folding', () {
+    test('[Example 6.6]', () {
+      expectYamlLoads("trimmed\n\n\nas space",
+        """
+        >-
+          trimmed
+          
+         
+
+          as
+          space
+        """);
+    });
+
+    test('[Example 6.7]', () {
+      expectYamlLoads("foo \n\n\t bar\n\nbaz\n",
+        """
+        >
+          foo 
+         
+          \t bar
+
+          baz
+        """);
+    });
+
+    test('[Example 6.8]', () {
+      expectYamlLoads(" foo\nbar\nbaz ",
+        '''
+        "
+          foo 
+         
+          \t bar
+
+          baz
+        "''');
+    });
+  });
+
+  group('6.6: Comments', () {
+    test('must be separated from other tokens by white space characters', () {
+      expectYamlLoads("foo#bar", "foo#bar");
+      expectYamlLoads("foo:#bar", "foo:#bar");
+      expectYamlLoads("-#bar", "-#bar");
+    });
+
+    test('[Example 6.9]', () {
+      expectYamlLoads({'key': 'value'},
+        """
+        key:    # Comment
+          value""");
+    });
+
+    group('outside of scalar content', () {
+      test('may appear on a line of their own', () {
+        expectYamlLoads([1, 2],
+        """
+        - 1
+        # Comment
+        - 2""");
+      });
+
+      test('are independent of indentation level', () {
+        expectYamlLoads([[1, 2]],
+        """
+        -
+          - 1
+         # Comment
+          - 2""");
+      });
+
+      test('include lines containing only white space characters', () {
+        expectYamlLoads([1, 2],
+        """
+        - 1
+          \t  
+        - 2""");
+      });
+    });
+
+    group('within scalar content', () {
+      test('may not appear on a line of their own', () {
+        expectYamlLoads(["foo\n# not comment\nbar\n"],
+        """
+        - |
+          foo
+          # not comment
+          bar
+        """);
+      });
+
+      test("don't include lines containing only white space characters", () {
+        expectYamlLoads(["foo\n  \t   \nbar\n"],
+        """
+        - |
+          foo
+            \t   
+          bar
+        """);
+      });
+    });
+
+    test('[Example 6.10]', () {
+      expectYamlLoads(null,
+        """
+          # Comment
+           
+        """);
+    });
+
+    test('[Example 6.11]', () {
+      expectYamlLoads({'key': 'value'},
+        """
+        key:    # Comment
+                # lines
+          value
+        """);
+    });
+
+    group('ending a block scalar header', () {
+      test('may not be followed by additional comment lines', () {
+        expectYamlLoads(["# not comment\nfoo\n"],
+        """
+        - | # comment
+            # not comment
+            foo
+        """);
+      });
+    });
+  });
+
+  group('6.7: Separation Lines', () {
+    test('may not be used within implicit keys', () {
+      Expect.throws(() => loadYaml(cleanUpLiteral(
+        """
+        [1,
+         2]: 3""")));
+    });
+
+    test('[Example 6.12]', () {
+      var doc = yamlMap();
+      doc[{'first': 'Sammy', 'last': 'Sosa'}] = {
+        'hr': 65,
+        'avg': 0.278
+      };
+      expectYamlLoads(doc,
+        """
+        { first: Sammy, last: Sosa }:
+        # Statistics:
+          hr:  # Home runs
+             65
+          avg: # Average
+           0.278""");
+    });
+  });
+
+  group('6.8: Directives', () {
+    // // TODO(nweiz): assert that this produces a warning
+    // test('[Example 6.13]', () {
+    //   expectYamlLoads("foo",
+    //     '''
+    //     %FOO  bar baz # Should be ignored
+    //                    # with a warning.
+    //     --- "foo"''');
+    // });
+
+    // // TODO(nweiz): assert that this produces a warning
+    // test('[Example 6.14]', () {
+    //   expectYamlLoads("foo",
+    //     '''
+    //     %YAML 1.3 # Attempt parsing
+    //                # with a warning
+    //     ---
+    //     "foo"''');
+    // });
+
+    // test('[Example 6.15]', () {
+    //   Expect.throws(() => loadYaml(cleanUpLiteral(
+    //     """
+    //     %YAML 1.2
+    //     %YAML 1.1
+    //     foo""")));
+    // });
+
+    // test('[Example 6.16]', () {
+    //   expectYamlLoads("foo",
+    //     '''
+    //     %TAG !yaml! tag:yaml.org,2002:
+    //     ---
+    //     !yaml!str "foo"''');
+    // });
+
+    // test('[Example 6.17]', () {
+    //   Expect.throws(() => loadYaml(cleanUpLiteral(
+    //     """
+    //     %TAG ! !foo
+    //     %TAG ! !foo
+    //     bar""")));
+    // });
+
+    // Examples 6.18 through 6.22 test custom tag URIs, which this
+    // implementation currently doesn't plan to support.
+  });
+
+  group('6.9: Node Properties', () {
+    // test('may be specified in any order', () {
+    //   expectYamlLoads(["foo", "bar"],
+    //     """
+    //     - !!str &a1 foo
+    //     - &a2 !!str bar""");
+    // });
+
+    // test('[Example 6.23]', () {
+    //   expectYamlLoads({
+    //     "foo": "bar",
+    //     "baz": "foo"
+    //   },
+    //     '''
+    //     !!str &a1 "foo":
+    //       !!str bar
+    //     &a2 baz : *a1''');
+    // });
+
+    // // Example 6.24 tests custom tag URIs, which this implementation currently
+    // // doesn't plan to support.
+
+    // test('[Example 6.25]', () {
+    //   Expect.throws(() => loadYaml("- !<!> foo"));
+    //   Expect.throws(() => loadYaml("- !<\$:?> foo"));
+    // });
+
+    // // Examples 6.26 and 6.27 test custom tag URIs, which this implementation
+    // // currently doesn't plan to support.
+
+    // test('[Example 6.28]', () {
+    //   expectYamlLoads(["12", 12, "12"],
+    //     '''
+    //     # Assuming conventional resolution:
+    //     - "12"
+    //     - 12
+    //     - ! 12''');
+    // });
+
+    // test('[Example 6.29]', () {
+    //   expectYamlLoads({
+    //     "First occurrence": "Value",
+    //     "Second occurrence": "anchor"
+    //   },
+    //     """
+    //     First occurrence: &anchor Value
+    //     Second occurrence: *anchor""");
+    // });
+  });
+
+  // Chapter 7: Flow Styles
+  group('7.1: Alias Nodes', () {
+    // test("must not use an anchor that doesn't previously occur", () {
+    //   Expect.throws(() => loadYaml(cleanUpLiteral(
+    //     """
+    //     - *anchor
+    //     - &anchor foo"""));
+    // });
+
+    // test("don't have to exist for a given anchor node", () {
+    //   expectYamlLoads(["foo"], "- &anchor foo");
+    // });
+
+    // group('must not specify', () {
+    //   test('tag properties', () => Expect.throws(() => loadYaml(cleanUpLiteral(
+    //     """
+    //     - &anchor foo
+    //     - !str *anchor""")));
+
+    //   test('anchor properties', () => Expect.throws(
+    //           () => loadYaml(cleanUpLiteral(
+    //     """
+    //     - &anchor foo
+    //     - &anchor2 *anchor""")));
+
+    //   test('content', () => Expect.throws(() => loadYaml(cleanUpLiteral(
+    //     """
+    //     - &anchor foo
+    //     - *anchor bar""")));
+    // });
+
+    // test('must preserve structural equality', () {
+    //   var doc = loadYaml(cleanUpLiteral(
+    //     """
+    //     anchor: &anchor [a, b, c]
+    //     alias: *anchor""");
+    //   var anchorList = doc['anchor'];
+    //   var aliasList = doc['alias'];
+    //   Expect.isTrue(anchorList === aliasList);
+    //   anchorList.add('d');
+    //   Expect.listEquals(['a', 'b', 'c', 'd'], aliasList);
+
+    //   doc = loadYaml(cleanUpLiteral(
+    //     """
+    //     ? &anchor [a, b, c]
+    //     : ? *anchor
+    //       : bar""");
+    //   anchorList = doc.keys[0];
+    //   aliasList = doc[['a', 'b', 'c']].keys[0];
+    //   Expect.isTrue(anchorList === aliasList);
+    //   anchorList.add('d');
+    //   Expect.listEquals(['a', 'b', 'c', 'd'], aliasList);
+    // });
+
+    // test('[Example 7.1]', () {
+    //   expectYamlLoads({
+    //     "First occurence": "Foo",
+    //     "Second occurence": "Foo",
+    //     "Override anchor": "Bar",
+    //     "Reuse anchor": "Bar",
+    //   },
+    //     """
+    //     First occurrence: &anchor Foo
+    //     Second occurrence: *anchor
+    //     Override anchor: &anchor Bar
+    //     Reuse anchor: *anchor""");
+    // });
+  });
+
+  group('7.2: Empty Nodes', () {
+    // test('[Example 7.2]', () {
+    //   expectYamlLoads({
+    //     "foo": "",
+    //     "": "bar"
+    //   },
+    //     """
+    //     {
+    //       foo : !!str,
+    //       !!str : bar,
+    //     }""");
+    // });
+
+    test('[Example 7.3]', () {
+      var doc = yamlMap({"foo": null});
+      doc[null] = "bar";
+      expectYamlLoads(doc,
+        """
+        {
+          ? foo :,
+          : bar,
+        }""");
+    });
+  });
+
+  group('7.3: Flow Scalar Styles', () {
+    test('[Example 7.4]', () {
+      expectYamlLoads({
+        "implicit block key": [{"implicit flow key": "value"}]
+      },
+        '''
+        "implicit block key" : [
+          "implicit flow key" : value,
+         ]''');
+    });
+
+    test('[Example 7.5]', () {
+      expectYamlLoads(
+        "folded to a space,\nto a line feed, or \t \tnon-content",
+        '''
+        "folded 
+        to a space,\t
+         
+        to a line feed, or \t\\
+         \\ \tnon-content"''');
+    });
+
+    test('[Example 7.6]', () {
+      expectYamlLoads(" 1st non-empty\n2nd non-empty 3rd non-empty ",
+        '''
+        " 1st non-empty
+
+         2nd non-empty 
+        \t3rd non-empty "''');
+    });
+
+    test('[Example 7.7]', () {
+      expectYamlLoads("here's to \"quotes\"", "'here''s to \"quotes\"'");
+    });
+
+    test('[Example 7.8]', () {
+      expectYamlLoads({
+        "implicit block key": [{"implicit flow key": "value"}]
+      },
+        """
+        'implicit block key' : [
+          'implicit flow key' : value,
+         ]""");
+    });
+
+    test('[Example 7.9]', () {
+      expectYamlLoads(" 1st non-empty\n2nd non-empty 3rd non-empty ",
+        """
+        ' 1st non-empty
+
+         2nd non-empty 
+        \t3rd non-empty '""");
+    });
+
+    test('[Example 7.10]', () {
+      expectYamlLoads([
+        "::vector", ": - ()", "Up, up, and away!", -123,
+        "http://example.com/foo#bar",
+        [
+          "::vector", ": - ()", "Up, up, and away!", -123,
+          "http://example.com/foo#bar"
+        ]
+      ],
+        '''
+        # Outside flow collection:
+        - ::vector
+        - ": - ()"
+        - Up, up, and away!
+        - -123
+        - http://example.com/foo#bar
+        # Inside flow collection:
+        - [ ::vector,
+          ": - ()",
+          "Up, up, and away!",
+          -123,
+          http://example.com/foo#bar ]''');
+    });
+
+    test('[Example 7.11]', () {
+      expectYamlLoads({
+        "implicit block key": [{"implicit flow key": "value"}]
+      },
+        """
+        implicit block key : [
+          implicit flow key : value,
+         ]""");
+    });
+
+    test('[Example 7.12]', () {
+      expectYamlLoads("1st non-empty\n2nd non-empty 3rd non-empty",
+        """
+        1st non-empty
+
+         2nd non-empty 
+        \t3rd non-empty""");
+    });
+  });
+
+  group('7.4: Flow Collection Styles', () {
+    test('[Example 7.13]', () {
+      expectYamlLoads([
+        ['one', 'two'],
+        ['three', 'four']
+      ],
+        """
+        - [ one, two, ]
+        - [three ,four]""");
+    });
+
+    test('[Example 7.14]', () {
+      expectYamlLoads([
+        "double quoted", "single quoted", "plain text", ["nested"],
+        {"single": "pair"}
+      ],
+        """
+        [
+        "double
+         quoted", 'single
+                   quoted',
+        plain
+         text, [ nested ],
+        single: pair,
+        ]""");
+    });
+
+    test('[Example 7.15]', () {
+      expectYamlLoads([
+        {"one": "two", "three": "four"},
+        {"five": "six", "seven": "eight"},
+      ],
+        """
+        - { one : two , three: four , }
+        - {five: six,seven : eight}""");
+    });
+
+    test('[Example 7.16]', () {
+      var doc = yamlMap({
+        "explicit": "entry",
+        "implicit": "entry"
+      });
+      doc[null] = null;
+      expectYamlLoads(doc,
+        """
+        {
+        ? explicit: entry,
+        implicit: entry,
+        ?
+        }""");
+    });
+
+    test('[Example 7.17]', () {
+      var doc = yamlMap({
+        "unquoted": "separate",
+        "http://foo.com": null,
+        "omitted value": null
+      });
+      doc[null] = "omitted key";
+      expectYamlLoads(doc,
+        '''
+        {
+        unquoted : "separate",
+        http://foo.com,
+        omitted value:,
+        : omitted key,
+        }''');
+    });
+
+    test('[Example 7.18]', () {
+      expectYamlLoads({
+        "adjacent": "value",
+        "readable": "value",
+        "empty": null
+      },
+        '''
+        {
+        "adjacent":value,
+        "readable": value,
+        "empty":
+        }''');
+    });
+
+    test('[Example 7.19]', () {
+      expectYamlLoads([{"foo": "bar"}],
+        """
+        [
+        foo: bar
+        ]""");
+    });
+
+    test('[Example 7.20]', () {
+      expectYamlLoads([{"foo bar": "baz"}],
+        """
+        [
+        ? foo
+         bar : baz
+        ]""");
+    });
+
+    test('[Example 7.21]', () {
+      var el1 = yamlMap();
+      el1[null] = "empty key entry";
+
+      var el2 = yamlMap();
+      el2[{"JSON": "like"}] = "adjacent";
+
+      expectYamlLoads([[{"YAML": "separate"}], [el1], [el2]],
+        """
+        - [ YAML : separate ]
+        - [ : empty key entry ]
+        - [ {JSON: like}:adjacent ]""");
+    });
+
+    test('[Example 7.22]', () {
+      Expect.throws(() => loadYaml(cleanUpLiteral(
+        """
+        [ foo
+         bar: invalid ]""")));
+
+      // TODO(nweiz): enable this when we throw an error for long keys
+      // var dotList = [];
+      // dotList.insertRange(0, 1024, ' ');
+      // var dots = Strings.join(dotList, '');
+      // Expect.throws(() => loadYaml('[ "foo...$dots...bar": invalid ]'));
+    });
+  });
+
+  group('7.5: Flow Nodes', () {
+    test('[Example 7.23]', () {
+      expectYamlLoads([["a", "b"], {"a": "b"}, "a", "b", "c"],
+        """
+        - [ a, b ]
+        - { a: b }
+        - "a"
+        - 'b'
+        - c""");
+    });
+
+    // test('[Example 7.24]', () {
+    //   expectYamlLoads(["a", "b", "c", "c", ""],
+    //     """
+    //     - !!str "a"
+    //     - 'b'
+    //     - &anchor "c"
+    //     - *anchor
+    //     - !!str""");
+    // });
+  });
+
+  // Chapter 8: Block Styles
+  group('8.1: Block Scalar Styles', () {
+    test('[Example 8.1]', () {
+      expectYamlLoads(["literal\n", " folded\n", "keep\n\n", " strip"],
+        """
+        - | # Empty header
+         literal
+        - >1 # Indentation indicator
+          folded
+        - |+ # Chomping indicator
+         keep
+
+        - >1- # Both indicators
+          strip""");
+    });
+
+    test('[Example 8.2]', () {
+      // Note: in the spec, the fourth element in this array is listed as
+      // "\t detected\n", not "\t\ndetected\n". However, I'm reasonably
+      // confident that "\t\ndetected\n" is correct when parsed according to the
+      // rest of the spec.
+      expectYamlLoads([
+        "detected\n",
+        "\n\n# detected\n",
+        " explicit\n",
+        "\t\ndetected\n"
+      ],
+        """
+        - |
+         detected
+        - >
+         
+          
+          # detected
+        - |1
+          explicit
+        - >
+         \t
+         detected
+        """);
+    });
+
+    test('[Example 8.3]', () {
+      Expect.throws(() => loadYaml(cleanUpLiteral(
+        """
+        - |
+          
+         text""")));
+
+      Expect.throws(() => loadYaml(cleanUpLiteral(
+        """
+        - >
+          text
+         text""")));
+
+      Expect.throws(() => loadYaml(cleanUpLiteral(
+        """
+        - |2
+         text""")));
+    });
+
+    test('[Example 8.4]', () {
+      expectYamlLoads({"strip": "text", "clip": "text\n", "keep": "text\n"},
+        """
+        strip: |-
+          text
+        clip: |
+          text
+        keep: |+
+          text
+        """);
+    });
+
+    test('[Example 8.5]', () {
+      // This example in the spec only includes a single newline in the "keep"
+      // value, but as far as I can tell that's not how it's supposed to be
+      // parsed according to the rest of the spec.
+      expectYamlLoads({
+        "strip": "# text",
+        "clip": "# text\n",
+        "keep": "# text\n\n"
+      },
+        """
+         # Strip
+          # Comments:
+        strip: |-
+          # text
+          
+         # Clip
+          # comments:
+
+        clip: |
+          # text
+         
+         # Keep
+          # comments:
+
+        keep: |+
+          # text
+
+         # Trail
+          # comments.
+        """);
+    });
+
+    test('[Example 8.6]', () {
+      expectYamlLoads({"strip": "", "clip": "", "keep": "\n"},
+        """
+        strip: >-
+
+        clip: >
+
+        keep: |+
+
+        """);
+    });
+
+    test('[Example 8.7]', () {
+      expectYamlLoads("literal\n\ttext\n",
+        """
+        |
+         literal
+         \ttext
+        """);
+    });
+
+    test('[Example 8.8]', () {
+      expectYamlLoads("\n\nliteral\n \n\ntext\n",
+        """
+        |
+         
+          
+          literal
+           
+          
+          text
+
+         # Comment""");
+    });
+
+    test('[Example 8.9]', () {
+      expectYamlLoads("folded text\n",
+        """
+        >
+         folded
+         text
+        """);
+    });
+
+    test('[Example 8.10]', () {
+      expectYamlLoads(
+        cleanUpLiteral("""
+
+        folded line
+        next line
+          * bullet
+
+          * list
+          * lines
+
+        last line
+        """),
+        """
+        >
+
+         folded
+         line
+
+         next
+         line
+           * bullet
+
+           * list
+           * lines
+
+         last
+         line
+
+        # Comment""");
+    });
+
+    // Examples 8.11 through 8.13 are duplicates of 8.10.
+  });
+
+  group('8.2: Block Collection Styles', () {
+    test('[Example 8.14]', () {
+      expectYamlLoads({"block sequence": ["one", {"two": "three"}]},
+        """
+        block sequence:
+          - one
+          - two : three""");
+    });
+
+    test('[Example 8.15]', () {
+      expectYamlLoads([
+        null, "block node\n", ["one", "two"], {"one": "two"}
+      ],
+        """
+        - # Empty
+        - |
+         block node
+        - - one # Compact
+          - two # sequence
+        - one: two # Compact mapping""");
+    });
+
+    test('[Example 8.16]', () {
+      expectYamlLoads({"block mapping": {"key": "value"}},
+        """
+        block mapping:
+         key: value""");
+    });
+
+    test('[Example 8.17]', () {
+      expectYamlLoads({
+        "explicit key": null,
+        "block key\n": ["one", "two"]
+      },
+        """
+        ? explicit key # Empty value
+        ? |
+          block key
+        : - one # Explicit compact
+          - two # block value""");
+    });
+
+    test('[Example 8.18]', () {
+      var doc = yamlMap({
+        'plain key': 'in-line value',
+        "quoted key": ["entry"]
+      });
+      doc[null] = null;
+      expectYamlLoads(doc,
+        '''
+        plain key: in-line value
+        : # Both empty
+        "quoted key":
+        - entry''');
+    });
+
+    test('[Example 8.19]', () {
+      var el = yamlMap();
+      el[{'earth': 'blue'}] = {'moon': 'white'};
+      expectYamlLoads([{'sun': 'yellow'}, el],
+        """
+        - sun: yellow
+        - ? earth: blue
+          : moon: white""");
+    });
+
+    // test('[Example 8.20]', () {
+    //   expectYamlLoads(["flow in block", "Block scalar\n", {"foo": "bar"}],
+    //     '''
+    //     -
+    //       "flow in block"
+    //     - >
+    //      Block scalar
+    //     - !!map # Block collection
+    //       foo : bar''');
+    // });
+
+    // test('[Example 8.21]', () {
+    //   expectYamlLoads({"literal": "value", "folded": "value"},
+    //     """
+    //     literal: |2
+    //       value
+    //     folded:
+    //        !!str
+    //       >1
+    //      value""");
+    // });
+
+    // test('[Example 8.22]', () {
+    //   expectYamlLoads({
+    //     "sequence": ["entry", ["nested"]],
+    //     "mapping": {"foo": "bar"}
+    //   },
+    //     """
+    //     sequence: !!seq
+    //     - entry
+    //     - !!seq
+    //      - nested
+    //     mapping: !!map
+    //      foo: bar""");
+    // });
+  });
+
+  // Chapter 9: YAML Character Stream
+  group('9.1: Documents', () {
+    // Example 9.1 tests the use of a BOM, which this implementation currently
+    // doesn't plan to support.
+
+    // test('[Example 9.2]', () {
+    //   expectYamlLoads("Document",
+    //     """
+    //     %YAML 1.2
+    //     ---
+    //     Document
+    //     ... # Suffix""");
+    // });
+
+    test('[Example 9.3]', () {
+      // The spec example indicates that the comment after "%!PS-Adobe-2.0"
+      // should be stripped, which would imply that that line is not part of the
+      // literal defined by the "|". The rest of the spec is ambiguous on this
+      // point; the allowable indentation for non-indented literal content is
+      // not clearly explained. However, if both the "|" and the text were
+      // indented the same amount, the text would be part of the literal, which
+      // implies that the spec's parse of this document is incorrect.
+      expectYamlStreamLoads(
+        ["Bare document", "%!PS-Adobe-2.0 # Not the first line\n"],
+        """
+        Bare
+        document
+        ...
+        # No document
+        ...
+        |
+        %!PS-Adobe-2.0 # Not the first line
+        """);
+    });
+
+    test('[Example 9.4]', () {
+      expectYamlStreamLoads([{"matches %": 20}, null],
+        """
+        ---
+        { matches
+        % : 20 }
+        ...
+        ---
+        # Empty
+        ...""");
+    });
+
+    // test('[Example 9.5]', () {
+    //   expectYamlStreamLoads(["%!PS-Adobe-2.0\n", null],
+    //     """
+    //     %YAML 1.2
+    //     --- |
+    //     %!PS-Adobe-2.0
+    //     ...
+    //     %YAML1.2
+    //     ---
+    //     # Empty
+    //     ...""");
+    // });
+
+    // test('[Example 9.6]', () {
+    //   expectYamlStreamLoads(["Document", null, {"matches %": 20}],
+    //     """
+    //     Document
+    //     ---
+    //     # Empty
+    //     ...
+    //     %YAML 1.2
+    //     ---
+    //     matches %: 20""");
+    // });
+  });
+
+  // Chapter 10: Recommended Schemas
+  group('10.1: Failsafe Schema', () {
+    // test('[Example 10.1]', () {
+    //   expectYamlStreamLoads({
+    //     "Block style": {
+    //       "Clark": "Evans",
+    //       "Ingy": "döt Net",
+    //       "Oren": "Ben-Kiki"
+    //     },
+    //     "Flow style": {
+    //       "Clark": "Evans",
+    //       "Ingy": "döt Net",
+    //       "Oren": "Ben-Kiki"
+    //     }
+    //   },
+    //     """
+    //     Block style: !!map
+    //       Clark : Evans
+    //       Ingy  : döt Net
+    //       Oren  : Ben-Kiki
+
+    //     Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki }""");
+    // });
+
+    // test('[Example 10.2]', () {
+    //   expectYamlStreamLoads({
+    //     "Block style": ["Clark Evans", "Ingy döt Net", "Oren Ben-Kiki"],
+    //     "Flow style": ["Clark Evans", "Ingy döt Net", "Oren Ben-Kiki"]
+    //   },
+    //     """
+    //     Block style: !!seq
+    //     - Clark Evans
+    //     - Ingy döt Net
+    //     - Oren Ben-Kiki
+
+    //     Flow style: !!seq [ Clark Evans, Ingy döt Net, Oren Ben-Kiki ]""");
+    // });
+
+    // test('[Example 10.3]', () {
+    //   expectYamlStreamLoads({
+    //     "Block style": "String: just a theory.",
+    //     "Flow style": "String: just a theory."
+    //   },
+    //     '''
+    //     Block style: !!str |-
+    //       String: just a theory.
+
+    //     Flow style: !!str "String: just a theory."''');
+    // });
+  });
+
+  group('10.2: JSON Schema', () {
+    // test('[Example 10.4]', () {
+    //   var doc = yamlMap({"key with null value": null});
+    //   doc[null] = "value for null key";
+    //   expectYamlStreamLoads(doc,
+    //     """
+    //     !!null null: value for null key
+    //     key with null value: !!null null""");
+    // });
+
+    // test('[Example 10.5]', () {
+    //   expectYamlStreamLoads({
+    //     "YAML is a superset of JSON": true,
+    //     "Pluto is a planet": false
+    //   },
+    //     """
+    //     YAML is a superset of JSON: !!bool true
+    //     Pluto is a planet: !!bool false""");
+    // });
+
+    // test('[Example 10.6]', () {
+    //   expectYamlStreamLoads({
+    //     "negative": -12,
+    //     "zero": 0,
+    //     "positive": 34
+    //   },
+    //     """
+    //     negative: !!int -12
+    //     zero: !!int 0
+    //     positive: !!int 34""");
+    // });
+
+    // test('[Example 10.7]', () {
+    //   expectYamlStreamLoads({
+    //     "negative": -1,
+    //     "zero": 0,
+    //     "positive": 23000,
+    //     "infinity": infinity,
+    //     "not a number": nan
+    //   },
+    //     """
+    //     negative: !!float -1
+    //     zero: !!float 0
+    //     positive: !!float 2.3e4
+    //     infinity: !!float .inf
+    //     not a number: !!float .nan""");
+    // });
+
+    // test('[Example 10.8]', () {
+    //   expectYamlStreamLoads({
+    //     "A null": null,
+    //     "Booleans": [true, false],
+    //     "Integers": [0, -0, 3, -19],
+    //     "Floats": [0, 0, 12000, -200000],
+    //     // Despite being invalid in the JSON schema, these values are valid in
+    //     // the core schema which this implementation supports.
+    //     "Invalid": [ true, null, 7, 0x3A, 12.3]
+    //   },
+    //     """
+    //     A null: null
+    //     Booleans: [ true, false ]
+    //     Integers: [ 0, -0, 3, -19 ]
+    //     Floats: [ 0., -0.0, 12e03, -2E+05 ]
+    //     Invalid: [ True, Null, 0o7, 0x3A, +12.3 ]""");
+    // });
+  });
+
+  group('10.3: Core Schema', () {
+    test('[Example 10.9]', () {
+      expectYamlLoads({
+        "A null": null,
+        "Also a null": null,
+        "Not a null": "",
+        "Booleans": [true, true, false, false],
+        "Integers": [0, 7, 0x3A, -19],
+        "Floats": [0, 0, 0.5, 12000, -200000],
+        "Also floats": [infinity, -infinity, infinity, nan]
+      },
+        '''
+        A null: null
+        Also a null: # Empty
+        Not a null: ""
+        Booleans: [ true, True, false, FALSE ]
+        Integers: [ 0, 0o7, 0x3A, -19 ]
+        Floats: [ 0., -0.0, .5, +12e03, -2E+05 ]
+        Also floats: [ .inf, -.Inf, +.INF, .NAN ]''');
+    });
+  });
+}