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/ffi.rs
CHANGED
|
@@ -485,25 +485,34 @@ pub unsafe extern "C" fn yerba_document_find(document: *const Document, path: *c
|
|
|
485
485
|
CString::new(json).unwrap_or_default().into_raw()
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
unsafe fn borrow_value<'a>(value: *const c_char, length: usize) -> &'a str {
|
|
489
|
+
if value.is_null() {
|
|
490
|
+
return "";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
std::str::from_utf8(std::slice::from_raw_parts(value as *const u8, length)).unwrap_or("")
|
|
494
|
+
}
|
|
495
|
+
|
|
488
496
|
#[no_mangle]
|
|
489
497
|
pub unsafe extern "C" fn yerba_document_set(
|
|
490
498
|
document: *mut Document,
|
|
491
499
|
path: *const c_char,
|
|
492
500
|
value: *const c_char,
|
|
501
|
+
value_length: usize,
|
|
493
502
|
value_type: YerbaValueType,
|
|
503
|
+
plain: bool,
|
|
494
504
|
all: bool,
|
|
495
505
|
) -> YerbaResult {
|
|
496
506
|
let document = &mut *document;
|
|
497
507
|
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
498
|
-
let value_string =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
508
|
+
let value_string = borrow_value(value, value_length);
|
|
509
|
+
let verbatim = plain || value_type != YerbaValueType::String;
|
|
510
|
+
|
|
511
|
+
let result = match (all, verbatim) {
|
|
512
|
+
(true, false) => document.set_all(selector_string, value_string),
|
|
513
|
+
(true, true) => document.set_all_plain(selector_string, value_string),
|
|
514
|
+
(false, false) => document.set(selector_string, value_string),
|
|
515
|
+
(false, true) => document.set_plain(selector_string, value_string),
|
|
507
516
|
};
|
|
508
517
|
|
|
509
518
|
match result {
|
|
@@ -517,17 +526,25 @@ pub unsafe extern "C" fn yerba_document_insert(
|
|
|
517
526
|
document: *mut Document,
|
|
518
527
|
path: *const c_char,
|
|
519
528
|
value: *const c_char,
|
|
529
|
+
value_length: usize,
|
|
520
530
|
value_type: YerbaValueType,
|
|
531
|
+
plain: bool,
|
|
532
|
+
style: *const c_char,
|
|
521
533
|
before: *const c_char,
|
|
522
534
|
after: *const c_char,
|
|
523
535
|
at: i64,
|
|
524
536
|
) -> YerbaResult {
|
|
525
537
|
let document = &mut *document;
|
|
526
538
|
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
527
|
-
let raw_value =
|
|
539
|
+
let raw_value = borrow_value(value, value_length);
|
|
528
540
|
|
|
529
541
|
let value_string = match value_type {
|
|
530
|
-
YerbaValueType::String =>
|
|
542
|
+
YerbaValueType::String if !plain => {
|
|
543
|
+
let style = if style.is_null() { None } else { CStr::from_ptr(style).to_str().ok() };
|
|
544
|
+
|
|
545
|
+
crate::syntax::quote_scalar_styled(raw_value, style)
|
|
546
|
+
}
|
|
547
|
+
|
|
531
548
|
_ => raw_value.to_string(),
|
|
532
549
|
};
|
|
533
550
|
|
|
@@ -543,7 +560,13 @@ pub unsafe extern "C" fn yerba_document_insert(
|
|
|
543
560
|
InsertPosition::Last
|
|
544
561
|
};
|
|
545
562
|
|
|
546
|
-
|
|
563
|
+
let result = if plain {
|
|
564
|
+
document.insert_fragment_into(selector_string, &value_string, position)
|
|
565
|
+
} else {
|
|
566
|
+
document.insert_into(selector_string, &value_string, position)
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
match result {
|
|
547
570
|
Ok(()) => YerbaResult::ok(),
|
|
548
571
|
Err(e) => YerbaResult::err(&e.to_string()),
|
|
549
572
|
}
|
|
@@ -843,6 +866,21 @@ pub unsafe extern "C" fn yerba_document_to_string(document: *const Document) ->
|
|
|
843
866
|
CString::new(content).unwrap_or_default().into_raw()
|
|
844
867
|
}
|
|
845
868
|
|
|
869
|
+
#[no_mangle]
|
|
870
|
+
pub unsafe extern "C" fn yerba_quote_scalar(value: *const c_char, length: usize, style: *const c_char) -> *mut c_char {
|
|
871
|
+
let bytes = std::slice::from_raw_parts(value as *const u8, length);
|
|
872
|
+
|
|
873
|
+
let Ok(value) = std::str::from_utf8(bytes) else {
|
|
874
|
+
return std::ptr::null_mut();
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
let style = if style.is_null() { None } else { CStr::from_ptr(style).to_str().ok() };
|
|
878
|
+
|
|
879
|
+
CString::new(crate::syntax::quote_scalar_styled(value, style))
|
|
880
|
+
.map(|string| string.into_raw())
|
|
881
|
+
.unwrap_or(std::ptr::null_mut())
|
|
882
|
+
}
|
|
883
|
+
|
|
846
884
|
#[no_mangle]
|
|
847
885
|
pub unsafe extern "C" fn yerba_string_free(s: *mut c_char) {
|
|
848
886
|
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,
|
|
24
|
-
needs_quoting, needs_quoting_in_flow,
|
|
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()
|
|
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:
|
|
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
|
|
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('
|
|
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
|
+
}
|
data/rust/src/yaml_writer.rs
CHANGED
|
@@ -10,6 +10,8 @@ pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize)
|
|
|
10
10
|
map
|
|
11
11
|
.iter()
|
|
12
12
|
.map(|(k, v)| match v {
|
|
13
|
+
Value::Array(arr) if arr.is_empty() => format!("{}{}: []", prefix, k),
|
|
14
|
+
|
|
13
15
|
Value::Array(arr) => {
|
|
14
16
|
let items: Vec<String> = arr
|
|
15
17
|
.iter()
|
|
@@ -26,6 +28,8 @@ pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize)
|
|
|
26
28
|
format!("{}{}:\n{}", prefix, k, items.join("\n"))
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
Value::Object(inner_map) if inner_map.is_empty() => format!("{}{}: {{}}", prefix, k),
|
|
32
|
+
|
|
29
33
|
Value::Object(_) => {
|
|
30
34
|
let inner = json_to_yaml_text(v, quote_style, indent + 2);
|
|
31
35
|
format!("{}{}:\n{}", prefix, k, inner)
|
|
@@ -149,31 +153,15 @@ fn format_yaml_scalar(value: &Value, quote_style: &QuoteStyle) -> String {
|
|
|
149
153
|
Value::Bool(boolean) => boolean.to_string(),
|
|
150
154
|
Value::Number(number) => number.to_string(),
|
|
151
155
|
Value::String(string) => match quote_style {
|
|
152
|
-
QuoteStyle::
|
|
153
|
-
let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
|
|
154
|
-
|
|
155
|
-
format!("\"{}\"", escaped)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
QuoteStyle::Single => {
|
|
156
|
+
QuoteStyle::Single if crate::syntax::is_single_quotable(string) => {
|
|
159
157
|
let escaped = string.replace('\'', "''");
|
|
160
158
|
|
|
161
159
|
format!("'{}'", escaped)
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
QuoteStyle::Plain =>
|
|
165
|
-
if crate::syntax::needs_quoting(string) {
|
|
166
|
-
crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
167
|
-
} else {
|
|
168
|
-
string.clone()
|
|
169
|
-
}
|
|
170
|
-
}
|
|
162
|
+
QuoteStyle::Plain if !crate::syntax::needs_quoting(string) => string.clone(),
|
|
171
163
|
|
|
172
|
-
_ =>
|
|
173
|
-
let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
|
|
174
|
-
|
|
175
|
-
format!("\"{}\"", escaped)
|
|
176
|
-
}
|
|
164
|
+
_ => crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR),
|
|
177
165
|
},
|
|
178
166
|
|
|
179
167
|
Value::Array(array) => {
|
data/rust/src/yerbafile.rs
CHANGED
|
@@ -42,6 +42,7 @@ pub enum PipelineStep {
|
|
|
42
42
|
Unique(UniqueConfig),
|
|
43
43
|
Schema(SchemaConfig),
|
|
44
44
|
FinalNewline(FinalNewlineConfig),
|
|
45
|
+
Escapes(EscapesConfig),
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -59,6 +60,12 @@ pub struct BlankLinesConfig {
|
|
|
59
60
|
#[serde(default)]
|
|
60
61
|
pub path: Option<String>,
|
|
61
62
|
pub count: usize,
|
|
63
|
+
#[serde(default)]
|
|
64
|
+
pub before: Vec<String>,
|
|
65
|
+
#[serde(default)]
|
|
66
|
+
pub after: Vec<String>,
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
pub skip_empty: bool,
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -102,6 +109,12 @@ pub struct FinalNewlineConfig {
|
|
|
102
109
|
pub count: usize,
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
113
|
+
pub struct EscapesConfig {
|
|
114
|
+
#[serde(default)]
|
|
115
|
+
pub path: Option<String>,
|
|
116
|
+
}
|
|
117
|
+
|
|
105
118
|
fn default_one() -> usize {
|
|
106
119
|
1
|
|
107
120
|
}
|
|
@@ -199,13 +212,18 @@ impl<'de> Deserialize<'de> for PipelineStep {
|
|
|
199
212
|
return Ok(PipelineStep::Schema(config));
|
|
200
213
|
}
|
|
201
214
|
|
|
215
|
+
if let Some(value) = mapping.get(yaml_serde::Value::String("escapes".to_string())) {
|
|
216
|
+
let config: EscapesConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
217
|
+
return Ok(PipelineStep::Escapes(config));
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
if let Some(value) = mapping.get(yaml_serde::Value::String("final_newline".to_string())) {
|
|
203
221
|
let config: FinalNewlineConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
204
222
|
return Ok(PipelineStep::FinalNewline(config));
|
|
205
223
|
}
|
|
206
224
|
|
|
207
225
|
Err(serde::de::Error::custom(
|
|
208
|
-
"unknown pipeline step: expected sort_keys, quote_style, collection_style, sequence_indent, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, schema, or final_newline",
|
|
226
|
+
"unknown pipeline step: expected sort_keys, quote_style, collection_style, sequence_indent, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, schema, escapes, or final_newline",
|
|
209
227
|
))
|
|
210
228
|
}
|
|
211
229
|
}
|
|
@@ -246,6 +264,8 @@ pub struct SetConfig {
|
|
|
246
264
|
pub value: String,
|
|
247
265
|
#[serde(default)]
|
|
248
266
|
pub condition: Option<String>,
|
|
267
|
+
#[serde(default)]
|
|
268
|
+
pub plain: bool,
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -254,6 +274,8 @@ pub struct InsertConfig {
|
|
|
254
274
|
pub value: String,
|
|
255
275
|
#[serde(default)]
|
|
256
276
|
pub condition: Option<String>,
|
|
277
|
+
#[serde(default)]
|
|
278
|
+
pub plain: bool,
|
|
257
279
|
}
|
|
258
280
|
|
|
259
281
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -432,6 +454,14 @@ impl Yerbafile {
|
|
|
432
454
|
|
|
433
455
|
let original = document.to_string();
|
|
434
456
|
|
|
457
|
+
if let Some(error) = control_character_error(&original) {
|
|
458
|
+
return RuleResult {
|
|
459
|
+
file: file.to_string(),
|
|
460
|
+
changed: false,
|
|
461
|
+
error: Some(error),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
435
465
|
if run_global {
|
|
436
466
|
if let Err(error) = execute_pipeline(&mut document, &self.pipeline, None, file, self) {
|
|
437
467
|
return RuleResult {
|
|
@@ -513,6 +543,14 @@ impl Yerbafile {
|
|
|
513
543
|
|
|
514
544
|
let original = document.to_string();
|
|
515
545
|
|
|
546
|
+
if let Some(error) = control_character_error(&original) {
|
|
547
|
+
return RuleResult {
|
|
548
|
+
file: file.to_string(),
|
|
549
|
+
changed: false,
|
|
550
|
+
error: Some(error),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
516
554
|
if let Err(error) = execute_pipeline(&mut document, &self.pipeline, None, file, self) {
|
|
517
555
|
return RuleResult {
|
|
518
556
|
file: file.to_string(),
|
|
@@ -574,6 +612,11 @@ impl Yerbafile {
|
|
|
574
612
|
|
|
575
613
|
pub fn apply_to_document(&self, document: &mut Document, file_path: &str) -> Result<bool, YerbaError> {
|
|
576
614
|
let original = document.to_string();
|
|
615
|
+
|
|
616
|
+
if let Some(error) = control_character_error(&original) {
|
|
617
|
+
return Err(error);
|
|
618
|
+
}
|
|
619
|
+
|
|
577
620
|
let relative_path = self.relativize_path(file_path);
|
|
578
621
|
let match_path = relative_path.as_deref().unwrap_or(file_path);
|
|
579
622
|
|
|
@@ -603,6 +646,12 @@ impl Yerbafile {
|
|
|
603
646
|
}
|
|
604
647
|
}
|
|
605
648
|
|
|
649
|
+
fn control_character_error(source: &str) -> Option<YerbaError> {
|
|
650
|
+
let found = crate::validation::find_control_characters(source);
|
|
651
|
+
|
|
652
|
+
(!found.is_empty()).then_some(YerbaError::ControlCharacters(found))
|
|
653
|
+
}
|
|
654
|
+
|
|
606
655
|
fn execute_pipeline(document: &mut Document, steps: &[PipelineStep], base_path: Option<&str>, file: &str, yerbafile: &Yerbafile) -> Result<(), YerbaError> {
|
|
607
656
|
use crate::document::style::StyleEnforcement;
|
|
608
657
|
|
|
@@ -704,7 +753,11 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
704
753
|
}
|
|
705
754
|
}
|
|
706
755
|
|
|
707
|
-
|
|
756
|
+
if config.plain {
|
|
757
|
+
document.set_plain(&full_path, &config.value)
|
|
758
|
+
} else {
|
|
759
|
+
document.set(&full_path, &config.value)
|
|
760
|
+
}
|
|
708
761
|
}
|
|
709
762
|
|
|
710
763
|
PipelineStep::Insert(config) => {
|
|
@@ -718,7 +771,13 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
718
771
|
}
|
|
719
772
|
}
|
|
720
773
|
|
|
721
|
-
|
|
774
|
+
let value = if config.plain {
|
|
775
|
+
config.value.clone()
|
|
776
|
+
} else {
|
|
777
|
+
crate::syntax::quote_scalar(&config.value)
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
document.insert_into(&full_path, &value, crate::InsertPosition::Last)
|
|
722
781
|
}
|
|
723
782
|
|
|
724
783
|
PipelineStep::Delete(config) => {
|
|
@@ -784,7 +843,13 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
784
843
|
PipelineStep::BlankLines(config) => {
|
|
785
844
|
let full_path = resolve_step_path(base_path, config.path.as_deref());
|
|
786
845
|
|
|
787
|
-
|
|
846
|
+
let options = crate::BlankLineOptions {
|
|
847
|
+
before: config.before.clone(),
|
|
848
|
+
after: config.after.clone(),
|
|
849
|
+
skip_empty: config.skip_empty,
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
document.enforce_blank_lines_with(&full_path, config.count, &options)
|
|
788
853
|
}
|
|
789
854
|
|
|
790
855
|
PipelineStep::Sort(config) => {
|
|
@@ -848,6 +913,13 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
|
|
|
848
913
|
}
|
|
849
914
|
|
|
850
915
|
PipelineStep::FinalNewline(config) => document.enforce_final_newline(config.count),
|
|
916
|
+
|
|
917
|
+
PipelineStep::Escapes(config) => {
|
|
918
|
+
let full_path = resolve_step_path(base_path, config.path.as_deref());
|
|
919
|
+
let scope = if full_path.is_empty() { None } else { Some(full_path.as_str()) };
|
|
920
|
+
|
|
921
|
+
document.decode_escapes(scope).map(|_| ())
|
|
922
|
+
}
|
|
851
923
|
}
|
|
852
924
|
}
|
|
853
925
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yerba
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: arm-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Marco Roth
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A CLI tool for editing YAML while preserving structure, comments, and
|
|
14
14
|
format.
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- rust/src/schema.rs
|
|
92
92
|
- rust/src/selector.rs
|
|
93
93
|
- rust/src/syntax.rs
|
|
94
|
+
- rust/src/validation.rs
|
|
94
95
|
- rust/src/yaml_writer.rs
|
|
95
96
|
- rust/src/yerbafile.rs
|
|
96
97
|
- sig/yerba.rbs
|