pgn2 2.0.2 → 2.0.3

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: 8fdc6dcb9543c25e2e1102902d031f9d733463780b6f5d16a1a4713c6cfe16b9
4
- data.tar.gz: 051ba77cdb0e657909ef5a22c1470f229c883e2bcb70cbfd3b9dbfbb83e92b8e
3
+ metadata.gz: 26cc37d3385d1f5516a2cff916f3219198914295b2ea83b587f1a8a6ee267480
4
+ data.tar.gz: 7efabc9471ace989a11a48879fb496f3999d35dbb42b3c8c138bdc392beeb597
5
5
  SHA512:
6
- metadata.gz: 3999930da7369748415bfb222108d436a64c67bf8e91c5b0d12d9dc5ab38f64c78f1e1e56abca9454ed8e1a215df0918d152e37b1271808d8e1962088029a0e7
7
- data.tar.gz: 2dec3d79ee03350d252bd846a789b3b96091f6ec34cd788d60e267506c2837436659395472fef192f3aeec8290ef32e3f7bd235308062a1de12f1add0e110ab7
6
+ metadata.gz: 3622fadf7c7918121f2c3e6700d1bb4284d9a258d76f56a2b47d20813f7f837ad357ff45892e252da96b6c52694909241809f203b97872b5bdb3dbe5a2b33023
7
+ data.tar.gz: e3205c5033d63601f1a64e175e7e1799ab9c698e736d216e15408c4eb358851ba8bf415139bf66780dbc5bd559fde6468ca06cf81a29df056d50c6ba52f9b6b9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.3 (2026-08-20)
4
+
5
+ ### Summary
6
+
7
+ Parser fix: a move may now be followed by any number of comments, NAGs,
8
+ and variations, in any order (PGN spec §3.2.5). Previously the grammar
9
+ accepted only a single trailing comment before a variation, so common
10
+ real-world annotations like `1... Kxh7 {a} {[%cal ...]} (20... Kf8)` raised
11
+ `parse error on value "("` and whole files were skipped. No public API
12
+ changes; `MoveText#comment` and `#variations` keep their existing shapes.
13
+
14
+ ### Changed
15
+
16
+ - **`PGN::PgnParser` (grammar)** — replace the single-comment
17
+ `move_trailer` + separate `variation_list` with a `move_suffix` list of
18
+ `[:comment, c] | [:nags, a] | [:variation, v]` items folded onto the
19
+ move by the new `build_move` helper. Multiple comments on one move are
20
+ concatenated with a space (each cleaned individually); variations keep
21
+ the legacy reversed storage order so round-trip and serializer behavior
22
+ are unchanged.
23
+ - **`PGN::MoveText.clean_text`** — extracted as a class method so the
24
+ parser can clean each trailing comment before merging; the instance
25
+ method delegates to it (behavior unchanged).
26
+
27
+ ### Fixed
28
+
29
+ - Two (or more) comments before a variation no longer abort parsing.
30
+ - A comment appearing after a variation now attaches to the move it
31
+ follows instead of becoming a stray game comment.
32
+ - `PGN.parse` no longer mutates the caller's input string (it dups before
33
+ `force_encoding`), so frozen-string-literal callers no longer raise
34
+ FrozenError on the target Ruby.
35
+
36
+ ### Known issues
37
+
38
+ - A lone backslash inside a comment that is not part of `\\`, `\{`, or
39
+ `\}` (e.g. `{/\Ba3#}`) still raises an "Unmatched input" lexer error;
40
+ the comment escape grammar remains stricter than the PGN spec.
41
+
42
+ ---
43
+
3
44
  ## 2.0.2 (2026-08-18)
4
45
 
5
46
  ### Summary
data/lib/pgn/game.rb CHANGED
@@ -9,7 +9,7 @@ module PGN
9
9
  def initialize(notation, annotation = nil, comment = nil, variations = [])
10
10
  @notation = notation
11
11
  @annotation = annotation
12
- @comment = clean_text(comment)
12
+ @comment = MoveText.clean_text(comment)
13
13
  @variations = variations
14
14
  end
15
15
 
@@ -34,16 +34,22 @@ module PGN
34
34
  san.include?('0') ? san.gsub('0', 'O') : san
