yerba 0.8.0 → 0.9.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.
@@ -89,6 +89,14 @@ impl Document {
89
89
  }
90
90
 
91
91
  pub fn insert_into(&mut self, dot_path: &str, value: &str, position: InsertPosition) -> Result<(), YerbaError> {
92
+ self.insert_with(dot_path, value, position, false)
93
+ }
94
+
95
+ pub fn insert_fragment_into(&mut self, dot_path: &str, value: &str, position: InsertPosition) -> Result<(), YerbaError> {
96
+ self.insert_with(dot_path, value, position, true)
97
+ }
98
+
99
+ fn insert_with(&mut self, dot_path: &str, value: &str, position: InsertPosition, fragment: bool) -> Result<(), YerbaError> {
92
100
  Self::validate_path(dot_path)?;
93
101
  self.refuse_flow_target(dot_path)?;
94
102
 
@@ -180,7 +188,7 @@ impl Document {
180
188
  };
181
189
 
182
190
  let indent = " ".repeat(start_col);
183
- let new_entry_text = Self::format_map_entry(key, value, &indent);
191
+ let new_entry_text = Self::format_map_entry(key, value, &indent, fragment);
184
192
 
185
193
  match &position {
186
194
  InsertPosition::After(target_key) => {
@@ -218,7 +226,7 @@ impl Document {
218
226
  return Ok(());
219
227
  }
220
228
 
221
- self.insert_map_key(parent_path, key, value, position)
229
+ self.insert_map_key(parent_path, key, value, position, fragment)
222
230
  }
223
231
 
224
232
  fn insert_sequence_item(&mut self, dot_path: &str, value: &str, position: InsertPosition) -> Result<(), YerbaError> {
@@ -339,7 +347,7 @@ impl Document {
339
347
  }
340
348
  }
341
349
 
342
- fn insert_map_key(&mut self, dot_path: &str, key: &str, value: &str, position: InsertPosition) -> Result<(), YerbaError> {
350
+ fn insert_map_key(&mut self, dot_path: &str, key: &str, value: &str, position: InsertPosition, fragment: bool) -> Result<(), YerbaError> {
343
351
  let current_node = self.navigate(dot_path)?;
344
352
 
345
353
  let map = match find_block_map(&current_node) {
@@ -378,7 +386,7 @@ impl Document {
378
386
  .map(|entry| preceding_whitespace_indent(entry.syntax()))
379
387
  .unwrap_or_default();
380
388
 
381
- let new_entry_text = Self::format_map_entry(key, value, &indent);
389
+ let new_entry_text = Self::format_map_entry(key, value, &indent, fragment);
382
390
 
383
391
  match position {
384
392
  InsertPosition::Last => {
@@ -422,7 +430,7 @@ impl Document {
422
430
  self.insert_after_node(target_entry.syntax(), &new_text)
423
431
  }
424
432
 
425
- InsertPosition::BeforeCondition(_) | InsertPosition::AfterCondition(_) => self.insert_map_key(dot_path, key, value, InsertPosition::Last),
433
+ InsertPosition::BeforeCondition(_) | InsertPosition::AfterCondition(_) => self.insert_map_key(dot_path, key, value, InsertPosition::Last, fragment),
426
434
 
427
435
  InsertPosition::FromSortOrder(order) => {
428
436
  let new_key_position = order.iter().position(|ordered_key| ordered_key == key);
@@ -447,7 +455,7 @@ impl Document {
447
455
  None => InsertPosition::Last,
448
456
  };
449
457
 
450
- self.insert_map_key(dot_path, key, value, resolved)
458
+ self.insert_map_key(dot_path, key, value, resolved, fragment)
451
459
  }
452
460
  }
453
461
  }
@@ -566,11 +574,15 @@ impl Document {
566
574
  Some((trailing[comment_start..].to_string(), rowan::TextSize::from((start + line_end) as u32)))
567
575
  }
568
576
 
569
- fn format_map_entry(key: &str, value: &str, indent: &str) -> String {
570
- let is_block_value = value.contains('\n') || value.starts_with("- ");
577
+ fn format_map_entry(key: &str, value: &str, indent: &str, fragment: bool) -> String {
578
+ let is_block_value = value.contains('\n') || value.starts_with("- ") || (fragment && is_block_collection(value));
571
579
 
572
580
  if !is_block_value {
573
- return format!("{}: {}", key, value);
581
+ if crate::syntax::is_inline_scalar_safe(value) {
582
+ return format!("{}: {}", key, value);
583
+ }
584
+
585
+ return format!("{}: {}", key, crate::syntax::quote_scalar(value));
574
586
  }
575
587
 
576
588
  let value_indent = format!("{} ", indent);
@@ -632,3 +644,7 @@ impl Document {
632
644
  }
633
645
  }
634
646
  }
647
+
648
+ fn is_block_collection(value: &str) -> bool {
649
+ !crate::syntax::is_flow_collection(value) && !crate::syntax::is_quoted_scalar(value) && !crate::syntax::is_plain_safe(value)
650
+ }
@@ -1,13 +1,17 @@
1
1
  mod condition;
2
- pub use condition::{validate_condition, validate_item_condition};
3
2
  mod delete;
4
3
  mod get;
5
4
  mod insert;
6
- mod schema;
7
5
  mod set;
8
6
  mod sort;
9
- pub mod style;
10
7
  mod unique;
8
+
9
+ #[cfg(feature = "schema")]
10
+ mod schema;
11
+
12
+ pub mod style;
13
+
14
+ pub use condition::{validate_condition, validate_item_condition};
11
15
  pub use unique::DuplicateInfo;
12
16
 
13
17
  use crate::syntax::YerbaValueType;
@@ -1,5 +1,23 @@
1
1
  use super::*;
2
2
 
3
+ fn is_plain_writable(value: &str) -> bool {
4
+ crate::syntax::is_plain_safe(value) && !crate::syntax::is_yaml_non_string(value)
5
+ }
6
+
7
+ fn scalar_replacement_text(value: &str, kind: SyntaxKind) -> String {
8
+ let representable = match kind {
9
+ SyntaxKind::PLAIN_SCALAR => is_plain_writable(value),
10
+ SyntaxKind::SINGLE_QUOTED_SCALAR => crate::syntax::is_single_quotable(value),
11
+ _ => true,
12
+ };
13
+
14
+ if !representable {
15
+ return format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR);
16
+ }
17
+
18
+ format_scalar_value(value, kind)
19
+ }
20
+
3
21
  fn holds_collection(node: &SyntaxNode) -> bool {
4
22
  node.descendants().any(|descendant| {
5
23
  matches!(
@@ -54,6 +72,34 @@ fn value_span(node: &SyntaxNode) -> Option<ValueSpan> {
54
72
  })
55
73
  }
56
74
 
75
+ fn scalar_edit(source: &str, node: &SyntaxNode, value: &str, plain: bool) -> Option<(TextRange, String)> {
76
+ if let Some(block_scalar) = node.descendants().find(|child| child.kind() == SyntaxKind::BLOCK_SCALAR) {
77
+ let replacement = if plain {
78
+ value.to_string()
79
+ } else {
80
+ Document::block_scalar_replacement(source, &block_scalar, value)
81
+ };
82
+
83
+ return Some((block_scalar.text_range(), replacement));
84
+ }
85
+
86
+ if holds_collection(node) {
87
+ let replacement = if plain { value.to_string() } else { crate::syntax::quote_scalar(value) };
88
+
89
+ return value_span(node).map(|span| (span.range, replacement_text(&span, &replacement)));
90
+ }
91
+
92
+ let scalar_token = find_scalar_token(node)?;
93
+
94
+ let replacement = if plain {
95
+ value.to_string()
96
+ } else {
97
+ scalar_replacement_text(value, scalar_token.kind())
98
+ };
99
+
100
+ Some((scalar_token.text_range(), replacement))
101
+ }
102
+
57
103
  fn replacement_text(span: &ValueSpan, value: &str) -> String {
58
104
  let mut text = if span.separated { format!(" {}", value) } else { value.to_string() };
59
105
 
@@ -67,51 +113,70 @@ fn replacement_text(span: &ValueSpan, value: &str) -> String {
67
113
 
68
114
  impl Document {
69
115
  pub fn set(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
70
- let current_node = self.navigate(dot_path)?;
116
+ self.set_with(dot_path, value, false)
117
+ }
71
118
 
72
- if let Some(block_scalar) = current_node.descendants().find(|node| node.kind() == SyntaxKind::BLOCK_SCALAR) {
73
- let source = self.source_text();
74
- let new_text = Self::block_scalar_replacement(&source, &block_scalar, value);
119
+ pub fn set_all(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
120
+ self.set_all_with(dot_path, value, false)
121
+ }
75
122
 
76
- return self.apply_edit(block_scalar.text_range(), &new_text);
77
- }
123
+ pub fn set_all_plain(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
124
+ self.set_all_with(dot_path, value, true)
125
+ }
78
126
 
79
- if holds_collection(&current_node) {
80
- let span = value_span(&current_node).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
127
+ fn set_all_with(&mut self, dot_path: &str, value: &str, plain: bool) -> Result<(), YerbaError> {
128
+ let nodes = self.navigate_all_compact(dot_path);
81
129
 
82
- return self.apply_edit(span.range, &replacement_text(&span, value));
130
+ if nodes.is_empty() {
131
+ return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
83
132
  }
84
133
 
85
- let scalar_token = find_scalar_token(&current_node).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
86
-
87
- let new_text = format_scalar_value(value, scalar_token.kind());
134
+ let source = self.source_text();
135
+ let edits = nodes.iter().filter_map(|node| scalar_edit(&source, node, value, plain)).collect();
88
136
 
89
- self.replace_token(&scalar_token, &new_text)
137
+ self.apply_edits(edits)
90
138
  }
91
139
 
92
- pub fn set_all(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
93
- let nodes = self.navigate_all_compact(dot_path);
140
+ pub fn set_where(&mut self, container_path: &str, relative_path: &str, value: &str, condition: &str, all: bool, plain: bool) -> Result<usize, YerbaError> {
141
+ validate_item_condition(condition)?;
94
142
 
95
- if nodes.is_empty() {
96
- return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
143
+ let items = self.navigate_all_compact(container_path);
144
+
145
+ if items.is_empty() {
146
+ return Err(YerbaError::SelectorNotFound(container_path.to_string()));
97
147
  }
98
148
 
99
149
  let source = self.source_text();
100
- let mut edits: Vec<(TextRange, String)> = Vec::new();
101
-
102
- for node in nodes {
103
- if let Some(block_scalar) = node.descendants().find(|child| child.kind() == SyntaxKind::BLOCK_SCALAR) {
104
- edits.push((block_scalar.text_range(), Self::block_scalar_replacement(&source, &block_scalar, value)));
105
- } else if holds_collection(&node) {
106
- if let Some(span) = value_span(&node) {
107
- edits.push((span.range, replacement_text(&span, value)));
108
- }
109
- } else if let Some(scalar_token) = find_scalar_token(&node) {
110
- edits.push((scalar_token.text_range(), format_scalar_value(value, scalar_token.kind())));
150
+ let mut targets: Vec<SyntaxNode> = Vec::new();
151
+
152
+ for item in &items {
153
+ if !self.evaluate_condition_on_node(item, condition) {
154
+ continue;
111
155
  }
156
+
157
+ targets.extend(navigate_from_node(item, relative_path));
112
158
  }
113
159
 
114
- self.apply_edits(edits)
160
+ if targets.is_empty() {
161
+ return Ok(0);
162
+ }
163
+
164
+ if !all && targets.len() > 1 {
165
+ let selector = if container_path.is_empty() {
166
+ relative_path.to_string()
167
+ } else {
168
+ format!("{}.{}", container_path, relative_path)
169
+ };
170
+
171
+ return Err(YerbaError::AmbiguousSelector(selector, targets.len()));
172
+ }
173
+
174
+ let edits: Vec<(TextRange, String)> = targets.iter().filter_map(|node| scalar_edit(&source, node, value, plain)).collect();
175
+ let count = edits.len();
176
+
177
+ self.apply_edits(edits)?;
178
+
179
+ Ok(count)
115
180
  }
116
181
 
117
182
  pub fn set_scalar_style(&mut self, dot_path: &str, style: &QuoteStyle) -> Result<(), YerbaError> {
@@ -136,22 +201,39 @@ impl Document {
136
201
  }
137
202
 
138
203
  pub fn set_plain(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
204
+ self.set_with(dot_path, value, true)
205
+ }
206
+
207
+ fn set_with(&mut self, dot_path: &str, value: &str, plain: bool) -> Result<(), YerbaError> {
139
208
  let current_node = self.navigate(dot_path)?;
140
209
 
141
210
  if let Some(block_scalar) = current_node.descendants().find(|node| node.kind() == SyntaxKind::BLOCK_SCALAR) {
142
- let range = block_scalar.text_range();
143
- return self.apply_edit(range, value);
211
+ if plain {
212
+ return self.apply_edit(block_scalar.text_range(), value);
213
+ }
214
+
215
+ let source = self.source_text();
216
+ let new_text = Self::block_scalar_replacement(&source, &block_scalar, value);
217
+
218
+ return self.apply_edit(block_scalar.text_range(), &new_text);
144
219
  }
145
220
 
146
221
  if holds_collection(&current_node) {
147
222
  let span = value_span(&current_node).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
223
+ let replacement = if plain { value.to_string() } else { crate::syntax::quote_scalar(value) };
148
224
 
149
- return self.apply_edit(span.range, &replacement_text(&span, value));
225
+ return self.apply_edit(span.range, &replacement_text(&span, &replacement));
150
226
  }
151
227
 
152
228
  let scalar_token = find_scalar_token(&current_node).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
153
229
 
154
- self.replace_token(&scalar_token, value)
230
+ if plain {
231
+ return self.replace_token(&scalar_token, value);
232
+ }
233
+
234
+ let new_text = scalar_replacement_text(value, scalar_token.kind());
235
+
236
+ self.replace_token(&scalar_token, &new_text)
155
237
  }
156
238
 
157
239
  fn block_scalar_replacement(source: &str, block_scalar: &SyntaxNode, value: &str) -> String {
@@ -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,8 @@ pub enum YerbaError {
10
10
  IndexOutOfBounds(usize, usize),
11
11
  UnknownKeys(Vec<String>),
12
12
  DuplicateValues(Vec<crate::DuplicateInfo>),
13
+ ControlCharacters(Vec<crate::ControlCharacter>),
14
+ #[cfg(feature = "schema")]
13
15
  SchemaValidation(Vec<crate::schema::ValidationError>),
14
16
  DuplicateKey {
15
17
  key: String,
@@ -74,10 +76,24 @@ impl std::fmt::Display for YerbaError {
74
76
  write!(f, "found {} {}: {}", duplicates.len(), noun, details.join(", "))
75
77
  }
76
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
+
77
92
  YerbaError::IndexOutOfBounds(index, length) => {
78
93
  write!(f, "index {} out of bounds (length {})", index, length)
79
94
  }
80
95
 
96
+ #[cfg(feature = "schema")]
81
97
  YerbaError::SchemaValidation(errors) => {
82
98
  let details: Vec<String> = errors.iter().map(|error| error.to_string()).collect();
83
99
 
@@ -137,6 +153,7 @@ impl GitHubAnnotations for YerbaError {
137
153
  })
138
154
  .collect(),
139
155
 
156
+ #[cfg(feature = "schema")]
140
157
  YerbaError::SchemaValidation(errors) => errors
141
158
  .iter()
142
159
  .map(|error| GitHubAnnotation {
@@ -147,6 +164,16 @@ impl GitHubAnnotations for YerbaError {
147
164
  })
148
165
  .collect(),
149
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
+
150
177
  YerbaError::DuplicateKey { key, duplicate_line, .. } => vec![GitHubAnnotation {
151
178
  level: "error ",
152
179
  file: file.to_string(),