psych-pure 0.1.2 → 0.1.4

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: 0fb90297ca479adfe033c6d8db3544ec41c8e8083f23884e990de4ee2d8eb402
4
- data.tar.gz: 8f895282ff5733903665ddcd0d1be2a28696dce0380242f8804ad42ce45123fe
3
+ metadata.gz: f1767eb97091e21f83527221b9ef4f156d9db40998b9008422b3dc017ff4ada6
4
+ data.tar.gz: b7e1d3b11886a31fda7fd7aed9937e2c26eaafa6d5e242a65bb89c8a02b00be7
5
5
  SHA512:
6
- metadata.gz: 4ad4470231f6587e40da0fe271ef65a1ba441f70ecc87bd7900f4fcc17a2da4be63396b73d15a131359977b62e53cfbd9d333969dbd920e984c6647741bfab34
7
- data.tar.gz: 17965e31133a24c3f14c9d452884665f0d40e0c2e3edbf573fc59db5888471fb934caed61b9bed836287253061e549e540e4b79085360f9ec7a9a8082f4b7107
6
+ metadata.gz: 7103d8ceae77bf15b2f21f9462f65fe495bf6e90206749fded264d192beb1caee6ae1a0585703ce914d7268a1e57f498f4cc1b824c21b3c5fe95edfb1dc1e320
7
+ data.tar.gz: 570ba899c74d5873ba243771ae3ff71605b59f81b1567f5f0712314aa2f8efbbdfab659c4e9bde63609ce2ef5ccce46352624c0f458ef6a8ddd31826edcfa9da
data/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.4] - 2025-11-10
10
+
11
+ - Fix up comment handling preceding sequence elements.
12
+ - Properly update hashes in mutation methods that are loaded through `Psych::Pure.load`.
13
+ - Properly raise syntax errors when the parser does not finish.
14
+
15
+ ## [0.1.3] - 2025-10-24
16
+
17
+ - Fix up roundtripping when using `<<` inside mappings.
18
+ - Fix up roundtripping when using duplicate keys inside mappings.
19
+ - Fix up comment handling when using duplicate keys inside mappings.
20
+
9
21
  ## [0.1.2] - 2025-03-04
10
22
 
11
23
  - Fix up comment dumping to not drift around objects.
@@ -25,7 +37,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
25
37
 
26
38
  - 🎉 Initial release. 🎉
27
39
 
