yerba 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +492 -15
- data/ext/yerba/extconf.rb +87 -30
- data/ext/yerba/include/yerba.h +168 -0
- data/ext/yerba/yerba.c +752 -0
- data/lib/yerba/collection.rb +31 -0
- data/lib/yerba/document.rb +59 -0
- data/lib/yerba/formatting.rb +18 -0
- data/lib/yerba/location.rb +5 -0
- data/lib/yerba/map.rb +166 -0
- data/lib/yerba/scalar.rb +85 -0
- data/lib/yerba/sequence.rb +182 -0
- data/lib/yerba/version.rb +1 -1
- data/lib/yerba.rb +30 -4
- data/rust/Cargo.lock +378 -2
- data/rust/Cargo.toml +5 -1
- data/rust/build.rs +11 -0
- data/rust/cbindgen.toml +27 -0
- data/rust/src/commands/apply.rs +5 -0
- data/rust/src/commands/blank_lines.rs +58 -0
- data/rust/src/commands/check.rs +5 -0
- data/rust/src/commands/delete.rs +35 -0
- data/rust/src/commands/get.rs +194 -0
- data/rust/src/commands/init.rs +89 -0
- data/rust/src/commands/insert.rs +106 -0
- data/rust/src/commands/mate.rs +55 -0
- data/rust/src/commands/mod.rs +349 -0
- data/rust/src/commands/move_item.rs +54 -0
- data/rust/src/commands/move_key.rs +87 -0
- data/rust/src/commands/quote_style.rs +62 -0
- data/rust/src/commands/remove.rs +35 -0
- data/rust/src/commands/rename.rs +37 -0
- data/rust/src/commands/set.rs +59 -0
- data/rust/src/commands/sort.rs +52 -0
- data/rust/src/commands/sort_keys.rs +62 -0
- data/rust/src/commands/version.rs +8 -0
- data/rust/src/document.rs +798 -340
- data/rust/src/error.rs +0 -5
- data/rust/src/ffi.rs +991 -0
- data/rust/src/json.rs +49 -90
- data/rust/src/lib.rs +9 -2
- data/rust/src/main.rs +55 -839
- data/rust/src/selector.rs +241 -0
- data/rust/src/syntax.rs +97 -21
- data/rust/src/yaml_writer.rs +89 -0
- data/rust/src/yerbafile.rs +34 -122
- data/yerba.gemspec +4 -0
- metadata +33 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/// A parsed selector used across selectors, conditions, and select fields.
|
|
2
|
+
///
|
|
3
|
+
/// Selectors starting with `.` are relative to the current item context.
|
|
4
|
+
/// All other selectors are absolute from the document root.
|
|
5
|
+
///
|
|
6
|
+
/// Examples:
|
|
7
|
+
/// ".title" → Relative([Key("title")])
|
|
8
|
+
/// ".speakers[].name" → Relative([Key("speakers"), AllItems, Key("name")])
|
|
9
|
+
/// "database.host" → Absolute([Key("database"), Key("host")])
|
|
10
|
+
/// "[].title" → Absolute([AllItems, Key("title")])
|
|
11
|
+
/// "[0].speakers[]" → Absolute([Index(0), Key("speakers"), AllItems])
|
|
12
|
+
/// "[]" → Absolute([AllItems])
|
|
13
|
+
///
|
|
14
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
15
|
+
pub enum Selector {
|
|
16
|
+
Relative(Vec<SelectorSegment>),
|
|
17
|
+
Absolute(Vec<SelectorSegment>),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
21
|
+
pub enum SelectorSegment {
|
|
22
|
+
Key(String),
|
|
23
|
+
AllItems,
|
|
24
|
+
Index(usize),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl Selector {
|
|
28
|
+
pub fn parse(input: &str) -> Self {
|
|
29
|
+
let input = input.trim();
|
|
30
|
+
|
|
31
|
+
if input.is_empty() {
|
|
32
|
+
return Selector::Absolute(Vec::new());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if let Some(rest) = input.strip_prefix('.') {
|
|
36
|
+
Selector::Relative(parse_segments(rest))
|
|
37
|
+
} else {
|
|
38
|
+
Selector::Absolute(parse_segments(input))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn is_relative(&self) -> bool {
|
|
43
|
+
matches!(self, Selector::Relative(_))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub fn is_absolute(&self) -> bool {
|
|
47
|
+
matches!(self, Selector::Absolute(_))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn segments(&self) -> &[SelectorSegment] {
|
|
51
|
+
match self {
|
|
52
|
+
Selector::Relative(segments) | Selector::Absolute(segments) => segments,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub fn is_empty(&self) -> bool {
|
|
57
|
+
self.segments().is_empty()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn ends_with_bracket(&self) -> bool {
|
|
61
|
+
matches!(
|
|
62
|
+
self.segments().last(),
|
|
63
|
+
Some(SelectorSegment::AllItems | SelectorSegment::Index(_))
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn has_brackets(&self) -> bool {
|
|
68
|
+
self
|
|
69
|
+
.segments()
|
|
70
|
+
.iter()
|
|
71
|
+
.any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub fn has_wildcard(&self) -> bool {
|
|
75
|
+
self.segments().iter().any(|s| matches!(s, SelectorSegment::AllItems))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Split into the container selector (up to and including the last []) and the remaining field selector.
|
|
79
|
+
/// Used to separate "where to search" from "what to extract".
|
|
80
|
+
///
|
|
81
|
+
/// "[].speakers[].name" → ("[].speakers[]", ".name")
|
|
82
|
+
/// "[].title" → ("[]", ".title")
|
|
83
|
+
/// "[]" → ("[]", "")
|
|
84
|
+
/// "database.host" → ("", "database.host")
|
|
85
|
+
pub fn split_at_last_bracket(&self) -> (Selector, Selector) {
|
|
86
|
+
let segments = self.segments();
|
|
87
|
+
|
|
88
|
+
let last_bracket = segments
|
|
89
|
+
.iter()
|
|
90
|
+
.rposition(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)));
|
|
91
|
+
|
|
92
|
+
match last_bracket {
|
|
93
|
+
Some(pos) => {
|
|
94
|
+
let container = segments[..=pos].to_vec();
|
|
95
|
+
let field = segments[pos + 1..].to_vec();
|
|
96
|
+
|
|
97
|
+
(Selector::Absolute(container), Selector::Relative(field))
|
|
98
|
+
}
|
|
99
|
+
None => (Selector::Absolute(Vec::new()), Selector::Absolute(segments.to_vec())),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Split at the first [] bracket — used for condition evaluation.
|
|
104
|
+
/// The condition is evaluated on items at the first container level.
|
|
105
|
+
///
|
|
106
|
+
/// "[].speakers[]" → ("[],", ".speakers[]")
|
|
107
|
+
/// "[].title" → ("[]", ".title")
|
|
108
|
+
/// "[]" → ("[]", "")
|
|
109
|
+
pub fn split_at_first_bracket(&self) -> (Selector, Selector) {
|
|
110
|
+
let segments = self.segments();
|
|
111
|
+
|
|
112
|
+
let first_bracket = segments
|
|
113
|
+
.iter()
|
|
114
|
+
.position(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)));
|
|
115
|
+
|
|
116
|
+
match first_bracket {
|
|
117
|
+
Some(position) => {
|
|
118
|
+
let container = segments[..=position].to_vec();
|
|
119
|
+
let rest = segments[position + 1..].to_vec();
|
|
120
|
+
|
|
121
|
+
(Selector::Absolute(container), Selector::Relative(rest))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
None => (Selector::Absolute(Vec::new()), Selector::Absolute(segments.to_vec())),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Resolve a relative selector against a base absolute selector.
|
|
129
|
+
///
|
|
130
|
+
/// ".title" + "[]" → "[].title"
|
|
131
|
+
/// ".name" + "[].speakers[]" → "[].speakers[].name"
|
|
132
|
+
/// If self is absolute, returns self unchanged.
|
|
133
|
+
pub fn resolve_relative(&self, base: &Selector) -> Selector {
|
|
134
|
+
if self.is_absolute() {
|
|
135
|
+
return self.clone();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let mut segments = base.segments().to_vec();
|
|
139
|
+
segments.extend_from_slice(self.segments());
|
|
140
|
+
|
|
141
|
+
Selector::Absolute(segments)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pub fn to_selector_string(&self) -> String {
|
|
145
|
+
let segments = self.segments();
|
|
146
|
+
|
|
147
|
+
if segments.is_empty() {
|
|
148
|
+
return String::new();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let mut result = String::new();
|
|
152
|
+
|
|
153
|
+
for (index, segment) in segments.iter().enumerate() {
|
|
154
|
+
match segment {
|
|
155
|
+
SelectorSegment::Key(key) => {
|
|
156
|
+
if index > 0 && !matches!(segments.get(index - 1), Some(SelectorSegment::Key(_))) {
|
|
157
|
+
if !result.ends_with('.') {
|
|
158
|
+
result.push('.');
|
|
159
|
+
}
|
|
160
|
+
} else if index > 0 {
|
|
161
|
+
result.push('.');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
result.push_str(key);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
SelectorSegment::AllItems => {
|
|
168
|
+
result.push_str("[]");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
SelectorSegment::Index(i) => {
|
|
172
|
+
result.push_str(&format!("[{}]", i));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
result
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn parse_segments(input: &str) -> Vec<SelectorSegment> {
|
|
182
|
+
let mut segments = Vec::new();
|
|
183
|
+
let mut rest = input;
|
|
184
|
+
|
|
185
|
+
while !rest.is_empty() {
|
|
186
|
+
if rest.starts_with('[') {
|
|
187
|
+
if let Some(close) = rest.find(']') {
|
|
188
|
+
let inner = &rest[1..close];
|
|
189
|
+
|
|
190
|
+
if inner.is_empty() {
|
|
191
|
+
segments.push(SelectorSegment::AllItems);
|
|
192
|
+
} else if let Ok(index) = inner.parse::<usize>() {
|
|
193
|
+
segments.push(SelectorSegment::Index(index));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
rest = &rest[close + 1..];
|
|
197
|
+
|
|
198
|
+
if rest.starts_with('.') {
|
|
199
|
+
rest = &rest[1..];
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
let dot_index = rest.find('.');
|
|
206
|
+
let bracket_index = rest.find('[');
|
|
207
|
+
|
|
208
|
+
let split_at = match (dot_index, bracket_index) {
|
|
209
|
+
(Some(dot), Some(bracket)) => Some(dot.min(bracket)),
|
|
210
|
+
(Some(dot), None) => Some(dot),
|
|
211
|
+
(None, Some(bracket)) => Some(bracket),
|
|
212
|
+
(None, None) => None,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
match split_at {
|
|
216
|
+
Some(index) => {
|
|
217
|
+
let key = &rest[..index];
|
|
218
|
+
|
|
219
|
+
if !key.is_empty() {
|
|
220
|
+
segments.push(SelectorSegment::Key(key.to_string()));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
rest = &rest[index..];
|
|
224
|
+
|
|
225
|
+
if rest.starts_with('.') {
|
|
226
|
+
rest = &rest[1..];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
None => {
|
|
230
|
+
if !rest.is_empty() {
|
|
231
|
+
segments.push(SelectorSegment::Key(rest.to_string()));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
segments
|
|
241
|
+
}
|
data/rust/src/syntax.rs
CHANGED
|
@@ -4,6 +4,55 @@ use rowan::{TextRange, TextSize};
|
|
|
4
4
|
use yaml_parser::ast::{BlockMap, BlockMapEntry};
|
|
5
5
|
use yaml_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
|
|
6
6
|
|
|
7
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
8
|
+
pub struct ScalarValue {
|
|
9
|
+
pub text: String,
|
|
10
|
+
pub kind: SyntaxKind,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[repr(C)]
|
|
14
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
15
|
+
pub enum YerbaValueType {
|
|
16
|
+
Null = 0,
|
|
17
|
+
Boolean = 1,
|
|
18
|
+
Integer = 2,
|
|
19
|
+
Float = 3,
|
|
20
|
+
String = 4,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub fn detect_yaml_type(scalar: &ScalarValue) -> YerbaValueType {
|
|
24
|
+
if scalar.kind != SyntaxKind::PLAIN_SCALAR {
|
|
25
|
+
return YerbaValueType::String;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
detect_yaml_type_from_plain(&scalar.text)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub fn extract_scalar(node: &SyntaxNode) -> Option<ScalarValue> {
|
|
32
|
+
let token = find_scalar_token(node)?;
|
|
33
|
+
|
|
34
|
+
let text = match token.kind() {
|
|
35
|
+
SyntaxKind::PLAIN_SCALAR => token.text().to_string(),
|
|
36
|
+
|
|
37
|
+
SyntaxKind::DOUBLE_QUOTED_SCALAR => {
|
|
38
|
+
let raw = token.text();
|
|
39
|
+
unescape_double_quoted(&raw[1..raw.len() - 1])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
SyntaxKind::SINGLE_QUOTED_SCALAR => {
|
|
43
|
+
let raw = token.text();
|
|
44
|
+
unescape_single_quoted(&raw[1..raw.len() - 1])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_ => return None,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
Some(ScalarValue {
|
|
51
|
+
text,
|
|
52
|
+
kind: token.kind(),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
7
56
|
pub fn is_map_key(token: &SyntaxToken) -> bool {
|
|
8
57
|
token
|
|
9
58
|
.parent_ancestors()
|
|
@@ -141,46 +190,73 @@ pub fn removal_range(node: &SyntaxNode) -> TextRange {
|
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
pub fn is_yaml_non_string(value: &str) -> bool {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Boolean (YAML 1.2)
|
|
150
|
-
if matches!(value, "true" | "True" | "TRUE" | "false" | "False" | "FALSE") {
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
193
|
+
detect_yaml_type_from_plain(value) != YerbaValueType::String
|
|
194
|
+
}
|
|
153
195
|
|
|
154
|
-
|
|
155
|
-
|
|
196
|
+
pub fn is_yaml_truthy(value: &str) -> bool {
|
|
197
|
+
matches!(
|
|
156
198
|
value,
|
|
157
|
-
"
|
|
158
|
-
)
|
|
159
|
-
|
|
199
|
+
"true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" | "y" | "Y"
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
|
|
204
|
+
// Null (YAML 1.1 + 1.2)
|
|
205
|
+
if matches!(value, "null" | "Null" | "NULL" | "~" | "") {
|
|
206
|
+
return YerbaValueType::Null;
|
|
160
207
|
}
|
|
161
208
|
|
|
162
|
-
//
|
|
209
|
+
// Boolean (YAML 1.2 + 1.1)
|
|
163
210
|
if matches!(
|
|
164
211
|
value,
|
|
165
|
-
"
|
|
212
|
+
"true"
|
|
213
|
+
| "True"
|
|
214
|
+
| "TRUE"
|
|
215
|
+
| "false"
|
|
216
|
+
| "False"
|
|
217
|
+
| "FALSE"
|
|
218
|
+
| "yes"
|
|
219
|
+
| "Yes"
|
|
220
|
+
| "YES"
|
|
221
|
+
| "no"
|
|
222
|
+
| "No"
|
|
223
|
+
| "NO"
|
|
224
|
+
| "on"
|
|
225
|
+
| "On"
|
|
226
|
+
| "ON"
|
|
227
|
+
| "off"
|
|
228
|
+
| "Off"
|
|
229
|
+
| "OFF"
|
|
230
|
+
| "y"
|
|
231
|
+
| "Y"
|
|
232
|
+
| "n"
|
|
233
|
+
| "N"
|
|
166
234
|
) {
|
|
167
|
-
return
|
|
235
|
+
return YerbaValueType::Boolean;
|
|
168
236
|
}
|
|
169
237
|
|
|
170
238
|
// Integer
|
|
171
239
|
if value.parse::<i64>().is_ok() {
|
|
172
|
-
return
|
|
240
|
+
return YerbaValueType::Integer;
|
|
173
241
|
}
|
|
174
242
|
|
|
175
243
|
// Octal (0o...) and hex (0x...)
|
|
176
244
|
if value.starts_with("0x") || value.starts_with("0X") || value.starts_with("0o") || value.starts_with("0O") {
|
|
177
|
-
return
|
|
245
|
+
return YerbaValueType::Integer;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Special floats (YAML 1.1 + 1.2)
|
|
249
|
+
if matches!(
|
|
250
|
+
value,
|
|
251
|
+
".inf" | ".Inf" | ".INF" | "-.inf" | "-.Inf" | "-.INF" | "+.inf" | "+.Inf" | "+.INF" | ".nan" | ".NaN" | ".NAN"
|
|
252
|
+
) {
|
|
253
|
+
return YerbaValueType::Float;
|
|
178
254
|
}
|
|
179
255
|
|
|
180
256
|
// Float
|
|
181
257
|
if value.parse::<f64>().is_ok() {
|
|
182
|
-
return
|
|
258
|
+
return YerbaValueType::Float;
|
|
183
259
|
}
|
|
184
260
|
|
|
185
|
-
|
|
261
|
+
YerbaValueType::String
|
|
186
262
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
use serde_json::Value;
|
|
2
|
+
|
|
3
|
+
use crate::QuoteStyle;
|
|
4
|
+
|
|
5
|
+
pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize) -> String {
|
|
6
|
+
match value {
|
|
7
|
+
Value::Object(map) => {
|
|
8
|
+
let prefix = " ".repeat(indent);
|
|
9
|
+
|
|
10
|
+
map
|
|
11
|
+
.iter()
|
|
12
|
+
.map(|(k, v)| match v {
|
|
13
|
+
Value::Array(arr) => {
|
|
14
|
+
let items: Vec<String> = arr
|
|
15
|
+
.iter()
|
|
16
|
+
.map(|item| match item {
|
|
17
|
+
Value::Object(_) => {
|
|
18
|
+
let inner = json_to_yaml_text(item, quote_style, indent + 4);
|
|
19
|
+
format!("{} - {}", prefix, inner.trim_start())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_ => format!("{} - {}", prefix, format_yaml_scalar(item, quote_style)),
|
|
23
|
+
})
|
|
24
|
+
.collect();
|
|
25
|
+
|
|
26
|
+
format!("{}{}:\n{}", prefix, k, items.join("\n"))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Value::Object(_) => {
|
|
30
|
+
let inner = json_to_yaml_text(v, quote_style, indent + 2);
|
|
31
|
+
format!("{}{}:\n{}", prefix, k, inner)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_ => format!("{}{}: {}", prefix, k, format_yaml_scalar(v, quote_style)),
|
|
35
|
+
})
|
|
36
|
+
.collect::<Vec<_>>()
|
|
37
|
+
.join("\n")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_ => {
|
|
41
|
+
let prefix = " ".repeat(indent);
|
|
42
|
+
|
|
43
|
+
format!("{}{}", prefix, format_yaml_scalar(value, quote_style))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn format_yaml_scalar(value: &Value, quote_style: &QuoteStyle) -> String {
|
|
49
|
+
match value {
|
|
50
|
+
Value::Null => "null".to_string(),
|
|
51
|
+
Value::Bool(boolean) => boolean.to_string(),
|
|
52
|
+
Value::Number(number) => number.to_string(),
|
|
53
|
+
Value::String(string) => match quote_style {
|
|
54
|
+
QuoteStyle::Double => {
|
|
55
|
+
let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
|
|
56
|
+
|
|
57
|
+
format!("\"{}\"", escaped)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
QuoteStyle::Single => {
|
|
61
|
+
let escaped = string.replace('\'', "''");
|
|
62
|
+
|
|
63
|
+
format!("'{}'", escaped)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
QuoteStyle::Plain => {
|
|
67
|
+
if crate::syntax::is_yaml_non_string(string) {
|
|
68
|
+
format!("\"{}\"", string.replace('"', "\\\""))
|
|
69
|
+
} else {
|
|
70
|
+
string.clone()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_ => {
|
|
75
|
+
let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
|
|
76
|
+
|
|
77
|
+
format!("\"{}\"", escaped)
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
Value::Array(array) => {
|
|
82
|
+
let items: Vec<String> = array.iter().map(|item| format_yaml_scalar(item, quote_style)).collect();
|
|
83
|
+
|
|
84
|
+
format!("[{}]", items.join(", "))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Value::Object(_) => format!("{:?}", value),
|
|
88
|
+
}
|
|
89
|
+
}
|