yerba 0.8.1-arm-linux-gnu → 0.9.0-arm-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +170 -19
- data/exe/arm-linux-gnu/yerba +0 -0
- data/ext/yerba/include/yerba.h +7 -0
- data/ext/yerba/yerba.c +54 -5
- data/lib/yerba/3.2/yerba.so +0 -0
- data/lib/yerba/3.3/yerba.so +0 -0
- data/lib/yerba/3.4/yerba.so +0 -0
- data/lib/yerba/4.0/yerba.so +0 -0
- data/lib/yerba/formatting.rb +9 -13
- data/lib/yerba/map.rb +11 -8
- data/lib/yerba/sequence.rb +2 -7
- data/lib/yerba/version.rb +1 -1
- data/rust/Cargo.lock +1 -1
- data/rust/Cargo.toml +1 -1
- data/rust/src/commands/blank_lines.rs +21 -1
- data/rust/src/commands/init.rs +6 -0
- data/rust/src/commands/insert.rs +12 -6
- data/rust/src/commands/mod.rs +2 -3
- data/rust/src/commands/set.rs +15 -5
- data/rust/src/commands/ui.rs +1 -2
- data/rust/src/commands/version.rs +1 -1
- data/rust/src/document/insert.rs +23 -11
- data/rust/src/document/set.rs +108 -34
- data/rust/src/document/style.rs +131 -11
- data/rust/src/error.rs +24 -0
- data/rust/src/ffi.rs +50 -12
- data/rust/src/lib.rs +5 -3
- data/rust/src/syntax.rs +169 -26
- data/rust/src/validation.rs +30 -0
- data/rust/src/yaml_writer.rs +7 -19
- data/rust/src/yerbafile.rs +76 -4
- metadata +3 -2
data/rust/src/commands/insert.rs
CHANGED
|
@@ -8,13 +8,13 @@ use super::{output, parse_file, resolve_files, run_op};
|
|
|
8
8
|
|
|
9
9
|
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
10
10
|
colorize_examples(indoc! {r#"
|
|
11
|
-
yerba insert config.yml "database.ssl" true
|
|
12
|
-
yerba insert config.yml "database.ssl" true --after "host"
|
|
13
|
-
yerba insert config.yml "database.ssl" true --before "port"
|
|
11
|
+
yerba insert config.yml "database.ssl" true --plain
|
|
12
|
+
yerba insert config.yml "database.ssl" true --plain --after "host"
|
|
13
|
+
yerba insert config.yml "database.ssl" true --plain --before "port"
|
|
14
14
|
yerba insert config.yml "tags" "yaml"
|
|
15
15
|
yerba insert config.yml "tags" "yaml" --at 0
|
|
16
16
|
yerba insert config.yml "tags" "yaml" --after "ruby"
|
|
17
|
-
yerba insert speakers.yml "" "name: Bob" --after ".name == Alice"
|
|
17
|
+
yerba insert speakers.yml "" "name: Bob" --plain --after ".name == Alice"
|
|
18
18
|
yerba insert videos.yml "[0].speakers" "Diana" --before ".name == Charlie"
|
|
19
19
|
yerba insert videos.yml "" --from "new_talk.yml" --after ".id == first-talk"
|
|
20
20
|
"#})
|
|
@@ -39,6 +39,8 @@ pub struct Args {
|
|
|
39
39
|
after: Option<String>,
|
|
40
40
|
#[arg(long)]
|
|
41
41
|
at: Option<usize>,
|
|
42
|
+
#[arg(long, help = "Write the value as raw YAML, for literals like null, true or 42 and for map or sequence fragments")]
|
|
43
|
+
plain: bool,
|
|
42
44
|
#[arg(long)]
|
|
43
45
|
dry_run: bool,
|
|
44
46
|
}
|
|
@@ -67,8 +69,12 @@ impl Args {
|
|
|
67
69
|
.trim()
|
|
68
70
|
.to_string()
|
|
69
71
|
}
|
|
70
|
-
} else if let Some(
|
|
71
|
-
|
|
72
|
+
} else if let Some(value) = self.value {
|
|
73
|
+
if self.plain {
|
|
74
|
+
value
|
|
75
|
+
} else {
|
|
76
|
+
yerba::quote_scalar(&value)
|
|
77
|
+
}
|
|
72
78
|
} else {
|
|
73
79
|
eprintln!("{} either a value argument or --from is required", ui::failure("Error:"));
|
|
74
80
|
|
data/rust/src/commands/mod.rs
CHANGED
|
@@ -188,11 +188,10 @@ pub(crate) fn run_yerbafile(write: bool, files: Vec<String>) {
|
|
|
188
188
|
process::exit(1);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
-
eprintln!("{}", ui::banner());
|
|
191
|
+
eprintln!("\n{}\n", ui::banner());
|
|
192
192
|
|
|
193
193
|
eprintln!(
|
|
194
|
-
"{}
|
|
195
|
-
ui::INDENT,
|
|
194
|
+
"{} {}\n",
|
|
196
195
|
ui::strong("Using Yerbafile rules"),
|
|
197
196
|
ui::subtle(format!("(from {})", yerbafile_path.display()))
|
|
198
197
|
);
|
data/rust/src/commands/set.rs
CHANGED
|
@@ -11,8 +11,11 @@ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
|
11
11
|
yerba set config.yml "database.host" "0.0.0.0" --if-exists
|
|
12
12
|
yerba set config.yml "database.host" "0.0.0.0" --condition ".port == 5432"
|
|
13
13
|
yerba set videos.yml "[0].title" "New Title"
|
|
14
|
+
yerba set videos.yml "[].title" "New Title" --condition ".id == talk-1"
|
|
14
15
|
yerba set "data/**/event.yml" "website" "" --if-exists
|
|
15
16
|
yerba set videos.yml "[].description" "" --all
|
|
17
|
+
yerba set config.yml "database.replica" null --plain
|
|
18
|
+
yerba set videos.yml "[].published" false --plain --all
|
|
16
19
|
"#})
|
|
17
20
|
});
|
|
18
21
|
|
|
@@ -35,6 +38,8 @@ pub struct Args {
|
|
|
35
38
|
condition: Option<String>,
|
|
36
39
|
#[arg(long)]
|
|
37
40
|
all: bool,
|
|
41
|
+
#[arg(long, help = "Write the value unquoted, for YAML literals like null, true or 42")]
|
|
42
|
+
plain: bool,
|
|
38
43
|
#[arg(long)]
|
|
39
44
|
dry_run: bool,
|
|
40
45
|
}
|
|
@@ -53,6 +58,9 @@ impl Args {
|
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
let leaf = self.selector.rsplit_once('.').map(|(_, leaf)| leaf).unwrap_or(&self.selector);
|
|
62
|
+
let per_item = yerba::Selector::parse(parent_path).has_brackets();
|
|
63
|
+
|
|
56
64
|
for resolved_file in resolve_files(&self.file) {
|
|
57
65
|
let mut document = parse_file(&resolved_file);
|
|
58
66
|
|
|
@@ -61,16 +69,18 @@ impl Args {
|
|
|
61
69
|
} else if self.if_missing {
|
|
62
70
|
!document.exists(&self.selector)
|
|
63
71
|
} else if let Some(condition) = &self.condition {
|
|
64
|
-
document.evaluate_condition(parent_path, condition).unwrap_or(false)
|
|
72
|
+
per_item || document.evaluate_condition(parent_path, condition).unwrap_or(false)
|
|
65
73
|
} else {
|
|
66
74
|
true
|
|
67
75
|
};
|
|
68
76
|
|
|
69
77
|
if should_set {
|
|
70
|
-
let result =
|
|
71
|
-
document.
|
|
72
|
-
|
|
73
|
-
document.
|
|
78
|
+
let result = match &self.condition {
|
|
79
|
+
Some(condition) if per_item => document.set_where(parent_path, leaf, &self.value, condition, self.all, self.plain).map(|_| ()),
|
|
80
|
+
_ if self.all && self.plain => document.set_all_plain(&self.selector, &self.value),
|
|
81
|
+
_ if self.all => document.set_all(&self.selector, &self.value),
|
|
82
|
+
_ if self.plain => document.set_plain(&self.selector, &self.value),
|
|
83
|
+
_ => document.set(&self.selector, &self.value),
|
|
74
84
|
};
|
|
75
85
|
|
|
76
86
|
run_op_with_hint(
|
data/rust/src/commands/ui.rs
CHANGED
|
@@ -110,10 +110,9 @@ pub fn strong(text: impl std::fmt::Display) -> String {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
pub fn banner() -> String {
|
|
113
|
-
format!("
|
|
113
|
+
format!("{} 🧉 {}", strong("Yerba"), subtle(format!("v{}", yerba::version())))
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
pub const INDENT: &str = " ";
|
|
117
116
|
pub const STATUS_INDENT: &str = " ";
|
|
118
117
|
|
|
119
118
|
pub mod glyph {
|
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,15 +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
|
-
if
|
|
574
|
-
return format!("{}: {}", key,
|
|
581
|
+
if crate::syntax::is_inline_scalar_safe(value) {
|
|
582
|
+
return format!("{}: {}", key, value);
|
|
575
583
|
}
|
|
576
584
|
|
|
577
|
-
return format!("{}: {}", key, value);
|
|
585
|
+
return format!("{}: {}", key, crate::syntax::quote_scalar(value));
|
|
578
586
|
}
|
|
579
587
|
|
|
580
588
|
let value_indent = format!("{} ", indent);
|
|
@@ -636,3 +644,7 @@ impl Document {
|
|
|
636
644
|
}
|
|
637
645
|
}
|
|
638
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/set.rs
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
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
|
+
|
|
3
7
|
fn scalar_replacement_text(value: &str, kind: SyntaxKind) -> String {
|
|
4
|
-
|
|
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 {
|
|
5
15
|
return format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR);
|
|
6
16
|
}
|
|
7
17
|
|
|
@@ -62,6 +72,34 @@ fn value_span(node: &SyntaxNode) -> Option<ValueSpan> {
|
|
|
62
72
|
})
|
|
63
73
|
}
|
|
64
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
|
+
|
|
65
103
|
fn replacement_text(span: &ValueSpan, value: &str) -> String {
|
|
66
104
|
let mut text = if span.separated { format!(" {}", value) } else { value.to_string() };
|
|
67
105
|
|
|
@@ -75,51 +113,70 @@ fn replacement_text(span: &ValueSpan, value: &str) -> String {
|
|
|
75
113
|
|
|
76
114
|
impl Document {
|
|
77
115
|
pub fn set(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
|
|
78
|
-
|
|
116
|
+
self.set_with(dot_path, value, false)
|
|
117
|
+
}
|
|
79
118
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
pub fn set_all(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
|
|
120
|
+
self.set_all_with(dot_path, value, false)
|
|
121
|
+
}
|
|
83
122
|
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
}
|
|
86
126
|
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
89
129
|
|
|
90
|
-
|
|
130
|
+
if nodes.is_empty() {
|
|
131
|
+
return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
|
|
91
132
|
}
|
|
92
133
|
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
let new_text = scalar_replacement_text(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();
|
|
96
136
|
|
|
97
|
-
self.
|
|
137
|
+
self.apply_edits(edits)
|
|
98
138
|
}
|
|
99
139
|
|
|
100
|
-
pub fn
|
|
101
|
-
|
|
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)?;
|
|
102
142
|
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
let items = self.navigate_all_compact(container_path);
|
|
144
|
+
|
|
145
|
+
if items.is_empty() {
|
|
146
|
+
return Err(YerbaError::SelectorNotFound(container_path.to_string()));
|
|
105
147
|
}
|
|
106
148
|
|
|
107
149
|
let source = self.source_text();
|
|
108
|
-
let mut
|
|
109
|
-
|
|
110
|
-
for
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
} else if holds_collection(&node) {
|
|
114
|
-
if let Some(span) = value_span(&node) {
|
|
115
|
-
edits.push((span.range, replacement_text(&span, value)));
|
|
116
|
-
}
|
|
117
|
-
} else if let Some(scalar_token) = find_scalar_token(&node) {
|
|
118
|
-
edits.push((scalar_token.text_range(), scalar_replacement_text(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;
|
|
119
155
|
}
|
|
156
|
+
|
|
157
|
+
targets.extend(navigate_from_node(item, relative_path));
|
|
120
158
|
}
|
|
121
159
|
|
|
122
|
-
|
|
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)
|
|
123
180
|
}
|
|
124
181
|
|
|
125
182
|
pub fn set_scalar_style(&mut self, dot_path: &str, style: &QuoteStyle) -> Result<(), YerbaError> {
|
|
@@ -144,22 +201,39 @@ impl Document {
|
|
|
144
201
|
}
|
|
145
202
|
|
|
146
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> {
|
|
147
208
|
let current_node = self.navigate(dot_path)?;
|
|
148
209
|
|
|
149
210
|
if let Some(block_scalar) = current_node.descendants().find(|node| node.kind() == SyntaxKind::BLOCK_SCALAR) {
|
|
150
|
-
|
|
151
|
-
|
|
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);
|
|
152
219
|
}
|
|
153
220
|
|
|
154
221
|
if holds_collection(¤t_node) {
|
|
155
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) };
|
|
156
224
|
|
|
157
|
-
return self.apply_edit(span.range, &replacement_text(&span,
|
|
225
|
+
return self.apply_edit(span.range, &replacement_text(&span, &replacement));
|
|
158
226
|
}
|
|
159
227
|
|
|
160
228
|
let scalar_token = find_scalar_token(¤t_node).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
|
|
161
229
|
|
|
162
|
-
|
|
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)
|
|
163
237
|
}
|
|
164
238
|
|
|
165
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,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(),
|