psych-pure 0.1.1 → 0.1.2

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: d126f1c02cd1f7eba6b74a19f76dc938a6c828dc8dfea96a5663b3b4c8ac5bac
4
- data.tar.gz: 6df58931a63090a6248a44d94986057fa60faa5c6ec2015b832da7a4d1fefe7a
3
+ metadata.gz: 0fb90297ca479adfe033c6d8db3544ec41c8e8083f23884e990de4ee2d8eb402
4
+ data.tar.gz: 8f895282ff5733903665ddcd0d1be2a28696dce0380242f8804ad42ce45123fe
5
5
  SHA512:
6
- metadata.gz: 00f44a5e4889ac4674d844f5dbfbff8a39fb1126f40b7135a33165539a3e6d27d5d5303b0289259471ba3d8c3dfd7e58e75f2fa8c5339258239a89b58e314c2d
7
- data.tar.gz: feaacc73c47c767edf1f454de1ecf4db4c812557f10578d88f715508b1975c524edc9acf5c21b40f77c40e1fc9eb25e7e79ee6496d8322cfb49e0030d457be3c
6
+ metadata.gz: 4ad4470231f6587e40da0fe271ef65a1ba441f70ecc87bd7900f4fcc17a2da4be63396b73d15a131359977b62e53cfbd9d333969dbd920e984c6647741bfab34
7
+ data.tar.gz: 17965e31133a24c3f14c9d452884665f0d40e0c2e3edbf573fc59db5888471fb934caed61b9bed836287253061e549e540e4b79085360f9ec7a9a8082f4b7107
data/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ 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.2] - 2025-03-04
10
+
11
+ - Fix up comment dumping to not drift around objects.
12
+ - Rely on source formatting when possible for scalar values.
13
+ - Fix up multi-line literals with the "keep" flag (|+).
14
+ - Fix up aliasing hashes that have comments loaded.
15
+
9
16
  ## [0.1.1] - 2025-02-13
10
17
 
11
18
  - Fix up comment handling to preserve within hashes.
@@ -18,6 +25,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
18
25
 
19
26
  - 🎉 Initial release. 🎉
20
27
 
