yerba 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a769e82895bc0bea8b8e25391f39cdbf8e915a46d524efd05af951738b4b046a
4
- data.tar.gz: 83d41f698f888f6065729b8adb5e6646e14b9832871eafde6af2fa60bfd4b862
3
+ metadata.gz: be42a92f67226567e1ce9b74e3a2fdba79b06a3dcb4f3a6636ef1b002f7d67c4
4
+ data.tar.gz: 5f1183494d12c2b27bc9658b50368d4fa5280fb676fbe9a11c5cb75b8c3deb74
5
5
  SHA512:
6
- metadata.gz: 64e3fe5431316bc5faf03187419f234f52c9d8f04710d06afdd6bf13dc5342a80dadae3a4345ee610f4f307976d45520f0cc6b4bcc7a7959e21a9902ae071343
7
- data.tar.gz: 3f12d6fa79ed13905bca60481eb3a87caffa46fd8205b6a9d56a8fcf250ce708698e218a4cec7126294940eb6ce8f44e02abf445144c12b9f39ad16f417a1da7
6
+ metadata.gz: d7b82a3ee82fa5fbdcf1bf7a937b7859647b73e3a4bf7d6248b5bddd84477e3cd0aa2b5d0f240d37ead5fad419e4b00c87baa6b846d426f23881cc877c65f496
7
+ data.tar.gz: 1a6f516d714bfb6ba41099796ce04e94d9090b6eaff9f47366551c00998cd73ff28eb4907bd3e85aa550e08123e7a0219b028951e3a6a649163ee407d07a97a6
data/README.md CHANGED
@@ -47,7 +47,7 @@ Use `yerba` as a library in your Rust project:
47
47
 
48
48
  ```toml
49
49
  [dependencies]
50
- yerba = "0.8"
50
+ yerba = "0.9"
51
51
  ```
52
52
 
53
53
  ```rust
@@ -169,34 +169,62 @@ Use `--condition` to only apply the change when a sibling field matches:
169
169
  yerba set config.yml "database.host" "0.0.0.0" --condition ".port == 5432"
170
170
  ```
171
171
 
172
+ Over a wildcard selector the condition picks out *which* items to edit, the same way it filters in
173
+ `get`. Items that do not match are left alone:
174
+
175
+ ```bash
176
+ yerba set videos.yml "[].title" "New Title" --condition ".id == talk-1"
177
+ ```
178
+
179
+ If the condition matches more than one item the command stops rather than guessing, so add `--all`
180
+ when editing several is what you meant:
181
+
182
+ ```bash
183
+ yerba set videos.yml "[].published" false --plain --condition ".kind == draft" --all
184
+ ```
185
+
172
186
  Use `--all` to update all nodes matching a wildcard selector:
173
187
 
174
188
  ```bash
175
189
  yerba set videos.yml "[].description" "" --all
176
190
  ```
177
191
 
192
+ Values are written in the quote style already at the path, which keeps string edits tidy. A value is quoted anyway when writing it plain would not read back as the string you passed, so `yes` becomes `"yes"` and `12345` becomes `"12345"` — whatever the field held before. Use `--plain` to write a YAML literal instead:
193
+
194
+ ```bash
195
+ yerba set config.yml "database.replica" null --plain
196
+ yerba set config.yml "database.ssl" true --plain
197
+ ```
198
+
178
199
  ### `insert`
179
200
 
180
201
  Insert a new key into a map or a new item into a sequence. By default, new items are appended at the end.
181
202
 
182
203
  ```bash
183
- yerba insert config.yml "database.ssl" true
204
+ yerba insert config.yml "database.ssl" true --plain
184
205
  yerba insert config.yml "tags" "yaml"
185
206
  ```
186
207
 
187
208
  Control placement with `--before`, `--after`, or `--at`:
188
209
 
