yerba 0.8.1-aarch64-linux-gnu → 0.9.1-aarch64-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/aarch64-linux-gnu/yerba +0 -0
- data/ext/yerba/include/yerba.h +9 -0
- data/ext/yerba/yerba.c +72 -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 +22 -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/mod.rs +13 -1
- 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 +55 -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/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(),
|
data/rust/src/ffi.rs
CHANGED
|
@@ -217,6 +217,11 @@ const EMPTY_LOCATION: YerbaLocation = YerbaLocation {
|
|
|
217
217
|
end_column: 0,
|
|
218
218
|
};
|
|
219
219
|
|
|
220
|
+
#[no_mangle]
|
|
221
|
+
pub unsafe extern "C" fn yerba_document_generation(document: *const Document) -> u64 {
|
|
222
|
+
(*document).generation()
|
|
223
|
+
}
|
|
224
|
+
|
|
220
225
|
#[no_mangle]
|
|
221
226
|
pub unsafe extern "C" fn yerba_document_free(document: *mut Document) {
|
|
222
227
|
if !document.is_null() {
|
|
@@ -485,25 +490,34 @@ pub unsafe extern "C" fn yerba_document_find(document: *const Document, path: *c
|
|
|
485
490
|
CString::new(json).unwrap_or_default().into_raw()
|
|
486
491
|
}
|
|
487
492
|
|
|
493
|
+
unsafe fn borrow_value<'a>(value: *const c_char, length: usize) -> &'a str {
|
|
494
|
+
if value.is_null() {
|
|
495
|
+
return "";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
std::str::from_utf8(std::slice::from_raw_parts(value as *const u8, length)).unwrap_or("")
|
|
499
|
+
}
|
|
500
|
+
|
|
488
501
|
#[no_mangle]
|
|
489
502
|
pub unsafe extern "C" fn yerba_document_set(
|
|
490
503
|
document: *mut Document,
|
|
491
504
|
path: *const c_char,
|
|
492
505
|
value: *const c_char,
|
|
506
|
+
value_length: usize,
|
|
493
507
|
value_type: YerbaValueType,
|
|
508
|
+
plain: bool,
|
|
494
509
|
all: bool,
|
|
495
510
|
) -> YerbaResult {
|
|
496
511
|
let document = &mut *document;
|
|
497
512
|
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
498
|
-
let value_string =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
513
|
+
let value_string = borrow_value(value, value_length);
|
|
514
|
+
let verbatim = plain || value_type != YerbaValueType::String;
|
|
515
|
+
|
|
516
|
+
let result = match (all, verbatim) {
|
|
517
|
+
(true, false) => document.set_all(selector_string, value_string),
|
|
518
|
+
(true, true) => document.set_all_plain(selector_string, value_string),
|
|
519
|
+
(false, false) => document.set(selector_string, value_string),
|
|
520
|
+
(false, true) => document.set_plain(selector_string, value_string),
|
|
507
521
|
};
|
|
508
522
|
|
|
509
523
|
match result {
|
|
@@ -517,17 +531,25 @@ pub unsafe extern "C" fn yerba_document_insert(
|
|
|
517
531
|
document: *mut Document,
|
|
518
532
|
path: *const c_char,
|
|
519
533
|
value: *const c_char,
|
|
534
|
+
value_length: usize,
|
|
520
535
|
value_type: YerbaValueType,
|
|
536
|
+
plain: bool,
|
|
537
|
+
style: *const c_char,
|
|
521
538
|
before: *const c_char,
|
|
522
539
|
after: *const c_char,
|
|
523
540
|
at: i64,
|
|
524
541
|
) -> YerbaResult {
|
|
525
542
|
let document = &mut *document;
|
|
526
543
|
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
527
|
-
let raw_value =
|
|
544
|
+
let raw_value = borrow_value(value, value_length);
|
|
528
545
|
|
|
529
546
|
let value_string = match value_type {
|
|
530
|
-
YerbaValueType::String =>
|
|
547
|
+
YerbaValueType::String if !plain => {
|
|
548
|
+
let style = if style.is_null() { None } else { CStr::from_ptr(style).to_str().ok() };
|
|
549
|
+
|
|
550
|
+
crate::syntax::quote_scalar_styled(raw_value, style)
|
|
551
|
+
}
|
|
552
|
+
|
|
531
553
|
_ => raw_value.to_string(),
|
|
532
554
|
};
|
|
533
555
|
|
|
@@ -543,7 +565,13 @@ pub unsafe extern "C" fn yerba_document_insert(
|
|
|
543
565
|
InsertPosition::Last
|
|
544
566
|
};
|
|
545
567
|
|
|
546
|
-
|
|
568
|
+
let result = if plain {
|
|
569
|
+
document.insert_fragment_into(selector_string, &value_string, position)
|
|
570
|
+
} else {
|
|
571
|
+
document.insert_into(selector_string, &value_string, position)
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
match result {
|
|
547
575
|
Ok(()) => YerbaResult::ok(),
|
|
548
576
|
Err(e) => YerbaResult::err(&e.to_string()),
|
|
549
577
|
}
|
|
@@ -843,6 +871,21 @@ pub unsafe extern "C" fn yerba_document_to_string(document: *const Document) ->
|
|
|
843
871
|
CString::new(content).unwrap_or_default().into_raw()
|
|
844
872
|
}
|
|
845
873
|
|
|
874
|
+
#[no_mangle]
|
|
875
|
+
pub unsafe extern "C" fn yerba_quote_scalar(value: *const c_char, length: usize, style: *const c_char) -> *mut c_char {
|
|
876
|
+
let bytes = std::slice::from_raw_parts(value as *const u8, length);
|
|
877
|
+
|
|
878
|
+
let Ok(value) = std::str::from_utf8(bytes) else {
|
|
879
|
+
return std::ptr::null_mut();
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
let style = if style.is_null() { None } else { CStr::from_ptr(style).to_str().ok() };
|
|
883
|
+
|
|
884
|
+
CString::new(crate::syntax::quote_scalar_styled(value, style))
|
|
885
|
+
.map(|string| string.into_raw())
|
|
886
|
+
.unwrap_or(std::ptr::null_mut())
|
|
887
|
+
}
|
|
888
|
+
|
|
846
889
|
#[no_mangle]
|
|
847
890
|
pub unsafe extern "C" fn yerba_string_free(s: *mut c_char) {
|
|
848
891
|
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
|
+
}
|