21
- [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.1.1...HEAD
28
+ [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.1.2...HEAD
29
+ [0.1.2]: https://github.com/kddnewton/psych-pure/compare/v0.1.1...v0.1.2
22
30
  [0.1.1]: https://github.com/kddnewton/psych-pure/compare/v0.1.0...v0.1.1
23
31
  [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.1"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
data/lib/psych/pure.rb CHANGED
@@ -147,43 +147,42 @@ module Psych
147
147
  end
148
148
  end
149
149
 
150
- # Wraps a Ruby object with its comments from the source input.
151
- class CommentsObject < SimpleDelegator
152
- attr_reader :psych_comments
150
+ # Wraps a Ruby object with its node from the source input.
151
+ class LoadedObject < SimpleDelegator
152
+ # The node associated with the object.
153
+ attr_reader :psych_node
153
154
 
154
- def initialize(object, psych_comments)
155
- @psych_comments = psych_comments
155
+ # Whether or not this object has been modified. If it has, then we cannot
156
+ # rely on the source formatting, and need to format it ourselves.
157
+ attr_reader :dirty
158
+
159
+ def initialize(object, psych_node, dirty = false)
156
160
  super(object)
161
+ @psych_node = psych_node
162
+ @dirty = dirty
157
163
  end
158
164
  end
159
165
 
160
- # Wraps a Ruby hash with its comments from the source input.
161
- class CommentsHash < SimpleDelegator
162
- attr_reader :psych_comments, :psych_key_comments
163
-
164
- def initialize(object, psych_comments, psych_key_comments = {})
165
- @psych_comments = psych_comments
166
- @psych_key_comments = psych_key_comments
167
- commentless = {}
166
+ # Wraps a Ruby hash with its node from the source input.
167
+ class LoadedHash < SimpleDelegator
168
+ # The node associated with the hash.
169
+ attr_reader :psych_node
168
170
 
169
- object.each do |key, value|
170
- if key.is_a?(CommentsObject)
171
- @psych_key_comments[key.__getobj__] = key.psych_comments
172
- commentless[key.__getobj__] = value
173
- else
174
- commentless[key] = value
175
- end
176
- end
171
+ # The nodes associated with the keys of the hash.
172
+ attr_reader :psych_node_keys
177
173
 
178
- super(commentless)
174
+ def initialize(object, psych_node, psych_node_keys = {})
175
+ super(object)
176
+ @psych_node = psych_node
177
+ @psych_node_keys = psych_node_keys
179
178
  end
180
179
 
181
180
  def []=(key, value)
182
181
  if (previous = self[key])
183
- if previous.is_a?(CommentsObject)
184
- value = CommentsObject.new(value, previous.psych_comments)
185
- elsif previous.is_a?(CommentsHash)
186
- value = CommentsHash.new(value, previous.psych_comments, previous.psych_key_comments)
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)
187
186
  end
188
187
  end
189
188
 
@@ -304,9 +303,18 @@ module Psych
304
303
  # Extend the document stream to be able to attach comments to the
305
304
  # document.
306
305
  module DocumentStream
306
+ def start_document(version, tag_directives, implicit)
307
+ node = Nodes::Document.new(version, tag_directives, implicit)
308
+ set_start_location(node)
309
+ push(node)
310
+ end
311
+
307
312
  def end_document(implicit_end = !streaming?)
308
313
  @last.implicit_end = implicit_end
309
- @block.call(attach_comments(pop))
314
+ node = pop
315
+ set_end_location(node)
316
+ attach_comments(node)
317
+ @block.call(node)
310
318
  end
311
319
  end
312
320
 
@@ -341,16 +349,38 @@ module Psych
341
349
  def initialize(ss, class_loader, symbolize_names: false, freeze: false, comments: false)
342
350
  super(ss, class_loader, symbolize_names: symbolize_names, freeze: freeze)
343
351
  @comments = comments
352
+ @nodeless_objs = {}.compare_by_identity
353
+ @nodeless_keys = {}.compare_by_identity
344
354
  end
345
355
 
346
356
  def accept(node)
347
357
  result = super
348
358
 
349
359
  if @comments
350
- if result.is_a?(Hash)
351
- result = CommentsHash.new(result, node.comments? ? node.comments : nil)
352
- elsif node.comments?
353
- result = CommentsObject.new(result, node.comments)
360
+ case result
361
+ when LoadedObject, LoadedHash
362
+ # 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
374
+ end
375
+ end
376
+
377
+ @nodeless_objs[result] = nodeless_obj
378
+ @nodeless_keys[result] = nodeless_key
379
+ end
380
+
381
+ result = LoadedHash.new(@nodeless_objs.fetch(result), node, @nodeless_keys.fetch(result))
382
+ else
383
+ result = LoadedObject.new(result, node)
354
384
  end
355
385
  end
356
386
 
@@ -362,7 +392,7 @@ module Psych
362
392
  module ToRubySingleton
363
393
  def create(symbolize_names: false, freeze: false, strict_integer: false, comments: false)
364
394
  class_loader = ClassLoader.new
365
- scanner = ScalarScanner.new(class_loader, strict_integer: strict_integer)
395
+ scanner = ScalarScanner.new(class_loader, strict_integer: strict_integer)
366
396
  new(scanner, class_loader, symbolize_names: symbolize_names, freeze: freeze, comments: comments)
367
397
  end
368
398
  end
@@ -475,7 +505,7 @@ module Psych
475
505
  end
476
506
 
477
507
  def accept(handler)
478
- handler.event_location(*@location)
508
+ handler.event_location(*@location.trim)
479
509
  handler.scalar(
480
510
  @value,
481
511
  @anchor,
@@ -2441,7 +2471,6 @@ module Psych
2441
2471
  } then
2442
2472
  @in_scalar = false
2443
2473
  lines = events_cache_pop
2444
- lines.pop if lines.length > 0 && lines.last.empty?
2445
2474
  value = lines.map { |line| "#{line}\n" }.join
2446
2475
 
2447
2476
  case t
@@ -2450,7 +2479,7 @@ module Psych
2450
2479
  when :strip
2451
2480
  value.sub!(/\n+\z/, "")
2452
2481
  when :keep
2453
- value.sub!(/\n(\n+)\z/) { $1 } if !value.match?(/\S/)
2482
+ # nothing
2454
2483
  else
2455
2484
  raise InternalException, t.inspect
2456
2485
  end
@@ -2469,6 +2498,8 @@ module Psych
2469
2498
  # l-empty(n,block-in)*
2470
2499
  # s-indent(n) nb-char+
2471
2500
  def parse_l_nb_literal_text(n)
2501
+ events_cache_size = @events_cache[-1].size
2502
+
2472
2503
  try do
2473
2504
  if star { parse_l_empty(n, :block_in) } && parse_s_indent(n)
2474
2505
  pos_start = @scanner.pos
@@ -2477,6 +2508,12 @@ module Psych
2477
2508
  events_push(from(pos_start))
2478
2509
  true
2479
2510
  end
2511
+ else
2512
+ # When parsing all of the l_empty calls, we may have added a bunch
2513
+ # of empty lines to the events cache. We need to clear those out
2514
+ # here.
2515
+ @events_cache[-1].slice!(events_cache_size..-1)
2516
+ false
2480
2517
  end
2481
2518
  end
2482
2519
  end
@@ -2739,7 +2776,7 @@ module Psych
2739
2776
 
2740
2777
  if try { plus { try { parse_s_indent(n + m) && parse_ns_l_block_map_entry(n + m) } } }
2741
2778
  events_cache_flush
2742
- events_push_flush_properties(MappingEnd.new(Location.point(@source, @scanner.pos))) # TODO
2779
+ events_push_flush_properties(MappingEnd.new(Location.point(@source, @scanner.pos)))
2743
2780
  true
2744
2781
  else
2745
2782
  events_cache_pop
@@ -2855,7 +2892,7 @@ module Psych
2855
2892
  star { try { parse_s_indent(n) && parse_ns_l_block_map_entry(n) } }
2856
2893
  } then
2857
2894
  events_cache_flush
2858
- events_push_flush_properties(MappingEnd.new(Location.point(@source, @scanner.pos))) # TODO
2895
+ events_push_flush_properties(MappingEnd.new(Location.point(@source, @scanner.pos)))
2859
2896
  true
2860
2897
  else
2861
2898
  events_cache_pop
@@ -3042,12 +3079,12 @@ module Psych
3042
3079
  def parse_l_yaml_stream
3043
3080
  events_push_flush_properties(StreamStart.new(Location.point(@source, @scanner.pos)))
3044
3081
 
3045
- @document_start_event = DocumentStart.new(Location.point(@source, @scanner.pos))
3046
- @tag_directives = @document_start_event.tag_directives
3047
- @document_end_event = nil
3048
-
3049
3082
  if try {
3050
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
3087
+
3051
3088
  parse_l_any_document
3052
3089
  star do
3053
3090
  try do
@@ -3105,11 +3142,11 @@ module Psych
3105
3142
  # aliases, since we may find that we need to add an anchor after the
3106
3143
  # object has already been flushed.
3107
3144
  class Node
3108
- attr_reader :value, :comments
3145
+ attr_reader :value, :psych_node
3109
3146
 
3110
- def initialize(value, comments)
3147
+ def initialize(value, psych_node)
3111
3148
  @value = value
3112
- @comments = comments
3149
+ @psych_node = psych_node
3113
3150
  @anchor = nil
3114
3151
  end
3115
3152
 
@@ -3127,7 +3164,7 @@ module Psych
3127
3164
 
3128
3165
  # Represents an array of nodes.
3129
3166
  class ArrayNode < Node
3130
- attr_accessor :anchor
3167
+ attr_accessor :anchor, :tag
3131
3168
 
3132
3169
  def accept(visitor)
3133
3170
  visitor.visit_array(self)
@@ -3136,7 +3173,7 @@ module Psych
3136
3173
 
3137
3174
  # Represents a hash of nodes.
3138
3175
  class HashNode < Node
3139
- attr_accessor :anchor
3176
+ attr_accessor :anchor, :tag
3140
3177
 
3141
3178
  def accept(visitor)
3142
3179
  visitor.visit_hash(self)
@@ -3150,6 +3187,14 @@ module Psych
3150
3187
  # Represents a generic object that is not matched by any of the other node
3151
3188
  # types.
3152
3189
  class ObjectNode < Node
3190
+ # The explicit tag associated with the object.
3191
+ attr_accessor :tag
3192
+
3193
+ # Whether or not this object was modified after being loaded. In this
3194
+ # case we cannot rely on the source formatting, and need to instead
3195
+ # format the value ourselves.
3196
+ attr_accessor :dirty
3197
+
3153
3198
  def accept(visitor)
3154
3199
  visitor.visit_object(self)
3155
3200
  end
@@ -3175,6 +3220,8 @@ module Psych
3175
3220
 
3176
3221
  # Represents a string object.
3177
3222
  class StringNode < Node
3223
+ attr_accessor :tag
3224
+
3178
3225
  def accept(visitor)
3179
3226
  visitor.visit_string(self)
3180
3227
  end
@@ -3195,14 +3242,10 @@ module Psych
3195
3242
  # Visit an ArrayNode.
3196
3243
  def visit_array(node)
3197
3244
  with_comments(node) do |value|
3198
- if (anchor = node.anchor)
3199
- @q.text("&#{anchor} ")
3200
- end
3201
-
3202
- if value.empty?
3203
- @q.text("[]")
3245
+ if value.empty? || ((psych_node = node.psych_node).is_a?(Nodes::Sequence) && psych_node.style == Nodes::Sequence::FLOW)
3246
+ visit_array_contents_flow(node.anchor, node.tag, value)
3204
3247
  else
3205
- visit_array_contents(value)
3248
+ visit_array_contents_block(node.anchor, node.tag, value)
3206
3249
  end
3207
3250
  end
3208
3251
  end
@@ -3210,16 +3253,10 @@ module Psych
3210
3253
  # Visit a HashNode.
3211
3254
  def visit_hash(node)
3212
3255
  with_comments(node) do |value|
3213
- if (anchor = node.anchor)
3214
- @q.text("&#{anchor}")
3215
- end
3216
-
3217
- if value.empty?
3218
- @q.text(" ") if anchor
3219
- @q.text("{}")
3256
+ if value.empty? || ((psych_node = node.psych_node).is_a?(Nodes::Mapping) && psych_node.style == Nodes::Mapping::FLOW)
3257
+ visit_hash_contents_flow(node.anchor, node.tag, value)
3220
3258
  else
3221
- @q.breakable if anchor
3222
- visit_hash_contents(value)
3259
+ visit_hash_contents_block(node.anchor, node.tag, value)
3223
3260
  end
3224
3261
  end
3225
3262
  end
@@ -3227,50 +3264,76 @@ module Psych
3227
3264
  # Visit an ObjectNode.
3228
3265
  def visit_object(node)
3229
3266
  with_comments(node) do |value|
3230
- @q.text(Psych.dump(value, indentation: @q.indent)[/\A--- (.+)\n\z/m, 1]) # TODO
3267
+ if (tag = node.tag)
3268
+ @q.text("#{tag} ")
3269
+ end
3270
+
3271
+ if !node.dirty && (psych_node = node.psych_node)
3272
+ @q.text(psych_node.value)
3273
+ else
3274
+ @q.text(dump_object(value))
3275
+ end
3231
3276
  end
3232
3277
  end
3233
3278
 
3234
3279
  # Visit an OmapNode.
3235
3280
  def visit_omap(node)
3236
3281
  with_comments(node) do |value|
3237
- if (anchor = node.anchor)
3238
- @q.text("&#{anchor} ")
3239
- end
3240
-
3241
- @q.text("!!omap")
3242
- @q.breakable
3243
-
3244
- visit_array_contents(value)
3282
+ visit_array_contents_block(node.anchor, "!!omap", value)
3245
3283
  end
3246
3284
  end
3247
3285
 
3248
3286
  # Visit a SetNode.
3249
3287
  def visit_set(node)
3250
3288
  with_comments(node) do |value|
3251
- if (anchor = node.anchor)
3252
- @q.text("&#{anchor} ")
3253
- end
3254
-
3255
- @q.text("!set")
3256
- @q.breakable
3257
-
3258
- visit_hash_contents(node.value)
3289
+ visit_hash_contents_block(node.anchor, "!set", value)
3259
3290
  end
3260
3291
  end
3261
3292
 
3262
3293
  # Visit a StringNode.
3263
- alias visit_string visit_object
3294
+ def visit_string(node)
3295
+ with_comments(node) do |value|
3296
+ if (tag = node.tag)
3297
+ @q.text("#{tag} ")
3298
+ end
3299
+
3300
+ @q.text(dump_object(value))
3301
+ end
3302
+ end
3264
3303
 
3265
3304
  private
3266
3305
 
3306
+ # TODO: Certain objects require special formatting. Usually this
3307
+ # involves scanning the object itself and determining what kind of YAML
3308
+ # object it is, then dumping it back out. We rely on Psych itself to do
3309
+ # this formatting for us.
3310
+ #
3311
+ # Note this is the one place where we indirectly rely on libyaml,
3312
+ # because Psych delegates to libyaml to dump the object. This is less
3313
+ # than ideal, because it means in some circumstances we have an indirect
3314
+ # dependency. Ideally this would all be removed in favor of our own
3315
+ # formatting.
3316
+ def dump_object(value)
3317
+ Psych.dump(value, indentation: @q.indent)[/\A--- (.+?)(?:\n\.\.\.)?\n\z/m, 1]
3318
+ end
3319
+
3267
3320
  # Shortcut to visit a node by passing this visitor to the accept method.
3268
3321
  def visit(node)
3269
3322
  node.accept(self)
3270
3323
  end
3271
3324
 
3272
- # Visit the elements within an array.
3273
- def visit_array_contents(contents)
3325
+ # Visit the elements within an array in the block format.
3326
+ def visit_array_contents_block(anchor, tag, contents)
3327
+ if anchor
3328
+ @q.text("&#{anchor}")
3329
+ tag ? @q.text(" ") : @q.breakable
3330
+ end
3331
+
3332
+ if tag
3333
+ @q.text(tag)
3334
+ @q.breakable
3335
+ end
3336
+
3274
3337
  @q.seplist(contents, -> { @q.breakable }) do |element|
3275
3338
  @q.text("-")
3276
3339
  next if element.is_a?(NilNode)
@@ -3278,79 +3341,147 @@ module Psych
3278
3341
  @q.text(" ")
3279
3342
  @q.nest(2) { visit(element) }
3280
3343
  end
3344
+
3345
+ @q.current_group.break
3281
3346
  end
3282
3347
 
3283
- # Visit the key/value pairs within a hash.
3284
- def visit_hash_contents(contents)
3285
- @q.seplist(contents, -> { @q.breakable }) do |key, value|
3286
- inlined = false
3348
+ # Visit the elements within an array in the flow format.
3349
+ def visit_array_contents_flow(anchor, tag, contents)
3350
+ @q.group do
3351
+ @q.text("&#{anchor} ") if anchor
3352
+ @q.text("#{tag} ") if tag
3353
+ @q.text("[")
3287
3354
 
3288
- case key
3289
- when NilNode
3290
- @q.text("! ''")
3291
- when ArrayNode, HashNode, OmapNode, SetNode
3292
- if key.anchor.nil?
3293
- @q.text("? ")
3294
- @q.nest(2) { visit(key) }
3295
- @q.breakable
3296
- inlined = true
3297
- else
3298
- visit(key)
3355
+ unless contents.empty?
3356
+ @q.nest(2) do
3357
+ @q.breakable("")
3358
+ @q.seplist(contents, -> { @q.comma_breakable }) { |element| visit(element) }
3299
3359
  end
3300
- when AliasNode, ObjectNode
3360
+ @q.breakable("")
3361
+ end
3362
+
3363
+ @q.text("]")
3364
+ end
3365
+ end
3366
+
3367
+ # Visit a key value pair within a hash.
3368
+ def visit_hash_key_value(key, value)
3369
+ inlined = false
3370
+
3371
+ case key
3372
+ when NilNode
3373
+ @q.text("! ''")
3374
+ when ArrayNode, HashNode, OmapNode, SetNode
3375
+ if key.anchor.nil?
3376
+ @q.text("? ")
3377
+ @q.nest(2) { visit(key) }
3378
+ @q.breakable
3379
+ inlined = true
3380
+ else
3301
3381
  visit(key)
3302
- when StringNode
3303
- if key.value.include?("\n")
3304
- @q.text("? ")
3305
- visit(key)
3306
- @q.breakable
3307
- inlined = true
3308
- else
3309
- visit(key)
3310
- end
3311
3382
  end
3383
+ when AliasNode, ObjectNode
3384
+ visit(key)
3385
+ when StringNode
3386
+ if key.value.include?("\n")
3387
+ @q.text("? ")
3388
+ visit(key)
3389
+ @q.breakable
3390
+ inlined = true
3391
+ else
3392
+ visit(key)
3393
+ end
3394
+ else
3395
+ raise InternalException
3396
+ end
3312
3397
 
3313
- @q.text(":")
3398
+ @q.text(":")
3314
3399
 
3315
- case value
3316
- when NilNode
3317
- # skip
3318
- when OmapNode, SetNode
3400
+ case value
3401
+ when NilNode
3402
+ # skip
3403
+ when OmapNode, SetNode
3404
+ @q.text(" ")
3405
+ @q.nest(2) { visit(value) }
3406
+ when ArrayNode
3407
+ if ((psych_node = value.psych_node).is_a?(Nodes::Sequence) && psych_node.style == Nodes::Sequence::FLOW) || value.value.empty?
3408
+ @q.text(" ")
3409
+ visit(value)
3410
+ elsif inlined || value.anchor || value.tag || value.value.empty?
3319
3411
  @q.text(" ")
3320
3412
  @q.nest(2) { visit(value) }
3321
- when ArrayNode
3322
- if value.value.empty?
3323
- @q.text(" []")
3324
- elsif inlined || value.anchor
3325
- @q.text(" ")
3326
- @q.nest(2) { visit(value) }
3327
- else
3413
+ else
3414
+ @q.breakable
3415
+ visit(value)
3416
+ end
3417
+ when HashNode
3418
+ if ((psych_node = value.psych_node).is_a?(Nodes::Mapping) && psych_node.style == Nodes::Mapping::FLOW) || value.value.empty?
3419
+ @q.text(" ")
3420
+ visit(value)
3421
+ elsif inlined || value.anchor || value.tag
3422
+ @q.text(" ")
3423
+ @q.nest(2) { visit(value) }
3424
+ else
3425
+ @q.nest(2) do
3328
3426
  @q.breakable
3329
3427
  visit(value)
3330
3428
  end
3331
- when HashNode
3332
- if value.value.empty?
3333
- @q.text(" {}")
3334
- elsif inlined || value.anchor
3335
- @q.text(" ")
3336
- @q.nest(2) { visit(value) }
3337
- else
3338
- @q.nest(2) do
3339
- @q.breakable
3340
- visit(value)
3429
+ end
3430
+ when AliasNode, ObjectNode, StringNode
3431
+ @q.text(" ")
3432
+ @q.nest(2) { visit(value) }
3433
+ else
3434
+ raise InternalException
3435
+ end
3436
+ end
3437
+
3438
+ # Visit the key/value pairs within a hash in the block format.
3439
+ def visit_hash_contents_block(anchor, tag, children)
3440
+ if anchor
3441
+ @q.text("&#{anchor}")
3442
+ tag ? @q.text(" ") : @q.breakable
3443
+ end
3444
+
3445
+ if tag
3446
+ @q.text(tag)
3447
+ @q.breakable
3448
+ end
3449
+
3450
+ ((0...children.length) % 2).each do |index|
3451
+ @q.breakable if index != 0
3452
+ visit_hash_key_value(children[index], children[index + 1])
3453
+ end
3454
+
3455
+ @q.current_group.break
3456
+ end
3457
+
3458
+ # Visit the key/value pairs within a hash in the flow format.
3459
+ def visit_hash_contents_flow(anchor, tag, children)
3460
+ @q.group do
3461
+ @q.text("&#{anchor} ") if anchor
3462
+ @q.text("#{tag} ") if tag
3463
+ @q.text("{")
3464
+
3465
+ unless children.empty?
3466
+ @q.nest(2) do
3467
+ @q.breakable
3468
+
3469
+ ((0...children.length) % 2).each do |index|
3470
+ @q.comma_breakable if index != 0
3471
+ visit_hash_key_value(children[index], children[index + 1])
3341
3472
  end
3342
3473
  end
3343
- when AliasNode, ObjectNode, StringNode
3344
- @q.text(" ")
3345
- @q.nest(2) { visit(value) }
3474
+ @q.breakable
3346
3475
  end
3476
+
3477
+ @q.text("}")
3347
3478
  end
3348
3479
  end
3349
3480
 
3350
3481
  # Print out the leading and trailing comments of a node, as well as
3351
3482
  # yielding the value of the node to the block.
3352
3483
  def with_comments(node)
3353
- if (comments = node.comments) && (leading = comments.leading).any?
3484
+ if (comments = node.psych_node&.comments) && (leading = comments.leading).any?
3354
3485
  line = nil
3355
3486
 
3356
3487
  leading.each do |comment|
@@ -3435,14 +3566,42 @@ module Psych
3435
3566
  # This is the main entrypoint into this object. It is responsible for
3436
3567
  # pushing a new object onto the emitter, which is then represented as a
3437
3568
  # YAML document.
3438
- def <<(object)
3569
+ def emit(object)
3439
3570
  if @started
3440
- @io << "...\n---"
3571
+ @io << "...\n"
3441
3572
  else
3442
- @io << "---"
3443
3573
  @started = true
3444
3574
  end
3445
3575
 
3576
+ # Very rare circumstance here that there are leading comments attached
3577
+ # to the root object of a document that occur before the --- marker. In
3578
+ # this case we want to output them first here, then dump the object.
3579
+ if (object.is_a?(LoadedObject) || object.is_a?(LoadedHash)) && (psych_node = object.psych_node).comments? && (leading = psych_node.comments.leading).any?
3580
+ leading = [*leading]
3581
+ line = psych_node.start_line - 1
3582
+
3583
+ while leading.any? && leading.last.start_line == line
3584
+ leading.pop
3585
+ line -= 1
3586
+ end
3587
+
3588
+ psych_node.comments.leading.slice!(0, leading.length)
3589
+ line = nil
3590
+
3591
+ leading.each do |comment|
3592
+ if line && (line < comment.start_line)
3593
+ @io << "\n" * (comment.start_line - line - 1)
3594
+ end
3595
+
3596
+ @io << comment.value
3597
+ @io << "\n"
3598
+
3599
+ line = comment.start_line
3600
+ end
3601
+ end
3602
+
3603
+ @io << "---"
3604
+
3446
3605
  if (node = dump(object)).is_a?(NilNode)
3447
3606
  @io << "\n"
3448
3607
  else
@@ -3465,40 +3624,74 @@ module Psych
3465
3624
 
3466
3625
  private
3467
3626
 
3468
- # Walk through the given object and convert it into a tree of nodes.
3469
- def dump(base_object, comments = nil)
3470
- object = base_object
3471
-
3472
- if base_object.is_a?(CommentsObject) || base_object.is_a?(CommentsHash)
3473
- object = base_object.__getobj__
3474
- comments = base_object.psych_comments
3627
+ # Dump the tag value for a given node.
3628
+ def dump_tag(value)
3629
+ case value
3630
+ when nil, "tag:yaml.org,2002:binary"
3631
+ nil
3632
+ when /\Atag:yaml.org,2002:(.+)\z/
3633
+ "!!#{$1}"
3634
+ else
3635
+ value
3475
3636
  end
3637
+ end
3476
3638
 
3477
- if object.nil?
3478
- NilNode.new(object, comments)
3479
- elsif @object_nodes.key?(object)
3480
- AliasNode.new(@object_nodes[object].anchor = (@object_anchors[object] ||= (@object_anchor += 1)), comments)
3639
+ # Walk through the given object and convert it into a tree of nodes.
3640
+ def dump(base_object)
3641
+ if base_object.nil?
3642
+ NilNode.new(nil, nil)
3481
3643
  else
3482
- case object
3483
- when Psych::Omap
3484
- @object_nodes[object] = OmapNode.new(object.map { |(key, value)| HashNode.new({ dump(key) => dump(value) }, nil) }, comments)
3485
- when Psych::Set
3486
- @object_nodes[object] = SetNode.new(object.to_h { |key, value| [dump(key), dump(value)] }, comments)
3487
- when Array
3488
- @object_nodes[object] = ArrayNode.new(object.map { |element| dump(element) }, comments)
3489
- when Hash
3490
- dumped =
3491
- if base_object.is_a?(CommentsHash)
3492
- object.to_h { |key, value| [dump(key, base_object.psych_key_comments[key]), dump(value)] }
3493
- else
3494
- object.to_h { |key, value| [dump(key), dump(value)] }
3495
- end
3644
+ object = base_object
3645
+ psych_node = nil
3646
+ dirty = false
3647
+
3648
+ if base_object.is_a?(LoadedObject)
3649
+ object = base_object.__getobj__
3650
+ psych_node = base_object.psych_node
3651
+ dirty = base_object.dirty
3652
+ elsif base_object.is_a?(LoadedHash)
3653
+ object = base_object.__getobj__
3654
+ psych_node = base_object.psych_node
3655
+ end
3496
3656
 
3497
- @object_nodes[object] = HashNode.new(dumped, comments)
3498
- when String
3499
- StringNode.new(object, comments)
3657
+ if @object_nodes.key?(object)
3658
+ AliasNode.new(@object_nodes[object].anchor = (@object_anchors[object] ||= (@object_anchor += 1)), nil)
3500
3659
  else
3501
- ObjectNode.new(object, comments)
3660
+ case object
3661
+ when Psych::Omap
3662
+ @object_nodes[object] = OmapNode.new(object.map { |(key, value)| HashNode.new([dump(key), dump(value)], nil) }, psych_node)
3663
+ when Psych::Set
3664
+ @object_nodes[object] = SetNode.new(object.flat_map { |key, value| [dump(key), dump(value)] }, psych_node)
3665
+ when Array
3666
+ dumped = ArrayNode.new(object.map { |element| dump(element) }, psych_node)
3667
+ dumped.tag = dump_tag(psych_node&.tag)
3668
+
3669
+ @object_nodes[object] = dumped
3670
+ when Hash
3671
+ contents =
3672
+ 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)] }
3675
+ else
3676
+ object.flat_map { |key, value| [dump(key), dump(value)] }
3677
+ end
3678
+
3679
+ dumped = HashNode.new(contents, psych_node)
3680
+ dumped.tag = dump_tag(psych_node&.tag)
3681
+
3682
+ @object_nodes[object] = dumped
3683
+ when String
3684
+ dumped = StringNode.new(object, psych_node)
3685
+ dumped.tag = dump_tag(psych_node&.tag)
3686
+
3687
+ dumped
3688
+ else
3689
+ dumped = ObjectNode.new(object, psych_node)
3690
+ dumped.tag = dump_tag(psych_node&.tag)
3691
+ dumped.dirty = dirty
3692
+
3693
+ dumped
3694
+ end
3502
3695
  end