189
210
  ```bash
190
- yerba insert config.yml "database.ssl" true --after "host"
191
- yerba insert config.yml "database.ssl" true --before "port"
211
+ yerba insert config.yml "database.ssl" true --plain --after "host"
212
+ yerba insert config.yml "database.ssl" true --plain --before "port"
192
213
  yerba insert config.yml "tags" "yaml" --at 0
193
214
  yerba insert config.yml "tags" "yaml" --after "ruby"
194
215
  ```
195
216
 
217
+ Values are written as strings, and quoted whenever writing them plain would change what they mean. `"Conf 2018: A Talk"` stays one string instead of becoming a nested map, and `"12345"` stays a string instead of becoming a number. Use `--plain` to write a YAML literal or a map or sequence fragment instead:
218
+
219
+ ```bash
220
+ yerba insert config.yml "database.ssl" true --plain
221
+ yerba insert speakers.yml "" "name: Bob" --plain --after ".name == Alice"
222
+ ```
223
+
196
224
  For sequences of maps, use conditions to position relative to other items:
197
225
 
198
226
  ```bash
199
- yerba insert speakers.yml "" "name: Bob" --after ".name == Alice"
227
+ yerba insert speakers.yml "" "name: Bob" --plain --after ".name == Alice"
200
228
  yerba insert videos.yml "[0].speakers" "Diana" --before ".name == Charlie"
201
229
  ```
202
230
 
@@ -324,17 +352,17 @@ yerba quote-style videos.yml "[].description" --values literal
324
352
 
325
353
  **Value styles** (`--values`):
326
354
 
327
- | Style | Symbol | Example | Behavior | |
328
- |----------------|--------|--------------------------|--------------------------------------|----------------------------|
329
- | `plain` | — | `host: localhost` | Unquoted | |
330
- | `single` | `'` | `host: 'localhost'` | Single-quoted | |
331
- | `double` | `"` | `host: "localhost"` | Double-quoted, supports `\n` escapes | |
332
- | `literal` | `\ | -` | Preserves newlines | Strip trailing newline |
333
- | `literal-clip` | `\ | ` | Preserves newlines | Keep one trailing newline |
334
- | `literal-keep` | `\ | +` | Preserves newlines | Keep all trailing newlines |
335
- | `folded` | `>-` | Folds newlines to spaces | Strip trailing newline | |
336
- | `folded-clip` | `>` | Folds newlines to spaces | Keep one trailing newline | |
337
- | `folded-keep` | `>+` | Folds newlines to spaces | Keep all trailing newlines | |
355
+ | Style | Symbol | Example | Behavior |
356
+ |----------------|--------|--------------------------|--------------------------------------|
357
+ | `plain` | — | `host: localhost` | Unquoted |
358
+ | `single` | `'` | `host: 'localhost'` | Single-quoted |
359
+ | `double` | `"` | `host: "localhost"` | Double-quoted, supports `\n` escapes |
360
+ | `literal` | `\|-` | Preserves newlines | Strip trailing newline |
361
+ | `literal-clip` | `\|` | Preserves newlines | Keep one trailing newline |
362
+ | `literal-keep` | `\|+` | Preserves newlines | Keep all trailing newlines |
363
+ | `folded` | `>-` | Folds newlines to spaces | Strip trailing newline |
364
+ | `folded-clip` | `>` | Folds newlines to spaces | Keep one trailing newline |
365
+ | `folded-keep` | `>+` | Folds newlines to spaces | Keep all trailing newlines |
338
366
 
339
367
  Block scalars are only converted when scoped to a specific selector. An unscoped `--values double` will not touch existing block scalars.
340
368
 
@@ -348,6 +376,52 @@ yerba blank-lines videos.yml "[]" 1
348
376
  yerba blank-lines config.yml "tags" 0
