Wrap recursively, prepare release (#28)
* Wrap recursively, prepare release
* Fix lints
* Fix review comments
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ebf013..356df41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,12 @@
-## v2.0.4
+## v2.1.0
+- **Breaking** `wrapAsYamlNode(value, collectionStyle, scalarStyle)` will apply
+ `collectionStyle` and `scalarStyle` recursively when wrapping a children of
+ `Map` and `List`.
+ While this may change the style of the YAML documents written by applications
+ that rely on the old behavior, such YAML documents should still be valid.
+ Hence, we hope it is reasonable to make this change in a minor release.
+- Fix for cases that can't be encodded correctedly with
+ `scalarStyle: ScalarStyle.SINGLE_QUOTED`.
- Fix YamlEditor `appendToList` and `insertIntoList` functions inserts new item into next yaml item
rather than at end of list.
([#23](https://github.com/dart-lang/yaml_edit/issues/23))
diff --git a/README.md b/README.md
index c13ecff..71639c3 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,32 @@
}
```
+### Example: Converting JSON to YAML (block formatted)
+
+```dart
+void main() {
+ final jsonString = r'''
+{
+ "key": "value",
+ "list": [
+ "first",
+ "second",
+ "last entry in the list"
+ ],
+ "map": {
+ "multiline": "this is a fairly long string with\nline breaks..."
+ }
+}
+''';
+ final jsonValue = json.decode(jsonString);
+
+ // Convert jsonValue to YAML
+ final yamlEditor = YamlEditor('');
+ yamlEditor.update([], jsonValue);
+ print(yamlEditor.toString());
+}
+```
+
## Testing
Testing is done in two strategies: Unit testing (`/test/editor_test.dart`) and
diff --git a/example/json2yaml.dart b/example/json2yaml.dart
new file mode 100644
index 0000000..d6204d3
--- /dev/null
+++ b/example/json2yaml.dart
@@ -0,0 +1,28 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert' show json;
+
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+ final jsonString = r'''
+{
+ "key": "value",
+ "list": [
+ "first",
+ "second",
+ "last entry in the list"
+ ],
+ "map": {
+ "multiline": "this is a fairly long string with\nline breaks..."
+ }
+}
+''';
+ final jsonValue = json.decode(jsonString);
+
+ final yamlEditor = YamlEditor('');
+ yamlEditor.update([], jsonValue);
+ print(yamlEditor.toString());
+}
diff --git a/lib/src/errors.dart b/lib/src/errors.dart
index 3096b38..e44ee4d 100644
--- a/lib/src/errors.dart
+++ b/lib/src/errors.dart
@@ -94,7 +94,6 @@
> ${newYaml.replaceAll('\n', '\n> ')}
Please file an issue at:
-'''
- 'https://github.com/google/dart-neats/issues/new?labels=pkg%3Ayaml_edit'
- '%2C+pending-triage&template=yaml_edit.md\n');
+https://github.com/dart-lang/yaml_edit/issues/new?labels=bug
+''');
}
diff --git a/lib/src/strings.dart b/lib/src/strings.dart
index 379364e..62fe280 100644
--- a/lib/src/strings.dart
+++ b/lib/src/strings.dart
@@ -70,8 +70,20 @@
/// single-quotes.
///
/// It is important that we ensure that [string] is free of unprintable
-/// characters by calling [assertValidScalar] before invoking this function.
+/// characters by calling [_hasUnprintableCharacters] before invoking this
+/// function.
String _tryYamlEncodeSingleQuoted(String string) {
+ // If [string] contains a newline we'll use double quoted strings instead.
+ // Single quoted strings can represent newlines, but then we have to use an
+ // empty line (replace \n with \n\n). But since leading spaces following
+ // line breaks are ignored, we can't represent "\n ".
+ // Thus, if the string contains `\n` and we're asked to do single quoted,
+ // we'll fallback to a double quoted string.
+ // TODO: Consider if we should make '\n' an unprintedable, this might make
+ // folded strings into double quoted -- some work is needed here.
+ if (string.contains('\n')) {
+ return _yamlEncodeDoubleQuoted(string);
+ }
final result = string.replaceAll('\'', '\'\'');
return '\'$result\'';
}
@@ -79,7 +91,8 @@
/// Generates a YAML-safe folded string.
///
/// It is important that we ensure that [string] is free of unprintable
-/// characters by calling [assertValidScalar] before invoking this function.
+/// characters by calling [_hasUnprintableCharacters] before invoking this
+/// function.
String _tryYamlEncodeFolded(String string, int indentation, String lineEnding) {
String result;
@@ -103,7 +116,8 @@
/// Generates a YAML-safe literal string.
///
/// It is important that we ensure that [string] is free of unprintable
-/// characters by calling [assertValidScalar] before invoking this function.
+/// characters by calling [_hasUnprintableCharacters] before invoking this
+/// function.
String _tryYamlEncodeLiteral(
String string, int indentation, String lineEnding) {
final result = '|-\n$string';
@@ -150,7 +164,10 @@
/// 'null'), in which case we will produce [value] with default styling
/// options.
String yamlEncodeBlockScalar(
- YamlNode value, int indentation, String lineEnding) {
+ YamlNode value,
+ int indentation,
+ String lineEnding,
+) {
if (value is YamlScalar) {
assertValidScalar(value.value);
@@ -215,7 +232,10 @@
///
/// If [value] is a [YamlNode], we respect its [style] parameter.
String yamlEncodeBlockString(
- YamlNode value, int indentation, String lineEnding) {
+ YamlNode value,
+ int indentation,
+ String lineEnding,
+) {
const additionalIndentation = 2;
if (!isBlockNode(value)) return yamlEncodeFlowString(value);
@@ -279,8 +299,7 @@
8233: '\\P', // Escaped Unicode paragraph separator (#x2029) character.
};
-/// List of escape characters. In particular, \x32 is not included because it
-/// can be processed normally.
+/// List of escape characters.
///
/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
final Map<int, String> doubleQuoteEscapeChars = {
diff --git a/lib/src/wrap.dart b/lib/src/wrap.dart
index fa02deb..8299a00 100644
--- a/lib/src/wrap.dart
+++ b/lib/src/wrap.dart
@@ -29,11 +29,17 @@
/// defined, and [value] is a collection or scalar, the wrapped [YamlNode] will
/// have the respective style, otherwise it defaults to the ANY style.
///
+/// If [value] is a [Map] or [List], then [wrapAsYamlNode] will be called
+/// recursively on all children, and [collectionStyle]/[scalarStyle] will be
+/// applied to any children that are not instances of [YamlNode].
+///
/// If a [YamlNode] is passed in, no further wrapping will be done, and the
/// [collectionStyle]/[scalarStyle] will not be applied.
-YamlNode wrapAsYamlNode(Object? value,
- {CollectionStyle collectionStyle = CollectionStyle.ANY,
- ScalarStyle scalarStyle = ScalarStyle.ANY}) {
+YamlNode wrapAsYamlNode(
+ Object? value, {
+ CollectionStyle collectionStyle = CollectionStyle.ANY,
+ ScalarStyle scalarStyle = ScalarStyle.ANY,
+}) {
if (value is YamlScalar) {
assertValidScalar(value.value);
return value;
@@ -53,9 +59,17 @@
return value;
} else if (value is Map) {
- return YamlMapWrap(value, collectionStyle: collectionStyle);
+ return YamlMapWrap(
+ value,
+ collectionStyle: collectionStyle,
+ scalarStyle: scalarStyle,
+ );
} else if (value is List) {
- return YamlListWrap(value, collectionStyle: collectionStyle);
+ return YamlListWrap(
+ value,
+ collectionStyle: collectionStyle,
+ scalarStyle: scalarStyle,
+ );
} else {
assertValidScalar(value);
@@ -98,24 +112,40 @@
@override
final SourceSpan span;
- factory YamlMapWrap(Map dartMap,
- {CollectionStyle collectionStyle = CollectionStyle.ANY,
- Object? sourceUrl}) {
+ factory YamlMapWrap(
+ Map dartMap, {
+ CollectionStyle collectionStyle = CollectionStyle.ANY,
+ ScalarStyle scalarStyle = ScalarStyle.ANY,
+ Object? sourceUrl,
+ }) {
final wrappedMap = deepEqualsMap<dynamic, YamlNode>();
for (final entry in dartMap.entries) {
- final wrappedKey = wrapAsYamlNode(entry.key);
- final wrappedValue = wrapAsYamlNode(entry.value);
+ final wrappedKey = wrapAsYamlNode(
+ entry.key,
+ collectionStyle: collectionStyle,
+ scalarStyle: scalarStyle,
+ );
+ final wrappedValue = wrapAsYamlNode(
+ entry.value,
+ collectionStyle: collectionStyle,
+ scalarStyle: scalarStyle,
+ );
wrappedMap[wrappedKey] = wrappedValue;
}
- return YamlMapWrap._(wrappedMap,
- style: collectionStyle, sourceUrl: sourceUrl);
+ return YamlMapWrap._(
+ wrappedMap,
+ style: collectionStyle,
+ sourceUrl: sourceUrl,
+ );
}
- YamlMapWrap._(this.nodes,
- {CollectionStyle style = CollectionStyle.ANY, Object? sourceUrl})
- : span = shellSpan(sourceUrl),
+ YamlMapWrap._(
+ this.nodes, {
+ CollectionStyle style = CollectionStyle.ANY,
+ Object? sourceUrl,
+ }) : span = shellSpan(sourceUrl),
style = nodes.isEmpty ? CollectionStyle.FLOW : style;
@override
@@ -149,12 +179,23 @@
throw UnsupportedError('Cannot modify an unmodifiable List');
}
- factory YamlListWrap(List dartList,
- {CollectionStyle collectionStyle = CollectionStyle.ANY,
- Object? sourceUrl}) {
- final wrappedList = dartList.map(wrapAsYamlNode).toList();
- return YamlListWrap._(wrappedList,
- style: collectionStyle, sourceUrl: sourceUrl);
+ factory YamlListWrap(
+ List dartList, {
+ CollectionStyle collectionStyle = CollectionStyle.ANY,
+ ScalarStyle scalarStyle = ScalarStyle.ANY,
+ Object? sourceUrl,
+ }) {
+ return YamlListWrap._(
+ dartList
+ .map((v) => wrapAsYamlNode(
+ v,
+ collectionStyle: collectionStyle,
+ scalarStyle: scalarStyle,
+ ))
+ .toList(),
+ style: collectionStyle,
+ sourceUrl: sourceUrl,
+ );
}
YamlListWrap._(this.nodes,
diff --git a/pubspec.yaml b/pubspec.yaml
index b3404bd..eca8e2b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: yaml_edit
-version: 2.0.3
+version: 2.1.0
description: A library for YAML manipulation with comment and whitespace preservation.
repository: https://github.com/dart-lang/yaml_edit
issue_tracker: https://github.com/dart-lang/yaml_edit/issues
diff --git a/test/string_test.dart b/test/string_test.dart
new file mode 100644
index 0000000..20299d4
--- /dev/null
+++ b/test/string_test.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+final _testStrings = [
+ "this is a fairly' long string with\nline breaks",
+ "whitespace\n after line breaks",
+ "word",
+ "foo bar",
+ "foo\nbar",
+ "\"",
+ '\'',
+ "word\"word",
+ 'word\'word'
+];
+
+final _scalarStyles = [
+ ScalarStyle.ANY,
+ ScalarStyle.DOUBLE_QUOTED,
+ //ScalarStyle.FOLDED, // TODO: Fix this test case!
+ ScalarStyle.LITERAL,
+ ScalarStyle.PLAIN,
+ ScalarStyle.SINGLE_QUOTED,
+];
+
+void main() {
+ for (final style in _scalarStyles) {
+ for (var i = 0; i < _testStrings.length; i++) {
+ final testString = _testStrings[i];
+ test('Root $style string (${i + 1})', () {
+ final yamlEditor = YamlEditor('');
+ yamlEditor.update([], wrapAsYamlNode(testString, scalarStyle: style));
+ final yaml = yamlEditor.toString();
+ expect(loadYaml(yaml), equals(testString));
+ });
+ }
+ }
+}
diff --git a/test/wrap_test.dart b/test/wrap_test.dart
index f80e6e1..57b9b29 100644
--- a/test/wrap_test.dart
+++ b/test/wrap_test.dart
@@ -106,8 +106,8 @@
], collectionStyle: CollectionStyle.BLOCK);
expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
- expect(list[0].style, equals(CollectionStyle.ANY));
- expect(list[1].style, equals(CollectionStyle.ANY));
+ expect(list[0].style, equals(CollectionStyle.BLOCK));
+ expect(list[1].style, equals(CollectionStyle.BLOCK));
});
test('wraps nested lists while preserving style', () {
@@ -194,6 +194,45 @@
});
});
+ test('applies collectionStyle recursively', () {
+ final list = wrapAsYamlNode([
+ [1, 2, 3],
+ {
+ 'foo': 'bar',
+ 'nested': [4, 5, 6]
+ },
+ ], collectionStyle: CollectionStyle.BLOCK);
+
+ expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
+ expect(list[0].style, equals(CollectionStyle.BLOCK));
+ expect(list[1].style, equals(CollectionStyle.BLOCK));
+ expect(list[1]['nested'].style, equals(CollectionStyle.BLOCK));
+ });
+
+ test('applies scalarStyle recursively', () {
+ final list = wrapAsYamlNode([
+ ['a', 'b', 'c'],
+ {
+ 'foo': 'bar',
+ },
+ 'hello',
+ ], scalarStyle: ScalarStyle.SINGLE_QUOTED);
+
+ expect((list as YamlList).style, equals(CollectionStyle.ANY));
+ final item1 = list.nodes[0] as YamlList;
+ final item2 = list.nodes[1] as YamlMap;
+ final item3 = list.nodes[2] as YamlScalar;
+ expect(item1.style, equals(CollectionStyle.ANY));
+ expect(item2.style, equals(CollectionStyle.ANY));
+ expect(item3.style, equals(ScalarStyle.SINGLE_QUOTED));
+
+ final item1entry1 = item1.nodes[0] as YamlScalar;
+ expect(item1entry1.style, equals(ScalarStyle.SINGLE_QUOTED));
+
+ final item2foo = item2.nodes['foo'] as YamlScalar;
+ expect(item2foo.style, equals(ScalarStyle.SINGLE_QUOTED));
+ });
+
group('deepHashCode', () {
test('returns the same result for scalar and its value', () {
final hashCode1 = deepHashCode('foo');