28
- [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.1.2...HEAD
40
+ [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.1.3...HEAD
41
+ [0.1.3]: https://github.com/kddnewton/psych-pure/compare/v0.1.2...v0.1.3
29
42
  [0.1.2]: https://github.com/kddnewton/psych-pure/compare/v0.1.1...v0.1.2
30
43
  [0.1.1]: https://github.com/kddnewton/psych-pure/compare/v0.1.0...v0.1.1
31
44
  [0.1.0]: https://github.com/kddnewton/psych-pure/compare/24de62...v0.1.0
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Psych
4
4
  module Pure
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.4"
6
6
  end
7
7
  end
data/lib/psych/pure.rb CHANGED
@@ -7,6 +7,14 @@ require "strscan"
7
7
  require "stringio"
8
8
 
9
9
  module Psych
10
+ module Nodes
11
+ class Scalar
12
+ # The source of the scalar, as it was found in the input. This may be set
13
+ # in order to be reused when dumping the object.
14
+ attr_accessor :source
15
+ end
16
+ end
17
+
10
18
  # A YAML parser written in Ruby.
11
19
  module Pure
12
20
  # An internal exception is an exception that should not have occurred. It is
@@ -17,7 +25,7 @@ module Psych
17
25
  end
18
26
  end
19
27
 
20
- # A source is wraps the input string and provides methods to access line and
28
+ # A source wraps the input string and provides methods to access line and
21
29
  # column information from a byte offset.
22
30
  class Source
23
31
  def initialize(string)
@@ -27,7 +35,13 @@ module Psych
27
35
  offset = 0
28
36
  string.each_line do |line|
29
37
  @line_offsets << offset
30
- @trimmable_lines << line.match?(/\A(?: *#.*)?\n\z/)
38
+ @trimmable_lines <<
39
+ case line
40
+ when /\A *#.*\n\z/ then :comment
41
+ when /\A *\n\z/ then :blank
42
+ else false
43
+ end
44
+
31
45
  offset += line.bytesize
32
46
  end
33
47
 
@@ -43,6 +57,14 @@ module Psych
43
57
  offset
44
58
  end
45
59
 
60
+ def trim_comments(offset)
61
+ while (l = line(offset)) != 0 && (offset == @line_offsets[l]) && @trimmable_lines[l - 1] == :comment
62
+ offset = @line_offsets[l - 1]
63
+ end
64
+
65
+ offset
66
+ end
67
+
46
68
  def line(offset)
47
69
  index = @line_offsets.bsearch_index { |line_offset| line_offset > offset }
48
70
  return @line_offsets.size - 1 if index.nil?
@@ -64,6 +86,10 @@ module Psych
64
86
  @pos_end = pos_end
65
87
  end
66
88
 
89
+ def range
90
+ @pos_start...@pos_end
91
+ end
92
+
67
93
  def start_line
68
94
  @source.line(@pos_start)
69
95
  end
@@ -89,6 +115,11 @@ module Psych
89
115
  Location.new(@source, @pos_start, @source.trim(@pos_end))
90
116
  end
91
117
 
118
+ # Trim trailing comments from this location.
119
+ def trim_comments
120
+ Location.new(@source, @pos_start, @source.trim_comments(@pos_end))
121
+ end
122
+
92
123
  def to_a
93
124
  [start_line, start_column, end_line, end_column]
94
125
  end
@@ -145,6 +176,20 @@ module Psych
145
176
  def trailing_comment(comment)
146
177
  @trailing << comment
147
178
  end
179
+
180
+ # Execute the given block without the leading comments being visible. This
181
+ # is used when a node has already handled its child nodes' leading
182
+ # comments, so they should not be processed again.
183
+ def without_leading
184
+ leading = @leading
185
+
186
+ begin
187
+ @leading = []
188
+ yield
189
+ ensure
190
+ @leading = leading
191
+ end
192
+ end
148
193
  end
149
194
 
150
195
  # Wraps a Ruby object with its node from the source input.
@@ -161,32 +206,274 @@ module Psych
161
206
  @psych_node = psych_node
162
207
  @dirty = dirty
163
208
  end
209
+
210
+ def replace(psych_node)
211
+ @psych_node = psych_node
212
+ @dirty = true
213
+ end
164
214
  end
165
215
 
166
216
  # Wraps a Ruby hash with its node from the source input.
167
217
  class LoadedHash < SimpleDelegator
218
+ class PsychKey
219
+ attr_reader :key_node, :value_node
220
+
221
+ def initialize(key_node, value_node)
222
+ @key_node = key_node
223
+ @value_node = value_node
224
+ end
225
+
226
+ def replace_key(key_node)
227
+ @key_node = key_node
228
+ end
229
+
230
+ def replace_value(value_node)
231
+ @value_node = value_node
232
+ end
233
+ end
234
+
168
235
  # The node associated with the hash.
169
236
  attr_reader :psych_node
170
237
 
171
- # The nodes associated with the keys of the hash.
172
- attr_reader :psych_node_keys
238
+ # The list of key/value pairs within the hash.
239
+ attr_reader :psych_keys
173
240
 
174
- def initialize(object, psych_node, psych_node_keys = {})
241
+ def initialize(object, psych_node)
175
242
  super(object)
176
243
  @psych_node = psych_node
177
- @psych_node_keys = psych_node_keys
244
+ @psych_keys = []
245
+ end
246
+
247
+ def set!(key_node, value_node)
248
+ @psych_keys << PsychKey.new(key_node, value_node)
249
+ __getobj__[key_node.__getobj__] = value_node
250
+ end
251
+
252
+ def join!(key_node, value_node)
253
+ @psych_keys << PsychKey.new(key_node, value_node)
254
+ merge!(value_node)
255
+ end
256
+
257
+ def psych_assocs
258
+ @psych_keys.map { |psych_key| [psych_key.key_node, psych_key.value_node] }
259
+ end
260
+
261
+ # Override Hash mutation methods to keep @psych_keys in sync.
262
+
263
+ def initialize_clone(obj, freeze: nil)
264
+ super
265
+ @psych_keys = obj.psych_keys.map(&:dup)
266
+ end
267
+
268
+ def initialize_dup(obj)
269
+ super
270
+ @psych_keys = obj.psych_keys.map(&:dup)
178
271
  end
179
272
 
180
273
  def []=(key, value)
181
- if (previous = self[key])
182
- if previous.is_a?(LoadedObject)
183
- value = LoadedObject.new(value, previous.psych_node, true)
184
- elsif previous.is_a?(LoadedHash)
185
- value = LoadedHash.new(value, previous.psych_node, previous.psych_node_keys)
274
+ super(key, value)
275
+
276
+ if (psych_key = @psych_keys.reverse_each.find { |psych_key| psych_compare?(psych_key.key_node, key) })
277
+ psych_key.replace_value(value)
278
+ else
279
+ @psych_keys << PsychKey.new(key, value)
280
+ end
281
+
282
+ value
283
+ end
284
+
285
+ alias store []=
286
+
287
+ def clear
288
+ super
289
+ @psych_keys.clear
290
+
291
+ self
292
+ end
293
+
294
+ def compact!
295
+ mutated = false
296
+ @psych_keys.each do |psych_key|
297
+ if psych_unwrap(psych_key.value_node).nil?
298
+ mutated = true
299
+ delete(psych_unwrap(psych_key.key_node))
186
300
  end
187
301
  end
188
302
 
189
- super(key, value)
303
+ self if mutated
304
+ end
305
+
306
+ def compact
307
+ dup.compact!
308
+ end
309
+
310
+ def delete(key)
311
+ result = super
312
+ psych_delete(key)
313
+
314
+ result
315
+ end
316
+
317
+ def delete_if(&block)
318
+ super do |key, value|
319
+ yield(key, psych_unwrap(value)).tap do |result|
320
+ psych_delete(key) if result
321
+ end
322
+ end
323
+
324
+ self
325
+ end
326
+
327
+ def except(*keys)
328
+ dup.delete_if { |key, _| keys.include?(key) }
329
+ end
330
+
331
+ def filter!(&block)
332
+ mutated = false
333
+ super do |key, value|
334
+ yield(key, psych_unwrap(value)).tap do |result|
335
+ unless result
336
+ psych_delete(key)
337
+ mutated = true
338
+ end
339
+ end
340
+ end
341
+
342
+ self if mutated
343
+ end
344
+
345
+ def filter(&block)
346
+ dup.filter!(&block)
347
+ end
348
+
349
+ def invert
350
+ result = LoadedHash.new({}, @psych_node)
351
+ each { |key, value| result[psych_unwrap(value)] = key }
352
+ result
353
+ end
354
+
355
+ def keep_if(&block)
356
+ super do |key, value|
357
+ yield(key, psych_unwrap(value)).tap do |result|
358
+ psych_delete(key) unless result
359
+ end
360
+ end
361
+
362
+ self
363
+ end
364
+
365
+ alias select! keep_if
366
+
367
+ def merge!(*others)
368
+ super
369
+ others.each do |other|
370
+ other.each do |key, value|
371
+ psych_delete(key)
372
+ @psych_keys << PsychKey.new(key, value)
373
+ end
374
+ end
375
+
376
+ self
377
+ end
378
+
379
+ alias update merge!
380
+
381
+ def merge(*others)
382
+ dup.merge!(*others)
383
+ end
384
+
385
+ def reject!(&block)
386
+ mutated = false
387
+ super do |key, value|
388
+ yield(key, psych_unwrap(value)).tap do |result|
389
+ if result
390
+ psych_delete(key)
391
+ mutated = true
392
+ end
393
+ end
394
+ end
395
+
396
+ self if mutated
397
+ end
398
+
399
+ def reject(&block)
400
+ dup.reject!(&block)
401
+ end
402
+
403
+ def replace(other)
404
+ super
405
+
406
+ @psych_keys.clear
407
+ other.each { |key, value| @psych_keys << PsychKey.new(key, value) }
408
+
409
+ self
410
+ end
411
+
412
+ def shift
413
+ unless empty?
414
+ key, value = super
415
+ psych_delete(key)
416
+
417
+ [key, value]
418
+ end
419
+ end
420
+
421
+ def slice(*keys)
422
+ dup.select! { |key, _| keys.include?(key) }
423
+ end
424
+
425
+ def transform_keys!(&block)
426
+ super do |key|
427
+ yield(key).tap do |result|
428
+ @psych_keys
429
+ .reverse_each
430
+ .find { |psych_key| psych_compare?(psych_key.key_node, key) }
431
+ &.replace_key(result)
432
+ end
433
+ end
434
+
435
+ self
436
+ end
437
+
438
+ def transform_keys(&block)
439
+ dup.transform_keys!(&block)
440
+ end
441
+
442
+ def transform_values!(&block)
443
+ super do |value|
444
+ yield(psych_unwrap(value)).tap do |result|
445
+ @psych_keys
446
+ .reverse_each
447
+ .find { |psych_key| psych_compare?(psych_key.value_node, value) }
448
+ &.replace_value(result)
449
+ end
450
+ end
451
+ end
452
+
453
+ def transform_values(&block)
454
+ dup.transform_values!(&block)
455
+ end
456
+
457
+ private
458
+
459
+ def psych_compare?(psych_node, value)
460
+ if compare_by_identity?
461
+ psych_unwrap(psych_node).equal?(value)
462
+ else
463
+ psych_unwrap(psych_node).eql?(value)
464
+ end
465
+ end
466
+
467
+ def psych_delete(key)
468
+ @psych_keys.reject! { |psych_key| psych_compare?(psych_key.key_node, key) }
469
+ end
470
+
471
+ def psych_unwrap(node)
472
+ if node.is_a?(LoadedHash) || node.is_a?(LoadedObject)
473
+ node.__getobj__
474
+ else
475
+ node
476
+ end
190
477
  end
191
478
  end
192
479
 
@@ -349,8 +636,6 @@ module Psych
349
636
  def initialize(ss, class_loader, symbolize_names: false, freeze: false, comments: false)
350
637
  super(ss, class_loader, symbolize_names: symbolize_names, freeze: freeze)
351
638
  @comments = comments
352
- @nodeless_objs = {}.compare_by_identity
353
- @nodeless_keys = {}.compare_by_identity
354
639
  end
355
640
 
356
641
  def accept(node)
@@ -360,31 +645,66 @@ module Psych
360
645
  case result
361
646
  when LoadedObject, LoadedHash
362
647
  # skip
363
- when Hash
364
- if !@nodeless_objs.key?(result)
365
- nodeless_obj = {}
366
- nodeless_key = {}
367
-
368
- result.each do |key, value|
369
- if key.is_a?(LoadedObject) || key.is_a?(LoadedHash)
370
- nodeless_obj[key.__getobj__] = value
371
- nodeless_key[key.__getobj__] = key
372
- else
373
- nodeless_obj[key] = value
648
+ else
649
+ result = LoadedObject.new(result, node)
650
+ end
651
+ end
652
+
653
+ result
654
+ end
655
+
656
+ private
657
+
658
+ def revive_hash(hash, node, tagged = false)
659
+ return super unless @comments
660
+
661
+ revived = LoadedHash.new(hash, node)
662
+ node.children.each_slice(2) do |key_node, value_node|
663
+ key = accept(key_node)
664
+ value = accept(value_node)
665
+
666
+ if key == "<<" && key_node.tag != "tag:yaml.org,2002:str"
667
+ case value_node
668
+ when Nodes::Alias, Nodes::Mapping
669
+ begin
670
+ # h1:
671
+ # <<: *h2
672
+ # <<: { k: v }
673
+ revived.join!(key, value)
674
+ rescue TypeError
675
+ # a: &a [1, 2, 3]
676
+ # h: { <<: *a }
677
+ revived.set!(key, value)
678
+ end
679
+ when Nodes::Sequence
680
+ # h1:
681
+ # <<: [*h2, *h3]
682
+ begin
683
+ temporary = {}
684
+ value.reverse_each { |value| temporary.merge!(value) }
685
+ rescue TypeError
686
+ revived.set!(key, value)
687
+ else
688
+ value_node.children.zip(value).reverse_each do |(child_value_node, child_value)|
689
+ revived.join!(key, child_value)
374
690
  end
375
691
  end
376
-
377
- @nodeless_objs[result] = nodeless_obj
378
- @nodeless_keys[result] = nodeless_key
692
+ else
693
+ # k: v
694
+ revived.set!(key, value)
379
695
  end
380
-
381
- result = LoadedHash.new(@nodeless_objs.fetch(result), node, @nodeless_keys.fetch(result))
382
696
  else
383
- result = LoadedObject.new(result, node)
697
+ if !tagged && @symbolize_names && key.is_a?(String)
698
+ key = key.to_sym
699
+ elsif !@freeze
700
+ key = deduplicate(key)
701
+ end
702
+
703
+ revived.set!(key, value)
384
704
  end
385
705
  end
386
706
 
387
- result
707
+ revived
388
708
  end
389
709
  end
390
710
 
@@ -493,11 +813,12 @@ module Psych
493
813
  # A scalar event represents a single value in the YAML document. It can be
494
814
  # many different types.
495
815
  class Scalar
496
- attr_reader :location, :value, :style
816
+ attr_reader :location, :source, :value, :style
497
817
  attr_accessor :anchor, :tag
498
818
 
499
- def initialize(location, value, style)
819
+ def initialize(location, source, value, style)
500
820
  @location = location
821
+ @source = source
501
822
  @value = value
502
823
  @anchor = nil
503
824
  @tag = nil
@@ -506,14 +827,19 @@ module Psych
506
827
 
507
828
  def accept(handler)
508
829
  handler.event_location(*@location.trim)
509
- handler.scalar(
510
- @value,
511
- @anchor,
512
- @tag,
513
- (!@tag || @tag == "!") && (@style == Nodes::Scalar::PLAIN),
514
- (!@tag || @tag == "!") && (@style != Nodes::Scalar::PLAIN),
515
- @style
516
- )
830
+
831
+ event =
832
+ handler.scalar(
833
+ @value,
834
+ @anchor,
835
+ @tag,
836
+ (!@tag || @tag == "!") && (@style == Nodes::Scalar::PLAIN),
837
+ (!@tag || @tag == "!") && (@style != Nodes::Scalar::PLAIN),
838
+ @style
839
+ )
840
+
841
+ event.source = source if event.is_a?(Nodes::Scalar)
842
+ event
517
843
  end
518
844
  end
519
845
 
@@ -650,9 +976,7 @@ module Psych
650
976
  @source = Source.new(yaml)
651
977
  @comments = {} if comments
652
978
 
653
- raise_syntax_error("Parser failed to complete") unless parse_l_yaml_stream
654
- raise_syntax_error("Parser finished before end of input") unless @scanner.eos?
655
-
979
+ parse_l_yaml_stream
656
980
  @comments = nil if comments
657
981
  true
658
982
  end
@@ -1149,11 +1473,8 @@ module Psych
1149
1473
  # b-comment
1150
1474
  def parse_s_b_comment
1151
1475
  try do
1152
- try do
1153
- if parse_s_separate_in_line
1154
- parse_c_nb_comment_text(true)
1155
- true
1156
- end
1476
+ if parse_s_separate_in_line
1477
+ parse_c_nb_comment_text(true)
1157
1478
  end
1158
1479
 
1159
1480
  parse_b_comment
@@ -1453,7 +1774,7 @@ module Psych
1453
1774
  # e-scalar ::=
1454
1775
  # <empty>
1455
1776
  def parse_e_scalar
1456
- events_push_flush_properties(Scalar.new(Location.point(@source, @scanner.pos), "", Nodes::Scalar::PLAIN))
1777
+ events_push_flush_properties(Scalar.new(Location.point(@source, @scanner.pos), "", "", Nodes::Scalar::PLAIN))
1457
1778
  true
1458
1779
  end
1459
1780
 
@@ -1557,7 +1878,7 @@ module Psych
1557
1878
  end
1558
1879
  end
1559
1880
 
1560
- events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), value, Nodes::Scalar::DOUBLE_QUOTED))
1881
+ events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), from(pos_start), value, Nodes::Scalar::DOUBLE_QUOTED))
1561
1882
  true
