Add an "ErrorListener" to collect errors during parsing
diff --git a/pkgs/yaml/lib/src/error_listener.dart b/pkgs/yaml/lib/src/error_listener.dart
new file mode 100644
index 0000000..d156655
--- /dev/null
+++ b/pkgs/yaml/lib/src/error_listener.dart
@@ -0,0 +1,33 @@
+import 'package:source_span/source_span.dart';
+
+import 'yaml_exception.dart';
+
+/// A listener that is notified of [YamlError]s during scanning/parsing.
+abstract class ErrorListener {
+  /// This method is invoked when an [error] has been found in the YAML.
+  void onError(YamlError error);
+}
+
+/// An error found in the YAML.
+class YamlError {
+  /// A message describing the exception.
+  final String message;
+
+  /// The span associated with this exception.
+  final FileSpan span;
+
+  YamlError(this.message, this.span);
+}
+
+extension YamlErrorExtensions on YamlError {
+  /// Creates a [YamlException] from a [YamlError].
+  YamlException toException() => YamlException(message, span);
+}
+
+/// An [ErrorListener] that collects all errors into [errors].
+class ErrorCollector extends ErrorListener {
+  final List<YamlError> errors = [];
+
+  @override
+  void onError(YamlError error) => errors.add(error);
+}
diff --git a/pkgs/yaml/lib/src/loader.dart b/pkgs/yaml/lib/src/loader.dart
index dafc59b..0056a64 100644
--- a/pkgs/yaml/lib/src/loader.dart
+++ b/pkgs/yaml/lib/src/loader.dart
@@ -4,6 +4,7 @@
 
 import 'package:charcode/ascii.dart';
 import 'package:source_span/source_span.dart';
+import 'package:yaml/src/error_listener.dart';
 
 import 'equality.dart';
 import 'event.dart';
@@ -30,8 +31,10 @@
   FileSpan _span;
 
   /// Creates a loader that loads [source].
-  factory Loader(String source, {Uri? sourceUrl, bool recover = false}) {
-    var parser = Parser(source, sourceUrl: sourceUrl, recover: recover);
+  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);
diff --git a/pkgs/yaml/lib/src/parser.dart b/pkgs/yaml/lib/src/parser.dart
index 695c1d0..2f759e9 100644
--- a/pkgs/yaml/lib/src/parser.dart
+++ b/pkgs/yaml/lib/src/parser.dart
@@ -4,6 +4,7 @@
 
 import 'package:source_span/source_span.dart';
 import 'package:string_scanner/string_scanner.dart';
+import 'package:yaml/src/error_listener.dart';
 
 import 'event.dart';
 import 'scanner.dart';
@@ -35,8 +36,12 @@
   bool get isDone => _state == _State.END;
 
   /// Creates a parser that parses [source].
-  Parser(String source, {Uri? sourceUrl, bool recover = false})
-      : _scanner = Scanner(source, sourceUrl: sourceUrl, recover: recover);
+  Parser(String source,
+      {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener})
+      : _scanner = Scanner(source,
+            sourceUrl: sourceUrl,
+            recover: recover,
+            errorListener: errorListener);
 
   /// Consumes and returns the next event.
   Event parse() {
diff --git a/pkgs/yaml/lib/src/scanner.dart b/pkgs/yaml/lib/src/scanner.dart
index 171364c..260da1c 100644
--- a/pkgs/yaml/lib/src/scanner.dart
+++ b/pkgs/yaml/lib/src/scanner.dart
@@ -5,6 +5,7 @@
 import 'package:collection/collection.dart';
 import 'package:source_span/source_span.dart';
 import 'package:string_scanner/string_scanner.dart';
+import 'package:yaml/src/error_listener.dart';
 
 import 'style.dart';
 import 'token.dart';
@@ -92,6 +93,9 @@
   /// 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
@@ -291,8 +295,10 @@
   }
 
   /// Creates a scanner that scans [source].
-  Scanner(String source, {Uri? sourceUrl, bool recover = false})
+  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.
@@ -490,11 +496,12 @@
       if (key.line == _scanner.line) continue;
 
       if (key.required) {
+        final error = _reportError("Expected ':'.", _scanner.emptySpan);
         if (_recover) {
           _tokens.insert(key.tokenNumber - _tokensParsed,
               Token(TokenType.key, key.location.pointSpan() as FileSpan));
         } else {
-          throw YamlException("Expected ':'.", _scanner.emptySpan);
+          throw error.toException();
         }
       }
 
@@ -1633,6 +1640,13 @@
       _scanner.readChar();
     }
   }
+
+  /// Reports an error to [_errorListener] and returns the [YamlError].
+  YamlError _reportError(String message, FileSpan span) {
+    final error = YamlError("Expected ':'.", _scanner.emptySpan);
+    _errorListener?.onError(error);
+    return error;
+  }
 }
 
 /// A record of the location of a potential simple key.
diff --git a/pkgs/yaml/lib/yaml.dart b/pkgs/yaml/lib/yaml.dart
index 988de2d..dec4bc0 100644
--- a/pkgs/yaml/lib/yaml.dart
+++ b/pkgs/yaml/lib/yaml.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'src/error_listener.dart';
 import 'src/loader.dart';
 import 'src/style.dart';
 import 'src/yaml_document.dart';
@@ -33,16 +34,30 @@
 ///
 /// If [recover] is true, will attempt to recover from parse errors and may return
 /// invalid or synthetic nodes.
-dynamic loadYaml(String yaml, {Uri? sourceUrl, bool recover = false}) =>
-    loadYamlNode(yaml, sourceUrl: sourceUrl, recover: recover).value;
+///
+/// If [errorListener] is supplied, its onError method will be called for each
+/// error. If [recover] is false, parsing will end early so only the first error
+/// may be emitted.
+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}) =>
-    loadYamlDocument(yaml, sourceUrl: sourceUrl, recover: recover).contents;
+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].
 ///
@@ -50,8 +65,9 @@
 /// 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}) {
-  var loader = Loader(yaml, sourceUrl: sourceUrl, recover: recover);
+    {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),
diff --git a/pkgs/yaml/test/utils.dart b/pkgs/yaml/test/utils.dart
index ce05481..0904458 100644
--- a/pkgs/yaml/test/utils.dart
+++ b/pkgs/yaml/test/utils.dart
@@ -4,6 +4,7 @@
 
 import 'package:test/test.dart';
 import 'package:yaml/src/equality.dart' as equality;
+import 'package:yaml/src/error_listener.dart' show YamlError;
 import 'package:yaml/yaml.dart';
 
 /// A matcher that validates that a closure or Future throws a [YamlException].
@@ -22,6 +23,13 @@
   return map;
 }
 
+/// Asserts that an error has the given message and starts at the given line/col.
+void expectErrorAtLineCol(YamlError 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(expected, String source) {
diff --git a/pkgs/yaml/test/yaml_test.dart b/pkgs/yaml/test/yaml_test.dart
index 014da96..dffb9d5 100644
--- a/pkgs/yaml/test/yaml_test.dart
+++ b/pkgs/yaml/test/yaml_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:test/test.dart';
+import 'package:yaml/src/error_listener.dart';
 import 'package:yaml/yaml.dart';
 
 import 'utils.dart';
@@ -62,13 +63,18 @@
   });
 
   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);
+      var result = loadYaml(yaml, recover: true, errorListener: collector);
       expect(
           result,
           deepEquals({
@@ -77,6 +83,10 @@
               '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.
@@ -92,7 +102,7 @@
             1.2.3
           six: 5.4.3
           ''');
-      var result = loadYaml(yaml, recover: true);
+      var result = loadYaml(yaml, recover: true, errorListener: collector);
       expect(
           result,
           deepEquals({
@@ -105,6 +115,12 @@
               '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'''