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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +170 -19
  3. data/exe/arm-linux-gnu/yerba +0 -0
  4. data/ext/yerba/include/yerba.h +7 -0
  5. data/ext/yerba/yerba.c +54 -5
  6. data/lib/yerba/3.2/yerba.so +0 -0
  7. data/lib/yerba/3.3/yerba.so +0 -0
  8. data/lib/yerba/3.4/yerba.so +0 -0
  9. data/lib/yerba/4.0/yerba.so +0 -0
  10. data/lib/yerba/formatting.rb +9 -13
  11. data/lib/yerba/map.rb +11 -8
  12. data/lib/yerba/sequence.rb +2 -7
  13. data/lib/yerba/version.rb +1 -1
  14. data/rust/Cargo.lock +2 -342
  15. data/rust/Cargo.toml +15 -6
  16. data/rust/src/commands/blank_lines.rs +23 -3
  17. data/rust/src/commands/directives.rs +4 -4
  18. data/rust/src/commands/get.rs +7 -10
  19. data/rust/src/commands/init.rs +10 -4
  20. data/rust/src/commands/insert.rs +18 -12
  21. data/rust/src/commands/location.rs +2 -2
  22. data/rust/src/commands/mate.rs +7 -7
  23. data/rust/src/commands/mod.rs +118 -90
  24. data/rust/src/commands/move_key.rs +9 -6
  25. data/rust/src/commands/quote_style.rs +4 -5
  26. data/rust/src/commands/schema.rs +6 -6
  27. data/rust/src/commands/selectors.rs +2 -2
  28. data/rust/src/commands/set.rs +18 -7
  29. data/rust/src/commands/sort.rs +36 -42
  30. data/rust/src/commands/sort_keys.rs +2 -2
  31. data/rust/src/commands/ui.rs +156 -0
  32. data/rust/src/commands/unique.rs +7 -7
  33. data/rust/src/commands/version.rs +13 -3
  34. data/rust/src/document/insert.rs +25 -9
  35. data/rust/src/document/mod.rs +7 -3
  36. data/rust/src/document/set.rs +115 -33
  37. data/rust/src/document/style.rs +131 -11
  38. data/rust/src/error.rs +27 -0
  39. data/rust/src/ffi.rs +55 -12
  40. data/rust/src/lib.rs +12 -2
  41. data/rust/src/quote_style.rs +12 -11
  42. data/rust/src/syntax.rs +285 -11
  43. data/rust/src/validation.rs +30 -0
  44. data/rust/src/yaml_writer.rs +9 -21
  45. data/rust/src/yerbafile.rs +76 -4
  46. metadata +4 -2
@@ -1,7 +1,7 @@
1
- use clap::ValueEnum;
2
1
  use yaml_parser::SyntaxKind;
3
2
 
4
- #[derive(Debug, Clone, PartialEq, ValueEnum)]
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, ValueEnum)]
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().to_string()),
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: dedent_block_scalar(block_token.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 quote_if_needed(value: &str) -> String {
186
- if is_yaml_non_string(value) {
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('\n') => {} // line continuation: skip newline and leading whitespace
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
+ }
@@ -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::is_yaml_non_string(string) {
73
- format!("\"{}\"", string.replace('"', "\\\""))
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::Double => {
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) => {
@@ -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
- document.set(&full_path, &config.value)
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
- document.insert_into(&full_path, &config.value, crate::InsertPosition::Last)
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
- document.enforce_blank_lines(&full_path, config.count)
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