349
377
  ```
350
378
 
379
+ By default every entry after the first is adjusted. Use `--before` to set apart only certain sections of a map and leave their neighbours alone:
380
+
381
+ ```bash
382
+ yerba blank-lines event.yml "" 1 --before "schedule,speakers"
383
+ ```
384
+
385
+ ```yaml
386
+ id: rubyconf-2024
387
+ name: RubyConf 2024
388
+ city: Chicago
389
+
390
+ schedule:
391
+ - day: 1
392
+
393
+ speakers:
394
+ - name: Alice
395
+ ```
396
+
397
+ `--after` is the mirror image, separating a key from what follows it. Listing a key in both sets it apart on both sides:
398
+
399
+ ```bash
400
+ yerba blank-lines event.yml "" 1 --before "location" --after "location"
401
+ ```
402
+
403
+ Keys that are not listed keep whatever spacing they already have, so these flags add separators without reflowing the rest of the document. Sequence entries have no key and are never matched by a filter, which means `--before` and `--after` on a sequence are no-ops.
404
+
405
+ Use `--skip-empty` to leave placeholder entries tucked against their neighbours. An entry counts as empty when its value is `null`, an empty collection, or an empty string:
406
+
407
+ ```bash
408
+ yerba blank-lines event.yml "" 1 --before "location,speakers,sponsors" --after "location" --skip-empty
409
+ ```
410
+
411
+ ```yaml
412
+ id: rubyconf-2024
413
+ name: RubyConf 2024
414
+
415
+ location:
416
+ city: Chicago
417
+ country: US
418
+
419
+ speakers: []
420
+ sponsors: []
421
+ ```
422
+
423
+ The match is ignored when the entry that *triggered* it is empty, not the one on the other side of the gap. That is why `--after "location"` still separates a populated `location:` block from the empty entries below it, while a bare `location: null` would stay packed. Without `--skip-empty`, every listed key gets its blank line whether or not it holds anything.
424
+
351
425
  ### `directives`
352
426
 
353
427
  Add or remove the document start marker (`---`):
@@ -535,15 +609,16 @@ Available pipeline steps:
535
609
  - `sequence_indent` Enforce compact or indented sequence style
536
610
  - `sort_keys` Reorder keys to match a predefined list
537
611
  - `sort` Sort sequence items by field(s)
538
- - `blank_lines` Enforce blank lines between sequence entries
539
- - `set` Set a value (supports conditions)
540
- - `insert` Insert a new key or sequence item
612
+ - `blank_lines` Enforce blank lines between sequence entries, optionally limited to named map keys with `before` and to non-empty values with `skip_empty`
613
+ - `set` Set a value (supports conditions, and `plain: true` for YAML literals)
614
+ - `insert` Insert a new key or sequence item (`plain: true` for YAML literals and fragments)
541
615
  - `delete` Remove a key (supports conditions)
542
616
  - `rename` Rename a key
543
617
  - `remove` Remove an item from a sequence
544
618
  - `directives` Add or remove the document start marker (`---`), with optional `max` validation
545
619
  - `unique` Find or remove duplicate items in a sequence
546
620
  - `schema` Validate against a JSON schema (with optional `path` for scoping)
621
+ - `escapes` Spell escaped characters literally where the quote style can carry them (with optional `path` for scoping)
547
622
  - `final_newline` Enforce exact number of trailing newlines (`count: 1` by default, strips extras)
548
623
 
549
624
  This makes it easy to enforce project-wide YAML conventions in CI:
@@ -552,6 +627,36 @@ This makes it easy to enforce project-wide YAML conventions in CI:
552
627
  yerba check
553
628
  ```
554
629
 
