yerba 0.7.6-x86_64-linux-gnu → 0.8.1-x86_64-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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -2
  3. data/exe/x86_64-linux-gnu/yerba +0 -0
  4. data/ext/yerba/include/yerba.h +8 -0
  5. data/ext/yerba/yerba.c +70 -8
  6. data/lib/yerba/3.2/yerba.so +0 -0
  7. data/lib/yerba/3.3/yerba.so +0 -0
  8. data/lib/yerba/3.4/yerba.so +0 -0
  9. data/lib/yerba/4.0/yerba.so +0 -0
  10. data/lib/yerba/map.rb +15 -10
  11. data/lib/yerba/node.rb +2 -2
  12. data/lib/yerba/sequence.rb +13 -1
  13. data/lib/yerba/version.rb +1 -1
  14. data/rust/Cargo.lock +2 -342
  15. data/rust/Cargo.toml +15 -6
  16. data/rust/src/commands/blank_lines.rs +2 -2
  17. data/rust/src/commands/directives.rs +4 -4
  18. data/rust/src/commands/get.rs +17 -10
  19. data/rust/src/commands/init.rs +4 -4
  20. data/rust/src/commands/insert.rs +6 -6
  21. data/rust/src/commands/location.rs +2 -2
  22. data/rust/src/commands/mate.rs +7 -7
  23. data/rust/src/commands/mod.rs +119 -90
  24. data/rust/src/commands/move_key.rs +9 -6
  25. data/rust/src/commands/quote_style.rs +4 -5
  26. data/rust/src/commands/rename.rs +3 -3
  27. data/rust/src/commands/schema.rs +6 -6
  28. data/rust/src/commands/selectors.rs +2 -2
  29. data/rust/src/commands/set.rs +12 -1
  30. data/rust/src/commands/sort.rs +36 -42
  31. data/rust/src/commands/sort_keys.rs +2 -2
  32. data/rust/src/commands/ui.rs +157 -0
  33. data/rust/src/commands/unique.rs +7 -7
  34. data/rust/src/commands/version.rs +13 -3
  35. data/rust/src/document/condition.rs +89 -32
  36. data/rust/src/document/delete.rs +6 -1
  37. data/rust/src/document/get.rs +94 -26
  38. data/rust/src/document/insert.rs +26 -0
  39. data/rust/src/document/mod.rs +73 -20
  40. data/rust/src/document/set.rs +91 -2
  41. data/rust/src/error.rs +17 -0
  42. data/rust/src/ffi.rs +76 -9
  43. data/rust/src/json.rs +10 -1
  44. data/rust/src/lib.rs +23 -7
  45. data/rust/src/main.rs +1 -0
  46. data/rust/src/quote_style.rs +12 -11
  47. data/rust/src/selector.rs +19 -3
  48. data/rust/src/syntax.rs +178 -26
  49. data/rust/src/yaml_writer.rs +4 -4
  50. data/rust/src/yerbafile.rs +30 -6
  51. metadata +3 -2
data/rust/src/ffi.rs CHANGED
@@ -330,6 +330,24 @@ pub unsafe extern "C" fn yerba_document_resolve_selectors(document: *const Docum
330
330
  CString::new(json_string).unwrap_or_default().into_raw()
331
331
  }
332
332
 
333
+ #[no_mangle]
334
+ pub unsafe extern "C" fn yerba_selector_has_wildcard(path: *const c_char) -> bool {
335
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
336
+
337
+ crate::selector::Selector::parse(selector_string).has_wildcard()
338
+ }
339
+
340
+ #[no_mangle]
341
+ pub unsafe extern "C" fn yerba_document_keys(document: *const Document, path: *const c_char) -> *mut c_char {
342
+ let document = &*document;
343
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
344
+
345
+ let keys = document.keys_at(selector_string);
346
+ let json_string = serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string());
347
+
348
+ CString::new(json_string).unwrap_or_default().into_raw()
349
+ }
350
+
333
351
  #[no_mangle]
