diff --git a/.github/ISSUE_TEMPLATE/yaml.md b/.github/ISSUE_TEMPLATE/yaml.md new file mode 100644 index 000000000..d6a7c7fef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/yaml.md @@ -0,0 +1,5 @@ +--- +name: "package:yaml" +about: "Create a bug or file a feature request against package:yaml." +labels: "package:yaml" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 64585f35a..84926222a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -143,3 +143,7 @@ 'package:watcher': - changed-files: - any-glob-to-any-file: 'pkgs/watcher/**' + +'package:yaml': + - changed-files: + - any-glob-to-any-file: 'pkgs/yaml/**' \ No newline at end of file diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml new file mode 100644 index 000000000..735461eec --- /dev/null +++ b/.github/workflows/yaml.yaml @@ -0,0 +1,75 @@ +name: package:yaml + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ main ] + paths: + - '.github/workflows/yaml.yaml' + - 'pkgs/yaml/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/yaml.yaml' + - 'pkgs/yaml/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + + +defaults: + run: + working-directory: pkgs/yaml/ + +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/README.md b/README.md index e58417bb7..a9608b7fa 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ don't naturally belong to other topic monorepos (like | [timing](pkgs/timing/) | A simple package for tracking the performance of synchronous and asynchronous actions. | [![package issues](https://img.shields.io/badge/package:timing-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atiming) | [![pub package](https://img.shields.io/pub/v/timing.svg)](https://pub.dev/packages/timing) | | [unified_analytics](pkgs/unified_analytics/) | A package for logging analytics for all Dart and Flutter related tooling to Google Analytics. | [![package issues](https://img.shields.io/badge/package:unified_analytics-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aunified_analytics) | [![pub package](https://img.shields.io/pub/v/unified_analytics.svg)](https://pub.dev/packages/unified_analytics) | | [watcher](pkgs/watcher/) | Monitor directories and send notifications when the contents change. | [![package issues](https://img.shields.io/badge/package:watcher-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Awatcher) | [![pub package](https://img.shields.io/pub/v/watcher.svg)](https://pub.dev/packages/watcher) | +| [yaml](pkgs/yaml/) | A parser for YAML, a human-friendly data serialization standard | [![package issues](https://img.shields.io/badge/package:yaml-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ayaml) | [![pub package](https://img.shields.io/pub/v/yaml.svg)](https://pub.dev/packages/yaml) | ## Publishing automation diff --git a/pkgs/yaml/.gitignore b/pkgs/yaml/.gitignore new file mode 100644 index 000000000..ab3cb76e6 --- /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 000000000..3f9d3fd22 --- /dev/null +++ b/pkgs/yaml/CHANGELOG.md @@ -0,0 +1,199 @@ +## 3.1.3 + +* Require Dart 3.4 +* Fix UTF-16 surrogate pair handling in plain scaler. +* Move to `dart-lang/tools` monorepo. + +## 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 000000000..e7589cbd1 --- /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 000000000..ba56893df --- /dev/null +++ b/pkgs/yaml/README.md @@ -0,0 +1,33 @@ +[![Build Status](https://github.com/dart-lang/tools/actions/workflows/yaml.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/yaml.yaml) +[![pub package](https://img.shields.io/pub/v/yaml.svg)](https://pub.dev/packages/yaml) +[![package publisher](https://img.shields.io/pub/publisher/yaml.svg)](https://pub.dev/packages/yaml/publisher) + + +A parser for [YAML](https://yaml.org/). + +## Usage + +Use `loadYaml` to load a single document, or `loadYamlStream` to load a +stream of documents. For example: + +```dart +import 'package:yaml/yaml.dart'; + +main() { + var doc = loadYaml("YAML: YAML Ain't Markup Language"); + print(doc['YAML']); +} +``` + +This library currently doesn't support dumping to YAML. You should use +`json.encode` from `dart:convert` instead: + +```dart +import 'dart:convert'; +import 'package:yaml/yaml.dart'; + +main() { + var doc = loadYaml("YAML: YAML Ain't Markup Language"); + print(json.encode(doc)); +} +``` diff --git a/pkgs/yaml/analysis_options.yaml b/pkgs/yaml/analysis_options.yaml new file mode 100644 index 000000000..46e45f0de --- /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 000000000..afc3c97ce --- /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 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 000000000..89bf9dcc2 --- /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 000000000..9e6cb8400 --- /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 000000000..bb283a3bb --- /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 000000000..602d597cb --- /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 000000000..c833dc608 --- /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 deepEqualsMap() => + 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 = []; + final _parents2 = []; + + /// 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 = []; + + 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(); + return equality.hash(value.keys.map(deepHashCodeInner)) ^ + equality.hash(value.values.map(deepHashCodeInner)); + } else if (value is Iterable) { + return const IterableEquality().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 000000000..0498d6818 --- /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 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 000000000..1476311aa --- /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 tagDirectives; + + /// Whether the document started implicitly (that is, without an explicit + /// `===` sequence). + final bool isImplicit; + + DocumentStartEvent(this.span, + {this.versionDirective, + List? 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 000000000..7cdf45a7e --- /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 = {}; + + /// 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 = []; + 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(); + 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 000000000..49e1a1c90 --- /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 000000000..e924e40ea --- /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 = {}; + + /// 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) _processDirectives() { + var token = _scanner.peek()!; + + VersionDirective? versionDirective; + var tagDirectives = []; + 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 000000000..1cfd3af61 --- /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(); + + /// 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 = [-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 000000000..96c3b94d8 --- /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 000000000..7d5d6bc9a --- /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 000000000..0dc132ff8 --- /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 000000000..da6aa1ec3 --- /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 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 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 000000000..7aa5389fc --- /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 000000000..bd17b6cb9 --- /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 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 nodes, super.span, this.style) + : nodes = UnmodifiableMapView(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 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 nodes, super.span, this.style) + : nodes = UnmodifiableListView(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 000000000..525084474 --- /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 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 + with pkg_collection.UnmodifiableMapMixin { + 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 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 { + 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 000000000..26cc9b86f --- /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 = []; + 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 loadYamlDocuments(String yaml, {Uri? sourceUrl}) { + var loader = Loader(yaml, sourceUrl: sourceUrl); + + var documents = []; + 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 000000000..fb37436f2 --- /dev/null +++ b/pkgs/yaml/pubspec.yaml @@ -0,0 +1,21 @@ +name: yaml +version: 3.1.3 +description: A parser for YAML, a human-friendly data serialization standard +repository: https://github.com/dart-lang/tools/tree/main/pkgs/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 000000000..03b7f9c47 --- /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 000000000..372440ae9 --- /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()); + +/// 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(); + 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 000000000..637b77832 --- /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()); + expect(map['list'].nodes[0], isA()); + expect(map['list'].span, isNullSpan(isNull)); + expect(map['map'], isA()); + expect(map['map'].nodes['foo'], isA()); + expect(map['map']['nested'], isA()); + expect(map['map'].span, isNullSpan(isNull)); + expect(map.nodes['scalar'], isA()); + 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())); + 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()); + expect(list[0].nodes[0], isA()); + expect(list[0].span, isNullSpan(isNull)); + expect(list[1], isA()); + expect(list[1].nodes['foo'], isA()); + expect(list[1]['nested'], isA()); + expect(list[1].span, isNullSpan(isNull)); + expect(list.nodes[2], isA()); + 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()); + 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 000000000..3b5b77d2f --- /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> _generatePermutations(List keys) sync* { + if (keys.length <= 1) { + yield keys; + return; + } + for (var i = 0; i < keys.length; i++) { + final first = keys[i]; + final rest = [...keys.sublist(0, i), ...keys.sublist(i + 1)]; + for (var subPermutation in _generatePermutations(rest)) { + yield [first, ...subPermutation]; + } + } +}