3503
3696
  end
3504
3697
  end
@@ -3538,10 +3731,10 @@ module Psych
3538
3731
  private
3539
3732
 
3540
3733
  # Dump the given object, ensuring that it is a permitted object.
3541
- def dump(base_object, comments = nil)
3734
+ def dump(base_object)
3542
3735
  object = base_object
3543
3736
 
3544
- if base_object.is_a?(CommentsObject) || base_object.is_a?(CommentsHash)
3737
+ if base_object.is_a?(LoadedObject) || base_object.is_a?(LoadedHash)
3545
3738
  object = base_object.__getobj__
3546
3739
  end
3547
3740
 
@@ -3677,7 +3870,7 @@ module Psych
3677
3870
 
3678
3871
  real_io = io || StringIO.new
3679
3872
  emitter = Emitter.new(real_io, options)
3680
- emitter << o
3873
+ emitter.emit(o)
3681
3874
  io || real_io.string
3682
3875
  end
3683
3876
 
@@ -3691,7 +3884,7 @@ module Psych
3691
3884
 
3692
3885
  real_io = io || StringIO.new
3693
3886
  emitter = SafeEmitter.new(real_io, options)
3694
- emitter << o
3887
+ emitter.emit(o)
3695
3888
  io || real_io.string
3696
3889
  end