630
+ The `escapes` step rewrites double-quoted values in the form yerba would write them, so a character the style can carry is spelled out rather than escaped. Text that arrives from an API or a scraper often carries `\U0001F631` where an emoji belongs, or a long value wrapped across two lines. Both read back correctly either way, but neither is pleasant to review in a diff:
631
+
632
+ ```yaml
633
+ pipeline:
634
+ - escapes: {}
635
+ ```
636
+
637
+ ```diff
638
+ - title: "Deploying on a Friday \U0001F631"
639
+ + title: "Deploying on a Friday 😱"
640
+
641
+ - summary: "Why the deploy went out on a Friday, and
642
+ - what we changed afterwards"
643
+ + summary: "Why the deploy went out on a Friday, and what we changed afterwards"
644
+ ```
645
+
646
+ Escapes that have to stay do: a control character, a quote or a backslash is still written escaped, since the value would not read back the same otherwise. Plain, single-quoted and block scalars are left alone, and a `path` scopes the step to part of the document.
647
+
648
+ Every checked file is also scanned for control characters, whatever the pipeline says. Yerba's parser accepts them but libyaml does not, so without this a file could pass `check` in CI and then fail to load in the application that reads it. Offending characters are reported with their line and column:
649
+
650
+ ```console
651
+ $ yerba check
652
+ ✗ data/rubyconf-taiwan/videos.yml
653
+ found 2 control characters that YAML parsers reject:
654
+ U+0008 at line 152, column 34
655
+ U+0008 at line 154, column 12
656
+ ```
657
+
658
+ Tabs, newlines and carriage returns are fine, and so are characters written escaped as `\xNN` inside a double-quoted value — only literal ones are reported.
659
+
555
660
  ## Ruby API
556
661
 
557
662
  Yerba includes a native C extension (backed by the same Rust core) that provides a full Ruby API for YAML editing.
@@ -718,6 +823,42 @@ Insert new keys with positional control:
718
823
  document["database"].insert("ssl", true, after: "host")
719
824
  ```
720
825
 
826
+ A `String` is written as a value, not as YAML text, and quoted whenever writing it plain would change what it means. This is the same rule the CLI's `insert` and `set` follow, so `"key: value"` stays one string instead of becoming a nested map:
827
+
828
+ ```ruby
829
+ document["notes"] = "key: value" # => notes: "key: value"
830
+ document["lines"] = "line\nbreak" # => lines: "line\nbreak"
831
+ document["item"] = "- a" # => item: "- a"
832
+ document["tags"] = "[a, b]" # => tags: "[a, b]"
833
+ ```
834
+
835
+ Other types are written as the YAML literal they stand for, so the value you read back is the value you wrote:
836
+
837
+ ```ruby
838
+ document["port"] = 5432 # => port: 5432
839
+ document["ssl"] = true # => ssl: true
840
+ document["replica"] = nil # => replica: null
841
+ ```
842
+
843
+ Both `set` and `insert` take `plain: true` when the value is YAML text you built yourself, to write it verbatim instead:
844
+
845
+ ```ruby
846
+ document.set("tags", "[a, b]", plain: true) # => tags: [a, b]
847
+ ```
848
+
849
+ ```ruby
850
+ document.insert("tags", "- ruby\n- rails", plain: true)
851
+ # tags:
852
+ # - ruby
853
+ # - rails
854
+ ```
855
+
856
+ Use `style:` to pick the quote style for the inserted value. A style that cannot carry the value falls back to double quotes rather than writing something that reads back differently:
857
+
858
+ ```ruby
859
+ document.insert("city", "Berlin", style: :double) # => city: "Berlin"
860
+ ```
861
+
721
862
  Set arrays and hashes as values, they default to block style:
722
863
 
723
864
  ```ruby
@@ -823,6 +964,16 @@ scalar.quote_style # => :double
823
964
  scalar.quote_style = :single