1562
1883
  end
1563
1884
  end
@@ -1701,7 +2022,7 @@ module Psych
1701
2022
  value.gsub!(/(?:[\ \t]*\r?\n[\ \t]*)/, "\n")
1702
2023
  value.gsub!(/\n(\n*)/) { $1.empty? ? " " : $1 }
1703
2024
  value.gsub!("''", "'")
1704
- events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), value, Nodes::Scalar::SINGLE_QUOTED))
2025
+ events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), from(pos_start), value, Nodes::Scalar::SINGLE_QUOTED))
1705
2026
  true
1706
2027
  end
1707
2028
  end
@@ -1952,12 +2273,9 @@ module Psych
1952
2273
  if parse_ns_flow_seq_entry(n, c)
1953
2274
  parse_s_separate(n, c)
1954
2275
 
1955
- try do
1956
- if match(",")
1957
- parse_s_separate(n, c)
1958
- parse_ns_s_flow_seq_entries(n, c)
1959
- true
1960
- end
2276
+ if match(",")
2277
+ parse_s_separate(n, c)
2278
+ parse_ns_s_flow_seq_entries(n, c)
1961
2279
  end
1962
2280
 
1963
2281
  true
@@ -2241,10 +2559,13 @@ module Psych
2241
2559
  end
2242
2560
 