3697
3890
 
@@ -3699,7 +3892,7 @@ module Psych
3699
3892
  def self.dump_stream(*objects)
3700
3893
  real_io = io || StringIO.new
3701
3894
  emitter = Emitter.new(real_io, {})
3702
- objects.each { |object| emitter << object }
3895
+ objects.each { |object| emitter.emit(object) }
3703
3896
  io || real_io.string
3704
3897
  end
3705
3898
  end
data/psych-pure.gemspec CHANGED
@@ -40,4 +40,5 @@ Gem::Specification.new do |spec|
40
40
  spec.add_development_dependency "bundler"
41
41
  spec.add_development_dependency "minitest"
42
42
  spec.add_development_dependency "rake"
43
+ spec.add_development_dependency "simplecov"
43
44
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: psych-pure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Newton
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2025-02-13 00:00:00.000000000 Z
11
+ date: 2025-03-04 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: psych
@@ -79,6 +80,21 @@ dependencies:
79
80
  - - ">="
80
81
  - !ruby/object:Gem::Version
81
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
82
98
  email:
83
99
  - kddnewton@gmail.com
84
100
  executables: []
@@ -97,9 +113,10 @@ licenses:
97
113
  - MIT
98
114
  metadata:
99
115
  bug_tracker_uri: https://github.com/kddnewton/psych-pure/issues
100
- changelog_uri: https://github.com/kddnewton/psych-pure/blob/v0.1.1/CHANGELOG.md
116
+ changelog_uri: https://github.com/kddnewton/psych-pure/blob/v0.1.2/CHANGELOG.md
101
117
  source_code_uri: https://github.com/kddnewton/psych-pure
102
118
  rubygems_mfa_required: 'true'
119
+ post_install_message:
103
120
  rdoc_options: []
104
121
  require_paths:
105
122
  - lib
@@ -114,7 +131,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
131
  - !ruby/object:Gem::Version
115
132
  version: '0'
116
133
  requirements: []
117
- rubygems_version: 3.6.2
134
+ rubygems_version: 3.5.16
135
+ signing_key:
118
136
  specification_version: 4
119
137
  summary: A YAML parser written in Ruby
120
138
  test_files: []