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.
- checksums.yaml +4 -4
- data/README.md +170 -19
- data/ext/yerba/include/yerba.h +7 -0
- data/ext/yerba/yerba.c +54 -5
- 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.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 +3 -1
data/rust/src/document/insert.rs
CHANGED
|
@@ -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(¤t_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
|
-
|
|
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
|
+
}
|
data/rust/src/document/mod.rs
CHANGED
|
@@ -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;
|
data/rust/src/document/set.rs
CHANGED
|
@@ -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
|
-
|
|
116
|
+
self.set_with(dot_path, value, false)
|
|
117
|
+
}
|
|
71
118
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
130
|
+
if nodes.is_empty() {
|
|
131
|
+
return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
|
|
83
132
|
}
|
|
84
133
|
|
|
85
|
-
let
|
|
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.
|
|
137
|
+
self.apply_edits(edits)
|
|
90
138
|
}
|
|
91
139
|
|
|
92
|
-
pub fn
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
for
|
|
103
|
-
if
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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(¤t_node) {
|
|
147
222
|
let span = value_span(¤t_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,
|
|
225
|
+
return self.apply_edit(span.range, &replacement_text(&span, &replacement));
|
|
150
226
|
}
|
|
151
227
|
|
|
152
228
|
let scalar_token = find_scalar_token(¤t_node).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
|
|
153
229
|
|
|
154
|
-
|
|
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 {
|
data/rust/src/document/style.rs
CHANGED
|
@@ -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
|
|
601
|
-
Some(FirstCollection::Sequence(sequence)) => sequence
|
|
602
|
-
|
|
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
|
|
607
|
-
|
|
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 =
|
|
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(),
|