334
352
  pub unsafe extern "C" fn yerba_document_get_quote_style(document: *const Document, path: *const c_char) -> *mut c_char {
335
353
  let document = &*document;
@@ -409,12 +427,33 @@ pub unsafe extern "C" fn yerba_document_set_sequence_indent(document: *mut Docum
409
427
  }
410
428
  }
411
429
 
430
+ #[no_mangle]
431
+ pub unsafe extern "C" fn yerba_validate_condition(condition: *const c_char, item_context: bool) -> *mut c_char {
432
+ if condition.is_null() {
433
+ return ptr::null_mut();
434
+ }
435
+
436
+ let cond = CStr::from_ptr(condition).to_str().unwrap_or("");
437
+
438
+ let result = if item_context {
439
+ crate::validate_item_condition(cond)
440
+ } else {
441
+ crate::validate_condition(cond)
442
+ };
443
+
444
+ match result {
445
+ Ok(()) => ptr::null_mut(),
446
+ Err(error) => CString::new(error.to_string()).unwrap_or_default().into_raw(),
447
+ }
448
+ }
449
+
412
450
  #[no_mangle]
413
451
  pub unsafe extern "C" fn yerba_document_evaluate_condition(document: *const Document, parent_path: *const c_char, condition: *const c_char) -> bool {
414
452
  let document = &*document;
415
453
  let parent = CStr::from_ptr(parent_path).to_str().unwrap_or("");
416
454
  let cond = CStr::from_ptr(condition).to_str().unwrap_or("");
417
- document.evaluate_condition(parent, cond)
455
+
456
+ document.evaluate_condition(parent, cond).unwrap_or(false)
418
457
  }
419
458
 
420
459
  #[no_mangle]
@@ -440,7 +479,7 @@ pub unsafe extern "C" fn yerba_document_find(document: *const Document, path: *c
440
479
 
441
480
  let select_string = if select.is_null() { None } else { CStr::from_ptr(select).to_str().ok() };
442
481
 
443
- let results = document.find_items(selector_string, condition_string, select_string);
482
+ let results = document.find_items(selector_string, condition_string, select_string).unwrap_or_default();
444
483
  let json = serde_json::to_string_pretty(&results).unwrap_or_else(|_| "[]".to_string());
445
484
 
446
485
  CString::new(json).unwrap_or_default().into_raw()
@@ -707,6 +746,7 @@ pub unsafe extern "C" fn yerba_document_blank_lines(document: *mut Document, pat
707
746
  }
708
747
 
709
748
  /// Caller must free with yerba_string_free.
749
+ #[cfg(feature = "schema")]
710
750
  #[no_mangle]
711
751
  pub unsafe extern "C" fn yerba_document_validate_schema(document: *const Document, schema_json: *const c_char, selector: *const c_char) -> *mut c_char {
712
752
  let document = &*document;
@@ -745,6 +785,7 @@ pub unsafe extern "C" fn yerba_document_validate_schema(document: *const Documen
745
785
  }
746
786
 
747
787
  /// Caller must free with yerba_string_free.
788
+ #[cfg(feature = "cli")]
748
789
  #[no_mangle]
749
790
  pub unsafe extern "C" fn yerba_yerbafile_find(directory: *const c_char) -> *mut c_char {
750
791
  let start = if directory.is_null() {
@@ -764,6 +805,7 @@ pub unsafe extern "C" fn yerba_yerbafile_find(directory: *const c_char) -> *mut
764
805
  }
765
806
  }
766
807
 
808
+ #[cfg(feature = "cli")]
767
809
  #[no_mangle]
768
810
  pub unsafe extern "C" fn yerba_document_apply_yerbafile(document: *mut Document, file_path: *const c_char, yerbafile_path: *const c_char) -> YerbaResult {
769
811
  let document = &mut *document;
@@ -830,12 +872,7 @@ pub unsafe extern "C" fn yerba_get_result_free(result: YerbaGetResult) {
830
872
  }
831
873
  }
832
874
 
833
- #[no_mangle]
834
- pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
835
- let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
836
- let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
837
- let nodes = crate::glob_get(pattern, selector_string);
838
-
875
+ fn located_nodes_to_list(nodes: &[crate::LocatedNode]) -> YerbaTypedList {
839
876
  let results: Vec<serde_json::Value> = nodes
840
877
  .iter()
841
878
  .map(|node| {
@@ -865,6 +902,18 @@ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *cons
865
902
  value["type"] = serde_json::json!(vt as u8);
866
903
  }
867
904
 
905
+ if let Some(key_name) = &node.key_name {
906
+ value["key_name"] = serde_json::json!(key_name);
907
+ value["key_location"] = serde_json::json!({
908
+ "start_line": node.key_location.start_line,
909
+ "start_column": node.key_location.start_column,
910
+ "end_line": node.key_location.end_line,
911
+ "end_column": node.key_location.end_column,
912
+ "start_offset": node.key_location.start_offset,
913
+ "end_offset": node.key_location.end_offset,
914
+ });
915
+ }
916
+
868
917
  value
869
918
  })
870
919
  .collect();
@@ -878,6 +927,24 @@ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *cons
878
927
  }
879
928
  }
