yerba 0.10.0-arm-linux-gnu → 0.10.1-arm-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7789ff3de901f30948a18b6cfd0c212338cc61745571c00d831f06b11e6ba20e
4
- data.tar.gz: 56b6aeb3fa2581196fc1263757938de9e91293e1e0ec41688ceeddf5ecb514b8
3
+ metadata.gz: 42897dffe011fcd43e99b7a67d1de70053e906cf3a3653537d2badf2163cc5fe
4
+ data.tar.gz: d3c63fa95e77e6b66c2994d72477e82579e9e86643fd29517d94d2377848b8ea
5
5
  SHA512:
6
- metadata.gz: 260346f5d5148442585ec2b000a83c12c807213d0d1a34b3e607ff881f3a8514820cfa895a3591bc042590ac50553f5236af132a7bb32ae34ba817cb96f6f04f
7
- data.tar.gz: 0f3658ebc475a23e90af6b70682dee1502994c6fcb9fc48fd266d7dfbb164b975af6e5ed8da07dc68c9c0d54b077af483700bd55f8b43848875260ba84f867b4
6
+ metadata.gz: 13b1ba3cd623e233181ddb6282e792a431b7fdba77b00342d1b1c0a7fe8e6d7f97b6db887d9880c537122421f0fbef8b5d0a2bc23719a4847e9d15dfccf7eada
7
+ data.tar.gz: 122d6d4d6cefa4a5ed626d90a03b9eac73d5d5715bf46292c61b500844b771a8c8a77dc19a8d5adda027a2014559d7574069b2ef01f100058d9712c60861e3a0
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`:
Binary file
@@ -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
  }
Binary file
Binary file
Binary file
Binary file
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yerba
4
- VERSION = "0.10.0"
4
+ VERSION = "0.10.1"
5
5
  end
data/rust/Cargo.lock CHANGED
@@ -1431,7 +1431,7 @@ dependencies = [
1431
1431
 
1432
1432
  [[package]]
1433
1433
  name = "yerba"
1434
- version = "0.10.0"
1434
+ version = "0.10.1"
1435
1435
  dependencies = [
1436
1436
  "anstyle-query",
1437
1437
  "cbindgen",
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "yerba"
3
- version = "0.10.0"
3
+ version = "0.10.1"
4
4
  edition = "2021"
5
5
  authors = ["Marco Roth <marco.roth@intergga.ch>"]
6
6
  description = "YAML Editing and Refactoring with Better Accuracy"
@@ -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.sort_items(selector, &sort_fields, self.case_sensitive).is_ok() {
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
  }
@@ -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!("{}\n{}", self.preceding, self.body)
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').to_string();
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..].trim_end_matches(['\n', ' ', '\t']).to_string();
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
- pub(crate) fn collect_preceding_sibling_comments(parent: &SyntaxNode) -> (String, Option<rowan::TextSize>) {
959
- let mut comments: Vec<String> = Vec::new();
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
- // Keep looking past whitespace
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
- (comments.join("\n"), earliest_start)
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!("{}\n{}", sibling_comments, first.preceding);
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!("{}{}\n{}{}", separator, group.preceding, indent, group.body)
1105
+ format!("{}{}{}{}{}", separator, group.preceding, group.gap, indent, group.body)
1054
1106
  }
1055
1107
  }
1056
1108
  })
@@ -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
- sortable.sort_by(|(values_a, _), (values_b, _)| compare_sort_values(values_a, values_b, sort_fields, case_sensitive));
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(values_a: &[String], values_b: &[String], sort_fields: &[SortField], case_sensitive: bool) -> std::cmp::Ordering {
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(document: *mut Document, selector: *const c_char, by: *const c_char, case_sensitive: bool) -> YerbaResult {
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
- match document.sort_items(selector_string, &sort_fields, case_sensitive) {
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, SortField,
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};
@@ -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 let Ok(pattern) = glob::Pattern::new(&rule.files) {
395
- if pattern.matches(file_path) || pattern.matches_path(Path::new(file_path)) {
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.iter().map(|path| path.to_string_lossy().to_string()).collect();
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 let Ok(pattern) = glob::Pattern::new(&rule.files) {
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
- if let Ok(pattern) = glob::Pattern::new(&rule.files) {
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.sort_items(&full_selector, &sort_fields, config.case_sensitive)
902
+ document.sort_items_with(&full_selector, &sort_fields, config.case_sensitive, config.missing)
877
903
  }
878
904
 
879
905
  PipelineStep::Directives(config) => {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yerba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.10.1
5
5
  platform: arm-linux-gnu
6
6
  authors:
7
7
  - Marco Roth