yerba 0.8.0-arm-linux-gnu → 0.9.0-arm-linux-gnu
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 +170 -19
- data/exe/arm-linux-gnu/yerba +0 -0
- data/ext/yerba/include/yerba.h +7 -0
- data/ext/yerba/yerba.c +54 -5
- data/lib/yerba/3.2/yerba.so +0 -0
- data/lib/yerba/3.3/yerba.so +0 -0
- data/lib/yerba/3.4/yerba.so +0 -0
- data/lib/yerba/4.0/yerba.so +0 -0
- data/lib/yerba/formatting.rb +9 -13
- data/lib/yerba/map.rb +11 -8
- data/lib/yerba/sequence.rb +2 -7
- data/lib/yerba/version.rb +1 -1
- data/rust/Cargo.lock +2 -342
- data/rust/Cargo.toml +15 -6
- data/rust/src/commands/blank_lines.rs +23 -3
- data/rust/src/commands/directives.rs +4 -4
- data/rust/src/commands/get.rs +7 -10
- data/rust/src/commands/init.rs +10 -4
- data/rust/src/commands/insert.rs +18 -12
- data/rust/src/commands/location.rs +2 -2
- data/rust/src/commands/mate.rs +7 -7
- data/rust/src/commands/mod.rs +118 -90
- data/rust/src/commands/move_key.rs +9 -6
- data/rust/src/commands/quote_style.rs +4 -5
- data/rust/src/commands/schema.rs +6 -6
- data/rust/src/commands/selectors.rs +2 -2
- data/rust/src/commands/set.rs +18 -7
- data/rust/src/commands/sort.rs +36 -42
- data/rust/src/commands/sort_keys.rs +2 -2
- data/rust/src/commands/ui.rs +156 -0
- data/rust/src/commands/unique.rs +7 -7
- data/rust/src/commands/version.rs +13 -3
- data/rust/src/document/insert.rs +25 -9
- data/rust/src/document/mod.rs +7 -3
- data/rust/src/document/set.rs +115 -33
- data/rust/src/document/style.rs +131 -11
- data/rust/src/error.rs +27 -0
- data/rust/src/ffi.rs +55 -12
- data/rust/src/lib.rs +12 -2
- data/rust/src/quote_style.rs +12 -11
- data/rust/src/syntax.rs +285 -11
- data/rust/src/validation.rs +30 -0
- data/rust/src/yaml_writer.rs +9 -21
- data/rust/src/yerbafile.rs +76 -4
- metadata +4 -2
data/rust/src/quote_style.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
use clap::ValueEnum;
|
|
2
1
|
use yaml_parser::SyntaxKind;
|
|
3
2
|
|
|
4
|
-
#[derive(Debug, Clone, PartialEq
|
|
3
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
4
|
+
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
|
5
5
|
pub enum KeyStyle {
|
|
6
6
|
/// Unquoted key (host:)
|
|
7
7
|
Plain,
|
|
@@ -34,33 +34,34 @@ impl KeyStyle {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
#[derive(Debug, Clone, PartialEq
|
|
37
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
38
|
+
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
|
38
39
|
pub enum QuoteStyle {
|
|
39
40
|
/// Unquoted value (host: localhost)
|
|
40
41
|
Plain,
|
|
41
42
|
/// Single-quoted value (host: 'localhost')
|
|
42
|
-
#[value(alias = "single-quoted")]
|
|
43
|
+
#[cfg_attr(feature = "cli", value(alias = "single-quoted"))]
|
|
43
44
|
Single,
|
|
44
45
|
/// Double-quoted value (host: "localhost")
|
|
45
|
-
#[value(alias = "double-quoted")]
|
|
46
|
+
#[cfg_attr(feature = "cli", value(alias = "double-quoted"))]
|
|
46
47
|
Double,
|
|
47
48
|
/// Literal block scalar, strip trailing newline (|-)
|
|
48
|
-
#[value(alias = "block-literal", alias = "|-")]
|
|
49
|
+
#[cfg_attr(feature = "cli", value(alias = "block-literal", alias = "|-"))]
|
|
49
50
|
Literal,
|
|
50
51
|
/// Literal block scalar, keep one trailing newline (|)
|
|
51
|
-
#[value(alias = "|")]
|
|
52
|
+
#[cfg_attr(feature = "cli", value(alias = "|"))]
|
|
52
53
|
LiteralClip,
|
|
53
54
|
/// Literal block scalar, keep all trailing newlines (|+)
|
|
54
|
-
#[value(alias = "|+")]
|
|
55
|
+
#[cfg_attr(feature = "cli", value(alias = "|+"))]
|
|
55
56
|
LiteralKeep,
|
|
56
57
|
/// Folded block scalar, strip trailing newline (>-)
|
|
57
|
-
#[value(alias = "block-folded", alias = ">-")]
|
|
58
|
+
#[cfg_attr(feature = "cli", value(alias = "block-folded", alias = ">-"))]
|
|
58
59
|
Folded,
|
|
59
60
|
/// Folded block scalar, keep one trailing newline (>)
|
|
60
|
-
#[value(alias = ">")]
|
|
61
|
+
#[cfg_attr(feature = "cli", value(alias = ">"))]
|
|
61
62
|
FoldedClip,
|
|
62
63
|
/// Folded block scalar, keep all trailing newlines (>+)
|
|
63
|
-
#[value(alias = ">+")]
|
|
64
|
+
#[cfg_attr(feature = "cli", value(alias = ">+"))]
|
|
64
65
|
FoldedKeep,
|
|
65
66
|
}
|
|
66
67
|
|
data/rust/src/syntax.rs
CHANGED
|
@@ -33,7 +33,7 @@ pub fn detect_yaml_type(scalar: &ScalarValue) -> YerbaValueType {
|
|
|
33
33
|
|
|
34
34
|
pub fn raw_scalar_value(token: &SyntaxToken) -> Option<String> {
|
|
35
35
|
match token.kind() {
|
|
36
|
-
SyntaxKind::PLAIN_SCALAR => Some(token.text()
|
|
36
|
+
SyntaxKind::PLAIN_SCALAR => Some(fold_flow_scalar(token.text())),
|
|
37
37
|
|
|
38
38
|
SyntaxKind::DOUBLE_QUOTED_SCALAR => {
|
|
39
39
|
let text = token.text();
|
|
@@ -65,8 +65,16 @@ pub fn extract_scalar(node: &SyntaxNode) -> Option<ScalarValue> {
|
|
|
65
65
|
.filter_map(|element| element.into_token())
|
|
66
66
|
.find(|token| token.kind() == SyntaxKind::BLOCK_SCALAR_TEXT)?;
|
|
67
67
|
|
|
68
|
+
let dedented = dedent_block_scalar(block_token.text());
|
|
69
|
+
|
|
70
|
+
let folded = node
|
|
71
|
+
.descendants()
|
|
72
|
+
.find(|descendant| descendant.kind() == SyntaxKind::BLOCK_SCALAR)
|
|
73
|
+
.map(|scalar| scalar.text().to_string().trim_start().starts_with('>'))
|
|
74
|
+
.unwrap_or(false);
|
|
75
|
+
|
|
68
76
|
Some(ScalarValue {
|
|
69
|
-
text:
|
|
77
|
+
text: if folded { fold_flow_scalar(&dedented) } else { dedented },
|
|
70
78
|
kind: SyntaxKind::BLOCK_SCALAR_TEXT,
|
|
71
79
|
file_path: None,
|
|
72
80
|
selector: None,
|
|
@@ -168,10 +176,7 @@ pub fn find_scalar_token(node: &SyntaxNode) -> Option<SyntaxToken> {
|
|
|
168
176
|
|
|
169
177
|
pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
|
|
170
178
|
match kind {
|
|
171
|
-
SyntaxKind::DOUBLE_QUOTED_SCALAR => {
|
|
172
|
-
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
|
|
173
|
-
format!("\"{}\"", escaped)
|
|
174
|
-
}
|
|
179
|
+
SyntaxKind::DOUBLE_QUOTED_SCALAR => format!("\"{}\"", escape_double_quoted(value)),
|
|
175
180
|
|
|
176
181
|
SyntaxKind::SINGLE_QUOTED_SCALAR => {
|
|
177
182
|
let escaped = value.replace('\'', "''");
|
|
@@ -182,14 +187,168 @@ pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
|
|
|
182
187
|
}
|
|
183
188
|
}
|
|
184
189
|
|
|
185
|
-
pub fn
|
|
186
|
-
|
|
190
|
+
pub fn is_control_character(character: char) -> bool {
|
|
191
|
+
matches!(character as u32, 0x00..=0x08 | 0x0b | 0x0c | 0x0e..=0x1f | 0x7f | 0x80..=0x84 | 0x86..=0x9f)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pub fn is_single_quotable(value: &str) -> bool {
|
|
195
|
+
!value.contains('\n') && !value.contains('\r') && !value.chars().any(is_control_character)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pub fn escape_double_quoted(value: &str) -> String {
|
|
199
|
+
let mut result = String::with_capacity(value.len());
|
|
200
|
+
|
|
201
|
+
for character in value.chars() {
|
|
202
|
+
match character {
|
|
203
|
+
'\\' => result.push_str("\\\\"),
|
|
204
|
+
'"' => result.push_str("\\\""),
|
|
205
|
+
'\n' => result.push_str("\\n"),
|
|
206
|
+
'\r' => result.push_str("\\r"),
|
|
207
|
+
'\t' => result.push_str("\\t"),
|
|
208
|
+
_ if is_control_character(character) => result.push_str(&format!("\\x{:02x}", character as u32)),
|
|
209
|
+
_ => result.push(character),
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
result
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const LEADING_INDICATORS: [char; 16] = ['#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`', ',', '[', ']', '{', '}'];
|
|
217
|
+
const FLOW_INDICATORS: [char; 5] = [',', '[', ']', '{', '}'];
|
|
218
|
+
|
|
219
|
+
pub fn is_plain_safe(value: &str) -> bool {
|
|
220
|
+
if value.is_empty() || value != value.trim() {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if value.contains('\n') || value.contains('\t') {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if value.chars().any(is_control_character) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if value.contains(": ") || value.ends_with(':') {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if value.contains(" #") {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if value.starts_with("---") || value.starts_with("...") {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let mut characters = value.chars();
|
|
245
|
+
let first = characters.next().expect("value is non-empty");
|
|
246
|
+
|
|
247
|
+
if LEADING_INDICATORS.contains(&first) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if matches!(first, '-' | '?' | ':') {
|
|
252
|
+
return matches!(characters.next(), Some(next) if next != ' ');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
pub fn is_plain_safe_in_flow(value: &str) -> bool {
|
|
259
|
+
is_plain_safe(value) && !value.contains(FLOW_INDICATORS)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pub fn is_quoted_scalar(value: &str) -> bool {
|
|
263
|
+
let bytes = value.as_bytes();
|
|
264
|
+
|
|
265
|
+
if bytes.len() < 2 {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
match (bytes[0], bytes[bytes.len() - 1]) {
|
|
270
|
+
(b'"', b'"') => {
|
|
271
|
+
let interior = &value[1..value.len() - 1];
|
|
272
|
+
let mut escaped = false;
|
|
273
|
+
|
|
274
|
+
for character in interior.chars() {
|
|
275
|
+
if escaped {
|
|
276
|
+
escaped = false;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
match character {
|
|
281
|
+
'\\' => escaped = true,
|
|
282
|
+
'"' => return false,
|
|
283
|
+
_ => {}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
!escaped
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
(b'\'', b'\'') => {
|
|
291
|
+
let interior = &value[1..value.len() - 1];
|
|
292
|
+
let mut characters = interior.chars().peekable();
|
|
293
|
+
|
|
294
|
+
while let Some(character) = characters.next() {
|
|
295
|
+
if character == '\'' {
|
|
296
|
+
if characters.peek() == Some(&'\'') {
|
|
297
|
+
characters.next();
|
|
298
|
+
} else {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_ => false,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
pub fn is_flow_collection(value: &str) -> bool {
|
|
312
|
+
(value.starts_with('[') && value.ends_with(']')) || (value.starts_with('{') && value.ends_with('}'))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
pub fn is_inline_scalar_safe(value: &str) -> bool {
|
|
316
|
+
if value.is_empty() {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if value.contains('\n') || value.starts_with("- ") {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
is_quoted_scalar(value) || is_flow_collection(value) || is_plain_safe(value)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
pub fn needs_quoting(value: &str) -> bool {
|
|
328
|
+
is_yaml_non_string(value) || !is_plain_safe(value)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
pub fn needs_quoting_in_flow(value: &str) -> bool {
|
|
332
|
+
is_yaml_non_string(value) || !is_plain_safe_in_flow(value)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
pub fn quote_scalar(value: &str) -> String {
|
|
336
|
+
if needs_quoting(value) {
|
|
187
337
|
format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
188
338
|
} else {
|
|
189
339
|
value.to_string()
|
|
190
340
|
}
|
|
191
341
|
}
|
|
192
342
|
|
|
343
|
+
pub fn quote_scalar_styled(value: &str, style: Option<&str>) -> String {
|
|
344
|
+
match style {
|
|
345
|
+
Some("double") => format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR),
|
|
346
|
+
Some("single") if is_single_quotable(value) => format_scalar_value(value, SyntaxKind::SINGLE_QUOTED_SCALAR),
|
|
347
|
+
|
|
348
|
+
_ => quote_scalar(value),
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
193
352
|
pub fn extract_scalar_text(node: &SyntaxNode) -> Option<String> {
|
|
194
353
|
extract_scalar(node).map(|scalar| scalar.text)
|
|
195
354
|
}
|
|
@@ -212,11 +371,114 @@ pub fn dedent_block_scalar(text: &str) -> String {
|
|
|
212
371
|
dedented.trim().to_string()
|
|
213
372
|
}
|
|
214
373
|
|
|
374
|
+
fn push_hex_escape(result: &mut String, characters: &mut Scan<'_>, digits: usize) {
|
|
375
|
+
let escape: String = characters.clone().take(digits).collect();
|
|
376
|
+
|
|
377
|
+
let decoded = (escape.len() == digits && escape.chars().all(|character| character.is_ascii_hexdigit()))
|
|
378
|
+
.then(|| u32::from_str_radix(&escape, 16).ok().and_then(char::from_u32))
|
|
379
|
+
.flatten();
|
|
380
|
+
|
|
381
|
+
match decoded {
|
|
382
|
+
Some(character) => {
|
|
383
|
+
for _ in 0..digits {
|
|
384
|
+
characters.next();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
result.push(character);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
None => {
|
|
391
|
+
result.push('\\');
|
|
392
|
+
result.push(match digits {
|
|
393
|
+
2 => 'x',
|
|
394
|
+
4 => 'u',
|
|
395
|
+
_ => 'U',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
type Scan<'a> = std::iter::Peekable<std::str::Chars<'a>>;
|
|
402
|
+
|
|
403
|
+
fn skip_spaces(characters: &mut Scan<'_>) {
|
|
404
|
+
while matches!(characters.peek(), Some(' ' | '\t')) {
|
|
405
|
+
characters.next();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fn fold_break(result: &mut String, characters: &mut Scan<'_>) {
|
|
410
|
+
while result.ends_with(' ') || result.ends_with('\t') {
|
|
411
|
+
result.pop();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let mut breaks = 1;
|
|
415
|
+
|
|
416
|
+
loop {
|
|
417
|
+
skip_spaces(characters);
|
|
418
|
+
|
|
419
|
+
match characters.peek() {
|
|
420
|
+
Some('\n') => {
|
|
421
|
+
characters.next();
|
|
422
|
+
breaks += 1;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
Some('\r') => {
|
|
426
|
+
characters.next();
|
|
427
|
+
characters.next_if_eq(&'\n');
|
|
428
|
+
breaks += 1;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_ => break,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if breaks == 1 {
|
|
436
|
+
result.push(' ');
|
|
437
|
+
} else {
|
|
438
|
+
result.extend(std::iter::repeat_n('\n', breaks - 1));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
pub fn fold_flow_scalar(text: &str) -> String {
|
|
443
|
+
if !text.contains('\n') && !text.contains('\r') {
|
|
444
|
+
return text.to_string();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let mut result = String::with_capacity(text.len());
|
|
448
|
+
let mut characters = text.chars().peekable();
|
|
449
|
+
|
|
450
|
+
while let Some(character) = characters.next() {
|
|
451
|
+
match character {
|
|
452
|
+
'\n' => fold_break(&mut result, &mut characters),
|
|
453
|
+
|
|
454
|
+
'\r' => {
|
|
455
|
+
characters.next_if_eq(&'\n');
|
|
456
|
+
fold_break(&mut result, &mut characters);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_ => result.push(character),
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
result
|
|
464
|
+
}
|
|
465
|
+
|
|
215
466
|
pub fn unescape_double_quoted(text: &str) -> String {
|
|
216
467
|
let mut result = String::with_capacity(text.len());
|
|
217
|
-
let mut chars = text.chars();
|
|
468
|
+
let mut chars = text.chars().peekable();
|
|
218
469
|
|
|
219
470
|
while let Some(character) = chars.next() {
|
|
471
|
+
if character == '\n' {
|
|
472
|
+
fold_break(&mut result, &mut chars);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if character == '\r' {
|
|
477
|
+
chars.next_if_eq(&'\n');
|
|
478
|
+
fold_break(&mut result, &mut chars);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
220
482
|
if character == '\\' {
|
|
221
483
|
match chars.next() {
|
|
222
484
|
Some('n') => result.push('\n'),
|
|
@@ -232,7 +494,19 @@ pub fn unescape_double_quoted(text: &str) -> String {
|
|
|
232
494
|
Some('v') => result.push('\u{0b}'),
|
|
233
495
|
Some(' ') => result.push(' '),
|
|
234
496
|
Some('_') => result.push('\u{a0}'),
|
|
235
|
-
Some('
|
|
497
|
+
Some('N') => result.push('\u{85}'),
|
|
498
|
+
Some('L') => result.push('\u{2028}'),
|
|
499
|
+
Some('P') => result.push('\u{2029}'),
|
|
500
|
+
Some('x') => push_hex_escape(&mut result, &mut chars, 2),
|
|
501
|
+
Some('u') => push_hex_escape(&mut result, &mut chars, 4),
|
|
502
|
+
Some('U') => push_hex_escape(&mut result, &mut chars, 8),
|
|
503
|
+
Some('\n') => skip_spaces(&mut chars),
|
|
504
|
+
|
|
505
|
+
Some('\r') => {
|
|
506
|
+
chars.next_if_eq(&'\n');
|
|
507
|
+
skip_spaces(&mut chars);
|
|
508
|
+
}
|
|
509
|
+
|
|
236
510
|
Some(other) => {
|
|
237
511
|
result.push('\\');
|
|
238
512
|
result.push(other);
|
|
@@ -248,7 +522,7 @@ pub fn unescape_double_quoted(text: &str) -> String {
|
|
|
248
522
|
}
|
|
249
523
|
|
|
250
524
|
pub fn unescape_single_quoted(text: &str) -> String {
|
|
251
|
-
text.replace("''", "'")
|
|
525
|
+
fold_flow_scalar(&text.replace("''", "'"))
|
|
252
526
|
}
|
|
253
527
|
|
|
254
528
|
pub fn line_at(source: &str, offset: usize) -> usize {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
use crate::syntax::is_control_character;
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone)]
|
|
4
|
+
pub struct ControlCharacter {
|
|
5
|
+
pub character: char,
|
|
6
|
+
pub line: usize,
|
|
7
|
+
pub column: usize,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl std::fmt::Display for ControlCharacter {
|
|
11
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
12
|
+
write!(f, "U+{:04X} at line {}, column {}", self.character as u32, self.line, self.column)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub fn find_control_characters(source: &str) -> Vec<ControlCharacter> {
|
|
17
|
+
source
|
|
18
|
+
.lines()
|
|
19
|
+
.enumerate()
|
|
20
|
+
.flat_map(|(index, line)| {
|
|
21
|
+
line.chars().enumerate().filter_map(move |(column, character)| {
|
|
22
|
+
is_control_character(character).then_some(ControlCharacter {
|
|
23
|
+
character,
|
|
24
|
+
line: index + 1,
|
|
25
|
+
column: column + 1,
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
.collect()
|
|
30
|
+
}
|
data/rust/src/yaml_writer.rs
CHANGED
|
@@ -10,6 +10,8 @@ pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize)
|
|
|
10
10
|
map
|
|
11
11
|
.iter()
|
|
12
12
|
.map(|(k, v)| match v {
|
|
13
|
+
Value::Array(arr) if arr.is_empty() => format!("{}{}: []", prefix, k),
|
|
14
|
+
|
|
13
15
|
Value::Array(arr) => {
|
|
14
16
|
let items: Vec<String> = arr
|
|
15
17
|
.iter()
|
|
@@ -26,6 +28,8 @@ pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize)
|
|
|
26
28
|
format!("{}{}:\n{}", prefix, k, items.join("\n"))
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
Value::Object(inner_map) if inner_map.is_empty() => format!("{}{}: {{}}", prefix, k),
|
|
32
|
+
|
|
29
33
|
Value::Object(_) => {
|
|
30
34
|
let inner = json_to_yaml_text(v, quote_style, indent + 2);
|
|
31
35
|
format!("{}{}:\n{}", prefix, k, inner)
|
|
@@ -69,8 +73,8 @@ pub fn yaml_value_to_flow_text(value: &yaml_serde::Value) -> String {
|
|
|
69
73
|
yaml_serde::Value::Number(number) => number.to_string(),
|
|
70
74
|
|
|
71
75
|
yaml_serde::Value::String(string) => {
|
|
72
|
-
if crate::syntax::
|
|
73
|
-
|
|
76
|
+
if crate::syntax::needs_quoting_in_flow(string) {
|
|
77
|
+
crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
74
78
|
} else {
|
|
75
79
|
string.clone()
|
|
76
80
|
}
|
|
@@ -149,31 +153,15 @@ fn format_yaml_scalar(value: &Value, quote_style: &QuoteStyle) -> String {
|
|
|
149
153
|
Value::Bool(boolean) => boolean.to_string(),
|
|
150
154
|
Value::Number(number) => number.to_string(),
|
|
151
155
|
Value::String(string) => match quote_style {
|
|
152
|
-
QuoteStyle::
|
|
153
|
-
let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
|
|
154
|
-
|
|
155
|
-
format!("\"{}\"", escaped)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
QuoteStyle::Single => {
|
|
156
|
+
QuoteStyle::Single if crate::syntax::is_single_quotable(string) => {
|
|
159
157
|
let escaped = string.replace('\'', "''");
|
|
160
158
|
|
|
161
159
|
format!("'{}'", escaped)
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
QuoteStyle::Plain =>
|
|
165
|
-
if crate::syntax::is_yaml_non_string(string) {
|
|
166
|
-
format!("\"{}\"", string.replace('"', "\\\""))
|
|
167
|
-
} else {
|
|
168
|
-
string.clone()
|
|
169
|
-
}
|
|
170
|
-
}
|
|
162
|
+
QuoteStyle::Plain if !crate::syntax::needs_quoting(string) => string.clone(),
|
|
171
163
|
|
|
172
|
-
_ =>
|
|
173
|
-
let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
|
|
174
|
-
|
|
175
|
-
format!("\"{}\"", escaped)
|
|
176
|
-
}
|
|
164
|
+
_ => crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR),
|
|
177
165
|
},
|
|
178
166
|
|
|
179
167
|
Value::Array(array) => {
|
data/rust/src/yerbafile.rs
CHANGED
|
@@ -42,6 +42,7 @@ pub enum PipelineStep {
|
|
|
42
42
|
Unique(UniqueConfig),
|
|
43
43
|
Schema(SchemaConfig),
|
|
44
44
|
FinalNewline(FinalNewlineConfig),
|
|
45
|
+
Escapes(EscapesConfig),
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -59,6 +60,12 @@ pub struct BlankLinesConfig {
|
|
|
59
60
|
#[serde(default)]
|
|
60
61
|
pub path: Option<String>,
|
|
61
62
|
pub count: usize,
|
|
63
|
+
#[serde(default)]
|
|
64
|
+
pub before: Vec<String>,
|
|
65
|
+
#[serde(default)]
|
|
66
|
+
pub after: Vec<String>,
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
pub skip_empty: bool,
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -102,6 +109,12 @@ pub struct FinalNewlineConfig {
|
|
|
102
109
|
pub count: usize,
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
113
|
+
pub struct EscapesConfig {
|
|
114
|
+
#[serde(default)]
|
|
115
|
+
pub path: Option<String>,
|
|
116
|
+
}
|
|
117
|
+
|
|
105
118
|
fn default_one() -> usize {
|
|
106
119
|
1
|
|
107
120
|
}
|
|
@@ -199,13 +212,18 @@ impl<'de> Deserialize<'de> for PipelineStep {
|
|
|
199
212
|
return Ok(PipelineStep::Schema(config));
|
|
200
213
|
}
|
|
201
214
|
|
|
215
|
+
if let Some(value) = mapping.get(yaml_serde::Value::String("escapes".to_string())) {
|
|
216
|
+
let config: EscapesConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
217
|
+
return Ok(PipelineStep::Escapes(config));
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
if let Some(value) = mapping.get(yaml_serde::Value::String("final_newline".to_string())) {
|
|
203
221
|
let config: FinalNewlineConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
204
222
|
return Ok(PipelineStep::FinalNewline(config));
|
|
205
223
|
}
|
|
206
224
|
|
|
207
225
|
Err(serde::de::Error::custom(
|
|
208
|
-
"unknown pipeline step: expected sort_keys, quote_style, collection_style, sequence_indent, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, schema, or final_newline",
|
|
226
|
+
"unknown pipeline step: expected sort_keys, quote_style, collection_style, sequence_indent, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, schema, escapes, or final_newline",
|
|
209
227
|
))
|
|
210
228
|
}
|
|
211
229
|
}
|
|
@@ -246,6 +264,8 @@ pub struct SetConfig {
|
|
|
246
264
|
pub value: String,
|
|
247
265
|
#[serde(default)]
|
|
248
266
|
pub condition: Option<String>,
|
|
267
|
+
#[serde(default)]
|
|
268
|
+
pub plain: bool,
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -254,6 +274,8 @@ pub struct InsertConfig {
|
|
|
254
274
|
pub value: String,
|
|
255
275
|
#[serde(default)]
|
|
256
276
|
pub condition: Option<String>,
|
|
277
|
+
#[serde(default)]
|
|
278
|
+
pub plain: bool,
|
|
257
279
|
}
|
|
258
280
|
|
|
259
281
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -432,6 +454,14 @@ impl Yerbafile {
|
|
|
432
454
|
|
|
433
455
|
let original = document.to_string();
|
|
434
456
|
|
|
457
|
+
if let Some(error) = control_character_error(&original) {
|
|
458
|
+
return RuleResult {
|
|
459
|
+
file: file.to_string(),
|
|
460
|
+
changed: false,
|
|
461
|
+
error: Some(error),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
435
465
|
if run_global {
|
|
436
466
|
if let Err(error) = execute_pipeline(&mut document, &self.pipeline, None, file, self) {
|
|
437
467
|
return RuleResult {
|
|
@@ -513,6 +543,14 @@ impl Yerbafile {
|
|
|
513
543
|
|
|
514
544
|
let original = document.to_string();
|
|
515
545
|
|
|
546
|
+
if let Some(error) = control_character_error(&original) {
|
|
547
|
+
return RuleResult {
|
|
548
|
+
file: file.to_string(),
|
|
549
|
+
changed: false,
|
|
550
|
+
error: Some(error),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
516
554
|
if let Err(error) = execute_pipeline(&mut document, &self.pipeline, None, file, self) {
|
|
517
555
|
return RuleResult {
|
|
518
556
|
file: file.to_string(),
|
|
@@ -574,6 +612,11 @@ impl Yerbafile {
|
|
|
574
612
|
|
|
575
613
|
pub fn apply_to_document(&self, document: &mut Document, file_path: &str) -> Result<bool, YerbaError> {
|
|
576
614
|
let original = document.to_string();
|
|
615
|
+
|
|
616
|
+
if let Some(error) = control_character_error(&original) {
|
|
617
|
+
return Err(error);
|
|
618
|
+
}
|
|
619
|
+
|
|
577
620
|
let relative_path = self.relativize_path(file_path);
|
|
578
621
|
let match_path = relative_path.as_deref().unwrap_or(file_path);
|
|
579
622
|
|
|
@@ -603,6 +646,12 @@ impl Yerbafile {
|
|
|
603
646
|
}
|
|
604
647
|
}
|
|
605
648
|
|
|
649
|
+
fn control_character_error(source: &str) -> Option<YerbaError> {
|
|
650
|
+
let found = crate::validation::find_control_characters(source);
|
|
651
|
+
|
|
652
|
+
(!found.is_empty()).then_some(YerbaError::ControlCharacters(found))
|
|
653
|
+
}
|
|
654
|
+
|
|
606
655
|
fn execute_pipeline(document: &mut Document, steps: &[PipelineStep], base_path: Option<&str>, file: &str, yerbafile: &Yerbafile) -> Result<(), YerbaError> {
|
|
607
656
|
use crate::document::style::StyleEnforcement;
|
|
608
657
|
|
|
@@ -704,7 +753,11 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
704
753
|
}
|
|
705
754
|
}
|
|
706
755
|
|
|
707
|
-
|
|
756
|
+
if config.plain {
|
|
757
|
+
document.set_plain(&full_path, &config.value)
|
|
758
|
+
} else {
|
|
759
|
+
document.set(&full_path, &config.value)
|
|
760
|
+
}
|
|
708
761
|
}
|
|
709
762
|
|
|
710
763
|
PipelineStep::Insert(config) => {
|
|
@@ -718,7 +771,13 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
718
771
|
}
|
|
719
772
|
}
|
|
720
773
|
|
|
721
|
-
|
|
774
|
+
let value = if config.plain {
|
|
775
|
+
config.value.clone()
|
|
776
|
+
} else {
|
|
777
|
+
crate::syntax::quote_scalar(&config.value)
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
document.insert_into(&full_path, &value, crate::InsertPosition::Last)
|
|
722
781
|
}
|
|
723
782
|
|
|
724
783
|
PipelineStep::Delete(config) => {
|
|
@@ -784,7 +843,13 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
784
843
|
PipelineStep::BlankLines(config) => {
|
|
785
844
|
let full_path = resolve_step_path(base_path, config.path.as_deref());
|
|
786
845
|
|
|
787
|
-
|
|
846
|
+
let options = crate::BlankLineOptions {
|
|
847
|
+
before: config.before.clone(),
|
|
848
|
+
after: config.after.clone(),
|
|
849
|
+
skip_empty: config.skip_empty,
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
document.enforce_blank_lines_with(&full_path, config.count, &options)
|
|
788
853
|
}
|
|
789
854
|
|
|
790
855
|
PipelineStep::Sort(config) => {
|
|
@@ -848,6 +913,13 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
848
913
|
}
|
|
849
914
|
|
|
850
915
|
PipelineStep::FinalNewline(config) => document.enforce_final_newline(config.count),
|
|
916
|
+
|
|
917
|
+
PipelineStep::Escapes(config) => {
|
|
918
|
+
let full_path = resolve_step_path(base_path, config.path.as_deref());
|
|
919
|
+
let scope = if full_path.is_empty() { None } else { Some(full_path.as_str()) };
|
|
920
|
+
|
|
921
|
+
document.decode_escapes(scope).map(|_| ())
|
|
922
|
+
}
|
|
851
923
|
}
|
|
852
924
|
}
|
|
853
925
|
|