yerba 0.8.1-arm-linux-gnu → 0.9.1-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.
@@ -7,6 +7,13 @@ pub struct StyleEnforcement {
7
7
  pub value_style: Option<QuoteStyle>,
8
8
  }
9
9
 
10
+ #[derive(Debug, Clone, Default)]
11
+ pub struct BlankLineOptions {
12
+ pub before: Vec<String>,
13
+ pub after: Vec<String>,
14
+ pub skip_empty: bool,
15
+ }
16
+
10
17
  impl Document {
11
18
  pub fn enforce_styles(&mut self, enforcement: &StyleEnforcement) -> Result<(), YerbaError> {
12
19
  let source = self.source_text();
@@ -588,6 +595,10 @@ impl Document {
588
595
  }
589
596
 
590
597
  pub fn enforce_blank_lines(&mut self, dot_path: &str, blank_lines: usize) -> Result<(), YerbaError> {
598
+ self.enforce_blank_lines_with(dot_path, blank_lines, &BlankLineOptions::default())
599
+ }
600
+
601
+ pub fn enforce_blank_lines_with(&mut self, dot_path: &str, blank_lines: usize, options: &BlankLineOptions) -> Result<(), YerbaError> {
591
602
  let nodes = if dot_path.contains('[') {
592
603
  self.navigate_all_compact(dot_path)
593
604
  } else {
@@ -597,14 +608,44 @@ impl Document {
597
608
  let mut edits: Vec<(TextRange, String)> = Vec::new();
598
609
 
599
610
  for current_node in &nodes {
600
- let entry_nodes: Vec<SyntaxNode> = match first_collection(current_node) {
601
- Some(FirstCollection::Sequence(sequence)) => sequence.entries().map(|entry| entry.syntax().clone()).collect(),
602
- Some(FirstCollection::Map(map)) => map.entries().map(|entry| entry.syntax().clone()).collect(),
611
+ let entries: Vec<BlankLineEntry> = match first_collection(current_node) {
612
+ Some(FirstCollection::Sequence(sequence)) => sequence
613
+ .entries()
614
+ .map(|entry| BlankLineEntry {
615
+ is_empty: is_empty_value(&node_to_yaml_value(entry.syntax())),
616
+ node: entry.syntax().clone(),
617
+ key: None,
618
+ })
619
+ .collect(),
620
+
621
+ Some(FirstCollection::Map(map)) => map
622
+ .entries()
623
+ .map(|entry| {
624
+ let value = entry
625
+ .value()
626
+ .map(|value_node| node_to_yaml_value(value_node.syntax()))
627
+ .unwrap_or(yaml_serde::Value::Null);
628
+
629
+ BlankLineEntry {
630
+ key: entry.key().and_then(|key_node| extract_scalar_text(key_node.syntax())),
631
+ is_empty: is_empty_value(&value),
632
+ node: entry.syntax().clone(),
633
+ }
634
+ })
635
+ .collect(),
636
+
603
637
  None => continue,
604
638
  };
605
639
 
606
- for entry_node in entry_nodes.iter().skip(1) {
607
- collect_blank_line_edits(entry_node, blank_lines, &mut edits);
640
+ for index in 1..entries.len() {
641
+ let entry = &entries[index];
642
+ let previous = &entries[index - 1];
643
+
644
+ if !should_adjust_gap(entry, previous, options) {
645
+ continue;
646
+ }
647
+
648
+ collect_blank_line_edits(&entry.node, blank_lines, &mut edits);
608
649
  }
609
650
  }
610
651
 
@@ -738,6 +779,41 @@ impl Document {
738
779
  self.enforce_quotes_at(style, None)
739
780
  }
740
781
 
782
+ pub fn decode_escapes(&mut self, dot_path: Option<&str>) -> Result<usize, YerbaError> {
783
+ let scope_ranges: Vec<TextRange> = match dot_path {
784
+ Some(path) if !path.is_empty() => self.navigate_all_compact(path).iter().map(|node| node.text_range()).collect(),
785
+ _ => vec![self.root.text_range()],
786
+ };
787
+
788
+ let mut edits: Vec<(TextRange, String)> = Vec::new();
789
+
790
+ for element in self.root.descendants_with_tokens() {
791
+ let Some(token) = element.into_token() else { continue };
792
+
793
+ if token.kind() != SyntaxKind::DOUBLE_QUOTED_SCALAR {
794
+ continue;
795
+ }
796
+
797
+ if !scope_ranges.iter().any(|range| range.contains_range(token.text_range())) {
798
+ continue;
799
+ }
800
+
801
+ let Some(value) = raw_scalar_value(&token) else { continue };
802
+
803
+ let rewritten = format_scalar_value(&value, SyntaxKind::DOUBLE_QUOTED_SCALAR);
804
+
805
+ if rewritten != token.text() {
806
+ edits.push((token.text_range(), rewritten));
807
+ }
808
+ }
809
+
810
+ let count = edits.len();
811
+
812
+ self.apply_edits(edits)?;
813
+
814
+ Ok(count)
815
+ }
816
+
741
817
  pub fn enforce_quotes_at(&mut self, style: &QuoteStyle, dot_path: Option<&str>) -> Result<Vec<String>, YerbaError> {
742
818
  let source = self.source_text();
743
819
 
@@ -819,11 +895,7 @@ impl Document {
819
895
  let is_multiline = trimmed.contains('\n');
820
896
 
821
897
  let new_text = match style {
822
- QuoteStyle::Double => {
823
- let escaped = trimmed.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
824
-
825
- format!("\"{}\"", escaped)
826
- }
898
+ QuoteStyle::Double => format_scalar_value(trimmed, SyntaxKind::DOUBLE_QUOTED_SCALAR),
827
899
 
828
900
  QuoteStyle::Single => {
829
901
  if is_multiline {
@@ -879,7 +951,7 @@ impl Document {
879
951
 
880
952
  let offset: usize = token.text_range().start().into();
881
953
  let line_prefix = &source[line_start_at(&source, offset)..offset];
882
- let indent = line_prefix.len() - line_prefix.trim_start().len() + 2;
954
+ let indent = block_scalar_indent(line_prefix);
883
955
  let indent_str = " ".repeat(indent);
884
956
  let header = style.block_header();
885
957
 
@@ -932,6 +1004,54 @@ impl Document {
932
1004
  }
933
1005
  }
934
1006
 
1007
+ struct BlankLineEntry {
1008
+ node: SyntaxNode,
1009
+ key: Option<String>,
1010
+ is_empty: bool,
1011
+ }
1012
+
1013
+ fn should_adjust_gap(entry: &BlankLineEntry, previous: &BlankLineEntry, options: &BlankLineOptions) -> bool {
1014
+ let usable = |candidate: &BlankLineEntry| !options.skip_empty || !candidate.is_empty;
1015
+
1016
+ if options.before.is_empty() && options.after.is_empty() {
1017
+ return usable(entry);
1018
+ }
1019
+
1020
+ (matches_key(entry.key.as_deref(), &options.before) && usable(entry)) || (matches_key(previous.key.as_deref(), &options.after) && usable(previous))
1021
+ }
1022
+
1023
+ fn matches_key(key: Option<&str>, keys: &[String]) -> bool {
1024
+ match key {
1025
+ Some(key) => keys.iter().any(|wanted| wanted == key),
1026
+ None => false,
1027
+ }
1028
+ }
1029
+
1030
+ fn is_empty_value(value: &yaml_serde::Value) -> bool {
1031
+ match value {
1032
+ yaml_serde::Value::Null => true,
1033
+ yaml_serde::Value::String(text) => text.is_empty(),
1034
+ yaml_serde::Value::Sequence(entries) => entries.is_empty(),
1035
+ yaml_serde::Value::Mapping(entries) => entries.is_empty(),
1036
+ _ => false,
1037
+ }
1038
+ }
1039
+
1040
+ fn block_scalar_indent(line_prefix: &str) -> usize {
1041
+ let mut column = 0;
1042
+ let bytes = line_prefix.as_bytes();
1043
+
1044
+ while column < bytes.len() {
1045
+ match bytes[column] {
1046
+ b' ' => column += 1,
1047
+ b'-' if bytes.get(column + 1) == Some(&b' ') => column += 2,
1048
+ _ => break,
1049
+ }
1050
+ }
1051
+
1052
+ column + 2
1053
+ }
1054
+
935
1055
  fn inline_quote_replacement(raw_value: &str, current_kind: SyntaxKind, style: &QuoteStyle) -> Option<String> {
936
1056
  match style {
937
1057
  QuoteStyle::Double => {
data/rust/src/error.rs CHANGED
@@ -10,6 +10,7 @@ pub enum YerbaError {
10
10
  IndexOutOfBounds(usize, usize),
11
11
  UnknownKeys(Vec<String>),
12
12
  DuplicateValues(Vec<crate::DuplicateInfo>),
13
+ ControlCharacters(Vec<crate::ControlCharacter>),
13
14
  #[cfg(feature = "schema")]
14
15
  SchemaValidation(Vec<crate::schema::ValidationError>),
15
16
  DuplicateKey {
@@ -75,6 +76,19 @@ impl std::fmt::Display for YerbaError {
75
76
  write!(f, "found {} {}: {}", duplicates.len(), noun, details.join(", "))
76
77
  }
77
78
 
79
+ YerbaError::ControlCharacters(found) => {
80
+ let noun = if found.len() == 1 { "character" } else { "characters" };
81
+ let details: Vec<String> = found.iter().map(|control| format!(" {}", control)).collect();
82
+
83
+ write!(
84
+ f,
85
+ "found {} control {} that YAML parsers reject:\n{}\n\n Control characters must be removed, or escaped as \\xNN inside a double-quoted value.\n",
86
+ found.len(),
87
+ noun,
88
+ details.join("\n")
89
+ )
90
+ }
91
+
78
92
  YerbaError::IndexOutOfBounds(index, length) => {
79
93
  write!(f, "index {} out of bounds (length {})", index, length)
80
94
  }
@@ -150,6 +164,16 @@ impl GitHubAnnotations for YerbaError {
150
164
  })
151
165
  .collect(),
152
166
 
167
+ YerbaError::ControlCharacters(found) => found
168
+ .iter()
169
+ .map(|control| GitHubAnnotation {
170
+ level: "error ",
171
+ file: file.to_string(),
172
+ line: Some(control.line),
173
+ message: format!("control character U+{:04X} at column {}", control.character as u32, control.column),
174
+ })
175
+ .collect(),
176
+
153
177
  YerbaError::DuplicateKey { key, duplicate_line, .. } => vec![GitHubAnnotation {
154
178
  level: "error ",
155
179
  file: file.to_string(),
data/rust/src/ffi.rs CHANGED
@@ -217,6 +217,11 @@ const EMPTY_LOCATION: YerbaLocation = YerbaLocation {
217
217
  end_column: 0,
218
218
  };
219
219
 
220
+ #[no_mangle]
221
+ pub unsafe extern "C" fn yerba_document_generation(document: *const Document) -> u64 {
222
+ (*document).generation()
223
+ }
224
+
220
225
  #[no_mangle]
221
226
  pub unsafe extern "C" fn yerba_document_free(document: *mut Document) {
222
227
  if !document.is_null() {
@@ -485,25 +490,34 @@ pub unsafe extern "C" fn yerba_document_find(document: *const Document, path: *c
485
490
  CString::new(json).unwrap_or_default().into_raw()
486
491
  }
487
492
 
493
+ unsafe fn borrow_value<'a>(value: *const c_char, length: usize) -> &'a str {
494
+ if value.is_null() {
495
+ return "";
496
+ }
497
+
498
+ std::str::from_utf8(std::slice::from_raw_parts(value as *const u8, length)).unwrap_or("")
499
+ }
500
+
488
501
  #[no_mangle]
489
502
  pub unsafe extern "C" fn yerba_document_set(
490
503
  document: *mut Document,
491
504
  path: *const c_char,
492
505
  value: *const c_char,
506
+ value_length: usize,
493
507
  value_type: YerbaValueType,
508
+ plain: bool,
494
509
  all: bool,
495
510
  ) -> YerbaResult {
496
511
  let document = &mut *document;
497
512
  let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
498
- let value_string = CStr::from_ptr(value).to_str().unwrap_or("");
499
-
500
- let result = if all {
501
- document.set_all(selector_string, value_string)
502
- } else {
503
- match value_type {
504
- YerbaValueType::String => document.set(selector_string, value_string),
505
- _ => document.set_plain(selector_string, value_string),
506
- }
513
+ let value_string = borrow_value(value, value_length);
514
+ let verbatim = plain || value_type != YerbaValueType::String;
515
+
516
+ let result = match (all, verbatim) {
517
+ (true, false) => document.set_all(selector_string, value_string),
518
+ (true, true) => document.set_all_plain(selector_string, value_string),
519
+ (false, false) => document.set(selector_string, value_string),
520
+ (false, true) => document.set_plain(selector_string, value_string),
507
521
  };
508
522
 
509
523
  match result {
@@ -517,17 +531,25 @@ pub unsafe extern "C" fn yerba_document_insert(
517
531
  document: *mut Document,
518
532
  path: *const c_char,
519
533
  value: *const c_char,
534
+ value_length: usize,
520
535
  value_type: YerbaValueType,
536
+ plain: bool,
537
+ style: *const c_char,
521
538
  before: *const c_char,
522
539
  after: *const c_char,
523
540
  at: i64,
524
541
  ) -> YerbaResult {
525
542
  let document = &mut *document;
526
543
  let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
527
- let raw_value = CStr::from_ptr(value).to_str().unwrap_or("");
544
+ let raw_value = borrow_value(value, value_length);
528
545
 
529
546
  let value_string = match value_type {
530
- YerbaValueType::String => crate::syntax::quote_if_needed(raw_value),
547
+ YerbaValueType::String if !plain => {
548
+ let style = if style.is_null() { None } else { CStr::from_ptr(style).to_str().ok() };
549
+
550
+ crate::syntax::quote_scalar_styled(raw_value, style)
551
+ }
552
+
531
553
  _ => raw_value.to_string(),
532
554
  };
533
555
 
@@ -543,7 +565,13 @@ pub unsafe extern "C" fn yerba_document_insert(
543
565
  InsertPosition::Last
544
566
  };
545
567
 
546
- match document.insert_into(selector_string, &value_string, position) {
568
+ let result = if plain {
569
+ document.insert_fragment_into(selector_string, &value_string, position)
570
+ } else {
571
+ document.insert_into(selector_string, &value_string, position)
572
+ };
573
+
574
+ match result {
547
575
  Ok(()) => YerbaResult::ok(),
548
576
  Err(e) => YerbaResult::err(&e.to_string()),
549
577
  }
@@ -843,6 +871,21 @@ pub unsafe extern "C" fn yerba_document_to_string(document: *const Document) ->
843
871
  CString::new(content).unwrap_or_default().into_raw()
844
872
  }
845
873
 
874
+ #[no_mangle]
875
+ pub unsafe extern "C" fn yerba_quote_scalar(value: *const c_char, length: usize, style: *const c_char) -> *mut c_char {
876
+ let bytes = std::slice::from_raw_parts(value as *const u8, length);
877
+
878
+ let Ok(value) = std::str::from_utf8(bytes) else {
879
+ return std::ptr::null_mut();
880
+ };
881
+
882
+ let style = if style.is_null() { None } else { CStr::from_ptr(style).to_str().ok() };
883
+
884
+ CString::new(crate::syntax::quote_scalar_styled(value, style))
885
+ .map(|string| string.into_raw())
886
+ .unwrap_or(std::ptr::null_mut())
887
+ }
888
+
846
889
  #[no_mangle]
847
890
  pub unsafe extern "C" fn yerba_string_free(s: *mut c_char) {
848
891
  if !s.is_null() {
data/rust/src/lib.rs CHANGED
@@ -8,11 +8,12 @@ mod quote_style;
8
8
  pub mod schema;
9
9
  pub mod selector;
10
10
  mod syntax;
11
+ mod validation;
11
12
  mod yaml_writer;
12
13
  #[cfg(feature = "cli")]
13
14
  pub mod yerbafile;
14
15
 
15
- pub use document::style::StyleEnforcement;
16
+ pub use document::style::{BlankLineOptions, StyleEnforcement};
16
17
  pub use document::{
17
18
  collect_selectors, validate_condition, validate_item_condition, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField,
18
19
  };
@@ -20,9 +21,10 @@ pub use error::YerbaError;
20
21
  pub use quote_style::{KeyStyle, QuoteStyle};
21
22
  pub use selector::Selector;
22
23
  pub use syntax::{
23
- detect_yaml_type, is_flow_collection, is_inline_scalar_safe, is_plain_safe, is_plain_safe_in_flow, is_quoted_scalar, is_raw_yaml_text, is_valid_inline_value,
24
- needs_quoting, needs_quoting_in_flow, quote_if_needed, ScalarValue, YerbaValueType,
24
+ detect_yaml_type, escape_double_quoted, is_control_character, is_flow_collection, is_inline_scalar_safe, is_plain_safe, is_plain_safe_in_flow,
25
+ is_quoted_scalar, is_single_quotable, needs_quoting, needs_quoting_in_flow, quote_scalar, quote_scalar_styled, ScalarValue, YerbaValueType,
25
26
  };
27
+ pub use validation::{find_control_characters, ControlCharacter};
26
28
  pub use yaml_writer::json_to_yaml_text;
27
29
  #[cfg(feature = "cli")]
28
30
  pub use yerbafile::Yerbafile;
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,6 +187,32 @@ pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
182
187
  }
183
188
  }
184
189
 
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
+
185
216
  const LEADING_INDICATORS: [char; 16] = ['#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`', ',', '[', ']', '{', '}'];
186
217
  const FLOW_INDICATORS: [char; 5] = [',', '[', ']', '{', '}'];
187
218
 
@@ -194,6 +225,10 @@ pub fn is_plain_safe(value: &str) -> bool {
194
225
  return false;
195
226
  }
196
227
 
228
+ if value.chars().any(is_control_character) {
229
+ return false;
230
+ }
231
+
197
232
  if value.contains(": ") || value.ends_with(':') {
198
233
  return false;
199
234
  }
@@ -277,18 +312,6 @@ pub fn is_flow_collection(value: &str) -> bool {
277
312
  (value.starts_with('[') && value.ends_with(']')) || (value.starts_with('{') && value.ends_with('}'))
278
313
  }
279
314
 
280
- pub fn is_raw_yaml_text(value: &str) -> bool {
281
- value.contains('\n') || value.starts_with("- ") || is_quoted_scalar(value) || is_flow_collection(value)
282
- }
283
-
284
- pub fn is_valid_inline_value(value: &str) -> bool {
285
- if value.is_empty() {
286
- return true;
287
- }
288
-
289
- is_raw_yaml_text(value) || is_plain_safe(value)
290
- }
291
-
292
315
  pub fn is_inline_scalar_safe(value: &str) -> bool {
293
316
  if value.is_empty() {
294
317
  return true;
@@ -309,11 +332,7 @@ pub fn needs_quoting_in_flow(value: &str) -> bool {
309
332
  is_yaml_non_string(value) || !is_plain_safe_in_flow(value)
310
333
  }
311
334
 
312
- pub fn quote_if_needed(value: &str) -> String {
313
- if is_raw_yaml_text(value) {
314
- return value.to_string();
315
- }
316
-
335
+ pub fn quote_scalar(value: &str) -> String {
317
336
  if needs_quoting(value) {
318
337
  format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR)
319
338
  } else {
@@ -321,6 +340,15 @@ pub fn quote_if_needed(value: &str) -> String {
321
340
  }
322
341
  }
323
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
+
324
352
  pub fn extract_scalar_text(node: &SyntaxNode) -> Option<String> {
325
353
  extract_scalar(node).map(|scalar| scalar.text)
326
354
  }
@@ -343,11 +371,114 @@ pub fn dedent_block_scalar(text: &str) -> String {
343
371
  dedented.trim().to_string()
344
372
  }
345
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
+
346
466
  pub fn unescape_double_quoted(text: &str) -> String {
347
467
  let mut result = String::with_capacity(text.len());
348
- let mut chars = text.chars();
468
+ let mut chars = text.chars().peekable();
349
469
 
350
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
+
351
482
  if character == '\\' {
352
483
  match chars.next() {
353
484
  Some('n') => result.push('\n'),
@@ -363,7 +494,19 @@ pub fn unescape_double_quoted(text: &str) -> String {
363
494
  Some('v') => result.push('\u{0b}'),
364
495
  Some(' ') => result.push(' '),
365
496
  Some('_') => result.push('\u{a0}'),
366
- 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
+
367
510
  Some(other) => {
368
511
  result.push('\\');
369
512
  result.push(other);
@@ -379,7 +522,7 @@ pub fn unescape_double_quoted(text: &str) -> String {
379
522
  }
380
523
 
381
524
  pub fn unescape_single_quoted(text: &str) -> String {
382
- text.replace("''", "'")
525
+ fold_flow_scalar(&text.replace("''", "'"))
383
526
  }
384
527
 
385
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
+ }