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');