2243
2561
  if result
2244
- value = from(pos_start)
2562
+ source = from(pos_start)
2563
+
2564
+ value = source.dup
2245
2565
  value.gsub!(/(?:[\ \t]*\r?\n[\ \t]*)/, "\n")
2246
2566
  value.gsub!(/\n(\n*)/) { $1.empty? ? " " : $1 }
2247
- events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), value, Nodes::Scalar::PLAIN))
2567
+
2568
+ events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), source, value, Nodes::Scalar::PLAIN))
2248
2569
  end
2249
2570
 
2250
2571
  result
@@ -2470,8 +2791,7 @@ module Psych
2470
2791
  parse_l_literal_content(n + m, t)
2471
2792
  } then
2472
2793
  @in_scalar = false
2473
- lines = events_cache_pop
2474
- value = lines.map { |line| "#{line}\n" }.join
2794
+ value = events_cache_pop.map { |line| "#{line}\n" }.join
2475
2795
 
2476
2796
  case t
2477
2797
  when :clip
@@ -2484,7 +2804,8 @@ module Psych
2484
2804
  raise InternalException, t.inspect
2485
2805
  end
2486
2806
 
2487
- events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), value, Nodes::Scalar::LITERAL))
2807
+ location = Location.new(@source, pos_start, @scanner.pos).trim_comments
2808
+ events_push_flush_properties(Scalar.new(location, @scanner.string.byteslice(location.range).chomp, value, Nodes::Scalar::LITERAL))
2488
2809
  true