824
965
  ```
825
966
 
967
+ `Yerba.quote_scalar` writes a value as YAML text without needing a document, using the rule the insert APIs follow. Pass a style to ask for one, and it is honoured unless the value cannot survive it:
968
+
969
+ ```ruby
970
+ Yerba.quote_scalar("plain") # => "plain"
971
+ Yerba.quote_scalar("key: value") # => "\"key: value\""
972
+ Yerba.quote_scalar("12345") # => "\"12345\""
973
+ Yerba.quote_scalar("ok", :single) # => "'ok'"
974
+ Yerba.quote_scalar("a\nb", :single) # => "\"a\\nb\""
975
+ ```
976
+
826
977
  ### Collection Style
827
978
 
828
979
  Read and change how collections are rendered, flow (inline) or block (multi-line):
@@ -129,13 +129,18 @@ char *yerba_document_find(const struct Document *document,
129
129
  struct YerbaResult yerba_document_set(struct Document *document,
130
130
  const char *path,
131
131
  const char *value,
132
+ uintptr_t value_length,
132
133
  enum YerbaValueType value_type,
134
+ bool plain,
133
135
  bool all);
134
136
 
135
137
  struct YerbaResult yerba_document_insert(struct Document *document,
136
138
  const char *path,
137
139
  const char *value,
140
+ uintptr_t value_length,
138
141
  enum YerbaValueType value_type,
142
+ bool plain,
143
+ const char *style,
139
144
  const char *before,
140
145
  const char *after,
141
146
  int64_t at);
@@ -211,6 +216,8 @@ struct YerbaResult yerba_document_apply_yerbafile(struct Document *document,
211
216
 
212
217
  char *yerba_document_to_string(const struct Document *document);
213
218
 
219
+ char *yerba_quote_scalar(const char *value, uintptr_t length, const char *style);
220
+
214
221
  void yerba_string_free(char *s);
215
222
 
216
223
  void yerba_result_free(struct YerbaResult result);
data/ext/yerba/yerba.c CHANGED
@@ -414,6 +414,31 @@ static VALUE document_resolve_selectors(VALUE self, VALUE path) {
414
414
  return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
415
415
  }
416
416
 
417
+ /* Yerba.quote_scalar(value, style = nil) → YAML text for value, quoted when the style cannot carry it. */
418
+ static VALUE yerba_s_quote_scalar(int argc, VALUE *argv, VALUE self) {
419
+ VALUE value, style_value;
420
+ rb_scan_args(argc, argv, "11", &value, &style_value);
421
+
422
+ StringValue(value);
423
+
424
+ const char *style = NULL;
425
+
426
+ if (!NIL_P(style_value)) {
427
+ VALUE style_string = rb_obj_is_kind_of(style_value, rb_cString) ? style_value : rb_sym2str(style_value);
428
+ style = StringValueCStr(style_string);
429
+ }
430
+
431
+ char *quoted = yerba_quote_scalar(RSTRING_PTR(value), (size_t)RSTRING_LEN(value), style);
432
+
433
+ if (quoted == NULL) return value;
434
+
435
+ VALUE result = make_utf8_string(quoted);
436
+
437
+ yerba_string_free(quoted);
438
+
439
+ return result;
440
+ }
441
+
417
442
  /* document.get_quote_style(path) → :plain, :single, :double, :literal, etc. or nil */
418
443
  static VALUE document_get_quote_style(VALUE self, VALUE path) {
419
444
  struct Document *document = get_document(self);
@@ -574,6 +599,7 @@ static VALUE document_find(int argc, VALUE *argv, VALUE self) {
574
599
  The caller must provide a number_buffer of at least 64 bytes. */
575
600
  struct TypedValue {
576
601
  const char *text;
602
+ size_t length;
577
603
  YerbaValueType type;
578
604
  };
579
605
 
@@ -598,14 +624,21 @@ static struct TypedValue ruby_to_typed_value(VALUE value, char *number_buffer) {
598
624
  result.text = number_buffer;
599
625
  result.type = YERBA_VALUE_TYPE_FLOAT;
600
626
  } else {
601
- result.text = StringValueCStr(value);
627
+ StringValue(value);
628
+
629
+ result.text = RSTRING_PTR(value);
630
+ result.length = (size_t)RSTRING_LEN(value);
602
631
  result.type = YERBA_VALUE_TYPE_STRING;
632
+
633
+ return result;
603
634
  }
604
635
 
636
+ result.length = strlen(result.text);
637
+
605
638
  return result;
606
639
  }
607
640
 
608
- /* document.set(path, value, condition: nil, if_exists: false, if_missing: false) */
641
+ /* document.set(path, value, condition: nil, if_exists: false, if_missing: false, plain: false, all: false) */
609
642
  static VALUE document_set(int argc, VALUE *argv, VALUE self) {
610
643
  VALUE path, value, opts;
611
644
  rb_scan_args(argc, argv, "2:", &path, &value, &opts);
@@ -625,43 +658,58 @@ static VALUE document_set(int argc, VALUE *argv, VALUE self) {
625
658
  char number_buffer[64];
626
659
  struct TypedValue typed_value = ruby_to_typed_value(value, number_buffer);
627
660
  bool all = false;
661
+ bool plain = false;
628
662
 
629
663
  if (!NIL_P(opts)) {
630
664
  VALUE v_all = rb_hash_aref(opts, ID2SYM(rb_intern("all")));
665
+ VALUE v_plain = rb_hash_aref(opts, ID2SYM(rb_intern("plain")));
631
666
 
632
667
  if (RTEST(v_all)) all = true;
668
+ if (RTEST(v_plain)) plain = true;
633
669
  }
634
670
 
635
- YerbaResult result = yerba_document_set(document, StringValueCStr(path), typed_value.text, typed_value.type, all);
671
+ YerbaResult result = yerba_document_set(document, StringValueCStr(path), typed_value.text, typed_value.length, typed_value.type, plain, all);
636
672
  check_result(result);
637
673
 
638
674
  return self;
639
675
  }
640
676
 
641
- /* document.insert(path, value, before: nil, after: nil, at: nil) */
677
+ /* document.insert(path, value, before: nil, after: nil, at: nil, plain: false, style: nil) */
642
678
  static VALUE document_insert(int argc, VALUE *argv, VALUE self) {
643
679
  VALUE path, value, opts;
644
680
  rb_scan_args(argc, argv, "2:", &path, &value, &opts);
645
681
 
646
682
  const char *before = NULL;
647
683
  const char *after = NULL;
684
+ const char *style = NULL;
648
685
  long long at = -1;
686
+ bool plain = false;
649
687
 
650
688
  if (!NIL_P(opts)) {
651
689
  VALUE v_before = rb_hash_aref(opts, ID2SYM(rb_intern("before")));
652
690
  VALUE v_after = rb_hash_aref(opts, ID2SYM(rb_intern("after")));
653
691
  VALUE v_at = rb_hash_aref(opts, ID2SYM(rb_intern("at")));
692
+ VALUE v_plain = rb_hash_aref(opts, ID2SYM(rb_intern("plain")));
693
+ VALUE v_style = rb_hash_aref(opts, ID2SYM(rb_intern("style")));
654
694
 
655
695
  if (!NIL_P(v_before)) before = StringValueCStr(v_before);
656
696
  if (!NIL_P(v_after)) after = StringValueCStr(v_after);
657
697
  if (!NIL_P(v_at)) at = NUM2LL(v_at);
698
+ if (RTEST(v_plain)) plain = true;
699
+
700
+ if (!NIL_P(v_style)) {
701
+ VALUE style_string = rb_obj_is_kind_of(v_style, rb_cString) ? v_style : rb_sym2str(v_style);
702
+ style = StringValueCStr(style_string);
703
+ }
658
704
  }
659
705
 
660
706
  char number_buffer[64];
661
707
  struct TypedValue typed_value = ruby_to_typed_value(value, number_buffer);
662
708
 
663
709
  struct Document *document = get_document(self);
664
- YerbaResult result = yerba_document_insert(document, StringValueCStr(path), typed_value.text, typed_value.type, before, after, at);
710
+ YerbaResult result =
711
+ yerba_document_insert(document, StringValueCStr(path), typed_value.text, typed_value.length, typed_value.type, plain, style,
712
+ before, after, at);
665
713
  check_result(result);
666
714
 
667
715
  return self;
@@ -1122,6 +1170,7 @@ void Init_yerba(void) {
1122
1170
  rb_require("json");
1123
1171
 
1124
1172
  rb_mYerba = rb_define_module("Yerba");
1173
+ rb_define_module_function(rb_mYerba, "quote_scalar", yerba_s_quote_scalar, -1);
1125
1174
  rb_eError = rb_define_class_under(rb_mYerba, "Error", rb_eStandardError);
1126
1175
  rb_ePathNotFoundError = rb_define_class_under(rb_mYerba, "PathNotFoundError", rb_eError);
1127
1176
  rb_eParseError = rb_define_class_under(rb_mYerba, "ParseError", rb_eError);
@@ -3,16 +3,7 @@
3
3
  module Yerba
4
4
  module Formatting
5
5
  def self.quote(value, style)
6
- case style
7
- when :double
8
- escaped = value.to_s.gsub("\\", "\\\\").gsub('"', '\\"')
9
- "\"#{escaped}\""
10
- when :single
11
- escaped = value.to_s.gsub("'", "''")
12
- "'#{escaped}'"
13
- else
14
- value.to_s
15
- end
6
+ Yerba.quote_scalar(value.to_s, style)
16
7
  end
17
8
 
18
9
  def self.to_yaml_value(value)
@@ -55,9 +46,13 @@ module Yerba
55
46
  return "{}" if value.empty?
56
47
 
57
48
  value.map { |key, value|
58
- if value.is_a?(Hash) || value.is_a?(Array)
59
- inner = to_block_yaml_value(value, indent + 1)
60
- "#{prefix}#{key}:\n#{inner}"
49
+ case value
50
+ when Array, Hash
51
+ if value.empty?
52
+ "#{prefix}#{key}: #{to_yaml_value(value)}"
53
+ else
54
+ "#{prefix}#{key}:\n#{to_block_yaml_value(value, indent + 1)}"
55
+ end
61
56
  else
62
57
  "#{prefix}#{key}: #{to_scalar_value(value)}"
63
58
  end
@@ -72,6 +67,7 @@ module Yerba
72
67
  when true then "true"
73
68
  when false then "false"
74
69
  when nil then "null"
70
+ when String then Yerba.quote_scalar(value)
75
71
  else value.to_s
76
72
  end
77
73
  end
data/lib/yerba/map.rb CHANGED
@@ -32,11 +32,11 @@ module Yerba
32
32
  if connected?
33
33
  new_path = @selector.empty? ? key.to_s : "#{@selector}.#{key}"
34
34
  coerced = coerce_value(value, style: style)
35
- is_block_value = coerced.is_a?(String) && (coerced.include?("\n") || coerced.start_with?("- "))
35
+
36
+ is_collection = value.is_a?(Array) || value.is_a?(Hash)
37
+ is_block_value = is_collection && !coerced.start_with?("{", "[")
36
38
 
37
39
  if document.exists?(new_path) && is_block_value
38
- # Block-style collections can't replace a scalar via set().
39
- # Delete the key and re-insert at the same position.
40
40
  all_keys = keys
41
41
  key_index = all_keys.index(key.to_s)
42
42
  after_key = key_index&.positive? ? all_keys[key_index - 1] : nil
@@ -44,14 +44,14 @@ module Yerba
44
44
  document.delete(new_path)
45
45
 
46
46
  if after_key
47
- document.insert(new_path, coerced, after: after_key)
47
+ document.insert(new_path, coerced, after: after_key, plain: true)
48
48
  else
49
- document.insert(new_path, coerced)
49
+ document.insert(new_path, coerced, plain: true)
50
50
  end
51
51
  elsif document.exists?(new_path)
52
- document.set(new_path, coerced)
52
+ document.set(new_path, coerced, plain: is_collection)
53
53
  else
54
- document.insert(new_path, coerced)
54
+ document.insert(new_path, coerced, plain: is_collection)
55
55
  end
56
56
  else
57
57
  @data[key] = value
@@ -62,7 +62,10 @@ module Yerba
62
62
  if connected?
63
63
  new_path = @selector.empty? ? key.to_s : "#{@selector}.#{key}"
64
64
 
65
- document.insert(new_path, coerce_value(value, style: style), before: before, after: after)
65
+ coerced = coerce_value(value, style: style)
66
+ is_collection = value.is_a?(Array) || value.is_a?(Hash)
67
+
68
+ document.insert(new_path, coerced, before: before, after: after, plain: is_collection)
66
69
  else
67
70
  @data[key] = value
68
71
  end
@@ -60,10 +60,9 @@ module Yerba
60
60
  when Hash
61
61
  document.insert_object(@selector, item)
62
62
  when Scalar
63
- document.insert(@selector, item.to_yaml)
63
+ document.insert(@selector, item.to_yaml, plain: true)
64
64
  else
65
- formatted = format_for_insert(item.to_s)
66
- document.insert(@selector, formatted)
65
+ document.insert(@selector, item.to_s, style: detect_quote_style)
67
66
  end
68
67
  else
69
68
  @data << item
@@ -425,10 +424,6 @@ module Yerba
425
424
  current
426
425
  end
427
426
 
428
- def format_for_insert(value)
429
- Formatting.quote(value, detect_quote_style)
430
- end
431
-
432
427
  def detect_quote_style
433
428
  document.get_quote_style("#{@selector}[0]")
434
429
  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.8.1"
4
+ VERSION = "0.9.0"
5
5
  end
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "yerba"
3
- version = "0.8.1"
3
+ version = "0.9.0"
4
4
  edition = "2021"
5
5
  authors = ["Marco Roth <marco.roth@intergga.ch>"]
6
6
  description = "YAML Editing and Refactoring with Better Accuracy"
@@ -11,6 +11,8 @@ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
11
11
  yerba blank-lines videos.yml "[]" 1
12
12
  yerba blank-lines videos.yml "[].speakers" 1
13
13
  yerba blank-lines config.yml "tags" 0
14
+ yerba blank-lines event.yml "" 1 --before "schedule,speakers"
15
+ yerba blank-lines event.yml "" 1 --after "location" --skip-empty
14
16
  "#})
15
17
  });
16
18
 
@@ -26,6 +28,14 @@ pub struct Args {
26
28
  first: String,
27
29
  /// Count (when selector is provided as first positional)
28
30
  second: Option<usize>,
31
+ /// Comma-separated map keys to separate from what precedes them
32
+ #[arg(long)]
33
+ before: Option<String>,
34
+ /// Comma-separated map keys to separate from what follows them
35
+ #[arg(long)]
36
+ after: Option<String>,
37
+ #[arg(long)]
38
+ skip_empty: bool,
29
39
  #[arg(long)]
30
40
  dry_run: bool,
31
41
  }
@@ -44,12 +54,22 @@ impl Args {
44
54
  std::process::exit(1);
45
55
  };
46
56
 
57
+ let options = yerba::BlankLineOptions {
58
+ before: split_keys(self.before.as_deref()),
59
+ after: split_keys(self.after.as_deref()),
60
+ skip_empty: self.skip_empty,
61
+ };
62
+
47
63
  for resolved_file in resolve_files(&self.file) {
48
64
  let mut document = parse_file(&resolved_file);
49
65
 
50
- if document.enforce_blank_lines(selector, count).is_ok() {
66
+ if document.enforce_blank_lines_with(selector, count, &options).is_ok() {
51
67
  output(&resolved_file, &document, self.dry_run);
52
68
  }
53
69
  }
54
70
  }
55
71
  }
72
+
73
+ fn split_keys(keys: Option<&str>) -> Vec<String> {
74
+ keys.map(|keys| keys.split(',').map(|key| key.trim().to_string()).collect()).unwrap_or_default()
75
+ }
@@ -45,6 +45,12 @@ pub fn run() {
45
45
  # # Enforce blank lines between sequence entries
46
46
  # - blank_lines:
47
47
  # count: 1
48
+ # # skip_empty: true # leave null/empty entries packed
49
+ # # before: # separate these keys from what precedes them
50
+ # # - schedule
51
+ # # - speakers
52
+ # # after: # separate these keys from what follows them
53
+ # # - location
48
54
  #
49
55
  # # Set a value (with optional condition)
50
56
  # - set: