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 @@
+[](https://github.com/dart-lang/yaml/actions?query=workflow%3A"Dart+CI"+branch%3Amaster)
+[](https://pub.dev/packages/yaml)
+[](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];
+ }
+ }
+}