2489
2810
  else
2490
2811
  @in_scalar = false
@@ -2582,7 +2903,8 @@ module Psych
2582
2903
  raise InternalException, t.inspect
2583
2904
  end
2584
2905
 
2585
- events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), value, Nodes::Scalar::FOLDED))
2906
+ location = Location.new(@source, pos_start, @scanner.pos).trim_comments
2907
+ events_push_flush_properties(Scalar.new(location, @scanner.string.byteslice(location.range).chomp, value, Nodes::Scalar::FOLDED))
2586
2908
  true
2587
2909
  else
2588
2910
  @in_scalar = false
@@ -3079,34 +3401,32 @@ module Psych
3079
3401
  def parse_l_yaml_stream
3080
3402
  events_push_flush_properties(StreamStart.new(Location.point(@source, @scanner.pos)))
3081
3403
 
3082
- if try {
3083
- if parse_l_document_prefix
3084
- @document_start_event = DocumentStart.new(Location.point(@source, @scanner.pos))
3085
- @tag_directives = @document_start_event.tag_directives
3086
- @document_end_event = nil
3404
+ star { parse_l_document_prefix }
3405
+ @document_start_event = DocumentStart.new(Location.point(@source, @scanner.pos))
3406
+ @tag_directives = @document_start_event.tag_directives
3407
+ @document_end_event = nil
3408
+ parse_l_any_document
3087
3409
 
3088
- parse_l_any_document
3089
- star do
3090
- try do
3091
- if parse_l_document_suffix
3092
- star { parse_l_document_prefix }
3093
- parse_l_any_document
3094
- true
3095
- end
3096
- end ||
3097
- try do
3098
- if parse_l_document_prefix
3099
- parse_l_explicit_document
3100
- true
3101
- end
3102
- end
3410
+ star do
3411
+ try do
3412
+ if parse_l_document_suffix
3413
+ star { parse_l_document_prefix }
3414
+ parse_l_any_document
3415
+ true
3416
+ end
3417
+ end ||
3418
+ try do
3419
+ if parse_l_document_prefix
3420
+ parse_l_explicit_document
3421
+ true
3103
3422
  end
3104
3423
  end
3105
- } then
3106
- document_end_event_flush
3107
- events_push_flush_properties(StreamEnd.new(Location.point(@source, @scanner.pos)))
3108
- true
3109
3424
  end
