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/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
|
}
|
|
@@ -746,6 +769,7 @@ pub unsafe extern "C" fn yerba_document_blank_lines(document: *mut Document, pat
|
|
|
746
769
|
}
|
|
747
770
|
|
|
748
771
|
/// Caller must free with yerba_string_free.
|
|
772
|
+
#[cfg(feature = "schema")]
|
|
749
773
|
#[no_mangle]
|
|
750
774
|
pub unsafe extern "C" fn yerba_document_validate_schema(document: *const Document, schema_json: *const c_char, selector: *const c_char) -> *mut c_char {
|
|
751
775
|
let document = &*document;
|
|
@@ -784,6 +808,7 @@ pub unsafe extern "C" fn yerba_document_validate_schema(document: *const Documen
|
|
|
784
808
|
}
|
|
785
809
|
|
|
786
810
|
/// Caller must free with yerba_string_free.
|
|
811
|
+
#[cfg(feature = "cli")]
|
|
787
812
|
#[no_mangle]
|
|
788
813
|
pub unsafe extern "C" fn yerba_yerbafile_find(directory: *const c_char) -> *mut c_char {
|
|
789
814
|
let start = if directory.is_null() {
|
|
@@ -803,6 +828,7 @@ pub unsafe extern "C" fn yerba_yerbafile_find(directory: *const c_char) -> *mut
|
|
|
803
828
|
}
|
|
804
829
|
}
|
|
805
830
|
|
|
831
|
+
#[cfg(feature = "cli")]
|
|
806
832
|
#[no_mangle]
|
|
807
833
|
pub unsafe extern "C" fn yerba_document_apply_yerbafile(document: *mut Document, file_path: *const c_char, yerbafile_path: *const c_char) -> YerbaResult {
|
|
808
834
|
let document = &mut *document;
|
|
@@ -840,6 +866,21 @@ pub unsafe extern "C" fn yerba_document_to_string(document: *const Document) ->
|
|
|
840
866
|
CString::new(content).unwrap_or_default().into_raw()
|
|
841
867
|
}
|
|
842
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
|
+
|
|
843
884
|
#[no_mangle]
|
|
844
885
|
pub unsafe extern "C" fn yerba_string_free(s: *mut c_char) {
|
|
845
886
|
if !s.is_null() {
|
|
@@ -924,6 +965,7 @@ fn located_nodes_to_list(nodes: &[crate::LocatedNode]) -> YerbaTypedList {
|
|
|
924
965
|
}
|
|
925
966
|
}
|
|
926
967
|
|
|
968
|
+
#[cfg(feature = "glob")]
|
|
927
969
|
#[no_mangle]
|
|
928
970
|
pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
|
|
929
971
|
let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
|
|
@@ -940,6 +982,7 @@ pub unsafe extern "C" fn yerba_document_get_all(document: *const Document, path:
|
|
|
940
982
|
located_nodes_to_list(&document.get_all_located(selector_string))
|
|
941
983
|
}
|
|
942
984
|
|
|
985
|
+
#[cfg(feature = "glob")]
|
|
943
986
|
#[no_mangle]
|
|
944
987
|
pub unsafe extern "C" fn yerba_glob_find(glob_pattern: *const c_char, path: *const c_char, condition: *const c_char, select: *const c_char) -> YerbaTypedList {
|
|
945
988
|
let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
|
data/rust/src/lib.rs
CHANGED
|
@@ -4,21 +4,29 @@ pub mod error;
|
|
|
4
4
|
pub mod ffi;
|
|
5
5
|
pub mod json;
|
|
6
6
|
mod quote_style;
|
|
7
|
+
#[cfg(feature = "schema")]
|
|
7
8
|
pub mod schema;
|
|
8
9
|
pub mod selector;
|
|
9
10
|
mod syntax;
|
|
11
|
+
mod validation;
|
|
10
12
|
mod yaml_writer;
|
|
13
|
+
#[cfg(feature = "cli")]
|
|
11
14
|
pub mod yerbafile;
|
|
12
15
|
|
|
13
|
-
pub use document::style::StyleEnforcement;
|
|
16
|
+
pub use document::style::{BlankLineOptions, StyleEnforcement};
|
|
14
17
|
pub use document::{
|
|
15
18
|
collect_selectors, validate_condition, validate_item_condition, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField,
|
|
16
19
|
};
|
|
17
20
|
pub use error::YerbaError;
|
|
18
21
|
pub use quote_style::{KeyStyle, QuoteStyle};
|
|
19
22
|
pub use selector::Selector;
|
|
20
|
-
pub use syntax::{
|
|
23
|
+
pub use syntax::{
|
|
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,
|
|
26
|
+
};
|
|
27
|
+
pub use validation::{find_control_characters, ControlCharacter};
|
|
21
28
|
pub use yaml_writer::json_to_yaml_text;
|
|
29
|
+
#[cfg(feature = "cli")]
|
|
22
30
|
pub use yerbafile::Yerbafile;
|
|
23
31
|
|
|
24
32
|
pub fn version() -> &'static str {
|
|
@@ -33,6 +41,7 @@ pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Document, YerbaEr
|
|
|
33
41
|
Document::parse_file(path)
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
#[cfg(feature = "glob")]
|
|
36
45
|
pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
|
|
37
46
|
use rayon::prelude::*;
|
|
38
47
|
|
|
@@ -55,6 +64,7 @@ pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
|
|
|
55
64
|
.collect()
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
#[cfg(feature = "glob")]
|
|
58
68
|
pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select: Option<&str>) -> Result<Vec<serde_json::Value>, YerbaError> {
|
|
59
69
|
use rayon::prelude::*;
|
|
60
70
|
|
data/rust/src/quote_style.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
use clap::ValueEnum;
|
|
2
1
|
use yaml_parser::SyntaxKind;
|
|
3
2
|
|
|
4
|
-
#[derive(Debug, Clone, PartialEq
|
|
3
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
4
|
+
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
|
5
5
|
pub enum KeyStyle {
|
|
6
6
|
/// Unquoted key (host:)
|
|
7
7
|
Plain,
|
|
@@ -34,33 +34,34 @@ impl KeyStyle {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
#[derive(Debug, Clone, PartialEq
|
|
37
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
38
|
+
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
|
38
39
|
pub enum QuoteStyle {
|
|
39
40
|
/// Unquoted value (host: localhost)
|
|
40
41
|
Plain,
|
|
41
42
|
/// Single-quoted value (host: 'localhost')
|
|
42
|
-
#[value(alias = "single-quoted")]
|
|
43
|
+
#[cfg_attr(feature = "cli", value(alias = "single-quoted"))]
|
|
43
44
|
Single,
|
|
44
45
|
/// Double-quoted value (host: "localhost")
|
|
45
|
-
#[value(alias = "double-quoted")]
|
|
46
|
+
#[cfg_attr(feature = "cli", value(alias = "double-quoted"))]
|
|
46
47
|
Double,
|
|
47
48
|
/// Literal block scalar, strip trailing newline (|-)
|
|
48
|
-
#[value(alias = "block-literal", alias = "|-")]
|
|
49
|
+
#[cfg_attr(feature = "cli", value(alias = "block-literal", alias = "|-"))]
|
|
49
50
|
Literal,
|
|
50
51
|
/// Literal block scalar, keep one trailing newline (|)
|
|
51
|
-
#[value(alias = "|")]
|
|
52
|
+
#[cfg_attr(feature = "cli", value(alias = "|"))]
|
|
52
53
|
LiteralClip,
|
|
53
54
|
/// Literal block scalar, keep all trailing newlines (|+)
|
|
54
|
-
#[value(alias = "|+")]
|
|
55
|
+
#[cfg_attr(feature = "cli", value(alias = "|+"))]
|
|
55
56
|
LiteralKeep,
|
|
56
57
|
/// Folded block scalar, strip trailing newline (>-)
|
|
57
|
-
#[value(alias = "block-folded", alias = ">-")]
|
|
58
|
+
#[cfg_attr(feature = "cli", value(alias = "block-folded", alias = ">-"))]
|
|
58
59
|
Folded,
|
|
59
60
|
/// Folded block scalar, keep one trailing newline (>)
|
|
60
|
-
#[value(alias = ">")]
|
|
61
|
+
#[cfg_attr(feature = "cli", value(alias = ">"))]
|
|
61
62
|
FoldedClip,
|
|
62
63
|
/// Folded block scalar, keep all trailing newlines (>+)
|
|
63
|
-
#[value(alias = ">+")]
|
|
64
|
+
#[cfg_attr(feature = "cli", value(alias = ">+"))]
|
|
64
65
|
FoldedKeep,
|
|
65
66
|
}
|
|
66
67
|
|
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,14 +187,168 @@ pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
|
|
|
182
187
|
}
|
|
183
188
|
}
|
|
184
189
|
|
|
185
|
-
pub fn
|
|
186
|
-
|
|
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
|
+
|
|
216
|
+
const LEADING_INDICATORS: [char; 16] = ['#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`', ',', '[', ']', '{', '}'];
|
|
217
|
+
const FLOW_INDICATORS: [char; 5] = [',', '[', ']', '{', '}'];
|
|
218
|
+
|
|
219
|
+
pub fn is_plain_safe(value: &str) -> bool {
|
|
220
|
+
if value.is_empty() || value != value.trim() {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if value.contains('\n') || value.contains('\t') {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if value.chars().any(is_control_character) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if value.contains(": ") || value.ends_with(':') {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if value.contains(" #") {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if value.starts_with("---") || value.starts_with("...") {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let mut characters = value.chars();
|
|
245
|
+
let first = characters.next().expect("value is non-empty");
|
|
246
|
+
|
|
247
|
+
if LEADING_INDICATORS.contains(&first) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if matches!(first, '-' | '?' | ':') {
|
|
252
|
+
return matches!(characters.next(), Some(next) if next != ' ');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
pub fn is_plain_safe_in_flow(value: &str) -> bool {
|
|
259
|
+
is_plain_safe(value) && !value.contains(FLOW_INDICATORS)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pub fn is_quoted_scalar(value: &str) -> bool {
|
|
263
|
+
let bytes = value.as_bytes();
|
|
264
|
+
|
|
265
|
+
if bytes.len() < 2 {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
match (bytes[0], bytes[bytes.len() - 1]) {
|
|
270
|
+
(b'"', b'"') => {
|
|
271
|
+
let interior = &value[1..value.len() - 1];
|
|
272
|
+
let mut escaped = false;
|
|
273
|
+
|
|
274
|
+
for character in interior.chars() {
|
|
275
|
+
if escaped {
|
|
276
|
+
escaped = false;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
match character {
|
|
281
|
+
'\\' => escaped = true,
|
|
282
|
+
'"' => return false,
|
|
283
|
+
_ => {}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
!escaped
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
(b'\'', b'\'') => {
|
|
291
|
+
let interior = &value[1..value.len() - 1];
|
|
292
|
+
let mut characters = interior.chars().peekable();
|
|
293
|
+
|
|
294
|
+
while let Some(character) = characters.next() {
|
|
295
|
+
if character == '\'' {
|
|
296
|
+
if characters.peek() == Some(&'\'') {
|
|
297
|
+
characters.next();
|
|
298
|
+
} else {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_ => false,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
pub fn is_flow_collection(value: &str) -> bool {
|
|
312
|
+
(value.starts_with('[') && value.ends_with(']')) || (value.starts_with('{') && value.ends_with('}'))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
pub fn is_inline_scalar_safe(value: &str) -> bool {
|
|
316
|
+
if value.is_empty() {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if value.contains('\n') || value.starts_with("- ") {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
is_quoted_scalar(value) || is_flow_collection(value) || is_plain_safe(value)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
pub fn needs_quoting(value: &str) -> bool {
|
|
328
|
+
is_yaml_non_string(value) || !is_plain_safe(value)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
pub fn needs_quoting_in_flow(value: &str) -> bool {
|
|
332
|
+
is_yaml_non_string(value) || !is_plain_safe_in_flow(value)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
pub fn quote_scalar(value: &str) -> String {
|
|
336
|
+
if needs_quoting(value) {
|
|
187
337
|
format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
188
338
|
} else {
|
|
189
339
|
value.to_string()
|
|
190
340
|
}
|
|
191
341
|
}
|
|
192
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
|
+
|
|
193
352
|
pub fn extract_scalar_text(node: &SyntaxNode) -> Option<String> {
|
|
194
353
|
extract_scalar(node).map(|scalar| scalar.text)
|
|
195
354
|
}
|
|
@@ -212,11 +371,114 @@ pub fn dedent_block_scalar(text: &str) -> String {
|
|
|
212
371
|
dedented.trim().to_string()
|
|
213
372
|
}
|
|
214
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
|
+
|
|
215
466
|
pub fn unescape_double_quoted(text: &str) -> String {
|
|
216
467
|
let mut result = String::with_capacity(text.len());
|
|
217
|
-
let mut chars = text.chars();
|
|
468
|
+
let mut chars = text.chars().peekable();
|
|
218
469
|
|
|
219
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
|
+
|
|
220
482
|
if character == '\\' {
|
|
221
483
|
match chars.next() {
|
|
222
484
|
Some('n') => result.push('\n'),
|
|
@@ -232,7 +494,19 @@ pub fn unescape_double_quoted(text: &str) -> String {
|
|
|
232
494
|
Some('v') => result.push('\u{0b}'),
|
|
233
495
|
Some(' ') => result.push(' '),
|
|
234
496
|
Some('_') => result.push('\u{a0}'),
|
|
235
|
-
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
|
+
|
|
236
510
|
Some(other) => {
|
|
237
511
|
result.push('\\');
|
|
238
512
|
result.push(other);
|
|
@@ -248,7 +522,7 @@ pub fn unescape_double_quoted(text: &str) -> String {
|
|
|
248
522
|
}
|
|
249
523
|
|
|
250
524
|
pub fn unescape_single_quoted(text: &str) -> String {
|
|
251
|
-
text.replace("''", "'")
|
|
525
|
+
fold_flow_scalar(&text.replace("''", "'"))
|
|
252
526
|
}
|
|
253
527
|
|
|
254
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)
|
|
@@ -69,8 +73,8 @@ pub fn yaml_value_to_flow_text(value: &yaml_serde::Value) -> String {
|
|
|
69
73
|
yaml_serde::Value::Number(number) => number.to_string(),
|
|
70
74
|
|
|
71
75
|
yaml_serde::Value::String(string) => {
|
|
72
|
-
if crate::syntax::
|
|
73
|
-
|
|
76
|
+
if crate::syntax::needs_quoting_in_flow(string) {
|
|
77
|
+
crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
74
78
|
} else {
|
|
75
79
|
string.clone()
|
|
76
80
|
}
|
|
@@ -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::is_yaml_non_string(string) {
|
|
166
|
-
format!("\"{}\"", string.replace('"', "\\\""))
|
|
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) => {
|