Merge package:yaml into the tools monorepo
diff --git a/pkgs/yaml/.github/dependabot.yml b/pkgs/yaml/.github/dependabot.yml
new file mode 100644
index 0000000..cde02ad
--- /dev/null
+++ b/pkgs/yaml/.github/dependabot.yml
@@ -0,0 +1,15 @@
+# Dependabot configuration file.
+# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
+version: 2
+
+updates:
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: monthly
+    labels:
+      - autosubmit
+    groups:
+      github-actions:
+        patterns:
+          - "*"
diff --git a/pkgs/yaml/.github/workflows/no-response.yml b/pkgs/yaml/.github/workflows/no-response.yml
new file mode 100644
index 0000000..ab1ac49
--- /dev/null
+++ b/pkgs/yaml/.github/workflows/no-response.yml
@@ -0,0 +1,37 @@
+# A workflow to close issues where the author hasn't responded to a request for
+# more information; see https://github.com/actions/stale.
+
+name: No Response
+
+# Run as a daily cron.
+on:
+  schedule:
+    # Every day at 8am
+    - cron: '0 8 * * *'
+
+# All permissions not specified are set to 'none'.
+permissions:
+  issues: write
+  pull-requests: write
+
+jobs:
+  no-response:
+    runs-on: ubuntu-latest
+    if: ${{ github.repository_owner == 'dart-lang' }}
+    steps:
+      - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e
+        with:
+          # Don't automatically mark inactive issues+PRs as stale.
+          days-before-stale: -1
+          # Close needs-info issues and PRs after 14 days of inactivity.
+          days-before-close: 14
+          stale-issue-label: "needs-info"
+          close-issue-message: >
+            Without additional information we're not able to resolve this issue.
+            Feel free to add more info or respond to any questions above and we
+            can reopen the case. Thanks for your contribution!
+          stale-pr-label: "needs-info"
+          close-pr-message: >
+            Without additional information we're not able to resolve this PR.
+            Feel free to add more info or respond to any questions above.
+            Thanks for your contribution!
diff --git a/pkgs/yaml/.github/workflows/publish.yaml b/pkgs/yaml/.github/workflows/publish.yaml
new file mode 100644
index 0000000..27157a0
--- /dev/null
+++ b/pkgs/yaml/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+  pull_request:
+    branches: [ master ]
+  push:
+    tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]
+
+jobs:
+  publish:
+    if: ${{ github.repository_owner == 'dart-lang' }}
+    uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
+    permissions:
+      id-token: write # Required for authentication using OIDC
+      pull-requests: write # Required for writing the pull request note
diff --git a/pkgs/yaml/.github/workflows/test-package.yml b/pkgs/yaml/.github/workflows/test-package.yml
new file mode 100644
index 0000000..7bc6a0b
--- /dev/null
+++ b/pkgs/yaml/.github/workflows/test-package.yml
@@ -0,0 +1,64 @@
+name: Dart CI
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release channel: dev
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # Add macos-latest and/or windows-latest if relevant for this package.
+        os: [ubuntu-latest]
+        sdk: [3.4, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run VM tests
+        run: dart test --platform vm
+        if: always() && steps.install.outcome == 'success'
+      - name: Run Chrome tests
+        run: dart test --platform chrome
+        if: always() && steps.install.outcome == 'success'
diff --git a/pkgs/yaml/.gitignore b/pkgs/yaml/.gitignore
new file mode 100644
index 0000000..ab3cb76
--- /dev/null
+++ b/pkgs/yaml/.gitignore
@@ -0,0 +1,16 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/yaml/CHANGELOG.md b/pkgs/yaml/CHANGELOG.md
new file mode 100644
index 0000000..cd800a8
--- /dev/null
+++ b/pkgs/yaml/CHANGELOG.md
@@ -0,0 +1,198 @@
+## 3.1.3-wip
+
+* Require Dart 3.4
+* Fix UTF-16 surrogate pair handling in plain scaler.
+
+## 3.1.2
+
+* Require Dart 2.19
+* Added `topics` in `pubspec.yaml`.
+
+## 3.1.1
+
+* Switch to using package:lints.
+* Populate the pubspec `repository` field.
+
+## 3.1.0
+
+* `loadYaml` and related functions now accept a `recover` flag instructing the parser
+  to attempt to recover from parse errors and may return invalid or synthetic nodes.
+  When recovering, an `ErrorListener` can also be supplied to listen for errors that
+  are recovered from.
+* Drop dependency on `package:charcode`.
+
+## 3.0.0
+
+* Stable null safety release.
+
+## 3.0.0-nullsafety.0
+
+* Updated to support 2.12.0 and null safety.
+* Allow `YamlNode`s to be wrapped with an optional `style` parameter.
+* **BREAKING** The `sourceUrl` named argument is statically typed as `Uri`
+  instead of allowing `String` or `Uri`.
+
+## 2.2.1
+
+* Update min Dart SDK to `2.4.0`.
+* Fixed span for null nodes in block lists.
+
+## 2.2.0
+
+* POSSIBLY BREAKING CHANGE: Make `YamlMap` preserve parsed key order.
+  This is breaking because some programs may rely on the
+  `HashMap` sort order.
+
+## 2.1.16
+
+* Fixed deprecated API usage in README.
+* Fixed lints that affect package score.
+
+## 2.1.15
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 2.1.14
+
+* Remove use of deprecated features.
+* Updated SDK version to 2.0.0-dev.17.0
+
+## 2.1.13
+
+* Stop using comment-based generic syntax.
+
+## 2.1.12
+
+* Properly refuse mappings with duplicate keys.
+
+## 2.1.11
+
+* Fix an infinite loop when parsing some invalid documents.
+
+## 2.1.10
+
+* Support `string_scanner` 1.0.0.
+
+## 2.1.9
+
+* Fix all strong-mode warnings.
+
+## 2.1.8
+
+* Remove the dependency on `path`, since we don't actually import it.
+
+## 2.1.7
+
+* Fix more strong mode warnings.
+
+## 2.1.6
+
+* Fix two analysis issues with DDC's strong mode.
+
+## 2.1.5
+
+* Fix a bug with 2.1.4 where source span information was being discarded for
+  scalar values.
+
+## 2.1.4
+
+* Substantially improve performance.
+
+## 2.1.3
+
+* Add a hint that a colon might be missing when a mapping value is found in the
+  wrong context.
+
+## 2.1.2
+
+* Fix a crashing bug when parsing block scalars.
+
+## 2.1.1
+
+* Properly scope `SourceSpan`s for scalar values surrounded by whitespace.
+
+## 2.1.0
+
+* Rewrite the parser for a 10x speed improvement.
+
+* Support anchors and aliases (`&foo` and `*foo`).
+
+* Support explicit tags (e.g. `!!str`). Note that user-defined tags are still
+  not fully supported.
+
+* `%YAML` and `%TAG` directives are now parsed, although again user-defined tags
+  are not fully supported.
+
+* `YamlScalar`, `YamlList`, and `YamlMap` now expose the styles in which they
+  were written (for example plain vs folded, block vs flow).
+
+* A `yamlWarningCallback` field is exposed. This field can be used to customize
+  how YAML warnings are displayed.
+
+## 2.0.1+1
+
+* Fix an import in a test.
+
+* Widen the version constraint on the `collection` package.
+
+## 2.0.1
+
+* Fix a few lingering references to the old `Span` class in documentation and
+  tests.
+
+## 2.0.0
+
+* Switch from `source_maps`' `Span` class to `source_span`'s `SourceSpan` class.
+
+* For consistency with `source_span` and `string_scanner`, all `sourceName`
+  parameters have been renamed to `sourceUrl`. They now accept Urls as well as
+  Strings.
+
+## 1.1.1
+
+* Fix broken type arguments that caused breakage on dart2js.
+
+* Fix an analyzer warning in `yaml_node_wrapper.dart`.
+
+## 1.1.0
+
+* Add new publicly-accessible constructors for `YamlNode` subclasses. These
+  constructors make it possible to use the same API to access non-YAML data as
+  YAML data.
+
+* Make `YamlException` inherit from source_map's `SpanFormatException`. This
+  improves the error formatting and allows callers access to source range
+  information.
+
+## 1.0.0+1
+
+* Fix a variable name typo.
+
+## 1.0.0
+
+* **Backwards incompatibility**: The data structures returned by `loadYaml` and
+  `loadYamlStream` are now immutable.
+
+* **Backwards incompatibility**: The interface of the `YamlMap` class has
+  changed substantially in numerous ways. External users may no longer construct
+  their own instances.
+
+* Maps and lists returned by `loadYaml` and `loadYamlStream` now contain
+  information about their source locations.
+
+* A new `loadYamlNode` function returns the source location of top-level scalars
+  as well.
+
+## 0.10.0
+
+* Improve error messages when a file fails to parse.
+
+## 0.9.0+2
+
+* Ensure that maps are order-independent when used as map keys.
+
+## 0.9.0+1
+
+* The `YamlMap` class is deprecated. In a future version, maps returned by
+  `loadYaml` and `loadYamlStream` will be Dart `HashMap`s with a custom equality
+  operation.
diff --git a/pkgs/yaml/LICENSE b/pkgs/yaml/LICENSE
new file mode 100644
index 0000000..e7589cb
--- /dev/null
+++ b/pkgs/yaml/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2014, the Dart project authors.
+Copyright (c) 2006, Kirill Simonov.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pkgs/yaml/README.md b/pkgs/yaml/README.md
new file mode 100644
index 0000000..6f5be28
--- /dev/null
+++ b/pkgs/yaml/README.md
@@ -0,0 +1,32 @@
+[![Build Status](https://github.com/dart-lang/yaml/workflows/Dart%20CI/badge.svg)](https://github.com/dart-lang/yaml/actions?query=workflow%3A"Dart+CI"+branch%3Amaster)
+[![Pub Package](https://img.shields.io/pub/v/yaml.svg)](https://pub.dev/packages/yaml)
+[![package publisher](https://img.shields.io/pub/publisher/yaml.svg)](https://pub.dev/packages/yaml/publisher)
+
+A parser for [YAML](https://yaml.org/).
+
+## Usage
+
+Use `loadYaml` to load a single document, or `loadYamlStream` to load a
+stream of documents. For example:
+
+```dart
+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
+`json.encode` from `dart:convert` instead:
+
+```dart
+import 'dart:convert';
+import 'package:yaml/yaml.dart';
+
+main() {
+  var doc = loadYaml("YAML: YAML Ain't Markup Language");
+  print(json.encode(doc));
+}
+```
diff --git a/pkgs/yaml/analysis_options.yaml b/pkgs/yaml/analysis_options.yaml
new file mode 100644
index 0000000..46e45f0
--- /dev/null
+++ b/pkgs/yaml/analysis_options.yaml
@@ -0,0 +1,18 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+
+linter:
+  rules:
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
+    - avoid_unused_constructor_parameters
+    - cancel_subscriptions
+    - join_return_with_assignment
+    - missing_whitespace_between_adjacent_strings
+    - no_runtimeType_toString
+    - prefer_const_declarations
+    - prefer_expression_function_bodies
+    - use_string_buffers
diff --git a/pkgs/yaml/benchmark/benchmark.dart b/pkgs/yaml/benchmark/benchmark.dart
new file mode 100644
index 0000000..afc3c97
--- /dev/null
+++ b/pkgs/yaml/benchmark/benchmark.dart
@@ -0,0 +1,65 @@
+// Copyright (c) 2015, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:yaml/yaml.dart';
+
+const numTrials = 100;
+const runsPerTrial = 1000;
+
+final source = _loadFile('input.yaml');
+final expected = _loadFile('output.json');
+
+void main(List<String> args) {
+  var best = double.infinity;
+
+  // Run the benchmark several times. This ensures the VM is warmed up and lets
+  // us see how much variance there is.
+  for (var i = 0; i <= numTrials; i++) {
+    var start = DateTime.now();
+
+    // For a single benchmark, convert the source multiple times.
+    Object? result;
+    for (var j = 0; j < runsPerTrial; j++) {
+      result = loadYaml(source);
+    }
+
+    var elapsed =
+        DateTime.now().difference(start).inMilliseconds / runsPerTrial;
+
+    // Keep track of the best run so far.
+    if (elapsed >= best) continue;
+    best = elapsed;
+
+    // Sanity check to make sure the output is what we expect and to make sure
+    // the VM doesn't optimize "dead" code away.
+    if (jsonEncode(result) != expected) {
+      print('Incorrect output:\n${jsonEncode(result)}');
+      exit(1);
+    }
+
+    // Don't print the first run. It's always terrible since the VM hasn't
+    // warmed up yet.
+    if (i == 0) continue;
+    _printResult("Run ${'#$i'.padLeft(3, '')}", elapsed);
+  }
+
+  _printResult('Best   ', best);
+}
+
+String _loadFile(String name) {
+  var path = p.join(p.dirname(p.fromUri(Platform.script)), name);
+  return File(path).readAsStringSync();
+}
+
+void _printResult(String label, double time) {
+  print('$label: ${time.toStringAsFixed(3).padLeft(4, '0')}ms '
+      "${'=' * ((time * 100).toInt())}");
+}
diff --git a/pkgs/yaml/benchmark/input.yaml b/pkgs/yaml/benchmark/input.yaml
new file mode 100644
index 0000000..89bf9dc
--- /dev/null
+++ b/pkgs/yaml/benchmark/input.yaml
@@ -0,0 +1,48 @@
+verb: RecommendCafes
+recipe:
+  - verb: List
+    outputs: ["Cafe[]"]
+  - verb: Fetch
+    inputs: ["Cafe[]"]
+    outputs: ["CafeWithMenu[]"]
+  - verb: Flatten
+    inputs: ["CafeWithMenu[]"]
+    outputs: ["DishOffering[]"]
+  - verb: Score
+    inputs: ["DishOffering[]"]
+    outputs: ["DishOffering[]/Scored"]
+  - verb: Display
+    inputs: ["DishOffering[]/Scored"]
+tags:
+  booleans: [ true, false ]
+  dates:
+  - canonical: 2001-12-15T02:59:43.1Z
+  - iso8601: 2001-12-14t21:59:43.10-05:00
+  - spaced: 2001-12-14 21:59:43.10 -5
+  - date: 2002-12-14
+  numbers:
+  - int: 12345
+  - negative: -345
+  - floating-point: 345.678
+  - hexidecimal: 0x123abc
+  - exponential: 12.3015e+02
+  - octal: 0o14
+  strings:
+  - 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: '|\-*-/|'
+  - plain:
+      This unquoted scalar
+      spans many lines.
+
+  - quoted: "So does this
+      quoted scalar.\n"
+  - accomplishment: >
+      Mark set a major league
+      home run record in 1998.
+  - stats: |
+      65 Home Runs
+      0.278 Batting Average
diff --git a/pkgs/yaml/benchmark/output.json b/pkgs/yaml/benchmark/output.json
new file mode 100644
index 0000000..9e6cb84
--- /dev/null
+++ b/pkgs/yaml/benchmark/output.json
@@ -0,0 +1 @@
+{"verb":"RecommendCafes","recipe":[{"verb":"List","outputs":["Cafe[]"]},{"verb":"Fetch","inputs":["Cafe[]"],"outputs":["CafeWithMenu[]"]},{"verb":"Flatten","inputs":["CafeWithMenu[]"],"outputs":["DishOffering[]"]},{"verb":"Score","inputs":["DishOffering[]"],"outputs":["DishOffering[]/Scored"]},{"verb":"Display","inputs":["DishOffering[]/Scored"]}],"tags":{"booleans":[true,false],"dates":[{"canonical":"2001-12-15T02:59:43.1Z"},{"iso8601":"2001-12-14t21:59:43.10-05:00"},{"spaced":"2001-12-14 21:59:43.10 -5"},{"date":"2002-12-14"}],"numbers":[{"int":12345},{"negative":-345},{"floating-point":345.678},{"hexidecimal":1194684},{"exponential":1230.15},{"octal":12}],"strings":[{"unicode":"Sosa did fine.☺"},{"control":"\b1998\t1999\t2000\n"},{"hex esc":"\r\n is \r\n"},{"single":"\"Howdy!\" he cried."},{"quoted":" # Not a 'comment'."},{"tie-fighter":"|\\-*-/|"},{"plain":"This unquoted scalar spans many lines."},{"quoted":"So does this quoted scalar.\n"},{"accomplishment":"Mark set a major league home run record in 1998.\n"},{"stats":"65 Home Runs\n0.278 Batting Average\n"}]}}
\ No newline at end of file
diff --git a/pkgs/yaml/example/example.dart b/pkgs/yaml/example/example.dart
new file mode 100644
index 0000000..bb283a3
--- /dev/null
+++ b/pkgs/yaml/example/example.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2020, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:yaml/yaml.dart';
+
+void main() {
+  var doc = loadYaml("YAML: YAML Ain't Markup Language") as Map;
+  print(doc['YAML']);
+}
diff --git a/pkgs/yaml/lib/src/charcodes.dart b/pkgs/yaml/lib/src/charcodes.dart
new file mode 100644
index 0000000..602d597
--- /dev/null
+++ b/pkgs/yaml/lib/src/charcodes.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2021, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+/// Character `+`.
+const int $plus = 0x2b;
+
+/// Character `-`.
+const int $minus = 0x2d;
+
+/// Character `.`.
+const int $dot = 0x2e;
+
+/// Character `0`.
+const int $0 = 0x30;
+
+/// Character `9`.
+const int $9 = 0x39;
+
+/// Character `F`.
+const int $F = 0x46;
+
+/// Character `N`.
+const int $N = 0x4e;
+
+/// Character `T`.
+const int $T = 0x54;
+
+/// Character `f`.
+const int $f = 0x66;
+
+/// Character `n`.
+const int $n = 0x6e;
+
+/// Character `o`.
+const int $o = 0x6f;
+
+/// Character `t`.
+const int $t = 0x74;
+
+/// Character `x`.
+const int $x = 0x78;
+
+/// Character `~`.
+const int $tilde = 0x7e;
diff --git a/pkgs/yaml/lib/src/equality.dart b/pkgs/yaml/lib/src/equality.dart
new file mode 100644
index 0000000..c833dc6
--- /dev/null
+++ b/pkgs/yaml/lib/src/equality.dart
@@ -0,0 +1,128 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+
+import 'yaml_node.dart';
+
+/// Returns a [Map] that compares its keys based on [deepEquals].
+Map<K, V> deepEqualsMap<K, V>() =>
+    LinkedHashMap(equals: deepEquals, hashCode: deepHashCode);
+
+/// Returns whether two objects are structurally equivalent.
+///
+/// This considers `NaN` values to be equivalent, handles self-referential
+/// structures, and considers [YamlScalar]s to be equal to their values.
+bool deepEquals(Object? obj1, Object? obj2) => _DeepEquals().equals(obj1, obj2);
+
+/// A class that provides access to the list of parent objects used for loop
+/// detection.
+class _DeepEquals {
+  final _parents1 = <Object?>[];
+  final _parents2 = <Object?>[];
+
+  /// Returns whether [obj1] and [obj2] are structurally equivalent.
+  bool equals(Object? obj1, Object? obj2) {
+    if (obj1 is YamlScalar) obj1 = obj1.value;
+    if (obj2 is YamlScalar) obj2 = obj2.value;
+
+    // _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);
+      } else if (obj1 is Map && obj2 is Map) {
+        return _mapEquals(obj1, obj2);
+      } else if (obj1 is num && obj2 is num) {
+        return _numEquals(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) {
+    if (list1.length != list2.length) return false;
+
+    for (var i = 0; i < list1.length; i++) {
+      if (!equals(list1[i], list2[i])) return false;
+    }
+
+    return true;
+  }
+
+  /// Returns whether [map1] and [map2] are structurally equal.
+  bool _mapEquals(Map map1, Map map2) {
+    if (map1.length != map2.length) return false;
+
+    for (var key in map1.keys) {
+      if (!map2.containsKey(key)) return false;
+      if (!equals(map1[key], map2[key])) return false;
+    }
+
+    return true;
+  }
+
+  /// Returns whether two numbers are equivalent.
+  ///
+  /// This differs from `n1 == n2` in that it considers `NaN` to be equal to
+  /// itself.
+  bool _numEquals(num n1, num n2) {
+    if (n1.isNaN && n2.isNaN) return true;
+    return n1 == n2;
+  }
+}
+
+/// Returns a hash code for [obj] such that structurally equivalent objects
+/// will have the same hash code.
+///
+/// This supports deep equality for maps and lists, including those with
+/// self-referential structures, and returns the same hash code for
+/// [YamlScalar]s and their values.
+int deepHashCode(Object? obj) {
+  var parents = <Object?>[];
+
+  int deepHashCodeInner(Object? value) {
+    if (parents.any((parent) => identical(parent, value))) return -1;
+
+    parents.add(value);
+    try {
+      if (value is Map) {
+        var equality = const UnorderedIterableEquality<Object?>();
+        return equality.hash(value.keys.map(deepHashCodeInner)) ^
+            equality.hash(value.values.map(deepHashCodeInner));
+      } else if (value is Iterable) {
+        return const IterableEquality<Object?>().hash(value.map(deepHashCode));
+      } else if (value is YamlScalar) {
+        return (value.value as Object?).hashCode;
+      } else {
+        return value.hashCode;
+      }
+    } finally {
+      parents.removeLast();
+    }
+  }
+
+  return deepHashCodeInner(obj);
+}
diff --git a/pkgs/yaml/lib/src/error_listener.dart b/pkgs/yaml/lib/src/error_listener.dart
new file mode 100644
index 0000000..0498d68
--- /dev/null
+++ b/pkgs/yaml/lib/src/error_listener.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2021, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'yaml_exception.dart';
+
+/// A listener that is notified of [YamlException]s during scanning/parsing.
+abstract class ErrorListener {
+  /// This method is invoked when an [error] has been found in the YAML.
+  void onError(YamlException error);
+}
+
+/// An [ErrorListener] that collects all errors into [errors].
+class ErrorCollector extends ErrorListener {
+  final List<YamlException> errors = [];
+
+  @override
+  void onError(YamlException error) => errors.add(error);
+}
diff --git a/pkgs/yaml/lib/src/event.dart b/pkgs/yaml/lib/src/event.dart
new file mode 100644
index 0000000..1476311
--- /dev/null
+++ b/pkgs/yaml/lib/src/event.dart
@@ -0,0 +1,171 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'parser.dart';
+import 'style.dart';
+import 'yaml_document.dart';
+
+/// An event emitted by a [Parser].
+class Event {
+  final EventType type;
+  final FileSpan span;
+
+  Event(this.type, this.span);
+
+  @override
+  String toString() => type.toString();
+}
+
+/// An event indicating the beginning of a YAML document.
+class DocumentStartEvent implements Event {
+  @override
+  EventType get type => EventType.documentStart;
+  @override
+  final FileSpan span;
+
+  /// The document's `%YAML` directive, or `null` if there was none.
+  final VersionDirective? versionDirective;
+
+  /// The document's `%TAG` directives, if any.
+  final List<TagDirective> tagDirectives;
+
+  /// Whether the document started implicitly (that is, without an explicit
+  /// `===` sequence).
+  final bool isImplicit;
+
+  DocumentStartEvent(this.span,
+      {this.versionDirective,
+      List<TagDirective>? tagDirectives,
+      this.isImplicit = true})
+      : tagDirectives = tagDirectives ?? [];
+
+  @override
+  String toString() => 'DOCUMENT_START';
+}
+
+/// An event indicating the end of a YAML document.
+class DocumentEndEvent implements Event {
+  @override
+  EventType get type => EventType.documentEnd;
+  @override
+  final FileSpan span;
+
+  /// Whether the document ended implicitly (that is, without an explicit
+  /// `...` sequence).
+  final bool isImplicit;
+
+  DocumentEndEvent(this.span, {this.isImplicit = true});
+
+  @override
+  String toString() => 'DOCUMENT_END';
+}
+
+/// An event indicating that an alias was referenced.
+class AliasEvent implements Event {
+  @override
+  EventType get type => EventType.alias;
+  @override
+  final FileSpan span;
+
+  /// The alias name.
+  final String name;
+
+  AliasEvent(this.span, this.name);
+
+  @override
+  String toString() => 'ALIAS $name';
+}
+
+/// An event that can have associated anchor and tag properties.
+abstract class _ValueEvent implements Event {
+  /// The name of the value's anchor, or `null` if it wasn't anchored.
+  String? get anchor;
+
+  /// The text of the value's tag, or `null` if it wasn't tagged.
+  String? get tag;
+
+  @override
+  String toString() {
+    var buffer = StringBuffer('$type');
+    if (anchor != null) buffer.write(' &$anchor');
+    if (tag != null) buffer.write(' $tag');
+    return buffer.toString();
+  }
+}
+
+/// An event indicating a single scalar value.
+class ScalarEvent extends _ValueEvent {
+  @override
+  EventType get type => EventType.scalar;
+  @override
+  final FileSpan span;
+  @override
+  final String? anchor;
+  @override
+  final String? tag;
+
+  /// The contents of the scalar.
+  final String value;
+
+  /// The style of the scalar in the original source.
+  final ScalarStyle style;
+
+  ScalarEvent(this.span, this.value, this.style, {this.anchor, this.tag});
+
+  @override
+  String toString() => '${super.toString()} "$value"';
+}
+
+/// An event indicating the beginning of a sequence.
+class SequenceStartEvent extends _ValueEvent {
+  @override
+  EventType get type => EventType.sequenceStart;
+  @override
+  final FileSpan span;
+  @override
+  final String? anchor;
+  @override
+  final String? tag;
+
+  /// The style of the collection in the original source.
+  final CollectionStyle style;
+
+  SequenceStartEvent(this.span, this.style, {this.anchor, this.tag});
+}
+
+/// An event indicating the beginning of a mapping.
+class MappingStartEvent extends _ValueEvent {
+  @override
+  EventType get type => EventType.mappingStart;
+  @override
+  final FileSpan span;
+  @override
+  final String? anchor;
+  @override
+  final String? tag;
+
+  /// The style of the collection in the original source.
+  final CollectionStyle style;
+
+  MappingStartEvent(this.span, this.style, {this.anchor, this.tag});
+}
+
+/// The types of [Event] objects.
+enum EventType {
+  streamStart,
+  streamEnd,
+  documentStart,
+  documentEnd,
+  alias,
+  scalar,
+  sequenceStart,
+  sequenceEnd,
+  mappingStart,
+  mappingEnd
+}
diff --git a/pkgs/yaml/lib/src/loader.dart b/pkgs/yaml/lib/src/loader.dart
new file mode 100644
index 0000000..7cdf45a
--- /dev/null
+++ b/pkgs/yaml/lib/src/loader.dart
@@ -0,0 +1,343 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'charcodes.dart';
+import 'equality.dart';
+import 'error_listener.dart';
+import 'event.dart';
+import 'parser.dart';
+import 'yaml_document.dart';
+import 'yaml_exception.dart';
+import 'yaml_node.dart';
+
+/// A loader that reads [Event]s emitted by a [Parser] and emits
+/// [YamlDocument]s.
+///
+/// This is based on the libyaml loader, available at
+/// https://github.com/yaml/libyaml/blob/master/src/loader.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Loader {
+  /// The underlying [Parser] that generates [Event]s.
+  final Parser _parser;
+
+  /// Aliases by the alias name.
+  final _aliases = <String, YamlNode>{};
+
+  /// The span of the entire stream emitted so far.
+  FileSpan get span => _span;
+  FileSpan _span;
+
+  /// Creates a loader that loads [source].
+  factory Loader(String source,
+      {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) {
+    var parser = Parser(source,
+        sourceUrl: sourceUrl, recover: recover, errorListener: errorListener);
+    var event = parser.parse();
+    assert(event.type == EventType.streamStart);
+    return Loader._(parser, event.span);
+  }
+
+  Loader._(this._parser, this._span);
+
+  /// Loads the next document from the stream.
+  ///
+  /// If there are no more documents, returns `null`.
+  YamlDocument? load() {
+    if (_parser.isDone) return null;
+
+    var event = _parser.parse();
+    if (event.type == EventType.streamEnd) {
+      _span = _span.expand(event.span);
+      return null;
+    }
+
+    var document = _loadDocument(event as DocumentStartEvent);
+    _span = _span.expand(document.span as FileSpan);
+    _aliases.clear();
+    return document;
+  }
+
+  /// Composes a document object.
+  YamlDocument _loadDocument(DocumentStartEvent firstEvent) {
+    var contents = _loadNode(_parser.parse());
+
+    var lastEvent = _parser.parse() as DocumentEndEvent;
+    assert(lastEvent.type == EventType.documentEnd);
+
+    return YamlDocument.internal(
+        contents,
+        firstEvent.span.expand(lastEvent.span),
+        firstEvent.versionDirective,
+        firstEvent.tagDirectives,
+        startImplicit: firstEvent.isImplicit,
+        endImplicit: lastEvent.isImplicit);
+  }
+
+  /// Composes a node.
+  YamlNode _loadNode(Event firstEvent) => switch (firstEvent.type) {
+        EventType.alias => _loadAlias(firstEvent as AliasEvent),
+        EventType.scalar => _loadScalar(firstEvent as ScalarEvent),
+        EventType.sequenceStart =>
+          _loadSequence(firstEvent as SequenceStartEvent),
+        EventType.mappingStart => _loadMapping(firstEvent as MappingStartEvent),
+        _ => throw StateError('Unreachable')
+      };
+
+  /// Registers an anchor.
+  void _registerAnchor(String? anchor, YamlNode node) {
+    if (anchor == null) return;
+
+    // libyaml throws an error for duplicate anchors, but example 7.1 makes it
+    // clear that they should be overridden:
+    // http://yaml.org/spec/1.2/spec.html#id2786448.
+
+    _aliases[anchor] = node;
+  }
+
+  /// Composes a node corresponding to an alias.
+  YamlNode _loadAlias(AliasEvent event) {
+    var alias = _aliases[event.name];
+    if (alias != null) return alias;
+
+    throw YamlException('Undefined alias.', event.span);
+  }
+
+  /// Composes a scalar node.
+  YamlNode _loadScalar(ScalarEvent scalar) {
+    YamlNode node;
+    if (scalar.tag == '!') {
+      node = YamlScalar.internal(scalar.value, scalar);
+    } else if (scalar.tag != null) {
+      node = _parseByTag(scalar);
+    } else {
+      node = _parseScalar(scalar);
+    }
+
+    _registerAnchor(scalar.anchor, node);
+    return node;
+  }
+
+  /// Composes a sequence node.
+  YamlNode _loadSequence(SequenceStartEvent firstEvent) {
+    if (firstEvent.tag != '!' &&
+        firstEvent.tag != null &&
+        firstEvent.tag != 'tag:yaml.org,2002:seq') {
+      throw YamlException('Invalid tag for sequence.', firstEvent.span);
+    }
+
+    var children = <YamlNode>[];
+    var node = YamlList.internal(children, firstEvent.span, firstEvent.style);
+    _registerAnchor(firstEvent.anchor, node);
+
+    var event = _parser.parse();
+    while (event.type != EventType.sequenceEnd) {
+      children.add(_loadNode(event));
+      event = _parser.parse();
+    }
+
+    setSpan(node, firstEvent.span.expand(event.span));
+    return node;
+  }
+
+  /// Composes a mapping node.
+  YamlNode _loadMapping(MappingStartEvent firstEvent) {
+    if (firstEvent.tag != '!' &&
+        firstEvent.tag != null &&
+        firstEvent.tag != 'tag:yaml.org,2002:map') {
+      throw YamlException('Invalid tag for mapping.', firstEvent.span);
+    }
+
+    var children = deepEqualsMap<dynamic, YamlNode>();
+    var node = YamlMap.internal(children, firstEvent.span, firstEvent.style);
+    _registerAnchor(firstEvent.anchor, node);
+
+    var event = _parser.parse();
+    while (event.type != EventType.mappingEnd) {
+      var key = _loadNode(event);
+      var value = _loadNode(_parser.parse());
+      if (children.containsKey(key)) {
+        throw YamlException('Duplicate mapping key.', key.span);
+      }
+
+      children[key] = value;
+      event = _parser.parse();
+    }
+
+    setSpan(node, firstEvent.span.expand(event.span));
+    return node;
+  }
+
+  /// Parses a scalar according to its tag name.
+  YamlScalar _parseByTag(ScalarEvent scalar) {
+    switch (scalar.tag) {
+      case 'tag:yaml.org,2002:null':
+        var result = _parseNull(scalar);
+        if (result != null) return result;
+        throw YamlException('Invalid null scalar.', scalar.span);
+      case 'tag:yaml.org,2002:bool':
+        var result = _parseBool(scalar);
+        if (result != null) return result;
+        throw YamlException('Invalid bool scalar.', scalar.span);
+      case 'tag:yaml.org,2002:int':
+        var result = _parseNumber(scalar, allowFloat: false);
+        if (result != null) return result;
+        throw YamlException('Invalid int scalar.', scalar.span);
+      case 'tag:yaml.org,2002:float':
+        var result = _parseNumber(scalar, allowInt: false);
+        if (result != null) return result;
+        throw YamlException('Invalid float scalar.', scalar.span);
+      case 'tag:yaml.org,2002:str':
+        return YamlScalar.internal(scalar.value, scalar);
+      default:
+        throw YamlException('Undefined tag: ${scalar.tag}.', scalar.span);
+    }
+  }
+
+  /// Parses [scalar], which may be one of several types.
+  YamlScalar _parseScalar(ScalarEvent scalar) =>
+      _tryParseScalar(scalar) ?? YamlScalar.internal(scalar.value, scalar);
+
+  /// Tries to parse [scalar].
+  ///
+  /// If parsing fails, this returns `null`, indicating that the scalar should
+  /// be parsed as a string.
+  YamlScalar? _tryParseScalar(ScalarEvent scalar) {
+    // Quickly check for the empty string, which means null.
+    var length = scalar.value.length;
+    if (length == 0) return YamlScalar.internal(null, scalar);
+
+    // Dispatch on the first character.
+    var firstChar = scalar.value.codeUnitAt(0);
+    return switch (firstChar) {
+      $dot || $plus || $minus => _parseNumber(scalar),
+      $n || $N => length == 4 ? _parseNull(scalar) : null,
+      $t || $T => length == 4 ? _parseBool(scalar) : null,
+      $f || $F => length == 5 ? _parseBool(scalar) : null,
+      $tilde => length == 1 ? YamlScalar.internal(null, scalar) : null,
+      _ => (firstChar >= $0 && firstChar <= $9) ? _parseNumber(scalar) : null
+    };
+  }
+
+  /// Parse a null scalar.
+  ///
+  /// Returns a Dart `null` if parsing fails.
+  YamlScalar? _parseNull(ScalarEvent scalar) => switch (scalar.value) {
+        '' ||
+        'null' ||
+        'Null' ||
+        'NULL' ||
+        '~' =>
+          YamlScalar.internal(null, scalar),
+        _ => null
+      };
+
+  /// Parse a boolean scalar.
+  ///
+  /// Returns `null` if parsing fails.
+  YamlScalar? _parseBool(ScalarEvent scalar) => switch (scalar.value) {
+        'true' || 'True' || 'TRUE' => YamlScalar.internal(true, scalar),
+        'false' || 'False' || 'FALSE' => YamlScalar.internal(false, scalar),
+        _ => null
+      };
+
+  /// Parses a numeric scalar.
+  ///
+  /// Returns `null` if parsing fails.
+  YamlScalar? _parseNumber(ScalarEvent scalar,
+      {bool allowInt = true, bool allowFloat = true}) {
+    var value = _parseNumberValue(scalar.value,
+        allowInt: allowInt, allowFloat: allowFloat);
+    return value == null ? null : YamlScalar.internal(value, scalar);
+  }
+
+  /// Parses the value of a number.
+  ///
+  /// Returns the number if it's parsed successfully, or `null` if it's not.
+  num? _parseNumberValue(String contents,
+      {bool allowInt = true, bool allowFloat = true}) {
+    assert(allowInt || allowFloat);
+
+    var firstChar = contents.codeUnitAt(0);
+    var length = contents.length;
+
+    // Quick check for single digit integers.
+    if (allowInt && length == 1) {
+      var value = firstChar - $0;
+      return value >= 0 && value <= 9 ? value : null;
+    }
+
+    var secondChar = contents.codeUnitAt(1);
+
+    // Hexadecimal or octal integers.
+    if (allowInt && firstChar == $0) {
+      // int.tryParse supports 0x natively.
+      if (secondChar == $x) return int.tryParse(contents);
+
+      if (secondChar == $o) {
+        var afterRadix = contents.substring(2);
+        return int.tryParse(afterRadix, radix: 8);
+      }
+    }
+
+    // Int or float starting with a digit or a +/- sign.
+    if ((firstChar >= $0 && firstChar <= $9) ||
+        ((firstChar == $plus || firstChar == $minus) &&
+            secondChar >= $0 &&
+            secondChar <= $9)) {
+      // Try to parse an int or, failing that, a double.
+      num? result;
+      if (allowInt) {
+        // Pass "radix: 10" explicitly to ensure that "-0x10", which is valid
+        // Dart but invalid YAML, doesn't get parsed.
+        result = int.tryParse(contents, radix: 10);
+      }
+
+      if (allowFloat) result ??= double.tryParse(contents);
+      return result;
+    }
+
+    if (!allowFloat) return null;
+
+    // Now the only possibility is to parse a float starting with a dot or a
+    // sign and a dot, or the signed/unsigned infinity values and not-a-numbers.
+    if ((firstChar == $dot && secondChar >= $0 && secondChar <= $9) ||
+        (firstChar == $minus || firstChar == $plus) && secondChar == $dot) {
+      // Starting with a . and a number or a sign followed by a dot.
+      if (length == 5) {
+        switch (contents) {
+          case '+.inf':
+          case '+.Inf':
+          case '+.INF':
+            return double.infinity;
+          case '-.inf':
+          case '-.Inf':
+          case '-.INF':
+            return -double.infinity;
+        }
+      }
+
+      return double.tryParse(contents);
+    }
+
+    if (length == 4 && firstChar == $dot) {
+      switch (contents) {
+        case '.inf':
+        case '.Inf':
+        case '.INF':
+          return double.infinity;
+        case '.nan':
+        case '.NaN':
+        case '.NAN':
+          return double.nan;
+      }
+    }
+
+    return null;
+  }
+}
diff --git a/pkgs/yaml/lib/src/null_span.dart b/pkgs/yaml/lib/src/null_span.dart
new file mode 100644
index 0000000..49e1a1c
--- /dev/null
+++ b/pkgs/yaml/lib/src/null_span.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'yaml_node.dart';
+
+/// A [SourceSpan] with no location information.
+///
+/// This is used with [YamlMap.wrap] and [YamlList.wrap] to provide means of
+/// accessing a non-YAML map that behaves transparently like a map parsed from
+/// YAML.
+class NullSpan extends SourceSpanMixin {
+  @override
+  final SourceLocation start;
+  @override
+  SourceLocation get end => start;
+  @override
+  final text = '';
+
+  NullSpan(Object? sourceUrl) : start = SourceLocation(0, sourceUrl: sourceUrl);
+}
diff --git a/pkgs/yaml/lib/src/parser.dart b/pkgs/yaml/lib/src/parser.dart
new file mode 100644
index 0000000..e924e40
--- /dev/null
+++ b/pkgs/yaml/lib/src/parser.dart
@@ -0,0 +1,805 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: constant_identifier_names
+
+import 'package:source_span/source_span.dart';
+import 'package:string_scanner/string_scanner.dart';
+
+import 'error_listener.dart';
+import 'event.dart';
+import 'scanner.dart';
+import 'style.dart';
+import 'token.dart';
+import 'utils.dart';
+import 'yaml_document.dart';
+import 'yaml_exception.dart';
+
+/// A parser that reads [Token]s emitted by a [Scanner] and emits [Event]s.
+///
+/// This is based on the libyaml parser, available at
+/// https://github.com/yaml/libyaml/blob/master/src/parser.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Parser {
+  /// The underlying [Scanner] that generates [Token]s.
+  final Scanner _scanner;
+
+  /// The stack of parse states for nested contexts.
+  final _states = <_State>[];
+
+  /// The current parse state.
+  var _state = _State.STREAM_START;
+
+  /// The custom tag directives, by tag handle.
+  final _tagDirectives = <String, TagDirective>{};
+
+  /// Whether the parser has finished parsing.
+  bool get isDone => _state == _State.END;
+
+  /// Creates a parser that parses [source].
+  ///
+  /// If [recover] is true, will attempt to recover from parse errors and may
+  /// return invalid or synthetic nodes. If [errorListener] is also supplied,
+  /// its onError method will be called for each error recovered from. It is not
+  /// valid to provide [errorListener] if [recover] is false.
+  Parser(String source,
+      {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener})
+      : assert(recover || errorListener == null),
+        _scanner = Scanner(source,
+            sourceUrl: sourceUrl,
+            recover: recover,
+            errorListener: errorListener);
+
+  /// Consumes and returns the next event.
+  Event parse() {
+    try {
+      if (isDone) throw StateError('No more events.');
+      var event = _stateMachine();
+      return event;
+    } on StringScannerException catch (error) {
+      throw YamlException(error.message, error.span);
+    }
+  }
+
+  /// Dispatches parsing based on the current state.
+  Event _stateMachine() {
+    switch (_state) {
+      case _State.STREAM_START:
+        return _parseStreamStart();
+      case _State.DOCUMENT_START:
+        return _parseDocumentStart();
+      case _State.DOCUMENT_CONTENT:
+        return _parseDocumentContent();
+      case _State.DOCUMENT_END:
+        return _parseDocumentEnd();
+      case _State.BLOCK_NODE:
+        return _parseNode(block: true);
+      case _State.BLOCK_NODE_OR_INDENTLESS_SEQUENCE:
+        return _parseNode(block: true, indentlessSequence: true);
+      case _State.FLOW_NODE:
+        return _parseNode();
+      case _State.BLOCK_SEQUENCE_FIRST_ENTRY:
+        // Scan past the `BLOCK-SEQUENCE-FIRST-ENTRY` token to the
+        // `BLOCK-SEQUENCE-ENTRY` token.
+        _scanner.scan();
+        return _parseBlockSequenceEntry();
+      case _State.BLOCK_SEQUENCE_ENTRY:
+        return _parseBlockSequenceEntry();
+      case _State.INDENTLESS_SEQUENCE_ENTRY:
+        return _parseIndentlessSequenceEntry();
+      case _State.BLOCK_MAPPING_FIRST_KEY:
+        // Scan past the `BLOCK-MAPPING-FIRST-KEY` token to the
+        // `BLOCK-MAPPING-KEY` token.
+        _scanner.scan();
+        return _parseBlockMappingKey();
+      case _State.BLOCK_MAPPING_KEY:
+        return _parseBlockMappingKey();
+      case _State.BLOCK_MAPPING_VALUE:
+        return _parseBlockMappingValue();
+      case _State.FLOW_SEQUENCE_FIRST_ENTRY:
+        return _parseFlowSequenceEntry(first: true);
+      case _State.FLOW_SEQUENCE_ENTRY:
+        return _parseFlowSequenceEntry();
+      case _State.FLOW_SEQUENCE_ENTRY_MAPPING_KEY:
+        return _parseFlowSequenceEntryMappingKey();
+      case _State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE:
+        return _parseFlowSequenceEntryMappingValue();
+      case _State.FLOW_SEQUENCE_ENTRY_MAPPING_END:
+        return _parseFlowSequenceEntryMappingEnd();
+      case _State.FLOW_MAPPING_FIRST_KEY:
+        return _parseFlowMappingKey(first: true);
+      case _State.FLOW_MAPPING_KEY:
+        return _parseFlowMappingKey();
+      case _State.FLOW_MAPPING_VALUE:
+        return _parseFlowMappingValue();
+      case _State.FLOW_MAPPING_EMPTY_VALUE:
+        return _parseFlowMappingValue(empty: true);
+      default:
+        throw StateError('Unreachable');
+    }
+  }
+
+  /// Parses the production:
+  ///
+  ///     stream ::=
+  ///       STREAM-START implicit_document? explicit_document* STREAM-END
+  ///       ************
+  Event _parseStreamStart() {
+    var token = _scanner.scan();
+    assert(token.type == TokenType.streamStart);
+
+    _state = _State.DOCUMENT_START;
+    return Event(EventType.streamStart, token.span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     implicit_document    ::= block_node DOCUMENT-END*
+  ///                              *
+  ///     explicit_document    ::=
+  ///       DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+  ///       *************************
+  Event _parseDocumentStart() {
+    var token = _scanner.peek()!;
+
+    // libyaml requires any document beyond the first in the stream to have an
+    // explicit document start indicator, but the spec allows it to be omitted
+    // as long as there was an end indicator.
+
+    // Parse extra document end indicators.
+    while (token.type == TokenType.documentEnd) {
+      token = _scanner.advance()!;
+    }
+
+    if (token.type != TokenType.versionDirective &&
+        token.type != TokenType.tagDirective &&
+        token.type != TokenType.documentStart &&
+        token.type != TokenType.streamEnd) {
+      // Parse an implicit document.
+      _processDirectives();
+      _states.add(_State.DOCUMENT_END);
+      _state = _State.BLOCK_NODE;
+      return DocumentStartEvent(token.span.start.pointSpan());
+    }
+
+    if (token.type == TokenType.streamEnd) {
+      _state = _State.END;
+      _scanner.scan();
+      return Event(EventType.streamEnd, token.span);
+    }
+
+    // Parse an explicit document.
+    var start = token.span;
+    var (versionDirective, tagDirectives) = _processDirectives();
+    token = _scanner.peek()!;
+    if (token.type != TokenType.documentStart) {
+      throw YamlException('Expected document start.', token.span);
+    }
+
+    _states.add(_State.DOCUMENT_END);
+    _state = _State.DOCUMENT_CONTENT;
+    _scanner.scan();
+    return DocumentStartEvent(start.expand(token.span),
+        versionDirective: versionDirective,
+        tagDirectives: tagDirectives,
+        isImplicit: false);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     explicit_document    ::=
+  ///       DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+  ///                                 ***********
+  Event _parseDocumentContent() {
+    var token = _scanner.peek()!;
+
+    switch (token.type) {
+      case TokenType.versionDirective:
+      case TokenType.tagDirective:
+      case TokenType.documentStart:
+      case TokenType.documentEnd:
+      case TokenType.streamEnd:
+        _state = _states.removeLast();
+        return _processEmptyScalar(token.span.start);
+      default:
+        return _parseNode(block: true);
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     implicit_document    ::= block_node DOCUMENT-END*
+  ///                                         *************
+  ///     explicit_document    ::=
+  ///       DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+  ///                                             *************
+  Event _parseDocumentEnd() {
+    _tagDirectives.clear();
+    _state = _State.DOCUMENT_START;
+
+    var token = _scanner.peek()!;
+    if (token.type == TokenType.documentEnd) {
+      _scanner.scan();
+      return DocumentEndEvent(token.span, isImplicit: false);
+    } else {
+      return DocumentEndEvent(token.span.start.pointSpan());
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_node_or_indentless_sequence    ::=
+  ///       ALIAS
+  ///       *****
+  ///       | properties (block_content | indentless_block_sequence)?
+  ///         **********  *
+  ///       | block_content | indentless_block_sequence
+  ///         *
+  ///     block_node           ::= ALIAS
+  ///                              *****
+  ///                              | properties block_content?
+  ///                                ********** *
+  ///                              | block_content
+  ///                                *
+  ///     flow_node            ::= ALIAS
+  ///                              *****
+  ///                              | properties flow_content?
+  ///                                ********** *
+  ///                              | flow_content
+  ///                                *
+  ///     properties           ::= TAG ANCHOR? | ANCHOR TAG?
+  ///                              *************************
+  ///     block_content        ::= block_collection | flow_collection | SCALAR
+  ///                                                                   ******
+  ///     flow_content         ::= flow_collection | SCALAR
+  ///                                                ******
+  Event _parseNode({bool block = false, bool indentlessSequence = false}) {
+    var token = _scanner.peek()!;
+
+    if (token is AliasToken) {
+      _scanner.scan();
+      _state = _states.removeLast();
+      return AliasEvent(token.span, token.name);
+    }
+
+    String? anchor;
+    TagToken? tagToken;
+    var span = token.span.start.pointSpan();
+    Token parseAnchor(AnchorToken token) {
+      anchor = token.name;
+      span = span.expand(token.span);
+      return _scanner.advance()!;
+    }
+
+    Token parseTag(TagToken token) {
+      tagToken = token;
+      span = span.expand(token.span);
+      return _scanner.advance()!;
+    }
+
+    if (token is AnchorToken) {
+      token = parseAnchor(token);
+      if (token is TagToken) token = parseTag(token);
+    } else if (token is TagToken) {
+      token = parseTag(token);
+      if (token is AnchorToken) token = parseAnchor(token);
+    }
+
+    String? tag;
+    if (tagToken != null) {
+      if (tagToken!.handle == null) {
+        tag = tagToken!.suffix;
+      } else {
+        var tagDirective = _tagDirectives[tagToken!.handle];
+        if (tagDirective == null) {
+          throw YamlException('Undefined tag handle.', tagToken!.span);
+        }
+
+        tag = tagDirective.prefix + (tagToken?.suffix ?? '');
+      }
+    }
+
+    if (indentlessSequence && token.type == TokenType.blockEntry) {
+      _state = _State.INDENTLESS_SEQUENCE_ENTRY;
+      return SequenceStartEvent(span.expand(token.span), CollectionStyle.BLOCK,
+          anchor: anchor, tag: tag);
+    }
+
+    if (token is ScalarToken) {
+      // All non-plain scalars have the "!" tag by default.
+      if (tag == null && token.style != ScalarStyle.PLAIN) tag = '!';
+
+      _state = _states.removeLast();
+      _scanner.scan();
+      return ScalarEvent(span.expand(token.span), token.value, token.style,
+          anchor: anchor, tag: tag);
+    }
+
+    if (token.type == TokenType.flowSequenceStart) {
+      _state = _State.FLOW_SEQUENCE_FIRST_ENTRY;
+      return SequenceStartEvent(span.expand(token.span), CollectionStyle.FLOW,
+          anchor: anchor, tag: tag);
+    }
+
+    if (token.type == TokenType.flowMappingStart) {
+      _state = _State.FLOW_MAPPING_FIRST_KEY;
+      return MappingStartEvent(span.expand(token.span), CollectionStyle.FLOW,
+          anchor: anchor, tag: tag);
+    }
+
+    if (block && token.type == TokenType.blockSequenceStart) {
+      _state = _State.BLOCK_SEQUENCE_FIRST_ENTRY;
+      return SequenceStartEvent(span.expand(token.span), CollectionStyle.BLOCK,
+          anchor: anchor, tag: tag);
+    }
+
+    if (block && token.type == TokenType.blockMappingStart) {
+      _state = _State.BLOCK_MAPPING_FIRST_KEY;
+      return MappingStartEvent(span.expand(token.span), CollectionStyle.BLOCK,
+          anchor: anchor, tag: tag);
+    }
+
+    if (anchor != null || tag != null) {
+      _state = _states.removeLast();
+      return ScalarEvent(span, '', ScalarStyle.PLAIN, anchor: anchor, tag: tag);
+    }
+
+    throw YamlException('Expected node content.', span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_sequence ::=
+  ///       BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
+  ///       ********************  *********** *             *********
+  Event _parseBlockSequenceEntry() {
+    var token = _scanner.peek()!;
+
+    if (token.type == TokenType.blockEntry) {
+      var start = token.span.start;
+      token = _scanner.advance()!;
+
+      if (token.type == TokenType.blockEntry ||
+          token.type == TokenType.blockEnd) {
+        _state = _State.BLOCK_SEQUENCE_ENTRY;
+        return _processEmptyScalar(start);
+      } else {
+        _states.add(_State.BLOCK_SEQUENCE_ENTRY);
+        return _parseNode(block: true);
+      }
+    }
+
+    if (token.type == TokenType.blockEnd) {
+      _scanner.scan();
+      _state = _states.removeLast();
+      return Event(EventType.sequenceEnd, token.span);
+    }
+
+    throw YamlException("While parsing a block collection, expected '-'.",
+        token.span.start.pointSpan());
+  }
+
+  /// Parses the productions:
+  ///
+  ///     indentless_sequence  ::= (BLOCK-ENTRY block_node?)+
+  ///                               *********** *
+  Event _parseIndentlessSequenceEntry() {
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.blockEntry) {
+      _state = _states.removeLast();
+      return Event(EventType.sequenceEnd, token.span.start.pointSpan());
+    }
+
+    var start = token.span.start;
+    token = _scanner.advance()!;
+
+    if (token.type == TokenType.blockEntry ||
+        token.type == TokenType.key ||
+        token.type == TokenType.value ||
+        token.type == TokenType.blockEnd) {
+      _state = _State.INDENTLESS_SEQUENCE_ENTRY;
+      return _processEmptyScalar(start);
+    } else {
+      _states.add(_State.INDENTLESS_SEQUENCE_ENTRY);
+      return _parseNode(block: true);
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_mapping        ::= BLOCK-MAPPING_START
+  ///                              *******************
+  ///                              ((KEY block_node_or_indentless_sequence?)?
+  ///                                *** *
+  ///                              (VALUE block_node_or_indentless_sequence?)?)*
+  ///
+  ///                              BLOCK-END
+  ///                              *********
+  Event _parseBlockMappingKey() {
+    var token = _scanner.peek()!;
+    if (token.type == TokenType.key) {
+      var start = token.span.start;
+      token = _scanner.advance()!;
+
+      if (token.type == TokenType.key ||
+          token.type == TokenType.value ||
+          token.type == TokenType.blockEnd) {
+        _state = _State.BLOCK_MAPPING_VALUE;
+        return _processEmptyScalar(start);
+      } else {
+        _states.add(_State.BLOCK_MAPPING_VALUE);
+        return _parseNode(block: true, indentlessSequence: true);
+      }
+    }
+
+    // libyaml doesn't allow empty keys without an explicit key indicator, but
+    // the spec does. See example 8.18:
+    // http://yaml.org/spec/1.2/spec.html#id2798896.
+    if (token.type == TokenType.value) {
+      _state = _State.BLOCK_MAPPING_VALUE;
+      return _processEmptyScalar(token.span.start);
+    }
+
+    if (token.type == TokenType.blockEnd) {
+      _scanner.scan();
+      _state = _states.removeLast();
+      return Event(EventType.mappingEnd, token.span);
+    }
+
+    throw YamlException('Expected a key while parsing a block mapping.',
+        token.span.start.pointSpan());
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_mapping        ::= BLOCK-MAPPING_START
+  ///
+  ///                              ((KEY block_node_or_indentless_sequence?)?
+  ///
+  ///                              (VALUE block_node_or_indentless_sequence?)?)*
+  ///                               ***** *
+  ///                              BLOCK-END
+  ///
+  Event _parseBlockMappingValue() {
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.value) {
+      _state = _State.BLOCK_MAPPING_KEY;
+      return _processEmptyScalar(token.span.start);
+    }
+
+    var start = token.span.start;
+    token = _scanner.advance()!;
+    if (token.type == TokenType.key ||
+        token.type == TokenType.value ||
+        token.type == TokenType.blockEnd) {
+      _state = _State.BLOCK_MAPPING_KEY;
+      return _processEmptyScalar(start);
+    } else {
+      _states.add(_State.BLOCK_MAPPING_KEY);
+      return _parseNode(block: true, indentlessSequence: true);
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence        ::= FLOW-SEQUENCE-START
+  ///                              *******************
+  ///                              (flow_sequence_entry FLOW-ENTRY)*
+  ///                               *                   **********
+  ///                              flow_sequence_entry?
+  ///                              *
+  ///                              FLOW-SEQUENCE-END
+  ///                              *****************
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///       *
+  Event _parseFlowSequenceEntry({bool first = false}) {
+    if (first) _scanner.scan();
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.flowSequenceEnd) {
+      if (!first) {
+        if (token.type != TokenType.flowEntry) {
+          throw YamlException(
+              "While parsing a flow sequence, expected ',' or ']'.",
+              token.span.start.pointSpan());
+        }
+
+        token = _scanner.advance()!;
+      }
+
+      if (token.type == TokenType.key) {
+        _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_KEY;
+        _scanner.scan();
+        return MappingStartEvent(token.span, CollectionStyle.FLOW);
+      } else if (token.type != TokenType.flowSequenceEnd) {
+        _states.add(_State.FLOW_SEQUENCE_ENTRY);
+        return _parseNode();
+      }
+    }
+
+    _scanner.scan();
+    _state = _states.removeLast();
+    return Event(EventType.sequenceEnd, token.span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                   *** *
+  Event _parseFlowSequenceEntryMappingKey() {
+    var token = _scanner.peek()!;
+
+    if (token.type == TokenType.value ||
+        token.type == TokenType.flowEntry ||
+        token.type == TokenType.flowSequenceEnd) {
+      // libyaml consumes the token here, but that seems like a bug, since it
+      // always causes [_parseFlowSequenceEntryMappingValue] to emit an empty
+      // scalar.
+
+      var start = token.span.start;
+      _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE;
+      return _processEmptyScalar(start);
+    } else {
+      _states.add(_State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE);
+      return _parseNode();
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                                   ***** *
+  Event _parseFlowSequenceEntryMappingValue() {
+    var token = _scanner.peek()!;
+
+    if (token.type == TokenType.value) {
+      token = _scanner.advance()!;
+      if (token.type != TokenType.flowEntry &&
+          token.type != TokenType.flowSequenceEnd) {
+        _states.add(_State.FLOW_SEQUENCE_ENTRY_MAPPING_END);
+        return _parseNode();
+      }
+    }
+
+    _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_END;
+    return _processEmptyScalar(token.span.start);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                                                   *
+  Event _parseFlowSequenceEntryMappingEnd() {
+    _state = _State.FLOW_SEQUENCE_ENTRY;
+    return Event(EventType.mappingEnd, _scanner.peek()!.span.start.pointSpan());
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_mapping         ::= FLOW-MAPPING-START
+  ///                              ******************
+  ///                              (flow_mapping_entry FLOW-ENTRY)*
+  ///                               *                  **********
+  ///                              flow_mapping_entry?
+  ///                              ******************
+  ///                              FLOW-MAPPING-END
+  ///                              ****************
+  ///     flow_mapping_entry   ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///       *           *** *
+  Event _parseFlowMappingKey({bool first = false}) {
+    if (first) _scanner.scan();
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.flowMappingEnd) {
+      if (!first) {
+        if (token.type != TokenType.flowEntry) {
+          throw YamlException(
+              "While parsing a flow mapping, expected ',' or '}'.",
+              token.span.start.pointSpan());
+        }
+
+        token = _scanner.advance()!;
+      }
+
+      if (token.type == TokenType.key) {
+        token = _scanner.advance()!;
+        if (token.type != TokenType.value &&
+            token.type != TokenType.flowEntry &&
+            token.type != TokenType.flowMappingEnd) {
+          _states.add(_State.FLOW_MAPPING_VALUE);
+          return _parseNode();
+        } else {
+          _state = _State.FLOW_MAPPING_VALUE;
+          return _processEmptyScalar(token.span.start);
+        }
+      } else if (token.type != TokenType.flowMappingEnd) {
+        _states.add(_State.FLOW_MAPPING_EMPTY_VALUE);
+        return _parseNode();
+      }
+    }
+
+    _scanner.scan();
+    _state = _states.removeLast();
+    return Event(EventType.mappingEnd, token.span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_mapping_entry   ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                *                  ***** *
+  Event _parseFlowMappingValue({bool empty = false}) {
+    var token = _scanner.peek()!;
+
+    if (empty) {
+      _state = _State.FLOW_MAPPING_KEY;
+      return _processEmptyScalar(token.span.start);
+    }
+
+    if (token.type == TokenType.value) {
+      token = _scanner.advance()!;
+      if (token.type != TokenType.flowEntry &&
+          token.type != TokenType.flowMappingEnd) {
+        _states.add(_State.FLOW_MAPPING_KEY);
+        return _parseNode();
+      }
+    }
+
+    _state = _State.FLOW_MAPPING_KEY;
+    return _processEmptyScalar(token.span.start);
+  }
+
+  /// Generate an empty scalar event.
+  Event _processEmptyScalar(SourceLocation location) =>
+      ScalarEvent(location.pointSpan() as FileSpan, '', ScalarStyle.PLAIN);
+
+  /// Parses directives.
+  (VersionDirective?, List<TagDirective>) _processDirectives() {
+    var token = _scanner.peek()!;
+
+    VersionDirective? versionDirective;
+    var tagDirectives = <TagDirective>[];
+    while (token.type == TokenType.versionDirective ||
+        token.type == TokenType.tagDirective) {
+      if (token is VersionDirectiveToken) {
+        if (versionDirective != null) {
+          throw YamlException('Duplicate %YAML directive.', token.span);
+        }
+
+        if (token.major != 1 || token.minor == 0) {
+          throw YamlException(
+              'Incompatible YAML document. This parser only supports YAML 1.1 '
+              'and 1.2.',
+              token.span);
+        } else if (token.minor > 2) {
+          // TODO(nweiz): Print to stderr when issue 6943 is fixed and dart:io
+          // is available.
+          warn('Warning: this parser only supports YAML 1.1 and 1.2.',
+              token.span);
+        }
+
+        versionDirective = VersionDirective(token.major, token.minor);
+      } else if (token is TagDirectiveToken) {
+        var tagDirective = TagDirective(token.handle, token.prefix);
+        _appendTagDirective(tagDirective, token.span);
+        tagDirectives.add(tagDirective);
+      }
+
+      token = _scanner.advance()!;
+    }
+
+    _appendTagDirective(TagDirective('!', '!'), token.span.start.pointSpan(),
+        allowDuplicates: true);
+    _appendTagDirective(
+        TagDirective('!!', 'tag:yaml.org,2002:'), token.span.start.pointSpan(),
+        allowDuplicates: true);
+
+    return (versionDirective, tagDirectives);
+  }
+
+  /// Adds a tag directive to the directives stack.
+  void _appendTagDirective(TagDirective newDirective, FileSpan span,
+      {bool allowDuplicates = false}) {
+    if (_tagDirectives.containsKey(newDirective.handle)) {
+      if (allowDuplicates) return;
+      throw YamlException('Duplicate %TAG directive.', span);
+    }
+
+    _tagDirectives[newDirective.handle] = newDirective;
+  }
+}
+
+/// The possible states for the parser.
+class _State {
+  /// Expect [TokenType.streamStart].
+  static const STREAM_START = _State('STREAM_START');
+
+  /// Expect [TokenType.documentStart].
+  static const DOCUMENT_START = _State('DOCUMENT_START');
+
+  /// Expect the content of a document.
+  static const DOCUMENT_CONTENT = _State('DOCUMENT_CONTENT');
+
+  /// Expect [TokenType.documentEnd].
+  static const DOCUMENT_END = _State('DOCUMENT_END');
+
+  /// Expect a block node.
+  static const BLOCK_NODE = _State('BLOCK_NODE');
+
+  /// Expect a block node or indentless sequence.
+  static const BLOCK_NODE_OR_INDENTLESS_SEQUENCE =
+      _State('BLOCK_NODE_OR_INDENTLESS_SEQUENCE');
+
+  /// Expect a flow node.
+  static const FLOW_NODE = _State('FLOW_NODE');
+
+  /// Expect the first entry of a block sequence.
+  static const BLOCK_SEQUENCE_FIRST_ENTRY =
+      _State('BLOCK_SEQUENCE_FIRST_ENTRY');
+
+  /// Expect an entry of a block sequence.
+  static const BLOCK_SEQUENCE_ENTRY = _State('BLOCK_SEQUENCE_ENTRY');
+
+  /// Expect an entry of an indentless sequence.
+  static const INDENTLESS_SEQUENCE_ENTRY = _State('INDENTLESS_SEQUENCE_ENTRY');
+
+  /// Expect the first key of a block mapping.
+  static const BLOCK_MAPPING_FIRST_KEY = _State('BLOCK_MAPPING_FIRST_KEY');
+
+  /// Expect a block mapping key.
+  static const BLOCK_MAPPING_KEY = _State('BLOCK_MAPPING_KEY');
+
+  /// Expect a block mapping value.
+  static const BLOCK_MAPPING_VALUE = _State('BLOCK_MAPPING_VALUE');
+
+  /// Expect the first entry of a flow sequence.
+  static const FLOW_SEQUENCE_FIRST_ENTRY = _State('FLOW_SEQUENCE_FIRST_ENTRY');
+
+  /// Expect an entry of a flow sequence.
+  static const FLOW_SEQUENCE_ENTRY = _State('FLOW_SEQUENCE_ENTRY');
+
+  /// Expect a key of an ordered mapping.
+  static const FLOW_SEQUENCE_ENTRY_MAPPING_KEY =
+      _State('FLOW_SEQUENCE_ENTRY_MAPPING_KEY');
+
+  /// Expect a value of an ordered mapping.
+  static const FLOW_SEQUENCE_ENTRY_MAPPING_VALUE =
+      _State('FLOW_SEQUENCE_ENTRY_MAPPING_VALUE');
+
+  /// Expect the and of an ordered mapping entry.
+  static const FLOW_SEQUENCE_ENTRY_MAPPING_END =
+      _State('FLOW_SEQUENCE_ENTRY_MAPPING_END');
+
+  /// Expect the first key of a flow mapping.
+  static const FLOW_MAPPING_FIRST_KEY = _State('FLOW_MAPPING_FIRST_KEY');
+
+  /// Expect a key of a flow mapping.
+  static const FLOW_MAPPING_KEY = _State('FLOW_MAPPING_KEY');
+
+  /// Expect a value of a flow mapping.
+  static const FLOW_MAPPING_VALUE = _State('FLOW_MAPPING_VALUE');
+
+  /// Expect an empty value of a flow mapping.
+  static const FLOW_MAPPING_EMPTY_VALUE = _State('FLOW_MAPPING_EMPTY_VALUE');
+
+  /// Expect nothing.
+  static const END = _State('END');
+
+  final String name;
+
+  const _State(this.name);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/yaml/lib/src/scanner.dart b/pkgs/yaml/lib/src/scanner.dart
new file mode 100644
index 0000000..1cfd3af
--- /dev/null
+++ b/pkgs/yaml/lib/src/scanner.dart
@@ -0,0 +1,1695 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: constant_identifier_names
+
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+import 'package:string_scanner/string_scanner.dart';
+
+import 'error_listener.dart';
+import 'style.dart';
+import 'token.dart';
+import 'utils.dart';
+import 'yaml_exception.dart';
+
+/// A scanner that reads a string of Unicode characters and emits [Token]s.
+///
+/// This is based on the libyaml scanner, available at
+/// https://github.com/yaml/libyaml/blob/master/src/scanner.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Scanner {
+  static const TAB = 0x9;
+  static const LF = 0xA;
+  static const CR = 0xD;
+  static const SP = 0x20;
+  static const DOLLAR = 0x24;
+  static const LEFT_PAREN = 0x28;
+  static const RIGHT_PAREN = 0x29;
+  static const PLUS = 0x2B;
+  static const COMMA = 0x2C;
+  static const HYPHEN = 0x2D;
+  static const PERIOD = 0x2E;
+  static const QUESTION = 0x3F;
+  static const COLON = 0x3A;
+  static const SEMICOLON = 0x3B;
+  static const EQUALS = 0x3D;
+  static const LEFT_SQUARE = 0x5B;
+  static const RIGHT_SQUARE = 0x5D;
+  static const LEFT_CURLY = 0x7B;
+  static const RIGHT_CURLY = 0x7D;
+  static const HASH = 0x23;
+  static const AMPERSAND = 0x26;
+  static const ASTERISK = 0x2A;
+  static const EXCLAMATION = 0x21;
+  static const VERTICAL_BAR = 0x7C;
+  static const LEFT_ANGLE = 0x3C;
+  static const RIGHT_ANGLE = 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 TILDE = 0x7E;
+
+  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 NEL = 0x85;
+  static const NBSP = 0xA0;
+  static const LINE_SEPARATOR = 0x2028;
+  static const PARAGRAPH_SEPARATOR = 0x2029;
+  static const BOM = 0xFEFF;
+
+  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_Z = 0x7A;
+
+  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 LETTER_CAP_Z = 0x5A;
+
+  /// Whether this scanner should attempt to recover when parsing invalid YAML.
+  final bool _recover;
+
+  /// A listener to report YAML errors to.
+  final ErrorListener? _errorListener;
+
+  /// The underlying [SpanScanner] used to read characters from the source text.
+  ///
+  /// This is also used to track line and column information and to generate
+  /// [SourceSpan]s.
+  final SpanScanner _scanner;
+
+  /// Whether this scanner has produced a [TokenType.streamStart] token
+  /// indicating the beginning of the YAML stream.
+  var _streamStartProduced = false;
+
+  /// Whether this scanner has produced a [TokenType.streamEnd] token
+  /// indicating the end of the YAML stream.
+  var _streamEndProduced = false;
+
+  /// The queue of tokens yet to be emitted.
+  ///
+  /// These are queued up in advance so that [TokenType.key] tokens can be
+  /// inserted once the scanner determines that a series of tokens represents a
+  /// mapping key.
+  final _tokens = QueueList<Token>();
+
+  /// The number of tokens that have been emitted.
+  ///
+  /// This doesn't count tokens in [_tokens].
+  var _tokensParsed = 0;
+
+  /// Whether the next token in [_tokens] is ready to be returned.
+  ///
+  /// It might not be ready if there may still be a [TokenType.key] inserted
+  /// before it.
+  var _tokenAvailable = false;
+
+  /// The stack of indent levels for the current nested block contexts.
+  ///
+  /// The YAML spec specifies that the initial indentation level is -1 spaces.
+  final _indents = <int>[-1];
+
+  /// Whether a simple key is allowed in this context.
+  ///
+  /// A simple key refers to any mapping key that doesn't have an explicit "?".
+  var _simpleKeyAllowed = true;
+
+  /// The stack of potential simple keys for each level of flow nesting.
+  ///
+  /// Entries in this list may be `null`, indicating that there is no valid
+  /// simple key for the associated level of nesting.
+  ///
+  /// When a ":" is parsed and there's a simple key available, a [TokenType.key]
+  /// token is inserted in [_tokens] before that key's token. This allows the
+  /// parser to tell that the key is intended to be a mapping key.
+  final _simpleKeys = <_SimpleKey?>[null];
+
+  /// The current indentation level.
+  int get _indent => _indents.last;
+
+  /// Whether the scanner's currently positioned in a block-level structure (as
+  /// opposed to flow-level).
+  bool get _inBlockContext => _simpleKeys.length == 1;
+
+  /// Whether the current character is a line break or the end of the source.
+  bool get _isBreakOrEnd => _scanner.isDone || _isBreak;
+
+  /// Whether the current character is a line break.
+  bool get _isBreak => _isBreakAt(0);
+
+  /// Whether the current character is whitespace or the end of the source.
+  bool get _isBlankOrEnd => _isBlankOrEndAt(0);
+
+  /// Whether the current character is whitespace.
+  bool get _isBlank => _isBlankAt(0);
+
+  /// Whether the current character is a valid tag name character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-tag-name.
+  bool get _isTagChar {
+    var char = _scanner.peekChar();
+    if (char == null) return false;
+    switch (char) {
+      case HYPHEN:
+      case SEMICOLON:
+      case SLASH:
+      case COLON:
+      case AT:
+      case AMPERSAND:
+      case EQUALS:
+      case PLUS:
+      case DOLLAR:
+      case PERIOD:
+      case TILDE:
+      case QUESTION:
+      case ASTERISK:
+      case SINGLE_QUOTE:
+      case LEFT_PAREN:
+      case RIGHT_PAREN:
+      case PERCENT:
+        return true;
+      default:
+        return (char >= NUMBER_0 && char <= NUMBER_9) ||
+            (char >= LETTER_A && char <= LETTER_Z) ||
+            (char >= LETTER_CAP_A && char <= LETTER_CAP_Z);
+    }
+  }
+
+  /// Whether the current character is a valid anchor name character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-anchor-name.
+  bool get _isAnchorChar {
+    if (!_isNonSpace) return false;
+
+    switch (_scanner.peekChar()) {
+      case COMMA:
+      case LEFT_SQUARE:
+      case RIGHT_SQUARE:
+      case LEFT_CURLY:
+      case RIGHT_CURLY:
+        return false;
+      default:
+        return true;
+    }
+  }
+
+  /// Whether the character at the current position is a decimal digit.
+  bool get _isDigit {
+    var char = _scanner.peekChar();
+    return char != null && (char >= NUMBER_0 && char <= NUMBER_9);
+  }
+
+  /// Whether the character at the current position is a hexidecimal
+  /// digit.
+  bool get _isHex {
+    var char = _scanner.peekChar();
+    if (char == null) return false;
+    return (char >= NUMBER_0 && char <= NUMBER_9) ||
+        (char >= LETTER_A && char <= LETTER_F) ||
+        (char >= LETTER_CAP_A && char <= LETTER_CAP_F);
+  }
+
+  /// Whether the character at the current position is a plain character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-plain-char(c).
+  bool get _isPlainChar => _isPlainCharAt(0);
+
+  /// Whether the character at the current position is a printable character
+  /// other than a line break or byte-order mark.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#nb-char.
+  bool get _isNonBreak {
+    var char = _scanner.peekChar();
+    return switch (char) {
+      null => false,
+      LF || CR || BOM => false,
+      TAB || NEL => true,
+      _ => _isStandardCharacterAt(0),
+    };
+  }
+
+  /// Whether the character at the current position is a printable character
+  /// other than whitespace.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#nb-char.
+  bool get _isNonSpace {
+    var char = _scanner.peekChar();
+    return switch (char) {
+      null => false,
+      LF || CR || BOM || SP => false,
+      NEL => true,
+      _ => _isStandardCharacterAt(0),
+    };
+  }
+
+  /// Returns Whether or not the current character begins a documentation
+  /// indicator.
+  ///
+  /// If so, this sets the scanner's last match to that indicator.
+  bool get _isDocumentIndicator =>
+      _scanner.column == 0 &&
+      _isBlankOrEndAt(3) &&
+      (_scanner.matches('---') || _scanner.matches('...'));
+
+  /// Creates a scanner that scans [source].
+  Scanner(String source,
+      {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener})
+      : _recover = recover,
+        _errorListener = errorListener,
+        _scanner = SpanScanner.eager(source, sourceUrl: sourceUrl);
+
+  /// Consumes and returns the next token.
+  Token scan() {
+    if (_streamEndProduced) throw StateError('Out of tokens.');
+    if (!_tokenAvailable) _fetchMoreTokens();
+
+    var token = _tokens.removeFirst();
+    _tokenAvailable = false;
+    _tokensParsed++;
+    _streamEndProduced = token.type == TokenType.streamEnd;
+    return token;
+  }
+
+  /// Consumes the next token and returns the one after that.
+  Token? advance() {
+    scan();
+    return peek();
+  }
+
+  /// Returns the next token without consuming it.
+  Token? peek() {
+    if (_streamEndProduced) return null;
+    if (!_tokenAvailable) _fetchMoreTokens();
+    return _tokens.first;
+  }
+
+  /// Ensures that [_tokens] contains at least one token which can be returned.
+  void _fetchMoreTokens() {
+    while (true) {
+      if (_tokens.isNotEmpty) {
+        _staleSimpleKeys();
+
+        // If there are no more tokens to fetch, break.
+        if (_tokens.last.type == TokenType.streamEnd) break;
+
+        // If the current token could be a simple key, we need to scan more
+        // tokens until we determine whether it is or not. Otherwise we might
+        // not emit the `KEY` token before we emit the value of the key.
+        if (!_simpleKeys
+            .any((key) => key != null && key.tokenNumber == _tokensParsed)) {
+          break;
+        }
+      }
+
+      _fetchNextToken();
+    }
+    _tokenAvailable = true;
+  }
+
+  /// The dispatcher for token fetchers.
+  void _fetchNextToken() {
+    if (!_streamStartProduced) {
+      _fetchStreamStart();
+      return;
+    }
+
+    _scanToNextToken();
+    _staleSimpleKeys();
+    _unrollIndent(_scanner.column);
+
+    if (_scanner.isDone) {
+      _fetchStreamEnd();
+      return;
+    }
+
+    if (_scanner.column == 0) {
+      if (_scanner.peekChar() == PERCENT) {
+        _fetchDirective();
+        return;
+      }
+
+      if (_isBlankOrEndAt(3)) {
+        if (_scanner.matches('---')) {
+          _fetchDocumentIndicator(TokenType.documentStart);
+          return;
+        }
+
+        if (_scanner.matches('...')) {
+          _fetchDocumentIndicator(TokenType.documentEnd);
+          return;
+        }
+      }
+    }
+
+    switch (_scanner.peekChar()) {
+      case LEFT_SQUARE:
+        _fetchFlowCollectionStart(TokenType.flowSequenceStart);
+        return;
+      case LEFT_CURLY:
+        _fetchFlowCollectionStart(TokenType.flowMappingStart);
+        return;
+      case RIGHT_SQUARE:
+        _fetchFlowCollectionEnd(TokenType.flowSequenceEnd);
+        return;
+      case RIGHT_CURLY:
+        _fetchFlowCollectionEnd(TokenType.flowMappingEnd);
+        return;
+      case COMMA:
+        _fetchFlowEntry();
+        return;
+      case ASTERISK:
+        _fetchAnchor(anchor: false);
+        return;
+      case AMPERSAND:
+        _fetchAnchor();
+        return;
+      case EXCLAMATION:
+        _fetchTag();
+        return;
+      case SINGLE_QUOTE:
+        _fetchFlowScalar(singleQuote: true);
+        return;
+      case DOUBLE_QUOTE:
+        _fetchFlowScalar();
+        return;
+      case VERTICAL_BAR:
+        if (!_inBlockContext) _invalidScalarCharacter();
+        _fetchBlockScalar(literal: true);
+        return;
+      case RIGHT_ANGLE:
+        if (!_inBlockContext) _invalidScalarCharacter();
+        _fetchBlockScalar();
+        return;
+      case PERCENT:
+      case AT:
+      case GRAVE_ACCENT:
+        _invalidScalarCharacter();
+        return;
+
+      // These characters may sometimes begin plain scalars.
+      case HYPHEN:
+        if (_isPlainCharAt(1)) {
+          _fetchPlainScalar();
+        } else {
+          _fetchBlockEntry();
+        }
+        return;
+      case QUESTION:
+        if (_isPlainCharAt(1)) {
+          _fetchPlainScalar();
+        } else {
+          _fetchKey();
+        }
+        return;
+      case COLON:
+        if (!_inBlockContext && _tokens.isNotEmpty) {
+          // If a colon follows a "JSON-like" value (an explicit map or list, or
+          // a quoted string) it isn't required to have whitespace after it
+          // since it unambiguously describes a map.
+          var token = _tokens.last;
+          if (token.type == TokenType.flowSequenceEnd ||
+              token.type == TokenType.flowMappingEnd ||
+              (token.type == TokenType.scalar &&
+                  (token as ScalarToken).style.isQuoted)) {
+            _fetchValue();
+            return;
+          }
+        }
+
+        if (_isPlainCharAt(1)) {
+          _fetchPlainScalar();
+        } else {
+          _fetchValue();
+        }
+        return;
+      default:
+        if (!_isNonBreak) _invalidScalarCharacter();
+
+        _fetchPlainScalar();
+        return;
+    }
+  }
+
+  /// Throws an error about a disallowed character.
+  void _invalidScalarCharacter() =>
+      _scanner.error('Unexpected character.', length: 1);
+
+  /// Checks the list of potential simple keys and remove the positions that
+  /// cannot contain simple keys anymore.
+  void _staleSimpleKeys() {
+    for (var i = 0; i < _simpleKeys.length; i++) {
+      var key = _simpleKeys[i];
+      if (key == null) continue;
+
+      // libyaml requires that all simple keys be a single line and no longer
+      // than 1024 characters. However, in section 7.4.2 of the spec
+      // (http://yaml.org/spec/1.2/spec.html#id2790832), these restrictions are
+      // only applied when the curly braces are omitted. It's difficult to
+      // retain enough context to know which keys need to have the restriction
+      // placed on them, so for now we go the other direction and allow
+      // everything but multiline simple keys in a block context.
+      if (!_inBlockContext) continue;
+
+      if (key.line == _scanner.line) continue;
+
+      if (key.required) {
+        _reportError(YamlException("Expected ':'.", _scanner.emptySpan));
+        _tokens.insert(key.tokenNumber - _tokensParsed,
+            Token(TokenType.key, key.location.pointSpan() as FileSpan));
+      }
+
+      _simpleKeys[i] = null;
+    }
+  }
+
+  /// Checks if a simple key may start at the current position and saves it if
+  /// so.
+  void _saveSimpleKey() {
+    // A simple key is required at the current position if the scanner is in the
+    // block context and the current column coincides with the indentation
+    // level.
+    var required = _inBlockContext && _indent == _scanner.column;
+
+    // A simple key is required only when it is the first token in the current
+    // line. Therefore it is always allowed. But we add a check anyway.
+    assert(_simpleKeyAllowed || !required);
+
+    if (!_simpleKeyAllowed) return;
+
+    // If the current position may start a simple key, save it.
+    _removeSimpleKey();
+    _simpleKeys[_simpleKeys.length - 1] = _SimpleKey(
+        _tokensParsed + _tokens.length,
+        _scanner.line,
+        _scanner.column,
+        _scanner.location,
+        required: required);
+  }
+
+  /// Removes a potential simple key at the current flow level.
+  void _removeSimpleKey() {
+    var key = _simpleKeys.last;
+    if (key != null && key.required) {
+      throw YamlException("Could not find expected ':' for simple key.",
+          key.location.pointSpan());
+    }
+
+    _simpleKeys[_simpleKeys.length - 1] = null;
+  }
+
+  /// Increases the flow level and resizes the simple key list.
+  void _increaseFlowLevel() {
+    _simpleKeys.add(null);
+  }
+
+  /// Decreases the flow level.
+  void _decreaseFlowLevel() {
+    if (_inBlockContext) return;
+    _simpleKeys.removeLast();
+  }
+
+  /// Pushes the current indentation level to the stack and sets the new level
+  /// if [column] is greater than [_indent].
+  ///
+  /// If it is, appends or inserts the specified token into [_tokens]. If
+  /// [tokenNumber] is provided, the corresponding token will be replaced;
+  /// otherwise, the token will be added at the end.
+  void _rollIndent(int column, TokenType type, SourceLocation location,
+      {int? tokenNumber}) {
+    if (!_inBlockContext) return;
+    if (_indent != -1 && _indent >= column) return;
+
+    // Push the current indentation level to the stack and set the new
+    // indentation level.
+    _indents.add(column);
+
+    // Create a token and insert it into the queue.
+    var token = Token(type, location.pointSpan() as FileSpan);
+    if (tokenNumber == null) {
+      _tokens.add(token);
+    } else {
+      _tokens.insert(tokenNumber - _tokensParsed, token);
+    }
+  }
+
+  /// Pops indentation levels from [_indents] until the current level becomes
+  /// less than or equal to [column].
+  ///
+  /// For each indentation level, appends a [TokenType.blockEnd] token.
+  void _unrollIndent(int column) {
+    if (!_inBlockContext) return;
+
+    while (_indent > column) {
+      _tokens.add(Token(TokenType.blockEnd, _scanner.emptySpan));
+      _indents.removeLast();
+    }
+  }
+
+  /// Pops indentation levels from [_indents] until the current level resets to
+  /// -1.
+  ///
+  /// For each indentation level, appends a [TokenType.blockEnd] token.
+  void _resetIndent() => _unrollIndent(-1);
+
+  /// Produces a [TokenType.streamStart] token.
+  void _fetchStreamStart() {
+    // Much of libyaml's initialization logic here is done in variable
+    // initializers instead.
+    _streamStartProduced = true;
+    _tokens.add(Token(TokenType.streamStart, _scanner.emptySpan));
+  }
+
+  /// Produces a [TokenType.streamEnd] token.
+  void _fetchStreamEnd() {
+    _resetIndent();
+    _removeSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(Token(TokenType.streamEnd, _scanner.emptySpan));
+  }
+
+  /// Produces a [TokenType.versionDirective] or [TokenType.tagDirective]
+  /// token.
+  void _fetchDirective() {
+    _resetIndent();
+    _removeSimpleKey();
+    _simpleKeyAllowed = false;
+    var directive = _scanDirective();
+    if (directive != null) _tokens.add(directive);
+  }
+
+  /// Produces a [TokenType.documentStart] or [TokenType.documentEnd] token.
+  void _fetchDocumentIndicator(TokenType type) {
+    _resetIndent();
+    _removeSimpleKey();
+    _simpleKeyAllowed = false;
+
+    // Consume the indicator token.
+    var start = _scanner.state;
+    _scanner.readCodePoint();
+    _scanner.readCodePoint();
+    _scanner.readCodePoint();
+
+    _tokens.add(Token(type, _scanner.spanFrom(start)));
+  }
+
+  /// Produces a [TokenType.flowSequenceStart] or
+  /// [TokenType.flowMappingStart] token.
+  void _fetchFlowCollectionStart(TokenType type) {
+    _saveSimpleKey();
+    _increaseFlowLevel();
+    _simpleKeyAllowed = true;
+    _addCharToken(type);
+  }
+
+  /// Produces a [TokenType.flowSequenceEnd] or [TokenType.flowMappingEnd]
+  /// token.
+  void _fetchFlowCollectionEnd(TokenType type) {
+    _removeSimpleKey();
+    _decreaseFlowLevel();
+    _simpleKeyAllowed = false;
+    _addCharToken(type);
+  }
+
+  /// Produces a [TokenType.flowEntry] token.
+  void _fetchFlowEntry() {
+    _removeSimpleKey();
+    _simpleKeyAllowed = true;
+    _addCharToken(TokenType.flowEntry);
+  }
+
+  /// Produces a [TokenType.blockEntry] token.
+  void _fetchBlockEntry() {
+    if (_inBlockContext) {
+      if (!_simpleKeyAllowed) {
+        throw YamlException(
+            'Block sequence entries are not allowed here.', _scanner.emptySpan);
+      }
+
+      _rollIndent(
+          _scanner.column, TokenType.blockSequenceStart, _scanner.location);
+    } else {
+      // It is an error for the '-' indicator to occur in the flow context, but
+      // we let the Parser detect and report it because it's able to point to
+      // the context.
+    }
+
+    _removeSimpleKey();
+    _simpleKeyAllowed = true;
+    _addCharToken(TokenType.blockEntry);
+  }
+
+  /// Produces the [TokenType.key] token.
+  void _fetchKey() {
+    if (_inBlockContext) {
+      if (!_simpleKeyAllowed) {
+        throw YamlException(
+            'Mapping keys are not allowed here.', _scanner.emptySpan);
+      }
+
+      _rollIndent(
+          _scanner.column, TokenType.blockMappingStart, _scanner.location);
+    }
+
+    // Simple keys are allowed after `?` in a block context.
+    _simpleKeyAllowed = _inBlockContext;
+    _addCharToken(TokenType.key);
+  }
+
+  /// Produces the [TokenType.value] token.
+  void _fetchValue() {
+    var simpleKey = _simpleKeys.last;
+    if (simpleKey != null) {
+      // Add a [TokenType.KEY] directive before the first token of the simple
+      // key so the parser knows that it's part of a key/value pair.
+      _tokens.insert(simpleKey.tokenNumber - _tokensParsed,
+          Token(TokenType.key, simpleKey.location.pointSpan() as FileSpan));
+
+      // In the block context, we may need to add the
+      // [TokenType.BLOCK_MAPPING_START] token.
+      _rollIndent(
+          simpleKey.column, TokenType.blockMappingStart, simpleKey.location,
+          tokenNumber: simpleKey.tokenNumber);
+
+      // Remove the simple key.
+      _simpleKeys[_simpleKeys.length - 1] = null;
+
+      // A simple key cannot follow another simple key.
+      _simpleKeyAllowed = false;
+    } else if (_inBlockContext) {
+      if (!_simpleKeyAllowed) {
+        throw YamlException(
+            'Mapping values are not allowed here. Did you miss a colon '
+            'earlier?',
+            _scanner.emptySpan);
+      }
+
+      // If we're here, we've found the ':' indicator following a complex key.
+
+      _rollIndent(
+          _scanner.column, TokenType.blockMappingStart, _scanner.location);
+      _simpleKeyAllowed = true;
+    } else if (_simpleKeyAllowed) {
+      // If we're here, we've found the ':' indicator with an empty key. This
+      // behavior differs from libyaml, which disallows empty implicit keys.
+      _simpleKeyAllowed = false;
+      _addCharToken(TokenType.key);
+    }
+
+    _addCharToken(TokenType.value);
+  }
+
+  /// Adds a token with [type] to [_tokens].
+  ///
+  /// The span of the new token is the current character.
+  void _addCharToken(TokenType type) {
+    var start = _scanner.state;
+    _scanner.readCodePoint();
+    _tokens.add(Token(type, _scanner.spanFrom(start)));
+  }
+
+  /// Produces a [TokenType.alias] or [TokenType.anchor] token.
+  void _fetchAnchor({bool anchor = true}) {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanAnchor(anchor: anchor));
+  }
+
+  /// Produces a [TokenType.tag] token.
+  void _fetchTag() {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanTag());
+  }
+
+  /// Produces a [TokenType.scalar] token with style [ScalarStyle.LITERAL] or
+  /// [ScalarStyle.FOLDED].
+  void _fetchBlockScalar({bool literal = false}) {
+    _removeSimpleKey();
+    _simpleKeyAllowed = true;
+    _tokens.add(_scanBlockScalar(literal: literal));
+  }
+
+  /// Produces a [TokenType.scalar] token with style [ScalarStyle.SINGLE_QUOTED]
+  /// or [ScalarStyle.DOUBLE_QUOTED].
+  void _fetchFlowScalar({bool singleQuote = false}) {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanFlowScalar(singleQuote: singleQuote));
+  }
+
+  /// Produces a [TokenType.scalar] token with style [ScalarStyle.PLAIN].
+  void _fetchPlainScalar() {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanPlainScalar());
+  }
+
+  /// Eats whitespace and comments until the next token is found.
+  void _scanToNextToken() {
+    var afterLineBreak = false;
+    while (true) {
+      // Allow the BOM to start a line.
+      if (_scanner.column == 0) _scanner.scan('\uFEFF');
+
+      // Eat whitespace.
+      //
+      // libyaml disallows tabs after "-", "?", or ":", but the spec allows
+      // them. See section 6.2: http://yaml.org/spec/1.2/spec.html#id2778241.
+      while (_scanner.peekChar() == SP ||
+          ((!_inBlockContext || !afterLineBreak) &&
+              _scanner.peekChar() == TAB)) {
+        _scanner.readChar();
+      }
+
+      if (_scanner.peekChar() == TAB) {
+        _scanner.error('Tab characters are not allowed as indentation.',
+            length: 1);
+      }
+
+      // Eat a comment until a line break.
+      _skipComment();
+
+      // If we're at a line break, eat it.
+      if (_isBreak) {
+        _skipLine();
+
+        // In the block context, a new line may start a simple key.
+        if (_inBlockContext) _simpleKeyAllowed = true;
+        afterLineBreak = true;
+      } else {
+        // Otherwise we've found a token.
+        break;
+      }
+    }
+  }
+
+  /// Scans a [TokenType.versionDirective] or [TokenType.tagDirective] token.
+  ///
+  ///     %YAML    1.2    # a comment \n
+  ///     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  ///     %TAG    !yaml!  tag:yaml.org,2002:  \n
+  ///     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  Token? _scanDirective() {
+    var start = _scanner.state;
+
+    // Eat '%'.
+    _scanner.readChar();
+
+    Token token;
+    var name = _scanDirectiveName();
+    if (name == 'YAML') {
+      token = _scanVersionDirectiveValue(start);
+    } else if (name == 'TAG') {
+      token = _scanTagDirectiveValue(start);
+    } else {
+      warn('Warning: unknown directive.', _scanner.spanFrom(start));
+
+      // libyaml doesn't support unknown directives, but the spec says to ignore
+      // them and warn: http://yaml.org/spec/1.2/spec.html#id2781147.
+      while (!_isBreakOrEnd) {
+        _scanner.readCodePoint();
+      }
+
+      return null;
+    }
+
+    // Eat the rest of the line, including any comments.
+    _skipBlanks();
+    _skipComment();
+
+    if (!_isBreakOrEnd) {
+      throw YamlException('Expected comment or line break after directive.',
+          _scanner.spanFrom(start));
+    }
+
+    _skipLine();
+    return token;
+  }
+
+  /// Scans a directive name.
+  ///
+  ///      %YAML   1.2     # a comment \n
+  ///       ^^^^
+  ///      %TAG    !yaml!  tag:yaml.org,2002:  \n
+  ///       ^^^
+  String _scanDirectiveName() {
+    // libyaml only allows word characters in directive names, but the spec
+    // disagrees: http://yaml.org/spec/1.2/spec.html#ns-directive-name.
+    var start = _scanner.position;
+    while (_isNonSpace) {
+      _scanner.readCodePoint();
+    }
+
+    var name = _scanner.substring(start);
+    if (name.isEmpty) {
+      throw YamlException('Expected directive name.', _scanner.emptySpan);
+    } else if (!_isBlankOrEnd) {
+      throw YamlException(
+          'Unexpected character in directive name.', _scanner.emptySpan);
+    }
+
+    return name;
+  }
+
+  /// Scans the value of a version directive.
+  ///
+  ///      %YAML   1.2     # a comment \n
+  ///           ^^^^^^
+  Token _scanVersionDirectiveValue(LineScannerState start) {
+    _skipBlanks();
+
+    var major = _scanVersionDirectiveNumber();
+    _scanner.expect('.');
+    var minor = _scanVersionDirectiveNumber();
+
+    return VersionDirectiveToken(_scanner.spanFrom(start), major, minor);
+  }
+
+  /// Scans the version number of a version directive.
+  ///
+  ///      %YAML   1.2     # a comment \n
+  ///              ^
+  ///      %YAML   1.2     # a comment \n
+  ///                ^
+  int _scanVersionDirectiveNumber() {
+    var start = _scanner.position;
+    while (_isDigit) {
+      _scanner.readChar();
+    }
+
+    var number = _scanner.substring(start);
+    if (number.isEmpty) {
+      throw YamlException('Expected version number.', _scanner.emptySpan);
+    }
+
+    return int.parse(number);
+  }
+
+  /// Scans the value of a tag directive.
+  ///
+  ///      %TAG    !yaml!  tag:yaml.org,2002:  \n
+  ///          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  Token _scanTagDirectiveValue(LineScannerState start) {
+    _skipBlanks();
+
+    var handle = _scanTagHandle(directive: true);
+    if (!_isBlank) {
+      throw YamlException('Expected whitespace.', _scanner.emptySpan);
+    }
+
+    _skipBlanks();
+
+    var prefix = _scanTagUri();
+    if (!_isBlankOrEnd) {
+      throw YamlException('Expected whitespace.', _scanner.emptySpan);
+    }
+
+    return TagDirectiveToken(_scanner.spanFrom(start), handle, prefix);
+  }
+
+  /// Scans a [TokenType.anchor] token.
+  Token _scanAnchor({bool anchor = true}) {
+    var start = _scanner.state;
+
+    // Eat the indicator character.
+    _scanner.readCodePoint();
+
+    // libyaml only allows word characters in anchor names, but the spec
+    // disagrees: http://yaml.org/spec/1.2/spec.html#ns-anchor-char.
+    var startPosition = _scanner.position;
+    while (_isAnchorChar) {
+      _scanner.readCodePoint();
+    }
+    var name = _scanner.substring(startPosition);
+
+    var next = _scanner.peekChar();
+    if (name.isEmpty ||
+        (!_isBlankOrEnd &&
+            next != QUESTION &&
+            next != COLON &&
+            next != COMMA &&
+            next != RIGHT_SQUARE &&
+            next != RIGHT_CURLY &&
+            next != PERCENT &&
+            next != AT &&
+            next != GRAVE_ACCENT)) {
+      throw YamlException(
+          'Expected alphanumeric character.', _scanner.emptySpan);
+    }
+
+    if (anchor) {
+      return AnchorToken(_scanner.spanFrom(start), name);
+    } else {
+      return AliasToken(_scanner.spanFrom(start), name);
+    }
+  }
+
+  /// Scans a [TokenType.tag] token.
+  Token _scanTag() {
+    String? handle;
+    String suffix;
+    var start = _scanner.state;
+
+    // Check if the tag is in the canonical form.
+    if (_scanner.peekChar(1) == LEFT_ANGLE) {
+      // Eat '!<'.
+      _scanner.readChar();
+      _scanner.readChar();
+
+      handle = '';
+      suffix = _scanTagUri();
+
+      _scanner.expect('>');
+    } else {
+      // The tag has either the '!suffix' or the '!handle!suffix' form.
+
+      // First, try to scan a handle.
+      handle = _scanTagHandle();
+
+      if (handle.length > 1 && handle.startsWith('!') && handle.endsWith('!')) {
+        suffix = _scanTagUri(flowSeparators: false);
+      } else {
+        suffix = _scanTagUri(head: handle, flowSeparators: false);
+
+        // There was no explicit handle.
+        if (suffix.isEmpty) {
+          // This is the special '!' tag.
+          handle = null;
+          suffix = '!';
+        } else {
+          handle = '!';
+        }
+      }
+    }
+
+    // libyaml insists on whitespace after a tag, but example 7.2 indicates
+    // that it's not required: http://yaml.org/spec/1.2/spec.html#id2786720.
+
+    return TagToken(_scanner.spanFrom(start), handle, suffix);
+  }
+
+  /// Scans a tag handle.
+  String _scanTagHandle({bool directive = false}) {
+    _scanner.expect('!');
+
+    var buffer = StringBuffer('!');
+
+    // libyaml only allows word characters in tags, but the spec disagrees:
+    // http://yaml.org/spec/1.2/spec.html#ns-tag-char.
+    var start = _scanner.position;
+    while (_isTagChar) {
+      _scanner.readChar();
+    }
+    buffer.write(_scanner.substring(start));
+
+    if (_scanner.peekChar() == EXCLAMATION) {
+      buffer.writeCharCode(_scanner.readCodePoint());
+    } else {
+      // It's either the '!' tag or not really a tag handle. If it's a %TAG
+      // directive, it's an error. If it's a tag token, it must be part of a
+      // URI.
+      if (directive && buffer.toString() != '!') _scanner.expect('!');
+    }
+
+    return buffer.toString();
+  }
+
+  /// Scans a tag URI.
+  ///
+  /// [head] is the initial portion of the tag that's already been scanned.
+  /// [flowSeparators] indicates whether the tag URI can contain flow
+  /// separators.
+  String _scanTagUri({String? head, bool flowSeparators = true}) {
+    var length = head == null ? 0 : head.length;
+    var buffer = StringBuffer();
+
+    // Copy the head if needed.
+    //
+    // Note that we don't copy the leading '!' character.
+    if (length > 1) buffer.write(head!.substring(1));
+
+    // The set of characters that may appear in URI is as follows:
+    //
+    //      '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&',
+    //      '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']',
+    //      '%'.
+    //
+    // In a shorthand tag annotation, the flow separators ',', '[', and ']' are
+    // disallowed.
+    var start = _scanner.position;
+    var char = _scanner.peekChar();
+    while (_isTagChar ||
+        (flowSeparators &&
+            (char == COMMA || char == LEFT_SQUARE || char == RIGHT_SQUARE))) {
+      _scanner.readChar();
+      char = _scanner.peekChar();
+    }
+
+    // libyaml manually decodes the URL, but we don't have to do that.
+    return Uri.decodeFull(_scanner.substring(start));
+  }
+
+  /// Scans a block scalar.
+  Token _scanBlockScalar({bool literal = false}) {
+    var start = _scanner.state;
+
+    // Eat the indicator '|' or '>'.
+    _scanner.readCodePoint();
+
+    // Check for a chomping indicator.
+    var chomping = _Chomping.clip;
+    var increment = 0;
+    var char = _scanner.peekChar();
+    if (char == PLUS || char == HYPHEN) {
+      chomping = char == PLUS ? _Chomping.keep : _Chomping.strip;
+      _scanner.readCodePoint();
+
+      // Check for an indentation indicator.
+      if (_isDigit) {
+        // Check that the indentation is greater than 0.
+        if (_scanner.peekChar() == NUMBER_0) {
+          throw YamlException('0 may not be used as an indentation indicator.',
+              _scanner.spanFrom(start));
+        }
+
+        increment = _scanner.readCodePoint() - NUMBER_0;
+      }
+    } else if (_isDigit) {
+      // Do the same as above, but in the opposite order.
+      if (_scanner.peekChar() == NUMBER_0) {
+        throw YamlException('0 may not be used as an indentation indicator.',
+            _scanner.spanFrom(start));
+      }
+
+      increment = _scanner.readCodePoint() - NUMBER_0;
+
+      char = _scanner.peekChar();
+      if (char == PLUS || char == HYPHEN) {
+        chomping = char == PLUS ? _Chomping.keep : _Chomping.strip;
+        _scanner.readCodePoint();
+      }
+    }
+
+    // Eat whitespace and comments to the end of the line.
+    _skipBlanks();
+    _skipComment();
+
+    // Check if we're at the end of the line.
+    if (!_isBreakOrEnd) {
+      throw YamlException(
+          'Expected comment or line break.', _scanner.emptySpan);
+    }
+
+    _skipLine();
+
+    // If the block scalar has an explicit indentation indicator, add that to
+    // the current indentation to get the indentation level for the scalar's
+    // contents.
+    var indent = 0;
+    if (increment != 0) {
+      indent = _indent >= 0 ? _indent + increment : increment;
+    }
+
+    // Scan the leading line breaks to determine the indentation level if
+    // needed.
+    var pair = _scanBlockScalarBreaks(indent);
+    indent = pair.indent;
+    var trailingBreaks = pair.trailingBreaks;
+
+    // Scan the block scalar contents.
+    var buffer = StringBuffer();
+    var leadingBreak = '';
+    var leadingBlank = false;
+    var trailingBlank = false;
+    var end = _scanner.state;
+    while (_scanner.column == indent && !_scanner.isDone) {
+      // Check for a document indicator. libyaml doesn't do this, but the spec
+      // mandates it. See example 9.5:
+      // http://yaml.org/spec/1.2/spec.html#id2801606.
+      if (_isDocumentIndicator) break;
+
+      // We are at the beginning of a non-empty line.
+
+      // Is there trailing whitespace?
+      trailingBlank = _isBlank;
+
+      // Check if we need to fold the leading line break.
+      if (!literal &&
+          leadingBreak.isNotEmpty &&
+          !leadingBlank &&
+          !trailingBlank) {
+        // Do we need to join the lines with a space?
+        if (trailingBreaks.isEmpty) buffer.writeCharCode(SP);
+      } else {
+        buffer.write(leadingBreak);
+      }
+      leadingBreak = '';
+
+      // Append the remaining line breaks.
+      buffer.write(trailingBreaks);
+
+      // Is there leading whitespace?
+      leadingBlank = _isBlank;
+
+      var startPosition = _scanner.position;
+      while (!_isBreakOrEnd) {
+        _scanner.readCodePoint();
+      }
+      buffer.write(_scanner.substring(startPosition));
+      end = _scanner.state;
+
+      // libyaml always reads a line here, but this breaks on block scalars at
+      // the end of the document that end without newlines. See example 8.1:
+      // http://yaml.org/spec/1.2/spec.html#id2793888.
+      if (!_scanner.isDone) leadingBreak = _readLine();
+
+      // Eat the following indentation and spaces.
+      var pair = _scanBlockScalarBreaks(indent);
+      indent = pair.indent;
+      trailingBreaks = pair.trailingBreaks;
+    }
+
+    // Chomp the tail.
+    if (chomping != _Chomping.strip) buffer.write(leadingBreak);
+    if (chomping == _Chomping.keep) buffer.write(trailingBreaks);
+
+    return ScalarToken(_scanner.spanFrom(start, end), buffer.toString(),
+        literal ? ScalarStyle.LITERAL : ScalarStyle.FOLDED);
+  }
+
+  /// Scans indentation spaces and line breaks for a block scalar.
+  ///
+  /// Determines the intendation level if needed. Returns the new indentation
+  /// level and the text of the line breaks.
+  ({int indent, String trailingBreaks}) _scanBlockScalarBreaks(int indent) {
+    var maxIndent = 0;
+    var breaks = StringBuffer();
+
+    while (true) {
+      while ((indent == 0 || _scanner.column < indent) &&
+          _scanner.peekChar() == SP) {
+        _scanner.readChar();
+      }
+
+      if (_scanner.column > maxIndent) maxIndent = _scanner.column;
+
+      // libyaml throws an error here if a tab character is detected, but the
+      // spec treats tabs like any other non-space character. See example 8.2:
+      // http://yaml.org/spec/1.2/spec.html#id2794311.
+
+      if (!_isBreak) break;
+      breaks.write(_readLine());
+    }
+
+    if (indent == 0) {
+      indent = maxIndent;
+      if (indent < _indent + 1) indent = _indent + 1;
+
+      // libyaml forces indent to be at least 1 here, but that doesn't seem to
+      // be supported by the spec.
+    }
+
+    return (indent: indent, trailingBreaks: breaks.toString());
+  }
+
+  // Scans a quoted scalar.
+  Token _scanFlowScalar({bool singleQuote = false}) {
+    var start = _scanner.state;
+    var buffer = StringBuffer();
+
+    // Eat the left quote.
+    _scanner.readChar();
+
+    while (true) {
+      // Check that there are no document indicators at the beginning of the
+      // line.
+      if (_isDocumentIndicator) {
+        _scanner.error('Unexpected document indicator.');
+      }
+
+      if (_scanner.isDone) {
+        throw YamlException('Unexpected end of file.', _scanner.emptySpan);
+      }
+
+      var leadingBlanks = false;
+      while (!_isBlankOrEnd) {
+        var char = _scanner.peekChar();
+        if (singleQuote &&
+            char == SINGLE_QUOTE &&
+            _scanner.peekChar(1) == SINGLE_QUOTE) {
+          // An escaped single quote.
+          _scanner.readChar();
+          _scanner.readChar();
+          buffer.writeCharCode(SINGLE_QUOTE);
+        } else if (char == (singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE)) {
+          // The closing quote.
+          break;
+        } else if (!singleQuote && char == BACKSLASH && _isBreakAt(1)) {
+          // An escaped newline.
+          _scanner.readChar();
+          _skipLine();
+          leadingBlanks = true;
+          break;
+        } else if (!singleQuote && char == BACKSLASH) {
+          var escapeStart = _scanner.state;
+
+          // An escape sequence.
+          int? codeLength;
+          switch (_scanner.peekChar(1)) {
+            case NUMBER_0:
+              buffer.writeCharCode(NULL);
+              break;
+            case LETTER_A:
+              buffer.writeCharCode(BELL);
+              break;
+            case LETTER_B:
+              buffer.writeCharCode(BACKSPACE);
+              break;
+            case LETTER_T:
+            case TAB:
+              buffer.writeCharCode(TAB);
+              break;
+            case LETTER_N:
+              buffer.writeCharCode(LF);
+              break;
+            case LETTER_V:
+              buffer.writeCharCode(VERTICAL_TAB);
+              break;
+            case LETTER_F:
+              buffer.writeCharCode(FORM_FEED);
+              break;
+            case LETTER_R:
+              buffer.writeCharCode(CR);
+              break;
+            case LETTER_E:
+              buffer.writeCharCode(ESCAPE);
+              break;
+            case SP:
+            case DOUBLE_QUOTE:
+            case SLASH:
+            case BACKSLASH:
+              // libyaml doesn't support an escaped forward slash, but it was
+              // added in YAML 1.2. See section 5.7:
+              // http://yaml.org/spec/1.2/spec.html#id2776092
+              buffer.writeCharCode(_scanner.peekChar(1)!);
+              break;
+            case LETTER_CAP_N:
+              buffer.writeCharCode(NEL);
+              break;
+            case UNDERSCORE:
+              buffer.writeCharCode(NBSP);
+              break;
+            case LETTER_CAP_L:
+              buffer.writeCharCode(LINE_SEPARATOR);
+              break;
+            case LETTER_CAP_P:
+              buffer.writeCharCode(PARAGRAPH_SEPARATOR);
+              break;
+            case LETTER_X:
+              codeLength = 2;
+              break;
+            case LETTER_U:
+              codeLength = 4;
+              break;
+            case LETTER_CAP_U:
+              codeLength = 8;
+              break;
+            default:
+              throw YamlException(
+                  'Unknown escape character.', _scanner.spanFrom(escapeStart));
+          }
+
+          _scanner.readChar();
+          _scanner.readChar();
+
+          if (codeLength != null) {
+            var value = 0;
+            for (var i = 0; i < codeLength; i++) {
+              if (!_isHex) {
+                _scanner.readChar();
+                throw YamlException(
+                    'Expected $codeLength-digit hexidecimal number.',
+                    _scanner.spanFrom(escapeStart));
+              }
+
+              value = (value << 4) + _asHex(_scanner.readChar());
+            }
+
+            // Check the value and write the character.
+            if ((value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF) {
+              throw YamlException('Invalid Unicode character escape code.',
+                  _scanner.spanFrom(escapeStart));
+            }
+
+            buffer.writeCharCode(value);
+          }
+        } else {
+          buffer.writeCharCode(_scanner.readCodePoint());
+        }
+      }
+
+      // Check if we're at the end of a scalar.
+      if (_scanner.peekChar() == (singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE)) {
+        break;
+      }
+
+      var whitespace = StringBuffer();
+      var leadingBreak = '';
+      var trailingBreaks = StringBuffer();
+      while (_isBlank || _isBreak) {
+        if (_isBlank) {
+          // Consume a space or a tab.
+          if (!leadingBlanks) {
+            whitespace.writeCharCode(_scanner.readChar());
+          } else {
+            _scanner.readChar();
+          }
+        } else {
+          // Check if it's a first line break.
+          if (!leadingBlanks) {
+            whitespace.clear();
+            leadingBreak = _readLine();
+            leadingBlanks = true;
+          } else {
+            trailingBreaks.write(_readLine());
+          }
+        }
+      }
+
+      // Join the whitespace or fold line breaks.
+      if (leadingBlanks) {
+        if (leadingBreak.isNotEmpty && trailingBreaks.isEmpty) {
+          buffer.writeCharCode(SP);
+        } else {
+          buffer.write(trailingBreaks);
+        }
+      } else {
+        buffer.write(whitespace);
+        whitespace.clear();
+      }
+    }
+
+    // Eat the right quote.
+    _scanner.readChar();
+
+    return ScalarToken(_scanner.spanFrom(start), buffer.toString(),
+        singleQuote ? ScalarStyle.SINGLE_QUOTED : ScalarStyle.DOUBLE_QUOTED);
+  }
+
+  /// Scans a plain scalar.
+  Token _scanPlainScalar() {
+    var start = _scanner.state;
+    var end = _scanner.state;
+    var buffer = StringBuffer();
+    var leadingBreak = '';
+    var trailingBreaks = '';
+    var whitespace = StringBuffer();
+    var indent = _indent + 1;
+
+    while (true) {
+      // Check for a document indicator.
+      if (_isDocumentIndicator) break;
+
+      // Check for a comment.
+      if (_scanner.peekChar() == HASH) break;
+
+      if (_isPlainChar) {
+        // Join the whitespace or fold line breaks.
+        if (leadingBreak.isNotEmpty) {
+          if (trailingBreaks.isEmpty) {
+            buffer.writeCharCode(SP);
+          } else {
+            buffer.write(trailingBreaks);
+          }
+          leadingBreak = '';
+          trailingBreaks = '';
+        } else {
+          buffer.write(whitespace);
+          whitespace.clear();
+        }
+      }
+
+      // libyaml's notion of valid identifiers differs substantially from YAML
+      // 1.2's. We use [_isPlainChar] instead of libyaml's character here.
+      var startPosition = _scanner.position;
+      while (_isPlainChar) {
+        _scanner.readCodePoint();
+      }
+      buffer.write(_scanner.substring(startPosition));
+      end = _scanner.state;
+
+      // Is it the end?
+      if (!_isBlank && !_isBreak) break;
+
+      while (_isBlank || _isBreak) {
+        if (_isBlank) {
+          // Check for a tab character messing up the intendation.
+          if (leadingBreak.isNotEmpty &&
+              _scanner.column < indent &&
+              _scanner.peekChar() == TAB) {
+            _scanner.error('Expected a space but found a tab.', length: 1);
+          }
+
+          if (leadingBreak.isEmpty) {
+            whitespace.writeCharCode(_scanner.readChar());
+          } else {
+            _scanner.readChar();
+          }
+        } else {
+          // Check if it's a first line break.
+          if (leadingBreak.isEmpty) {
+            leadingBreak = _readLine();
+            whitespace.clear();
+          } else {
+            trailingBreaks = _readLine();
+          }
+        }
+      }
+
+      // Check the indentation level.
+      if (_inBlockContext && _scanner.column < indent) break;
+    }
+
+    // Allow a simple key after a plain scalar with leading blanks.
+    if (leadingBreak.isNotEmpty) _simpleKeyAllowed = true;
+
+    return ScalarToken(
+        _scanner.spanFrom(start, end), buffer.toString(), ScalarStyle.PLAIN);
+  }
+
+  /// Moves past the current line break, if there is one.
+  void _skipLine() {
+    var char = _scanner.peekChar();
+    if (char != CR && char != LF) return;
+    _scanner.readChar();
+    if (char == CR && _scanner.peekChar() == LF) _scanner.readChar();
+  }
+
+  // Moves past the current line break and returns a newline.
+  String _readLine() {
+    var char = _scanner.peekChar();
+
+    // libyaml supports NEL, PS, and LS characters as line separators, but this
+    // is explicitly forbidden in section 5.4 of the YAML spec.
+    if (char != CR && char != LF) {
+      throw YamlException('Expected newline.', _scanner.emptySpan);
+    }
+
+    _scanner.readChar();
+    // CR LF | CR | LF -> LF
+    if (char == CR && _scanner.peekChar() == LF) _scanner.readChar();
+    return '\n';
+  }
+
+  // Returns whether the character at [offset] is whitespace.
+  bool _isBlankAt(int offset) {
+    var char = _scanner.peekChar(offset);
+    return char == SP || char == TAB;
+  }
+
+  // Returns whether the character at [offset] is a line break.
+  bool _isBreakAt(int offset) {
+    // Libyaml considers NEL, LS, and PS to be line breaks as well, but that's
+    // contrary to the spec.
+    var char = _scanner.peekChar(offset);
+    return char == CR || char == LF;
+  }
+
+  // Returns whether the character at [offset] is whitespace or past the end of
+  // the source.
+  bool _isBlankOrEndAt(int offset) {
+    var char = _scanner.peekChar(offset);
+    return char == null ||
+        char == SP ||
+        char == TAB ||
+        char == CR ||
+        char == LF;
+  }
+
+  /// Returns whether the character at [offset] is a plain character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-plain-char(c).
+  bool _isPlainCharAt(int offset) {
+    switch (_scanner.peekChar(offset)) {
+      case COLON:
+        return _isPlainSafeAt(offset + 1);
+      case HASH:
+        var previous = _scanner.peekChar(offset - 1);
+        return previous != SP && previous != TAB;
+      default:
+        return _isPlainSafeAt(offset);
+    }
+  }
+
+  /// Returns whether the character at [offset] is a plain-safe character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-plain-safe(c).
+  bool _isPlainSafeAt(int offset) {
+    var char = _scanner.peekChar(offset);
+    return switch (char) {
+      null => false,
+      COMMA ||
+      LEFT_SQUARE ||
+      RIGHT_SQUARE ||
+      LEFT_CURLY ||
+      RIGHT_CURLY =>
+        // These characters are delimiters in a flow context and thus are only
+        // safe in a block context.
+        _inBlockContext,
+      SP || TAB || LF || CR || BOM => false,
+      NEL => true,
+      _ => _isStandardCharacterAt(offset)
+    };
+  }
+
+  bool _isStandardCharacterAt(int offset) {
+    var first = _scanner.peekChar(offset);
+    if (first == null) return false;
+
+    if (isHighSurrogate(first)) {
+      var next = _scanner.peekChar(offset + 1);
+      // A surrogate pair encodes code points from U+010000 to U+10FFFF, so it
+      // must be a standard character.
+      return next != null && isLowSurrogate(next);
+    }
+
+    return _isStandardCharacter(first);
+  }
+
+  bool _isStandardCharacter(int char) =>
+      (char >= 0x0020 && char <= 0x007E) ||
+      (char >= 0x00A0 && char <= 0xD7FF) ||
+      (char >= 0xE000 && char <= 0xFFFD);
+
+  /// Returns the hexidecimal value of [char].
+  int _asHex(int char) {
+    if (char <= NUMBER_9) return char - NUMBER_0;
+    if (char <= LETTER_CAP_F) return 10 + char - LETTER_CAP_A;
+    return 10 + char - LETTER_A;
+  }
+
+  /// Moves the scanner past any blank characters.
+  void _skipBlanks() {
+    while (_isBlank) {
+      _scanner.readChar();
+    }
+  }
+
+  /// Moves the scanner past a comment, if one starts at the current position.
+  void _skipComment() {
+    if (_scanner.peekChar() != HASH) return;
+    while (!_isBreakOrEnd) {
+      _scanner.readChar();
+    }
+  }
+
+  /// Reports a [YamlException] to [_errorListener] if [_recover] is true,
+  /// otherwise throws the exception.
+  void _reportError(YamlException exception) {
+    if (!_recover) {
+      throw exception;
+    }
+    _errorListener?.onError(exception);
+  }
+}
+
+/// A record of the location of a potential simple key.
+class _SimpleKey {
+  /// The index of the token that begins the simple key.
+  ///
+  /// This is the index relative to all tokens emitted, rather than relative to
+  /// [location].
+  final int tokenNumber;
+
+  /// The source location of the beginning of the simple key.
+  ///
+  /// This is used for error reporting and for determining when a simple key is
+  /// no longer on the current line.
+  final SourceLocation location;
+
+  /// The line on which the key appears.
+  ///
+  /// We could get this from [location], but that requires a binary search
+  /// whereas this is O(1).
+  final int line;
+
+  /// The column on which the key appears.
+  ///
+  /// We could get this from [location], but that requires a binary search
+  /// whereas this is O(1).
+  final int column;
+
+  /// Whether this key must exist for the document to be scanned.
+  final bool required;
+
+  _SimpleKey(
+    this.tokenNumber,
+    this.line,
+    this.column,
+    this.location, {
+    required this.required,
+  });
+}
+
+/// The ways to handle trailing whitespace for a block scalar.
+///
+/// See http://yaml.org/spec/1.2/spec.html#id2794534.
+enum _Chomping {
+  /// All trailing whitespace is discarded.
+  strip,
+
+  /// A single trailing newline is retained.
+  clip,
+
+  /// All trailing whitespace is preserved.
+  keep
+}
diff --git a/pkgs/yaml/lib/src/style.dart b/pkgs/yaml/lib/src/style.dart
new file mode 100644
index 0000000..96c3b94
--- /dev/null
+++ b/pkgs/yaml/lib/src/style.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: constant_identifier_names
+
+import 'yaml_node.dart';
+
+/// An enum of source scalar styles.
+class ScalarStyle {
+  /// No source style was specified.
+  ///
+  /// This usually indicates a scalar constructed with [YamlScalar.wrap].
+  static const ANY = ScalarStyle._('ANY');
+
+  /// The plain scalar style, unquoted and without a prefix.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#style/flow/plain.
+  static const PLAIN = ScalarStyle._('PLAIN');
+
+  /// The literal scalar style, with a `|` prefix.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2795688.
+  static const LITERAL = ScalarStyle._('LITERAL');
+
+  /// The folded scalar style, with a `>` prefix.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2796251.
+  static const FOLDED = ScalarStyle._('FOLDED');
+
+  /// The single-quoted scalar style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#style/flow/single-quoted.
+  static const SINGLE_QUOTED = ScalarStyle._('SINGLE_QUOTED');
+
+  /// The double-quoted scalar style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#style/flow/double-quoted.
+  static const DOUBLE_QUOTED = ScalarStyle._('DOUBLE_QUOTED');
+
+  final String name;
+
+  /// Whether this is a quoted style ([SINGLE_QUOTED] or [DOUBLE_QUOTED]).
+  bool get isQuoted => this == SINGLE_QUOTED || this == DOUBLE_QUOTED;
+
+  const ScalarStyle._(this.name);
+
+  @override
+  String toString() => name;
+}
+
+/// An enum of collection styles.
+class CollectionStyle {
+  /// No source style was specified.
+  ///
+  /// This usually indicates a collection constructed with [YamlList.wrap] or
+  /// [YamlMap.wrap].
+  static const ANY = CollectionStyle._('ANY');
+
+  /// The indentation-based block style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2797293.
+  static const BLOCK = CollectionStyle._('BLOCK');
+
+  /// The delimiter-based block style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2790088.
+  static const FLOW = CollectionStyle._('FLOW');
+
+  final String name;
+
+  const CollectionStyle._(this.name);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/yaml/lib/src/token.dart b/pkgs/yaml/lib/src/token.dart
new file mode 100644
index 0000000..7d5d6bc
--- /dev/null
+++ b/pkgs/yaml/lib/src/token.dart
@@ -0,0 +1,158 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'scanner.dart';
+import 'style.dart';
+
+/// A token emitted by a [Scanner].
+class Token {
+  final TokenType type;
+  final FileSpan span;
+
+  Token(this.type, this.span);
+
+  @override
+  String toString() => type.toString();
+}
+
+/// A token representing a `%YAML` directive.
+class VersionDirectiveToken implements Token {
+  @override
+  TokenType get type => TokenType.versionDirective;
+  @override
+  final FileSpan span;
+
+  /// The declared major version of the document.
+  final int major;
+
+  /// The declared minor version of the document.
+  final int minor;
+
+  VersionDirectiveToken(this.span, this.major, this.minor);
+
+  @override
+  String toString() => 'VERSION_DIRECTIVE $major.$minor';
+}
+
+/// A token representing a `%TAG` directive.
+class TagDirectiveToken implements Token {
+  @override
+  TokenType get type => TokenType.tagDirective;
+  @override
+  final FileSpan span;
+
+  /// The tag handle used in the document.
+  final String handle;
+
+  /// The tag prefix that the handle maps to.
+  final String prefix;
+
+  TagDirectiveToken(this.span, this.handle, this.prefix);
+
+  @override
+  String toString() => 'TAG_DIRECTIVE $handle $prefix';
+}
+
+/// A token representing an anchor (`&foo`).
+class AnchorToken implements Token {
+  @override
+  TokenType get type => TokenType.anchor;
+  @override
+  final FileSpan span;
+
+  final String name;
+
+  AnchorToken(this.span, this.name);
+
+  @override
+  String toString() => 'ANCHOR $name';
+}
+
+/// A token representing an alias (`*foo`).
+class AliasToken implements Token {
+  @override
+  TokenType get type => TokenType.alias;
+  @override
+  final FileSpan span;
+
+  final String name;
+
+  AliasToken(this.span, this.name);
+
+  @override
+  String toString() => 'ALIAS $name';
+}
+
+/// A token representing a tag (`!foo`).
+class TagToken implements Token {
+  @override
+  TokenType get type => TokenType.tag;
+  @override
+  final FileSpan span;
+
+  /// The tag handle for named tags.
+  final String? handle;
+
+  /// The tag suffix.
+  final String suffix;
+
+  TagToken(this.span, this.handle, this.suffix);
+
+  @override
+  String toString() => 'TAG $handle $suffix';
+}
+
+/// A scalar value.
+class ScalarToken implements Token {
+  @override
+  TokenType get type => TokenType.scalar;
+  @override
+  final FileSpan span;
+
+  /// The unparsed contents of the value..
+  final String value;
+
+  /// The style of the scalar in the original source.
+  final ScalarStyle style;
+
+  ScalarToken(this.span, this.value, this.style);
+
+  @override
+  String toString() => 'SCALAR $style "$value"';
+}
+
+/// The types of [Token] objects.
+enum TokenType {
+  streamStart,
+  streamEnd,
+
+  versionDirective,
+  tagDirective,
+  documentStart,
+  documentEnd,
+
+  blockSequenceStart,
+  blockMappingStart,
+  blockEnd,
+
+  flowSequenceStart,
+  flowSequenceEnd,
+  flowMappingStart,
+  flowMappingEnd,
+
+  blockEntry,
+  flowEntry,
+  key,
+  value,
+
+  alias,
+  anchor,
+  tag,
+  scalar
+}
diff --git a/pkgs/yaml/lib/src/utils.dart b/pkgs/yaml/lib/src/utils.dart
new file mode 100644
index 0000000..0dc132f
--- /dev/null
+++ b/pkgs/yaml/lib/src/utils.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2013, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+/// Print a warning.
+///
+/// If [span] is passed, associates the warning with that span.
+void warn(String message, [SourceSpan? span]) =>
+    yamlWarningCallback(message, span);
+
+/// A callback for emitting a warning.
+///
+/// [message] is the text of the warning. If [span] is passed, it's the portion
+/// of the document that the warning is associated with and should be included
+/// in the printed warning.
+typedef YamlWarningCallback = void Function(String message, [SourceSpan? span]);
+
+/// A callback for emitting a warning.
+///
+/// In a very few cases, the YAML spec indicates that an implementation should
+/// emit a warning. To do so, it calls this callback. The default implementation
+/// prints a message using [print].
+// ignore: prefer_function_declarations_over_variables
+YamlWarningCallback yamlWarningCallback = (message, [SourceSpan? span]) {
+  // TODO(nweiz): Print to stderr with color when issue 6943 is fixed and
+  // dart:io is available.
+  if (span != null) message = span.message(message);
+  print(message);
+};
+
+/// Whether [codeUnit] is a UTF-16 high surrogate.
+bool isHighSurrogate(int codeUnit) => codeUnit >>> 10 == 0x36;
+
+/// Whether [codeUnit] is a UTF-16 low surrogate.
+bool isLowSurrogate(int codeUnit) => codeUnit >>> 10 == 0x37;
diff --git a/pkgs/yaml/lib/src/yaml_document.dart b/pkgs/yaml/lib/src/yaml_document.dart
new file mode 100644
index 0000000..da6aa1e
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_document.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection';
+
+import 'package:source_span/source_span.dart';
+
+import 'yaml_node.dart';
+
+/// A YAML document, complete with metadata.
+class YamlDocument {
+  /// The contents of the document.
+  final YamlNode contents;
+
+  /// The span covering the entire document.
+  final SourceSpan span;
+
+  /// The version directive for the document, if any.
+  final VersionDirective? versionDirective;
+
+  /// The tag directives for the document.
+  final List<TagDirective> tagDirectives;
+
+  /// Whether the beginning of the document was implicit (versus explicit via
+  /// `===`).
+  final bool startImplicit;
+
+  /// Whether the end of the document was implicit (versus explicit via `...`).
+  final bool endImplicit;
+
+  /// Users of the library should not use this constructor.
+  YamlDocument.internal(this.contents, this.span, this.versionDirective,
+      List<TagDirective> tagDirectives,
+      {this.startImplicit = false, this.endImplicit = false})
+      : tagDirectives = UnmodifiableListView(tagDirectives);
+
+  @override
+  String toString() => contents.toString();
+}
+
+/// A directive indicating which version of YAML a document was written to.
+class VersionDirective {
+  /// The major version number.
+  final int major;
+
+  /// The minor version number.
+  final int minor;
+
+  VersionDirective(this.major, this.minor);
+
+  @override
+  String toString() => '%YAML $major.$minor';
+}
+
+/// A directive describing a custom tag handle.
+class TagDirective {
+  /// The handle for use in the document.
+  final String handle;
+
+  /// The prefix that the handle maps to.
+  final String prefix;
+
+  TagDirective(this.handle, this.prefix);
+
+  @override
+  String toString() => '%TAG $handle $prefix';
+}
diff --git a/pkgs/yaml/lib/src/yaml_exception.dart b/pkgs/yaml/lib/src/yaml_exception.dart
new file mode 100644
index 0000000..7aa5389
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_exception.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2013, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+/// An error thrown by the YAML processor.
+class YamlException extends SourceSpanFormatException {
+  YamlException(super.message, super.span);
+}
diff --git a/pkgs/yaml/lib/src/yaml_node.dart b/pkgs/yaml/lib/src/yaml_node.dart
new file mode 100644
index 0000000..bd17b6c
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_node.dart
@@ -0,0 +1,191 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection' as collection;
+
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+
+import 'event.dart';
+import 'null_span.dart';
+import 'style.dart';
+import 'yaml_node_wrapper.dart';
+
+/// An interface for parsed nodes from a YAML source tree.
+///
+/// [YamlMap]s and [YamlList]s implement this interface in addition to the
+/// normal [Map] and [List] interfaces, so any maps and lists will be
+/// [YamlNode]s regardless of how they're accessed.
+///
+/// Scalars values like strings and numbers, on the other hand, don't have this
+/// interface by default. Instead, they can be accessed as [YamlScalar]s via
+/// [YamlMap.nodes] or [YamlList.nodes].
+abstract class YamlNode {
+  /// The source span for this node.
+  ///
+  /// [SourceSpan.message] can be used to produce a human-friendly message about
+  /// this node.
+  SourceSpan get span => _span;
+  SourceSpan _span;
+
+  YamlNode._(this._span);
+
+  /// The inner value of this node.
+  ///
+  /// For [YamlScalar]s, this will return the wrapped value. For [YamlMap] and
+  /// [YamlList], it will return `this`, since they already implement [Map] and
+  /// [List], respectively.
+  dynamic get value;
+}
+
+/// A read-only [Map] parsed from YAML.
+class YamlMap extends YamlNode with collection.MapMixin, UnmodifiableMapMixin {
+  /// A view of `this` where the keys and values are guaranteed to be
+  /// [YamlNode]s.
+  ///
+  /// The key type is `dynamic` to allow values to be accessed using
+  /// non-[YamlNode] keys, but [Map.keys] and [Map.forEach] will always expose
+  /// them as [YamlNode]s. For example, for `{"foo": [1, 2, 3]}` [nodes] will be
+  /// a map from a [YamlScalar] to a [YamlList], but since the key type is
+  /// `dynamic` `map.nodes["foo"]` will still work.
+  final Map<dynamic, YamlNode> nodes;
+
+  /// The style used for the map in the original document.
+  final CollectionStyle style;
+
+  @override
+  Map get value => this;
+
+  @override
+  Iterable get keys => nodes.keys.map((node) => (node as YamlNode).value);
+
+  /// Creates an empty YamlMap.
+  ///
+  /// This map's [span] won't have useful location information. However, it will
+  /// have a reasonable implementation of [SourceSpan.message]. If [sourceUrl]
+  /// is passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlMap({Object? sourceUrl}) => YamlMapWrapper(const {}, sourceUrl);
+
+  /// Wraps a Dart map so that it can be accessed (recursively) like a
+  /// [YamlMap].
+  ///
+  /// Any [SourceSpan]s returned by this map or its children will be dummies
+  /// without useful location information. However, they will have a reasonable
+  /// implementation of [SourceSpan.message]. If [sourceUrl] is
+  /// passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlMap.wrap(Map dartMap,
+          {Object? sourceUrl, CollectionStyle style = CollectionStyle.ANY}) =>
+      YamlMapWrapper(dartMap, sourceUrl, style: style);
+
+  /// Users of the library should not use this constructor.
+  YamlMap.internal(Map<dynamic, YamlNode> nodes, super.span, this.style)
+      : nodes = UnmodifiableMapView<dynamic, YamlNode>(nodes),
+        super._();
+
+  @override
+  dynamic operator [](Object? key) => nodes[key]?.value;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// A read-only [List] parsed from YAML.
+class YamlList extends YamlNode with collection.ListMixin {
+  final List<YamlNode> nodes;
+
+  /// The style used for the list in the original document.
+  final CollectionStyle style;
+
+  @override
+  List get value => this;
+
+  @override
+  int get length => nodes.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  /// Creates an empty YamlList.
+  ///
+  /// This list's [span] won't have useful location information. However, it
+  /// will have a reasonable implementation of [SourceSpan.message]. If
+  /// [sourceUrl] is passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlList({Object? sourceUrl}) => YamlListWrapper(const [], sourceUrl);
+
+  /// Wraps a Dart list so that it can be accessed (recursively) like a
+  /// [YamlList].
+  ///
+  /// Any [SourceSpan]s returned by this list or its children will be dummies
+  /// without useful location information. However, they will have a reasonable
+  /// implementation of [SourceSpan.message]. If [sourceUrl] is
+  /// passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlList.wrap(List dartList,
+          {Object? sourceUrl, CollectionStyle style = CollectionStyle.ANY}) =>
+      YamlListWrapper(dartList, sourceUrl, style: style);
+
+  /// Users of the library should not use this constructor.
+  YamlList.internal(List<YamlNode> nodes, super.span, this.style)
+      : nodes = UnmodifiableListView<YamlNode>(nodes),
+        super._();
+
+  @override
+  dynamic operator [](int index) => nodes[index].value;
+
+  @override
+  void operator []=(int index, Object? value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+}
+
+/// A wrapped scalar value parsed from YAML.
+class YamlScalar extends YamlNode {
+  @override
+  final dynamic value;
+
+  /// The style used for the scalar in the original document.
+  final ScalarStyle style;
+
+  /// Wraps a Dart value in a [YamlScalar].
+  ///
+  /// This scalar's [span] won't have useful location information. However, it
+  /// will have a reasonable implementation of [SourceSpan.message]. If
+  /// [sourceUrl] is passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  YamlScalar.wrap(this.value, {Object? sourceUrl, this.style = ScalarStyle.ANY})
+      : super._(NullSpan(sourceUrl)) {
+    ArgumentError.checkNotNull(style, 'style');
+  }
+
+  /// Users of the library should not use this constructor.
+  YamlScalar.internal(this.value, ScalarEvent scalar)
+      : style = scalar.style,
+        super._(scalar.span);
+
+  /// Users of the library should not use this constructor.
+  YamlScalar.internalWithSpan(this.value, SourceSpan span)
+      : style = ScalarStyle.ANY,
+        super._(span);
+
+  @override
+  String toString() => value.toString();
+}
+
+/// Sets the source span of a [YamlNode].
+///
+/// This method is not exposed publicly.
+void setSpan(YamlNode node, SourceSpan span) {
+  node._span = span;
+}
diff --git a/pkgs/yaml/lib/src/yaml_node_wrapper.dart b/pkgs/yaml/lib/src/yaml_node_wrapper.dart
new file mode 100644
index 0000000..5250844
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_node_wrapper.dart
@@ -0,0 +1,189 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart' as pkg_collection;
+import 'package:source_span/source_span.dart';
+
+import 'null_span.dart';
+import 'style.dart';
+import 'yaml_node.dart';
+
+/// A wrapper that makes a normal Dart map behave like a [YamlMap].
+class YamlMapWrapper extends MapBase
+    with pkg_collection.UnmodifiableMapMixin
+    implements YamlMap {
+  @override
+  final CollectionStyle style;
+
+  final Map _dartMap;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  final Map<dynamic, YamlNode> nodes;
+
+  @override
+  Map get value => this;
+
+  @override
+  Iterable get keys => _dartMap.keys;
+
+  YamlMapWrapper(Map dartMap, Object? sourceUrl,
+      {CollectionStyle style = CollectionStyle.ANY})
+      : this._(dartMap, NullSpan(sourceUrl), style: style);
+
+  YamlMapWrapper._(Map dartMap, this.span, {this.style = CollectionStyle.ANY})
+      : _dartMap = dartMap,
+        nodes = _YamlMapNodes(dartMap, span) {
+    ArgumentError.checkNotNull(style, 'style');
+  }
+
+  @override
+  dynamic operator [](Object? key) {
+    var value = _dartMap[key];
+    if (value is Map) return YamlMapWrapper._(value, span);
+    if (value is List) return YamlListWrapper._(value, span);
+    return value;
+  }
+
+  @override
+  int get hashCode => _dartMap.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is YamlMapWrapper && other._dartMap == _dartMap;
+}
+
+/// The implementation of [YamlMapWrapper.nodes] as a wrapper around the Dart
+/// map.
+class _YamlMapNodes extends MapBase<dynamic, YamlNode>
+    with pkg_collection.UnmodifiableMapMixin<dynamic, YamlNode> {
+  final Map _dartMap;
+
+  final SourceSpan _span;
+
+  @override
+  Iterable get keys =>
+      _dartMap.keys.map((key) => YamlScalar.internalWithSpan(key, _span));
+
+  _YamlMapNodes(this._dartMap, this._span);
+
+  @override
+  YamlNode? operator [](Object? key) {
+    // Use "as" here because key being assigned to invalidates type propagation.
+    if (key is YamlScalar) key = key.value;
+    if (!_dartMap.containsKey(key)) return null;
+    return _nodeForValue(_dartMap[key], _span);
+  }
+
+  @override
+  int get hashCode => _dartMap.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is _YamlMapNodes && other._dartMap == _dartMap;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// A wrapper that makes a normal Dart list behave like a [YamlList].
+class YamlListWrapper extends ListBase implements YamlList {
+  @override
+  final CollectionStyle style;
+
+  final List _dartList;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  final List<YamlNode> nodes;
+
+  @override
+  List get value => this;
+
+  @override
+  int get length => _dartList.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  YamlListWrapper(List dartList, Object? sourceUrl,
+      {CollectionStyle style = CollectionStyle.ANY})
+      : this._(dartList, NullSpan(sourceUrl), style: style);
+
+  YamlListWrapper._(List dartList, this.span,
+      {this.style = CollectionStyle.ANY})
+      : _dartList = dartList,
+        nodes = _YamlListNodes(dartList, span) {
+    ArgumentError.checkNotNull(style, 'style');
+  }
+
+  @override
+  dynamic operator [](int index) {
+    var value = _dartList[index];
+    if (value is Map) return YamlMapWrapper._(value, span);
+    if (value is List) return YamlListWrapper._(value, span);
+    return value;
+  }
+
+  @override
+  void operator []=(int index, Object? value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  @override
+  int get hashCode => _dartList.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is YamlListWrapper && other._dartList == _dartList;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// The implementation of [YamlListWrapper.nodes] as a wrapper around the Dart
+/// list.
+class _YamlListNodes extends ListBase<YamlNode> {
+  final List _dartList;
+
+  final SourceSpan _span;
+
+  @override
+  int get length => _dartList.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  _YamlListNodes(this._dartList, this._span);
+
+  @override
+  YamlNode operator [](int index) => _nodeForValue(_dartList[index], _span);
+
+  @override
+  void operator []=(int index, Object? value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  @override
+  int get hashCode => _dartList.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is _YamlListNodes && other._dartList == _dartList;
+}
+
+YamlNode _nodeForValue(Object? value, SourceSpan span) {
+  if (value is Map) return YamlMapWrapper._(value, span);
+  if (value is List) return YamlListWrapper._(value, span);
+  return YamlScalar.internalWithSpan(value, span);
+}
diff --git a/pkgs/yaml/lib/yaml.dart b/pkgs/yaml/lib/yaml.dart
new file mode 100644
index 0000000..26cc9b8
--- /dev/null
+++ b/pkgs/yaml/lib/yaml.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'src/error_listener.dart';
+import 'src/loader.dart';
+import 'src/style.dart';
+import 'src/yaml_document.dart';
+import 'src/yaml_exception.dart';
+import 'src/yaml_node.dart';
+
+export 'src/style.dart';
+export 'src/utils.dart' show YamlWarningCallback, yamlWarningCallback;
+export 'src/yaml_document.dart';
+export 'src/yaml_exception.dart';
+export 'src/yaml_node.dart' hide setSpan;
+
+/// Loads a single document from a YAML string.
+///
+/// If the string contains more than one document, this throws a
+/// [YamlException]. In future releases, this will become an [ArgumentError].
+///
+/// The return value is mostly normal Dart objects. However, since YAML mappings
+/// support some key types that the default Dart map implementation doesn't
+/// (NaN, lists, and maps), all maps in the returned document are [YamlMap]s.
+/// These have a few small behavioral differences from the default Map
+/// implementation; for details, see the [YamlMap] class.
+///
+/// If [sourceUrl] is passed, it's used as the URL from which the YAML
+/// originated for error reporting.
+///
+/// If [recover] is true, will attempt to recover from parse errors and may
+/// return invalid or synthetic nodes. If [errorListener] is also supplied, its
+/// onError method will be called for each error recovered from. It is not valid
+/// to provide [errorListener] if [recover] is false.
+dynamic loadYaml(String yaml,
+        {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) =>
+    loadYamlNode(yaml,
+            sourceUrl: sourceUrl,
+            recover: recover,
+            errorListener: errorListener)
+        .value;
+
+/// Loads a single document from a YAML string as a [YamlNode].
+///
+/// This is just like [loadYaml], except that where [loadYaml] would return a
+/// normal Dart value this returns a [YamlNode] instead. This allows the caller
+/// to be confident that the return value will always be a [YamlNode].
+YamlNode loadYamlNode(String yaml,
+        {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) =>
+    loadYamlDocument(yaml,
+            sourceUrl: sourceUrl,
+            recover: recover,
+            errorListener: errorListener)
+        .contents;
+
+/// Loads a single document from a YAML string as a [YamlDocument].
+///
+/// This is just like [loadYaml], except that where [loadYaml] would return a
+/// normal Dart value this returns a [YamlDocument] instead. This allows the
+/// caller to access document metadata.
+YamlDocument loadYamlDocument(String yaml,
+    {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) {
+  var loader = Loader(yaml,
+      sourceUrl: sourceUrl, recover: recover, errorListener: errorListener);
+  var document = loader.load();
+  if (document == null) {
+    return YamlDocument.internal(YamlScalar.internalWithSpan(null, loader.span),
+        loader.span, null, const []);
+  }
+
+  var nextDocument = loader.load();
+  if (nextDocument != null) {
+    throw YamlException('Only expected one document.', nextDocument.span);
+  }
+
+  return document;
+}
+
+/// 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
+/// (NaN, lists, and maps), all maps in the returned document are [YamlMap]s.
+/// These have a few small behavioral differences from the default Map
+/// implementation; for details, see the [YamlMap] class.
+///
+/// If [sourceUrl] is passed, it's used as the URL from which the YAML
+/// originated for error reporting.
+YamlList loadYamlStream(String yaml, {Uri? sourceUrl}) {
+  var loader = Loader(yaml, sourceUrl: sourceUrl);
+
+  var documents = <YamlDocument>[];
+  var document = loader.load();
+  while (document != null) {
+    documents.add(document);
+    document = loader.load();
+  }
+
+  // TODO(jmesserly): the type on the `document` parameter is a workaround for:
+  // https://github.com/dart-lang/dev_compiler/issues/203
+  return YamlList.internal(
+      documents.map((YamlDocument document) => document.contents).toList(),
+      loader.span,
+      CollectionStyle.ANY);
+}
+
+/// Loads a stream of documents from a YAML string.
+///
+/// This is like [loadYamlStream], except that it returns [YamlDocument]s with
+/// metadata wrapping the document contents.
+List<YamlDocument> loadYamlDocuments(String yaml, {Uri? sourceUrl}) {
+  var loader = Loader(yaml, sourceUrl: sourceUrl);
+
+  var documents = <YamlDocument>[];
+  var document = loader.load();
+  while (document != null) {
+    documents.add(document);
+    document = loader.load();
+  }
+
+  return documents;
+}
diff --git a/pkgs/yaml/pubspec.yaml b/pkgs/yaml/pubspec.yaml
new file mode 100644
index 0000000..be7d165
--- /dev/null
+++ b/pkgs/yaml/pubspec.yaml
@@ -0,0 +1,20 @@
+name: yaml
+version: 3.1.3-wip
+description: A parser for YAML, a human-friendly data serialization standard
+repository: https://github.com/dart-lang/yaml
+topics:
+ - yaml
+ - config-format
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  collection: ^1.15.0
+  source_span: ^1.8.0
+  string_scanner: ^1.2.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  path: ^1.8.0
+  test: ^1.16.6
diff --git a/pkgs/yaml/test/span_test.dart b/pkgs/yaml/test/span_test.dart
new file mode 100644
index 0000000..03b7f9c
--- /dev/null
+++ b/pkgs/yaml/test/span_test.dart
@@ -0,0 +1,173 @@
+// Copyright (c) 2019, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+void _expectSpan(SourceSpan source, String expected) {
+  final result = source.message('message');
+  printOnFailure("r'''\n$result'''");
+
+  expect(result, expected);
+}
+
+void main() {
+  late YamlMap yaml;
+
+  setUpAll(() {
+    yaml = loadYaml(const JsonEncoder.withIndent(' ').convert({
+      'num': 42,
+      'nested': {
+        'null': null,
+        'num': 42,
+      },
+      'null': null,
+    })) as YamlMap;
+  });
+
+  test('first root key', () {
+    _expectSpan(
+      yaml.nodes['num']!.span,
+      r'''
+line 2, column 9: message
+  ╷
+2 │  "num": 42,
+  │         ^^
+  ╵''',
+    );
+  });
+
+  test('first root key', () {
+    _expectSpan(
+      yaml.nodes['null']!.span,
+      r'''
+line 7, column 10: message
+  ╷
+7 │  "null": null
+  │          ^^^^
+  ╵''',
+    );
+  });
+
+  group('nested', () {
+    late YamlMap nestedMap;
+
+    setUpAll(() {
+      nestedMap = yaml.nodes['nested'] as YamlMap;
+    });
+
+    test('first root key', () {
+      _expectSpan(
+        nestedMap.nodes['null']!.span,
+        r'''
+line 4, column 11: message
+  ╷
+4 │   "null": null,
+  │           ^^^^
+  ╵''',
+      );
+    });
+
+    test('first root key', () {
+      _expectSpan(
+        nestedMap.nodes['num']!.span,
+        r'''
+line 5, column 10: message
+  ╷
+5 │     "num": 42
+  │ ┌──────────^
+6 │ │  },
+  │ └─^
+  ╵''',
+      );
+    });
+  });
+
+  group('block', () {
+    late YamlList list, nestedList;
+
+    setUpAll(() {
+      const yamlStr = '''
+- foo
+- 
+  - one
+  - 
+  - three
+  - 
+  - five
+  -
+- 
+  a : b
+  c : d
+- bar
+''';
+
+      list = loadYaml(yamlStr) as YamlList;
+      nestedList = list.nodes[1] as YamlList;
+    });
+
+    test('root nodes span', () {
+      _expectSpan(list.nodes[0].span, r'''
+line 1, column 3: message
+  ╷
+1 │ - foo
+  │   ^^^
+  ╵''');
+
+      _expectSpan(list.nodes[1].span, r'''
+line 3, column 3: message
+  ╷
+3 │ ┌   - one
+4 │ │   - 
+5 │ │   - three
+6 │ │   - 
+7 │ │   - five
+8 │ └   -
+  ╵''');
+
+      _expectSpan(list.nodes[2].span, r'''
+line 10, column 3: message
+   ╷
+10 │ ┌   a : b
+11 │ └   c : d
+   ╵''');
+
+      _expectSpan(list.nodes[3].span, r'''
+line 12, column 3: message
+   ╷
+12 │ - bar
+   │   ^^^
+   ╵''');
+    });
+
+    test('null nodes span', () {
+      _expectSpan(nestedList.nodes[1].span, r'''
+line 4, column 3: message
+  ╷
+4 │   - 
+  │   ^
+  ╵''');
+
+      _expectSpan(nestedList.nodes[3].span, r'''
+line 6, column 3: message
+  ╷
+6 │   - 
+  │   ^
+  ╵''');
+
+      _expectSpan(nestedList.nodes[5].span, r'''
+line 8, column 3: message
+  ╷
+8 │   -
+  │   ^
+  ╵''');
+    });
+  });
+}
diff --git a/pkgs/yaml/test/utils.dart b/pkgs/yaml/test/utils.dart
new file mode 100644
index 0000000..372440a
--- /dev/null
+++ b/pkgs/yaml/test/utils.dart
@@ -0,0 +1,95 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:test/test.dart';
+import 'package:yaml/src/equality.dart' as equality;
+import 'package:yaml/yaml.dart';
+
+/// A matcher that validates that a closure or Future throws a [YamlException].
+final Matcher throwsYamlException = throwsA(isA<YamlException>());
+
+/// Returns a matcher that asserts that the value equals [expected].
+///
+/// This handles recursive loops and considers `NaN` to equal itself.
+Matcher deepEquals(Object? expected) => predicate(
+    (actual) => equality.deepEquals(actual, expected), 'equals $expected');
+
+/// Constructs a new yaml.YamlMap, optionally from a normal Map.
+Map deepEqualsMap([Map? from]) {
+  var map = equality.deepEqualsMap<Object?, Object?>();
+  if (from != null) map.addAll(from);
+  return map;
+}
+
+/// Asserts that an error has the given message and starts at the given line/col.
+void expectErrorAtLineCol(
+    YamlException error, String message, int line, int col) {
+  expect(error.message, equals(message));
+  expect(error.span!.start.line, equals(line));
+  expect(error.span!.start.column, equals(col));
+}
+
+/// Asserts that a string containing a single YAML document produces a given
+/// value when loaded.
+void expectYamlLoads(Object? expected, String source) {
+  var actual = loadYaml(cleanUpLiteral(source));
+  expect(actual, deepEquals(expected));
+}
+
+/// Asserts that a string containing a stream of YAML documents produces a given
+/// list of values when loaded.
+void expectYamlStreamLoads(List expected, String source) {
+  var actual = loadYamlStream(cleanUpLiteral(source));
+  expect(actual, deepEquals(expected));
+}
+
+/// Asserts that a string containing a single YAML document throws a
+/// [YamlException].
+void expectYamlFails(String source) {
+  expect(() => loadYaml(cleanUpLiteral(source)), throwsYamlException);
+}
+
+/// Removes eight spaces of leading indentation from a multiline string.
+///
+/// Note that this is very sensitive to how the literals are styled. They should
+/// be:
+///     '''
+///     Text starts on own line. Lines up with subsequent lines.
+///     Lines are indented exactly 8 characters from the left margin.
+///     Close is on the same line.'''
+///
+/// This does nothing if text is only a single line.
+String cleanUpLiteral(String text) {
+  var lines = text.split('\n');
+  if (lines.length <= 1) return text;
+
+  for (var j = 0; j < lines.length; j++) {
+    if (lines[j].length > 8) {
+      lines[j] = lines[j].substring(8, lines[j].length);
+    } else {
+      lines[j] = '';
+    }
+  }
+
+  return lines.join('\n');
+}
+
+/// Indents each line of [text] so that, when passed to [cleanUpLiteral], it
+/// will produce output identical to [text].
+///
+/// This is useful for literals that need to include newlines but can't be
+/// conveniently represented as multi-line strings.
+String indentLiteral(String text) {
+  var lines = text.split('\n');
+  if (lines.length <= 1) return text;
+
+  for (var i = 0; i < lines.length; i++) {
+    lines[i] = '        ${lines[i]}';
+  }
+
+  return lines.join('\n');
+}
diff --git a/pkgs/yaml/test/yaml_node_wrapper_test.dart b/pkgs/yaml/test/yaml_node_wrapper_test.dart
new file mode 100644
index 0000000..637b778
--- /dev/null
+++ b/pkgs/yaml/test/yaml_node_wrapper_test.dart
@@ -0,0 +1,235 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: avoid_dynamic_calls
+
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+void main() {
+  test('YamlMap() with no sourceUrl', () {
+    var map = YamlMap();
+    expect(map, isEmpty);
+    expect(map.nodes, isEmpty);
+    expect(map.span, isNullSpan(isNull));
+  });
+
+  test('YamlMap() with a sourceUrl', () {
+    var map = YamlMap(sourceUrl: 'source');
+    expect(map.span, isNullSpan(Uri.parse('source')));
+  });
+
+  test('YamlList() with no sourceUrl', () {
+    var list = YamlList();
+    expect(list, isEmpty);
+    expect(list.nodes, isEmpty);
+    expect(list.span, isNullSpan(isNull));
+  });
+
+  test('YamlList() with a sourceUrl', () {
+    var list = YamlList(sourceUrl: 'source');
+    expect(list.span, isNullSpan(Uri.parse('source')));
+  });
+
+  test('YamlMap.wrap() with no sourceUrl', () {
+    var map = YamlMap.wrap({
+      'list': [1, 2, 3],
+      'map': {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'scalar': 'value'
+    });
+
+    expect(
+        map,
+        equals({
+          'list': [1, 2, 3],
+          'map': {
+            'foo': 'bar',
+            'nested': [4, 5, 6]
+          },
+          'scalar': 'value'
+        }));
+
+    expect(map.span, isNullSpan(isNull));
+    expect(map['list'], isA<YamlList>());
+    expect(map['list'].nodes[0], isA<YamlScalar>());
+    expect(map['list'].span, isNullSpan(isNull));
+    expect(map['map'], isA<YamlMap>());
+    expect(map['map'].nodes['foo'], isA<YamlScalar>());
+    expect(map['map']['nested'], isA<YamlList>());
+    expect(map['map'].span, isNullSpan(isNull));
+    expect(map.nodes['scalar'], isA<YamlScalar>());
+    expect(map.nodes['scalar']!.value, 'value');
+    expect(map.nodes['scalar']!.span, isNullSpan(isNull));
+    expect(map['scalar'], 'value');
+    expect(map.keys, unorderedEquals(['list', 'map', 'scalar']));
+    expect(map.nodes.keys, everyElement(isA<YamlScalar>()));
+    expect(map.nodes[YamlScalar.wrap('list')], equals([1, 2, 3]));
+    expect(map.style, equals(CollectionStyle.ANY));
+    expect((map.nodes['list'] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((map.nodes['map'] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((map['map'].nodes['nested'] as YamlList).style,
+        equals(CollectionStyle.ANY));
+  });
+
+  test('YamlMap.wrap() with a sourceUrl', () {
+    var map = YamlMap.wrap({
+      'list': [1, 2, 3],
+      'map': {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'scalar': 'value'
+    }, sourceUrl: 'source');
+
+    var source = Uri.parse('source');
+    expect(map.span, isNullSpan(source));
+    expect(map['list'].span, isNullSpan(source));
+    expect(map['map'].span, isNullSpan(source));
+    expect(map.nodes['scalar']!.span, isNullSpan(source));
+  });
+
+  test('YamlMap.wrap() with a sourceUrl and style', () {
+    var map = YamlMap.wrap({
+      'list': [1, 2, 3],
+      'map': {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'scalar': 'value'
+    }, sourceUrl: 'source', style: CollectionStyle.BLOCK);
+
+    expect(map.style, equals(CollectionStyle.BLOCK));
+    expect((map.nodes['list'] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((map.nodes['map'] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((map['map'].nodes['nested'] as YamlList).style,
+        equals(CollectionStyle.ANY));
+  });
+
+  test('YamlList.wrap() with no sourceUrl', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'value'
+    ]);
+
+    expect(
+        list,
+        equals([
+          [1, 2, 3],
+          {
+            'foo': 'bar',
+            'nested': [4, 5, 6]
+          },
+          'value'
+        ]));
+
+    expect(list.span, isNullSpan(isNull));
+    expect(list[0], isA<YamlList>());
+    expect(list[0].nodes[0], isA<YamlScalar>());
+    expect(list[0].span, isNullSpan(isNull));
+    expect(list[1], isA<YamlMap>());
+    expect(list[1].nodes['foo'], isA<YamlScalar>());
+    expect(list[1]['nested'], isA<YamlList>());
+    expect(list[1].span, isNullSpan(isNull));
+    expect(list.nodes[2], isA<YamlScalar>());
+    expect(list.nodes[2].value, 'value');
+    expect(list.nodes[2].span, isNullSpan(isNull));
+    expect(list[2], 'value');
+    expect(list.style, equals(CollectionStyle.ANY));
+    expect((list[0] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((list[1] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((list[1]['nested'] as YamlList).style, equals(CollectionStyle.ANY));
+  });
+
+  test('YamlList.wrap() with a sourceUrl', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'value'
+    ], sourceUrl: 'source');
+
+    var source = Uri.parse('source');
+    expect(list.span, isNullSpan(source));
+    expect(list[0].span, isNullSpan(source));
+    expect(list[1].span, isNullSpan(source));
+    expect(list.nodes[2].span, isNullSpan(source));
+  });
+
+  test('YamlList.wrap() with a sourceUrl and style', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'value'
+    ], sourceUrl: 'source', style: CollectionStyle.FLOW);
+
+    expect(list.style, equals(CollectionStyle.FLOW));
+    expect((list[0] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((list[1] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((list[1]['nested'] as YamlList).style, equals(CollectionStyle.ANY));
+  });
+
+  test('re-wrapped objects equal one another', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {'foo': 'bar'}
+    ]);
+
+    expect(list[0] == list[0], isTrue);
+    expect(list[0].nodes == list[0].nodes, isTrue);
+    expect(list[0] == YamlList.wrap([1, 2, 3]), isFalse);
+    expect(list[1] == list[1], isTrue);
+    expect(list[1].nodes == list[1].nodes, isTrue);
+    expect(list[1] == YamlMap.wrap({'foo': 'bar'}), isFalse);
+  });
+
+  test('YamlScalar.wrap() with no sourceUrl', () {
+    var scalar = YamlScalar.wrap('foo');
+
+    expect(scalar.span, isNullSpan(isNull));
+    expect(scalar.value, 'foo');
+    expect(scalar.style, equals(ScalarStyle.ANY));
+  });
+
+  test('YamlScalar.wrap() with sourceUrl', () {
+    var scalar = YamlScalar.wrap('foo', sourceUrl: 'source');
+
+    var source = Uri.parse('source');
+    expect(scalar.span, isNullSpan(source));
+  });
+
+  test('YamlScalar.wrap() with sourceUrl and style', () {
+    var scalar = YamlScalar.wrap('foo',
+        sourceUrl: 'source', style: ScalarStyle.DOUBLE_QUOTED);
+
+    expect(scalar.style, equals(ScalarStyle.DOUBLE_QUOTED));
+  });
+}
+
+Matcher isNullSpan(Object sourceUrl) => predicate((SourceSpan span) {
+      expect(span, isA<SourceSpan>());
+      expect(span.length, equals(0));
+      expect(span.text, isEmpty);
+      expect(span.start, equals(span.end));
+      expect(span.start.offset, equals(0));
+      expect(span.start.line, equals(0));
+      expect(span.start.column, equals(0));
+      expect(span.sourceUrl, sourceUrl);
+      return true;
+    });
diff --git a/pkgs/yaml/test/yaml_test.dart b/pkgs/yaml/test/yaml_test.dart
new file mode 100644
index 0000000..3b5b77d
--- /dev/null
+++ b/pkgs/yaml/test/yaml_test.dart
@@ -0,0 +1,1921 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: avoid_dynamic_calls
+
+import 'package:test/test.dart';
+import 'package:yaml/src/error_listener.dart';
+import 'package:yaml/yaml.dart';
+
+import 'utils.dart';
+
+void main() {
+  var infinity = double.parse('Infinity');
+  var nan = double.parse('NaN');
+
+  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)));
+    });
+  });
+
+  group('refuses', () {
+    // Regression test for #19.
+    test('invalid contents', () {
+      expectYamlFails('{');
+    });
+
+    test('duplicate mapping keys', () {
+      expectYamlFails('{a: 1, a: 2}');
+    });
+
+    group('documents that declare version', () {
+      test('1.0', () {
+        expectYamlFails('''
+         %YAML 1.0
+         --- text
+         ''');
+      });
+
+      test('1.3', () {
+        expectYamlFails('''
+           %YAML 1.3
+           --- text
+           ''');
+      });
+
+      test('2.0', () {
+        expectYamlFails('''
+           %YAML 2.0
+           --- text
+           ''');
+      });
+    });
+  });
+
+  group('recovers', () {
+    var collector = ErrorCollector();
+    setUp(() {
+      collector = ErrorCollector();
+    });
+
+    test('from incomplete leading keys', () {
+      final yaml = cleanUpLiteral(r'''
+        dependencies:
+          zero
+          one: any
+          ''');
+      var result = loadYaml(yaml, recover: true, errorListener: collector);
+      expect(
+          result,
+          deepEquals({
+            'dependencies': {
+              'zero': null,
+              'one': 'any',
+            }
+          }));
+      expect(collector.errors.length, equals(1));
+      // These errors are reported at the start of the next token (after the
+      // whitespace/newlines).
+      expectErrorAtLineCol(collector.errors[0], "Expected ':'.", 2, 2);
+      // Skipped because this case is not currently handled. If it's the first
+      // package without the colon, because the value is indented from the line
+      // above, the whole `zero\n     one` is treated as a scalar value.
+    }, skip: true);
+    test('from incomplete keys', () {
+      final yaml = cleanUpLiteral(r'''
+        dependencies:
+          one: any
+          two
+          three:
+          four
+          five:
+            1.2.3
+          six: 5.4.3
+          ''');
+      var result = loadYaml(yaml, recover: true, errorListener: collector);
+      expect(
+          result,
+          deepEquals({
+            'dependencies': {
+              'one': 'any',
+              'two': null,
+              'three': null,
+              'four': null,
+              'five': '1.2.3',
+              'six': '5.4.3',
+            }
+          }));
+
+      expect(collector.errors.length, equals(2));
+      // These errors are reported at the start of the next token (after the
+      // whitespace/newlines).
+      expectErrorAtLineCol(collector.errors[0], "Expected ':'.", 3, 2);
+      expectErrorAtLineCol(collector.errors[1], "Expected ':'.", 5, 2);
+    });
+    test('from incomplete trailing keys', () {
+      final yaml = cleanUpLiteral(r'''
+        dependencies:
+          six: 5.4.3
+          seven
+          ''');
+      var result = loadYaml(yaml, recover: true);
+      expect(
+          result,
+          deepEquals({
+            'dependencies': {
+              'six': '5.4.3',
+              'seven': null,
+            }
+          }));
+    });
+  });
+
+  test('includes source span information', () {
+    var yaml = loadYamlNode(r'''
+- foo:
+    bar
+- 123
+''') as YamlList;
+
+    expect(yaml.span.start.line, equals(0));
+    expect(yaml.span.start.column, equals(0));
+    expect(yaml.span.end.line, equals(3));
+    expect(yaml.span.end.column, equals(0));
+
+    var map = yaml.nodes.first as YamlMap;
+    expect(map.span.start.line, equals(0));
+    expect(map.span.start.column, equals(2));
+    expect(map.span.end.line, equals(2));
+    expect(map.span.end.column, equals(0));
+
+    var key = map.nodes.keys.first;
+    expect(key.span.start.line, equals(0));
+    expect(key.span.start.column, equals(2));
+    expect(key.span.end.line, equals(0));
+    expect(key.span.end.column, equals(5));
+
+    var value = map.nodes.values.first;
+    expect(value.span.start.line, equals(1));
+    expect(value.span.start.column, equals(4));
+    expect(value.span.end.line, equals(1));
+    expect(value.span.end.column, equals(7));
+
+    var scalar = yaml.nodes.last;
+    expect(scalar.span.start.line, equals(2));
+    expect(scalar.span.start.column, equals(2));
+    expect(scalar.span.end.line, equals(2));
+    expect(scalar.span.end.column, equals(5));
+  });
+
+  // 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 = deepEqualsMap();
+      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 \u{1F680}',
+        'control': '\b1998\t1999\t2000\n',
+        'hex esc': '\r\n is \r\n',
+        'single': '"Howdy!" he cried.',
+        'quoted': " # Not a 'comment'.",
+        'tie-fighter': '|\\-*-/|',
+        'surrogate-pair': 'I \u{D83D}\u{DE03}  ️Dart!',
+        'key-\u{D83D}\u{DD11}': 'Look\u{D83D}\u{DE03}\u{D83C}\u{DF89}surprise!',
+      }, """
+        unicode: "Sosa did fine.\\u263A \\U0001F680"
+        control: "\\b1998\\t1999\\t2000\\n"
+        hex esc: "\\x0d\\x0a is \\r\\n"
+
+        single: '"Howdy!" he cried.'
+        quoted: ' # Not a ''comment''.'
+        tie-fighter: '|\\-*-/|'
+        
+        surrogate-pair: I \u{D83D}\u{DE03}  ️Dart!
+        key-\u{D83D}\u{DD11}: Look\u{D83D}\u{DE03}\u{D83C}\u{DF89}surprise!""");
+    });
+
+    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 = deepEqualsMap({
+        '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', () {
+    void expectAllowsCharacter(int charCode) {
+      var char = String.fromCharCodes([charCode]);
+      expectYamlLoads('The character "$char" is allowed',
+          'The character "$char" is allowed');
+    }
+
+    void expectAllowsQuotedCharacter(int charCode) {
+      var char = String.fromCharCodes([charCode]);
+      expectYamlLoads("The character '$char' is allowed",
+          '"The character \'$char\' is allowed"');
+    }
+
+    void expectDisallowsCharacter(int charCode) {
+      var char = String.fromCharCodes([charCode]);
+      expectYamlFails('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]', () {
+      expectYamlFails('commercial-at: @text');
+      expectYamlFails('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', () => expectYamlFails('- 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]', () {
+      expectYamlFails('Bad escape: "\\c"');
+      expectYamlFails('Bad escape: "\\xq-"');
+    });
+  });
+
+  // Chapter 6: Basic Structures
+  group('6.1: Indentation Spaces', () {
+    test('may not include TAB characters', () {
+      expectYamlFails('''
+        -
+        \t- foo
+        \t- bar''');
+    });
+
+    test('must be the same for all sibling nodes', () {
+      expectYamlFails('''
+        -
+          - 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', () {
+      expectYamlFails('''
+        [1,
+         2]: 3''');
+    });
+
+    test('[Example 6.12]', () {
+      var doc = deepEqualsMap();
+      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]', () {
+      expectYamlFails('''
+        %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]', () {
+      expectYamlFails('''
+        %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]', () {
+      expectYamlFails('- !<!> foo');
+      expectYamlFails('- !<\$:?> 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': 'Value'}, '''
+        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", () {
+      expectYamlFails('''
+        - *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', () => expectYamlFails('''
+        - &anchor foo
+        - !str *anchor'''));
+
+      test('anchor properties', () => expectYamlFails('''
+        - &anchor foo
+        - &anchor2 *anchor'''));
+
+      test('content', () => expectYamlFails('''
+        - &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(anchorList, same(aliasList));
+
+      doc = loadYaml(cleanUpLiteral('''
+        ? &anchor [a, b, c]
+        : ? *anchor
+          : bar'''));
+      anchorList = doc.keys.first;
+      aliasList = doc[['a', 'b', 'c']].keys.first;
+      expect(anchorList, same(aliasList));
+    });
+
+    test('[Example 7.1]', () {
+      expectYamlLoads({
+        'First occurrence': 'Foo',
+        'Second occurrence': '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 = deepEqualsMap({'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 = deepEqualsMap({'explicit': 'entry', 'implicit': 'entry'});
+      doc[null] = null;
+      expectYamlLoads(doc, '''
+        {
+        ? explicit: entry,
+        implicit: entry,
+        ?
+        }''');
+    });
+
+    test('[Example 7.17]', () {
+      var doc = deepEqualsMap({
+        '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 = deepEqualsMap();
+      el1[null] = 'empty key entry';
+
+      var el2 = deepEqualsMap();
+      el2[{'JSON': 'like'}] = 'adjacent';
+
+      expectYamlLoads([
+        [
+          {'YAML': 'separate'}
+        ],
+        [el1],
+        [el2]
+      ], '''
+        - [ YAML : separate ]
+        - [ : empty key entry ]
+        - [ {JSON: like}:adjacent ]''');
+    });
+
+    // TODO(nweiz): enable this when we throw an error for long or multiline
+    // keys.
+    // test('[Example 7.22]', () {
+    //   expectYamlFails(
+    //     """
+    //     [ foo
+    //      bar: invalid ]""");
+    //
+    //   var dotList = new List.filled(1024, ' ');
+    //   var dots = dotList.join();
+    //   expectYamlFails('[ "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]', () {
+      expectYamlFails('''
+        - |
+          
+         text''');
+
+      expectYamlFails('''
+        - >
+          text
+         text''');
+
+      expectYamlFails('''
+        - |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 = deepEqualsMap({
+        '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 = deepEqualsMap();
+      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]', () {
+      // The spec doesn't include a newline after "value" in the parsed map, but
+      // the block scalar is clipped so it should be retained.
+      expectYamlLoads({'literal': 'value\n', '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]', () {
+      // The spec doesn't have a space between the second
+      // "YAML" and "1.2", but this seems to be a typo.
+      expectYamlStreamLoads(['%!PS-Adobe-2.0\n', null], '''
+        %YAML 1.2
+        --- |
+        %!PS-Adobe-2.0
+        ...
+        %YAML 1.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]', () {
+      expectYamlLoads({
+        '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]', () {
+      expectYamlLoads({
+        '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]', () {
+      expectYamlLoads({
+        '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 = deepEqualsMap({'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''');
+    }, skip: 'Fails for single digit float');
+
+    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 ]''');
+    });
+  });
+
+  test('preserves key order', () {
+    const keys = ['a', 'b', 'c', 'd', 'e', 'f'];
+    var sanityCheckCount = 0;
+    for (var permutation in _generatePermutations(keys)) {
+      final yaml = permutation.map((key) => '$key: value').join('\n');
+      expect(loadYaml(yaml).keys.toList(), permutation);
+      sanityCheckCount++;
+    }
+    final expectedPermutationCount =
+        List.generate(keys.length, (i) => i + 1).reduce((n, i) => n * i);
+    expect(sanityCheckCount, expectedPermutationCount);
+  });
+}
+
+Iterable<List<String>> _generatePermutations(List<String> keys) sync* {
+  if (keys.length <= 1) {
+    yield keys;
+    return;
+  }
+  for (var i = 0; i < keys.length; i++) {
+    final first = keys[i];
+    final rest = <String>[...keys.sublist(0, i), ...keys.sublist(i + 1)];
+    for (var subPermutation in _generatePermutations(rest)) {
+      yield <String>[first, ...subPermutation];
+    }
+  }
+}