3425
+
3426
+ raise_syntax_error("Parser finished before end of input") unless @scanner.eos?
3427
+ document_end_event_flush
3428
+ events_push_flush_properties(StreamEnd.new(Location.point(@source, @scanner.pos)))
3429
+ true
3110
3430
  end
3111
3431
 
3112
3432
  # ------------------------------------------------------------------------
@@ -3182,6 +3502,9 @@ module Psych
3182
3502
 
3183
3503
  # Represents the nil value.
3184
3504
  class NilNode < Node
3505
+ def accept(visitor)
3506
+ raise "Visiting NilNode is not supported"
3507
+ end
3185
3508
  end
3186
3509
 
3187
3510
  # Represents a generic object that is not matched by any of the other node
@@ -3220,8 +3543,14 @@ module Psych
3220
3543
 
3221
3544
  # Represents a string object.
3222
3545
  class StringNode < Node
3546
+ # The explicit tag associated with the object.
3223
3547
  attr_accessor :tag
3224
3548
 
3549
+ # Whether or not this object was modified after being loaded. In this
3550
+ # case we cannot rely on the source formatting, and need to instead
3551
+ # format the value ourselves.
3552
+ attr_accessor :dirty
3553
+
3225
3554
  def accept(visitor)
3226
3555
  visitor.visit_string(self)
3227
3556
  end
@@ -3264,13 +3593,17 @@ module Psych
3264
3593
  # Visit an ObjectNode.
3265
3594
  def visit_object(node)
3266
3595
  with_comments(node) do |value|
3267
- if (tag = node.tag)
3268
- @q.text("#{tag} ")
3269
- end
3270
-
3271
3596
  if !node.dirty && (psych_node = node.psych_node)
3272
- @q.text(psych_node.value)
3597
+ if (tag = node.tag)
3598
+ @q.text("#{tag} ")
3599
+ end
3600
+
3601
+ @q.text(psych_node.source || psych_node.value)
3273
3602
  else