35
35
  end
36
36
 
37
- def clean_text(text)
37
+ # Strip the single outermost brace pair, then unescape \{ \} \\ so
38
+ # the internal representation is the raw comment body. {Serializer}
39
+ # re-escapes on output, so parse -> serialize -> parse is byte-stable.
40
+ # Exposed as a class method so the parser can clean each trailing comment
41
+ # individually before concatenating multiple comments on one move.
42
+ def self.clean_text(text)
38
43
  return unless text
39
44
 
40
- # Strip the single outermost brace pair, then unescape \{ \} \\ so
41
- # the internal representation is the raw comment body. {Serializer}
42
- # re-escapes on output, so parse -> serialize -> parse is byte-stable.
43
45
  text = text[1..-2] if text.start_with?('{') && text.end_with?('}')
44
46
  text = text.gsub(/\\([\\{}])/, '\1')
45
47
  text.gsub(/\s+/, ' ').strip
46
48
  end
49
+
50
+ def clean_text(text)
51
+ MoveText.clean_text(text)
52
+ end
47
53
  end
48
54
 
49
55
  # {PGN::Game} holds all of the information about a game. It is either
@@ -8,7 +8,7 @@ require 'racc/parser.rb'
8
8
  module PGN
9
9
  class PgnParser < Racc::Parser
10
10
 
11
- module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 96)
11
+ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 87)
12
12
 
13
13
  def parse(input)
14
14
  @lexer = PGN::Lexer.new(input)
@@ -29,6 +29,26 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 96)
29
29
 
30
30
  private
31
31
 
32
+ # Fold a move's trailing suffix (comments, NAGs, variations, in any
33
+ # order) onto a MoveText. Multiple comments are concatenated with a
34
+ # space after each is individually cleaned; NAGs are merged in order;
35
+ # variations are collected in source order and then reversed, preserving
36
+ # the legacy order the serializer and round-trip specs depend on.
37
+ def build_move(notation, suffix)
38
+ comment_parts = []
39
+ nags = nil
40
+ variations = []
41
+ suffix.each do |kind, payload|
42
+ case kind
43
+ when :comment then comment_parts << MoveText.clean_text(payload)
44
+ when :nags then nags = nags ? nags + payload : payload
45
+ when :variation then variations << payload
46
+ end
47
+ end
48
+ comment = comment_parts.empty? ? nil : comment_parts.join(' ')
49
+ MoveText.new(notation, nags, comment, variations.reverse)
50
+ end
51
+
32
52
  # Punctuation tokens are racc'd by their literal character (taken from
33
53
  # PGN::Lexer::LITERAL_BYTES, the single source of truth for those
34
54
  # characters); everything else is racc'd by the upcased lexer symbol.
@@ -66,42 +86,44 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 96)
66
86
  ##### State transition tables begin ###
67
87
 
68
88
  racc_action_table = [
69
- 2, 16, 7, 17, 23, 14, 6, 25, 11, 6,
70
- 32, 16, 12, 17, 30, 14, 18, 31, 21, 26,
71
- 21, 25, 31 ]
89
+ 2, 16, 7, 17, 21, 14, 6, 24, 21, 6,
90
+ 30, 24, 25, 16, 12, 17, 25, 14, 11, 18,
91
+ 26, 28 ]
72
92
 
73
93
  racc_action_check = [
74
- 1, 28, 2, 28, 17, 28, 4, 17, 6, 1,
75
- 28, 10, 10, 10, 24, 10, 11, 24, 15, 18,
76
- 19, 23, 29 ]
94
+ 1, 29, 2, 29, 17, 29, 4, 17, 19, 1,
95
+ 29, 19, 17, 10, 10, 10, 19, 10, 6, 11,
96
+ 18, 22 ]
77
97
 
78
98
  racc_action_pointer = [
79
- nil, 0, 2, nil, -3, nil, 0, nil, nil, nil,
80
- 8, 14, nil, nil, nil, 7, nil, 1, 9, 9,
81
- nil, nil, nil, 15, 11, nil, nil, nil, -2, 16,
82
- nil, nil, nil ]
99
+ nil, 0, 2, nil, -3, nil, 10, nil, nil, nil,
100
+ 10, 17, nil, nil, nil, nil, nil, 1, 10, 5,
101
+ nil, nil, 15, nil, nil, nil, nil, nil, nil, -2,
102
+ nil ]
83
103
 
