Skip to content

Commit abb3cb5

Browse files
authored
Wrap recursively, prepare release (dart-archive/yaml_edit#28)
* Wrap recursively, prepare release * Fix lints * Fix review comments
1 parent dedf495 commit abb3cb5

File tree

9 files changed

+237
-35
lines changed

9 files changed

+237
-35
lines changed

pkgs/yaml_edit/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
## v2.0.4
1+
## v2.1.0
2+
- **Breaking** `wrapAsYamlNode(value, collectionStyle, scalarStyle)` will apply
3+
`collectionStyle` and `scalarStyle` recursively when wrapping a children of
4+
`Map` and `List`.
5+
While this may change the style of the YAML documents written by applications
6+
that rely on the old behavior, such YAML documents should still be valid.
7+
Hence, we hope it is reasonable to make this change in a minor release.
8+
- Fix for cases that can't be encodded correctedly with
9+
`scalarStyle: ScalarStyle.SINGLE_QUOTED`.
210
- Fix YamlEditor `appendToList` and `insertIntoList` functions inserts new item into next yaml item
311
rather than at end of list.
412
([#23](https://github.com/dart-lang/yaml_edit/issues/23))

pkgs/yaml_edit/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ void main() {
2020
}
2121
```
2222

23+
### Example: Converting JSON to YAML (block formatted)
24+
25+
```dart
26+
void main() {
27+
final jsonString = r'''
28+
{
29+
"key": "value",
30+
"list": [
31+
"first",
32+
"second",
33+
"last entry in the list"
34+
],
35+
"map": {
36+
"multiline": "this is a fairly long string with\nline breaks..."
37+
}
38+
}
39+
''';
40+
final jsonValue = json.decode(jsonString);
41+
42+
// Convert jsonValue to YAML
43+
final yamlEditor = YamlEditor('');
44+
yamlEditor.update([], jsonValue);
45+
print(yamlEditor.toString());
46+
}
47+
```
48+
2349
## Testing
2450

2551
Testing is done in two strategies: Unit testing (`/test/editor_test.dart`) and

pkgs/yaml_edit/example/json2yaml.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert' show json;
6+
7+
import 'package:yaml_edit/yaml_edit.dart';
8+
9+
void main() {
10+
final jsonString = r'''
11+
{
12+
"key": "value",
13+
"list": [
14+
"first",
15+
"second",
16+
"last entry in the list"
17+
],
18+
"map": {
19+
"multiline": "this is a fairly long string with\nline breaks..."
20+
}
21+
}
22+
''';
23+
final jsonValue = json.decode(jsonString);
24+
25+
final yamlEditor = YamlEditor('');
26+
yamlEditor.update([], jsonValue);
27+
print(yamlEditor.toString());
28+
}

pkgs/yaml_edit/lib/src/errors.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ Error createAssertionError(String message, String oldYaml, String newYaml) {
9494
> ${newYaml.replaceAll('\n', '\n> ')}
9595
9696
Please file an issue at:
97-
'''
98-
'https://github.com/google/dart-neats/issues/new?labels=pkg%3Ayaml_edit'
99-
'%2C+pending-triage&template=yaml_edit.md\n');
97+
https://github.com/dart-lang/yaml_edit/issues/new?labels=bug
98+
''');
10099
}

pkgs/yaml_edit/lib/src/strings.dart

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,29 @@ String _yamlEncodeDoubleQuoted(String string) {
7070
/// single-quotes.
7171
///
7272
/// It is important that we ensure that [string] is free of unprintable
73-
/// characters by calling [assertValidScalar] before invoking this function.
73+
/// characters by calling [_hasUnprintableCharacters] before invoking this
74+
/// function.
7475
String _tryYamlEncodeSingleQuoted(String string) {
76+
// If [string] contains a newline we'll use double quoted strings instead.
77+
// Single quoted strings can represent newlines, but then we have to use an
78+
// empty line (replace \n with \n\n). But since leading spaces following
79+
// line breaks are ignored, we can't represent "\n ".
80+
// Thus, if the string contains `\n` and we're asked to do single quoted,
81+
// we'll fallback to a double quoted string.
82+
// TODO: Consider if we should make '\n' an unprintedable, this might make
83+
// folded strings into double quoted -- some work is needed here.
84+
if (string.contains('\n')) {
85+
return _yamlEncodeDoubleQuoted(string);
86+
}
7587
final result = string.replaceAll('\'', '\'\'');
7688
return '\'$result\'';
7789
}
7890

7991
/// Generates a YAML-safe folded string.
8092
///
8193
/// It is important that we ensure that [string] is free of unprintable
82-
/// characters by calling [assertValidScalar] before invoking this function.
94+
/// characters by calling [_hasUnprintableCharacters] before invoking this
95+
/// function.
8396
String _tryYamlEncodeFolded(String string, int indentation, String lineEnding) {
8497
String result;
8598

@@ -103,7 +116,8 @@ String _tryYamlEncodeFolded(String string, int indentation, String lineEnding) {
103116
/// Generates a YAML-safe literal string.
104117
///
105118
/// It is important that we ensure that [string] is free of unprintable
106-
/// characters by calling [assertValidScalar] before invoking this function.
119+
/// characters by calling [_hasUnprintableCharacters] before invoking this
120+
/// function.
107121
String _tryYamlEncodeLiteral(
108122
String string, int indentation, String lineEnding) {
109123
final result = '|-\n$string';
@@ -150,7 +164,10 @@ String _yamlEncodeFlowScalar(YamlNode value) {
150164
/// 'null'), in which case we will produce [value] with default styling
151165
/// options.
152166
String yamlEncodeBlockScalar(
153-
YamlNode value, int indentation, String lineEnding) {
167+
YamlNode value,
168+
int indentation,
169+
String lineEnding,
170+
) {
154171
if (value is YamlScalar) {
155172
assertValidScalar(value.value);
156173

@@ -215,7 +232,10 @@ String yamlEncodeFlowString(YamlNode value) {
215232
///
216233
/// If [value] is a [YamlNode], we respect its [style] parameter.
217234
String yamlEncodeBlockString(
218-
YamlNode value, int indentation, String lineEnding) {
235+
YamlNode value,
236+
int indentation,
237+
String lineEnding,
238+
) {
219239
const additionalIndentation = 2;
220240

221241
if (!isBlockNode(value)) return yamlEncodeFlowString(value);
@@ -279,8 +299,7 @@ final Map<int, String> unprintableCharCodes = {
279299
8233: '\\P', // Escaped Unicode paragraph separator (#x2029) character.
280300
};
281301

282-
/// List of escape characters. In particular, \x32 is not included because it
283-
/// can be processed normally.
302+
/// List of escape characters.
284303
///
285304
/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
286305
final Map<int, String> doubleQuoteEscapeChars = {

pkgs/yaml_edit/lib/src/wrap.dart

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@ YamlMap updatedYamlMap(YamlMap map, Function(Map) update) {
2929
/// defined, and [value] is a collection or scalar, the wrapped [YamlNode] will
3030
/// have the respective style, otherwise it defaults to the ANY style.
3131
///
32+
/// If [value] is a [Map] or [List], then [wrapAsYamlNode] will be called
33+
/// recursively on all children, and [collectionStyle]/[scalarStyle] will be
34+
/// applied to any children that are not instances of [YamlNode].
35+
///
3236
/// If a [YamlNode] is passed in, no further wrapping will be done, and the
3337
/// [collectionStyle]/[scalarStyle] will not be applied.
34-
YamlNode wrapAsYamlNode(Object? value,
35-
{CollectionStyle collectionStyle = CollectionStyle.ANY,
36-
ScalarStyle scalarStyle = ScalarStyle.ANY}) {
38+
YamlNode wrapAsYamlNode(
39+
Object? value, {
40+
CollectionStyle collectionStyle = CollectionStyle.ANY,
41+
ScalarStyle scalarStyle = ScalarStyle.ANY,
42+
}) {
3743
if (value is YamlScalar) {
3844
assertValidScalar(value.value);
3945
return value;
@@ -53,9 +59,17 @@ YamlNode wrapAsYamlNode(Object? value,
5359

5460
return value;
5561
} else if (value is Map) {
56-
return YamlMapWrap(value, collectionStyle: collectionStyle);
62+
return YamlMapWrap(
63+
value,
64+
collectionStyle: collectionStyle,
65+
scalarStyle: scalarStyle,
66+
);
5767
} else if (value is List) {
58-
return YamlListWrap(value, collectionStyle: collectionStyle);
68+
return YamlListWrap(
69+
value,
70+
collectionStyle: collectionStyle,
71+
scalarStyle: scalarStyle,
72+
);
5973
} else {
6074
assertValidScalar(value);
6175

@@ -98,24 +112,40 @@ class YamlMapWrap
98112
@override
99113
final SourceSpan span;
100114

101-
factory YamlMapWrap(Map dartMap,
102-
{CollectionStyle collectionStyle = CollectionStyle.ANY,
103-
Object? sourceUrl}) {
115+
factory YamlMapWrap(
116+
Map dartMap, {
117+
CollectionStyle collectionStyle = CollectionStyle.ANY,
118+
ScalarStyle scalarStyle = ScalarStyle.ANY,
119+
Object? sourceUrl,
120+
}) {
104121
final wrappedMap = deepEqualsMap<dynamic, YamlNode>();
105122

106123
for (final entry in dartMap.entries) {
107-
final wrappedKey = wrapAsYamlNode(entry.key);
108-
final wrappedValue = wrapAsYamlNode(entry.value);
124+
final wrappedKey = wrapAsYamlNode(
125+
entry.key,
126+
collectionStyle: collectionStyle,
127+
scalarStyle: scalarStyle,
128+
);
129+
final wrappedValue = wrapAsYamlNode(
130+
entry.value,
131+
collectionStyle: collectionStyle,
132+
scalarStyle: scalarStyle,
133+
);
109134
wrappedMap[wrappedKey] = wrappedValue;
110135
}
111136

112-
return YamlMapWrap._(wrappedMap,
113-
style: collectionStyle, sourceUrl: sourceUrl);
137+
return YamlMapWrap._(
138+
wrappedMap,
139+
style: collectionStyle,
140+
sourceUrl: sourceUrl,
141+
);
114142
}
115143

116-
YamlMapWrap._(this.nodes,
117-
{CollectionStyle style = CollectionStyle.ANY, Object? sourceUrl})
118-
: span = shellSpan(sourceUrl),
144+
YamlMapWrap._(
145+
this.nodes, {
146+
CollectionStyle style = CollectionStyle.ANY,
147+
Object? sourceUrl,
148+
}) : span = shellSpan(sourceUrl),
119149
style = nodes.isEmpty ? CollectionStyle.FLOW : style;
120150

121151
@override
@@ -149,12 +179,23 @@ class YamlListWrap with collection.ListMixin implements YamlList {
149179
throw UnsupportedError('Cannot modify an unmodifiable List');
150180
}
151181

152-
factory YamlListWrap(List dartList,
153-
{CollectionStyle collectionStyle = CollectionStyle.ANY,
154-
Object? sourceUrl}) {
155-
final wrappedList = dartList.map(wrapAsYamlNode).toList();
156-
return YamlListWrap._(wrappedList,
157-
style: collectionStyle, sourceUrl: sourceUrl);
182+
factory YamlListWrap(
183+
List dartList, {
184+
CollectionStyle collectionStyle = CollectionStyle.ANY,
185+
ScalarStyle scalarStyle = ScalarStyle.ANY,
186+
Object? sourceUrl,
187+
}) {
188+
return YamlListWrap._(
189+
dartList
190+
.map((v) => wrapAsYamlNode(
191+
v,
192+
collectionStyle: collectionStyle,
193+
scalarStyle: scalarStyle,
194+
))
195+
.toList(),
196+
style: collectionStyle,
197+
sourceUrl: sourceUrl,
198+
);
158199
}
159200

160201
YamlListWrap._(this.nodes,

pkgs/yaml_edit/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: yaml_edit
2-
version: 2.0.3
2+
version: 2.1.0
33
description: A library for YAML manipulation with comment and whitespace preservation.
44
repository: https://github.com/dart-lang/yaml_edit
55
issue_tracker: https://github.com/dart-lang/yaml_edit/issues

pkgs/yaml_edit/test/string_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:test/test.dart';
6+
import 'package:yaml/yaml.dart';
7+
import 'package:yaml_edit/yaml_edit.dart';
8+
9+
final _testStrings = [
10+
"this is a fairly' long string with\nline breaks",
11+
"whitespace\n after line breaks",
12+
"word",
13+
"foo bar",
14+
"foo\nbar",
15+
"\"",
16+
'\'',
17+
"word\"word",
18+
'word\'word'
19+
];
20+
21+
final _scalarStyles = [
22+
ScalarStyle.ANY,
23+
ScalarStyle.DOUBLE_QUOTED,
24+
//ScalarStyle.FOLDED, // TODO: Fix this test case!
25+
ScalarStyle.LITERAL,
26+
ScalarStyle.PLAIN,
27+
ScalarStyle.SINGLE_QUOTED,
28+
];
29+
30+
void main() {
31+
for (final style in _scalarStyles) {
32+
for (var i = 0; i < _testStrings.length; i++) {
33+
final testString = _testStrings[i];
34+
test('Root $style string (${i + 1})', () {
35+
final yamlEditor = YamlEditor('');
36+
yamlEditor.update([], wrapAsYamlNode(testString, scalarStyle: style));
37+
final yaml = yamlEditor.toString();
38+
expect(loadYaml(yaml), equals(testString));
39+
});
40+
}
41+
}
42+
}

pkgs/yaml_edit/test/wrap_test.dart

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ void main() {
106106
], collectionStyle: CollectionStyle.BLOCK);
107107

108108
expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
109-
expect(list[0].style, equals(CollectionStyle.ANY));
110-
expect(list[1].style, equals(CollectionStyle.ANY));
109+
expect(list[0].style, equals(CollectionStyle.BLOCK));
110+
expect(list[1].style, equals(CollectionStyle.BLOCK));
111111
});
112112

113113
test('wraps nested lists while preserving style', () {
@@ -194,6 +194,45 @@ void main() {
194194
});
195195
});
196196

197+
test('applies collectionStyle recursively', () {
198+
final list = wrapAsYamlNode([
199+
[1, 2, 3],
200+
{
201+
'foo': 'bar',
202+
'nested': [4, 5, 6]
203+
},
204+
], collectionStyle: CollectionStyle.BLOCK);
205+
206+
expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
207+
expect(list[0].style, equals(CollectionStyle.BLOCK));
208+
expect(list[1].style, equals(CollectionStyle.BLOCK));
209+
expect(list[1]['nested'].style, equals(CollectionStyle.BLOCK));
210+
});
211+
212+
test('applies scalarStyle recursively', () {
213+
final list = wrapAsYamlNode([
214+
['a', 'b', 'c'],
215+
{
216+
'foo': 'bar',
217+
},
218+
'hello',
219+
], scalarStyle: ScalarStyle.SINGLE_QUOTED);
220+
221+
expect((list as YamlList).style, equals(CollectionStyle.ANY));
222+
final item1 = list.nodes[0] as YamlList;
223+
final item2 = list.nodes[1] as YamlMap;
224+
final item3 = list.nodes[2] as YamlScalar;
225+
expect(item1.style, equals(CollectionStyle.ANY));
226+
expect(item2.style, equals(CollectionStyle.ANY));
227+
expect(item3.style, equals(ScalarStyle.SINGLE_QUOTED));
228+
229+
final item1entry1 = item1.nodes[0] as YamlScalar;
230+
expect(item1entry1.style, equals(ScalarStyle.SINGLE_QUOTED));
231+
232+
final item2foo = item2.nodes['foo'] as YamlScalar;
233+
expect(item2foo.style, equals(ScalarStyle.SINGLE_QUOTED));
234+
});
235+
197236
group('deepHashCode', () {
198237
test('returns the same result for scalar and its value', () {
199238
final hashCode1 = deepHashCode('foo');

0 commit comments

Comments
 (0)