3603
+ if (tag = node.tag) && tag != "tag:yaml.org,2002:binary"
3604
+ @q.text("#{tag} ")
3605
+ end
3606
+
3274
3607
  @q.text(dump_object(value))
3275
3608
  end
3276
3609
  end
@@ -3293,11 +3626,19 @@ module Psych
3293
3626
  # Visit a StringNode.
3294
3627
  def visit_string(node)
3295
3628
  with_comments(node) do |value|
3296
- if (tag = node.tag)
3297
- @q.text("#{tag} ")
3298
- end
3629
+ if !node.dirty && (psych_node = node.psych_node)
3630
+ if (tag = node.tag)
3631
+ @q.text("#{tag} ")
3632
+ end
3299
3633
 
3300
- @q.text(dump_object(value))
3634
+ @q.text(psych_node.source || psych_node.value)
3635
+ else
3636
+ if (tag = node.tag) && tag != "tag:yaml.org,2002:binary"
3637
+ @q.text("#{tag} ")
3638
+ end
3639
+
3640
+ @q.text(dump_object(value))
3641
+ end
3301
3642
  end
3302
3643
  end
3303
3644
 
@@ -3334,12 +3675,38 @@ module Psych
3334
3675
  @q.breakable
3335
3676
  end
3336
3677
 
3337
- @q.seplist(contents, -> { @q.breakable }) do |element|
3678
+ current_line = nil
3679
+ contents.each_with_index do |element, index|
3680
+ psych_node = element.psych_node
3681
+ leading = psych_node&.comments&.leading
3682
+
3683
+ if index > 0
3684
+ @q.breakable
3685
+
3686
+ if current_line && psych_node
3687
+ start_line = (leading&.first || psych_node).start_line
3688
+ @q.breakable if start_line - current_line >= 2
3689
+ end
3690
+ end
3691
+
3692
+ current_line = psych_node&.end_line
3693
+ visit_leading_comments(leading) if leading&.any?
3694
+
3695
+ if psych_node && (trailing = psych_node.comments.trailing).any?
3696
+ current_line = trailing.last.end_line
3697
+ end
3698
+
3338
3699
  @q.text("-")
3339
3700
  next if element.is_a?(NilNode)
3340
3701
 
3341
3702
  @q.text(" ")
3342
- @q.nest(2) { visit(element) }
3703
+ @q.nest(2) do
3704
+ if psych_node
3705
+ psych_node.comments.without_leading { visit(element) }
3706
+ else
3707
+ visit(element)
3708
+ end
3709
+ end
3343
3710
  end
3344
3711
 
3345
3712
  @q.current_group.break
@@ -3447,9 +3814,26 @@ module Psych
3447
3814
  @q.breakable
3448
3815
  end
3449
3816
 
3817
+ current_line = nil
3450
3818
  ((0...children.length) % 2).each do |index|
3451
- @q.breakable if index != 0
3452
- visit_hash_key_value(children[index], children[index + 1])
3819
+ key = children[index]
3820
+ value = children[index + 1]
3821
+
3822
+ if index > 0
3823
+ @q.breakable
3824
+
3825
+ if current_line && (psych_node = key.psych_node)
3826
+ start_line = psych_node.start_line
3827
+ if (leading = key.psych_node.comments.leading).any?
3828
+ start_line = leading.first.start_line
3829
+ end
3830
+
3831
+ @q.breakable if start_line - current_line >= 2
3832
+ end
3833
+ end
3834
+
3835
+ current_line = (psych_node = value.psych_node) ? psych_node.end_line : nil
3836
+ visit_hash_key_value(key, value)
3453
3837
  end
3454
3838
 
3455
3839
  @q.current_group.break
@@ -3478,23 +3862,29 @@ module Psych
3478
3862
  end
3479
3863
  end
3480
3864
 
3865
+ # Visit the leading comments for a node, printing them out with proper
3866
+ # line breaks.
3867
+ def visit_leading_comments(comments)
3868
+ line = nil
3869
+
3870
+ comments.each do |comment|
3871
+ while line && line < comment.start_line
3872
+ @q.breakable
3873
+ line += 1
3874
+ end
3875
+
3876
+ @q.text(comment.value)
3877
+ line = comment.end_line
3878
+ end
3879
+
3880
+ @q.breakable
3881
+ end
3882
+
3481
3883
  # Print out the leading and trailing comments of a node, as well as
3482
3884
  # yielding the value of the node to the block.
3483
3885
  def with_comments(node)
3484
3886
  if (comments = node.psych_node&.comments) && (leading = comments.leading).any?