84
104
  racc_action_default = [
85
- -1, -25, -25, -2, -8, -4, -25, 33, -3, -5,
86
- -25, -25, -7, -9, -10, -11, -13, -14, -25, -12,
87
- -22, -8, -15, -16, -17, -20, -6, -23, -25, -19,
88
- -18, -21, -24 ]
105
+ -1, -23, -23, -2, -8, -4, -23, 31, -3, -5,
106
+ -23, -23, -7, -9, -10, -11, -12, -13, -23, -14,
107
+ -15, -17, -18, -19, -20, -8, -6, -16, -21, -23,
108
+ -22 ]
89
109
 
90
110
  racc_goto_table = [
91
- 10, 24, 20, 1, 3, 5, 27, 29, 9, 4,
92
- 8, 19, 22, nil, nil, nil, nil, 28 ]
111
+ 10, 5, 1, 20, 9, 27, 3, 4, 8, 19,
112
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
113
+ nil, 29 ]
93
114
 
94
115
  racc_goto_check = [
95
- 6, 11, 12, 1, 2, 5, 12, 11, 5, 3,
96
- 4, 9, 10, nil, nil, nil, nil, 6 ]
116
+ 6, 5, 1, 10, 5, 10, 2, 3, 4, 9,
117
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
118
+ nil, 6 ]
97
119
 
98
120
  racc_goto_pointer = [
99
- nil, 3, 3, 8, 6, 4, -4, nil, nil, -4,
100
- -5, -16, -13 ]
121
+ nil, 2, 5, 6, 4, 0, -4, nil, nil, -8,
122
+ -14, nil, nil ]
101
123
 
102
124
  racc_goto_default = [
103
125
  nil, nil, nil, nil, nil, nil, nil, 13, 15, nil,
104
- nil, nil, nil ]
126
+ nil, 22, 23 ]
105
127
 
106
128
  racc_reduce_table = [
107
129
  0, 0, :racc_error,
@@ -116,23 +138,21 @@ racc_reduce_table = [
116
138
  2, 19, :_reduce_9,
117
139
  1, 20, :_reduce_10,
118
140
  1, 20, :_reduce_none,
119
- 2, 20, :_reduce_12,
120
- 1, 20, :_reduce_13,
121
- 1, 21, :_reduce_14,
122
- 2, 21, :_reduce_15,
123
- 1, 23, :_reduce_16,
141
+ 1, 20, :_reduce_12,
142
+ 1, 21, :_reduce_13,
143
+ 2, 21, :_reduce_14,
144
+ 1, 22, :_reduce_15,
145
+ 2, 22, :_reduce_16,
124
146
  1, 23, :_reduce_17,
125
- 2, 23, :_reduce_18,
126
- 2, 23, :_reduce_19,
147
+ 1, 23, :_reduce_18,
148
+ 1, 23, :_reduce_19,
127
149
  1, 24, :_reduce_20,
128
150
  2, 24, :_reduce_21,
129
- 1, 22, :_reduce_22,
130
- 2, 22, :_reduce_23,
131
- 3, 25, :_reduce_24 ]
151
+ 3, 25, :_reduce_22 ]
132
152
 
133
- racc_reduce_n = 25
153
+ racc_reduce_n = 23
134
154
 
135
- racc_shift_n = 33
155
+ racc_shift_n = 31
136
156
 
137
157
  racc_token_table = {
138
158
  false => 0,
@@ -193,8 +213,8 @@ Racc_token_to_s_table = [
193
213
  "element_sequence",
194
214
  "element",
195
215
  "san_move_annotated",
196
- "variation_list",
197
- "move_trailer",
216
+ "move_suffix",
217
+ "suffix_item",
198
218
  "annotation_list",
199
219
  "variation" ]
200
220
  Ractor.make_shareable(Racc_token_to_s_table) if defined?(Ractor)
@@ -293,101 +313,80 @@ module_eval(<<'.,.,', 'pgn_parser.y', 50)
293
313
 
294
314
  module_eval(<<'.,.,', 'pgn_parser.y', 54)
295
315
  def _reduce_12(val, _values, result)
296
- result = val[0]
297
- result.variations = val[1].reverse
298
- result
316
+ # A standalone comment (not attached to a move) becomes the game
317
+ # comment; the last such comment in the game wins.
318
+ @game_comment = val[0]
319
+ result = nil
299
320
 
300
321
  result
301
322
  end
302
323
  .,.,
303
324
 
304
- module_eval(<<'.,.,', 'pgn_parser.y', 60)
325
+ module_eval(<<'.,.,', 'pgn_parser.y', 61)
305
326
  def _reduce_13(val, _values, result)
306
- # A standalone comment (not attached to a move) becomes the game
307
- # comment; the last such comment in the game wins.
308
- @game_comment = val[0]
309
- result = nil
310
-
327
+ result = MoveText.new(val[0])
311
328
  result
312
329
  end
313
330
  .,.,
314
331
 
315
- module_eval(<<'.,.,', 'pgn_parser.y', 67)
332
+ module_eval(<<'.,.,', 'pgn_parser.y', 62)
316
333
  def _reduce_14(val, _values, result)
317
- result = MoveText.new(val[0])
334
+ result = build_move(val[0], val[1])
318
335
  result
319
336
  end
320
337
  .,.,
321
338
 
322
- module_eval(<<'.,.,', 'pgn_parser.y', 68)
339
+ module_eval(<<'.,.,', 'pgn_parser.y', 70)
323
340
  def _reduce_15(val, _values, result)
324
- result = MoveText.new(val[0], *val[1])
341
+ result = [val[0]]
325
342
  result
326
343
  end
327
344
  .,.,
328
345
 
329
- module_eval(<<'.,.,', 'pgn_parser.y', 72)
346
+ module_eval(<<'.,.,', 'pgn_parser.y', 71)
330
347
  def _reduce_16(val, _values, result)
331
- result = [nil, val[0]]
348
+ result = val[0] << val[1]
332
349
  result
333
350
  end
334
351
  .,.,
335
352
 
336
- module_eval(<<'.,.,', 'pgn_parser.y', 73)
353
+ module_eval(<<'.,.,', 'pgn_parser.y', 74)
337
354
  def _reduce_17(val, _values, result)
338
- result = [val[0], nil]
355
+ result = [:comment, val[0]]
339
356
  result
340
357
  end
341
358
  .,.,
342
359
 
343
- module_eval(<<'.,.,', 'pgn_parser.y', 74)
360
+ module_eval(<<'.,.,', 'pgn_parser.y', 75)
344
361
  def _reduce_18(val, _values, result)
345
- result = [val[0], val[1]]
362
+ result = [:nags, val[0]]
346
363
  result
347
364
  end
348
365
  .,.,
349
366
 
350
- module_eval(<<'.,.,', 'pgn_parser.y', 75)
367
+ module_eval(<<'.,.,', 'pgn_parser.y', 76)
351
368
  def _reduce_19(val, _values, result)
352
- result = [val[1], val[0]]
369
+ result = [:variation, val[0]]
353
370
  result
354
371
  end
355
372
  .,.,
356
373
 
357
- module_eval(<<'.,.,', 'pgn_parser.y', 78)
374
+ module_eval(<<'.,.,', 'pgn_parser.y', 79)
358
375
  def _reduce_20(val, _values, result)
359
376
  result = [val[0]]
360
377
  result
361
378
  end
362
379
  .,.,
363
380
 
364
- module_eval(<<'.,.,', 'pgn_parser.y', 79)
381
+ module_eval(<<'.,.,', 'pgn_parser.y', 80)
365
382
  def _reduce_21(val, _values, result)
366
383
  result = val[0] << val[1]
367
384
  result
368
385
  end
369
386
  .,.,
370
387
 
371
- module_eval(<<'.,.,', 'pgn_parser.y', 82)
388
+ module_eval(<<'.,.,', 'pgn_parser.y', 83)
372
389
  def _reduce_22(val, _values, result)
373
- result = [val[0]]
374
- result
375
- end
376
- .,.,
377
-
378
- module_eval(<<'.,.,', 'pgn_parser.y', 85)
379
- def _reduce_23(val, _values, result)
380
- # Left-recursive; the legacy variation-order reversal is applied as
381
- # a single explicit `.reverse` where the list is consumed in
382
- # {element}, instead of being implicit in recursion direction.
383
- result = val[0] << val[1]
384
-
385
- result
386
- end
387
- .,.,
388
-
389
- module_eval(<<'.,.,', 'pgn_parser.y', 92)
390
- def _reduce_24(val, _values, result)
391
390
  result = val[1]
392
391
  result
393
392
  end
data/lib/pgn/pgn_parser.y CHANGED
@@ -50,12 +50,6 @@ rule
50
50
  element:
51
51
  MOVE_NUMBER { result = nil }
52
52
  | san_move_annotated
53
- | san_move_annotated variation_list
54
- {
55
- result = val[0]
56
- result.variations = val[1].reverse
57
- result
58
- }
59
53
  | COMMENT
60
54
  {
61
55
  # A standalone comment (not attached to a move) becomes the game
@@ -66,28 +60,25 @@ rule
66
60
 
67
61
  san_move_annotated:
68
62
  SAN_MOVE { result = MoveText.new(val[0]) }
69
- | SAN_MOVE move_trailer { result = MoveText.new(val[0], *val[1]) }
70
-
71
- # [annotation_list, comment], in whichever order they followed the move.
72
- move_trailer:
73
- COMMENT { result = [nil, val[0]] }
74
- | annotation_list { result = [val[0], nil] }
75
- | annotation_list COMMENT { result = [val[0], val[1]] }
76
- | COMMENT annotation_list { result = [val[1], val[0]] }
63
+ | SAN_MOVE move_suffix { result = build_move(val[0], val[1]) }
64
+
65
+ # The trailing material after a move: any number of comments, NAGs, and
66
+ # variations, in any order (PGN spec §3.2.5). Each suffix item is tagged
67
+ # [:comment, c] | [:nags, a] | [:variation, v]; {build_move} folds them
68
+ # onto the MoveText. This generalizes the old single-comment move_trailer,
69
+ # which rejected two comments before a variation (`{a} {b} (...)`).
70
+ move_suffix:
71
+ suffix_item { result = [val[0]] }
72
+ | move_suffix suffix_item { result = val[0] << val[1] }
73
+
74
+ suffix_item:
75
+ COMMENT { result = [:comment, val[0]] }
76
+ | annotation_list { result = [:nags, val[0]] }
77
+ | variation { result = [:variation, val[0]] }
77
78
 
78
79
  annotation_list:
79
- NAG { result = [val[0]] }
80
- | annotation_list NAG { result = val[0] << val[1] }
81
-
82
- variation_list:
83
- variation { result = [val[0]] }
84
- | variation_list variation
85
- {
86
- # Left-recursive; the legacy variation-order reversal is applied as
87
- # a single explicit `.reverse` where the list is consumed in
88
- # {element}, instead of being implicit in recursion direction.
89
- result = val[0] << val[1]
90
- }
80
+ NAG { result = [val[0]] }
81
+ | annotation_list NAG { result = val[0] << val[1] }
91
82
 
92
83
  variation:
93
84
  '(' element_sequence ')' { result = val[1] }
@@ -113,6 +104,26 @@ rule
113
104
 
114
105
  private
115
106
 
107
+ # Fold a move's trailing suffix (comments, NAGs, variations, in any
108
+ # order) onto a MoveText. Multiple comments are concatenated with a
109
+ # space after each is individually cleaned; NAGs are merged in order;
110
+ # variations are collected in source order and then reversed, preserving
111
+ # the legacy order the serializer and round-trip specs depend on.
112
+ def build_move(notation, suffix)
113
+ comment_parts = []
114
+ nags = nil
115
+ variations = []
116
+ suffix.each do |kind, payload|
117
+ case kind
118
+ when :comment then comment_parts << MoveText.clean_text(payload)
119
+ when :nags then nags = nags ? nags + payload : payload
120
+ when :variation then variations << payload
121
+ end
122
+ end
123
+ comment = comment_parts.empty? ? nil : comment_parts.join(' ')
124
+ MoveText.new(notation, nags, comment, variations.reverse)
125
+ end
126
+
116
127
  # Punctuation tokens are racc'd by their literal character (taken from
117
128
  # PGN::Lexer::LITERAL_BYTES, the single source of truth for those
118
129
  # characters); everything else is racc'd by the upcased lexer symbol.
data/lib/pgn/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module PGN
2
- VERSION = '2.0.2'.freeze
2
+ VERSION = '2.0.3'.freeze
3
3
  end
data/lib/pgn.rb CHANGED
@@ -26,6 +26,10 @@ module PGN
26
26
  # @see http://www.chessclub.com/help/PGN-spec PGN Specification
27
27
  #
28
28
  def self.parse(pgn, encoding = Encoding::ISO_8859_1)
29
+ # Operate on a private copy so a frozen caller string is never mutated
30
+ # (force_encoding mutates its receiver; under frozen-string literals
31
+ # this would otherwise raise FrozenError on the target Ruby).
32
+ pgn = pgn.dup
29
33
  pgn.force_encoding(encoding) if encoding
30
34
 
31
35
  PGN::Parser.new.parse(pgn).map do |game|
@@ -176,12 +176,51 @@ RSpec.describe PGN, '.parse (racc parser)' do
176
176
  end
177
177
  end
178
178
 
179
+ describe 'move suffix (comments, NAGs, variations in any order)' do
180
+ it 'attaches two comments before a variation to the move' do
181
+ game = PGN.parse('[White "A"] 1. Bxh7+ Kxh7 {a} {b} (20... Kf8) 2. Ng5+ *').first
182
+ move = game.moves[1]
183
+ expect(move.notation).to eq('Kxh7')
184
+ expect(move.comment).to eq('a b')
185
+ expect(variations_as(move)).to eq([%w[Kf8]])
186
+ end
187
+
188
+ it 'attaches a comment after a variation to the same move' do
189
+ game = PGN.parse('[White "A"] 1. e4 (1... c5) {a} (2... f5) 2. Nf3 *').first
190
+ move = game.moves.first
191
+ expect(move.comment).to eq('a')
192
+ expect(variations_as(move).sort).to eq([%w[c5], %w[f5]].sort)
193
+ end
194
+
195
+ it 'merges an arbitrary mix of comments, NAGs, and variations' do
196
+ game = PGN.parse('[White "A"] 1. e4 {c1} $1 (1... d5) {c2} $2 *').first
197
+ move = game.moves.first
198
+ expect(move.comment).to eq('c1 c2')
199
+ expect(move.annotation).to eq(%w[$1 $2])
200
+ expect(variations_as(move)).to eq([%w[d5]])
201
+ end
202
+
203
+ it 'round-trips a multi-comment move through parse -> serialize -> parse' do
204
+ input = +"[White \"A\"]\n\n1. e4 {a} {b} (1... d5) *\n"
205
+ once = PGN.parse(input, Encoding::UTF_8).first.to_pgn
206
+ twice = PGN.parse(once, Encoding::UTF_8).first.to_pgn
207
+ expect(twice).to eq(once)
208
+ end
209
+ end
210
+
179
211
  describe 'pgn (verbatim raw text)' do
180
212
  it 'returns the verbatim text for a single game' do
181
213
  input = "[White \"A\"] 1. e4 1-0\n"
182
214
  expect(PGN.parse(input).first.pgn).to eq(input)
183
215
  end
184
216
 
217
+ it 'does not mutate a frozen input string' do
218
+ input = "[White \"A\"] 1. e4 1-0\n".freeze
219
+ expect { PGN.parse(input, Encoding::UTF_8) }.not_to raise_error
220
+ expect(input.encoding).to eq(Encoding::UTF_8)
221
+ expect(PGN.parse(input, Encoding::UTF_8).first.pgn).to eq(input)
222
+ end
223
+
185
224
  it 'partitions consecutive games contiguously' do
186
225
  input = "[White \"A\"] 1. e4 1-0\n\n[White \"B\"] 1. d4 0-1\n"
187
226
  games = PGN.parse(input)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgn2
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stacey Touset
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-08-18 00:00:00.000000000 Z
12
+ date: 2026-08-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rb_sys