880
929
 
930
+ #[cfg(feature = "glob")]
931
+ #[no_mangle]
932
+ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
933
+ let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
934
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
935
+
936
+ located_nodes_to_list(&crate::glob_get(pattern, selector_string))
937
+ }
938
+
939
+ #[no_mangle]
940
+ pub unsafe extern "C" fn yerba_document_get_all(document: *const Document, path: *const c_char) -> YerbaTypedList {
941
+ let document = &*document;
942
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
943
+
944
+ located_nodes_to_list(&document.get_all_located(selector_string))
945
+ }
946
+
947
+ #[cfg(feature = "glob")]
881
948
  #[no_mangle]
882
949
  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 {
883
950
  let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
@@ -885,7 +952,7 @@ pub unsafe extern "C" fn yerba_glob_find(glob_pattern: *const c_char, path: *con
885
952
  let condition_string = if condition.is_null() { None } else { CStr::from_ptr(condition).to_str().ok() };
886
953
  let select_string = if select.is_null() { None } else { CStr::from_ptr(select).to_str().ok() };
887
954
 
888
- let all_results = crate::glob_find(pattern, selector_string, condition_string, select_string);
955
+ let all_results = crate::glob_find(pattern, selector_string, condition_string, select_string).unwrap_or_default();
889
956
  let length = all_results.len();
890
957
  let json = serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string());
891
958
 
data/rust/src/json.rs CHANGED
@@ -71,6 +71,12 @@ pub fn resolve_select_field(value: &yaml_serde::Value, field: &str) -> serde_jso
71
71
  }
72
72
  }
73
73
 
74
+ SelectorSegment::AllKeys => {
75
+ if let yaml_serde::Value::Mapping(mapping) = current {
76
+ next_values.extend(mapping.iter().map(|(_, entry)| entry.clone()));
77
+ }
78
+ }
79
+
74
80
  SelectorSegment::Index(index) => {
75
81
  if let yaml_serde::Value::Sequence(sequence) = current {
76
82
  if let Some(item) = sequence.get(*index) {
@@ -96,7 +102,10 @@ pub fn resolve_select_field(value: &yaml_serde::Value, field: &str) -> serde_jso
96
102
  current_values = next_values;
97
103
  }
98
104
 
99
- let used_all_items = parsed.segments().iter().any(|s| matches!(s, SelectorSegment::AllItems));
105
+ let used_all_items = parsed
106
+ .segments()
107
+ .iter()
108
+ .any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::AllKeys));
100
109
 
101
110
  if current_values.is_empty() {
102
111
  if used_all_items {
data/rust/src/lib.rs CHANGED
@@ -4,19 +4,27 @@ 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;
10
11
  mod yaml_writer;
12
+ #[cfg(feature = "cli")]
11
13
  pub mod yerbafile;
12
14
 
13
15
  pub use document::style::StyleEnforcement;
14
- pub use document::{collect_selectors, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField};
16
+ pub use document::{
17
+ collect_selectors, validate_condition, validate_item_condition, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField,
18
+ };
15
19
  pub use error::YerbaError;
16
20
  pub use quote_style::{KeyStyle, QuoteStyle};
17
21
  pub use selector::Selector;
18
- pub use syntax::{detect_yaml_type, ScalarValue, YerbaValueType};
22
+ pub use syntax::{
23
+ detect_yaml_type, is_flow_collection, is_inline_scalar_safe, is_plain_safe, is_plain_safe_in_flow, is_quoted_scalar, is_raw_yaml_text, is_valid_inline_value,
24
+ needs_quoting, needs_quoting_in_flow, quote_if_needed, ScalarValue, YerbaValueType,
25
+ };
19
26
  pub use yaml_writer::json_to_yaml_text;
27
+ #[cfg(feature = "cli")]
20
28
  pub use yerbafile::Yerbafile;
21
29
 
22
30
  pub fn version() -> &'static str {
@@ -31,6 +39,7 @@ pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Document, YerbaEr
31
39
  Document::parse_file(path)
32
40
  }
33
41
 
42
+ #[cfg(feature = "glob")]
34
43
  pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
35
44
  use rayon::prelude::*;
36
45
 
@@ -53,24 +62,29 @@ pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
53
62
  .collect()
54
63
  }
55
64
 
56
- pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select: Option<&str>) -> Vec<serde_json::Value> {
65
+ #[cfg(feature = "glob")]
66
+ pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select: Option<&str>) -> Result<Vec<serde_json::Value>, YerbaError> {
57
67
  use rayon::prelude::*;
58
68
 
69
+ if let Some(cond) = condition {
70
+ crate::document::validate_condition(cond)?;
71
+ }
72
+
59
73
  let files = match glob::glob(pattern) {
60
74
  Ok(paths) => paths.filter_map(|p| p.ok()).collect::<Vec<_>>(),
61
- Err(_) => return vec![],
75
+ Err(_) => return Ok(vec![]),
62
76
  };
63
77
 
64
78
  let select_fields: Option<Vec<&str>> = select.map(|s| s.split(',').collect());
65
79
 
66
- files
80
+ let results: Vec<serde_json::Value> = files
67
81
  .par_iter()
68
82
  .flat_map(|file| {
69
83
  let mut file_results = Vec::new();
70
84
 
71
85
  if let Ok(document) = Document::parse_file(file) {
72
86
  let values = match condition {
73
- Some(cond) => document.filter(selector, cond),
87
+ Some(cond) => document.filter(selector, cond).unwrap_or_default(),
74
88
  None => document.get_values(selector),
75
89
  };
76
90
 
@@ -108,5 +122,7 @@ pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select:
108
122
 
109
123
  file_results
110
124
  })
111
- .collect()
125
+ .collect();
126
+
127
+ Ok(results)
112
128
  }
data/rust/src/main.rs CHANGED
@@ -20,6 +20,7 @@ static HELP: LazyLock<String> = LazyLock::new(|| {
20
20
  key.nested Nested key path "database.settings.pool"
21
21
  [] All items in array "[].title"
22
22
  [N] Item at index "[0].title"
23
+ * All values in a map "database.*"
23
24
  [].key[].nested Nested array access "[].speakers[].name"
24
25
 
25
26
  Conditions:
@@ -1,7 +1,7 @@
1
- use clap::ValueEnum;
2
1
  use yaml_parser::SyntaxKind;
3
2
 
4
- #[derive(Debug, Clone, PartialEq, ValueEnum)]
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, ValueEnum)]
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/selector.rs CHANGED
@@ -21,6 +21,7 @@ pub enum Selector {
21
21
  pub enum SelectorSegment {
22
22
  Key(String),
23
23
  AllItems,
24
+ AllKeys,
24
25
  Index(usize),
25
26
  }
26
27
 
@@ -69,7 +70,10 @@ impl Selector {
69
70
  }
70
71
 
71
72
  pub fn has_wildcard(&self) -> bool {
72
- self.segments().iter().any(|s| matches!(s, SelectorSegment::AllItems))
73
+ self
74
+ .segments()
75
+ .iter()
76
+ .any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::AllKeys))
73
77
  }