3485
- line = nil
3486
-
3487
- leading.each do |comment|
3488
- while line && line < comment.start_line
3489
- @q.breakable
3490
- line += 1
3491
- end
3492
-
3493
- @q.text(comment.value)
3494
- line = comment.end_line
3495
- end
3496
-
3497
- @q.breakable
3887
+ visit_leading_comments(leading)
3498
3888
  end
3499
3889
 
3500
3890
  yield node.value
@@ -3576,6 +3966,7 @@ module Psych
3576
3966
  # Very rare circumstance here that there are leading comments attached
3577
3967
  # to the root object of a document that occur before the --- marker. In
3578
3968
  # this case we want to output them first here, then dump the object.
3969
+ reload_comments = nil
3579
3970
  if (object.is_a?(LoadedObject) || object.is_a?(LoadedHash)) && (psych_node = object.psych_node).comments? && (leading = psych_node.comments.leading).any?
3580
3971
  leading = [*leading]
3581
3972
  line = psych_node.start_line - 1
@@ -3598,6 +3989,8 @@ module Psych
3598
3989
 
3599
3990
  line = comment.start_line
3600
3991
  end
3992
+
3993
+ reload_comments = leading.concat(psych_node.comments.leading)
3601
3994
  end
3602
3995
 
3603
3996
  @io << "---"
@@ -3620,6 +4013,12 @@ module Psych
3620
4013
 
3621
4014
  @io << q.output
3622
4015
  end
4016
+
4017
+ # If we initially split up the leading comments, then we need to reload
4018
+ # them back to their original state here.
4019
+ unless reload_comments.nil?
4020
+ object.psych_node.comments.leading.replace(reload_comments)
4021
+ end
3623
4022
  end
3624
4023
 
3625
4024
  private
@@ -3627,8 +4026,6 @@ module Psych
3627
4026
  # Dump the tag value for a given node.
3628
4027
  def dump_tag(value)
3629
4028
  case value
3630
- when nil, "tag:yaml.org,2002:binary"
3631
- nil
3632
4029
  when /\Atag:yaml.org,2002:(.+)\z/
3633
4030
  "!!#{$1}"
3634
4031
  else
@@ -3655,7 +4052,14 @@ module Psych
3655
4052
  end
3656
4053
 
3657
4054
  if @object_nodes.key?(object)
3658
- AliasNode.new(@object_nodes[object].anchor = (@object_anchors[object] ||= (@object_anchor += 1)), nil)
4055
+ @object_anchors[object] ||=
4056
+ if psych_node.is_a?(Nodes::Alias)
4057
+ psych_node.anchor
4058
+ else
4059
+ @object_anchor += 1
4060
+ end
4061
+
4062
+ AliasNode.new(@object_nodes[object].anchor = @object_anchors[object], psych_node)
3659
4063
  else
3660
4064
  case object
3661
4065
  when Psych::Omap
@@ -3670,8 +4074,7 @@ module Psych
3670
4074
  when Hash
3671
4075
  contents =
3672
4076
  if base_object.is_a?(LoadedHash)
3673
- psych_node_keys = base_object.psych_node_keys
3674
- object.flat_map { |key, value| [dump(psych_node_keys.fetch(key, key)), dump(value)] }
4077
+ base_object.psych_assocs.flat_map { |(key, value)| [dump(key), dump(value)] }
3675
4078
  else
3676
4079
  object.flat_map { |key, value| [dump(key), dump(value)] }
3677
4080
  end
@@ -3683,13 +4086,12 @@ module Psych
3683
4086
  when String
3684
4087
  dumped = StringNode.new(object, psych_node)
3685
4088
  dumped.tag = dump_tag(psych_node&.tag)
3686
-
4089
+ dumped.dirty = dirty
3687
4090
  dumped
3688
4091
  else
3689
4092
  dumped = ObjectNode.new(object, psych_node)
3690
4093
  dumped.tag = dump_tag(psych_node&.tag)
3691
4094
  dumped.dirty = dirty
3692
-
3693
4095
  dumped
3694
4096
  end
3695
4097
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: psych-pure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Newton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-04 00:00:00.000000000 Z
11
+ date: 2025-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: psych
@@ -113,7 +113,7 @@ licenses:
113
113
  - MIT
114
114
  metadata:
115
115
  bug_tracker_uri: https://github.com/kddnewton/psych-pure/issues
116
- changelog_uri: https://github.com/kddnewton/psych-pure/blob/v0.1.2/CHANGELOG.md
116
+ changelog_uri: https://github.com/kddnewton/psych-pure/blob/v0.1.4/CHANGELOG.md
117
117
  source_code_uri: https://github.com/kddnewton/psych-pure
118
118
  rubygems_mfa_required: 'true'
119
119
  post_install_message: