yerba 0.10.0-x86_64-linux-gnu → 0.10.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.
- checksums.yaml +4 -4
- data/README.md +21 -2
- data/exe/x86_64-linux-gnu/yerba +0 -0
- data/ext/yerba/include/yerba.h +2 -1
- data/ext/yerba/yerba.c +14 -2
- 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/sequence.rb +2 -2
- data/lib/yerba/version.rb +1 -1
- data/rust/Cargo.lock +1 -1
- data/rust/Cargo.toml +1 -1
- data/rust/src/commands/sort.rs +5 -1
- data/rust/src/document/mod.rs +64 -12
- data/rust/src/document/sort.rs +53 -8
- data/rust/src/ffi.rs +17 -2
- data/rust/src/lib.rs +2 -1
- data/rust/src/yerbafile.rs +45 -19
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e96b96fae7c38798de545a68efed672c2e03e984a638225c9fbb4256eadab8bd
|
|
4
|
+
data.tar.gz: b83bb656a8d66cddb052bbca6bd504fcb975e71645147e2c4218f83234047069
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b530e93fde58c6b6f30d1938def129ec052190d17a3328e017dd4e695716e170db29ec87bf4fff32580ac9e044b7b4b19ffe53fa023aaeb0b60bb7d8f64676f
|
|
7
|
+
data.tar.gz: d16f9a4c41340bb897cfc14a30fb9e5a80dfc7071964c49690bc2d3d7d0a14b17d93f068c166a35d29ce6f40092d2d6a5cd53d7b723ce9c924520a57c4b4ebd8
|
data/README.md
CHANGED
|
@@ -300,6 +300,17 @@ yerba sort videos.yml --by ".date" --order desc --by ".title"
|
|
|
300
300
|
yerba sort videos.yml "[].speakers" --by ".name"
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
+
Items without a value for a sort field sort as an empty value, so they come first. Use `--missing` to decide what happens to them instead:
|
|
304
|
+
|
|
305
|
+
- `first` or `last` puts a missing value before or after the present ones, comparing one field at a time like SQL's `NULLS FIRST` and `NULLS LAST`. Sorted by `.date` and `.start_time`, a talk without a time goes first or last within its date.
|
|
306
|
+
- `keep` leaves items with a missing value where they are and sorts the other items around them.
|
|
307
|
+
- `skip` leaves the whole sequence unsorted if any item has a missing value.
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
yerba sort videos.yml --by ".date" --by ".start_time" --missing keep
|
|
312
|
+
```
|
|
313
|
+
|
|
303
314
|
Use `--order` with a comma-separated list to specify an explicit custom order. All items must be listed:
|
|
304
315
|
|
|
305
316
|
```bash
|
|
@@ -582,6 +593,8 @@ pipeline:
|
|
|
582
593
|
|
|
583
594
|
rules:
|
|
584
595
|
- files: "data/**/videos.yml"
|
|
596
|
+
exclude:
|
|
597
|
+
- "data/archive/**/videos.yml"
|
|
585
598
|
pipeline:
|
|
586
599
|
- quote_style:
|
|
587
600
|
selector: "[].speakers"
|
|
@@ -607,7 +620,7 @@ rules:
|
|
|
607
620
|
by: name
|
|
608
621
|
```
|
|
609
622
|
|
|
610
|
-
Rules and steps take a `selector` to scope them. `path` is still accepted as the older name for it.
|
|
623
|
+
Rules and steps take a `selector` to scope them. `path` is still accepted as the older name for it. A rule's `exclude` takes a glob or a list of globs for files the rule skips.
|
|
611
624
|
|
|
612
625
|
Available pipeline steps:
|
|
613
626
|
|
|
@@ -615,7 +628,7 @@ Available pipeline steps:
|
|
|
615
628
|
- `collection_style` Enforce flow or block style on collections
|
|
616
629
|
- `sequence_indent` Enforce compact or indented sequence style
|
|
617
630
|
- `sort_keys` Reorder keys to match a predefined list
|
|
618
|
-
- `sort` Sort sequence items by field(s), `by: ".date,.start_time"` or `by: ".date .start_time"` for tie-breakers
|
|
631
|
+
- `sort` Sort sequence items by field(s), `by: ".date,.start_time"` or `by: ".date .start_time"` for tie-breakers, and `missing: first`, `last`, `keep` or `skip` for items without a value
|
|
619
632
|
- `blank_lines` Enforce blank lines between sequence entries, optionally limited to named map keys with `before` and to non-empty values with `skip_empty`
|
|
620
633
|
- `set` Set a value (supports conditions, and `plain: true` for YAML literals)
|
|
621
634
|
- `insert` Insert a new key or sequence item (`plain: true` for YAML literals and fragments)
|
|
@@ -916,6 +929,12 @@ The `by:` option accepts symbols, strings, or dot-prefixed strings (`:name`, `"n
|
|
|
916
929
|
|
|
917
930
|
The `order:` option is `:asc`, `:desc`, or an explicit order of values as an Array or comma-separated String.
|
|
918
931
|
|
|
932
|
+
The `missing:` option decides what happens to items without a value for a sort field: `:first` or `:last` put the missing value before or after the present ones for each field, `:keep` leaves those items in place and sorts the others into the remaining positions, and `:skip` leaves the sequence unsorted if any value is missing. Without it, a missing value sorts as an empty string.
|
|
933
|
+
|
|
934
|
+
```ruby
|
|
935
|
+
document.sort(by: [:date, :start_time], missing: :keep)
|
|
936
|
+
```
|
|
937
|
+
|
|
919
938
|
### Querying
|
|
920
939
|
|
|
921
940
|
Find and filter items in sequences with `find_by`, `where`, and `pluck`:
|
data/exe/x86_64-linux-gnu/yerba
CHANGED
|
Binary file
|
data/ext/yerba/include/yerba.h
CHANGED
|
@@ -211,7 +211,8 @@ struct YerbaResult yerba_document_rename(struct Document *document,
|
|
|
211
211
|
struct YerbaResult yerba_document_sort(struct Document *document,
|
|
212
212
|
const char *selector,
|
|
213
213
|
const char *by,
|
|
214
|
-
bool case_sensitive
|
|
214
|
+
bool case_sensitive,
|
|
215
|
+
const char *missing);
|
|
215
216
|
|
|
216
217
|
struct YerbaResult yerba_document_reorder(struct Document *document,
|
|
217
218
|
const char *selector,
|
data/ext/yerba/yerba.c
CHANGED
|
@@ -902,10 +902,11 @@ static VALUE document_rename(VALUE self, VALUE source, VALUE destination) {
|
|
|
902
902
|
return self;
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
-
/* document.sort(selector = "", by: nil, order: nil, case_sensitive: false)
|
|
905
|
+
/* document.sort(selector = "", by: nil, order: nil, case_sensitive: false, missing: nil)
|
|
906
906
|
*
|
|
907
907
|
* by: a field, several fields separated by commas or spaces, or an Array of fields
|
|
908
908
|
* order: :asc, :desc, or an explicit order of values (Array or comma-separated String)
|
|
909
|
+
* missing: :first, :last, :keep or :skip, for items without a value for a sort field
|
|
909
910
|
*/
|
|
910
911
|
static VALUE document_sort(int argc, VALUE *argv, VALUE self) {
|
|
911
912
|
VALUE selector, opts;
|
|
@@ -917,6 +918,7 @@ static VALUE document_sort(int argc, VALUE *argv, VALUE self) {
|
|
|
917
918
|
VALUE order_csv = Qnil;
|
|
918
919
|
bool descending = false;
|
|
919
920
|
bool case_sensitive = false;
|
|
921
|
+
VALUE missing_value = Qnil;
|
|
920
922
|
|
|
921
923
|
if (!NIL_P(opts)) {
|
|
922
924
|
VALUE v_by = rb_hash_aref(opts, ID2SYM(rb_intern("by")));
|
|
@@ -947,11 +949,20 @@ static VALUE document_sort(int argc, VALUE *argv, VALUE self) {
|
|
|
947
949
|
if (RTEST(v_case_sensitive)) {
|
|
948
950
|
case_sensitive = true;
|
|
949
951
|
}
|
|
952
|
+
|
|
953
|
+
VALUE v_missing = rb_hash_aref(opts, ID2SYM(rb_intern("missing")));
|
|
954
|
+
|
|
955
|
+
if (SYMBOL_P(v_missing)) {
|
|
956
|
+
missing_value = rb_sym2str(v_missing);
|
|
957
|
+
} else if (!NIL_P(v_missing)) {
|
|
958
|
+
missing_value = rb_obj_as_string(v_missing);
|
|
959
|
+
}
|
|
950
960
|
}
|
|
951
961
|
|
|
952
962
|
struct Document *document = get_document(self);
|
|
953
963
|
const char *path_string = StringValueCStr(selector);
|
|
954
964
|
const char *by = NIL_P(by_value) ? NULL : StringValueCStr(by_value);
|
|
965
|
+
const char *missing = NIL_P(missing_value) ? NULL : StringValueCStr(missing_value);
|
|
955
966
|
|
|
956
967
|
if (!NIL_P(order_csv)) {
|
|
957
968
|
YerbaResult result = yerba_document_reorder(document, path_string, by ? by : ".", StringValueCStr(order_csv));
|
|
@@ -970,11 +981,12 @@ static VALUE document_sort(int argc, VALUE *argv, VALUE self) {
|
|
|
970
981
|
by = StringValueCStr(by_with_order);
|
|
971
982
|
}
|
|
972
983
|
|
|
973
|
-
YerbaResult result = yerba_document_sort(document, path_string, by, case_sensitive);
|
|
984
|
+
YerbaResult result = yerba_document_sort(document, path_string, by, case_sensitive, missing);
|
|
974
985
|
check_result(result);
|
|
975
986
|
|
|
976
987
|
RB_GC_GUARD(by_value);
|
|
977
988
|
RB_GC_GUARD(by_with_order);
|
|
989
|
+
RB_GC_GUARD(missing_value);
|
|
978
990
|
|
|
979
991
|
return self;
|
|
980
992
|
}
|
data/lib/yerba/3.2/yerba.so
CHANGED
|
Binary file
|
data/lib/yerba/3.3/yerba.so
CHANGED
|
Binary file
|
data/lib/yerba/3.4/yerba.so
CHANGED
|
Binary file
|
data/lib/yerba/4.0/yerba.so
CHANGED
|
Binary file
|
data/lib/yerba/sequence.rb
CHANGED
|
@@ -268,8 +268,8 @@ module Yerba
|
|
|
268
268
|
self
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
-
def sort(by: nil, order: nil, case_sensitive: false)
|
|
272
|
-
document&.sort(@selector, by: by, order: order, case_sensitive: case_sensitive)
|
|
271
|
+
def sort(by: nil, order: nil, case_sensitive: false, missing: nil)
|
|
272
|
+
document&.sort(@selector, by: by, order: order, case_sensitive: case_sensitive, missing: missing)
|
|
273
273
|
|
|
274
274
|
self
|
|
275
275
|
end
|
data/lib/yerba/version.rb
CHANGED
data/rust/Cargo.lock
CHANGED
data/rust/Cargo.toml
CHANGED
data/rust/src/commands/sort.rs
CHANGED
|
@@ -12,6 +12,7 @@ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
|
12
12
|
yerba sort config.yml "tags"
|
|
13
13
|
yerba sort videos.yml --by ".title"
|
|
14
14
|
yerba sort videos.yml --by ".date" --order desc --by ".title"
|
|
15
|
+
yerba sort videos.yml --by ".date" --by ".start_time" --missing keep
|
|
15
16
|
yerba sort videos.yml "[].speakers" --by ".name"
|
|
16
17
|
yerba sort videos.yml "[]" --by ".id" --order "talk-3,talk-1,talk-2"
|
|
17
18
|
yerba sort speakers.yml "[]" --by ".name" --order "Charlie,Alice,Bob"
|
|
@@ -40,6 +41,9 @@ pub struct Args {
|
|
|
40
41
|
/// Case-sensitive sort (default: case-insensitive)
|
|
41
42
|
#[arg(long)]
|
|
42
43
|
case_sensitive: bool,
|
|
44
|
+
/// Items without a value for a sort field: first, last, keep (stay in place) or skip (leave the sequence unsorted)
|
|
45
|
+
#[arg(long)]
|
|
46
|
+
missing: Option<yerba::SortMissing>,
|
|
43
47
|
#[arg(long)]
|
|
44
48
|
dry_run: bool,
|
|
45
49
|
}
|
|
@@ -199,7 +203,7 @@ impl Args {
|
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
if document.
|
|
206
|
+
if document.sort_items_with(selector, &sort_fields, self.case_sensitive, self.missing).is_ok() {
|
|
203
207
|
output(&resolved_file, &document, self.dry_run);
|
|
204
208
|
}
|
|
205
209
|
}
|
data/rust/src/document/mod.rs
CHANGED
|
@@ -116,6 +116,29 @@ impl SortField {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
|
120
|
+
#[serde(rename_all = "lowercase")]
|
|
121
|
+
pub enum SortMissing {
|
|
122
|
+
First,
|
|
123
|
+
Last,
|
|
124
|
+
Keep,
|
|
125
|
+
Skip,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl std::str::FromStr for SortMissing {
|
|
129
|
+
type Err = String;
|
|
130
|
+
|
|
131
|
+
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
132
|
+
match input {
|
|
133
|
+
"first" => Ok(SortMissing::First),
|
|
134
|
+
"last" => Ok(SortMissing::Last),
|
|
135
|
+
"keep" => Ok(SortMissing::Keep),
|
|
136
|
+
"skip" => Ok(SortMissing::Skip),
|
|
137
|
+
_ => Err(format!("unknown missing: {} (expected first, last, keep or skip)", input)),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
#[repr(C)]
|
|
120
143
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
121
144
|
pub enum NodeType {
|
|
@@ -849,6 +872,7 @@ pub(crate) fn navigate_from_node(node: &SyntaxNode, selector: &str) -> Vec<Synta
|
|
|
849
872
|
pub(crate) struct EntryGroup {
|
|
850
873
|
pub(crate) separator: String,
|
|
851
874
|
pub(crate) preceding: String,
|
|
875
|
+
pub(crate) gap: String,
|
|
852
876
|
pub(crate) body: String,
|
|
853
877
|
}
|
|
854
878
|
|
|
@@ -857,7 +881,7 @@ impl EntryGroup {
|
|
|
857
881
|
if self.preceding.is_empty() {
|
|
858
882
|
self.body.clone()
|
|
859
883
|
} else {
|
|
860
|
-
format!("{}
|
|
884
|
+
format!("{}{}{}", self.preceding, self.gap, self.body)
|
|
861
885
|
}
|
|
862
886
|
}
|
|
863
887
|
}
|
|
@@ -892,15 +916,17 @@ fn collect_entry_groups(parent: &SyntaxNode) -> Vec<EntryGroup> {
|
|
|
892
916
|
let entry_text = child.as_node().unwrap().text().to_string();
|
|
893
917
|
|
|
894
918
|
if groups.is_empty() {
|
|
895
|
-
let preceding = buffer.trim_start_matches('\n')
|
|
919
|
+
let (preceding, gap) = split_gap(buffer.trim_start_matches('\n'));
|
|
896
920
|
|
|
897
921
|
groups.push(EntryGroup {
|
|
898
922
|
separator: String::new(),
|
|
899
923
|
preceding,
|
|
924
|
+
gap,
|
|
900
925
|
body: entry_text,
|
|
901
926
|
});
|
|
902
927
|
} else {
|
|
903
928
|
let (trailing, separator, preceding) = split_at_blank_line(&buffer);
|
|
929
|
+
let (preceding, gap) = split_gap(&preceding);
|
|
904
930
|
|
|
905
931
|
if let Some(last) = groups.last_mut() {
|
|
906
932
|
last.body.push_str(&trailing);
|
|
@@ -909,6 +935,7 @@ fn collect_entry_groups(parent: &SyntaxNode) -> Vec<EntryGroup> {
|
|
|
909
935
|
groups.push(EntryGroup {
|
|
910
936
|
separator,
|
|
911
937
|
preceding,
|
|
938
|
+
gap,
|
|
912
939
|
body: entry_text,
|
|
913
940
|
});
|
|
914
941
|
}
|
|
@@ -947,7 +974,7 @@ fn split_at_blank_line(text: &str) -> (String, String, String) {
|
|
|
947
974
|
let rest = &text[position..];
|
|
948
975
|
let content_start = rest.len() - rest.trim_start_matches('\n').len();
|
|
949
976
|
let separator = rest[..content_start].to_string();
|
|
950
|
-
let preceding = rest[content_start..].
|
|
977
|
+
let preceding = rest[content_start..].to_string();
|
|
951
978
|
|
|
952
979
|
(trailing, separator, preceding)
|
|
953
980
|
} else {
|
|
@@ -955,8 +982,16 @@ fn split_at_blank_line(text: &str) -> (String, String, String) {
|
|
|
955
982
|
}
|
|
956
983
|
}
|
|
957
984
|
|
|
958
|
-
|
|
959
|
-
let
|
|
985
|
+
fn split_gap(text: &str) -> (String, String) {
|
|
986
|
+
let content = text.trim_end_matches(['\n', ' ', '\t']);
|
|
987
|
+
let newlines = text[content.len()..].matches('\n').count().max(1);
|
|
988
|
+
|
|
989
|
+
(content.to_string(), "\n".repeat(newlines))
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
pub(crate) fn collect_preceding_sibling_comments(parent: &SyntaxNode) -> (String, String, Option<rowan::TextSize>) {
|
|
993
|
+
let mut comments: Vec<(String, usize)> = Vec::new();
|
|
994
|
+
let mut newlines = 0;
|
|
960
995
|
let mut earliest_start = None;
|
|
961
996
|
let mut node = parent.clone();
|
|
962
997
|
|
|
@@ -967,10 +1002,11 @@ pub(crate) fn collect_preceding_sibling_comments(parent: &SyntaxNode) -> (String
|
|
|
967
1002
|
match element {
|
|
968
1003
|
rowan::NodeOrToken::Token(token) => {
|
|
969
1004
|
if token.kind() == SyntaxKind::COMMENT {
|
|
970
|
-
comments.push(token.text().to_string());
|
|
1005
|
+
comments.push((token.text().to_string(), newlines));
|
|
1006
|
+
newlines = 0;
|
|
971
1007
|
earliest_start = Some(token.text_range().start());
|
|
972
1008
|
} else if token.kind() == SyntaxKind::WHITESPACE {
|
|
973
|
-
|
|
1009
|
+
newlines += token.text().matches('\n').count();
|
|
974
1010
|
} else {
|
|
975
1011
|
break;
|
|
976
1012
|
}
|
|
@@ -984,7 +1020,7 @@ pub(crate) fn collect_preceding_sibling_comments(parent: &SyntaxNode) -> (String
|
|
|
984
1020
|
};
|
|
985
1021
|
}
|
|
986
1022
|
|
|
987
|
-
if !comments.is_empty() {
|
|
1023
|
+
if !comments.is_empty() || sibling.is_some() {
|
|
988
1024
|
break;
|
|
989
1025
|
}
|
|
990
1026
|
|
|
@@ -996,21 +1032,37 @@ pub(crate) fn collect_preceding_sibling_comments(parent: &SyntaxNode) -> (String
|
|
|
996
1032
|
}
|
|
997
1033
|
}
|
|
998
1034
|
|
|
1035
|
+
let gap = "\n".repeat(comments.first().map_or(1, |(_, newlines)| (*newlines).max(1)));
|
|
1036
|
+
|
|
999
1037
|
comments.reverse();
|
|
1000
|
-
|
|
1038
|
+
|
|
1039
|
+
let text = comments
|
|
1040
|
+
.iter()
|
|
1041
|
+
.enumerate()
|
|
1042
|
+
.map(|(index, (comment, newlines))| {
|
|
1043
|
+
if index + 1 < comments.len() {
|
|
1044
|
+
format!("{}{}", comment, "\n".repeat((*newlines).max(1)))
|
|
1045
|
+
} else {
|
|
1046
|
+
comment.clone()
|
|
1047
|
+
}
|
|
1048
|
+
})
|
|
1049
|
+
.collect();
|
|
1050
|
+
|
|
1051
|
+
(text, gap, earliest_start)
|
|
1001
1052
|
}
|
|
1002
1053
|
|
|
1003
1054
|
pub(crate) fn collect_groups_with_range(parent: &SyntaxNode) -> (Vec<EntryGroup>, TextRange) {
|
|
1004
1055
|
let mut groups = collect_entry_groups(parent);
|
|
1005
1056
|
|
|
1006
|
-
let (sibling_comments, earliest_start) = collect_preceding_sibling_comments(parent);
|
|
1057
|
+
let (sibling_comments, sibling_gap, earliest_start) = collect_preceding_sibling_comments(parent);
|
|
1007
1058
|
|
|
1008
1059
|
if !sibling_comments.is_empty() {
|
|
1009
1060
|
if let Some(first) = groups.first_mut() {
|
|
1010
1061
|
if first.preceding.is_empty() {
|
|
1011
1062
|
first.preceding = sibling_comments;
|
|
1063
|
+
first.gap = sibling_gap;
|
|
1012
1064
|
} else {
|
|
1013
|
-
first.preceding = format!("{}
|
|
1065
|
+
first.preceding = format!("{}{}{}", sibling_comments, sibling_gap, first.preceding);
|
|
1014
1066
|
}
|
|
1015
1067
|
}
|
|
1016
1068
|
}
|
|
@@ -1050,7 +1102,7 @@ pub(crate) fn rebuild_from_groups(groups: &[EntryGroup], indent: &str, preserve_
|
|
|
1050
1102
|
if group.preceding.is_empty() {
|
|
1051
1103
|
format!("{}{}{}", separator, indent, group.body)
|
|
1052
1104
|
} else {
|
|
1053
|
-
format!("{}{}
|
|
1105
|
+
format!("{}{}{}{}{}", separator, group.preceding, group.gap, indent, group.body)
|
|
1054
1106
|
}
|
|
1055
1107
|
}
|
|
1056
1108
|
})
|
data/rust/src/document/sort.rs
CHANGED
|
@@ -225,8 +225,12 @@ impl Document {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
pub fn sort_items(&mut self, selector: &str, sort_fields: &[SortField], case_sensitive: bool) -> Result<(), YerbaError> {
|
|
228
|
+
self.sort_items_with(selector, sort_fields, case_sensitive, None)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
pub fn sort_items_with(&mut self, selector: &str, sort_fields: &[SortField], case_sensitive: bool, missing: Option<SortMissing>) -> Result<(), YerbaError> {
|
|
228
232
|
if selector.contains("[].") {
|
|
229
|
-
return self.sort_each_items(selector, sort_fields, case_sensitive);
|
|
233
|
+
return self.sort_each_items(selector, sort_fields, case_sensitive, missing);
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
let current_node = self.navigate(selector)?;
|
|
@@ -236,13 +240,13 @@ impl Document {
|
|
|
236
240
|
None => return Ok(()),
|
|
237
241
|
};
|
|
238
242
|
|
|
239
|
-
match sort_sequence_edit(&sequence, sort_fields, case_sensitive) {
|
|
243
|
+
match sort_sequence_edit(&sequence, sort_fields, case_sensitive, missing) {
|
|
240
244
|
Some(edit) => self.apply_edits(vec![edit]),
|
|
241
245
|
None => Ok(()),
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
|
|
245
|
-
fn sort_each_items(&mut self, selector: &str, sort_fields: &[SortField], case_sensitive: bool) -> Result<(), YerbaError> {
|
|
249
|
+
fn sort_each_items(&mut self, selector: &str, sort_fields: &[SortField], case_sensitive: bool, missing: Option<SortMissing>) -> Result<(), YerbaError> {
|
|
246
250
|
let (parent_selector, child_selector) = if let Some(last_bracket) = selector.rfind("[].") {
|
|
247
251
|
(&selector[..last_bracket + 2], &selector[last_bracket + 3..])
|
|
248
252
|
} else {
|
|
@@ -265,7 +269,7 @@ impl Document {
|
|
|
265
269
|
None => continue,
|
|
266
270
|
};
|
|
267
271
|
|
|
268
|
-
if let Some(edit) = sort_sequence_edit(&sequence, sort_fields, case_sensitive) {
|
|
272
|
+
if let Some(edit) = sort_sequence_edit(&sequence, sort_fields, case_sensitive, missing) {
|
|
269
273
|
edits.push(edit);
|
|
270
274
|
}
|
|
271
275
|
}
|
|
@@ -398,7 +402,7 @@ fn sort_map_edit(map: &BlockMap, key_order: &[&str]) -> Option<(TextRange, Strin
|
|
|
398
402
|
Some((range, rebuild_from_groups(&sorted_groups, &indent, false)))
|
|
399
403
|
}
|
|
400
404
|
|
|
401
|
-
fn sort_sequence_edit(sequence: &BlockSeq, sort_fields: &[SortField], case_sensitive: bool) -> Option<(TextRange, String)> {
|
|
405
|
+
fn sort_sequence_edit(sequence: &BlockSeq, sort_fields: &[SortField], case_sensitive: bool, missing: Option<SortMissing>) -> Option<(TextRange, String)> {
|
|
402
406
|
let entries: Vec<_> = sequence.entries().collect();
|
|
403
407
|
|
|
404
408
|
if entries.len() <= 1 {
|
|
@@ -415,7 +419,30 @@ fn sort_sequence_edit(sequence: &BlockSeq, sort_fields: &[SortField], case_sensi
|
|
|
415
419
|
|
|
416
420
|
let original_bodies: Vec<String> = sortable.iter().map(|(_, group)| group.body.clone()).collect();
|
|
417
421
|
|
|
418
|
-
|
|
422
|
+
let compare = |(values_a, _): &(Vec<String>, EntryGroup), (values_b, _): &(Vec<String>, EntryGroup)| {
|
|
423
|
+
compare_sort_values(values_a, values_b, sort_fields, case_sensitive, missing)
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
let is_absent = |(values, _): &(Vec<String>, EntryGroup)| values.iter().any(|value| value.is_empty());
|
|
427
|
+
|
|
428
|
+
match missing {
|
|
429
|
+
Some(SortMissing::Skip) if sortable.iter().any(is_absent) => return None,
|
|
430
|
+
Some(SortMissing::Keep) => {
|
|
431
|
+
let absent_positions: Vec<bool> = sortable.iter().map(is_absent).collect();
|
|
432
|
+
let (absent, mut present): (Vec<_>, Vec<_>) = sortable.into_iter().partition(is_absent);
|
|
433
|
+
|
|
434
|
+
present.sort_by(compare);
|
|
435
|
+
|
|
436
|
+
let mut present = present.into_iter();
|
|
437
|
+
let mut absent = absent.into_iter();
|
|
438
|
+
|
|
439
|
+
sortable = absent_positions
|
|
440
|
+
.iter()
|
|
441
|
+
.filter_map(|is_absent| if *is_absent { absent.next() } else { present.next() })
|
|
442
|
+
.collect();
|
|
443
|
+
}
|
|
444
|
+
_ => sortable.sort_by(compare),
|
|
445
|
+
}
|
|
419
446
|
|
|
420
447
|
let sorted_bodies: Vec<String> = sortable.iter().map(|(_, group)| group.body.clone()).collect();
|
|
421
448
|
|
|
@@ -449,7 +476,13 @@ fn entry_sort_values(entry: &yaml_parser::ast::BlockSeqEntry, sort_fields: &[Sor
|
|
|
449
476
|
.collect()
|
|
450
477
|
}
|
|
451
478
|
|
|
452
|
-
fn compare_sort_values(
|
|
479
|
+
fn compare_sort_values(
|
|
480
|
+
values_a: &[String],
|
|
481
|
+
values_b: &[String],
|
|
482
|
+
sort_fields: &[SortField],
|
|
483
|
+
case_sensitive: bool,
|
|
484
|
+
missing: Option<SortMissing>,
|
|
485
|
+
) -> std::cmp::Ordering {
|
|
453
486
|
let compare = |value_a: &String, value_b: &String| {
|
|
454
487
|
if case_sensitive {
|
|
455
488
|
value_a.cmp(value_b)
|
|
@@ -458,7 +491,19 @@ fn compare_sort_values(values_a: &[String], values_b: &[String], sort_fields: &[
|
|
|
458
491
|
}
|
|
459
492
|
};
|
|
460
493
|
|
|
494
|
+
let missing_ordering = |value_a: &String, value_b: &String| match (missing, value_a.is_empty(), value_b.is_empty()) {
|
|
495
|
+
(Some(SortMissing::First), true, false) => Some(std::cmp::Ordering::Less),
|
|
496
|
+
(Some(SortMissing::First), false, true) => Some(std::cmp::Ordering::Greater),
|
|
497
|
+
(Some(SortMissing::Last), true, false) => Some(std::cmp::Ordering::Greater),
|
|
498
|
+
(Some(SortMissing::Last), false, true) => Some(std::cmp::Ordering::Less),
|
|
499
|
+
_ => None,
|
|
500
|
+
};
|
|
501
|
+
|
|
461
502
|
for (index, field) in sort_fields.iter().enumerate().take(values_a.len()) {
|
|
503
|
+
if let Some(ordering) = missing_ordering(&values_a[index], &values_b[index]) {
|
|
504
|
+
return ordering;
|
|
505
|
+
}
|
|
506
|
+
|
|
462
507
|
let ordering = compare(&values_a[index], &values_b[index]);
|
|
463
508
|
let ordering = if field.ascending { ordering } else { ordering.reverse() };
|
|
464
509
|
|
|
@@ -468,7 +513,7 @@ fn compare_sort_values(values_a: &[String], values_b: &[String], sort_fields: &[
|
|
|
468
513
|
}
|
|
469
514
|
|
|
470
515
|
if sort_fields.is_empty() && !values_a.is_empty() && !values_b.is_empty() {
|
|
471
|
-
return compare(&values_a[0], &values_b[0]);
|
|
516
|
+
return missing_ordering(&values_a[0], &values_b[0]).unwrap_or_else(|| compare(&values_a[0], &values_b[0]));
|
|
472
517
|
}
|
|
473
518
|
|
|
474
519
|
std::cmp::Ordering::Equal
|
data/rust/src/ffi.rs
CHANGED
|
@@ -811,7 +811,13 @@ pub unsafe extern "C" fn yerba_document_rename(document: *mut Document, source:
|
|
|
811
811
|
}
|
|
812
812
|
|
|
813
813
|
#[no_mangle]
|
|
814
|
-
pub unsafe extern "C" fn yerba_document_sort(
|
|
814
|
+
pub unsafe extern "C" fn yerba_document_sort(
|
|
815
|
+
document: *mut Document,
|
|
816
|
+
selector: *const c_char,
|
|
817
|
+
by: *const c_char,
|
|
818
|
+
case_sensitive: bool,
|
|
819
|
+
missing: *const c_char,
|
|
820
|
+
) -> YerbaResult {
|
|
815
821
|
let document = &mut *document;
|
|
816
822
|
let selector_string = CStr::from_ptr(selector).to_str().unwrap_or("");
|
|
817
823
|
|
|
@@ -822,7 +828,16 @@ pub unsafe extern "C" fn yerba_document_sort(document: *mut Document, selector:
|
|
|
822
828
|
None => vec![],
|
|
823
829
|
};
|
|
824
830
|
|
|
825
|
-
|
|
831
|
+
let missing = if missing.is_null() {
|
|
832
|
+
None
|
|
833
|
+
} else {
|
|
834
|
+
match CStr::from_ptr(missing).to_str().unwrap_or("").parse::<crate::SortMissing>() {
|
|
835
|
+
Ok(missing) => Some(missing),
|
|
836
|
+
Err(error) => return YerbaResult::err(&error),
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
match document.sort_items_with(selector_string, &sort_fields, case_sensitive, missing) {
|
|
826
841
|
Ok(()) => YerbaResult::ok(),
|
|
827
842
|
Err(e) => YerbaResult::err(&e.to_string()),
|
|
828
843
|
}
|
data/rust/src/lib.rs
CHANGED
|
@@ -15,7 +15,8 @@ pub mod yerbafile;
|
|
|
15
15
|
|
|
16
16
|
pub use document::style::{BlankLineOptions, StyleEnforcement};
|
|
17
17
|
pub use document::{
|
|
18
|
-
collect_selectors, validate_condition, validate_item_condition, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType,
|
|
18
|
+
collect_selectors, validate_condition, validate_item_condition, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType,
|
|
19
|
+
SortField, SortMissing,
|
|
19
20
|
};
|
|
20
21
|
pub use error::YerbaError;
|
|
21
22
|
pub use quote_style::{KeyStyle, QuoteStyle};
|
data/rust/src/yerbafile.rs
CHANGED
|
@@ -22,12 +22,44 @@ pub struct Yerbafile {
|
|
|
22
22
|
#[derive(Debug, Clone, Deserialize)]
|
|
23
23
|
pub struct Rule {
|
|
24
24
|
pub files: String,
|
|
25
|
+
#[serde(default, deserialize_with = "string_or_list")]
|
|
26
|
+
pub exclude: Vec<String>,
|
|
25
27
|
#[serde(default)]
|
|
26
28
|
#[serde(alias = "path")]
|
|
27
29
|
pub selector: Option<String>,
|
|
28
30
|
pub pipeline: Vec<PipelineStep>,
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
impl Rule {
|
|
34
|
+
pub fn matches(&self, file: &str) -> bool {
|
|
35
|
+
matches_glob(&self.files, file) && !self.excludes(file)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn excludes(&self, file: &str) -> bool {
|
|
39
|
+
self.exclude.iter().any(|pattern| matches_glob(pattern, file))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn matches_glob(pattern: &str, file: &str) -> bool {
|
|
44
|
+
glob::Pattern::new(pattern)
|
|
45
|
+
.map(|pattern| pattern.matches(file) || pattern.matches_path(Path::new(file)))
|
|
46
|
+
.unwrap_or(false)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn string_or_list<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Vec<String>, D::Error> {
|
|
50
|
+
#[derive(Deserialize)]
|
|
51
|
+
#[serde(untagged)]
|
|
52
|
+
enum StringOrList {
|
|
53
|
+
String(String),
|
|
54
|
+
List(Vec<String>),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Ok(match StringOrList::deserialize(deserializer)? {
|
|
58
|
+
StringOrList::String(string) => vec![string],
|
|
59
|
+
StringOrList::List(list) => list,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
#[derive(Debug, Clone)]
|
|
32
64
|
pub enum PipelineStep {
|
|
33
65
|
SortKeys(SortKeysConfig),
|
|
@@ -57,6 +89,8 @@ pub struct SortConfig {
|
|
|
57
89
|
pub by: Option<String>,
|
|
58
90
|
#[serde(default)]
|
|
59
91
|
pub case_sensitive: bool,
|
|
92
|
+
#[serde(default)]
|
|
93
|
+
pub missing: Option<crate::SortMissing>,
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
#[derive(Debug, Clone, Deserialize)]
|
|
@@ -391,10 +425,8 @@ impl Yerbafile {
|
|
|
391
425
|
continue;
|
|
392
426
|
}
|
|
393
427
|
|
|
394
|
-
if
|
|
395
|
-
|
|
396
|
-
return Some(config.order.clone());
|
|
397
|
-
}
|
|
428
|
+
if rule.matches(file_path) {
|
|
429
|
+
return Some(config.order.clone());
|
|
398
430
|
}
|
|
399
431
|
}
|
|
400
432
|
}
|
|
@@ -421,7 +453,11 @@ impl Yerbafile {
|
|
|
421
453
|
}
|
|
422
454
|
};
|
|
423
455
|
|
|
424
|
-
let file_strings: Vec<String> = files
|
|
456
|
+
let file_strings: Vec<String> = files
|
|
457
|
+
.iter()
|
|
458
|
+
.map(|path| path.to_string_lossy().to_string())
|
|
459
|
+
.filter(|file| !rule.excludes(file))
|
|
460
|
+
.collect();
|
|
425
461
|
|
|
426
462
|
let file_results: Vec<RuleResult> = file_strings
|
|
427
463
|
.par_iter()
|
|
@@ -526,11 +562,7 @@ impl Yerbafile {
|
|
|
526
562
|
let mut ran_global = false;
|
|
527
563
|
|
|
528
564
|
for rule in &self.rules {
|
|
529
|
-
if
|
|
530
|
-
if !pattern.matches(file) && !pattern.matches_path(Path::new(file)) {
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
} else {
|
|
565
|
+
if !rule.matches(file) {
|
|
534
566
|
continue;
|
|
535
567
|
}
|
|
536
568
|
|
|
@@ -642,14 +674,8 @@ impl Yerbafile {
|
|
|
642
674
|
}
|
|
643
675
|
|
|
644
676
|
for rule in &self.rules {
|
|
645
|
-
if !match_path.is_empty() {
|
|
646
|
-
|
|
647
|
-
if !pattern.matches(match_path) && !pattern.matches_path(Path::new(match_path)) {
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
} else {
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
677
|
+
if !match_path.is_empty() && !rule.matches(match_path) {
|
|
678
|
+
continue;
|
|
653
679
|
}
|
|
654
680
|
|
|
655
681
|
let base_selector = rule.selector.as_deref();
|
|
@@ -873,7 +899,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_selector: Opt
|
|
|
873
899
|
let full_selector = resolve_step_selector(base_selector, config.selector.as_deref());
|
|
874
900
|
let sort_fields = config.by.as_deref().map(crate::SortField::parse_list).unwrap_or_default();
|
|
875
901
|
|
|
876
|
-
document.
|
|
902
|
+
document.sort_items_with(&full_selector, &sort_fields, config.case_sensitive, config.missing)
|
|
877
903
|
}
|
|
878
904
|
|
|
879
905
|
PipelineStep::Directives(config) => {
|