74
78
 
75
79
  /// Split into the container selector (up to and including the last []) and the remaining field selector.
@@ -163,6 +167,14 @@ impl Selector {
163
167
  result.push_str("[]");
164
168
  }
165
169
 
170
+ SelectorSegment::AllKeys => {
171
+ if index > 0 && !result.ends_with('.') {
172
+ result.push('.');
173
+ }
174
+
175
+ result.push('*');
176
+ }
177
+
166
178
  SelectorSegment::Index(i) => {
167
179
  result.push_str(&format!("[{}]", i));
168
180
  }
@@ -227,7 +239,9 @@ fn parse_segments(input: &str) -> Vec<SelectorSegment> {
227
239
  Some(index) => {
228
240
  let key = &rest[..index];
229
241
 
230
- if !key.is_empty() {
242
+ if key == "*" {
243
+ segments.push(SelectorSegment::AllKeys);
244
+ } else if !key.is_empty() {
231
245
  segments.push(SelectorSegment::Key(key.to_string()));
232
246
  }
233
247
 
@@ -238,7 +252,9 @@ fn parse_segments(input: &str) -> Vec<SelectorSegment> {
238
252
  }
239
253
  }
240
254
  None => {
241
- if !rest.is_empty() {
255
+ if rest == "*" {
256
+ segments.push(SelectorSegment::AllKeys);
257
+ } else if !rest.is_empty() {
242
258
  segments.push(SelectorSegment::Key(rest.to_string()));
243
259
  }
244
260
 
data/rust/src/syntax.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  use rowan::ast::AstNode;
2
2
  use rowan::{TextRange, TextSize};
3
3
 
4
- use yaml_parser::ast::{BlockMap, BlockMapEntry, BlockSeq};
4
+ use yaml_parser::ast::{BlockMap, BlockMapEntry, BlockSeq, FlowMap, FlowMapEntry, FlowSeq};
5
5
  use yaml_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
6
6
 
7
7
  #[derive(Debug, Clone, PartialEq)]
@@ -82,6 +82,46 @@ pub fn find_block_sequence(node: &SyntaxNode) -> Option<BlockSeq> {
82
82
  node.descendants().find_map(BlockSeq::cast)
83
83
  }
84
84
 
85
+ fn first_collection_node(node: &SyntaxNode) -> Option<SyntaxNode> {
86
+ node.descendants().find(|descendant| {
87
+ matches!(
88
+ descendant.kind(),
89
+ SyntaxKind::BLOCK_MAP | SyntaxKind::BLOCK_SEQ | SyntaxKind::FLOW_MAP | SyntaxKind::FLOW_SEQ
90
+ )
91
+ })
92
+ }
93
+
94
+ pub fn find_flow_sequence(node: &SyntaxNode) -> Option<FlowSeq> {
95
+ first_collection_node(node).and_then(FlowSeq::cast)
96
+ }
97
+
98
+ pub fn find_flow_map(node: &SyntaxNode) -> Option<FlowMap> {
99
+ first_collection_node(node).and_then(FlowMap::cast)
100
+ }
101
+
102
+ pub fn flow_sequence_entries(sequence: &FlowSeq) -> Vec<SyntaxNode> {
103
+ sequence
104
+ .entries()
105
+ .map(|entries| entries.entries().map(|entry| entry.syntax().clone()).collect())
106
+ .unwrap_or_default()
107
+ }
108
+
109
+ pub fn flow_map_entries(map: &FlowMap) -> Vec<FlowMapEntry> {
110
+ map.entries().map(|entries| entries.entries().collect()).unwrap_or_default()
111
+ }
112
+
113
+ pub fn find_flow_entry_by_key(map: &FlowMap, key: &str) -> Option<FlowMapEntry> {
114
+ flow_map_entries(map)
115
+ .into_iter()
116
+ .find(|entry| entry.key().and_then(|found| extract_scalar_text(found.syntax())).as_deref() == Some(key))
117
+ }
118
+
119
+ pub fn in_flow_collection(node: &SyntaxNode) -> bool {
120
+ node
121
+ .ancestors()
122
+ .any(|ancestor| matches!(ancestor.kind(), SyntaxKind::FLOW_MAP | SyntaxKind::FLOW_SEQ))
123
+ }
124
+
85
125
  pub enum FirstCollection {
86
126
  Map(BlockMap),
87
127
  Sequence(BlockSeq),
@@ -142,8 +182,139 @@ pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
142
182
  }
143
183
  }
144
184
 
185
+ const LEADING_INDICATORS: [char; 16] = ['#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`', ',', '[', ']', '{', '}'];
186
+ const FLOW_INDICATORS: [char; 5] = [',', '[', ']', '{', '}'];
187
+
188
+ pub fn is_plain_safe(value: &str) -> bool {
189
+ if value.is_empty() || value != value.trim() {
190
+ return false;
191
+ }
192
+
193
+ if value.contains('\n') || value.contains('\t') {
194
+ return false;
195
+ }
196
+
197
+ if value.contains(": ") || value.ends_with(':') {
198
+ return false;
199
+ }
200
+
201
+ if value.contains(" #") {
202
+ return false;
203
+ }
204
+
205
+ if value.starts_with("---") || value.starts_with("...") {
206
+ return false;
207
+ }
208
+
209
+ let mut characters = value.chars();
210
+ let first = characters.next().expect("value is non-empty");
211
+
212
+ if LEADING_INDICATORS.contains(&first) {
213
+ return false;
214
+ }
215
+
216
+ if matches!(first, '-' | '?' | ':') {
217
+ return matches!(characters.next(), Some(next) if next != ' ');
218
+ }
219
+
220
+ true
221
+ }
222
+
223
+ pub fn is_plain_safe_in_flow(value: &str) -> bool {
224
+ is_plain_safe(value) && !value.contains(FLOW_INDICATORS)
225
+ }
226
+
227
+ pub fn is_quoted_scalar(value: &str) -> bool {
228
+ let bytes = value.as_bytes();
229
+
230
+ if bytes.len() < 2 {
231
+ return false;
232
+ }
233
+
234
+ match (bytes[0], bytes[bytes.len() - 1]) {
235
+ (b'"', b'"') => {
236
+ let interior = &value[1..value.len() - 1];
237
+ let mut escaped = false;
238
+
239
+ for character in interior.chars() {
240
+ if escaped {
241
+ escaped = false;
242
+ continue;
243
+ }
244
+
245
+ match character {
246
+ '\\' => escaped = true,
247
+ '"' => return false,
248
+ _ => {}
249
+ }
250
+ }
251
+
252
+ !escaped
253
+ }
254
+
255
+ (b'\'', b'\'') => {
256
+ let interior = &value[1..value.len() - 1];
257
+ let mut characters = interior.chars().peekable();
258
+
259
+ while let Some(character) = characters.next() {
260
+ if character == '\'' {
261
+ if characters.peek() == Some(&'\'') {
262
+ characters.next();
263
+ } else {
264
+ return false;
265
+ }
266
+ }
267
+ }
268
+
269
+ true
270
+ }
271
+
272
+ _ => false,
273
+ }
274
+ }
275
+
276
+ pub fn is_flow_collection(value: &str) -> bool {
277
+ (value.starts_with('[') && value.ends_with(']')) || (value.starts_with('{') && value.ends_with('}'))
278
+ }
279
+
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
+ pub fn is_inline_scalar_safe(value: &str) -> bool {
293
+ if value.is_empty() {
294
+ return true;
295
+ }
296
+
297
+ if value.contains('\n') || value.starts_with("- ") {
298
+ return false;
299
+ }
300
+
301
+ is_quoted_scalar(value) || is_flow_collection(value) || is_plain_safe(value)
302
+ }
303
+
304
+ pub fn needs_quoting(value: &str) -> bool {
305
+ is_yaml_non_string(value) || !is_plain_safe(value)
306
+ }
307
+
308
+ pub fn needs_quoting_in_flow(value: &str) -> bool {
309
+ is_yaml_non_string(value) || !is_plain_safe_in_flow(value)
310
+ }
311
+
145
312
  pub fn quote_if_needed(value: &str) -> String {
146
- if is_yaml_non_string(value) {
313
+ if is_raw_yaml_text(value) {
314
+ return value.to_string();
315
+ }
316
+
317
+ if needs_quoting(value) {
147
318
  format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR)
148
319
  } else {
149
320
  value.to_string()
@@ -288,7 +459,7 @@ pub fn is_yaml_non_string(value: &str) -> bool {
288
459
  }
289
460
 
290
461
  pub fn is_yaml_truthy(value: &str) -> bool {
291
- matches!(value, "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" | "y" | "Y")
462
+ matches!(value, "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON")
292
463
  }
293
464
 
294
465
  pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
@@ -297,31 +468,12 @@ pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
297
468
  return YerbaValueType::Null;
298
469
  }
299
470
 
300
- // Boolean (YAML 1.2 + 1.1)
471
+ // Boolean, resolved the way libyaml/Psych resolve it: the YAML 1.1 bool set
472
+ // without the single-letter y/Y/n/N forms (which libyaml treats as strings),
473
+ // and broader than YAML 1.2 core schema (which only accepts true/false).
301
474
  if matches!(
302
475
  value,
303
- "true"
304
- | "True"
305
- | "TRUE"
306
- | "false"
307
- | "False"
308
- | "FALSE"
309
- | "yes"
310
- | "Yes"
311
- | "YES"
312
- | "no"
313
- | "No"
314
- | "NO"
315
- | "on"
316
- | "On"
317
- | "ON"
318
- | "off"
319
- | "Off"
320
- | "OFF"
321
- | "y"
322
- | "Y"
323
- | "n"
324
- | "N"
476
+ "true" | "True" | "TRUE" | "false" | "False" | "FALSE" | "yes" | "Yes" | "YES" | "no" | "No" | "NO" | "on" | "On" | "ON" | "off" | "Off" | "OFF"
325
477
  ) {
326
478
  return YerbaValueType::Boolean;
327
479
  }
@@ -69,8 +69,8 @@ pub fn yaml_value_to_flow_text(value: &yaml_serde::Value) -> String {
69
69
  yaml_serde::Value::Number(number) => number.to_string(),
70
70
 
71
71
  yaml_serde::Value::String(string) => {
72
- if crate::syntax::is_yaml_non_string(string) {
73
- format!("\"{}\"", string.replace('"', "\\\""))
72
+ if crate::syntax::needs_quoting_in_flow(string) {
73
+ crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR)
74
74
  } else {
75
75
  string.clone()
76
76
  }
@@ -162,8 +162,8 @@ fn format_yaml_scalar(value: &Value, quote_style: &QuoteStyle) -> String {
162
162
  }
163
163
 
164
164
  QuoteStyle::Plain => {
165
- if crate::syntax::is_yaml_non_string(string) {
166
- format!("\"{}\"", string.replace('"', "\\\""))
165
+ if crate::syntax::needs_quoting(string) {
166
+ crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR)
167
167
  } else {
168
168
  string.clone()
169
169
  }